package database import ( "context" "fmt" "log" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitemigration" "zombiezen.com/go/sqlite/sqlitex" ) const dbLocation = "./context.db" // TODO: put this in either a config or automatically place in the appData folder var schema = sqlitemigration.Schema{ // Each element of the Migrations slice is applied in sequence. When you // want to change the schema, add a new SQL script to this list. // // Existing databases will pick up at the same position in the Migrations // slice as they last left off. Migrations: []string{ // sqlite create a table called context_store with an id, key, and content column "CREATE TABLE IF NOT EXISTS context_store ( id INTEGER PRIMARY KEY, key_name TEXT NOT NULL, value TEXT );", // sqlite create a virtual table called context using fts5 with a key and value column "CREATE VIRTUAL TABLE context_search USING fts5 ( key_name, value );", `CREATE TRIGGER context_ai AFTER INSERT ON context_store BEGIN INSERT INTO context_search(rowid, key_name, value) VALUES (new.id, new.key_name, new.value); END;`, `CREATE TRIGGER context_ad AFTER DELETE ON context_store BEGIN DELETE FROM context_search WHERE key_name = old.key_name AND value = old.value; END;`, `CREATE TRIGGER context_au AFTER UPDATE ON context_store BEGIN UPDATE context_search SET key_name = new.key_name, value = new.value WHERE id = old.id; END;`, }, } type DB struct { db *sqlitex.Pool } func NewDB() (*DB, error) { err := runMigrations() if err != nil { return nil, fmt.Errorf("error running migrations | %w", err) } dbpool, err := sqlitex.Open(dbLocation, 0, 10) if err != nil { return nil, fmt.Errorf("error opening db | %w", err) } return &DB{ db: dbpool, }, nil } func (d *DB) Close() error { return d.db.Close() } func runMigrations() error { // Open a pool. This does not block, and will start running any migrations // asynchronously. pool := sqlitemigration.NewPool(dbLocation, schema, sqlitemigration.Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, PrepareConn: func(conn *sqlite.Conn) error { // Enable foreign keys. See https://sqlite.org/foreignkeys.html return sqlitex.ExecuteTransient(conn, "PRAGMA foreign_keys = ON;", nil) }, OnError: func(e error) { log.Println(e) }, }) defer pool.Close() // Get a connection. This blocks until the migration completes. conn, err := pool.Get(context.Background()) if err != nil { return fmt.Errorf("error getting db connection | %w", err) } defer pool.Put(conn) // Print the list of schema objects created. const listSchemaQuery = `SELECT "type", "name" FROM sqlite_master ORDER BY 1, 2;` err = sqlitex.ExecuteTransient(conn, listSchemaQuery, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { //fmt.Printf("%-5s %s\n", stmt.ColumnText(0), stmt.ColumnText(1)) return nil }, }) if err != nil { return fmt.Errorf("error executing query | %w", err) } return nil } func (d *DB) Get(ctx context.Context, search string) ([]string, error) { conn := d.db.Get(ctx) if conn == nil { return []string{}, fmt.Errorf("error getting db connection") } defer d.db.Put(conn) query := "SELECT * FROM context_search WHERE context_search MATCH $search;" stmt := conn.Prep(query) defer stmt.Finalize() stmt.SetText("$search", search) for { if hasRow, err := stmt.Step(); err != nil { return []string{}, fmt.Errorf("error getting value from db | %w", err) } else if !hasRow { break } keyName := stmt.GetText("key_name") value := stmt.GetText("value") return []string{keyName, value}, nil } return []string{}, nil } func (d *DB) Save(ctx context.Context, key, value string) error { conn := d.db.Get(ctx) if conn == nil { return fmt.Errorf("error getting db connection to save info") } defer d.db.Put(conn) query := "INSERT INTO context_store (key_name, value) VALUES ($key, $value);" stmt := conn.Prep(query) defer stmt.Finalize() stmt.SetText("$key", key) stmt.SetText("$value", value) _, err := stmt.Step() if err != nil { return fmt.Errorf("error inserting new context | %w", err) } return nil }