diff --git a/registry/store.go b/registry/store.go new file mode 100644 index 00000000..c7cac284 --- /dev/null +++ b/registry/store.go @@ -0,0 +1,307 @@ +package registry + +import ( + "encoding/json" + "fmt" + "strings" + "time" +) + +func marshalStrings(ss []string) string { + if ss == nil { + ss = []string{} + } + b, _ := json.Marshal(ss) + return string(b) +} + +func unmarshalStrings(s string) []string { + var out []string + json.Unmarshal([]byte(s), &out) + if out == nil { + out = []string{} + } + return out +} + +func marshalProps(ps []PropDef) string { + if ps == nil { + ps = []PropDef{} + } + b, _ := json.Marshal(ps) + return string(b) +} + +func unmarshalProps(s string) []PropDef { + var out []PropDef + json.Unmarshal([]byte(s), &out) + return out +} + +// InsertFunction inserts or replaces a function entry. +func (db *DB) InsertFunction(f *Function) error { + now := time.Now().UTC().Format(time.RFC3339) + if f.CreatedAt.IsZero() { + f.CreatedAt = time.Now().UTC() + } + f.UpdatedAt = time.Now().UTC() + + if f.ID == "" { + f.ID = GenerateID(f.Name, f.Lang, f.Domain) + } + + var hasState *int + if f.HasState != nil { + v := 0 + if *f.HasState { + v = 1 + } + hasState = &v + } + + _, err := db.conn.Exec(` + INSERT OR REPLACE INTO functions ( + id, name, kind, lang, domain, version, purity, signature, + description, tags, uses_functions, uses_types, returns, + returns_optional, error_type, imports, example, tested, + tests, test_file_path, file_path, created_at, updated_at, + props, emits, has_state, framework, variant + ) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?, ?, ?, ?, ? + )`, + f.ID, f.Name, string(f.Kind), f.Lang, f.Domain, f.Version, string(f.Purity), f.Signature, + f.Description, marshalStrings(f.Tags), marshalStrings(f.UsesFunctions), marshalStrings(f.UsesTypes), marshalStrings(f.Returns), + f.ReturnsOptional, f.ErrorType, marshalStrings(f.Imports), f.Example, f.Tested, + marshalStrings(f.Tests), f.TestFilePath, f.FilePath, f.CreatedAt.Format(time.RFC3339), now, + marshalProps(f.Props), marshalStrings(f.Emits), hasState, f.Framework, marshalStrings(f.Variant), + ) + return err +} + +// InsertType inserts or replaces a type entry. +func (db *DB) InsertType(t *Type) error { + now := time.Now().UTC().Format(time.RFC3339) + if t.CreatedAt.IsZero() { + t.CreatedAt = time.Now().UTC() + } + t.UpdatedAt = time.Now().UTC() + + if t.ID == "" { + t.ID = GenerateID(t.Name, t.Lang, t.Domain) + } + + _, err := db.conn.Exec(` + INSERT OR REPLACE INTO types ( + id, name, lang, domain, version, algebraic, + definition, description, tags, uses_types, + file_path, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + t.ID, t.Name, t.Lang, t.Domain, t.Version, string(t.Algebraic), + t.Definition, t.Description, marshalStrings(t.Tags), marshalStrings(t.UsesTypes), + t.FilePath, t.CreatedAt.Format(time.RFC3339), now, + ) + return err +} + +// SearchFunctions performs FTS search on functions with optional filters. +func (db *DB) SearchFunctions(query string, kind Kind, purity Purity, lang, domain string) ([]Function, error) { + where := []string{} + args := []any{} + + if query != "" { + where = append(where, "f.id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH ?)") + args = append(args, query) + } + if kind != "" { + where = append(where, "f.kind = ?") + args = append(args, string(kind)) + } + if purity != "" { + where = append(where, "f.purity = ?") + args = append(args, string(purity)) + } + if lang != "" { + where = append(where, "f.lang = ?") + args = append(args, lang) + } + if domain != "" { + where = append(where, "f.domain = ?") + args = append(args, domain) + } + + sql := "SELECT * FROM functions f" + if len(where) > 0 { + sql += " WHERE " + strings.Join(where, " AND ") + } + sql += " ORDER BY f.name" + + rows, err := db.conn.Query(sql, args...) + if err != nil { + return nil, fmt.Errorf("search functions: %w", err) + } + defer rows.Close() + + return scanFunctions(rows) +} + +// SearchTypes performs FTS search on types with optional filters. +func (db *DB) SearchTypes(query string, lang, domain string) ([]Type, error) { + where := []string{} + args := []any{} + + if query != "" { + where = append(where, "t.id IN (SELECT id FROM types_fts WHERE types_fts MATCH ?)") + args = append(args, query) + } + if lang != "" { + where = append(where, "t.lang = ?") + args = append(args, lang) + } + if domain != "" { + where = append(where, "t.domain = ?") + args = append(args, domain) + } + + sql := "SELECT * FROM types t" + if len(where) > 0 { + sql += " WHERE " + strings.Join(where, " AND ") + } + sql += " ORDER BY t.name" + + rows, err := db.conn.Query(sql, args...) + if err != nil { + return nil, fmt.Errorf("search types: %w", err) + } + defer rows.Close() + + return scanTypes(rows) +} + +// GetFunction returns a single function by ID. +func (db *DB) GetFunction(id string) (*Function, error) { + rows, err := db.conn.Query("SELECT * FROM functions WHERE id = ?", id) + if err != nil { + return nil, err + } + defer rows.Close() + + fns, err := scanFunctions(rows) + if err != nil { + return nil, err + } + if len(fns) == 0 { + return nil, fmt.Errorf("function %q not found", id) + } + return &fns[0], nil +} + +// GetType returns a single type by ID. +func (db *DB) GetType(id string) (*Type, error) { + rows, err := db.conn.Query("SELECT * FROM types WHERE id = ?", id) + if err != nil { + return nil, err + } + defer rows.Close() + + ts, err := scanTypes(rows) + if err != nil { + return nil, err + } + if len(ts) == 0 { + return nil, fmt.Errorf("type %q not found", id) + } + return &ts[0], nil +} + +// DeleteFunction removes a function by ID. +func (db *DB) DeleteFunction(id string) error { + _, err := db.conn.Exec("DELETE FROM functions WHERE id = ?", id) + return err +} + +// DeleteType removes a type by ID. +func (db *DB) DeleteType(id string) error { + _, err := db.conn.Exec("DELETE FROM types WHERE id = ?", id) + return err +} + +// Purge deletes all data from both tables. Used before re-indexing. +func (db *DB) Purge() error { + if _, err := db.conn.Exec("DELETE FROM functions"); err != nil { + return err + } + _, err := db.conn.Exec("DELETE FROM types") + return err +} + +func scanFunctions(rows interface{ Next() bool; Scan(...any) error }) ([]Function, error) { + var result []Function + for rows.Next() { + var f Function + var tagsJSON, usesFnJSON, usesTypJSON, returnsJSON, importsJSON, testsJSON string + var propsJSON, emitsJSON, variantJSON string + var hasState *int + var createdAt, updatedAt string + + err := rows.Scan( + &f.ID, &f.Name, &f.Kind, &f.Lang, &f.Domain, &f.Version, &f.Purity, &f.Signature, + &f.Description, &tagsJSON, &usesFnJSON, &usesTypJSON, &returnsJSON, + &f.ReturnsOptional, &f.ErrorType, &importsJSON, &f.Example, &f.Tested, + &testsJSON, &f.TestFilePath, &f.FilePath, &createdAt, &updatedAt, + &propsJSON, &emitsJSON, &hasState, &f.Framework, &variantJSON, + ) + if err != nil { + return nil, fmt.Errorf("scanning function: %w", err) + } + + f.Tags = unmarshalStrings(tagsJSON) + f.UsesFunctions = unmarshalStrings(usesFnJSON) + f.UsesTypes = unmarshalStrings(usesTypJSON) + f.Returns = unmarshalStrings(returnsJSON) + f.Imports = unmarshalStrings(importsJSON) + f.Tests = unmarshalStrings(testsJSON) + f.Props = unmarshalProps(propsJSON) + f.Emits = unmarshalStrings(emitsJSON) + f.Variant = unmarshalStrings(variantJSON) + f.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) + f.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) + + if hasState != nil { + v := *hasState == 1 + f.HasState = &v + } + + result = append(result, f) + } + return result, nil +} + +func scanTypes(rows interface{ Next() bool; Scan(...any) error }) ([]Type, error) { + var result []Type + for rows.Next() { + var t Type + var tagsJSON, usesTypJSON string + var createdAt, updatedAt string + + err := rows.Scan( + &t.ID, &t.Name, &t.Lang, &t.Domain, &t.Version, &t.Algebraic, + &t.Definition, &t.Description, &tagsJSON, &usesTypJSON, + &t.FilePath, &createdAt, &updatedAt, + ) + if err != nil { + return nil, fmt.Errorf("scanning type: %w", err) + } + + t.Tags = unmarshalStrings(tagsJSON) + t.UsesTypes = unmarshalStrings(usesTypJSON) + t.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) + t.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) + + result = append(result, t) + } + return result, nil +}