9ba1f86c34
Añade Execution, Assertion, AssertionResult al paquete fn_operations. Motor de evaluación de assertions con reescritura SQL automática. Bucle reactivo: ExecuteAndReact evalúa assertions y cambia status de entities (corrupted/stale) + auto-crea proposals en registry. CLI fn ops: assertion (add/list/show/delete/eval) y execution (add/list/show). Migración 002_executions_assertions.sql con FTS para assertions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
118 lines
3.6 KiB
Go
118 lines
3.6 KiB
Go
package fn_operations
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// fieldPattern matches bare field names (word chars) that are NOT already inside
|
|
// a json_extract call, a SQL keyword, a string literal, or a number.
|
|
var fieldPattern = regexp.MustCompile(`\b([a-zA-Z_][a-zA-Z0-9_]*)\b`)
|
|
|
|
// sqlKeywords that should not be rewritten to json_extract.
|
|
var sqlKeywords = 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,
|
|
}
|
|
|
|
// sqlFunctions that should not be rewritten.
|
|
var sqlFunctions = 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 into SQL that operates on entity metadata.
|
|
// Bare field names are rewritten to json_extract(metadata, '$.field').
|
|
// If the rule already uses json_extract, it is left as-is.
|
|
func rewriteRule(rule string) string {
|
|
if strings.Contains(rule, "json_extract") {
|
|
return rule
|
|
}
|
|
|
|
return fieldPattern.ReplaceAllStringFunc(rule, func(match string) string {
|
|
upper := strings.ToUpper(match)
|
|
if sqlKeywords[upper] {
|
|
return match
|
|
}
|
|
if sqlFunctions[strings.ToLower(match)] {
|
|
return match
|
|
}
|
|
// Skip numeric-looking tokens (shouldn't match the regex, but be safe)
|
|
if match[0] >= '0' && match[0] <= '9' {
|
|
return match
|
|
}
|
|
return fmt.Sprintf("json_extract(metadata, '$.%s')", match)
|
|
})
|
|
}
|
|
|
|
// EvalAssertion evaluates a single assertion against its entity's metadata.
|
|
// Returns a result with pass/fail/skip status.
|
|
func EvalAssertion(db *DB, a *Assertion, executionID string) (*AssertionResult, error) {
|
|
result := &AssertionResult{
|
|
ID: fmt.Sprintf("ar_%s_%d", a.ID, time.Now().UnixNano()),
|
|
AssertionID: a.ID,
|
|
ExecutionID: executionID,
|
|
EvaluatedAt: time.Now().UTC(),
|
|
}
|
|
|
|
rewritten := rewriteRule(a.Rule)
|
|
|
|
q := fmt.Sprintf(`
|
|
SELECT CASE WHEN (%s) THEN 'pass' ELSE 'fail' END,
|
|
metadata
|
|
FROM entities WHERE id = ?`, rewritten)
|
|
|
|
var status, metadataJSON string
|
|
err := db.conn.QueryRow(q, a.EntityID).Scan(&status, &metadataJSON)
|
|
if err != nil {
|
|
result.Status = ResultSkip
|
|
result.Message = fmt.Sprintf("evaluation error: %v", err)
|
|
return result, nil
|
|
}
|
|
|
|
if status == "pass" {
|
|
result.Status = ResultPass
|
|
} else {
|
|
result.Status = ResultFail
|
|
result.Value = unmarshalJSON(metadataJSON)
|
|
result.Message = fmt.Sprintf("rule failed: %s", a.Rule)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// EvalEntityAssertions evaluates all active assertions for an entity.
|
|
// Returns results and persists them in the database.
|
|
func EvalEntityAssertions(db *DB, entityID, executionID string) ([]AssertionResult, error) {
|
|
active := true
|
|
assertions, err := db.ListAssertions(entityID, &active)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing assertions for entity %s: %w", entityID, err)
|
|
}
|
|
|
|
var results []AssertionResult
|
|
for _, a := range assertions {
|
|
ar, err := EvalAssertion(db, &a, executionID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("evaluating assertion %s: %w", a.ID, err)
|
|
}
|
|
|
|
if err := db.InsertAssertionResult(ar); err != nil {
|
|
return nil, fmt.Errorf("storing result for assertion %s: %w", a.ID, err)
|
|
}
|
|
|
|
results = append(results, *ar)
|
|
}
|
|
|
|
return results, nil
|
|
}
|