querysql

package
v0.0.0-...-c24bff2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package querysql compiles a CEL filter, evaluated against QueryResources' result envelope, into a parameterized SQL predicate.

Canonical ProtoJSON-shaped input

QueryResources filters are a strict subset of CEL over the canonical ProtoJSON-shaped query result:

  • Message fields are exposed by their canonical JSON name only (normally lowerCamelCase, or an explicit json_name).
  • Map and google.protobuf.Struct keys are exact, case-sensitive data keys.
  • Filter input is never case-normalized and has no proto-name/JSON-name aliases.
  • Select and string-index syntax with the same raw key are equivalent (standard CEL map semantics).

Authors should copy field names from the JSON response. Use brackets when an exact map key is not a legal CEL selector.

Why not github.com/spandigital/cel2sql/v3

github.com/spandigital/cel2sql/v3 (v3.8.8 at evaluation time) is the most prominent Go CEL-to-SQL library and was evaluated hands-on rather than assumed. It does not fit. That library does target Postgres by default (it is genuinely multi-dialect -- Postgres, MySQL, SQLite, DuckDB, BigQuery, Spark -- with real JSON/JSONB and parameterized-query support), so the objection isn't "wrong database". Two concrete incompatibilities with this package's required field set surfaced from compiling representative filters through it directly:

  1. Map-keyed JSONB access nested under a dynamically-typed parent -- exactly this package's resource.labels["team"], resource.localLabels[...], and resource.conditions["Ready"].status shapes -- does not compile to a keyed lookup. `resource.labels["team"] == "platform"` compiled (across every schema declaration style tried: WithJSONVariables, an opaque WithSchemas entry, and a structured nested WithSchemas entry) to `resource->>'labels'[1] = 'platform'`: the string key is discarded and replaced with a literal array index, which is wrong SQL, not merely suboptimal SQL. The chained conditions["Ready"].status shape produced invalid SQL (`resource->'inventory'->>'conditions'[1].status`) under every tested declaration. Labels are a common field across every resource kind in this data model, so this isn't a corner case.
  2. Its schema model (schema.Schema/FieldSchema) describes one fixed, closed-world shape per compiled expression -- there is no per-row discriminator concept. This package's resource.spec.*/resource.observation.* fields are read from a JSON column whose *shape differs by resourceType*, resolved only once resourceType == "..." is known (see hasResourceTypeGuard), across a single query spanning every extension resource type. A library built around one static schema per Convert call has no hook for that.

Given both, this package instead implements the documented supported CEL subset directly over cel-go's parser/checker, behind a CELSQLCompiler interface named around the role a cel2sql-style dependency would play, so repository code does not need to change if a future library fixes these gaps.

Package split

This package owns only CEL AST lowering: boolean/logical structure, comparison, "in", and startsWith handling, literal binding (including ParseCELTimestamp for timestamp() string literals), and resourceType guard detection (compiler.go). It does not know what field paths actually mean — column names, JSON extraction, label/condition map keys, schema-backed path validation, dialect boolean/collation spelling, or timestamp/JSON SQL rendering are all the concern of whatever FieldResolver the caller supplies (see field_resolver.go for that contract and the postgres/sqlite packages' query_filter.go + query_expr_*.go for this project's backend implementations). This split exists because querysql's supported CEL subset is a QueryResources-wide contract — any storage backend would parse and validate filters the same way — while the row shape a field path resolves to is backend-specific.

Parameter placeholder style is likewise a dialect concern, owned by the caller's ParamBinder (see param_binder.go). Compiler.Params is required; there is no default binder.

Supported filter shape: see compiler.go for the supported operators (&&, ||, !, ==, !=, <, <=, >, >=, in, startsWith, timestamp) and field-path syntax (identifiers, dotted selects, and string-keyed index expressions). Timestamp response fields remain ProtoJSON strings (direct comparison uses the canonical spelling); chronological / instant comparisons use timestamp() on both sides and may wrap any string-valued path. Anything else -- unsupported operators, arithmetic, regex, endsWith/contains/matches, and exists/all/map/filter/has macros -- fails closed with domain.ErrInvalidArgument, as does any field path a configured FieldResolver doesn't recognize.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ParseCELTimestamp

func ParseCELTimestamp(s string) (time.Time, error)

ParseCELTimestamp parses a CEL timestamp() string argument. It accepts RFC 3339 / RFC 3339 Nano and rejects values outside years 0001–9999. The compiler binds successful parses as time.Time literals; backends decide how to render those literals in SQL.

Types

type CELSQLCompiler

type CELSQLCompiler interface {
	CompileFilter(ctx context.Context, in CompileFilterInput) (SQLPredicate, error)
}

CELSQLCompiler compiles a CEL filter into a parameterized SQL predicate. Exists as an interface -- rather than a bare function -- so repository code depends on this role rather than a concrete cel-go wiring.

type ComparisonOperator

type ComparisonOperator int

ComparisonOperator is the named comparison the compiler asks a SQLExpr.Compare hook to handle. Using a named type (rather than raw SQL operator strings) keeps the FleetShift field resolver from depending on the compiler's SQL spelling of each operator.

const (
	OpEqual ComparisonOperator = iota
	OpNotEqual
	OpLess
	OpLessEqual
	OpGreater
	OpGreaterEqual
)

type CompileFilterInput

type CompileFilterInput struct {
	Filter string
}

CompileFilterInput is CELSQLCompiler.CompileFilter's input. Filter is the raw CEL expression from domain.QueryResourcesRequest.Filter. Compile treats an empty Filter as "match everything"; callers may also short-circuit empty filters themselves to skip compilation entirely.

type Compiler

type Compiler struct {
	// Fields resolves the field paths a filter references (envelope
	// columns, resource.*, ...) to SQL
	// expressions. A nil Fields is only valid for filters that
	// reference no fields at all (e.g. the empty filter, or a filter
	// built entirely from macros/literals -- both already rejected
	// for other reasons); any real filter compiled against a nil
	// Fields fails with a descriptive error rather than a nil-pointer
	// panic.
	Fields FieldResolver

	// Params formats bind-parameter placeholders in the generated
	// SQL. Required: a nil Params fails CompileFilter with a
	// descriptive error. Each storage backend supplies its own
	// ParamBinder (Postgres $N, SQLite ?N).
	Params ParamBinder
}

Compiler is the only CELSQLCompiler implementation. It is safe for concurrent use as long as Fields and Params are (or are nil/immutable). CompileFilter shares a package-level *cel.Env (see filterCELEnv) whose declarations never change, so concurrent Compile calls are safe once that env has been initialized.

func (Compiler) CompileFilter

func (c Compiler) CompileFilter(ctx context.Context, in CompileFilterInput) (SQLPredicate, error)

CompileFilter implements CELSQLCompiler.

type FieldPath

type FieldPath struct {
	Segments []string
}

FieldPath is a CEL field-select/index chain flattened to its segment names, outermost-first -- e.g. resource.labels["team"] and resource.labels.team both become ["resource", "labels", "team"]; a bare envelope identifier like name becomes ["name"]. Select and string-index syntax with the same raw key are equivalent; segment kind is not retained. See fieldPathFromExpr for how a CEL AST expression becomes a FieldPath.

func (FieldPath) String

func (p FieldPath) String() string

String returns the dot-joined path, for error messages.

type FieldResolver

type FieldResolver interface {
	Resolve(path FieldPath, hint TypeHint, ctx ResolveContext) (SQLExpr, error)
}

FieldResolver maps a CEL field path to a SQL expression. This package's compiler owns CEL AST lowering -- boolean/comparison structure, literals, in, startsWith, parameter binding, resourceType guard detection -- and knows about field paths only generically; a FieldResolver owns the actual row shape a path reads from (column names, JSON extraction, schema-backed path validation). The postgres and sqlite packages each supply a QueryResources FieldResolver for their row shapes.

type ParamBinder

type ParamBinder interface {
	// Placeholder returns the SQL text for the 1-based parameter
	// index n. n is always >= 1.
	Placeholder(n int) string
}

ParamBinder formats SQL bind-parameter placeholders. The compiler accumulates argument values in order and asks the binder for the placeholder text that should appear in the generated SQL for each 1-based index. Implementations must be safe for concurrent use (they are typically pure functions of the index).

This is the dialect seam for parameter style: each storage backend supplies its own ParamBinder (Postgres $N, SQLite ?N, …). Numbered forms matter when a FieldResolver embeds one bound placeholder into an expression that is later repeated (e.g. SQLite's safe JSON casts): a bare "?" would consume a fresh positional slot on every occurrence. Field resolvers never see the binder directly — they call ResolveContext.Bind, which already applies the compiler's configured ParamBinder.

type ResolveContext

type ResolveContext struct {
	// Context is QueryResources' call context, threaded through for
	// resolvers that need it (e.g. to call a
	// [domain.QuerySchemaProvider] while validating a type-specific
	// path).
	Context context.Context

	// GuardedResourceType is the resourceType literal from the
	// filter's top-level `resourceType == "..."` conjunct (see
	// hasResourceTypeGuard's doc), or nil if the filter has none.
	// When non-nil, resolvers may optionally validate type-shaped
	// paths (resource.spec.*/resource.observation.*) against that
	// type's schema. A guard is not required to compile those paths.
	GuardedResourceType *domain.ResourceType

	// Bind registers v as a SQL bind parameter and returns the
	// placeholder text produced by the compiler's [ParamBinder].
	// FieldResolver implementations must call this for any
	// filter-supplied *value* they need in the generated SQL -- e.g.
	// a label key from resource.labels["team"] -- rather than writing
	// it into SQL text directly, so a key containing SQL
	// metacharacters can never become part of the query text itself.
	Bind func(v any) string
}

ResolveContext carries the per-compilation state a FieldResolver needs beyond the field path and type hint themselves.

type SQLExpr

type SQLExpr struct {
	SQL string

	Compare    func(op ComparisonOperator, lit any, bind func(any) string) (sql string, handled bool, err error)
	In         func(values []any, bind func(any) string) (sql string, handled bool, err error)
	StartsWith func(prefix string, bind func(any) string) (sql string, handled bool, err error)
}

SQLExpr is a field path resolved to a SQL expression.

Compare, when non-nil, lets a field mapping override the generic "SQL op <bound literal>" compilation for a specific comparison. Returning handled=false falls back to the generic path. This is how a Postgres resolver turns resource.labels["k"] == "v" into a GIN-friendly JSONB containment predicate, and how name/resourceType equality can special-case to constituent-column predicates.

In, when non-nil, likewise overrides the generic "SQL IN (...)" path -- e.g. a Postgres resolver may rewrite resourceType in ["a/T", "b/U"] to (er.service_name, er.type_name) IN (...).

StartsWith, when non-nil, overrides the generic "SQL LIKE <escaped prefix>% ESCAPE ..." path for field.startsWith("prefix"). Resolvers use this for field-specific case folding (e.g. lowercasing the prefix for stored-lowercase columns) or dialect-specific prefix predicates; handled=false keeps the generic LIKE.

type SQLPredicate

type SQLPredicate struct {
	SQL  string
	Args []any
}

SQLPredicate is a compiled filter: a boolean SQL expression plus the ordered bind parameter values its placeholders reference. SQL never contains user-supplied *values* -- every literal in the filter is bound through builder.bind, and every field path is either a static column name or run through the configured FieldResolver, which must do the same for any value it needs to inline (see ResolveContext.Bind's doc). Placeholder spelling is controlled by the compiler's ParamBinder.

type TypeHint

type TypeHint int

TypeHint tells a FieldResolver what SQL value shape the surrounding comparison expects, since CEL's own type system doesn't know a field's real SQL shape (e.g. resource.generation is a plain CEL int whether it's backed by a native integer column or JSON-extracted text). The compiler derives it from the *other* side of a comparison/in -- e.g. `resource.generation > 4` derives TypeHintNumber from the literal 4 -- so a resolver backing a field with JSON-extracted text (see the postgres field resolver's jsonTextField) knows what to cast it to.

const (
	TypeHintUnknown TypeHint = iota
	TypeHintString
	TypeHintBool
	TypeHintNumber
	// TypeHintTimestamp is used when both sides of a comparison are
	// timestamp() conversions (CEL timestamps), not ProtoJSON strings.
	TypeHintTimestamp
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL