Skip to content

SPANDigital/Cel2Sql.NET

Repository files navigation

Cel2Sql.NET

A .NET library that converts CEL (Common Expression Language) expressions into SQL WHERE clauses. Write filter expressions once in CEL and target any supported SQL dialect.

NuGet NuGet Downloads CI License: MIT .NET 8.0

PostgreSQL MySQL SQLite DuckDB BigQuery Spark

Features

  • 6 SQL dialects — PostgreSQL, MySQL, SQLite, DuckDB, BigQuery, Apache Spark
  • Parameterized queries — safe placeholder-based output to prevent SQL injection
  • Index analysis — dialect-specific index recommendations for your query patterns
  • JSON/JSONB support — field access, existence checks, array operations
  • Array comprehensionsall, exists, exists_one, map, filter
  • Timestamp arithmetic — durations, intervals, EXTRACT, timezone handling
  • Regex with ReDoS protection — RE2 pattern conversion per dialect
  • Security limits — configurable max recursion depth and output length

Installation

dotnet add package Cel2Sql

Or add a PackageReference to your project file:

<PackageReference Include="Cel2Sql" Version="1.0.0" />

Requirements: .NET 8.0 or later.

Quick Start

Basic Conversion

using Cel2Sql;
using Cel2Sql.Cel;

// 1. Build a CEL environment with your variable declarations
var env = CelEnvironment.NewBuilder()
    .AddVariable("name", CelVarType.String)
    .AddVariable("age", CelVarType.Int)
    .Build();

// 2. Compile (parse + type-check) the CEL expression
CelAst ast = env.Compile("name == \"alice\" && age > 21");

// 3. Convert to SQL (defaults to PostgreSQL)
string sql = Cel2SqlConverter.Convert(ast);
// => "name = 'alice' AND age > 21"

Choosing a Dialect

using Cel2Sql;
using Cel2Sql.Cel;
using Cel2Sql.Dialects.Postgres;
using Cel2Sql.Dialects.MySql;
using Cel2Sql.Dialects.Sqlite;
using Cel2Sql.Dialects.DuckDb;
using Cel2Sql.Dialects.BigQuery;
using Cel2Sql.Dialects.Spark;

var env = CelEnvironment.NewBuilder()
    .AddVariable("age", CelVarType.Int)
    .Build();
CelAst ast = env.Compile("age > 21");

string pg     = Cel2SqlConverter.Convert(ast);                                       // PostgreSQL (default)
string pgExpl = Cel2SqlConverter.Convert(ast, o => o.WithDialect(new PostgresDialect()));
string mysql  = Cel2SqlConverter.Convert(ast, o => o.WithDialect(new MySqlDialect()));
string sqlite = Cel2SqlConverter.Convert(ast, o => o.WithDialect(new SqliteDialect()));
string duckdb = Cel2SqlConverter.Convert(ast, o => o.WithDialect(new DuckDbDialect()));
string bq     = Cel2SqlConverter.Convert(ast, o => o.WithDialect(new BigQueryDialect()));
string spark  = Cel2SqlConverter.Convert(ast, o => o.WithDialect(new SparkDialect()));

Parameterized Queries

Literal values are replaced with placeholders and returned separately, keeping your queries safe from injection:

using Cel2Sql;
using Cel2Sql.Cel;

var env = CelEnvironment.NewBuilder()
    .AddVariable("age", CelVarType.Int)
    .Build();
CelAst ast = env.Compile("age == 18");

ConvertResult result = Cel2SqlConverter.ConvertParameterized(ast);

string sql = result.Sql;
// PostgreSQL: "age = $1"
// MySQL:      "age = ?"

IReadOnlyList<object?> parameters = result.Parameters;
// [18]

Booleans and nulls are always inlined (never parameterized) so the database can plan the query optimally.

Advanced Usage

Index Analysis

Get dialect-specific index recommendations for your query patterns:

using Cel2Sql;
using Cel2Sql.Cel;
using Cel2Sql.Dialects;
using Cel2Sql.Dialects.Postgres;

var env = CelEnvironment.NewBuilder()
    .AddVariable("name", CelVarType.String)
    .AddVariable("age", CelVarType.Int)
    .Build();
CelAst ast = env.Compile("name == \"alice\" && age > 21");

AnalyzeResult result = Cel2SqlConverter.AnalyzeQuery(ast, o => o.WithDialect(new PostgresDialect()));

Console.WriteLine(result.Sql);
// "name = 'alice' AND age > 21"

foreach (IndexRecommendation rec in result.Recommendations)
{
    Console.WriteLine(rec.Column);      // "name", "age"
    Console.WriteLine(rec.IndexType);   // "BTREE"
    Console.WriteLine(rec.Expression);  // "CREATE INDEX idx_name ON table_name (name);"
    Console.WriteLine(rec.Reason);      // why this index helps
}

Index analysis is a no-op on Spark — AnalyzeQuery returns an empty Recommendations list because indexing on Spark is storage-layer-specific (Delta Z-order vs Iceberg sort vs plain Parquet) and not portable.

JSON/JSONB Field Schemas

Define schemas to enable JSON field access and type-aware conversions:

using Cel2Sql;
using Cel2Sql.Cel;
using Cel2Sql.Dialects.Postgres;
using Cel2Sql.Schema;

var schema = new Schema(new[]
{
    new FieldSchema("metadata", "jsonb", isJson: true, isJsonb: true),
});

string sql = Cel2SqlConverter.Convert(ast, o => o
    .WithDialect(new PostgresDialect())
    .WithSchemas(new Dictionary<string, Schema> { ["default"] = schema }));

Configuration Options

All options are set via fluent With... calls on ConvertOptions. Pass them inline as params Action<ConvertOptions>[], or build a ConvertOptions once and reuse it:

using Cel2Sql;
using Cel2Sql.Dialects.Postgres;

string sql = Cel2SqlConverter.Convert(ast, o => o
    .WithDialect(new PostgresDialect())                                 // SQL dialect (default: PostgreSQL)
    .WithSchemas(schemas)                                               // Schema map for JSON field detection
    .WithJsonVariables("context", "tags")                              // Flat JSONB columns (use ->> instead of .)
    .WithColumnAliases(new Dictionary<string, string> { ["name"] = "usr_name" }) // CEL identifier -> SQL column rename
    .WithParamStartIndex(5)                                            // Embed in larger query: placeholders start at $5
    .WithMaxDepth(100)                                                 // Max AST recursion depth (default: 100)
    .WithMaxOutputLength(50000)                                        // Max SQL output length (default: 50,000)
    .WithLogger(logger));                                              // ILogger for debugging

Flat JSONB columns — mark variables typed as JSONB columns so dot-access uses JSON operators rather than plain SQL dot notation:

// CEL: context.host == "web-1"
// Without:                            context.host = 'web-1'
// With .WithJsonVariables("context"): context->>'host' = 'web-1'

Column aliases — map user-facing CEL names to actual database column names (e.g. when columns are prefixed). Alias values are validated against the dialect's identifier rules:

string sql = Cel2SqlConverter.Convert(ast, o => o
    .WithColumnAliases(new Dictionary<string, string>
    {
        ["name"] = "usr_name",
        ["active"] = "usr_active",
    }));
// CEL: name == "Alice"  ->  SQL: usr_name = 'Alice'

Parameter start index — useful when embedding the generated SQL into a larger pre-parameterized query so placeholders don't clash:

ConvertResult res = Cel2SqlConverter.ConvertParameterized(ast, o => o.WithParamStartIndex(5));
// PostgreSQL: name = $5 AND age > $6
// Caller appends res.Parameters to their args at positions 5+.

Power users: bring your own Cel.NET AST

If you already hold a type-checked Cel.NET Ast, wrap it directly with CelAst.FromCelNet(ast) instead of going through CelEnvironment.

Supported CEL Functions

Category Functions
String predicates contains, startsWith, endsWith, matches, size
String manipulation lowerAscii, upperAscii, trim, charAt, indexOf, lastIndexOf, substring, replace, reverse, split, join, format
Type conversion bool, bytes, double, int, string, duration, timestamp
Timestamp extraction getFullYear, getMonth, getDate, getDayOfMonth, getDayOfYear, getDayOfWeek, getHours, getMinutes, getSeconds, getMilliseconds
Comprehensions all, exists, exists_one, map, filter

Note on format() — supported on PostgreSQL (via FORMAT()), BigQuery (via FORMAT()), SQLite and DuckDB (via printf()). MySQL throws because it has no printf-style FORMAT. Only %s, %d, %f specifiers are accepted; format strings are bounded to 1000 characters.

Supported Dialects

Feature PostgreSQL MySQL SQLite DuckDB BigQuery Spark
Param style $N ? ? $N @pN ?
String concat || CONCAT() || || || concat()
Array type native JSON JSON native native native (ARRAY<T>)
Contains POSITION() LOCATE() INSTR() CONTAINS() STRPOS() LOCATE()
Regex ~ / ~* REGEXP unsupported ~ / ~* REGEXP_CONTAINS() RLIKE (Java regex)
JSON access ->> ->>'$.k' json_extract ->> JSON_VALUE get_json_object
Index advisor yes yes yes yes yes no (storage-specific)

CEL Expression Examples

CEL Expression PostgreSQL Output
age > 21 age > 21
name == "alice" name = 'alice'
active == true active = TRUE
name == "a" && age > 18 name = 'a' AND age > 18
status == "active" || role == "admin" status = 'active' OR role = 'admin'
email.startsWith("admin") email LIKE 'admin%' ESCAPE E'\\'
name.contains("test") POSITION('test' IN name) > 0
name.matches("^a.*z$") name ~ '^a.*z$'
"admin" in roles 'admin' = ANY(roles)
scores.all(s, s >= 60) NOT EXISTS (SELECT 1 FROM UNNEST(scores) AS s WHERE NOT (s >= 60))
age > 18 ? "adult" : "minor" CASE WHEN age > 18 THEN 'adult' ELSE 'minor' END

Resource Limits

Cel2Sql.NET enforces several caps to keep generated SQL safe and bounded:

Limit Default Configurable Purpose
Recursion depth 100 WithMaxDepth(int) Prevent stack overflow on deeply nested CEL
SQL output length 50 000 chars WithMaxOutputLength(int) Cap total generated SQL size
Byte literal length (inline mode) 10 000 bytes constant Prevent CWE-400 hex expansion (parameterized mode bypasses this — bytes go straight to the parameter list)
format() string length 1 000 chars constant Bound expansion in formatted SQL

Error Handling

Conversion failures throw ConversionException (in Cel2Sql.Errors), which separates a UserMessage (safe to surface to end users) from InternalDetails (for logs only) — the CWE-209 pattern.

Origin

Cel2Sql.NET is a C# port of cel2sql4j (the Java port), which is itself a port of cel2sql, originally written in Go. It preserves the same API design, dialect coverage, and test cases.

CEL parsing and type-checking are provided by the Cel.NET library (a .NET port of cel-java).

License

MIT © Span Digital

About

Convert CEL (Common Expression Language) expressions into SQL WHERE clauses across PostgreSQL, MySQL, SQLite, DuckDB, BigQuery, and Spark. A C# port of cel2sql4j.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages