cel2sql

package module
v2.12.1 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2025 License: Apache-2.0 Imports: 15 Imported by: 0

README

cel2sql

Convert CEL (Common Expression Language) expressions to PostgreSQL SQL

Go Version PostgreSQL License

cel2sql makes it easy to build dynamic SQL queries using CEL expressions. Write type-safe, expressive filters in CEL and automatically convert them to PostgreSQL-compatible SQL.

Quick Start

Installation
go get github.com/spandigital/cel2sql/v2
Basic Example
package main

import (
    "fmt"
    "github.com/google/cel-go/cel"
    "github.com/spandigital/cel2sql/v2"
    "github.com/spandigital/cel2sql/v2/pg"
)

func main() {
    // 1. Define your database table schema
    userSchema := pg.Schema{
        {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?

Type-Safe: Catch errors at compile time, not runtime ✅ PostgreSQL 17: Fully compatible with the latest PostgreSQL ✅ Rich Features: JSON/JSONB, arrays, regex, timestamps, and more ✅ Well-Tested: 100+ tests including integration tests with real PostgreSQL ✅ Easy to Use: Simple API, comprehensive documentation ✅ Secure by Default: Built-in protections against SQL injection and ReDoS attacks

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)
  • ⏱️ 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/spandigital/cel2sql/v2"
)

// 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:

  • 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)

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))

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)
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

Type Mapping

CEL Type PostgreSQL Type
int bigint
double double precision
bool boolean
string text
bytes bytea
list ARRAY
timestamp timestamp with time zone
duration INTERVAL

Dynamic Schema Loading

Load table schemas directly from your PostgreSQL database:

// Connect to database and load schema
provider, _ := pg.NewTypeProviderWithConnection(ctx, "postgres://user:pass@localhost/db")
defer provider.Close()

// Load table schema dynamically
provider.LoadTableSchema(ctx, "users")

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

See Getting Started Guide for more details.

Requirements

  • Go 1.24 or higher
  • PostgreSQL 17 (also compatible with PostgreSQL 15+)

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 converts CEL (Common Expression Language) expressions to PostgreSQL SQL conditions.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Convert

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

Convert converts a CEL AST to a PostgreSQL SQL WHERE clause condition. Options can be provided to configure the conversion behavior.

Example without options:

sql, err := cel2sql.Convert(ast)

Example with schema information for JSON/JSONB support:

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

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 added in v2.12.0

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 added in v2.12.0

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 added in v2.12.0

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 added in v2.12.0

func (e *ConversionError) Unwrap() error

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

type ConvertOption added in v2.11.0

type ConvertOption func(*convertOptions)

ConvertOption is a functional option for configuring the Convert function.

func WithContext added in v2.11.0

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 WithLogger added in v2.11.0

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 added in v2.12.0

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 added in v2.12.0

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 added in v2.11.0

func WithSchemas(schemas map[string]pg.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 Result added in v2.12.0

type Result struct {
	SQL        string        // The generated SQL WHERE clause with placeholders
	Parameters []interface{} // 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 added in v2.12.0

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

ConvertParameterized converts a CEL AST to a parameterized PostgreSQL SQL WHERE clause. Returns both the SQL string with placeholders ($1, $2, etc.) and the parameter values. This enables query plan caching and provides additional SQL injection protection.

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
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.
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.
Package pg provides PostgreSQL type provider for CEL type system integration.
Package pg provides PostgreSQL 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.

Jump to

Keyboard shortcuts

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