diff --git a/go.mod b/go.mod index b2d5d014..e2be7856 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module fn-registry go 1.22.2 + +require github.com/mattn/go-sqlite3 v1.14.37 diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..9c79a75d --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= +github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= diff --git a/registry/db.go b/registry/db.go new file mode 100644 index 00000000..f184864f --- /dev/null +++ b/registry/db.go @@ -0,0 +1,153 @@ +package registry + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + + _ "github.com/mattn/go-sqlite3" +) + +const schemaSQL = ` +CREATE TABLE IF NOT EXISTS functions ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + kind TEXT NOT NULL CHECK(kind IN ('function','pipeline','component')), + lang TEXT NOT NULL, + domain TEXT NOT NULL, + version TEXT NOT NULL DEFAULT '1.0.0', + purity TEXT NOT NULL CHECK(purity IN ('pure','impure')), + signature TEXT NOT NULL DEFAULT '', + description TEXT NOT NULL, + tags TEXT NOT NULL DEFAULT '[]', + uses_functions TEXT NOT NULL DEFAULT '[]', + uses_types TEXT NOT NULL DEFAULT '[]', + returns TEXT NOT NULL DEFAULT '[]', + returns_optional INTEGER NOT NULL DEFAULT 0, + error_type TEXT NOT NULL DEFAULT '', + imports TEXT NOT NULL DEFAULT '[]', + example TEXT NOT NULL DEFAULT '', + tested INTEGER NOT NULL DEFAULT 0, + tests TEXT NOT NULL DEFAULT '[]', + test_file_path TEXT NOT NULL DEFAULT '', + file_path TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + -- Component fields + props TEXT NOT NULL DEFAULT '[]', + emits TEXT NOT NULL DEFAULT '[]', + has_state INTEGER, + framework TEXT NOT NULL DEFAULT '', + variant TEXT NOT NULL DEFAULT '[]' +); + +CREATE TABLE IF NOT EXISTS types ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + lang TEXT NOT NULL, + domain TEXT NOT NULL, + version TEXT NOT NULL DEFAULT '1.0.0', + algebraic TEXT NOT NULL CHECK(algebraic IN ('product','sum')), + definition TEXT NOT NULL DEFAULT '', + description TEXT NOT NULL, + tags TEXT NOT NULL DEFAULT '[]', + uses_types TEXT NOT NULL DEFAULT '[]', + file_path TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE VIRTUAL TABLE IF NOT EXISTS functions_fts USING fts5( + id, + name, + description, + tags, + signature, + domain, + content='functions', + content_rowid='rowid' +); + +CREATE VIRTUAL TABLE IF NOT EXISTS types_fts USING fts5( + id, + name, + description, + tags, + domain, + content='types', + content_rowid='rowid' +); + +-- Triggers to keep FTS in sync +CREATE TRIGGER IF NOT EXISTS functions_ai AFTER INSERT ON functions BEGIN + INSERT INTO functions_fts(rowid, id, name, description, tags, signature, domain) + VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.signature, new.domain); +END; + +CREATE TRIGGER IF NOT EXISTS functions_ad AFTER DELETE ON functions BEGIN + INSERT INTO functions_fts(functions_fts, rowid, id, name, description, tags, signature, domain) + VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.signature, old.domain); +END; + +CREATE TRIGGER IF NOT EXISTS functions_au AFTER UPDATE ON functions BEGIN + INSERT INTO functions_fts(functions_fts, rowid, id, name, description, tags, signature, domain) + VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.signature, old.domain); + INSERT INTO functions_fts(rowid, id, name, description, tags, signature, domain) + VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.signature, new.domain); +END; + +CREATE TRIGGER IF NOT EXISTS types_ai AFTER INSERT ON types BEGIN + INSERT INTO types_fts(rowid, id, name, description, tags, domain) + VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.domain); +END; + +CREATE TRIGGER IF NOT EXISTS types_ad AFTER DELETE ON types BEGIN + INSERT INTO types_fts(types_fts, rowid, id, name, description, tags, domain) + VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.domain); +END; + +CREATE TRIGGER IF NOT EXISTS types_au AFTER UPDATE ON types BEGIN + INSERT INTO types_fts(types_fts, rowid, id, name, description, tags, domain) + VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.domain); + INSERT INTO types_fts(rowid, id, name, description, tags, domain) + VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.domain); +END; +` + +// DB wraps a SQLite connection for the registry. +type DB struct { + conn *sql.DB + path string +} + +// Open opens or creates the registry database at the given path. +func Open(path string) (*DB, error) { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0o755); err != nil { + return nil, fmt.Errorf("creating db directory: %w", err) + } + + conn, err := sql.Open("sqlite3", path+"?_journal_mode=WAL&_foreign_keys=on") + if err != nil { + return nil, fmt.Errorf("opening database: %w", err) + } + + if _, err := conn.Exec(schemaSQL); err != nil { + conn.Close() + return nil, fmt.Errorf("applying schema: %w", err) + } + + return &DB{conn: conn, path: path}, nil +} + +// Close closes the database connection. +func (db *DB) Close() error { + return db.conn.Close() +} + +// Drop removes the database file. Used by `fn index` to regenerate. +func (db *DB) Drop() error { + db.Close() + return os.Remove(db.path) +}