aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/data/db.go
diff options
context:
space:
mode:
authorJP Appel <jeanpierre.appel01@gmail.com>2025-04-27 00:49:27 -0400
committerJP Appel <jeanpierre.appel01@gmail.com>2025-04-27 00:49:27 -0400
commit34b8d8ff1f9d65c08a9156d72f08cf548183c6f4 (patch)
treea00fa0410a7bcde125a37b50b3a4956c838fa569 /pkg/data/db.go
parent42527fdb0aca0d30652bb3052b80ab75ab057572 (diff)
Large commit; many features
Diffstat (limited to 'pkg/data/db.go')
-rw-r--r--pkg/data/db.go259
1 files changed, 259 insertions, 0 deletions
diff --git a/pkg/data/db.go b/pkg/data/db.go
new file mode 100644
index 0000000..fcf56f9
--- /dev/null
+++ b/pkg/data/db.go
@@ -0,0 +1,259 @@
+package data
+
+import (
+ "context"
+ "database/sql"
+ "strings"
+
+ "github.com/jpappel/atlas/pkg/index"
+ _ "github.com/mattn/go-sqlite3"
+)
+
+type Query struct {
+ db *sql.DB
+}
+
+// Append n copies of val to query
+//
+// output is in the form
+//
+// <query> <start><(n-1)*(<val><delim)>><val><delim><stop>
+func BatchQuery[T any](query string, start string, val string, delim string, stop string, n int, baseArgs []T) (string, []any) {
+ args := make([]any, len(baseArgs))
+ for i, arg := range baseArgs {
+ args[i] = arg
+ }
+
+ b := strings.Builder{}
+ b.Grow(len(query) + 1 + len(start) + n*len(val) + ((n - 1) * len(delim)) + len(stop))
+
+ b.WriteString(query)
+ b.WriteRune(' ')
+ b.WriteString(start)
+ for range n - 1 {
+ b.WriteString(val)
+ b.WriteString(delim)
+ }
+ b.WriteString(val)
+ b.WriteString(stop)
+
+ return b.String(), args
+}
+
+func NewQuery(filename string) *Query {
+ query := &Query{NewDB(filename)}
+ return query
+}
+
+func NewDB(filename string) *sql.DB {
+ connStr := "file:" + filename + "?_fk=true&_journal=WAL"
+ db, err := sql.Open("sqlite3", connStr)
+ if err != nil {
+ panic(err)
+ }
+
+ if err := createSchema(db); err != nil {
+ panic(err)
+ }
+
+ return db
+}
+
+func NewMemDB() *sql.DB {
+ db, err := sql.Open("sqlite3", ":memory:?_fk=true")
+ if err != nil {
+ panic(err)
+ }
+
+ if err := createSchema(db); err != nil {
+ panic(err)
+ }
+
+ return db
+}
+
+func createSchema(db *sql.DB) error {
+ ctx := context.TODO()
+ tx, err := db.BeginTx(ctx, nil)
+ if err != nil {
+ return err
+ }
+ defer tx.Commit()
+
+ _, err = tx.Exec(`
+ CREATE TABLE IF NOT EXISTS Indexes(
+ root TEXT NOT NULL,
+ followSym DATE
+ )`)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ _, err = tx.Exec(`
+ CREATE TABLE IF NOT EXISTS Documents(
+ id INTEGER PRIMARY KEY,
+ path TEXT UNIQUE NOT NULL,
+ title TEXT,
+ date INT,
+ fileTime INT,
+ meta BLOB
+ )`)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ _, err = tx.Exec(`
+ CREATE TABLE IF NOT EXISTS Authors(
+ id INTEGER PRIMARY KEY,
+ name TEXT UNIQUE NOT NULL
+ )`)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ _, err = tx.Exec(`
+ CREATE TABLE IF NOT EXISTS Aliases(
+ authorId INT NOT NULL,
+ alias TEXT UNIQUE NOT NULL,
+ FOREIGN KEY (authorId) REFERENCES Authors(id)
+ )`)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ _, err = tx.Exec(`
+ CREATE TABLE IF NOT EXISTS Tags(
+ id INTEGER PRIMARY KEY,
+ name TEXT UNIQUE NOT NULL
+ )`)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ _, err = tx.Exec(`
+ CREATE TABLE IF NOT EXISTS Links(
+ referencedId INT,
+ refererId INT,
+ FOREIGN KEY (referencedId) REFERENCES Documents(id),
+ FOREIGN KEY (refererId) REFERENCES Documents(id)
+ )`)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ _, err = tx.Exec(`
+ CREATE TABLE IF NOT EXISTS DocumentAuthors(
+ docId INT NOT NULL,
+ authorId INT NOT NULL,
+ FOREIGN KEY (docId) REFERENCES Documents(id),
+ FOREIGN KEY (authorId) REFERENCES Authors(id)
+ )`)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ _, err = tx.Exec(`
+ CREATE TABLE IF NOT EXISTS DocumentTags(
+ docId INT NOT NULL,
+ tagId INT NOT NULL,
+ FOREIGN KEY (docId) REFERENCES Documents(id),
+ FOREIGN KEY (tagId) REFERENCES Tags(id),
+ UNIQUE(docId, tagId)
+ )`)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ _, err = tx.Exec("CREATE INDEX IF NOT EXISTS idx_doc_dates ON Documents (date)")
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ _, err = tx.Exec("CREATE INDEX IF NOT EXISTS idx_doc_titles ON Documents (title)")
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ _, err = tx.Exec("CREATE INDEX IF NOT EXISTS idx_aliases_alias ON Aliases(alias)")
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ _, err = tx.Exec("CREATE INDEX IF NOT EXISTS idx_aliases_authorId ON Aliases(authorId)")
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ _, err = tx.Exec("CREATE INDEX IF NOT EXISTS idx_doctags_tagid ON DocumentTags (tagId)")
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ return nil
+}
+
+func (q Query) Close() error {
+ return q.db.Close()
+}
+
+// Create an index
+func (q Query) Get(indexRoot string) (*index.Index, error) {
+ ctx := context.TODO()
+
+ docs, err := FillMany{Db: q.db}.Get(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ idx := &index.Index{
+ Root: indexRoot,
+ Documents: docs,
+ Filters: index.DefaultFilters(),
+ }
+
+ return idx, nil
+}
+
+// Write from index to database
+func (q Query) Put(idx index.Index) error {
+ ctx := context.TODO()
+
+ insertCtx, cancel := context.WithCancelCause(ctx)
+ defer cancel(nil)
+
+ p, err := NewPutMany(q.db, idx.Documents)
+ if err != nil {
+ return err
+ }
+
+ if err := p.Insert(insertCtx); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Update database with values from index
+func (q Query) Update(idx index.Index) error {
+ // TODO: implement
+ return nil
+}
+
+func (q Query) GetDocument(path string) (*index.Document, error) {
+ ctx := context.TODO()
+ f := Fill{Path: path, Db: q.db}
+ return f.Get(ctx)
+}