Datastores and database pools
Three things are kept separate, and a request touches all three:
| Thing | What it is | Where you declare it |
|---|---|---|
| Backend | A database driver: pool, SQL driver, transaction factory, query compiler, DDL dialect | Not declared; selected by type |
| Database pool | A physical connection endpoint for one tenant: host, port, database, credentials, sizing | hives.tenant[].dbpools.<name> |
| Datastore | A named logical binding that points at exactly one pool | hives.tenant[].datastores.<name> |
Your schema declares entities against a datastore name. At request time
data.svc resolves tenant → datastore → pool, builds a connection pool, and
injects it into an engine:
X-Customer / X-Product / X-Env / X-Tenant (the CEPT request headers) └── tenant config ├── datastores.main.dbpool: main └── dbpools.main → host:port/database, schema, isolation → backend type → dialect + compilerNothing here is built at boot, the pool, the schema, the hook chain, and the engine are constructed on the first request that names a given (tenant, datastore) pair, cached per CEPT, and torn down as one unit on config change or shutdown.
Declaring pools and datastores
Section titled “Declaring pools and datastores”Both blocks are maps keyed by name, under the tenant’s config node. The map
key is the identity. An inline name: field is overwritten by the key after
binding.
hives: tenant: - path: "" name: dev active: true domain: - localhost - 127.0.0.1 datastores: main: dbpool: main dbpools: main: type: postgres dbserver: localhost:5432 dbuser: erpadmin database: erpdb password: erpadmin schema: erptest # this tenant's physical PostgreSQL schema maxconnections: 10The datastore key main and the pool key main are independent names that
happen to match here. The binding is the dbpool: main line, and dbpool is a
child of the datastore, never a sibling of it.
Database pool keys
Section titled “Database pool keys”| Key | Type | Default | Notes |
|---|---|---|---|
type | string | postgres | Empty and postgresql both canonicalise to postgres. postgres, clickhouse, duckdb, sqlite and sqlite3 are served; anything else is refused |
dbserver | string | localhost:5432 | host:port. A bare host gets port 5432. A non-numeric port fails the build |
dbuser | string | — | Required unless dsn is set |
database | string | — | Required unless dsn is set. On duckdb and sqlite it is the FILE PATH, and it is still required: an unset one would open an in-memory database that loses everything on restart, so say :memory: when that is what you mean |
password | string | "" | A literal, or a key resolved through Vault |
schema | string | derived from the tenant key | The physical PostgreSQL schema this tenant’s objects live in. Absent → the segments of the tenant key below the prefix the pool serves, joined by _, or the tenant name when nothing remains |
serves | string | the config node the pool is declared at | The tenant-key prefix this pool serves. Stamped from the node’s path; write it only when a pool deliberately spans more than its node. A tenant outside the prefix is refused |
isolation | string | dedicated | dedicated or shared |
tlsconfig | string | driver default | disable, require, verify-ca, verify-full. Passed through as the driver’s sslmode |
maxconnections | int | 10 | Maximum open connections. Applied only when greater than zero |
connection_life_time | duration | unlimited | e.g. 30m. Maximum lifetime of a connection |
connect_timeout | duration or int | 5s | A bare integer is read as seconds |
dsn | string | "" | Explicit connection-string escape hatch |
password is resolved through the registered vault client before the pool is
built. When no vault client is registered, the local dev path, the value is
used as a literal password. The config shape is the same either way.
Several knobs exist on the runtime pool object but have no configuration key:
idle connections, idle time, health-check period, pool-level statement timeout,
and arbitrary driver options beyond sslmode. They stay at their zero value and
the driver default applies. Do not expect to tune them from YAML.
Datastore keys
Section titled “Datastore keys”| Key | Type | Required | Notes |
|---|---|---|---|
dbpool | string | yes | Name of the entry under dbpools. The sole binding to a physical pool |
remoteconnections | map | no | Non-database (HTTP-style) backends attached to this datastore |
remoteconnections.<name>.url | string | no | Remote endpoint URL |
remoteconnections.<name>.protocol | string | no | Remote endpoint protocol |
remoteconnections.<name>.authorization | string | no | Authorization header value sent to the remote endpoint |
The schema side of a datastore
Section titled “The schema side of a datastore”A schema file declares the datastores its entities target as a list, and names the one used when a request selects none:
default-datastore: maindatastores: - name: main version: "18" extensions: - pg_trgm - pgvector init: scripts: - path: sql/extensions.sql description: extensions and shared functions kind: ddl transactional: false| Key | Type | Notes |
|---|---|---|
name | string | Must match a key in the tenant’s datastores: map |
type | string | Optional dialect hint. Omit it: see below |
prefix | string | Put in front of every physical table name of this datastore, the engine’s own included: em1_ turns customer into em1_customer, enum_order_status into em1_enum_order_status and data_audit_events into em1_data_audit_events. The exceptions are the shared coordination tables — data_schema_upgrade, its step table and data_locks — which keep their names everywhere. A lower-case identifier fragment; the trailing underscore is yours to write. See Several products in one schema |
version | string | Backend version. Not enforced: see Versions |
extensions | list | Installed database extensions; registers their types and operators with the compiler |
init.scripts | list | Raw SQL run once per (tenant, datastore) |
encryption.keys | list | Where this datastore’s field-encryption keys come from: - {name: <key>, key: <source>}, one entry per key NAME — the name a field asks for with compliances: [{type: encrypted, key: <name>}]. A source beginning vault:// is read from the secret store under the name after the scheme; anything else is the key material. A vault path may carry {customer}, {env}, {product}, {tenant} or {cept}, which is what makes that key per tenant. Names are merged across every datastore of the composed schema and overridden by properties.encryption.keys; one name declared twice with different sources is refused. See Providing encryption keys |
database | string | Parses, never read |
pool | string | Parses, never read |
schemas | list | Parses, never read |
path | string | Parses, never read |
settings | map | Parses, never read |
logging | map | Parses, never read |
default-datastore is the fallback when a caller passes no datastore selector.
Requests select another with the datastore query parameter, see the
Data API HTTP reference. When a schema declares exactly one
datastore and no default-datastore, that one is used. If the requested name is
absent from the loaded schema the build fails with
schema has no datastore <name>, and the message enumerates the default and
every available name.
Init scripts
Section titled “Init scripts”init.scripts[] is raw SQL run once per (tenant, datastore) pair on the first
engine build, extensions, base functions, default triggers, type definitions.
Each entry takes path (required), description, kind (ddl or dml), and
transactional (default true; set it false for statements PostgreSQL refuses to
run inside a transaction).
The runner records applied checksums in a ledger and skips any script whose
content checksum is already there, so a new process boot applies only
newly-added scripts. Script bodies must be idempotent on their own,
CREATE EXTENSION IF NOT EXISTS, CREATE OR REPLACE FUNCTION.
This is also where a CHECK constraint goes. The schema language does not
model them and will not: there is no checks: key. A deployment that needs one
writes the ALTER TABLE ... ADD CONSTRAINT ... CHECK (...) here.
The schema diff ignores check constraints entirely, which is the point — a constraint added this way is deliberate, so it is not reported as drift on every later plan. The trade is the other side of that: nothing verifies it is still there, and nothing recreates it if someone drops it.
Dialect resolution
Section titled “Dialect resolution”The datastore’s backend type selects the SQL dialect and compiler. It may be
declared on the schema’s datastores: entry, but that is redundant with the
pool’s type:. When the schema omits type, the service infers it from the
resolved pool and logs that it did so. The inference is copy-on-write; the
cached schema is never mutated.
Spelling is not uniform across the surfaces, the canonical pool type and the
shared-endpoint key use postgres; the backend driver reports itself as
postgresql; schema-side dialect lookup accepts both, as does engine assembly.
Match the spelling to the field rather than assuming one canonical string.
Backend vocabulary
Section titled “Backend vocabulary”symbols: and functions: are top-level schema keys: siblings of
entities:, not children of a datastores: entry. Both describe SQL vocabulary
that belongs to a specific database backend, and both are attached to the dialect
built for the resolved datastore.
| Key | Shape | Declares |
|---|---|---|
symbols | map keyed by backend name | Database function and keyword names exposed for that backend |
functions | list of named entries | User-defined functions the schema refers to by name |
Symbols
Section titled “Symbols”A symbols: block is keyed by backend. Each backend bucket takes one key,
systemfunctions, a list of names:
symbols: postgres: systemfunctions: - min - max - count clickhouse: systemfunctions: - multiMatchAllIndices - multiFuzzyMatchAllIndicesEach name is flattened into one symbol entry with category function and one
mapping per backend that lists it. The mapping value is the name itself, so a
symbol is never rewritten into different SQL, listing it exposes the name for
that backend, and nothing more. A name listed under two backends produces a
single entry carrying both mappings.
Backend keys are matched by alias when a symbol is resolved: postgresql,
postgres and pg all match PostgreSQL; clickhouse and ch match ClickHouse;
sqlite and sqlite3 match SQLite; duckdb matches only itself, a key outside
those sets matches no dialect and the block has no effect.
Schema-declared symbols are consulted before the dialect’s own built-in symbol table, so listing a name the dialect already provides overrides the built-in.
Across multiple schema files the merge is last-write-wins per backend key, on
the whole bucket, a second file declaring symbols: with a postgres: key
replaces the first file’s systemfunctions list rather than appending to it.
Keep one backend’s symbols in one file. In a layered schema the key is dropped
entirely. The base’s symbols are carried and a layer’s are ignored, see
Schema sources.
Database functions
Section titled “Database functions”functions: declares user-defined functions by name so the schema can refer to
them. Entries are a list:
functions: - name: tax kind: computed runtime: sql entity: invoice source: tax(amount, rate)| Key | Values | Notes |
|---|---|---|
name | string | The lookup key. Names are unique per schema; a later file with the same name replaces the earlier entry |
kind | trigger, validation, transform | Anything else, including an omitted value, is read as computed |
runtime | sql, expr, cel, js, lua, starlark | sql marks the function inlinable into compiled SQL; every other runtime is executed outside the query |
entity | string | The entity the function applies to |
source | string | The expression or file path |
trigger.event | string | e.g. before_insert, after_update |
trigger.entity | string | The entity the trigger fires on |
Physical schema qualification
Section titled “Physical schema qualification”The PostgreSQL schema a tenant’s objects live in comes from that tenant’s pool,
never from a process constant. Resolution is: the explicit schema: when set,
otherwise derived from the tenant key: a pool serves the tenant-key prefix
of the config node it is declared at (recorded as the pool’s serves), and the
schema is the key’s remaining segments joined by _. The tenant name alone
for a pool declared at the product node, shop_prod_alpha for a pool declared
at the customer acme and the tenant acme:prod:shop:alpha. When the same
database also backs another of the tenant’s datastores, the datastore name is
appended. Two products’ tenants that share a leaf therefore never share a
schema, and a tenant resolved against a pool that does not serve it is refused
with dbpool-scope rather than pointed at another product’s tables.
That value is written into the composed schema’s name, so the query compiler
qualifies all DML as "<schema>"."<table>", and it is also sent as the pool’s
search_path startup parameter, the same namespace drives the migration
pipeline. The dialect’s schema name so DDL is emitted qualified, the discovery
inputs so introspection inspects the right namespace, and the ledger so plan and
step rows land in the right place.
Qualification applies in both isolation modes. An earlier design had shared
pools clear the schema name and rely on the per-checkout search_path flip;
that was reversed, because the engine’s non-transactional read path uses the raw
database handle, which never passes through connection checkout. With
unqualified SQL, two tenants sharing an endpoint wrote into one schema. The
checkout flip and app.tenant_id remain as defence in depth.
An omitted schema: is the normal case, schema-per-tenant isolation in a
shared database for free, but it ties the physical namespace to the tenant
key, so a tenant rename moves the namespace, the identifier used in the search_path SET
is quoted; a value containing a double quote or semicolon is discarded and
replaced with a fixed placeholder identifier, so a schema name can never inject
into the statement.
Schema qualification is PostgreSQL-only. ClickHouse, DuckDB and SQLite suppress the schema prefix in compiled SQL, so the whole physical-namespace mechanism does not apply to them.
Several products in one schema
Section titled “Several products in one schema”A datastore prefix is what lets more than one product, or more than one service, keep its
tables in one database schema without touching each other:
datastores: - name: main prefix: em1_Everything physical carries it and nothing logical does. The table of customer is
em1_customer, its history side table em1_customer_history, its audit table
em1_customer_audit, its archive companion em1_customer_archive, the index ix_customer_code
is created as em1_ix_customer_code, and the constraint a create: true reference asks for is
named em1_<reference>. An entity’s own table: gets the prefix in front of it as well. The
engine’s bookkeeping tables follow, so each datastore has its own locks, audit events, handoff
queue, refresh tasks and tenant overlay rows: em1_data_audit_events, em1_tenant_extensions and so on.
Entity names, paths, references, includes and the GraphQL types are unchanged.
The shared coordination tables are the exception and keep their names:
data_schema_upgrade, its step
table and data_locks are used by every kis.ai service that migrates this database, and their rows
separate by the service, product and datastore columns. A prefix there would give this
datastore a private copy — and since only bootstrap creates these tables, changing a prefix later
would strand the copy it left behind.
The migration planner sees only the tables that carry the prefix: another product’s tables in
the same schema are neither drift nor drop candidates, and its own bookkeeping tables are
recognised under their prefixed names. A raw init script reaches the same tables through the
{{prefix}} binding ({{prefix}}customer).
Adding a prefix to a datastore that already has tables is a rename of every table, which the planner treats as new tables plus drops of the old ones. Choose the prefix before the first bootstrap.
Pool isolation
Section titled “Pool isolation”dedicated (default) | shared | |
|---|---|---|
| Pool scope | One private pool per (CEPT, datastore) | One reference-counted base pool per physical endpoint |
| Endpoint key | n/a | <backend>://<host>:<port>/<database> |
schema: | Optional, derived when absent | Optional, derived when absent; the derived or explicit name is what keeps tenants apart |
| Connectivity check | Pinged once after creation; a failed ping fails the build | No probe ping; the first query validates |
| Failure surface | Credential and network errors at pool build | Credential and network errors at first query |
| Engine, schema, hooks | Per tenant | Per tenant: only the physical pool is shared |
The endpoint key deliberately excludes schema and user, because the schema flips per checkout and including it would partition the pool. Two consequences:
- All tenants on a shared endpoint connect as the same database role. Varying
dbuseracross tenants on one endpoint does not partition the pool, they all connect as whichever tenant built it first. - The base pool is sized by the first tenant’s
maxconnections. Size a shared endpoint for the whole fleet, not for one tenant.
Acquire on an endpoint builds the base pool on first miss and increments a
refcount; each tenant’s router releases it on invalidation, and the base closes
when the last tenant releases. Shutdown force-closes every base pool regardless
of refcount, because every engine is being torn down anyway.
Per-checkout tenant session
Section titled “Per-checkout tenant session”Tenant identity is not stored on the pool object. It flows through the request context and is applied per connection checkout. On PostgreSQL the hook emits, in order:
SET search_path TO "<schema>"; -- shared isolation onlySET app.tenant_id = '<cept key>';SET app.user_id = '<user>'; -- when non-emptySET statement_timeout = 30000;app.tenant_id and app.user_id are session variables for database-native
policies you create yourself, through an init script or a
migration, the Data block’s own
row-level security does not
read them: it compiles a predicate from the request principal and ANDs it into
the SQL, and no CREATE POLICY DDL is emitted for an entity’s RLS rules.
A failure anywhere in this setup closes the connection instead of handing it to the caller, so no partial session state leaks.
The raw handle bypasses the tenant session deliberately, for migrations and introspection. Every raw-SQL subsystem, migration ledger, lock manager, retention eraser, archive handoff queue, materialised-refresh task table, init scripts, is therefore explicitly namespace-qualified. This is why qualification, not the checkout flip, is the isolation guarantee on a shared pool.
Session reset
Section titled “Session reset”Every connection hand-off on PostgreSQL runs DISCARD ALL, wired through both
the pool’s after-release hook and the driver’s session-reset option. It wipes
search_path, timeouts, prepared statements, temp tables, cursors, LISTEN
channels and SET variables. A failed reset drops the connection rather than
returning it to the pool, under a persistent failure such as revoked privileges
this shows up as constant connection churn and pool exhaustion, not an obvious
error.
Because DISCARD ALL implies DEALLOCATE ALL, prepared-statement caching is
disabled and the driver runs in simple-protocol mode. That is a deliberate
per-query cost traded for correctness: a cached statement name the server has
dropped fails with prepared statement does not exist.
Backends
Section titled “Backends”data.svc serves PostgreSQL, ClickHouse, DuckDB and SQLite for tenant
datastores. The backend is a property of the pool, not of the service, so one
tenant can keep its ledger in PostgreSQL and its telemetry in ClickHouse by
declaring two datastores.
| Type | Aliases | Shape |
|---|---|---|
postgres | postgresql, pg | Server. dbuser, dbserver and database required unless dsn is set |
clickhouse | ch | Server |
duckdb | — | File. database is the path |
sqlite | sqlite3 | File. database is the path |
Anything else is refused at config validation with
unknown db pool type "<type>" for pool "<name>".
The tables below are what each backend declares about itself. They are the
portability reference for the SQL the compiler emits per backend. The
schema-refusal gates built on those declarations are a separate matter, see
Capability gates for what actually runs in data.svc.
Transactions
Section titled “Transactions”| PostgreSQL | ClickHouse | DuckDB | SQLite | |
|---|---|---|---|---|
| Transactions | Yes | 24.8+ only | Yes | Yes |
| Savepoints | Yes | No | Yes | Yes |
| Transactional DDL | Yes | No | Yes | Yes |
| Isolation levels | Read committed, repeatable read, serializable | Snapshot (24.8+) | Read committed, repeatable read, serializable | Serializable |
Query and mutation model
Section titled “Query and mutation model”| PostgreSQL | ClickHouse | DuckDB | SQLite | |
|---|---|---|---|---|
| Joins, subqueries, set ops | Yes | Yes | Yes | Yes |
| Window functions | Yes | Yes | Yes | Yes |
| Lateral joins | Yes | No | Yes | No |
| Writable CTEs | Yes | No | Yes | Yes |
Upsert (ON CONFLICT) | Yes | No | Yes | 3.35+ |
RETURNING | Yes | No | Yes | 3.35+ |
| Regex operators | Yes | Yes | Yes | No |
| Information schema | Yes | No | Yes | No |
| Placeholder style | $1 | ? | $1 | ? |
Storage, DDL and security
Section titled “Storage, DDL and security”| PostgreSQL | ClickHouse | DuckDB | SQLite | |
|---|---|---|---|---|
| Orientation | Row | Column | Hybrid | Row |
| Default engine | heap | MergeTree | — | — |
| Row-level security | Yes (incl. per-role) | No | No | No |
| Triggers, sequences, identity, functions | Yes | No | No | No |
| Online add column | Yes | Yes | Yes | Yes |
| Online schema change | Yes | No | No | No |
| Concurrent index build | Yes | No | No | No |
| Tablespaces | Yes | No | No | No |
| Schema qualification | Yes | Suppressed | Suppressed | Suppressed |
ClickHouse also offers ReplacingMergeTree, SummingMergeTree and
AggregatingMergeTree; all of them require an ORDER BY.
Indexes, partitioning and views
Section titled “Indexes, partitioning and views”| PostgreSQL | ClickHouse | DuckDB | SQLite | |
|---|---|---|---|---|
| Index types | BTree, Hash, GIN, GiST, BRIN, full-text, partial, covering, expression, concurrent, multicolumn | minmax data-skipping (reported as BTree), multicolumn, Bloom at 24.1+ | BTree, Hash, partial, expression, multicolumn | BTree, partial, expression, multicolumn |
| Max index columns | 32 | — | — | — |
| Partitioning | Range, list, hash, nested, detach/attach | Range, auto-create, detach/attach, compression | None | None |
| Materialized views | Yes, concurrent refresh | Yes, incremental | No | No |
| Regular views | Yes | Yes | Yes | Yes |
| Recursive views | Yes | No | Yes | Yes |
History and soft delete
Section titled “History and soft delete”| PostgreSQL | ClickHouse | DuckDB | SQLite | |
|---|---|---|---|---|
| History strategies | Audit, Type 3, Type 4, Type 2, Type 6 | None | Audit, Type 3, Type 4, Type 2, Type 6 | Audit, Type 3, Type 4, Type 2, Type 6 |
Point-in-time (asof) queries | Yes | No | Yes | Yes |
| Soft delete | Yes | No | Yes | Yes |
ClickHouse declares no support for history or soft delete because its updates and deletes are asynchronous background mutations: the “delete returned, next read hides the row” guarantee cannot hold, and with no transactional rollback an audit side-table write can succeed while the main mutation fails. Model temporal semantics there explicitly with a deduplicating table engine instead.
Extensions
Section titled “Extensions”Declared under datastores[].extensions. On PostgreSQL the list reaches the
compiler dialect, which registers the extension’s types and operators and
unlocks the features gated on them:
| Extension | What declaring it adds |
|---|---|
pgvector | vector, halfvec, sparsevec types; distance operators; vector-operation feature gate |
postgis | geometry, geography, box2d, box3d types; spatial operators; geospatial feature gate |
pg_trgm | trigram operators and similarity functions |
citext | case-insensitive text type |
uuid-ossp | UUID generation functions |
Names outside that set are accepted and ignored. A second, separate list,
pgvector, pg_trgm, timescaledb, postgis, pg_cron on PostgreSQL, vss
(or duckdb-vss) and fts on DuckDB, fts5 and rtree on SQLite, none on
ClickHouse, feeds the capability matrix, which data.svc does not resolve.
Declaring timescaledb or pg_cron therefore changes nothing in the deployed
service.
Versions
Section titled “Versions”Each backend declares support ranges. Comparison is numeric on major and minor
only, so 24.3.1 compares as 24.3.
| Backend | Supported | Deprecated | End of life |
|---|---|---|---|
| PostgreSQL | 13.0+ | 12.0 – 13.0 | below 12.0 |
| ClickHouse | 23.8+ (LTS) | 23.3 – 23.8 | below 23.3 |
| DuckDB | 0.10+ | 0.9 – 0.10 | below 0.9 |
| SQLite | 3.37+ | 3.35 – 3.37 | below 3.35 |
Capability gates
Section titled “Capability gates”Four validators reject a schema that asks for something the resolved backend does not declare.
| Check | Trigger | Message |
|---|---|---|
| Soft delete | An entity carries a deletedon field on a backend without synchronous row-level UPDATE | unsupported soft-delete: entity <name> uses kisai.softdelete but backend <b> does not support the contract |
| History | An entity’s history strategy is not in the backend’s list | unsupported history strategy: entity <name> declares History.Strategy=<s> but backend <b> supports only [<set>] |
| Partitioning | A partition key on a backend with no partitioning | unsupported partitioning: entity <name> declares Storage.PartitionBy=<k> but backend <b> has no partitioning support |
| Policy | A datastore policy enables RLS, triggers, concurrent indexes, exclusion constraints, transactional DDL, or a default engine the backend lacks | policy enables RLS but <b> does not support it (and siblings) |
Soft-delete detection is purely the presence of a deletedon field.
Pulling in another service’s entities
Section titled “Pulling in another service’s entities”Several kis.ai services keep their tables in one database schema per
tenant, so iam’s users — physically iam_principal_user — sits beside a
product’s orders. The row is right there and a join is ordinary SQL. What is
missing is the definition: the schema feed is per-service, so data.svc is
never handed iam’s entity YAML and orders.userid → users.id has nothing to
resolve against.
injectservices: names the services this datastore may pull definitions from:
datastores: - name: main path: main injectservices: - iamThe loader asks for the entities this schema named and could not find — the
target of an unresolved augment, the far side of a relation — plus whatever
those need to be valid. It does not pull the other service’s whole datastore.
That is a security boundary: iam’s datastore also holds jwt_signing_key,
credentials and api_key, and asking for users must not put those on this
service’s REST surface.
| Key | Type | Notes |
|---|---|---|
injectservices | []string | Services whose entity definitions may be pulled INTO this datastore |
inject | bool | Marks the datastore injectable; implied by a non-empty list |
Errors
Section titled “Errors”| Code or message | Meaning |
|---|---|
datastore-not-found | The requested datastore name is not in the tenant’s datastores map |
dbpool-not-found | A datastore’s dbpool: names no key under dbpools: |
invalid-db-pool-config | The pool exists but failed validation |
tenant-config-unavailable | The tenant’s config could not be bound to the pools/datastores shape at all |
unknown db pool type "<t>" for pool "<n>" | type is not one of postgres/postgresql/pg, clickhouse/ch, duckdb, sqlite/sqlite3 |
invalid postgres db pool "<n>": dbuser, dbserver and database are required | A required field is missing and no dsn was supplied |
no v2 pool factory for DBPool type "<t>" (datastore=<n>) | Type is valid but no factory is registered for it |
unsupported backend type "<t>" (supported: postgresql, clickhouse, duckdb, sqlite) | Engine components could not be assembled for the resolved type |
no dialect implementation for datastore type "<t>" | The migration path could not map the type to a dialect |
DBPool declares isolation: shared but no schema: property | Shared isolation with no namespace resolved. No tenant identity reached the engine |
pool "<p>" serves "<prefix>", cannot serve "<tenant>" | The tenant lies outside the tenant-key prefix its pool serves (dbpool-scope). Declare the pool at a node above the tenant, or set serves: |
no DStore config for "<tenant>":"<ds>" (pass ?datastore=<name>) | No datastore selected and none resolvable for the tenant |
schema has no datastore "<n>" (default=…, available=…) | Name mismatch between tenant config and the loaded schema |
pool: parse db port "<p>" | The port in dbserver is not an integer |
postgres: parse config / postgres: connect | Malformed DSN, unreachable host, or bad credentials |
pool/shared: tenantize | Per-checkout session setup failed; the connection is closed rather than returned |
Pool and endpoint diagnostics, refcounts, endpoint snapshots, tenantize error counts, are collected internally but not exposed on any route.