cel2sql

package module
v3.0.0-...-da5ff38 Latest Latest
Warning

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

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

README

cel2sql

Fork of SPANDigital/cel2sql maintained by Observe, Inc. Module path changed from github.com/spandigital/cel2sql/v3 to github.com/observeinc/cel2sql/v3.

Convert CEL (Common Expression Language) expressions to SQL for PostgreSQL, MySQL, SQLite, DuckDB, and BigQuery

Go Version PostgreSQL MySQL SQLite DuckDB BigQuery License Benchmarks

cel2sql makes it easy to build dynamic SQL queries using CEL expressions. Write type-safe, expressive filters in CEL and automatically convert them to SQL for your database of choice.

Quick Start

Installation
go get github.com/observeinc/cel2sql/v3
Basic Example
package main

import (
    "fmt"
    "github.com/google/cel-go/cel"
    "github.com/observeinc/cel2sql/v3"
    "github.com/observeinc/cel2sql/v3/pg"
)

func main() {
    // 1. Define your database table schema
    userSchema := pg.NewSchema([]pg.FieldSchema{
        {Name: "name", Type: "text"},
        {Name: "age", Type: "integer"},
        {Name: "active", Type: "boolean"},
    })

    // 2. Create CEL environment
    env, _ := cel.NewEnv(
        cel.CustomTypeProvider(pg.NewTypeProvider(map[string]pg.Schema{
            "User": userSchema,
        })),
        cel.Variable("user", cel.ObjectType("User")),
    )

    // 3. Write your filter expression in CEL
    ast, _ := env.Compile(`user.age >= 18 && user.active`)

    // 4. Convert to SQL
    sqlWhere, _ := cel2sql.Convert(ast)

    fmt.Println(sqlWhere)
    // Output: user.age >= 18 AND user.active IS TRUE

    // 5. Use in your query
    query := "SELECT * FROM users WHERE " + sqlWhere
}

Why cel2sql?

Multi-Dialect: PostgreSQL, MySQL, SQLite, DuckDB, and BigQuery from a single API ✅ Type-Safe: Catch errors at compile time, not runtime ✅ Rich Features: JSON/JSONB, arrays, regex, timestamps, and more ✅ Well-Tested: 100+ tests including integration tests with real databases ✅ Easy to Use: Simple API, comprehensive documentation ✅ Secure by Default: Built-in protections against SQL injection and ReDoS attacks ✅ Performance Tracked: Continuous benchmark monitoring to prevent regressions

Security Features

cel2sql includes comprehensive security protections:

  • 🛡️ Field Name Validation - Prevents SQL injection via field names
  • 🔒 JSON Field Escaping - Automatic quote escaping in JSON paths
  • 🚫 ReDoS Protection - Validates regex patterns to prevent catastrophic backtracking
  • 🔄 Recursion Depth Limits - Prevents stack overflow from deeply nested expressions (default: 100)
  • 📏 SQL Output Length Limits - Prevents memory exhaustion from extremely large SQL queries (default: 50,000 chars)
  • 🔢 Byte Array Length Limits - Prevents memory exhaustion from large hex-encoded byte arrays (max: 10,000 bytes)
  • ⏱️ Context Timeouts - Optional timeout protection for complex expressions

All security features are enabled by default with zero configuration required.

Advanced Options

cel2sql supports optional advanced features via functional options:

import (
    "context"
    "log/slog"
    "github.com/observeinc/cel2sql/v3"
)

// Basic conversion
sql, err := cel2sql.Convert(ast)

// With schemas for JSON/JSONB support
sql, err := cel2sql.Convert(ast,
    cel2sql.WithSchemas(schemas))

// With context for timeouts
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
sql, err := cel2sql.Convert(ast,
    cel2sql.WithContext(ctx),
    cel2sql.WithSchemas(schemas))

// With logging for observability
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
sql, err := cel2sql.Convert(ast,
    cel2sql.WithContext(ctx),
    cel2sql.WithSchemas(schemas),
    cel2sql.WithLogger(logger))

Available Options:

  • WithDialect(dialect.Dialect) - Select target SQL dialect (default: PostgreSQL)
  • WithSchemas(map[string]pg.Schema) - Provide table schemas for JSON detection
  • WithContext(context.Context) - Enable cancellation and timeouts
  • WithLogger(*slog.Logger) - Enable structured logging
  • WithMaxDepth(int) - Set custom recursion depth limit (default: 100)

Multi-Dialect Support

cel2sql supports 5 SQL dialects. PostgreSQL is the default; select other dialects with WithDialect():

import (
    "github.com/observeinc/cel2sql/v3"
    "github.com/observeinc/cel2sql/v3/dialect/mysql"
    "github.com/observeinc/cel2sql/v3/dialect/sqlite"
    "github.com/observeinc/cel2sql/v3/dialect/duckdb"
    "github.com/observeinc/cel2sql/v3/dialect/bigquery"
)

// PostgreSQL (default - no option needed)
sql, err := cel2sql.Convert(ast)

// MySQL
sql, err := cel2sql.Convert(ast, cel2sql.WithDialect(mysql.New()))

// SQLite
sql, err := cel2sql.Convert(ast, cel2sql.WithDialect(sqlite.New()))

// DuckDB
sql, err := cel2sql.Convert(ast, cel2sql.WithDialect(duckdb.New()))

// BigQuery
sql, err := cel2sql.Convert(ast, cel2sql.WithDialect(bigquery.New()))
Dialect Comparison
Feature PostgreSQL MySQL SQLite DuckDB BigQuery
String concat || CONCAT() || || ||
Regex ~ / ~* REGEXP unsupported ~ / ~* REGEXP_CONTAINS()
JSON access ->>'f' ->>'$.f' json_extract() ->>'f' JSON_VALUE()
Arrays ARRAY[...] JSON arrays JSON arrays [...] [...]
UNNEST UNNEST(x) JSON_TABLE(...) json_each(x) UNNEST(x) UNNEST(x)
Param placeholder $1, $2 ?, ? ?, ? $1, $2 @p1, @p2
Timestamp cast TIMESTAMP WITH TIME ZONE DATETIME datetime() TIMESTAMPTZ TIMESTAMP
Contains POSITION() LOCATE() INSTR() CONTAINS() STRPOS()
Index analysis BTREE, GIN, GIN+trgm BTREE, FULLTEXT BTREE ART CLUSTERING, SEARCH_INDEX
Per-Dialect Type Providers

Each dialect has its own type provider for mapping database types to CEL types. All providers support both pre-defined schemas (NewTypeProvider) and dynamic schema loading (LoadTableSchema):

import "github.com/observeinc/cel2sql/v3/pg"       // PostgreSQL (pgxpool connection string)
import "github.com/observeinc/cel2sql/v3/mysql"     // MySQL (*sql.DB)
import "github.com/observeinc/cel2sql/v3/sqlite"    // SQLite (*sql.DB)
import "github.com/observeinc/cel2sql/v3/duckdb"    // DuckDB (*sql.DB)
import "github.com/observeinc/cel2sql/v3/bigquery"  // BigQuery (*bigquery.Client)

Query Analysis and Index Recommendations

cel2sql can analyze your CEL queries and recommend database indexes to optimize performance. The AnalyzeQuery() function returns both the converted SQL and dialect-specific index recommendations.

How It Works

AnalyzeQuery() examines your CEL expression and detects patterns that would benefit from indexing, then generates dialect-appropriate DDL:

  • Comparison operations (==, >, <, >=, <=) → B-tree (PG/MySQL/SQLite), ART (DuckDB), Clustering (BigQuery)
  • JSON/JSONB path operations (->>, ?) → GIN (PG), functional index (MySQL), Search Index (BigQuery), ART (DuckDB)
  • Regex matching (matches()) → GIN with pg_trgm (PG), FULLTEXT (MySQL)
  • Array operations (comprehensions, IN clauses) → GIN (PG), ART (DuckDB)
Usage
// PostgreSQL (default dialect)
sql, recommendations, err := cel2sql.AnalyzeQuery(ast,
    cel2sql.WithSchemas(schemas))

// Or specify a dialect
sql, recommendations, err := cel2sql.AnalyzeQuery(ast,
    cel2sql.WithSchemas(schemas),
    cel2sql.WithDialect(mysql.New()))

if err != nil {
    log.Fatal(err)
}

// Use the generated SQL
rows, err := db.Query("SELECT * FROM users WHERE " + sql)

// Review and apply index recommendations
for _, rec := range recommendations {
    fmt.Printf("Column: %s\n", rec.Column)
    fmt.Printf("Type: %s\n", rec.IndexType)
    fmt.Printf("Reason: %s\n", rec.Reason)
    fmt.Printf("Execute: %s\n\n", rec.Expression)
}
Per-Dialect Index Types
Pattern PostgreSQL MySQL SQLite DuckDB BigQuery
Comparison BTREE BTREE BTREE ART CLUSTERING
JSON access GIN BTREE (functional) (skip) ART SEARCH_INDEX
Regex GIN + pg_trgm FULLTEXT (skip) (skip) (skip)
Array membership GIN (skip) (skip) ART (skip)
Comprehension GIN (skip) (skip) ART (skip)

Unsupported patterns are silently skipped (no recommendation emitted).

Example
celExpr := `person.age > 18 && person.metadata.verified == true`
ast, _ := env.Compile(celExpr)

// PostgreSQL recommendations
sql, recs, _ := cel2sql.AnalyzeQuery(ast, cel2sql.WithSchemas(schemas))
// Recommendations:
// 1. CREATE INDEX idx_person_age_btree ON table_name (person.age);
// 2. CREATE INDEX idx_person_metadata_gin ON table_name USING GIN (person.metadata);

// MySQL recommendations
sql, recs, _ = cel2sql.AnalyzeQuery(ast,
    cel2sql.WithSchemas(schemas),
    cel2sql.WithDialect(mysql.New()))
// Recommendations:
// 1. CREATE INDEX idx_person_age_btree ON table_name (person.age);
// 2. CREATE INDEX idx_person_metadata_json ON table_name ((CAST(person.metadata->>'$.path' AS CHAR(255))));

// BigQuery recommendations
sql, recs, _ = cel2sql.AnalyzeQuery(ast,
    cel2sql.WithSchemas(schemas),
    cel2sql.WithDialect(bigquery.New()))
// Recommendations:
// 1. ALTER TABLE table_name SET OPTIONS (clustering_columns=['person.age']);
// 2. CREATE SEARCH INDEX idx_person_metadata ON table_name (person.metadata);
When to Use
  • Development: Discover which indexes your queries need
  • Performance tuning: Identify missing indexes causing slow queries
  • Production monitoring: Analyze user-generated filter expressions

See examples/index_analysis/ for a complete working example with all 5 dialects.

Parameterized Queries

cel2sql supports parameterized queries (prepared statements) for improved performance, security, and monitoring.

Benefits

🚀 Performance - PostgreSQL caches query plans for parameterized queries, enabling plan reuse across executions 🔒 Security - Parameters are passed separately from SQL, providing defense-in-depth SQL injection protection 📊 Monitoring - Same query pattern appears in logs/metrics, making analysis easier

Usage
// Convert to parameterized SQL
result, err := cel2sql.ConvertParameterized(ast)
if err != nil {
    log.Fatal(err)
}

fmt.Println(result.SQL)         // "user.age > $1 AND user.name = $2"
fmt.Println(result.Parameters)  // [18 "John"]

// Execute with database/sql
rows, err := db.Query(
    "SELECT * FROM users WHERE " + result.SQL,
    result.Parameters...,
)
What Gets Parameterized?

Parameterized (values become placeholders):

  • ✅ String literals: 'John'$1
  • ✅ Numeric literals: 42, 3.14$1, $2
  • ✅ Byte literals: b"data"$1

Kept Inline (for query plan optimization):

  • TRUE, FALSE - Boolean constants
  • NULL - Null values

PostgreSQL's query planner optimizes better when it knows boolean and null values at plan time.

Example Comparison
celExpr := `user.age > 18 && user.active == true && user.name == "John"`
ast, _ := env.Compile(celExpr)

// Non-parameterized (inline values)
sql, _ := cel2sql.Convert(ast)
// SQL: user.age > 18 AND user.active IS TRUE AND user.name = 'John'

// Parameterized (placeholders + parameters)
result, _ := cel2sql.ConvertParameterized(ast)
// SQL: user.age > $1 AND user.active IS TRUE AND user.name = $2
// Parameters: [18 "John"]
// Note: TRUE is kept inline for query plan efficiency
Prepared Statements

For maximum performance with repeated queries, use prepared statements:

result, _ := cel2sql.ConvertParameterized(ast)

// Prepare once
stmt, err := db.Prepare("SELECT * FROM users WHERE " + result.SQL)
defer stmt.Close()

// Execute multiple times with different parameters
rows1, _ := stmt.Query(25)  // age > 25
rows2, _ := stmt.Query(30)  // age > 30
rows3, _ := stmt.Query(35)  // age > 35 (reuses cached plan!)

See the parameterized example for a complete working demo with PostgreSQL integration.

Common Use Cases

1. User Filters
// CEL: Simple comparison
user.age > 21 && user.country == "USA"
// SQL: user.age > 21 AND user.country = 'USA'
// CEL: String operations
user.email.startsWith("admin") || user.name.contains("John")
// SQL: user.email LIKE 'admin%' OR POSITION('John' IN user.name) > 0
3. Date Filters
// CEL: Date comparisons
user.created_at > timestamp("2024-01-01T00:00:00Z")
// SQL: user.created_at > CAST('2024-01-01T00:00:00Z' AS TIMESTAMP WITH TIME ZONE)
4. JSON/JSONB Fields
// CEL: JSON field access
user.preferences.theme == "dark"
// SQL: user.preferences->>'theme' = 'dark'
5. Array Operations
// CEL: Check if all items match
user.scores.all(s, s >= 60)
// SQL: NOT EXISTS (SELECT 1 FROM UNNEST(user.scores) AS s WHERE NOT (s >= 60))
6. Multi-Dimensional Arrays

cel2sql supports PostgreSQL multi-dimensional arrays (1D, 2D, 3D, 4D+) with automatic dimension detection:

// Define schema with multi-dimensional arrays
schema := pg.NewSchema([]pg.FieldSchema{
    {Name: "tags", Type: "text", Repeated: true, Dimensions: 1},      // 1D: text[]
    {Name: "matrix", Type: "integer", Repeated: true, Dimensions: 2},  // 2D: integer[][]
    {Name: "cube", Type: "float", Repeated: true, Dimensions: 3},      // 3D: float[][][]
})

// CEL: size() automatically uses correct dimension
ast, _ := env.Compile("size(data.matrix) > 0")
// SQL: COALESCE(ARRAY_LENGTH(data.matrix, 2), 0) > 0

// Or load dimensions automatically from database
provider, _ := pg.NewTypeProviderWithConnection(ctx, connString)
provider.LoadTableSchema(ctx, "products")  // Dimensions detected from schema

Dimension Detection:

  • Detects dimensions from PostgreSQL type strings (integer[][], _int4[])
  • Works with both bracket notation and underscore notation
  • Defaults to 1D for backward compatibility when no schema is provided

Documentation

Supported Features

Feature CEL Example PostgreSQL SQL
Comparisons age > 18 age > 18
Logic active && verified active IS TRUE AND verified IS TRUE
Strings name.startsWith("A") name LIKE 'A%'
Lists "admin" in roles 'admin' IN UNNEST(roles)
Multi-Dim Arrays size(matrix) > 0 COALESCE(ARRAY_LENGTH(matrix, 2), 0) > 0
JSON data.key == "value" data->>'key' = 'value'
Regex email.matches(r".*@test\.com") email ~ '.*@test\.com'
Dates created_at.getFullYear() == 2024 EXTRACT(YEAR FROM created_at) = 2024
Conditionals age > 30 ? "senior" : "junior" CASE WHEN age > 30 THEN 'senior' ELSE 'junior' END
Regex Matching Limitations

cel2sql automatically converts CEL's RE2 regex patterns to PostgreSQL POSIX regex. While most common patterns work, some RE2 features are not supported and will return errors:

Supported:

  • ✅ Basic patterns: .*, [a-z]+, \d{3}
  • ✅ Case-insensitive flag: (?i)pattern → Uses ~* operator
  • ✅ Character classes: \d, \w, \s (converted to POSIX)
  • ✅ Non-capturing groups: (?:...) (converted to regular groups)

Unsupported:

  • ❌ Lookahead assertions: (?=...), (?!...)
  • ❌ Lookbehind assertions: (?<=...), (?<!...)
  • ❌ Named capture groups: (?P<name>...)
  • ❌ Inline flags (except (?i)): (?m), (?s), (?-i), etc.

ReDoS Protection: cel2sql includes automatic validation to prevent Regular Expression Denial of Service attacks:

  • Pattern length limited to 500 characters
  • Nested quantifiers blocked: (a+)+
  • Quantified alternation blocked: (a|a)*
  • Capture group limit: 20 maximum
  • Nesting depth limit: 10 levels

See Regex Matching documentation for complete details, safe pattern examples, and performance tips.

Type Mapping

CEL Type PostgreSQL MySQL SQLite DuckDB BigQuery
int bigint SIGNED INTEGER BIGINT INT64
double double precision DECIMAL REAL DOUBLE FLOAT64
bool boolean UNSIGNED INTEGER BOOLEAN BOOL
string text CHAR TEXT VARCHAR STRING
bytes bytea BINARY BLOB BLOB BYTES
list ARRAY JSON array JSON array LIST ARRAY
timestamp timestamptz DATETIME datetime() TIMESTAMPTZ TIMESTAMP
duration INTERVAL INTERVAL string modifier INTERVAL INTERVAL

Dynamic Schema Loading

Load table schemas directly from your database at runtime instead of defining them manually. Each dialect provider supports introspecting table schemas from a live database connection.

PostgreSQL
import "github.com/observeinc/cel2sql/v3/pg"

// PostgreSQL accepts a connection string and manages its own connection pool
provider, _ := pg.NewTypeProviderWithConnection(ctx, "postgres://user:pass@localhost/db")
defer provider.Close()

provider.LoadTableSchema(ctx, "users")

env, _ := cel.NewEnv(
    cel.CustomTypeProvider(provider),
    cel.Variable("user", cel.ObjectType("users")),
)
MySQL
import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "github.com/observeinc/cel2sql/v3/mysql"
)

// MySQL accepts a *sql.DB — you own the connection
db, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/mydb?parseTime=true")
defer db.Close()

provider, _ := mysql.NewTypeProviderWithConnection(ctx, db)
provider.LoadTableSchema(ctx, "users")

env, _ := cel.NewEnv(
    cel.CustomTypeProvider(provider),
    cel.Variable("user", cel.ObjectType("users")),
)

sql, _ := cel2sql.Convert(ast, cel2sql.WithDialect(mysqlDialect.New()),
    cel2sql.WithSchemas(provider.GetSchemas()))
SQLite
import (
    "database/sql"
    _ "modernc.org/sqlite"
    "github.com/observeinc/cel2sql/v3/sqlite"
)

db, _ := sql.Open("sqlite", "mydb.sqlite")
defer db.Close()

provider, _ := sqlite.NewTypeProviderWithConnection(ctx, db)
provider.LoadTableSchema(ctx, "users")

env, _ := cel.NewEnv(
    cel.CustomTypeProvider(provider),
    cel.Variable("user", cel.ObjectType("users")),
)

sql, _ := cel2sql.Convert(ast, cel2sql.WithDialect(sqliteDialect.New()),
    cel2sql.WithSchemas(provider.GetSchemas()))
DuckDB
import (
    "database/sql"
    "github.com/observeinc/cel2sql/v3/duckdb"
)

// DuckDB accepts *sql.DB — works with any DuckDB driver (requires CGO)
db, _ := sql.Open("duckdb", "mydb.duckdb")
defer db.Close()

provider, _ := duckdb.NewTypeProviderWithConnection(ctx, db)
provider.LoadTableSchema(ctx, "users")

env, _ := cel.NewEnv(
    cel.CustomTypeProvider(provider),
    cel.Variable("user", cel.ObjectType("users")),
)

sql, _ := cel2sql.Convert(ast, cel2sql.WithDialect(duckdbDialect.New()),
    cel2sql.WithSchemas(provider.GetSchemas()))
BigQuery
import (
    "cloud.google.com/go/bigquery"
    bqprovider "github.com/observeinc/cel2sql/v3/bigquery"
)

// BigQuery uses the BigQuery client API (not database/sql)
client, _ := bigquery.NewClient(ctx, "my-project")
defer client.Close()

provider, _ := bqprovider.NewTypeProviderWithClient(ctx, client, "my_dataset")
provider.LoadTableSchema(ctx, "users")

env, _ := cel.NewEnv(
    cel.CustomTypeProvider(provider),
    cel.Variable("user", cel.ObjectType("users")),
)

sql, _ := cel2sql.Convert(ast, cel2sql.WithDialect(bigqueryDialect.New()),
    cel2sql.WithSchemas(provider.GetSchemas()))
Notes
  • PostgreSQL manages its own connection pool via pgxpool — call provider.Close() when done.
  • MySQL, SQLite, DuckDB accept a *sql.DB you provide — you own the connection lifecycle. Close() is a no-op.
  • BigQuery accepts a *bigquery.Client + dataset ID — you own the client lifecycle. Close() is a no-op.
  • All providers also support pre-defined schemas via NewTypeProvider(schemas) if you don't need runtime introspection.

See Getting Started Guide for more details.

Requirements

  • Go 1.24 or higher
CGO Requirement (DuckDB only)

The DuckDB dialect's LoadTableSchema requires a DuckDB Go driver (e.g., github.com/marcboeker/go-duckdb) which depends on CGO and a C/C++ compiler. This means:

  • You must have CGO_ENABLED=1 (the Go default on most platforms)
  • A C/C++ compiler must be installed (GCC, Clang, or MSVC)
  • Cross-compilation requires a C cross-compiler for the target platform

All other dialects (PostgreSQL, MySQL, SQLite, BigQuery) use pure Go drivers and do not require CGO.

If you only use DuckDB with pre-defined schemas via duckdb.NewTypeProvider() (no live database connection), CGO is not required.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

License

Apache 2.0 - See LICENSE for details.

  • CEL-Go - Common Expression Language implementation in Go
  • CEL Spec - Common Expression Language specification

Need Help?

Documentation

Overview

Package cel2sql provides query analysis and index recommendations

Package cel2sql converts CEL (Common Expression Language) expressions to SQL conditions. It supports multiple SQL dialects through the dialect interface, with PostgreSQL as the default.

Modified by Observe, Inc. (2026): Added WithJSONVariables and WithColumnAliases options. Original source: github.com/SPANDigital/cel2sql

Modified by Observe, Inc. (2026): Added isJSONVariable and flat ident check in shouldUseJSONPath for WithJSONVariables support. Original source: github.com/SPANDigital/cel2sql

Index

Constants

View Source
const (
	// IndexTypeBTree represents a B-tree index for efficient range queries and equality checks
	IndexTypeBTree = "BTREE"
	// IndexTypeGIN represents a GIN (Generalized Inverted Index) for JSON, arrays, and full-text search
	IndexTypeGIN = "GIN"
	// IndexTypeGIST represents a GIST (Generalized Search Tree) index
	IndexTypeGIST = "GIST"
)

Index type constants for recommendations (kept for backward compatibility).

Variables

View Source
var (
	// ErrUnsupportedExpression indicates an unsupported CEL expression type
	ErrUnsupportedExpression = errors.New("unsupported expression type")

	// ErrInvalidFieldName indicates an invalid or empty field name
	ErrInvalidFieldName = errors.New("invalid field name")

	// ErrInvalidSchema indicates a problem with the provided schema
	ErrInvalidSchema = errors.New("invalid schema")

	// ErrInvalidRegexPattern indicates an invalid regex pattern
	ErrInvalidRegexPattern = errors.New("invalid regex pattern")

	// ErrMaxDepthExceeded indicates recursion depth limit exceeded
	ErrMaxDepthExceeded = errors.New("maximum recursion depth exceeded")

	// ErrMaxOutputLengthExceeded indicates output length limit exceeded
	ErrMaxOutputLengthExceeded = errors.New("maximum output length exceeded")

	// ErrInvalidComprehension indicates an invalid comprehension expression
	ErrInvalidComprehension = errors.New("invalid comprehension expression")

	// ErrMaxComprehensionDepthExceeded indicates comprehension nesting depth exceeded
	ErrMaxComprehensionDepthExceeded = errors.New("maximum comprehension depth exceeded")

	// ErrInvalidArguments indicates invalid function arguments
	ErrInvalidArguments = errors.New("invalid function arguments")

	// ErrUnsupportedOperation indicates an operation that cannot be converted to SQL
	ErrUnsupportedOperation = errors.New("unsupported operation")

	// ErrInvalidTimestampOperation indicates an invalid timestamp operation
	ErrInvalidTimestampOperation = errors.New("invalid timestamp operation")

	// ErrInvalidDuration indicates an invalid duration value
	ErrInvalidDuration = errors.New("invalid duration value")

	// ErrInvalidJSONPath indicates an invalid JSON path expression
	ErrInvalidJSONPath = errors.New("invalid JSON path")

	// ErrInvalidOperator indicates an invalid operator
	ErrInvalidOperator = errors.New("invalid operator")

	// ErrUnsupportedType indicates an unsupported type
	ErrUnsupportedType = errors.New("unsupported type")

	// ErrContextCanceled indicates the operation was cancelled via context
	ErrContextCanceled = errors.New("operation cancelled")

	// ErrInvalidByteArrayLength indicates byte array exceeds maximum length
	ErrInvalidByteArrayLength = errors.New("byte array exceeds maximum length")

	// ErrUnsupportedDialectFeature indicates a feature not supported by the selected dialect
	ErrUnsupportedDialectFeature = errors.New("unsupported dialect feature")
)

Sentinel errors for common error conditions. These exported errors allow callers to use errors.Is() for specific error handling.

Functions

func Convert

func Convert(ast *cel.Ast, opts ...ConvertOption) (string, error)

Convert converts a CEL AST to a SQL WHERE clause condition. By default, PostgreSQL SQL is generated. Use WithDialect to select a different dialect.

Example without options (PostgreSQL):

sql, err := cel2sql.Convert(ast)

Example with schema information for JSON/JSONB support:

sql, err := cel2sql.Convert(ast, cel2sql.WithSchemas(schemas))

Example with a different dialect:

sql, err := cel2sql.Convert(ast, cel2sql.WithDialect(mysql.New()))

Types

type ComprehensionInfo

type ComprehensionInfo struct {
	Type      ComprehensionType
	IterVar   string
	IndexVar  string // for two-variable comprehensions
	AccuVar   string
	HasFilter bool
	IsTwoVar  bool
	Transform *exprpb.Expr // transform expression for map/transformList
	Predicate *exprpb.Expr // predicate expression for filtering
	Filter    *exprpb.Expr // filter expression for map with filter
}

ComprehensionInfo holds metadata about a comprehension operation

type ComprehensionType

type ComprehensionType int

ComprehensionType represents the type of comprehension operation

const (
	ComprehensionAll               ComprehensionType = iota // All elements satisfy condition
	ComprehensionExists                                     // At least one element satisfies condition
	ComprehensionExistsOne                                  // Exactly one element satisfies condition
	ComprehensionMap                                        // Transform elements using expression
	ComprehensionFilter                                     // Filter elements by predicate
	ComprehensionTransformList                              // Transform list elements
	ComprehensionTransformMap                               // Transform map entries
	ComprehensionTransformMapEntry                          // Transform map key-value pairs
	ComprehensionUnknown                                    // Unrecognized comprehension pattern
)

CEL comprehension types supported by cel2sql

func (ComprehensionType) String

func (ct ComprehensionType) String() string

String returns a string representation of the comprehension type

type ConversionError

type ConversionError struct {
	// UserMessage is the sanitized error message safe to display to end users
	UserMessage string

	// InternalDetails contains detailed information for logging and debugging
	// This should NEVER be exposed to end users
	InternalDetails string

	// WrappedErr is the underlying error, if any
	WrappedErr error
}

ConversionError represents an error that occurred during CEL to SQL conversion. It provides a sanitized user-facing message while preserving detailed information for logging and debugging. This prevents information disclosure through error messages (CWE-209: Information Exposure Through Error Message).

func (*ConversionError) Error

func (e *ConversionError) Error() string

Error returns the user-facing error message. This is what gets displayed when the error is returned to callers.

func (*ConversionError) Internal

func (e *ConversionError) Internal() string

Internal returns the full internal details for logging purposes. This should only be used with structured logging, never displayed to users.

func (*ConversionError) Unwrap

func (e *ConversionError) Unwrap() error

Unwrap returns the wrapped error for use with errors.Is and errors.As

type ConvertOption

type ConvertOption func(*convertOptions)

ConvertOption is a functional option for configuring the Convert function.

func WithColumnAliases

func WithColumnAliases(aliases map[string]string) ConvertOption

WithColumnAliases maps CEL variable names to SQL column names. When a CEL identifier matches a key in the alias map, the SQL output uses the mapped column name instead. This is useful when the database column names differ from the user-facing CEL variable names (e.g., prefixed column names in views or tables).

Example:

result, err := cel2sql.ConvertParameterized(ast,
    cel2sql.WithColumnAliases(map[string]string{
        "name":   "usr_name",
        "active": "usr_active",
    }))
// CEL: name == "Alice"
// SQL: usr_name = $1

func WithContext

func WithContext(ctx context.Context) ConvertOption

WithContext provides a context for cancellation and timeout support. If not provided, operations will run without cancellation checks. This allows long-running conversions to be cancelled and enables timeout protection.

Example with timeout:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
sql, err := cel2sql.Convert(ast, cel2sql.WithContext(ctx))

Example with cancellation:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sql, err := cel2sql.Convert(ast, cel2sql.WithContext(ctx), cel2sql.WithSchemas(schemas))

func WithDialect

func WithDialect(d dialect.Dialect) ConvertOption

WithDialect sets the SQL dialect for conversion. If not provided, PostgreSQL is used as the default dialect.

Example:

import "github.com/observeinc/cel2sql/v3/dialect/mysql"

sql, err := cel2sql.Convert(ast, cel2sql.WithDialect(mysql.New()))

func WithJSONVariables

func WithJSONVariables(vars ...string) ConvertOption

WithJSONVariables declares CEL variable names that correspond to JSONB columns. When a variable is marked as JSONB, field access via dot notation (context.host) and bracket notation (context["host"]) will produce PostgreSQL JSONB operators (e.g., context->>'host') instead of plain dot notation (context.host).

Example:

result, err := cel2sql.ConvertParameterized(ast,
    cel2sql.WithJSONVariables("context"))
// CEL: context.host == "web-1"
// SQL: context->>'host' = $1

func WithLogger

func WithLogger(logger *slog.Logger) ConvertOption

WithLogger provides a logger for observability and debugging. If not provided, logging is disabled with zero overhead using slog.DiscardHandler.

The logger enables visibility into:

  • JSON path detection decisions (table, field, operator selection)
  • Comprehension type identification (all, exists, filter, map)
  • Schema lookups (hits/misses, field types)
  • Performance metrics (conversion duration)
  • Regex pattern transformations (RE2 to POSIX)
  • Operator mapping decisions
  • Error contexts with full details

Example with JSON output:

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
sql, err := cel2sql.Convert(ast, cel2sql.WithLogger(logger))

Example with text output:

logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
sql, err := cel2sql.Convert(ast, cel2sql.WithSchemas(schemas), cel2sql.WithLogger(logger))

func WithMaxDepth

func WithMaxDepth(maxDepth int) ConvertOption

WithMaxDepth sets the maximum recursion depth for expression traversal. If not provided, defaultMaxRecursionDepth (100) is used. This protects against stack overflow from deeply nested expressions (CWE-674).

Example with custom depth:

sql, err := cel2sql.Convert(ast, cel2sql.WithMaxDepth(150))

Example with multiple options:

sql, err := cel2sql.Convert(ast,
    cel2sql.WithMaxDepth(50),
    cel2sql.WithContext(ctx),
    cel2sql.WithSchemas(schemas))

func WithMaxOutputLength

func WithMaxOutputLength(maxLength int) ConvertOption

WithMaxOutputLength sets the maximum length of generated SQL output. If not provided, defaultMaxSQLOutputLength (50000) is used. This protects against resource exhaustion from extremely large SQL queries (CWE-400).

Example with custom output length limit:

sql, err := cel2sql.Convert(ast, cel2sql.WithMaxOutputLength(100000))

Example with multiple options:

sql, err := cel2sql.Convert(ast,
    cel2sql.WithMaxOutputLength(25000),
    cel2sql.WithMaxDepth(50),
    cel2sql.WithContext(ctx))

func WithSchemas

func WithSchemas(schemas map[string]schema.Schema) ConvertOption

WithSchemas provides schema information for proper JSON/JSONB field handling. This option is required for correct SQL generation when using JSON/JSONB fields.

Example:

schemas := provider.GetSchemas()
sql, err := cel2sql.Convert(ast, cel2sql.WithSchemas(schemas))

type IndexRecommendation

type IndexRecommendation struct {
	// Column is the database column that should be indexed
	Column string

	// IndexType specifies the index type (e.g., "BTREE", "GIN", "ART", "CLUSTERING")
	IndexType string

	// Expression is the complete DDL statement that can be executed directly
	Expression string

	// Reason explains why this index is recommended and what query patterns it optimizes
	Reason string
}

IndexRecommendation represents a database index recommendation based on CEL query patterns. It provides actionable guidance for optimizing query performance through appropriate indexing strategies.

func AnalyzeQuery

func AnalyzeQuery(ast *cel.Ast, opts ...ConvertOption) (string, []IndexRecommendation, error)

AnalyzeQuery converts a CEL AST to SQL and provides dialect-specific index recommendations. It analyzes the query patterns to suggest indexes that would optimize performance.

The function detects patterns that benefit from specific index types:

  • JSON/JSONB path operations → GIN indexes (PostgreSQL), functional indexes (MySQL), search indexes (BigQuery)
  • Array operations → GIN indexes (PostgreSQL), ART indexes (DuckDB)
  • Regex matching → GIN indexes with pg_trgm (PostgreSQL), FULLTEXT indexes (MySQL)
  • Comparison operations → B-tree indexes (PostgreSQL/MySQL/SQLite), ART (DuckDB), clustering (BigQuery)

Use WithDialect() to get dialect-specific index recommendations. Defaults to PostgreSQL.

Example:

sql, recommendations, err := cel2sql.AnalyzeQuery(ast,
    cel2sql.WithSchemas(schemas),
    cel2sql.WithDialect(mysql.New()))
if err != nil {
    return err
}

// Use the SQL query
rows, err := db.Query("SELECT * FROM table WHERE " + sql)

// Apply index recommendations
for _, rec := range recommendations {
    fmt.Printf("Recommendation: %s\n", rec.Reason)
    fmt.Printf("Execute: %s\n\n", rec.Expression)
}

type Result

type Result struct {
	SQL        string // The generated SQL WHERE clause with placeholders
	Parameters []any  // Parameter values in order ($1, $2, etc.)
}

Result represents the output of a CEL to SQL conversion with parameterized queries. It contains the SQL string with placeholders ($1, $2, etc.) and the corresponding parameter values.

func ConvertParameterized

func ConvertParameterized(ast *cel.Ast, opts ...ConvertOption) (*Result, error)

ConvertParameterized converts a CEL AST to a parameterized SQL WHERE clause. Returns both the SQL string with placeholders and the parameter values. By default uses PostgreSQL ($1, $2). Use WithDialect for other placeholder styles.

Constants that are parameterized:

  • String literals: 'John' → $1
  • Numeric literals: 42, 3.14 → $1, $2
  • Byte literals: b"data" → $1

Constants kept inline (for query plan optimization):

  • TRUE, FALSE (boolean constants)
  • NULL

Example:

result, err := cel2sql.ConvertParameterized(ast,
    cel2sql.WithSchemas(schemas),
    cel2sql.WithContext(ctx))
// result.SQL: "user.age = $1 AND user.name = $2"
// result.Parameters: []interface{}{18, "John"}

// Execute with database/sql
rows, err := db.Query("SELECT * FROM users WHERE "+result.SQL, result.Parameters...)

Directories

Path Synopsis
Package bigquery provides BigQuery type provider for CEL type system integration.
Package bigquery provides BigQuery type provider for CEL type system integration.
Package dialect defines the interface for SQL dialect-specific code generation.
Package dialect defines the interface for SQL dialect-specific code generation.
bigquery
Package bigquery implements the BigQuery SQL dialect for cel2sql.
Package bigquery implements the BigQuery SQL dialect for cel2sql.
duckdb
Package duckdb implements the DuckDB SQL dialect for cel2sql.
Package duckdb implements the DuckDB SQL dialect for cel2sql.
mysql
Package mysql implements the MySQL SQL dialect for cel2sql.
Package mysql implements the MySQL SQL dialect for cel2sql.
postgres
Package postgres implements the PostgreSQL SQL dialect for cel2sql.
Package postgres implements the PostgreSQL SQL dialect for cel2sql.
sqlite
Package sqlite implements the SQLite SQL dialect for cel2sql.
Package sqlite implements the SQLite SQL dialect for cel2sql.
Package duckdb provides DuckDB type provider for CEL type system integration.
Package duckdb provides DuckDB type provider for CEL type system integration.
examples
basic command
Package main demonstrates basic usage of cel2sql with a predefined schema.
Package main demonstrates basic usage of cel2sql with a predefined schema.
comprehensions command
Package main demonstrates CEL comprehensions support in cel2sql with PostgreSQL integration.
Package main demonstrates CEL comprehensions support in cel2sql with PostgreSQL integration.
context command
Package main demonstrates context usage with cel2sql for cancellation and timeout support.
Package main demonstrates context usage with cel2sql for cancellation and timeout support.
index_analysis command
Package main demonstrates how to use cel2sql.AnalyzeQuery() for index recommendations
Package main demonstrates how to use cel2sql.AnalyzeQuery() for index recommendations
load_table_schema command
Package main demonstrates loading table schema dynamically from PostgreSQL.
Package main demonstrates loading table schema dynamically from PostgreSQL.
logging command
Package main demonstrates cel2sql structured logging with log/slog.
Package main demonstrates cel2sql structured logging with log/slog.
parameterized command
Package main demonstrates parameterized query support in cel2sql.
Package main demonstrates parameterized query support in cel2sql.
string_extensions command
Package main demonstrates CEL string extension functions in cel2sql
Package main demonstrates CEL string extension functions in cel2sql
Package mysql provides MySQL type provider for CEL type system integration.
Package mysql provides MySQL type provider for CEL type system integration.
Package pg provides PostgreSQL type provider for CEL type system integration.
Package pg provides PostgreSQL type provider for CEL type system integration.
Package schema provides dialect-agnostic database schema types for CEL to SQL conversion.
Package schema provides dialect-agnostic database schema types for CEL to SQL conversion.
Package sqlite provides SQLite type provider for CEL type system integration.
Package sqlite provides SQLite type provider for CEL type system integration.
Package sqltypes provides custom SQL type definitions for CEL (Date, Time, DateTime).
Package sqltypes provides custom SQL type definitions for CEL (Date, Time, DateTime).
Package test provides PostgreSQL schema definitions for testing.
Package test provides PostgreSQL schema definitions for testing.
Package testcases defines shared test case types and helpers for multi-dialect testing.
Package testcases defines shared test case types and helpers for multi-dialect testing.
Package testutil provides multi-dialect test runners and helpers.
Package testutil provides multi-dialect test runners and helpers.

Jump to

Keyboard shortcuts

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