DuckDB (native Quack protocol)
Besides Arrow Flight SQL, the manager serves DuckDB's own Quack protocol on a dedicated
listener (default port 9494). Any DuckDB that carries the quack extension, the CLI, the
Python package, an embedded DuckDB, attaches the gateway as a database with no driver in
between, and can join the attached catalog with its local tables in one query.
ATTACH 'quack:qod.example.com:9494' AS qod
(TYPE quack, TOKEN 'tenant=acme&pool=bi&user=alice&password=demo-alice');
SELECT s.label, count(*) AS customers
FROM qod.tpch1.customer c JOIN my_local_segments s ON c.c_mktsegment = s.segment
GROUP BY 1 ORDER BY 2 DESC;
-- one shot, no ATTACH
SELECT * FROM quack_query('quack:qod.example.com:9494', 'SELECT count(*) FROM tpch1.orders',
token := 'tenant=acme&pool=bi&user=alice&password=demo-alice');
What applies is exactly what applies to a FlightSQL client: the same handshake gates (tenant
scope, pool grant), per-statement routing across the pool, the
ACL, column masking and row filters, metadata filtering, lockdown,
author stamping, statement history, the audit log (origin
quack), kill, and scale-to-zero wake-up. The manager relays each statement to a node and
hands the node's result back to the client byte for byte; it never re-encodes data.
The token
The Quack wire carries one opaque token string and no user identity. QoD defines the token as
the same parameters a FlightSQL JDBC URL takes after ?, percent-encoded:
| Form | Token |
|---|---|
| Password | tenant=<tenant>&pool=<pool>&user=<user>&password=<password> |
| OIDC bearer | tenant=<tenant>&pool=<pool>&token=<jwt> |
| Superuser (system realm) | append &superuser=true |
tenant accepts the display name or the surrogate id (t-<8 hex>), like the Flight header.
A password containing &, = or % is percent-encoded. Personal access tokens are not
accepted on this wire (the same rule as FlightSQL); use a user and password or an OIDC bearer.
The token can be passed three ways, all DuckDB-native:
-- inline on ATTACH
ATTACH 'quack:host:9494' AS qod (TYPE quack, TOKEN '...');
-- inline on quack_query
SELECT * FROM quack_query('quack:host:9494', 'SELECT 1', token := '...');
-- through a DuckDB secret scoped to the endpoint (the ATTACH then needs no TOKEN)
CREATE SECRET qod (TYPE quack, TOKEN '...', SCOPE 'quack:host:9494');
ATTACH 'quack:host:9494' AS qod (TYPE quack);
A malformed token or a bad password answers Authentication failed: ... at ATTACH time; a
valid principal with no grant on the pool answers permission denied: ....
TLS and remote hosts
The DuckDB client speaks plain HTTP to localhost / 127.0.0.1 / ::1 and TLS to every
other host, and cannot be told to use TLS on a loopback host. The listener is therefore plain
HTTP by default (QOD_QUACK_TLS_ENABLED=false). Before exposing it beyond the host, either:
- set
QOD_QUACK_TLS_ENABLED=true(the FlightSQL edge's certificate pair is reused by default; the DuckDB client does not verify self-signed certificates unless told to), or - terminate TLS in front of the port, as you would for the REST API.
A remote client talking to a plain-HTTP listener adds DISABLE_SSL true to the ATTACH
options (disable_ssl := true on quack_query). See TLS for the knobs.
What the client sees
- Statements route per statement. As on the Flight edge, each statement lands on a node
picked by the router (reads on readers, writes on writers). Session state outside an explicit
transaction (
SET, temp tables) does not carry over between statements. - Transactions.
BEGIN; ...; COMMITfrom a DuckDB client runs on one node connection, so a client transaction is a real transaction on one node (the Flight edge only pins the node; see Sessions and transactions). - Writes.
INSERT INTO qod.<schema>.<table> ...from local data is authorized as a write on that table before any row reaches a node, and is author-stamped like a FlightSQL write. - Denials answer inside DuckDB's error text with the manager's own wording:
access denied: no grant on .... - Catalog browsing. The client bootstraps its catalog view with
information_schemaandduckdb_tables()queries; with filtered metadata on, a user only sees the objects they hold a grant on.
Operations
- Configuration: the
quack-nativeblock (QOD_QUACK_ENABLED,QOD_QUACK_HOST,QOD_QUACK_PORT,QOD_QUACK_TLS_ENABLED,QOD_QUACK_TLS_CERT_CHAIN,QOD_QUACK_TLS_PRIVATE_KEY,QOD_QUACK_MAX_HEARTBEAT_SEC,QOD_QUACK_MAX_BODY_BYTES); see the configuration reference. Session TTL and the scale-to-zero resume hold reusequack-flightsql.sessionTtlSecandresumeHoldTimeoutSec. - Ports: Docker exposes
9494(QUACK_PORTin the compose file); the Helm chart adds a<release>-quackService (service.quack.*,quack.enabled,quack.tls.enabled). - HA: Quack sessions are per manager replica, like FlightSQL sessions. Under HA, put a
session-sticky balancer in front of the port; a client whose next request lands on another
replica gets
Invalid connection idand must re-attach. - The boot banner,
qod serveand the pool page print the ready-to-pasteATTACHstring.
Known client limitation
The DuckDB 1.5.4 quack extension (40de7ba) fails on a multi-column result larger than its
inline batch (about 24k rows) with Attempted to access index 1 within vector of size 1. This
happens against a raw quack_serve node too, so the gateway cannot mask it. Single-column
results of any size, and multi-column results that fit inline, work.