From b5a6711c64e297f0fcfa232f9f94ca24d807cd85 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Mon, 30 Mar 2026 14:24:00 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20funciones=20core=20=E2=80=94=20detect?= =?UTF-8?q?=5Fcycle,=20generate=5Fid,=20rewrite=5Frule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tres funciones puras para el dominio core: detección de ciclos en grafos dirigidos (DFS), generación de IDs determinísticos, y reescritura de reglas con pattern matching. Co-Authored-By: Claude Opus 4.6 (1M context) --- functions/core/detect_cycle.go | 68 ++++++++++++++++++++++++++++++++++ functions/core/detect_cycle.md | 38 +++++++++++++++++++ functions/core/generate_id.go | 7 ++++ functions/core/generate_id.md | 32 ++++++++++++++++ functions/core/rewrite_rule.go | 58 +++++++++++++++++++++++++++++ functions/core/rewrite_rule.md | 36 ++++++++++++++++++ 6 files changed, 239 insertions(+) create mode 100644 functions/core/detect_cycle.go create mode 100644 functions/core/detect_cycle.md create mode 100644 functions/core/generate_id.go create mode 100644 functions/core/generate_id.md create mode 100644 functions/core/rewrite_rule.go create mode 100644 functions/core/rewrite_rule.md diff --git a/functions/core/detect_cycle.go b/functions/core/detect_cycle.go new file mode 100644 index 00000000..b0a9e1bb --- /dev/null +++ b/functions/core/detect_cycle.go @@ -0,0 +1,68 @@ +package core + +import ( + "database/sql" + "fmt" +) + +// DetectCycle checks if adding a directed edge (from -> to) would create a cycle +// in a directed graph stored in a SQLite table. +// It performs BFS from toNode following edges where the filter column is non-empty. +// If it reaches fromNode, a cycle exists. +// +// Parameters: +// - conn: open *sql.DB connection +// - table: table name containing the edges (e.g. "relations") +// - fromCol: column name for edge source (e.g. "from_entity") +// - toCol: column name for edge destination (e.g. "to_entity") +// - filterCol: column name that must be non-empty for causal edges (e.g. "via"); pass "" to consider all edges +// - fromNode: source node of the proposed new edge +// - toNode: destination node of the proposed new edge +func DetectCycle(conn *sql.DB, table, fromCol, toCol, filterCol, fromNode, toNode string) error { + if fromNode == "" || toNode == "" { + return nil + } + + var query string + if filterCol != "" { + query = fmt.Sprintf("SELECT %s FROM %s WHERE %s = ? AND %s != ''", toCol, table, fromCol, filterCol) + } else { + query = fmt.Sprintf("SELECT %s FROM %s WHERE %s = ?", toCol, table, fromCol) + } + + visited := map[string]bool{} + queue := []string{toNode} + + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + + if visited[current] { + continue + } + visited[current] = true + + if current == fromNode { + return fmt.Errorf("cycle detected: adding edge %s -> %s would create a cycle", fromNode, toNode) + } + + rows, err := conn.Query(query, current) + if err != nil { + return fmt.Errorf("querying %s for cycle detection: %w", table, err) + } + + for rows.Next() { + var next string + if err := rows.Scan(&next); err != nil { + rows.Close() + return err + } + if !visited[next] { + queue = append(queue, next) + } + } + rows.Close() + } + + return nil +} diff --git a/functions/core/detect_cycle.md b/functions/core/detect_cycle.md new file mode 100644 index 00000000..318c2cc0 --- /dev/null +++ b/functions/core/detect_cycle.md @@ -0,0 +1,38 @@ +--- +name: detect_cycle +kind: function +lang: go +domain: core +version: "1.0.0" +purity: impure +signature: "func DetectCycle(conn *sql.DB, table, fromCol, toCol, filterCol, fromNode, toNode string) error" +description: "Detecta ciclos en un grafo dirigido almacenado en SQLite usando BFS antes de insertar una arista." +tags: [graph, cycle, bfs, sqlite, validation] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: ["database/sql"] +tested: false +tests: [] +test_file_path: "" +file_path: "functions/core/detect_cycle.go" +--- + +## Ejemplo + +```go +// Verificar si agregar A -> B crearia un ciclo en la tabla "relations" +err := DetectCycle(db, "relations", "from_entity", "to_entity", "via", "A", "B") +if err != nil { + // ciclo detectado +} + +// Sin filtro — considerar todas las aristas +err = DetectCycle(db, "edges", "source", "target", "", "X", "Y") +``` + +## Notas + +Usa BFS desde toNode siguiendo aristas existentes. Si alcanza fromNode, la nueva arista crearia un ciclo. El parametro filterCol permite ignorar aristas semanticas (no causales) — pasar "" para considerar todas. diff --git a/functions/core/generate_id.go b/functions/core/generate_id.go new file mode 100644 index 00000000..2fdd8714 --- /dev/null +++ b/functions/core/generate_id.go @@ -0,0 +1,7 @@ +package core + +// GenerateID builds a canonical ID from name, lang, and domain. +// Format: {name}_{lang}_{domain} +func GenerateID(name, lang, domain string) string { + return name + "_" + lang + "_" + domain +} diff --git a/functions/core/generate_id.md b/functions/core/generate_id.md new file mode 100644 index 00000000..5c37751d --- /dev/null +++ b/functions/core/generate_id.md @@ -0,0 +1,32 @@ +--- +name: generate_id +kind: function +lang: go +domain: core +version: "1.0.0" +purity: pure +signature: "func GenerateID(name, lang, domain string) string" +description: "Genera un ID canonico determinista a partir de nombre, lenguaje y dominio." +tags: [id, naming, deterministic] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "functions/core/generate_id.go" +--- + +## Ejemplo + +```go +id := GenerateID("filter_slice", "go", "core") +// id = "filter_slice_go_core" +``` + +## Notas + +Funcion pura sin dependencias. Util para cualquier sistema que necesite IDs compuestos deterministas a partir de componentes con nombre. diff --git a/functions/core/rewrite_rule.go b/functions/core/rewrite_rule.go new file mode 100644 index 00000000..aac45fd0 --- /dev/null +++ b/functions/core/rewrite_rule.go @@ -0,0 +1,58 @@ +package core + +import ( + "fmt" + "regexp" + "strings" +) + +var rewriteFieldPattern = regexp.MustCompile(`\b([a-zA-Z_][a-zA-Z0-9_]*)\b`) + +var rewriteSQLKeywords = map[string]bool{ + "AND": true, "OR": true, "NOT": true, "IS": true, "NULL": true, + "IN": true, "BETWEEN": true, "LIKE": true, "GLOB": true, + "TRUE": true, "FALSE": true, "CASE": true, "WHEN": true, + "THEN": true, "ELSE": true, "END": true, "SELECT": true, + "FROM": true, "WHERE": true, "AS": true, "CAST": true, +} + +var rewriteSQLFunctions = map[string]bool{ + "json_extract": true, "datetime": true, "now": true, + "abs": true, "avg": true, "count": true, "max": true, "min": true, + "sum": true, "total": true, "length": true, "typeof": true, + "coalesce": true, "ifnull": true, "nullif": true, + "upper": true, "lower": true, "trim": true, "replace": true, + "substr": true, "instr": true, "round": true, +} + +// RewriteRule transforms a rule expression so that bare field names become +// json_extract calls on a given JSON column. +// For example, with column "metadata": +// +// "price > 100 AND status IS NOT NULL" +// +// becomes: +// +// "json_extract(metadata, '$.price') > 100 AND json_extract(metadata, '$.status') IS NOT NULL" +// +// If the rule already contains json_extract, it is returned as-is. +// SQL keywords and common SQL functions are preserved. +func RewriteRule(rule, jsonColumn string) string { + if strings.Contains(rule, "json_extract") { + return rule + } + + return rewriteFieldPattern.ReplaceAllStringFunc(rule, func(match string) string { + upper := strings.ToUpper(match) + if rewriteSQLKeywords[upper] { + return match + } + if rewriteSQLFunctions[strings.ToLower(match)] { + return match + } + if match[0] >= '0' && match[0] <= '9' { + return match + } + return fmt.Sprintf("json_extract(%s, '$.%s')", jsonColumn, match) + }) +} diff --git a/functions/core/rewrite_rule.md b/functions/core/rewrite_rule.md new file mode 100644 index 00000000..c3eb739b --- /dev/null +++ b/functions/core/rewrite_rule.md @@ -0,0 +1,36 @@ +--- +name: rewrite_rule +kind: function +lang: go +domain: core +version: "1.0.0" +purity: pure +signature: "func RewriteRule(rule, jsonColumn string) string" +description: "Reescribe campos bare en una expresion SQL a llamadas json_extract sobre una columna JSON de SQLite." +tags: [sql, json, sqlite, rewrite, assertion] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["regexp"] +tested: false +tests: [] +test_file_path: "" +file_path: "functions/core/rewrite_rule.go" +--- + +## Ejemplo + +```go +out := RewriteRule("price > 100 AND status IS NOT NULL", "metadata") +// out = "json_extract(metadata, '$.price') > 100 AND json_extract(metadata, '$.status') IS NOT NULL" + +// Si ya tiene json_extract, no modifica nada +out = RewriteRule("json_extract(data, '$.x') > 0", "data") +// out = "json_extract(data, '$.x') > 0" +``` + +## Notas + +Funcion pura. Preserva keywords SQL y funciones SQLite conocidas. Util para construir queries dinamicas sobre columnas JSON en SQLite sin que el usuario tenga que escribir json_extract manualmente.