228 lines
5.5 KiB
Go
228 lines
5.5 KiB
Go
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()
|
|
}
|