EdgeSQL
A Python library that parses Postgres and Databricks SQL, resolves every column, and edits a typed syntax tree. Preview before apply. No string replacement.
EdgeSQL is a SQL parser, SQL rewriter, and query builder for Python. It reads a statement, resolves every column against its real table, and edits a copy of the syntax tree. It prints the result in the same dialect. It never edits SQL text with string replacement, so a rename cannot break a comment, a literal, or a second edit on the same name.
Use EdgeSQL to rename a column safely, patch a filter value, change a range, a
list, or a limit, rewrite a WHERE clause, add or drop a CTE, and build a
dynamic filter screen from any query.
Supported dialects: postgres and databricks.
- PyPI: https://pypi.org/project/edgesql/
- Documentation and tutorial: https://raphaelmansuy.github.io/EdgeSQL/
- Example app:
examples/report-helper(FastAPI and native HTML) - Licence: Apache 2.0
from edgesql import Query
query = Query.parse("SELECT cust_id FROM sales.orders WHERE cust_id = 42", dialect="postgres")
print(query.to_sql())
# SELECT cust_id FROM sales.orders WHERE cust_id = 42
Why not string replacement
| Task | String replacement | EdgeSQL |
|---|---|---|
Rename cust_id |
Changes comments and literals too | Changes only the resolved column |
| Know the owner table | Guess from the text | Reports the physical table and the CTE hops |
| Two edits on one name | The second edit corrupts the first | Each edit runs on a copy of the tree |
| Check the result | Read the diff by eye | Preview blocks a change that cannot reparse |
| Change a filter value | Build a new SQL string | Patches one node and keeps the operator |
Install
From PyPI with pip:
pip install edgesql
With uv, which resolves and installs in one step:
uv add edgesql # add it to a project
uv pip install edgesql # or install into the active environment
To run it once without installing it:
uvx --from edgesql python -c "from edgesql import Query; print(Query.parse('SELECT 1', dialect='postgres').to_sql())"
Check the install:
python -c "import edgesql, importlib.metadata as m; print(m.version('edgesql'))"
To add the optional Postgres check with pglast:
pip install "edgesql[postgres]" # or: uv add "edgesql[postgres]"
Install from the repository
For development, clone the repository and use the dev extra. It adds pytest,
FastAPI, uvicorn, Playwright, and ruff.
git clone https://github.com/raphaelmansuy/EdgeSQL.git
cd EdgeSQL
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
With uv, the same step is:
uv venv
uv pip install -e ".[dev]"
The two ways to change a query
A Query is immutable. Each method returns a new Query and leaves the
original unchanged.
- Lane B: a named transform returns a new
Query. Preview it, then apply it. - Lane A: a value patch replaces one comparison value, range, list, or limit.
Both lanes pass through preview(). Apply is the only commit.
flowchart LR
text["SQL text"] --> parse["Query.parse"]
parse --> tree["typed tree"]
tree --> transform["named transform or value patch"]
transform --> preview["preview: before, after, diff, reasons"]
preview -->|"clean"| apply["apply"]
preview -->|"blocked"| refuse["no SQL"]
Read a statement
query = Query.parse(sql, dialect="postgres")
query.tables() # ['sales.orders', 'sales.customers']
query.ctes() # ['paid']
query.columns() # ColumnRef objects with table, name, and hop list
query.filters() # one Filter per conjunct
query.params() # marker names, for example '1' or 'fare_parameter'
query.to_tree() # the product IR, schemaVersion '0.1'
filters() returns one item per conjunct. An OR group is one item. Each
item reports its scope, clause, op, values, and the columns it touches.
The hop list records the CTEs and derived tables on the path to the physical
table.
Rename a column with preview and apply
query = Query.parse(
"SELECT cust_id, status FROM sales.orders",
dialect="postgres",
)
preview = query.preview(lambda item: item.rename_column("cust_id", "customer_id", table="sales.orders"))
if preview.blocked:
print(preview.blocked_reasons)
else:
applied = preview.apply(actor="builder")
print(applied.to_sql())
# SELECT customer_id AS cust_id, status FROM sales.orders
The output name stays cust_id. Set keep_output_names=False to change it.
preview() computes these fields:
| Field | Meaning |
|---|---|
before_sql |
Pretty SQL of the original |
after_sql |
Pretty SQL of the result |
ast_diff |
Edit script from sqlglot.diff |
semantic_diff |
Tables, filters, joins, projection, and limit |
blocked_reasons |
Empty when the preview is clean |
collapsed |
True when the pretty SQL did not change |
fingerprint |
First 16 hex characters of SHA-256 of canonical SQL |
apply() raises BLOCKED when the preview is blocked. A second apply of the
same preview returns the same fingerprint.
Patch one value
A value patch needs a node id from to_tree(). It replaces the right-hand
value only.
query = Query.parse(
"SELECT cust_id, status FROM sales.orders WHERE cust_id = 42 AND status = 'paid'",
dialect="postgres",
)
patched = query.set_params([{"op": "set_comparison", "nodeId": "n_cmp_cust_eq", "params": {"value": 99}}])
preview = query.preview(lambda _item: patched)
applied = preview.apply(actor="reader")
The allowed operations are set_comparison, set_between, set_in_list, and
set_limit. A patch that sends left, op, column, or children raises
STRUCTURE_PATCH.
A comparison is editable only when its right side is a literal, a boolean, a
NULL, a negative number, or a parameter marker. A comparison against another
column or a function is read-only, because a value patch would change the
meaning.
Edit a filter in place
from edgesql import col
query = Query.parse(sql, dialect="postgres")
wider = query.cte("paid").and_where(col("region").eq("EU"))
status = query.filters().where(column="status")[0]
without = query.remove_filter(status)
Build a query
from edgesql import col, param, select
query = select("sales.orders").where(col("status").eq("paid")).where(col("amount").gt(param("floor"))).limit(100)
print(query.to_sql("postgres"))
print(query.to_sql("databricks"))
A built query has no parse dialect, so both dialects are available. A parsed
query keeps its dialect. to_sql with another dialect raises CROSS_DIALECT.
Dialect rules
| Topic | Rule |
|---|---|
| Dialect | postgres or databricks, always explicit |
| Markers | Postgres prints $1; Databricks keeps :name and ? |
| QUALIFY | Databricks only |
| SEMI and ANTI joins | Databricks only |
| DISTINCT ON and FETCH | Postgres only, and FETCH refuses on Databricks |
| Recursive CTE | UNION ALL on Databricks; MAX RECURSION LEVEL is preserved |
| Root statement | SELECT and WITH only |
| Mixed markers | :name with ? in one Databricks statement raises MIXED_MARKERS |
Errors
Every error carries a phase, a code, an optional hint, and a 12 character
track id. The display text is [phase/code] message, plus Hint: when a hint
exists.
| Phase | Codes |
|---|---|
| parse | UNKNOWN_DIALECT, EMPTY_SQL, PARSE_FAILED, MULTI_STATEMENT, UNSUPPORTED_STATEMENT, DIALECT_NODE, MIXED_MARKERS |
| resolve | AMBIGUOUS_COLUMN, UNKNOWN_SOURCE, UNKNOWN_COLUMN |
| transform | UNKNOWN_SCOPE, UNSUPPORTED_REMOVE, UNSUPPORTED_NODE, DUPLICATE_CTE, CTE_IN_USE, RECURSIVE_PROJECTION, STRUCTURE_PATCH, UNKNOWN_NODE, NOT_IN_V1 |
| preview and apply | BLOCKED |
| render | CROSS_DIALECT |
from edgesql import Query, SqlCraftError
try:
Query.parse("DELETE FROM sales.orders", dialect="postgres")
except SqlCraftError as error:
print(error.display())
# [parse/UNSUPPORTED_STATEMENT] This statement is DELETE. v1 changes read statements only.
# Hint: v1 accepts `SELECT` and `WITH ... SELECT`.
Blocked reasons are strings, not codes: NO_MATCH, REPARSE_FAILED,
TABLE_SET_CHANGED, DISALLOWED_STATEMENT, and MIGRATE_STUB.
The report helper
examples/report-helper is a FastAPI app with a native HTML screen. The screen
builds its filter panel from the product IR. Every editable node becomes a
control: a text field, a number field, a boolean choice, a range, a list, or a
limit. Read-only predicates show their printed text.
make dev # http://127.0.0.1:8765
Development
make install # virtual environment, library, dev tools, Chromiummake dev # serve the report helper
make site # serve the tutorial site
make check # lint, formatting, and every test
make test # the whole suite
make test-lib # library and official SQL suites
make test-http # HTTP status map
make test-docs # docs site checks
make test-e2e # browser flow and screenshots
make screenshots # list the saved screenshots
make lint # style, imports, and formatting
make fmt # apply formatting and safe fixes
make build # sdist and wheel into dist/
make clean # remove caches and build output
Tests cover the prototype cases, every error code, the official Postgres
regression files, the Databricks manual examples, the HTTP status map, the docs
site, and the browser flow. Screenshots are in
specs/01-first-spec/e2e/screenshots.
Publishing
The package builds and validates with make release-check.
make release-check # build the sdist and the wheel, then check the metadata
make publish-test # upload to TestPyPI
make publish # upload to PyPI
CI publishes from .github/workflows/publish.yml when you publish a GitHub release. Setup and the release checklist are in docs/PUBLISHING.md.
Tutorial
A step by step tutorial, the full example gallery, and the API reference are on
the published site: https://raphaelmansuy.github.io/EdgeSQL/. The source is in
docs/. Serve it locally with make site.
Questions
Can EdgeSQL rename a column without touching comments or string literals? Yes. The rename resolves the column to its table and edits only that column. A comment that contains the old name keeps its text.
Does it support CTEs, subqueries, and Recursive CTEs?
Yes. It resolves CTEs, derived tables, set operations, recursive CTEs, LATERAL
sources, and correlated subqueries. A filter reports the CTE hops on the path to
the physical table.
Can it change a filter value without rewriting the SQL string?
Yes. set_params replaces the right-hand value of one node and keeps the column,
the operator, and the structure.
Does it work with Databricks and Postgres?
Yes. Both dialects are supported. QUALIFY, SEMI, and ANTI joins are
Databricks only. DISTINCT ON and FETCH are Postgres only. A cross-dialect
emit raises CROSS_DIALECT rather than printing a quiet substitute.
Does it connect to a database?
No. EdgeSQL reads and writes SQL text. It does not run a query and does not need
a connection. Pass schema to disambiguate a column when a statement joins two
tables.
Can a web form change a query safely? Yes. The report helper builds its filter panel from the product IR, so any query gets controls that match its predicates. A change that cannot reparse is blocked, and Apply stays disabled until a clean preview exists.
What are the limits?
inline_cte and rewrite are not in v1, and migrate returns a blocked preview.
A transform that changes a recursive CTE projection is refused.
Author
Raphael Mansuy is a data engineering and AI strategist with more than 20 years of global technology leadership experience. He works on democratizing data management and AI, and helps organizations build scalable data platforms. His work covers resilient data infrastructure, machine learning models, and coaching teams on practice. He leads a startup studio in Hong Kong and writes about AI and data strategy.
EdgeSQL started from a simple observation: changing SQL with string replacement breaks as soon as two edits touch the same name. A typed tree with a preview gate is the smallest honest fix.
- LinkedIn: linkedin.com/in/raphaelmansuy
- Repository: github.com/raphaelmansuy/EdgeSQL
- Tutorial site: raphaelmansuy.github.io/EdgeSQL
- PyPI: pypi.org/project/edgesql
Connect with Raphael on LinkedIn.
Recognition
Favikon ranks Raphael at 12 in Top 20 AI Experts in France in 2026, with a Favikon Authority Score of 7,012 points. The list covers the most visible AI practitioners in France, and the entry cites his 20 years of data and AI leadership and his work on scalable data platforms.
Book
The Definitive Guide to Data Integration (Packt, 2024). Subtitle: Unlock the power of data integration to efficiently manage, transform, and analyze data. Raphael is a co-author with Pierre-Yves Bonnefoy, Emeric Chaize, Mehdi Tazi, and Stephane Heckel. The book is available through O'Reilly.
Other projects by Raphael
|
EdgeQuake - Graph-RAG in Rust: one binary, one Postgres, any LLM. It reads PDFs and text, builds a knowledge graph in PostgreSQL, and answers questions with sources. Six retrieval modes: naive, local, global, hybrid, mix, and bypass. Apache 2.0. |
|
|
EdgeCrab - a personal and coding SuperAgent in
one native Rust binary, with a ratatui terminal UI and a ReAct tool loop. It
supports 24 LLM providers and 17 messaging gateways. Install with
|
|
|
EdgeWhisper - voice dictation for macOS, powered by Voxtral from Mistral AI. It runs fully on the device, with no cloud and no data collection. It supports 13 languages, an Apple Silicon Mac, and macOS 14 or later. |
License
Copyright 2026 Raphael Mansuy.
Licensed under the Apache License, Version 2.0. See LICENSE for the full text and NOTICE for the attribution of third-party components.
SQLGlot supplies the parser and the printer and keeps its own MIT license. The Postgres regression fixtures keep the PostgreSQL license, and the Databricks examples come from the Databricks documentation.
Metadata
Release files for edgesql 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| edgesql-0.1.0.tar.gz | 60.2 kB | Without provenance | Details for edgesql-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| edgesql-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for edgesql-0.1.0-py3-none-any.whl |
Total release size: 101.2 kB
Release files / edgesql-0.1.0.tar.gz
| Download URL | edgesql-0.1.0.tar.gz |
|---|---|
| Size | 60.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f6f9b4881eefab1a672162dd28468a6ccdfd5d18acb3ac00283c4cb149019c63
|
|
BLAKE2b-256 checksum How to use checksums |
10999a39cf9c03dae034b7df468c61d5e6d7ff6d7eaab193150c8efbb7ddd77c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / edgesql-0.1.0-py3-none-any.whl
| Download URL | edgesql-0.1.0-py3-none-any.whl |
|---|---|
| Size | 41.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
42d4b22b1b78b2a5b94be4b117914f254774e547fdc84334bda9886597f64645
|
|
BLAKE2b-256 checksum How to use checksums |
76ef17c0078e974eda4ec565548fe7d80e778c86c0ca6b3933cd5bf8333ad128
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Security for edgesql 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.