Skip to content

CEL expression to SQL condition, compatible with PostgreSQL, MySQL, SQLite, DuckDB, and BigQuery. Java port of go project.

License

Notifications You must be signed in to change notification settings

SPANDigital/cel2sql4j

Repository files navigation

cel2sql4j

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

Java 17+ License: MIT Maven Central

PostgreSQL MySQL SQLite DuckDB BigQuery

Features

  • 5 SQL dialects — PostgreSQL, MySQL, SQLite, DuckDB, BigQuery
  • 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

Quick Start

Installation

Gradle (Kotlin DSL)

dependencies {
    implementation("com.spandigital:cel2sql4j:1.0.0-SNAPSHOT")
}

Gradle (Groovy)

dependencies {
    implementation 'com.spandigital:cel2sql4j:1.0.0-SNAPSHOT'
}

Maven

<dependency>
    <groupId>com.spandigital</groupId>
    <artifactId>cel2sql4j</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

Basic Conversion

import com.spandigital.cel2sql.Cel2Sql;
import dev.cel.compiler.CelCompiler;
import dev.cel.compiler.CelCompilerFactory;
import dev.cel.common.types.SimpleType;
import dev.cel.common.CelAbstractSyntaxTree;

// 1. Set up a CEL compiler with your variable declarations
CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder()
    .addVar("name", SimpleType.STRING)
    .addVar("age", SimpleType.INT)
    .build();

// 2. Parse and check the CEL expression
CelAbstractSyntaxTree ast = compiler.compile("name == \"alice\" && age > 21")
    .getAst();

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

Choosing a Dialect

import com.spandigital.cel2sql.dialect.mysql.MySqlDialect;
import com.spandigital.cel2sql.dialect.sqlite.SqliteDialect;
import com.spandigital.cel2sql.dialect.duckdb.DuckDbDialect;
import com.spandigital.cel2sql.dialect.bigquery.BigQueryDialect;
import com.spandigital.cel2sql.dialect.postgres.PostgresDialect;

// PostgreSQL (default)
String sql = Cel2Sql.convert(ast);

// MySQL
String sql = Cel2Sql.convert(ast, opts -> opts.withDialect(new MySqlDialect()));

// SQLite
String sql = Cel2Sql.convert(ast, opts -> opts.withDialect(new SqliteDialect()));

// DuckDB
String sql = Cel2Sql.convert(ast, opts -> opts.withDialect(new DuckDbDialect()));

// BigQuery
String sql = Cel2Sql.convert(ast, opts -> opts.withDialect(new BigQueryDialect()));

Parameterized Queries

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

import com.spandigital.cel2sql.ConvertResult;

ConvertResult result = Cel2Sql.convertParameterized(ast);

String sql = result.sql();
// PostgreSQL: "name = $1 AND age > $2"
// MySQL:      "name = ? AND age > ?"

List<Object> params = result.parameters();
// ["alice", 21]

Index Analysis

Get dialect-specific index recommendations for your query patterns:

import com.spandigital.cel2sql.AnalyzeResult;
import com.spandigital.cel2sql.dialect.IndexRecommendation;

AnalyzeResult result = Cel2Sql.analyzeQuery(ast,
    opts -> opts.withDialect(new PostgresDialect()));

System.out.println(result.sql());
// "name = 'alice' AND age > 21"

for (IndexRecommendation rec : result.recommendations()) {
    System.out.println(rec.column());     // "name", "age"
    System.out.println(rec.indexType());   // "BTREE"
    System.out.println(rec.expression());  // "CREATE INDEX idx_name ON table_name (name);"
    System.out.println(rec.reason());      // "Comparison operations on 'name' benefit from..."
}

JSON/JSONB Field Schemas

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

import com.spandigital.cel2sql.schema.Schema;
import com.spandigital.cel2sql.schema.FieldSchema;

Schema schema = new Schema(Map.of(
    "metadata", new FieldSchema("metadata", "jsonb", false, 0, null, true, true, null)
));

String sql = Cel2Sql.convert(ast, opts -> opts
    .withDialect(new PostgresDialect())
    .withSchemas(Map.of("default", schema)));

Configuration Options

String sql = Cel2Sql.convert(ast, opts -> opts
    .withDialect(new MySqlDialect())     // SQL dialect (default: PostgreSQL)
    .withSchemas(schemas)                 // Schema map for JSON field detection
    .withMaxDepth(100)                    // Max AST recursion depth (default: 100)
    .withMaxOutputLength(50000)           // Max SQL output length (default: 50,000)
    .withLogger(myLogger));               // SLF4J logger for debugging

Supported Dialects

Dialect Placeholders Regex Arrays JSON Index Types
PostgreSQL $1, $2 ~ / ~* (POSIX) ANY(), UNNEST() ->, ->>, jsonb_ BTREE, GIN
MySQL ?, ? REGEXP (ICU) JSON functions ->, ->>, JSON_ BTREE, FULLTEXT
SQLite ?, ? Not supported JSON functions ->, ->>, json_ BTREE
DuckDB $1, $2 ~ / ~* (RE2) ANY(), UNNEST() ->, ->>, json_ ART
BigQuery @p1, @p2 REGEXP_CONTAINS UNNEST(), arrays JSON_VALUE, JSON_QUERY CLUSTERING, SEARCH_INDEX

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

Building from Source

# Clone the repository
git clone https://github.com/spandigital/cel2sql4j.git
cd cel2sql4j

# Build
./gradlew build

# Run tests
./gradlew test

# Run a single test class
./gradlew test --tests "com.spandigital.cel2sql.Cel2SqlBasicTest"

Requirements: Java 17 or later.

Origin

This library is a Java port of cel2sql, originally written in Go. It preserves the same API design, dialect coverage, and test cases.

License

MIT © Span Digital

About

CEL expression to SQL condition, compatible with PostgreSQL, MySQL, SQLite, DuckDB, and BigQuery. Java port of go project.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages