Skip to main content

Demo bootstrap (LOAD_TPCH / LOAD_TPCDS)

LOAD_TPCH=N, LOAD_TPCDS=N, and LOAD_SSB=N turn a fresh install into a self-contained, fully populated multi-tenant demo. One command, the tenants you ask for, real datasets, a complete RBAC graph, and a cross-tenant federated catalog, all reproducible and all live against a real Postgres + DuckDB stack.

# native, both TPC benchmarks at SF=1
NUKE=1 LOAD_TPCH=1 LOAD_TPCDS=1 qod start

# native, TPC-H only (fast path, ~10 s)
NUKE=1 LOAD_TPCH=1 qod start

# native, TPC-H + the SSB star schema derived from it
NUKE=1 LOAD_TPCH=1 LOAD_SSB=1 qod start

# docker compose, TPC-DS only at SF=10
QOD_VERSION=BUILD NUKE=1 LOAD_TPCDS=10 ./scripts/run-docker-compose.sh

# kubernetes (local stack), both TPC benchmarks at independent scale factors
NUKE=1 LOAD_TPCH=1 LOAD_TPCDS=10 ./charts/quack-on-demand/local-stack-k8s/run-local-stack-k8s.sh

Each N is that benchmark's scale factor. SF=1 is the fast path (~10 s of TPC-H + ~30 s of TPC-DS); SF=10 is ~5-10 minutes; SF=100+ takes hours and spills to disk. The legacy LOAD_TPC=N env var still works as a shortcut for setting LOAD_TPCH=N, LOAD_TPCDS=N, and LOAD_SSB=N at the same SF; explicit per-benchmark vars override it.

Profiles: full and minimal

DEMO selects which bundled manifest a seed boot imports. DEMO=full (the default) is the two-tenant demo documented on the rest of this page: it exercises multi-tenancy, multiple pools, and the cross-tenant federated catalog. DEMO=minimal imports bootstrap-demo-minimal.yaml instead - the shape for running the gateway in front of a single DuckDB/DuckLake database: one tenant (acme), one pool (bi), and one dual node serving both reads and writes, plus the analyst RLS/CLS demo (roles analyst and tenant_admin, group analysts, users root, admin, alice, and acme-admin; the analyst role masks customer.c_phone and restricts customer rows to c_mktsegment = 'BUILDING').

The profile is only consulted when a LOAD_* flag is set and QOD_BOOTSTRAP_YAML is unset. Bootstrap only imports into a fresh control plane, so switch profiles with NUKE=1:

NUKE=1 DEMO=minimal LOAD_TPCH=1 qod start

DEMO=minimal plus LOAD_TPCDS warns and skips the TPC-DS loader (this profile has no globex tenant).

What gets created

ComponentWhere it comes from
Bundled manifest at src/main/resources/bootstrap-demo.yamlLoaded into the JVM classpath; imported on boot via DemoBootstrapHook when QOD_BOOTSTRAP_YAML=classpath:bootstrap-demo.yaml is set (the launcher script sets this automatically whenever any seed flag is non-empty).
acme_tpch Postgres database, seeded with TPC-Hscripts/load-tpch-dbgen.sh forked by the launcher; runs DuckDB's dbgen(sf=N) and copies the 8 TPC-H tables into the DuckLake catalog.
globex_tpcds Postgres database, seeded with TPC-DSscripts/load-tpcds-dbgen.sh forked by the launcher; runs DuckDB's dsdgen(sf=N) and copies the 24 TPC-DS tables.
Schema ssb1 in acme_tpch, seeded with the SSB star schema (LOAD_SSB=N)scripts/load-ssb-dbgen.sh forked by the launcher; runs DuckDB's dbgen(sf=N) and derives the 5 SSB tables (lineorder, customer, supplier, part, dwdate) per the SSB spec's TPC-H mapping. No extra tenant or pool: the acme pools serve it, and the demo tenant_admin grant (*.*.* ALL) covers it.
Two tenants, three pools, six quack nodes, an RBAC graph, a federated catalogThe bootstrap manifest is applied to the control plane, then PoolSupervisor.reconcile spawns nodes from each pool's role distribution.

Tenants and tenant-dbs

TenantTenant-dbDefault schemaPools
acmeacme_tpch (DuckLake on Postgres)tpch1bi (1 read-only + 1 dual), etl (1 write-only + 1 dual)
globexglobex_tpcds (DuckLake on Postgres)tpcds1bi (1 read-only + 1 dual)

acme carries the TPC-H tables (customer, lineitem, nation, orders, part, partsupp, region, supplier), plus, with LOAD_SSB, the SSB star schema in the sibling schema ssb1 (lineorder, customer, supplier, part, dwdate; query them as ssb1.<table> since the pools default to tpch1). globex carries the TPC-DS tables (store_sales, web_sales, catalog_sales, customer, customer_demographics, and 19 others).

Roles and verb-matrix

The four verb classes are exercised explicitly. See Access control model for what RO / RW / DDL / ALL cover.

acme

RoleGrantsDemonstrates
analystRO on customer, orders, nation, regionNarrow per-table read
etlRW on lineitem, RO on ordersRW includes read so a writer can also SELECT
dbaDDL on tpch1.*DDL alone, no data verbs
tenant_adminALL on *.*.*Wildcard, tenant-scoped (does NOT reach globex)

globex

RoleGrantsDemonstrates
analystRO on store_sales, customer, customer_demographicsSame pattern, different schema
etlRW on web_sales, RO on store_sales
cross_tenant_analystRO on acme_pg.tpch1.customer, RO on acme_pg.tpch1.ordersCross-tenant read via federation, explicit grants required
tenant_adminALL on *.*.*Wildcard, tenant-scoped

Groups

TenantGroupRoles
acmeanalystsanalyst
acmedata-engetl, dba (multi-role group)
globexanalystsanalyst

Users

All passwords are plaintext in the bundled manifest and are bcrypted by ManifestImporter at import time. Anyone reading the file in the jar sees the credentials, which is the point of a demo. The file is opt-in: nothing imports it unless LOAD_TPCH, LOAD_TPCDS, the legacy LOAD_TPC, or QOD_BOOTSTRAP_YAML is set.

UsernameTenantPasswordRole-grant pathPool grant
rootnone (superuser)demo-rootbypassn/a
aliceacmedemo-alicedirect: analystbi
bobacmedemo-bobgroup data-eng -> etl, dbaetl
daveacmedemo-davedirect: dbaetl
acme-adminacmedemo-acme-admindirect: tenant_adminbi, etl
carolglobexdemo-carolgroup analysts -> analyst AND direct: cross_tenant_analystbi
globex-adminglobexdemo-globex-admindirect: tenant_adminbi

Federation

globex_tpcds carries a federated source named acme_pg that ATTACHes the acme_tpch Postgres database read-only. The connection secrets pull from the manager's existing QOD_PG_* environment variables via externalRef: env:QOD_PG_HOST (etc.), so the demo works unchanged in both native (localhost) and docker (postgres service) modes without a YAML edit.

When a globex user runs SELECT * FROM acme_pg.tpch1.customer, the query is routed to a globex node which transparently forwards it to acme's Postgres via DuckDB's postgres extension. The result returns through the same FlightSQL session.

ACL is still enforced over federated tables: cross_tenant_analyst is the role that opens the door, and it grants RO only on customer and orders under acme_pg.tpch1. Other acme tables (lineitem, supplier, ...) remain unreachable from globex.

Walkthrough: see the ACL in action

The matrix below is also pinned as BootstrapDemoEffectiveSpec in the test suite, so the documented decisions and the bundled YAML cannot drift apart silently.

alice (acme analyst, RO on a few TPC-H tables)

StatementDecisionWhy
SELECT * FROM customerAllowedanalyst RO covers acme_tpch.tpch1.customer
INSERT INTO customer VALUES (...)DeniedRO does not cover Write
SELECT * FROM globex_tpcds.tpcds1.store_salesDeniedanalyst has no globex grant; * wildcard is tenant-scoped

bob (acme etl + dba via data-eng group)

StatementDecisionWhy
INSERT INTO lineitem VALUES (...)Allowedetl role grants RW on lineitem (via data-eng group)
CREATE TABLE x (id INT)Alloweddba role grants DDL on tpch1.* (via data-eng group)

dave (acme dba only)

StatementDecisionWhy
SELECT * FROM customerDenieddba role grants DDL only, no Read
CREATE TABLE foo (id INT)AllowedDDL covers Ddl access

acme-admin (acme tenant admin)

StatementDecisionWhy
SELECT * FROM customerAllowedtenant_admin *.*.* ALL covers everything in acme
SELECT * FROM globex_tpcds.tpcds1.store_salesDeniedcatalog * is tenant-scoped; does not reach a sibling tenant's catalog

carol (globex analyst + cross-tenant federation reader)

StatementDecisionWhy
SELECT * FROM store_salesAllowedanalyst via analysts group
SELECT * FROM acme_pg.tpch1.customerAllowedcross_tenant_analyst direct grant on the federated table
SELECT * FROM acme_pg.tpch1.lineitemDeniedno grant on lineitem under acme_pg

globex-admin (globex tenant admin)

StatementDecisionWhy
SELECT * FROM acme_pg.tpch1.customerDeniedtenantCatalogs enumerates tenant-dbs only, not federated aliases. *.*.* ALL does NOT reach acme_pg; an explicit named grant would be required.

root (superuser, no tenant)

StatementDecisionWhy
SELECT * FROM globex_tpcds.tpcds1.store_salesAllowedsuperuser bypass; no per-statement check

Logging in

Admin UI (http://localhost:20900/ui/)

  • Tenant ID blank, username root, password demo-root for the superuser console.
  • Tenant ID acme and any acme username/password for a tenant-scoped view of acme's roles, groups, users, and pool permissions.
  • Tenant ID globex for the globex view (which also surfaces the acme_pg federated source under globex_tpcds).

FlightSQL (grpc+tls://localhost:31338)

The standard FlightSQL JDBC URL form:

jdbc:arrow-flight-sql://localhost:31338
?useEncryption=true
&disableCertificateVerification=true
&tenant=acme
&pool=bi
&username=alice
&password=demo-alice

Replace tenant, pool, username, password with the personas from the table above. For the superuser, swap the tenant=... parameter for superuser=true.

What to try next

  • Walk through the Access control model with concrete tenants in front of you.
  • Add a new role to globex.cross_tenant_analyst via the Administering access page and watch carol's effective set widen on the next handshake.
  • Read Federation to see how acme_pg's setupSql is composed and how secrets are resolved.
  • Strip the demo out: run without any LOAD_TPC* var, get an empty manager, build your own tenants via REST/UI.

Cleanup

NUKE=1 (without any LOAD_TPC* var) tears down state and boots empty:

NUKE=1 qod start

It drops the control-plane database, the acme_tpch and globex_tpcds tenant-db databases, the certs, and the ducklake data directories. Run-as-you-like operator setups built via the REST API or UI on top of the demo will be wiped along with everything else.