Skip to main content

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:

FormToken
Passwordtenant=<tenant>&pool=<pool>&user=<user>&password=<password>
OIDC bearertenant=<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; ...; COMMIT from 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_schema and duckdb_tables() queries; with filtered metadata on, a user only sees the objects they hold a grant on.

Operations

  • Configuration: the quack-native block (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 reuse quack-flightsql.sessionTtlSec and resumeHoldTimeoutSec.
  • Ports: Docker exposes 9494 (QUACK_PORT in the compose file); the Helm chart adds a <release>-quack Service (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 id and must re-attach.
  • The boot banner, qod serve and the pool page print the ready-to-paste ATTACH string.

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.