Attach from DuckDB
An n6k server publishes one or more catalogs. A DuckDB client attaches a
catalog by name and from then on treats it as an ordinary database: SHOW ALL TABLES, SELECT, JOIN, INSERT. Nothing about the query has to know the
table is remote.
Every example on this page runs against the demo server this site hosts, and the output shown is what it returns.
Install and load
The extension is unsigned, so the CLI has to be started with -unsigned:
-- launch the CLI as: duckdb -unsigned
INSTALL n6k FROM 'https://storage.googleapis.com/n6k-duckdb-release';
LOAD n6k;
Attach
CREATE SECRET n6kdemo (
TYPE n6k,
TOKEN 'demo-token',
SCOPE 'n6kd-demo-106595059298.us-central1.run.app'
);
ATTACH 'n6ks://n6kd-demo-106595059298.us-central1.run.app' AS demo (TYPE n6k);
TYPE n6k is required — DuckDB does not infer the storage type from the URL
scheme. n6k:// is plaintext, n6ks:// is TLS; the alias after AS is the
name the catalog gets locally and the catalog requested from the server.
With no port, the scheme's default applies — 443 for n6ks://, 80 for n6k://.
A server on any other port needs it spelled out, which for a local one is the
common case: n6k://localhost:8080.
Read
The catalog behaves like any other:
SHOW ALL TABLES;
| database | schema | name | column_names | column_types |
|---|---|---|---|---|
| demo | main | audit_log | [id, occurred_at, actor, action] | [INTEGER, TIMESTAMP, VARCHAR, VARCHAR] |
| demo | main | customers | [id, name, region, signed_up] | [INTEGER, VARCHAR, VARCHAR, DATE] |
| demo | main | orders | [id, customer_id, placed_at, amount, status] | [INTEGER, INTEGER, DATE, DECIMAL(10,2), VARCHAR] |
Types survive the wire — DECIMAL(10,2) arrives as DECIMAL(10,2), not as a
float. A join across two remote tables is one query:
SELECT c.region, count(*) AS orders, sum(o.amount) AS revenue
FROM demo.orders o
JOIN demo.customers c ON c.id = o.customer_id
GROUP BY 1 ORDER BY 3 DESC;
| region | orders | revenue |
|---|---|---|
| APAC | 50000 | 23009000.00 |
| LATAM | 50000 | 23003100.00 |
| NA | 50000 | 22996300.00 |
| EMEA | 50000 | 22989500.00 |
That scanned 200,000 rows. Only the columns the query names are fetched, and
=, !=, <, <=, >, >=, IN, IS NULL and IS NOT NULL predicates
are pushed to the server rather than filtered after transfer — so narrowing the
query narrows the traffic.
Join a remote table to a local file
This is the part that has no equivalent in a REST API. The attached catalog and
your own local data live in the same FROM clause:
SELECT r.rep, count(*) AS orders, sum(o.amount) AS revenue
FROM demo.orders o
JOIN demo.customers c ON c.id = o.customer_id
JOIN 'reps.parquet' r ON r.region = c.region -- a file on your laptop
GROUP BY 1 ORDER BY 3 DESC;
| rep | orders | revenue |
|---|---|---|
| Lin | 50000 | 23009000.00 |
| Sofia | 50000 | 23003100.00 |
| Ada | 50000 | 22996300.00 |
| Grace | 50000 | 22989500.00 |
The server never sees reps.parquet, and your laptop never downloads
demo.orders. DuckDB plans across both, pushes what it can to each side, and
joins the results locally.
Write
Tables the server marks writable accept ordinary DML:
UPDATE demo.orders SET status = 'refunded' WHERE id = 42;
SELECT id, status FROM demo.orders WHERE id = 42;
| id | status |
|---|---|
| 42 | refunded |
The write lands on the server, not in a local copy — detach, reattach from a
different machine, and it is still there. INSERT, DELETE, CREATE TABLE AS
and views work the same way.
Run SQL on the server
Sometimes you want the server to evaluate the statement rather than the client. Each attached catalog exposes:
| Function | What it does |
|---|---|
demo.query(sql) | Run a SELECT on the server, stream the result back |
demo.exec(sql) | Run a statement for its effect |
demo.func(args) | Call an RPC the server registered |
When it does not work
| What you did | What comes back |
|---|---|
| Attached with no token, to a server that requires one | Invalid Error: InvalidInputException: unauthorized |
Wrote to a table the server marks read | Binder Error: Table 'audit_log' is read-only |
n6k:// against a TLS-only host | Failed reading HTTP status line from ws://… — use n6ks:// |
| Reached for the server's filesystem | Permission Error: … file system operations are disabled by configuration |
The last two rows are the server's policy, not the client's: see permissions.