67401cb967
Paquete Go completo con modelos (Entity, Relation, RelationInput, TypeSnapshot), DB SQLite con WAL + FTS5 en entities, CRUD para las 4 tablas, validacion de integridad, deteccion de ciclos solo en relaciones causales (via != ''), y operaciones de alto nivel con snapshot automatico de tipos del registry. 9 tests, todos pasan.
141 lines
4.1 KiB
Go
141 lines
4.1 KiB
Go
package fn_operations
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
const schemaSQL = `
|
|
CREATE TABLE IF NOT EXISTS types_snapshot (
|
|
id TEXT PRIMARY KEY,
|
|
version TEXT NOT NULL DEFAULT '1.0.0',
|
|
lang TEXT NOT NULL,
|
|
algebraic TEXT NOT NULL CHECK(algebraic IN ('product','sum')),
|
|
definition TEXT NOT NULL DEFAULT '',
|
|
description TEXT NOT NULL DEFAULT '',
|
|
snapped_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS entities (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
type_ref TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','stale','corrupted','archived')),
|
|
description TEXT NOT NULL DEFAULT '',
|
|
domain TEXT NOT NULL DEFAULT '',
|
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
source TEXT NOT NULL,
|
|
metadata TEXT NOT NULL DEFAULT '{}',
|
|
notes TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS relations (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
from_entity TEXT NOT NULL DEFAULT '',
|
|
to_entity TEXT NOT NULL,
|
|
via TEXT NOT NULL DEFAULT '',
|
|
description TEXT NOT NULL DEFAULT '',
|
|
purity TEXT NOT NULL DEFAULT '' CHECK(purity IN ('','pure','impure')),
|
|
direction TEXT NOT NULL DEFAULT 'unidirectional' CHECK(direction IN ('unidirectional','bidirectional','inverse')),
|
|
weight REAL,
|
|
status TEXT NOT NULL DEFAULT 'designed' CHECK(status IN ('designed','implemented','tested','running','deprecated')),
|
|
started_at TEXT,
|
|
ended_at TEXT,
|
|
"order" INTEGER,
|
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
notes TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS relation_inputs (
|
|
id TEXT PRIMARY KEY,
|
|
relation_id TEXT NOT NULL REFERENCES relations(id) ON DELETE CASCADE,
|
|
entity_id TEXT NOT NULL REFERENCES entities(id),
|
|
role TEXT NOT NULL,
|
|
"order" INTEGER
|
|
);
|
|
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
|
id,
|
|
name,
|
|
description,
|
|
tags,
|
|
domain,
|
|
content='entities',
|
|
content_rowid='rowid'
|
|
);
|
|
|
|
-- Triggers to keep entities FTS in sync
|
|
CREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN
|
|
INSERT INTO entities_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 entities_ad AFTER DELETE ON entities BEGIN
|
|
INSERT INTO entities_fts(entities_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 entities_au AFTER UPDATE ON entities BEGIN
|
|
INSERT INTO entities_fts(entities_fts, rowid, id, name, description, tags, domain)
|
|
VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.domain);
|
|
INSERT INTO entities_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 an operations database.
|
|
type DB struct {
|
|
conn *sql.DB
|
|
path string
|
|
}
|
|
|
|
// Open opens or creates an operations 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+"?_foreign_keys=on")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening database: %w", err)
|
|
}
|
|
|
|
if _, err := conn.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
|
conn.Close()
|
|
return nil, fmt.Errorf("setting WAL mode: %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
|
|
}
|
|
|
|
// Conn returns the underlying sql.DB for transaction use.
|
|
func (db *DB) Conn() *sql.DB {
|
|
return db.conn
|
|
}
|
|
|
|
// Close closes the database connection.
|
|
func (db *DB) Close() error {
|
|
return db.conn.Close()
|
|
}
|
|
|
|
// Drop removes the database file.
|
|
func (db *DB) Drop() error {
|
|
db.Close()
|
|
return os.Remove(db.path)
|
|
}
|