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 }