The databases
A text means the same on every database. This chapter says how ButtercupQL’s compiler gives each database that meaning, which another implementation can do in its own way, and what each database can’t do, which every implementation refuses alike.
SQLite
- Placeholders are
?. /is writtenx * 1.0 / y, so that whole numbers divide exactly. SQLite’s builds may have nofloor()and nopower(), so//is written withCAST, and a whole power up to 16 is multiplied out. Any other power needs a build with SQLite’s math functions, as Python’ssqlite3has.- SQLite puts missing values first when sorting low to high, so
NULLS LASTis written there. - Dates and times are text,
'2026-10-20 14:30:00', worked on withdate(),datetime()andstrftime(); the columns ofdateanddatetimeareTEXT. - SQLite has no users, so
grantandrevokeare refused, and keeps no queries, so@storedis.
PostgreSQL
- Placeholders are
$1, or%sfor psycopg. - PostgreSQL puts missing values first when sorting high to low, so
DESC NULLS LASTis written there, and in an index’s keys. - A stored query
defis a SQL function,RETURNS TABLEwith its columns’ types, which the schema says, orRETURNS SETOFa table for its whole rows. A storeddefthat changes rows is a procedure, written withBEGIN ATOMIC, which needs PostgreSQL 14 or later. Its parameters are written with their types.
DuckDB
- Placeholders are
$1. /already divides exactly, and missing values already sort last.- A condition on a window value is written in
QUALIFY. - A stored query
defis a table macro. DuckDB keeps no procedures, so a storeddefthat changes rows is refused. - DuckDB has no users, so
grantandrevokeare refused. Its indexes take no condition and no direction, sowhere=on an index is refused, and its keys are written low to high. - DuckDB refuses some changes to tables that other tables’ keys point to, and says so as it runs them: renaming one
of their columns, making one never missing, and giving back the rows an
updatechanged.
ClickHouse
- Placeholders are named and typed,
{p1:String}, and their values are passed by name. - A total’s condition is written in ClickHouse’s own functions,
countIf()andsumIf(), and a total of no rows that is missing in Python,max(), withmaxIfOrNull(). - A query with a
left joinis givenSETTINGS join_use_nulls = 1, so that a row with no match has missing values, not empty ones. - ClickHouse reads a name as a column the query gives wherever the name is written alone, so where a
selectgives a column the name of one it reads, every column of the query is named with its table. - An
updateis a mutation,ALTER TABLE ... UPDATE, withmutations_sync = 1so that it is done before the statement returns. Adeletewith nowhereis writtenWHERE true. - A table is a
MergeTree, its rows kept in the order of its primary key, and a column that can be missing isNullable. ClickHouse keeps no unique keys and no references, so they are left out, with a warning. - A stored query
defis a view with parameters, which take no defaults: they are left out, with a warning. - Permissions are granted one table at a time, and
writeisINSERT,ALTER UPDATEandALTER DELETE. - ClickHouse runs no transactions, has no indexes like
create index, can’t give back the rows a change makes, and keeps every row of a recursive query, sowith transaction:,create index, aselectafter a change, and|in a recursive query are refused.