package kjv import ( "database/sql" "fmt" "sort" _ "modernc.org/sqlite" ) // Verse is one row of the fts_kjv table. ID is book*1_000_000 + chapter*1000 + verse. type Verse struct { ID int64 Book int Chapter int Verse int Text string } type BookInfo struct { Title string Category string OTNT string Chapters int } // Store holds the companion database plus everything read from it at startup. // The whole text is only ~4MB, so verse lookups never touch SQLite; the FTS // index is still queried for keyword search. type Store struct { db *sql.DB verses []Verse // sorted by ID verseIDs []int64 chapterList []int64 // chapter keys (ID/1000), sorted and unique bookNames []string bookName map[int]string bookByName map[string]int chapterLen map[int]int bookInfo []BookInfo abbrevs []abbrev stateOK bool // reading plans and shortcodes can be written } type abbrev struct { Name string Book int } func OpenStore(path string) (*Store, error) { dsn := "file:" + path + "?_pragma=busy_timeout(5000)" db, err := sql.Open("sqlite", dsn) if err != nil { return nil, err } s := &Store{db: db, bookName: map[int]string{}, bookByName: map[string]int{}, chapterLen: map[int]int{}} if err := s.load(); err != nil { db.Close() return nil, fmt.Errorf("%s: %w", path, err) } s.stateOK = s.initState() == nil return s, nil } func (s *Store) Close() error { return s.db.Close() } func (s *Store) load() error { rows, err := s.db.Query("SELECT b, n FROM key_english ORDER BY b") if err != nil { return err } for rows.Next() { var id int var name string if err := rows.Scan(&id, &name); err != nil { rows.Close() return err } s.bookNames = append(s.bookNames, name) s.bookName[id] = name s.bookByName[name] = id } rows.Close() if len(s.bookNames) == 0 { return fmt.Errorf("key_english is empty") } rows, err = s.db.Query("SELECT id, b, c, v, t FROM fts_kjv ORDER BY id") if err != nil { return err } for rows.Next() { var v Verse if err := rows.Scan(&v.ID, &v.Book, &v.Chapter, &v.Verse, &v.Text); err != nil { rows.Close() return err } s.verses = append(s.verses, v) } rows.Close() if len(s.verses) == 0 { return fmt.Errorf("fts_kjv is empty") } for _, v := range s.verses { s.verseIDs = append(s.verseIDs, v.ID) if key := v.ID / 1000; len(s.chapterList) == 0 || s.chapterList[len(s.chapterList)-1] != key { s.chapterList = append(s.chapterList, key) } if v.Chapter > s.chapterLen[v.Book] { s.chapterLen[v.Book] = v.Chapter } } rows, err = s.db.Query(`SELECT title_short, category, otnt, chapters FROM book_info ORDER BY "order"`) if err != nil { return err } for rows.Next() { var b BookInfo if err := rows.Scan(&b.Title, &b.Category, &b.OTNT, &b.Chapters); err != nil { rows.Close() return err } s.bookInfo = append(s.bookInfo, b) } rows.Close() rows, err = s.db.Query("SELECT a, b FROM key_abbreviations_english ORDER BY id") if err != nil { return err } for rows.Next() { var a abbrev if err := rows.Scan(&a.Name, &a.Book); err != nil { rows.Close() return err } s.abbrevs = append(s.abbrevs, a) } rows.Close() return nil } // initState creates the tables that hold user-generated data (reading plans and // shortcodes) inside the companion database. func (s *Store) initState() error { for _, stmt := range []string{ "CREATE TABLE IF NOT EXISTS reading_tracker (uid TEXT PRIMARY KEY, verse INTEGER, last_used TEXT)", "CREATE TABLE IF NOT EXISTS shortcodes (shortcode TEXT PRIMARY KEY, query TEXT UNIQUE, creation_date TEXT)", } { if _, err := s.db.Exec(stmt); err != nil { return err } } return nil } func (s *Store) BookName(id int) string { return s.bookName[id] } func (s *Store) FormatRef(verseID int64) string { return fmt.Sprintf("%s %d:%d", s.BookName(int(verseID/1_000_000)), verseID/1000%1000, verseID%1000) } // Range returns the verses with start <= id <= end (end <= 0 means just start). func (s *Store) Range(start, end int64) []Verse { i := sort.Search(len(s.verses), func(i int) bool { return s.verses[i].ID >= start }) if end <= 0 { if i < len(s.verses) && s.verses[i].ID == start { return s.verses[i : i+1] } return nil } j := sort.Search(len(s.verses), func(j int) bool { return s.verses[j].ID > end }) if i >= j { return nil } return s.verses[i:j] } // VerseIndex is the position of a verse id in the whole Bible, or -1. func (s *Store) VerseIndex(id int64) int { i := sort.Search(len(s.verseIDs), func(i int) bool { return s.verseIDs[i] >= id }) if i < len(s.verseIDs) && s.verseIDs[i] == id { return i } return -1 } // FTS runs a full text query and returns the matching verses in Bible order. func (s *Store) FTS(query string) ([]Verse, error) { rows, err := s.db.Query("SELECT id FROM fts_kjv(?)", query) if err != nil { return nil, err } defer rows.Close() var out []Verse for rows.Next() { var id int64 if err := rows.Scan(&id); err != nil { return nil, err } if v := s.Range(id, 0); len(v) == 1 { out = append(out, v[0]) } } return out, rows.Err() } type crossRef struct{ Start, End int64 } func (s *Store) CrossReferences(verseID int64) ([]crossRef, error) { rows, err := s.db.Query("SELECT sv, ev FROM cross_reference WHERE vid = ? ORDER BY r DESC LIMIT 15", verseID) if err != nil { return nil, err } defer rows.Close() var out []crossRef for rows.Next() { var c crossRef if err := rows.Scan(&c.Start, &c.End); err != nil { return nil, err } out = append(out, c) } return out, rows.Err() }