package main import ( "database/sql" "fmt" "regexp" "strings" "fn-registry/functions/infra" ) var namedParamRe = regexp.MustCompile(`:([a-zA-Z_][a-zA-Z0-9_]*)`) // QueryEngine executes queries with parameter resolution. type QueryEngine struct { config *DashboardConfig pool map[string]*sql.DB } func NewQueryEngine(cfg *DashboardConfig, pool map[string]*sql.DB) *QueryEngine { return &QueryEngine{config: cfg, pool: pool} } // Execute runs the query for a widget, resolving filter references and named params. func (qe *QueryEngine) Execute(widgetID string, filters map[string]any) ([]map[string]any, error) { // Find the widget and its query. widget, err := qe.findWidget(widgetID) if err != nil { return nil, err } qdef, ok := qe.config.Queries[widget.Query] if !ok { return nil, fmt.Errorf("query %q not found", widget.Query) } db, ok := qe.pool[qdef.Connection] if !ok { return nil, fmt.Errorf("connection %q not found", qdef.Connection) } // Resolve parameter values: static or $filter.xxx references. resolvedParams := qe.resolveParams(qdef.Params, filters) // Convert :name placeholders to driver-appropriate positional params. driver := qe.config.Connections[qdef.Connection].Driver query, args := convertNamedParams(qdef.SQL, resolvedParams, driver) return infra.DBQuery(db, query, args...) } func (qe *QueryEngine) findWidget(id string) (*WidgetDef, error) { for _, sec := range qe.config.Sections { for i := range sec.Widgets { if sec.Widgets[i].ID == id { return &sec.Widgets[i], nil } } } return nil, fmt.Errorf("widget %q not found", id) } // resolveParams replaces $filter.xxx references with actual filter values // and resolves relative dates. func (qe *QueryEngine) resolveParams(params map[string]string, filters map[string]any) map[string]string { resolved := make(map[string]string, len(params)) for name, ref := range params { val := ref // $filter.date_range.from → filters["date_range"]["from"] if strings.HasPrefix(ref, "$filter.") { val = resolveFilterRef(ref, filters) } // Resolve relative dates (now-7d, etc.) if resolved, ok := ResolveRelativeDate(val); ok { val = resolved } resolved[name] = val } return resolved } // resolveFilterRef extracts a value from the filters map using dot notation. // "$filter.date_range.from" → filters["date_range"] → map["from"] func resolveFilterRef(ref string, filters map[string]any) string { // Strip "$filter." prefix. path := strings.TrimPrefix(ref, "$filter.") parts := strings.SplitN(path, ".", 2) v, ok := filters[parts[0]] if !ok { return "" } // Simple value (e.g. $filter.category). if len(parts) == 1 { return fmt.Sprint(v) } // Nested value (e.g. $filter.date_range.from). if m, ok := v.(map[string]any); ok { if val, ok := m[parts[1]]; ok { return fmt.Sprint(val) } } return "" } // convertNamedParams replaces :name placeholders with positional params ($1, ?, etc.) // and builds the args slice in the correct order. func convertNamedParams(query string, params map[string]string, driver string) (string, []any) { matches := namedParamRe.FindAllStringSubmatch(query, -1) if len(matches) == 0 { return query, nil } // Deduplicate and order params as they appear. seen := make(map[string]int) var ordered []string for _, m := range matches { name := m[1] if _, ok := seen[name]; !ok { seen[name] = len(ordered) ordered = append(ordered, name) } } // Build args slice. args := make([]any, len(ordered)) for i, name := range ordered { args[i] = params[name] } // Replace :name with positional placeholder. result := namedParamRe.ReplaceAllStringFunc(query, func(match string) string { name := match[1:] // strip leading ':' idx := seen[name] return placeholder(driver, idx) }) return result, args } func placeholder(driver string, idx int) string { switch driver { case "postgres": return fmt.Sprintf("$%d", idx+1) default: return "?" } }