Convert CEL (Common Expression Language) expressions to SQL WHERE clauses
Project description
pycel2sql
Convert CEL (Common Expression Language) expressions to SQL WHERE clauses.
Python port of cel2sql (Go).
Installation
pip install pycel2sql
Requires Python 3.12+.
Quick Start
from pycel2sql import convert
# Default dialect is PostgreSQL
sql = convert('name == "alice" && age > 30')
# => name = 'alice' AND age > 30
sql = convert('status == "active" || tags.size() > 0')
# => status = 'active' OR ARRAY_LENGTH(tags, 1) > 0
Dialects
Five SQL dialects are supported:
from pycel2sql import convert
from pycel2sql.dialect import get_dialect
# Using get_dialect() factory
sql = convert('name == "alice"', dialect=get_dialect("postgresql"))
sql = convert('name == "alice"', dialect=get_dialect("mysql"))
sql = convert('name == "alice"', dialect=get_dialect("sqlite"))
sql = convert('name == "alice"', dialect=get_dialect("duckdb"))
sql = convert('name == "alice"', dialect=get_dialect("bigquery"))
# Or instantiate directly
from pycel2sql import PostgresDialect, MySQLDialect, SQLiteDialect, DuckDBDialect, BigQueryDialect
sql = convert('name == "alice"', dialect=MySQLDialect())
Parameterized Queries
Use convert_parameterized() to produce parameterized SQL with bind placeholders:
from pycel2sql import convert_parameterized, MySQLDialect
result = convert_parameterized('name == "alice" && age > 30')
# result.sql => 'name = $1 AND age > $2' (PostgreSQL default)
# result.parameters => ['alice', 30]
result = convert_parameterized('name == "alice"', dialect=MySQLDialect())
# result.sql => 'name = ?'
# result.parameters => ['alice']
Placeholder styles per dialect:
| Dialect | Placeholder |
|---|---|
| PostgreSQL | $1, $2, ... |
| DuckDB | $1, $2, ... |
| BigQuery | @p1, @p2, ... |
| MySQL | ? |
| SQLite | ? |
JSON Fields
Provide schemas to enable JSON field detection:
from pycel2sql import convert, PostgresDialect
from pycel2sql.schema import Schema, FieldSchema
schemas = {
"usr": Schema([FieldSchema("metadata", is_jsonb=True)])
}
sql = convert(
'usr.metadata.role == "admin"',
dialect=PostgresDialect(),
schemas=schemas,
)
# => usr.metadata->>'role' = 'admin'
Schema Introspection
Auto-discover table schemas from a live database connection instead of building Schema objects manually:
from pycel2sql import convert, introspect
from pycel2sql.dialect.postgres import PostgresDialect
import psycopg
conn = psycopg.connect("postgresql://localhost/mydb")
# Introspect specific tables — detects JSON, JSONB, and array columns
schemas = introspect("postgresql", conn, table_names=["users", "orders"])
sql = convert(
'users.metadata.role == "admin"',
dialect=PostgresDialect(),
schemas=schemas,
)
# => users.metadata->>'role' = 'admin'
Per-dialect functions are also available:
from pycel2sql.introspect import introspect_postgres, introspect_sqlite
# PostgreSQL — detects JSONB, JSON, and ARRAY columns
schemas = introspect_postgres(conn, table_names=["users"], schema_name="public")
# SQLite — explicit json_columns since SQLite has no JSON type
schemas = introspect_sqlite(
conn,
table_names=["events"],
json_columns={"events": ["payload", "tags"]},
)
All five dialects are supported: introspect_postgres, introspect_duckdb, introspect_bigquery, introspect_mysql, introspect_sqlite.
Supported CEL Features
- Comparisons:
==,!=,<,<=,>,>= - Logic:
&&,||,! - Arithmetic:
+,-,*,/,% - String functions:
contains(),startsWith(),endsWith(),matches(),size(),split(),join() - Type casting:
int(),uint(),double(),string(),bool(),bytes(),timestamp(),duration() - Collections:
inoperator, list literals,size(), index access - Timestamps & durations: arithmetic,
getFullYear(),getMonth(),getDayOfMonth(),getHours(),getMinutes(),getSeconds() - Macros:
exists(),all(),exists_one(),map(),filter() - Ternary: conditional expressions
- JSON: field access,
has()existence checks, nested paths - Regex:
matches()with RE2 syntax - Structs: struct construction
Index Analysis
Analyze expressions for PostgreSQL index recommendations:
from pycel2sql import analyze
result = analyze('name == "alice" && age > 30')
# result.sql => "name = 'alice' AND age > 30"
# result.recommendations => [IndexRecommendation(...), ...]
Security Limits
Configurable resource limits prevent abuse:
from pycel2sql import convert
sql = convert(
cel_expr,
max_depth=100, # AST recursion depth (default: 100)
max_output_length=50000, # Max SQL output bytes (default: 50000)
)
Additional built-in limits: comprehension nesting (3 levels), regex pattern length (500 chars), field name length (63 chars), byte array size (10,000).
Development
# Setup
uv venv && uv pip install -e ".[dev]"
# Tests
uv run pytest tests/ --ignore=tests/integration -v
# Integration tests (requires Docker/Podman)
uv pip install -e ".[integration]"
uv run pytest tests/integration/ -v
# Lint & type check
uv run ruff check src/ tests/
uv run mypy src/pycel2sql/
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pycel2sql-0.1.0.tar.gz.
File metadata
- Download URL: pycel2sql-0.1.0.tar.gz
- Upload date:
- Size: 141.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72a8226f0aec2f7469bddb8d1e3b04ceb1710c28bc56b99ada4e22577c0da15f
|
|
| MD5 |
0f2f99f67f19dd0371f0dce59aa8460a
|
|
| BLAKE2b-256 |
35547831f9f0ef95ab3464a8b5ad0df30cec0c95b940a2bf9cc544e12e534e7e
|
Provenance
The following attestation bundles were made for pycel2sql-0.1.0.tar.gz:
Publisher:
release.yml on SPANDigital/pycel2sql
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycel2sql-0.1.0.tar.gz -
Subject digest:
72a8226f0aec2f7469bddb8d1e3b04ceb1710c28bc56b99ada4e22577c0da15f - Sigstore transparency entry: 1003352446
- Sigstore integration time:
-
Permalink:
SPANDigital/pycel2sql@4b600fa375a15b1cbc4f0247bd30135c57d1ef89 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/SPANDigital
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4b600fa375a15b1cbc4f0247bd30135c57d1ef89 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pycel2sql-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pycel2sql-0.1.0-py3-none-any.whl
- Upload date:
- Size: 51.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0108d8ffea854e4b33bea6986b9bd7be443cb7b4cafe30163a3beb7b3320f631
|
|
| MD5 |
1c994f2254a9272b62beada60f62cda5
|
|
| BLAKE2b-256 |
fc8a790df56115781a22b29759a734e75c4781eab8f3a1b15dcfdad19d36016e
|
Provenance
The following attestation bundles were made for pycel2sql-0.1.0-py3-none-any.whl:
Publisher:
release.yml on SPANDigital/pycel2sql
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycel2sql-0.1.0-py3-none-any.whl -
Subject digest:
0108d8ffea854e4b33bea6986b9bd7be443cb7b4cafe30163a3beb7b3320f631 - Sigstore transparency entry: 1003352454
- Sigstore integration time:
-
Permalink:
SPANDigital/pycel2sql@4b600fa375a15b1cbc4f0247bd30135c57d1ef89 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/SPANDigital
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4b600fa375a15b1cbc4f0247bd30135c57d1ef89 -
Trigger Event:
push
-
Statement type: