Repository navigation
Exec routes: run the request's SQL directly (CREATE ROUTE … AS $sql) - #48
Merged
Merged
Conversation
A route whose handler is exactly one parameter, e.g. CREATE ROUTE sql POST '/sql' AS $sql; is an exec route. CREATE skips handler validation for it. At request time the parameter binds through the normal path (PARAM constraints such as MIN_LENGTH still apply), then its text is split with DuckDB's parser and every statement runs once, in order, on the request connection: DDL, DML, PRAGMA, SET, INSTALL/LOAD, COPY. The LAST statement's result is the response through the existing format negotiation (JSON, CSV, NDJSON, Arrow, route FORMAT, html/text columns); a final no-column statement answers []. The first failing statement stops the batch and answers 422 with the FastAPI-shaped detail on the parameter's loc. REQUIRE runs before any request SQL, query_timeout_ms bounds the batch (504), and a bound row-access/masking policy makes the route fail closed (403). This lets a deployment serve /sql without looping back through quack_query (a second connection per request). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DuckDB's statement extraction truncates at the first null byte, so a batch like 'SELECT 111 AS first<NUL>garbage' executed the prefix and silently dropped the tail while returning 200. Null bytes are never legitimate in request SQL text; fail fast with a 422 naming the parameter instead.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A route whose handler is exactly one parameter is an exec route:
quackapi recognises the single-
$paramhandler, skips CREATE-time validation for it, and at request time executes the bound text as SQL on the request's connection. This lets a deployment serve/sqldirectly instead of looping back throughquack_query('quack:localhost:…', $sql)(a second Quack connection per request).Design
AS $<name>— the handler, trimmed (trailing;stripped), is one$identifierand nothing else.$claims_*never qualifies (server-bound). Anything else is an ordinary handler and is still prepared/validated at CREATE.SELECT $sql) so the parameter goes through the normal binding path: JSON body field, form field, query string,HEADER/COOKIE, andPARAM … MIN_LENGTH/DEFAULTall apply. The number-like-integer heuristic is skipped for the exec parameter (it is always VARCHAR SQL text).Connection::ExtractStatements(DuckDB's parser, incl. PRAGMA expansion) thenConnection::Query(statement)for each statement in order — DDL, DML, PRAGMA, SET, INSTALL/LOAD, COPY. Each statement runs once.Connection::Query(string), which answers with the first row-producing statement). It flows through the existing response path: JSON default,Accept: text/csv/ NDJSON / Arrow, routeFORMAT,html/textcolumns,max_response_bytes. A final statement with no columns (SET, call-PRAGMA) answers[]rather than an empty body.detail:loc= the parameter's location (["body","sql"],["query","q"]),type=value_error,msg= DuckDB's error, prefixedStatement i of n failed:for batches. Earlier statements are autocommitted, as in the DuckDB CLI.REQUIREauth runs before any request SQL;query_timeout_msbounds the whole batch (504). Row-access/masking policies cannot rewrite arbitrary SQL, so while any are bound the exec route fails closed (403, existing policy path). The pg_dsn native path and BODY TYPE rewrite are skipped for exec routes. OpenAPI documents the parameter and advertises no response columns.Tests
test/sql/quackapi_exec_route.test(81 assertions, all pass locally against the freshly built test binary):single SELECT; zero-row SELECT; multi-statement returns the last; DDL → DML → SELECT; TEMP table shared within a request and gone after; trailing SET →
[]; each statement once (INSERT side-effect counter + PRAGMA appears exactly once in DuckDB'sQueryLog); error mid-batch → 422 with loc/type,Statement 2 of 3 failed, statement 1 committed and statement 3 not run; parser error runs nothing; conversion error 422; empty SQL / missing field 422; CSV body + NDJSON via Accept;htmlcolumn mode;PARAM sql VARCHAR MIN_LENGTH422/200; GET query-string exec route;REQUIRE401 runs no SQL / 200 with key; row-access policy 403 then 200 after unbind;query_timeout_ms504 on a batch; OpenAPI lists the route.Real HTTP (
test/conformance, 7 new cases): JSON, last-statement batch, CSV, Arrow (Accept: application/vnd.apache.arrow.stream, via community nanoarrow), 422 error shape, auth 401/200. Locally: 92 PASS / 0 FAIL.Full
test/sql/quackapi*: all pass (3308 assertions, 2 skipped).make format-ci-checkclean.Not in this PR
Switching dev's
/sqlroute fromquack_querytoAS $sqlhappens after merge + release.🤖 Generated with Claude Code