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.
The public surface, the product IR, the error catalog, and the
dialect rules. Public names are Query, Filter,
FilterSet, Preview, AuditRecord,
SqlCraftError, col, select, and
param.
| Call | Result |
|---|---|
Query.parse(sql, *, dialect, schema=None, mode="library") | A Query, or a SqlCraftError |
query.to_sql(dialect=None, pretty=False) | SQL text. A parsed query keeps its dialect. |
query.to_tree() | The product IR document |
Parse runs the gates in order and stops at the first miss: known dialect,
non-empty text, one parse result, exactly one statement, a SELECT or
WITH root, the dialect rules, column resolution, then the optional
pglast check on Postgres.
| Call | Result |
|---|---|
query.tables() | Physical tables in scope order. CTE names are excluded. |
query.ctes() | CTE aliases |
query.columns() | ColumnRef items: table, name, via, tags |
query.filters() | A FilterSet. Each item has scope, clause, op, columns, values, negated, sql, and path. |
query.filters().where(column=, table=, scope=, clause=, op=) | A filtered FilterSet |
query.filters().to_dicts() | JSON-ready dictionaries |
query.params() | [{"param": name}] in source order |
A filter reports one conjunct. An OR group is one item. The
via list records each CTE or derived table on the path to the
physical table.
Each call returns a new Query and leaves the original unchanged.
| Call | Effect |
|---|---|
cte(name) | Focus later verbs inside cte:name |
rename_column(old, new, *, table, keep_output_names=True) | Rename a column. A search transform: zero matches blocks the preview. |
and_where(predicate, *, scope=None) | Add a conjunct |
where(predicate) | Alias for and_where |
limit(value) | Set LIMIT |
remove_filter(filter) | Remove one top-level conjunct |
replace_filter(filter, predicate) | Replace one top-level conjunct |
with_cte(name, query) | Add a CTE. A duplicate name raises DUPLICATE_CTE. |
rename_cte(old, new) | Rename the definition and every reference |
drop_cte(name) | Remove an unreferenced CTE. A live reference raises CTE_IN_USE. |
set_params(patches) | Lane A value patches |
inline_cte(name), rewrite(fn) | Raise NOT_IN_V1 |
migrate(target) | A blocked preview with reason MIGRATE_STUB |
op | params | Node kind |
|---|---|---|
set_comparison | value | Comparison |
set_between | low, high | Between |
set_in_list | values | InList |
set_limit | value | Limit |
A patch that sends left, op, column, or
children raises STRUCTURE_PATCH. An unknown node id
raises UNKNOWN_NODE.
| Member | Content |
|---|---|
before_sql, after_sql | Pretty SQL in the parse dialect |
ast_diff | The sqlglot.diff edit script |
semantic_diff | tables, filters_added, filters_removed, joins_changed, projection_changed, limit_changed |
blocked, blocked_reasons | Blocked reasons: NO_MATCH, REPARSE_FAILED, TABLE_SET_CHANGED, DISALLOWED_STATEMENT, MIGRATE_STUB |
collapsed | True when the pretty SQL did not change |
fingerprint | First 16 hex characters of SHA-256 of canonical SQL |
apply(actor=) | A new Query, or BLOCKED. Sets last_audit and query.audit. |
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")) # ... amount > %(floor)s LIMIT 100
print(query.to_sql("databricks")) # ... amount > :floor LIMIT 100
col(name) offers eq, ne,
gt, gte, lt, and lte.
param(name) makes a marker. select(table) starts a
SELECT *. A built query has no parse dialect, so both dialects are
available.
schemaVersion is "0.1". The document has
schemaVersion, dialect, rootId, and a flat
nodes map. Every node has kind, children,
and editable. An editable node adds
editMode: "param_value".
{
"schemaVersion": "0.1",
"dialect": "postgres",
"rootId": "n_select",
"nodes": {
"n_select": {"kind": "Select", "children": ["n_from", "n_where"], "editable": false},
"n_from": {"kind": "From", "table": "sales.orders", "children": [], "editable": false},
"n_where": {"kind": "Where", "children": ["n_cmp_cust_eq"], "editable": false},
"n_cmp_cust_eq": {
"kind": "Comparison",
"editable": true,
"editMode": "param_value",
"children": [],
"op": "=",
"left": {"column": "cust_id"},
"right": {"value": 42}
}
}
}
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 a column or a function is read-only.
SqlCraftError carries phase, code,
hint, line, col, and
track_id (12 lowercase hex characters). display()
returns [phase/code] message, plus Hint: when a hint
exists. to_body() returns the HTTP object.
| 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 |
| apply | BLOCKED |
| render | CROSS_DIALECT |
| http | STALE_FINGERPRINT, DRAFT_NOT_FOUND, CSRF_REQUIRED, DIALECT_MISMATCH |
| Topic | Postgres | Databricks |
|---|---|---|
| Quotes | "name" | `name` |
| Markers | $1, %(name)s | :name, ? |
| QUALIFY | refused | accepted |
| SEMI and ANTI joins | refused | accepted |
| DISTINCT ON | accepted | refused |
| FETCH | accepted | refused |
| MAX RECURSION LEVEL | refused | kept |
| Recursive CTE | UNION or UNION ALL | UNION ALL only |
| Mixed markers | not applicable | MIXED_MARKERS |
| Route | Result |
|---|---|
GET / | The HTML screen, with a session cookie and a CSRF token |
GET /healthz | {"ok": true} |
GET /v1/examples | The example gallery |
POST /v1/queries | 201 with draft_id, dialect, and fingerprint |
GET /v1/queries/{id}/tree | The product IR |
PATCH /v1/queries/{id}/params | A patch, guarded by If-Match |
POST /v1/queries/{id}/preview | Before, after, diff, reasons, preview_fingerprint, and filter_values |
POST /v1/queries/{id}/apply | A new draft and the audit record |
POST /v1/queries/{id}/to_sql | SQL in the draft dialect |
| Situation | Status | Code |
|---|---|---|
| Created | 201 | none |
| Read, preview, or SQL | 200 | none |
| Bad SQL or bad patch | 422 | catalog code |
| Stale fingerprint | 409 | STALE_FINGERPRINT |
| Unknown draft | 404 | DRAFT_NOT_FOUND |
| Missing CSRF | 403 | CSRF_REQUIRED |
Copyright 2026 Raphael Mansuy. Licensed under the Apache License, Version 2.0. 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. See the notice for the full list.
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. The entry cites his 20 years of data and AI leadership, his work on scalable data platforms, and the startup studio he leads in Hong Kong.
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.
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.
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 npm install -g edgecrab-cli,
pip install edgecrab-cli, or cargo install edgecrab-cli.
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.
Building something with SQL? Tell Raphael about it.
Connect on LinkedIn