126 lines
3.5 KiB
Go
126 lines
3.5 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"fmt"
|
|
"text/template"
|
|
|
|
"fiatjaf.com/nostr/eventstore"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
var _ eventstore.Store = (*SqliteBackend)(nil)
|
|
|
|
type SqliteBackend struct {
|
|
db *sql.DB
|
|
Path string
|
|
Prefix string
|
|
FTSAvailable bool
|
|
}
|
|
|
|
func (s *SqliteBackend) Close() {
|
|
if s.db != nil {
|
|
s.db.Close()
|
|
}
|
|
}
|
|
|
|
func (s *SqliteBackend) Init() error {
|
|
if s.Path == "" {
|
|
return fmt.Errorf("missing Path")
|
|
}
|
|
|
|
var err error
|
|
s.db, err = sql.Open("sqlite3", s.Path+"?_journal_mode=WAL&_sync=NORMAL&_cache_size=1000&_foreign_keys=true")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open database: %w", err)
|
|
}
|
|
|
|
// Create tables and indexes
|
|
if err := s.createSchema(); err != nil {
|
|
return fmt.Errorf("failed to create schema: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SqliteBackend) tmpl(t string) string {
|
|
var buf bytes.Buffer
|
|
err := template.Must(template.New("schema").Parse(t)).Execute(&buf, s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
func (s *SqliteBackend) createSchema() error {
|
|
// Create basic schema first
|
|
basicSchema := s.tmpl(`
|
|
CREATE TABLE IF NOT EXISTS {{.Prefix}}events (
|
|
id TEXT PRIMARY KEY,
|
|
created_at INTEGER NOT NULL,
|
|
kind INTEGER NOT NULL,
|
|
pubkey TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
tags TEXT NOT NULL,
|
|
sig TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_created_at ON {{.Prefix}}events(created_at);
|
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_kind ON {{.Prefix}}events(kind);
|
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_pubkey ON {{.Prefix}}events(pubkey);
|
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_kind_pubkey ON {{.Prefix}}events(kind, pubkey);
|
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_kind_pubkey_created_at ON {{.Prefix}}events(kind, pubkey, created_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS {{.Prefix}}event_tags (
|
|
event_id TEXT NOT NULL,
|
|
key TEXT NOT NULL,
|
|
value TEXT NOT NULL,
|
|
FOREIGN KEY (event_id) REFERENCES {{.Prefix}}events(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_event_tags_event_id ON {{.Prefix}}event_tags(event_id);
|
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_event_tags_key ON {{.Prefix}}event_tags(key);
|
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_event_tags_key_value ON {{.Prefix}}event_tags(key, value);
|
|
`)
|
|
|
|
if _, err := s.db.Exec(basicSchema); err != nil {
|
|
return fmt.Errorf("failed to create schema: %w", err)
|
|
}
|
|
|
|
// Try to create FTS5 schema - if it fails, continue without it
|
|
ftsSchema := `
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS {{.Prefix}}events_fts USING fts5(
|
|
content,
|
|
content='{{.Prefix}}events',
|
|
content_rowid='rowid'
|
|
);
|
|
|
|
CREATE TRIGGER IF NOT EXISTS {{.Prefix}}events_ai AFTER INSERT ON {{.Prefix}}events BEGIN
|
|
INSERT INTO {{.Prefix}}events_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS {{.Prefix}}events_ad AFTER DELETE ON {{.Prefix}}events BEGIN
|
|
INSERT INTO {{.Prefix}}events_fts({{.Prefix}}events_fts, rowid, content)
|
|
VALUES('delete', old.rowid, old.content);
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS {{.Prefix}}events_au AFTER UPDATE ON {{.Prefix}}events BEGIN
|
|
INSERT INTO {{.Prefix}}events_fts({{.Prefix}}events_fts, rowid, content)
|
|
VALUES('delete', old.rowid, old.content);
|
|
INSERT INTO {{.Prefix}}events_fts(rowid, content)
|
|
VALUES (new.rowid, new.content);
|
|
END;
|
|
`
|
|
|
|
if _, err := s.db.Exec(ftsSchema); err != nil {
|
|
// FTS5 not available, continue without full-text search
|
|
s.FTSAvailable = false
|
|
} else {
|
|
s.FTSAvailable = true
|
|
}
|
|
|
|
return nil
|
|
}
|