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 }