Qwiratry — SQL-AST-Comparison
SQL AST comparison: Red vs SQL::Abstract
Goal
The proposal is that we use a common SQL AST (Abstract Syntax Tree) that can be shared by:
Red
SQL::Abstract
Qwiratry
The hope is that, instead of having 2 or 3 incomplete SQL AST representations, we can have one that combines the best of all of them.
Update
It's likely that the sensible thing to do is to layer Qwiratry on top of SQL::Abstract, then layer Red on top of Qwiratry. So I'm going to retire this for now, but reference it in Post-SQL-TaOP-Improvements.md.
Overview
Research note for a combined SQL AST. Compares the two Raku approaches that build a query AST and then render/translate it to SQL:
Red (Red::AST + Red::Driver::*) | SQL::Abstract (zef:leont) | |
|---|---|---|
| Latest release | 0.2.4 — 2025-11-13 | 0.0.9 — 2024-04-28 |
| Role | ORM query/DDL pipeline that happens to emit SQL | Standalone AST + helpers + dialect renderers (+ builder API) |
| Public product | Models + ResultSeq (grep/map/…) | select/insert/update/delete/upsert from Raku data structures, or SQL::Abstract::Builder |
| Output | Driver runs SQL (optional debug of SQL/binds) | SQL::Query (.sql + .arguments) via renderer |
| DDL | Yes (tables, views, alter-ish) | No |
| Multi-dialect emit | CommonSQL + Pg + SQLite | Renderers: Postgres, SQLite, MySQL |
Release dates from raku.land badges pages (checked 2026-08-11).
Sources: Red docs, Red architecture, FCO/Red, SQL::Abstract / Leont/sql-abstract.
1. Verdict (feature range / options)
Most first-class DML / SELECT options: SQL::Abstract.It has a portable query AST plus CTEs (WITH), window definitions,
DISTINCT, HAVING, compound queries (UNION-family), row locking clauses,
upsert/ON CONFLICT, multi-row INSERT, INSERT…SELECT, UPDATE…FROM,
DELETE…USING, NATURAL/CROSS joins, rich Conditions coercions (ranges →
BETWEEN, junctions → IN, JSON ops, etc.), and separate Postgres / SQLite /
MySQL renderers (including ? vs $n placeholders on Postgres). ~3500-line
core module; designed to represent any SELECT/UPDATE/INSERT/DELETE valid in
both PostgreSQL and SQLite.
Widest statement kinds including schema/DDL: Red.Beyond DML it builds CREATE TABLE/VIEW, column alter/FK AST, transactions,
set ops, and ORM-tied prefetch/locking, with driver translate and an
actively released codebase (newer of the two). It is not a free-form SQL
builder: you go through models/ResultSeq. Notable DQL gap vs Abstract:
no HAVING on the Select AST.
| Question | Answer |
|---|---|
| Most options for ad-hoc DML SQL (no ORM)? | SQL::Abstract |
| Most options for schema + executed ORM queries? | Red |
2. The Layers
Overview
In the Proposal column, a - means that each framework does their own thing (or nothing, if preferred).
| Layer | Red | SQL::Abstract | Proposal |
|---|---|---|---|
| User-facing API | Models + ResultSeq (grep / map / sort / …) | Functional select/insert/… or SQL::Abstract::Builder chains | - |
| API -> AST | Callable/block evaluation and ResultSeq chaining grow a Red::AST tree | Helpers coerce Hashes/Lists/Pairs (and Builder methods) into Expression/Query nodes | - |
| AST | Red::AST::* (Select, Insert, Update, Delete, DDL, operators, …) | Expression / Query AST (Select, Update, ops, Conditions, Common/CTE, …) | Unified SQL AST, with the best of both |
| Rendering (AST -> Query Dialect) | Red::Driver.translate (CommonSQL, then Pg / SQLite overrides) | Renderer::Postgres / SQLite / MySQL | Shared Rendering Layer |
| Query text (eg. SQL) | SQL string + bind list (often only seen in debug / dry-run) | SQL::Query with .sql and .arguments | Shared Query Text |
| Execution call | Built in: driver prepare/execute | Not built in (caller) | - |
| Actual Execution | DBIish | DBIish, or Net::Postgres::Abstract | - |
Individual Layers
User-facing API
How application code asks for a query, before any SQL exists.
This layer is where the two projects feel most different. Red presents
database work as Raku collections: Person.^all.grep({ .age > 18 }).sort(*.name).
Models, traits (is column, relationships), and meta-methods (^create,
^load, ^create-table) are the product; SQL is incidental. SQL::Abstract
is deliberately SQL-shaped: you either pass tables/columns/conditions as Raku
data to select/insert/update/delete/upsert, or chain
SQL::Abstract::Builder (on → where → select → …). No models required.
API -> AST
How that request becomes typed AST nodes.
Red grows the tree by interpreting ResultSeq operations and (for grep /
map blocks) by evaluating callables with column proxies whose operators
build Red::AST nodes instead of Bools. That is powerful and Rakuish, but
historically heuristic; a RakuAST-based path is planned. SQL::Abstract
uses explicit coercions: Hashes/Lists/Pairs/Range/Junction become
Conditions, Column::List, joins, CTEs, etc. The Builder path constructs
the same nodes method-by-method. Abstract’s strength here is predictable
data→AST mapping (e.g. 1..10 → BETWEEN, 1|2|3 → IN) without running
user blocks.
AST
The dialect-neutral representation of the query (or DDL).
Both keep a real node tree rather than a bag of SQL strings. Red’s
Red::AST::* spans DML and schema/admin (CreateTable/View, column alter,
FK, transactions, MultiSelect union/intersect/minus, Case, JSON ops, …).
Select has filter/order/group/limit/offset/lock/prefetch, but no HAVING
node. SQL::Abstract’s Expression/Query AST is DML-only but denser for
modern SELECT: CTEs (Common), windows, DISTINCT, HAVING, compound
queries, locking, upsert/ON CONFLICT, UPDATE…FROM, DELETE…USING. For a
portable query IR checklist, Abstract’s node set is the richer reference;
for schema IR, only Red has first-class nodes.
Rendering (AST -> Query Dialect)
Turn the AST into dialect-specific SQL text and placeholders.
Red centralizes common SQL in Red::Driver::CommonSQL.translate, with
Pg / SQLite (and related) overrides for types, placeholders, RETURNING,
EXCEPT vs MINUS, JSON, and so on. Emit is coupled to “the current driver.”
SQL::Abstract picks a named renderer at construction
(:renderer<postgres|sqlite|mysql>), including Postgres $n vs DBI ?
placeholder styles. Abstract also lists MySQL as a first-class renderer;
Red’s MySQL story has historically been more WIP. Neither is a full SQL
standardizer — dialect quirks still live in this layer.
Query text (eg. SQL)
The value you can inspect or hand to a DB client: text + binds.
Red usually hides this: iteration triggers translate+execute; you see SQL
mainly via $*RED-DEBUG, dry-run, or driver internals. SQL::Abstract
stops at a SQL::Query (.sql, .arguments) — the natural hand-off for
any executor. That makes Abstract easier to reuse as a pure encoder;
Red’s product is “rows/objects,” with SQL as an internal side effect.
Execution call
Who triggers running the statement.
Red owns this: prepare, bind, execute, inflate model objects (and
ResultSeq laziness). SQL::Abstract does not; callers invoke a DB
client themselves (DBIish, Net::Postgres, or Net::Postgres::Abstract).
Actual Execution
The DB client that talks to the database.
Red uses DBIish under the hood. SQL::Abstract has no executor of its own; typical callers use DBIish or Net::Postgres::Abstract (Abstract
async Postgres client).
Per-model Comments
Red
model / ResultSeq methods → Red::AST tree → Red::Driver.translate → SQL + binds → executeUser-facing API is Rakuish (
Person.^all.grep({ .age > 18 }).sort(*.name)).~40 AST modules;
CommonSQLalone ~80translatemultis (~1000 lines), plus Pg/SQLite overrides.SQL is an ORM implementation detail.
SQL::Abstract
Raku Hash/List/Pair helpers → Expression/Query AST → Renderer::* → SQL::Query(.sql, .arguments)
# or:
SQL::Abstract::Builder.new(...).on(...).select(...).where(...).buildThree layers: AST, coercions from ordinary Raku data, dialect renderers.
Functional API (
$abstract.select(...)) and chainableBuilder.No ORM; no built-in execution (pair with DBIish /
Net::Postgres::Abstract, etc.).
3. AST object comparison
For each SQL construct, the concrete AST type (or role) each library uses. — = no dedicated node; behaviour may still exist via driver defaults, projection tricks, model metadata, or runtime Raku code.
Qwiratry / RakuAST / SQL mapping for the same rows lives in SQL-AST-Qwiratry-Mapping.md.
| Represented item | Red | SQL::Abstract |
|---|---|---|
| AST root | Red::AST (role) | SQL::Abstract::Expression / SQL::Abstract::Query roles |
| Clause chain bag | Red::AST::Chained (filter, order, group, limit, offset, table-list) | fields on each query class (via SQL::Abstract::Query::Common) |
| SELECT | Red::AST::Select | SQL::Abstract::Select |
| INSERT | Red::AST::Insert | SQL::Abstract::Insert::Values, SQL::Abstract::Insert::Select, SQL::Abstract::Insert::Defaults |
| UPDATE | Red::AST::Update | SQL::Abstract::Update |
| DELETE | Red::AST::Delete | SQL::Abstract::Delete |
| UPSERT / ON CONFLICT | — | SQL::Abstract::Conflicts, SQL::Abstract::Conflict::Update, SQL::Abstract::Conflict::Nothing, SQL::Abstract::Conflict::Target::* on SQL::Abstract::Insert |
| UNION / INTERSECT / EXCEPT | Red::AST::Union, Red::AST::Intersect, Red::AST::Minus (Red::AST::MultiSelect) | SQL::Abstract::Compound on SQL::Abstract::Select |
| VALUES (inline relation) | — | SQL::Abstract::Values |
| CREATE TABLE | Red::AST::CreateTable | — |
| CREATE VIEW | Red::AST::CreateView | — |
| ADD / CHANGE / DROP COLUMN | Red::AST::CreateColumn, Red::AST::ChangeColumn, Red::AST::DropColumn | — |
| ADD FOREIGN KEY | Red::AST::AddForeignKeyOnTable | — |
| BEGIN / COMMIT / ROLLBACK | Red::AST::BeginTransaction, Red::AST::CommitTransaction, Red::AST::RollbackTransaction | — |
| Column reference | Red::Column (also does Red::AST) | SQL::Abstract::Identifier |
| Table reference | Red::Model in Red::AST::Select.of / @.table-list | SQL::Abstract::Table::Simple, SQL::Abstract::Table::Renamed |
| Literal value | Red::AST::Value | SQL::Abstract::Literal |
| Bind placeholder | Red::AST::Value / unary·infix $.bind | SQL::Abstract::Placeholder |
| Function call | Red::AST::Function | SQL::Abstract::Function |
Column alias (AS) | projection via map / @.fields | SQL::Abstract::Expression::Renamed |
* in projection | model column expansion | SQL::Abstract::Star |
| WHERE | Red::AST on Red::AST::Select/Red::AST::Chained $.filter | SQL::Abstract::Conditions on SQL::Abstract::Select $.where |
| AND / OR | Red::AST::AND, Red::AST::OR | SQL::Abstract::Op::And, SQL::Abstract::Op::Or (in SQL::Abstract::Conditions) |
| NOT | Red::AST::Not | SQL::Abstract::Op::Not |
Comparisons (=, <, …) | Red::AST::Eq, Red::AST::Lt, Red::AST::Gt, Red::AST::Le, Red::AST::Ge, Red::AST::Ne | SQL::Abstract::Op::Equals, SQL::Abstract::Op::LessThan, SQL::Abstract::Op::GreaterThan, … |
| LIKE | Red::AST::Like | SQL::Abstract::Op::Like, SQL::Abstract::Op::NotLike |
| IN (value list) | Red::AST::In | SQL::Abstract::Op::In::List |
| IN / EXISTS (subquery) | Red::AST::In + nested Red::AST::Select | SQL::Abstract::Op::In::Query, SQL::Abstract::Op::Exists |
| BETWEEN | Red::AST::Between | SQL::Abstract::Op::Between, SQL::Abstract::Op::NotBetween |
| IS NULL / definedness | Red::AST::IsDefined | SQL::Abstract::Op::IsNull |
| CASE | Red::AST::Case | SQL::Abstract::Op::Case |
| CAST | Red::AST::Cast | SQL::Abstract::Op::Cast |
| JSON path ops | Red::AST::JsonItem, Red::AST::JsonRemoveItem | SQL::Abstract::Op::JSON::GetElem, SQL::Abstract::Op::JSON::GetElemText, SQL::Abstract::Op::JSON::GetPath, SQL::Abstract::Op::JSON::GetPathText |
| XML path ops | — | — |
| Arithmetic / concat | Red::AST::Sum, Red::AST::Sub, Red::AST::Mul, Red::AST::Div, Red::AST::Concat, … | SQL::Abstract::Op::Plus, SQL::Abstract::Op::Minus, SQL::Abstract::Op::Times, SQL::Abstract::Op::Concat, … |
| Generic operator | Red::AST::Generic::Infix, Red::AST::Generic::Prefix, Red::AST::Generic::Postfix | typed SQL::Abstract::Op::* hierarchy |
| FROM / joined tables | @.table-list + primary $.of (Red::Model) | SQL::Abstract::Source (SQL::Abstract::Table, SQL::Abstract::Join, …) |
| JOIN … ON | join metadata on joined Red::Model entries | SQL::Abstract::Join::On |
| JOIN … USING | — | SQL::Abstract::Join::Using |
| NATURAL / CROSS JOIN | — | SQL::Abstract::Join::Natural, SQL::Abstract::Join::Cross |
| Subquery in FROM | Red::AST::Select (:sub-select) | SQL::Abstract::Source::Query |
| GROUP BY | @.group on Red::AST::Select/Red::AST::Chained | SQL::Abstract::GroupBy |
| HAVING | — | SQL::Abstract::Conditions on SQL::Abstract::Select $.having |
| DISTINCT | Red::AST::Function (func<DISTINCT>) in projection | SQL::Abstract::Distinction, SQL::Abstract::Distinction::Columns on SQL::Abstract::Select |
| ORDER BY | @.order (Red::AST / Red::Column) | SQL::Abstract::OrderBy, SQL::Abstract::Order::Modifier |
| LIMIT / OFFSET | Int on Red::AST::Select/Red::AST::Chained | SQL::Abstract::Limit, SQL::Abstract::Offset |
CTE (WITH) | — | SQL::Abstract::Common (+ SQL::Abstract::Query::Common $.common) |
| Window definition / use | — | SQL::Abstract::Window::Clauses, SQL::Abstract::Window::Definition, SQL::Abstract::Window::Function |
Row locking (FOR UPDATE, …) | Red::LockType on Red::AST::Select $.for | SQL::Abstract::Locking, SQL::Abstract::Locking::Clause |
| RETURNING | driver emit context (e.g. "delete-returning") | SQL::Abstract::Column::List on DML queries |
| String functions | Red::AST::Substring, Red::AST::Index, Red::AST::Lowercase, Red::AST::Uppercase | SQL::Abstract::Function |
| Date/time functions | Red::AST::DateTimePart, Red::AST::DateTimeCoerce, Red::AST::DateTimeNow, Red::AST::DateTimeToday | SQL::Abstract::Function, SQL::Abstract::Current |
| PK / table constraints | Red::AST::Pk, Red::AST::Constraint | — |
| SQL comments | Red::AST::Comment, Red::AST::TableComment | — |
| Custom / escape SQL | — | SQL::Abstract::Expression::Custom |
| Map / transform rows | via map / Red::ResultSeq | — |
| Aggregate / fold | via classify / aggregates | — |
| Projection (column pick) | @.fields / map | SQL::Abstract::Column::List |
| Equijoin / outer / semijoin / antijoin | via model join metadata | SQL::Abstract::Join::* variants |
Red spreads SELECT clauses across Red::AST::Select and a linked
Red::AST::Chained bag on Red::ResultSeq; Abstract keeps them as typed
fields on SQL::Abstract::Select (and siblings). Red joins are not separate
AST nodes — joined tables appear as Red::Model-shaped entries in
@.table-list with join type/on supplied via model metaclass. Abstract models
joins explicitly as SQL::Abstract::Join::* implementing
SQL::Abstract::Source.
Type-name prefixes in the table: Red::* and SQL::Abstract::* (nested in
the SQL::Abstract unit class).
4. Statement coverage matrix
Legend: Yes = first-class; Partial = limited / dialect-only / escape-hatch; No = not in API/AST without raw/custom SQL.
| Capability | Red | SQL::Abstract |
|---|---|---|
| SELECT | Yes | Yes |
Columns / aliases / * | Yes (map / model cols) | Yes (Column::List, pairs AS, *) |
| WHERE | Yes | Yes (rich Conditions) |
| JOIN INNER/LEFT/RIGHT/FULL | Yes | Yes |
| NATURAL / CROSS JOIN | Partial / uncommon | Yes |
| JOIN of subquery | Partial | Yes |
| GROUP BY | Yes | Yes |
| HAVING | No | Yes |
| DISTINCT | Partial | Yes (full / columns) |
| ORDER BY / ASC·DESC | Yes | Yes (+ NULLS ordering) |
| LIMIT / OFFSET | Yes | Yes |
| Subselect in FROM | Yes | Yes |
| Subselect / EXISTS / IN (query) | Yes | Yes |
| UNION / INTERSECT / EXCEPT | Yes | Yes (:compound) |
| FOR UPDATE / locking | Yes (LockType) | Yes (Locking) |
Window functions / WINDOW | No | Yes |
CTEs (WITH) | No | Yes (Common / Builder.with) |
| INSERT values | Yes | Yes (map + multi-row lists) |
| INSERT … SELECT | Partial | Yes |
| ON CONFLICT / upsert | No | Yes (upsert / conflicts) |
| UPDATE | Yes | Yes (+ optional FROM) |
| DELETE | Yes | Yes (+ optional USING) |
| RETURNING | Partial (often * on Pg) | Yes |
| CREATE TABLE / VIEW | Yes | No |
| ALTER TABLE (column/FK) | Yes | No |
| Transactions BEGIN/COMMIT/ROLLBACK | Yes | No |
| Dialect renderers | CommonSQL + Pg + SQLite | Postgres + SQLite + MySQL |
| Escape / custom SQL | Limited | Literals / captures / Expression::Custom |
| Pretty-print | Debug / comments | Renderer formatting |
| Clone / reuse | ResultSeq patterns | Builder + AST values |
5. What Red can build (detail)
5.1 DQL / DML
Filter/project/order/limit (
grep/map/sort/head/skip), aggregates,classify→ GROUP BY–style results, joins/join-model, prefetch,union/intersect/minus, ResultSeq delete/update,FOR UPDATE/SKIP LOCKED.
5.2 Expressions
Large Red::Operators + Red::ColumnMethods: comparisons, arithmetic,
AND/OR/NOT, LIKE helpers, IN, BETWEEN, CASE, casts, string/datetime funcs,
JSON -> / remove, generic ops, Red::AST::Function.
5.3 DDL / schema
CreateTable, CreateView, column create/change/drop, FK add, constraints,
comments, transactions; schema readers on drivers.
5.4 Gaps / coupling
Model-tied; no HAVING; coarse RETURNING; no CTEs/windows/upsert as AST; block→AST historically heuristic (RakuAST planned).
6. What SQL::Abstract can build (detail)
6.1 Functional + builder APIs
$abstract.select($from, $columns, $where, :$common, :$distinct, :$group-by, :$having, :$windows, :$compound, :$order-by, :$limit, :$offset, :$locking)$abstract.insert(map, multi-row list, or select),update,delete,upsert.SQL::Abstract::Builder:on/with(CTEs) /join/select/insert/update/delete/upsert/ clause methods includingdistinct,windows,compound,locking,returning.
Stated goal: any SELECT/UPDATE/INSERT/DELETE valid in both PostgreSQL and SQLite (portable core); MySQL renderer also present.
6.2 Conditions & expressions
Coercions from hashes/lists/pairs:
Operators (
=, comparisons,like/not-like, JSON->/->>/#>/#>>, arithmetic, bitwise,IS DISTINCT FROM, …).Nil/undefined →IS NULL; Ranges →BETWEEN; Junctions →IN; nestedand/or; subquery EXISTS/IN; CASE; CAST; functions; row lists.
6.3 Advanced SELECT / DML
CTEs (
Common), window frames/clauses, compound queries, locking clauses, DISTINCT ON–style column distinction where represented.Upsert /
ON CONFLICTtargets and update actions.Multi-row INSERT; INSERT…SELECT; UPDATE FROM; DELETE USING; RETURNING.
Join types including NATURAL and CROSS; subquery / function sources (incl.
WITH ORDINALITYrendering paths).
6.4 Gaps
No DDL (CREATE/ALTER TABLE, etc.) — Red still unique here among AST builders.
No ORM models or automatic schema mapping.
Version still
0.0.x; last raku.land release April 2024 (older than Red). Large feature surface in one module; fewer public tutorials than Red.
7. Options count (rough)
| Axis | Red | SQL::Abstract |
|---|---|---|
| Top-level families | DML + MultiSelect + DDL + Txn | Select/Insert/Update/Delete + Upsert (+ Builder) |
| First-class SELECT extras | FOR lock, set ops | HAVING, DISTINCT, CTE, WINDOW, compound, locking |
| Dialects | CommonSQL, Pg, SQLite | Postgres, SQLite, MySQL |
| Expression surface | Very large (ORM ops) | Very large (AST ops + coercions) |
| Escape hatch | Limited | Literals / custom expressions |
| Approx. AST/render code size | Large (AST + drivers + ORM) | ~3.5k LOC Abstract + small Query/Placeholders |
Most DML options: SQL::Abstract.Most overall SQL kinds (incl. DDL): Red.
8. Excluded SQL builders
These generate or run SQL but are out of scope here because they do not use an AST → render/translate pipeline (the criterion for this note).
| Module | Why excluded (no AST → SQL) |
|---|---|
SQL::Builder (zef:avuserow) | Chainable builder assembles typed fragments (Identifier, Placeholder, Raw, Fn) straight into SQL strings + binds — no query AST and no dialect renderer layer. Companion: SQL::Builder::ExecuteWithDBIish. |
| ORM::ActiveRecord | Structured Query clause bags (hashes/lists of where/order/joins/…) then adapter string builders (to-sql / SqlBuilders). Intermediate state, not an expression AST. |
| DB::Xoos (+ Pg/SQLite/MySQL/Oracle) | Filters/options → string concat in DB::Xoos::SQL (sql-select / sql-insert / …). |
| DB::ORM::Quicky | Search hashes → SQL strings via !processtosql (SELECT/INSERT/UPDATE/DELETE). |
Also not AST builders (hand-written or non-generative SQL): Badger (.sql
files → Raku subs), Slang::SQL (embed SQL in Raku), SQL::Lexer
(lexical grammar only). Marrow reads schemas and generates Cro apps; it
does not emit query SQL. Net::Postgres::Abstract wraps SQL::Abstract
rather than defining its own AST.
9. Source pointers
| Area | Path / URL |
|---|---|
| Red Select AST | Red/lib/Red/AST/Select.rakumod |
| Red SQL emit | Red/lib/Red/Driver/CommonSQL.rakumod |
| Red ResultSeq | Red/lib/Red/ResultSeq.rakumod |
| Red architecture | https://fco.github.io/Red/tutorials/architecture.html |
| SQL::Abstract | lib/SQL/Abstract.rakumod / https://raku.land/zef:leont/SQL::Abstract |
| Red releases | https://raku.land/zef:FCO/Red/badges |
| Abstract releases | https://raku.land/zef:leont/SQL::Abstract/badges |
Generated from upstream sources and raku.land release metadata checked 2026-08-11.