Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .changeset/datasource-sqlite-pool-loud-reject.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
"@objectstack/service-datasource": minor
---

fix(service-datasource): a `pool` block on a sqlite datasource is rejected, not dropped in silence (#5714)

`datasource.pool` is declared, strict and documented, and until now it reached a
driver only from the arms that build a pooled client: `postgres` / `mysql` hand
`buildSqlPool(spec)` to `SqlDriver`, `mongo` maps `min`/`max` onto the client's
`minPoolSize`/`maxPoolSize`. The `sqlite` and `sqlite-wasm` arms passed no pool
at all — `resolveSqliteDriver` has no such option and `SqliteWasmDriver` does
not take one — so an author who sized their pool got the driver's own single
connection and nothing said otherwise. Measured through the real factory:

```text
sqlite + pool{min:3,max:9} knex.client.config.pool {"createTimeoutMillis":15000} live {min:1,max:1}
postgres + pool{min:3,max:9} knex config.pool {"min":3,"max":9} live {min:3,max:9}
```

`examples/app-crm` was the live specimen: `CrmDatasource` asked for
`{ min: 1, max: 5 }` and ran on one connection.

**Wiring it through would be wrong, not merely more work.** Knex's
better-sqlite3 dialect pins `{min:1,max:1}` on purpose: every pool acquire runs
`new Database(filename)`, so two connections to `:memory:` are two separate,
mutually invisible databases. Honouring `max: 5` there would split one
datasource's data across five stores. Sizing a SQLite pool is not a knob the
platform can offer, so the declaration is rejected at authoring/publish instead
— Prime Directive #12: fix the metadata at the producer, reject it loudly, never
tolerate it in the consumer.

**Observable behaviour change — read this if any datasource declares `pool`.**
A `sqlite` / `sqlite-wasm` datasource carrying a `pool` block now **fails**
where it used to boot with the block ignored:

- **Boot** (`DatasourceConnectionService.connectDeclared`) refuses before a
single connection is attempted, naming every offending datasource in one
throw. Every *declared, active* datasource is judged, including the ones the
ADR-0062 D2 gate leaves unconnected — a pool block on a datasource nobody
connects is exactly as dropped as a connected one's. `active: false` is
skipped, so switching a datasource off remains the way out.
- **Setup → Datasources** (`createDatasource` / `updateDatasource`) rejects the
draft before the record is stored. An update that touches neither `pool` nor
`driver` is not re-judged, so a record written before this gate stays editable
— including the `active: false` that takes it out of service.
- **The driver factory** (`createDefaultDatasourceDriverFactory`) rejects it as
the last door, for hosts that build drivers directly.

The fix is to delete the block: `pool` is a no-op on SQLite either way, so
removing it changes nothing about how the datasource runs.

```diff
export const CrmDatasource = defineDatasource({
name: 'crm_primary',
driver: 'sqlite',
config: { filename: ':memory:' },
- pool: { min: 1, max: 5 },
active: true,
});
```

`pool` is unchanged and still honoured on `postgres` / `mysql` / `mongo`, and a
plugin-contributed driver id (`com.vendor.snowflake`) is not judged at all —
the same boundary the `datasource.config` gate draws in #4410: the platform
validates what it can construct.

This verdict is an **authoring** error, not a connect failure: it never goes
through the ADR-0062 D5 degradation path, so `OS_ALLOW_DRIVER_CONNECT_FAILURE`
does not apply to it and is not suggested. That hatch exists for a database that
is unreachable — a fact about the world that may resolve itself. A `pool` the
driver cannot read is a fact about the metadata.

Hosts that inject their own driver factory can hold the same contract with the
newly exported `assertDatasourcePoolSupported` / `driverReadsDeclaredPool` /
`unsupportedPoolIssue` / `POOL_UNSUPPORTED_DRIVER_IDS`.
13 changes: 11 additions & 2 deletions content/docs/data-modeling/drivers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,17 @@ save, the connection probe — reported success.
Two things live **outside** `config`, because they are not driver-specific:

- **Pool sizing** — the `pool` block on the datasource (`min`, `max`,
`idleTimeoutMillis`, `connectionTimeoutMillis`), honoured for every SQL driver
and mapped onto the Mongo client's `minPoolSize` / `maxPoolSize`.
`idleTimeoutMillis`, `connectionTimeoutMillis`), honoured by the pooled
drivers: `postgres` and `mysql` pass it to Knex, `mongo` maps `min` / `max`
onto the client's `minPoolSize` / `maxPoolSize`. Declaring it on a **sqlite**
or **sqlite-wasm** datasource is rejected — by the Setup wizard when you save,
and by the boot when a declared datasource carries one: a SQLite connection
strategy is owned by the driver (one connection per database, because a second
connection to `:memory:` opens a separate, empty one), so a pool declared
there could never take effect. It used to be dropped in silence — an `app-crm`
datasource asking for `max: 5` measurably ran on one connection
([#5714](https://github.com/objectstack-ai/objectstack/issues/5714)). The fix
is to delete the block; it is a no-op on SQLite either way.
- **TLS certificates** — the `ssl` block on the datasource (`enabled`,
`rejectUnauthorized`, `ca`, `cert`, `key`). Inside `config`, `ssl` is the
on/off boolean shorthand.
Expand Down
10 changes: 6 additions & 4 deletions examples/app-crm/src/datasources/crm.datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { defineDatasource } from '@objectstack/spec/data';
/**
* Primary CRM datasource — in-memory SQLite for the example.
* In production, swap `driver` to 'postgres' and supply real `config`.
*
* No `pool` block: SQLite's connection strategy is owned by the driver (one
* connection per database — a second connection to `:memory:` would open a
* separate, empty one). This example declared `pool: { min: 1, max: 5 }` and
* measurably ran on `{min:1,max:1}` with no indication at all, which is what
* #5714 turned from a silent drop into a loud rejection.
*/
export const CrmDatasource = defineDatasource({
name: 'crm_primary',
Expand All @@ -13,10 +19,6 @@ export const CrmDatasource = defineDatasource({
config: {
filename: ':memory:',
},
pool: {
min: 1,
max: 5,
},
active: true,
});

Expand Down
10 changes: 8 additions & 2 deletions examples/app-showcase/src/system/datasources/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

/** Primary datasource — in-memory SQLite for the example. */
/**
* Primary datasource — in-memory SQLite for the example.
*
* No `pool` block: a SQLite connection strategy is owned by the driver, so the
* `pool: { min: 1, max: 5 }` this used to declare reached nothing and the
* datasource ran on the dialect's single connection. Declaring it is a loud
* rejection since #5714 rather than a silent drop.
*/
export const ShowcaseDatasource = {
name: 'showcase_primary',
label: 'Showcase Primary Database',
driver: 'sqlite',
config: { filename: ':memory:' },
pool: { min: 1, max: 5 },
active: true,
};

Expand Down
Loading
Loading