merge: quick/fase2-models-sqlite — modelos Go, SQLite FTS5 y CRUD
This commit is contained in:
@@ -58,6 +58,14 @@ Tipos algebraicos: `product` (struct, todos los campos presentes) o `sum` (union
|
||||
- **Diseño del schema:** carpeta docs/
|
||||
- **registry.db:** solo indice, regenerable con `fn index`
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# Requiere CGO y tag fts5 para SQLite FTS5
|
||||
CGO_ENABLED=1 go build -tags fts5 ./...
|
||||
CGO_ENABLED=1 go test -tags fts5 ./...
|
||||
```
|
||||
|
||||
## CLI (cmd/fn)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
module fn-registry
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require github.com/mattn/go-sqlite3 v1.14.37
|
||||
|
||||
@@ -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=
|
||||
+153
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package registry
|
||||
|
||||
import "time"
|
||||
|
||||
// Kind classifies a registry entry.
|
||||
type Kind string
|
||||
|
||||
const (
|
||||
KindFunction Kind = "function"
|
||||
KindPipeline Kind = "pipeline"
|
||||
KindComponent Kind = "component"
|
||||
)
|
||||
|
||||
// Purity indicates whether a function has side effects.
|
||||
type Purity string
|
||||
|
||||
const (
|
||||
PurityPure Purity = "pure"
|
||||
PurityImpure Purity = "impure"
|
||||
)
|
||||
|
||||
// Algebraic classifies a type.
|
||||
type Algebraic string
|
||||
|
||||
const (
|
||||
AlgebraicProduct Algebraic = "product"
|
||||
AlgebraicSum Algebraic = "sum"
|
||||
)
|
||||
|
||||
// Function represents an entry in the functions table.
|
||||
// Covers kind: function, pipeline, and component.
|
||||
type Function struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Kind Kind `json:"kind"`
|
||||
Lang string `json:"lang"`
|
||||
Domain string `json:"domain"`
|
||||
Version string `json:"version"`
|
||||
Purity Purity `json:"purity"`
|
||||
Signature string `json:"signature"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
UsesFunctions []string `json:"uses_functions"`
|
||||
UsesTypes []string `json:"uses_types"`
|
||||
Returns []string `json:"returns"`
|
||||
ReturnsOptional bool `json:"returns_optional"`
|
||||
ErrorType string `json:"error_type"`
|
||||
Imports []string `json:"imports"`
|
||||
Example string `json:"example"`
|
||||
Tested bool `json:"tested"`
|
||||
Tests []string `json:"tests"`
|
||||
TestFilePath string `json:"test_file_path"`
|
||||
FilePath string `json:"file_path"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Component-only fields (kind: component)
|
||||
Props []PropDef `json:"props,omitempty"`
|
||||
Emits []string `json:"emits,omitempty"`
|
||||
HasState *bool `json:"has_state,omitempty"`
|
||||
Framework string `json:"framework,omitempty"`
|
||||
Variant []string `json:"variant,omitempty"`
|
||||
}
|
||||
|
||||
// PropDef describes a component prop.
|
||||
type PropDef struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Type represents an entry in the types table.
|
||||
type Type struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Lang string `json:"lang"`
|
||||
Domain string `json:"domain"`
|
||||
Version string `json:"version"`
|
||||
Algebraic Algebraic `json:"algebraic"`
|
||||
Definition string `json:"definition"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
UsesTypes []string `json:"uses_types"`
|
||||
FilePath string `json:"file_path"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// GenerateID builds the canonical ID: {name}_{lang}_{domain}
|
||||
func GenerateID(name, lang, domain string) string {
|
||||
return name + "_" + lang + "_" + domain
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func tempDB(t *testing.T) *DB {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "test.db")
|
||||
db, err := Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { db.Close() })
|
||||
return db
|
||||
}
|
||||
|
||||
func TestInsertAndGetFunction(t *testing.T) {
|
||||
db := tempDB(t)
|
||||
|
||||
f := &Function{
|
||||
Name: "filter_slice",
|
||||
Kind: KindFunction,
|
||||
Lang: "go",
|
||||
Domain: "core",
|
||||
Version: "1.0.0",
|
||||
Purity: PurityPure,
|
||||
Signature: "func FilterSlice[T any](xs []T, pred func(T) bool) []T",
|
||||
Description: "Filtra un slice con un predicado sin mutar el original",
|
||||
Tags: []string{"slice", "functional", "generic"},
|
||||
FilePath: "functions/core/filter_slice.go",
|
||||
}
|
||||
|
||||
if err := db.InsertFunction(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if f.ID != "filter_slice_go_core" {
|
||||
t.Fatalf("expected ID filter_slice_go_core, got %s", f.ID)
|
||||
}
|
||||
|
||||
got, err := db.GetFunction("filter_slice_go_core")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got.Name != "filter_slice" {
|
||||
t.Errorf("name: got %q, want %q", got.Name, "filter_slice")
|
||||
}
|
||||
if got.Purity != PurityPure {
|
||||
t.Errorf("purity: got %q, want %q", got.Purity, PurityPure)
|
||||
}
|
||||
if len(got.Tags) != 3 {
|
||||
t.Errorf("tags: got %d, want 3", len(got.Tags))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertAndGetType(t *testing.T) {
|
||||
db := tempDB(t)
|
||||
|
||||
typ := &Type{
|
||||
Name: "ohlcv",
|
||||
Lang: "go",
|
||||
Domain: "finance",
|
||||
Version: "1.0.0",
|
||||
Algebraic: AlgebraicProduct,
|
||||
Definition: `type OHLCV struct {
|
||||
Open, High, Low, Close, Volume float64
|
||||
}`,
|
||||
Description: "Vela de mercado con precios OHLCV",
|
||||
Tags: []string{"finance", "market", "candle"},
|
||||
FilePath: "types/finance/ohlcv.go",
|
||||
}
|
||||
|
||||
if err := db.InsertType(typ); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := db.GetType("ohlcv_go_finance")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got.Algebraic != AlgebraicProduct {
|
||||
t.Errorf("algebraic: got %q, want %q", got.Algebraic, AlgebraicProduct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchFunctionsFTS(t *testing.T) {
|
||||
db := tempDB(t)
|
||||
|
||||
fns := []*Function{
|
||||
{Name: "filter_slice", Kind: KindFunction, Lang: "go", Domain: "core", Purity: PurityPure, Description: "Filtra un slice con un predicado", Version: "1.0.0"},
|
||||
{Name: "map_slice", Kind: KindFunction, Lang: "go", Domain: "core", Purity: PurityPure, Description: "Transforma cada elemento de un slice", Version: "1.0.0"},
|
||||
{Name: "fetch_ticks", Kind: KindFunction, Lang: "go", Domain: "io", Purity: PurityImpure, Description: "Obtiene ticks de un exchange", ErrorType: "error_go_core", Version: "1.0.0"},
|
||||
}
|
||||
for _, f := range fns {
|
||||
if err := db.InsertFunction(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// FTS search
|
||||
results, err := db.SearchFunctions("slice", "", "", "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(results) != 2 {
|
||||
t.Errorf("FTS 'slice': got %d results, want 2", len(results))
|
||||
}
|
||||
|
||||
// Filter by purity
|
||||
results, err = db.SearchFunctions("", "", PurityImpure, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(results) != 1 || results[0].Name != "fetch_ticks" {
|
||||
t.Errorf("filter impure: unexpected results %v", results)
|
||||
}
|
||||
|
||||
// Filter by domain
|
||||
results, err = db.SearchFunctions("", "", "", "", "core")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(results) != 2 {
|
||||
t.Errorf("filter domain=core: got %d, want 2", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPurge(t *testing.T) {
|
||||
db := tempDB(t)
|
||||
|
||||
db.InsertFunction(&Function{Name: "test_fn", Kind: KindFunction, Lang: "go", Domain: "core", Purity: PurityPure, Description: "test", Version: "1.0.0"})
|
||||
db.InsertType(&Type{Name: "test_type", Lang: "go", Domain: "core", Algebraic: AlgebraicProduct, Description: "test", Version: "1.0.0"})
|
||||
|
||||
if err := db.Purge(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fns, _ := db.SearchFunctions("", "", "", "", "")
|
||||
if len(fns) != 0 {
|
||||
t.Errorf("after purge: got %d functions, want 0", len(fns))
|
||||
}
|
||||
|
||||
ts, _ := db.SearchTypes("", "", "")
|
||||
if len(ts) != 0 {
|
||||
t.Errorf("after purge: got %d types, want 0", len(ts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrop(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "drop.db")
|
||||
db, err := Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := db.Drop(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Error("db file should not exist after Drop")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user