Skip to content

Commit f22f0e8

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20148-aggregation-filter-doors
2 parents d831680 + 369bcbe commit f22f0e8

20 files changed

Lines changed: 1843 additions & 140 deletions
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec): a `joined` report refuses a top-level `dataset` / `rows` / `columns` / `values`, pointing each onto `blocks[]` (#19856)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING** — shipped as `minor` under the launch-window convention
10+
(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by
11+
this banner, the `(narrowing)` arm above and the ADR-0087 disposition below,
12+
never by the level).
13+
14+
A `joined` report selects nothing itself: each block binds its own `dataset` and
15+
selects its own `rows` / `columns` / `values`, and the renderer's joined branch
16+
reads `blocks` and returns before it reads any top-level selection key.
17+
`ReportSchema` already refused a container-level `order` for exactly that
18+
reason, but the four selection keys beside it parsed green and were then dropped
19+
without a word. Each is now refused at its own path:
20+
21+
```
22+
FROM ReportSchema.safeParse({ name: 'overview', label: 'Overview', type: 'joined',
23+
blocks: [/* … */], dataset: 'tasks', values: ['task_count'] })
24+
-> { success: true } // both keys silently ignored at render
25+
26+
TO -> { success: false, issues: [
27+
{ code: 'custom', path: ['dataset'],
28+
message: 'a `joined` report selects per block — move `dataset` onto `blocks[]`, or delete it; on the container it selects nothing.' },
29+
{ code: 'custom', path: ['values'],
30+
message: 'a `joined` report selects per block — move `values` onto `blocks[]`, or delete it; on the container it selects nothing.' } ] }
31+
```
32+
33+
**Fix.** Move the key onto the `blocks[]` entries that need it, or delete it.
34+
Either way the report renders exactly as before, because the container value was
35+
never read.
36+
37+
**What does not change.** A present `dataset` and a NON-EMPTY list are refused;
38+
an empty `rows` / `columns` / `values` list selects nothing and still parses (the
39+
same threshold as the container `order` refusal, whose message is unchanged). A
40+
joined report's container `runtimeFilter` and `drilldown` — the two container keys
41+
the joined branch does read — parse as before, and every non-joined report is
42+
untouched. An aliased spelling (`measures` → `values`, `dataSet` → `dataset`, …)
43+
is still renamed first, and the renamed key on a joined container then meets this
44+
refusal.
45+
46+
<!-- adr-0087: registered ui-report-joined-container-selection-refused -->
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`DecisionConfigSchema` declares an optional `mode: 'exclusive' | 'inclusive'` — the contract half of the #15429 ruling. The author of a `decision` node that routes on its out-edges can now declare whether it takes only the first out-edge whose condition holds or every one of them — with taking every one as the value that must be written down, the way BPMN separates the exclusive gateway from the inclusive one and n8n's Switch keeps "send to all matching outputs" behind an off-by-default toggle (#19867).
6+
7+
Clause-②: yes (widening) — one new OPTIONAL key on a published, strict node-config schema, so the set of accepted configs grows. Nothing previously accepted is refused, no key is renamed or retired, and the parsed output of an existing config is unchanged (the key has no `.default()`).
8+
9+
- **`'exclusive'`** — only the first out-edge whose condition holds, in the order the edges are declared; this is what an omitted `mode` means. **`'inclusive'`** — every out-edge whose condition holds. Any other value is refused at `mode` with a prescription naming both members' meanings.
10+
- **⚠️ Declared ahead of its enforcement, on purpose.** The ruling's split order lands this key first, then the engine semantics together with the `os migrate meta` conversion in one change, then the docs. Until that second step ships, nothing reads `mode`: an edge-branched decision still takes EVERY out-edge whose condition holds, whatever `mode` says. The key's own description says so, and the conversion that writes `mode: 'inclusive'` onto every decision relying on today's behaviour ships in the same change as the new traversal, so no flow changes behaviour silently.
11+
- **A `conditions` list is unaffected**: it is ordered first-match on its own, and `mode` speaks about the out-edges.
12+
- **Where it binds today**: `decision` config stays export-only (nothing parses it at run time), so the closed pair is enforced by `tsc`, by the published JSON Schema and by a direct parse — the same doors `conditions` has.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/driver-turso": minor
3+
---
4+
5+
fix(driver-turso)!: a REMOTE `TursoDriver` answers or refuses every public `SqlDriver` method it inherited, instead of failing on a Knex connection it does not have (#20055)
6+
7+
Clause-②: yes (narrowing)
8+
9+
`TursoDriver` extends `SqlDriver`. Before this change, 23 public `SqlDriver` members had no remote-mode arm, so a remote driver ran their Knex implementations. Remote mode builds Knex with no connection, so those calls failed with knex's `Unable to acquire a connection`, which reads as a network fault, or they answered from state no remote schema sync fills. Each one is now answered on the remote database or refused with `NOT_IMPLEMENTED` / 501, and the driver's source lists every public `SqlDriver` member with its remote answer. A method added to `SqlDriver` later fails this package's type check until its remote answer is decided.
10+
11+
**BREAKING.** On a remote driver, six members that used to answer now refuse or answer differently. This narrows what a published driver accepts. It ships as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`).
12+
13+
- `explain()` and `analyzeQuery()` used to resolve with the SQL the local compiler would build, plus an `error` field in place of a plan. They now reject with `NOT_IMPLEMENTED` / 501.
14+
- `applyMigrationEntries()` used to resolve. Every entry came back `skipped`, including a destructive entry that `allowDestructive` permitted. It now rejects with `NOT_IMPLEMENTED` / 501, with or without entries.
15+
- `getKnex()` used to return a Knex instance on which every statement failed. It now throws `NOT_IMPLEMENTED` / 501.
16+
- `supportsRotation` used to read `true`. It now reads `false`. The lifecycle service reads it, and for an object that declares a rotation storage policy it now takes its age-based reap instead of calling `rotateShards()`, which failed.
17+
- `setFileColumnsMovedResolver()` used to return `true` ("taken"), and then never asked the resolver. It now returns `false`. Remote mode keeps writing media columns in the JSON encoding, as before.
18+
19+
**Refused with a clearer error.** These calls already failed. They now reject with `NOT_IMPLEMENTED` / 501, and the message names the local or embedded-replica transport, or `execute()`, as the alternative:
20+
21+
- `introspectSchema()`. The datasource connection test still answers `ok: false`, and its error now says introspection is not supported in remote mode;
22+
- `findWithWindowFunctions()`;
23+
- `rotateShards()`;
24+
- `distinct()` called with `options.tenantId` on an object that has a tenant column. No remote read applies the tenant scope, so an answer would list every organization's values.
25+
26+
**Now answered on the remote database.** These used to fail:
27+
28+
- `distinct()` without a tenant scope runs a `SELECT DISTINCT` and answers what local mode answers for the same rows. The filter and the value presentation match, and an unknown column is refused with `INVALID_FIELD` / 400.
29+
- `reclaimSpace()` sends the statement local mode issues, `PRAGMA incremental_vacuum`, to the remote database. The lifecycle service's sweep now counts the datasource as reclaimed instead of logging a warning. The statement returns pages only on a database whose `auto_vacuum` mode is `INCREMENTAL`.
30+
31+
`RemoteTransport` gains one public method, `compileDistinct()`. It builds the statement behind a remote `distinct()`.
32+
33+
**Unchanged.** Local and embedded-replica modes run the inherited Knex members as before. On a remote driver, `commitTransaction()` and `rollbackTransaction()` still reject through the `commit()` / `rollback()` refusal. The deferred-DDL readers still report nothing deferred, and `getSchemaSyncStats()` still answers `{ created: 0, existing: 0 }`, which the `IDataDriver` contract reads as "cannot say". The bookkeeping and dialect members also answer as before.
34+
35+
<!-- adr-0087: not-required (no-migration-prescription) A narrowing of which calls a remote `TursoDriver` answers: no key, spec symbol, Zod schema, object definition or stored representation is added, removed or renamed. `TursoDriverConfig` and both `TursoConfigSchema` copies are untouched, and every `SqlDriver` member keeps its name and signature. What moves is only whether a remote driver answers a call or refuses it with NOT_IMPLEMENTED/501, so `objectstack migrate meta` has nothing to visit and there is no tombstone to mint. -->

‎AGENTS.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,9 @@ Even inside your own worktree, operate defensively:
507507
(**Prime Directive #14**, which names them and holds the current list — **this file and `CLAUDE.md` are on it**,
508508
so re-read it rather than recalling it); (b) the **Version Packages** PR, or any PR whose merge performs a
509509
release (**Prime Directive #15**); (c) a PR whose **changed lines exceed 5,000** (`additions + deletions`,
510-
generated files included) — it lands only by a human merge, which is its review record. Read the PR's file
511-
list (`get_files`), **its author and its size** before you arm anything.
510+
generated files included) — it lands the way a Tier H surface does: an authorized APPROVED review and then
511+
the owning seat, or a human merge. Read the PR's file list (`get_files`), **its author and its size** before
512+
you arm anything.
512513

513514
**Green means the gate-carrying jobs' `conclusion` is `success`** — not "no failure
514515
yet"; `in_progress` is not a pass. Arming a red PR does not queue it, it hides it:

‎content/docs/references/automation/schemaless-node-config.mdx‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,13 @@ The two halves reach different audiences, which is why they shipped together:
6666
nothing read — and then refuses, naming the `function` it does not have,
6767
instead of logging a line and reporting success as it used to.
6868

69-
`decision` stays export-only, deliberately: it may carry no `conditions` at
70-
all when it branches purely on edge predicates (a plain BPMN exclusive
71-
gateway), and `conditions` is its only key — so a parse would have nothing
72-
left to check. Its enforcement remains the objectui reconciliation test,
73-
which is what #4278 was actually about (a form authoring keys nothing reads).
69+
`decision` stays export-only: nothing parses it at run time. It may carry no
70+
`conditions` at all when it branches purely on edge predicates, its executor
71+
reads `conditions` and nothing else, and its one other key — `mode` — is
72+
declared AHEAD of the engine change that reads it (#15429; see
73+
`DecisionConfigSchema`). Its enforcement remains the objectui
74+
reconciliation test, which is what #4278 was actually about (a form
75+
authoring keys nothing reads).
7476

7577
Undeclared aliases are NOT part of these contracts: `subflow`'s historical
7678
`flow` spelling graduated into the ADR-0087 D2 conversion
@@ -135,6 +137,7 @@ const result = DecisionConditionSchema.parse(data);
135137
| Property | Type | Required | Description |
136138
| :--- | :--- | :--- | :--- |
137139
| **conditions** | `{ label: string; expression: string }[]` | optional | Ordered decision branches (first true expression wins; omit to branch purely on edge conditions) |
140+
| **mode** | `Enum<'exclusive' \| 'inclusive'>` | optional | Declares how many out-edges an edge-branched decision takes when more than one out-edge condition holds: 'exclusive' = only the first, in the order the edges are declared (what an omitted mode means); 'inclusive' = every one that holds. Declared ahead of the engine change that reads it: until that lands, an edge-branched decision takes every out-edge whose condition holds, whatever this says. A conditions list is first-match on its own. |
138141

139142
### Nested Shape: `DecisionConfig.conditions[number]`
140143

‎packages/drivers/driver-turso/README.md‎

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,24 @@ const users = await driver.find('users', { where: { active: true } });
123123

124124
Each call below is refused in remote mode with `code: 'NOT_IMPLEMENTED'` and
125125
`status: 501`: the call is valid, and the remote transport does not have the
126-
capability. The check runs before the call reads or writes anything.
126+
capability. The check runs before the call reads or writes anything. Every other
127+
public `SqlDriver` method is answered in remote mode; the section after the
128+
table lists the ones that answer differently from local mode.
127129

128130
| Operation | Refused call | Use instead |
129131
|:---|:---|:---|
130-
| Transactions | `beginTransaction()`, `commit()`, `rollback()`, and `options.transaction` passed to any `RemoteTransport` method in the tree above, to `aggregate()` or to `syncSchemasBatch()` | The local or embedded-replica transport, which run Knex transactions and honour `options.transaction` |
132+
| Transactions | `beginTransaction()`, `commit()`, `rollback()` (and their deprecated aliases `commitTransaction()` and `rollbackTransaction()`), and `options.transaction` passed to any `RemoteTransport` method in the tree above, to `aggregate()` or to `syncSchemasBatch()` | The local or embedded-replica transport, which run Knex transactions and honour `options.transaction` |
131133
| Record numbers | `create()`, `bulkCreate()`, and an `upsert()` with no `id`, `_id` or `conflictKeys`, when a row leaves an `autonumber` field empty (`undefined`, `null` or `''`) | The local or embedded-replica transport, which generate record numbers, or a value you supply, which is written unchanged |
132134
| Deferring schema DDL | `setDeferredDdl(true)`, which `os migrate plan` calls. `setDeferredDdl(false)` is accepted | Run the command against a local SQLite copy of the database (a `file:` URL) |
133135
| Schema drift detection | `detectManagedDrift()` | `os migrate plan` against a local SQLite copy of the database (a `file:` URL) |
134136
| Planning the ADR-0104 media column move | `planMediaColumnMove()`, the column step of `os migrate files-to-references` | The local or embedded-replica transport, which plan it |
137+
| Applying drift entries | `applyMigrationEntries()`, with or without entries | An ordinary boot against the datasource (`os serve` / `os start`) performs the additive schema sync |
138+
| Schema introspection | `introspectSchema()`, which the datasource connection test and the federation validation sweep call | A datasource pointed at a local SQLite copy of the database (a `file:` URL) or at an embedded replica |
139+
| Window-function reads | `findWithWindowFunctions()` | The local or embedded-replica transport, or the window statement sent through `execute()` |
140+
| Query plans | `explain()`, `analyzeQuery()` | The same query against the local or embedded-replica transport |
141+
| Shard rotation (ADR-0057) | `rotateShards()` | The local or embedded-replica transport. In remote mode the lifecycle service enforces the same window with an age-based reap (see below) |
142+
| The Knex instance | `getKnex()`: remote mode builds Knex with no connection | `execute()`, or the libSQL client from `getLibsqlClient()` |
143+
| Tenant-scoped distinct values | `distinct()` with `options.tenantId` on an object that has a tenant column | The same call without `tenantId`, or the local or embedded-replica transport |
135144

136145
- **Transactions.** Remote mode declares `supports.transactionsUnsupported: true`.
137146
When the remote driver is the engine's default datasource, `engine.transaction()`
@@ -150,6 +159,28 @@ capability. The check runs before the call reads or writes anything.
150159
sends either one to this driver: it fetches the rows and computes both in
151160
memory. The local transport also refuses a per-aggregation `filter`.
152161

162+
Answered in remote mode, differently from local mode:
163+
164+
- **`distinct()`** runs a `SELECT DISTINCT` on the remote database and answers
165+
the values local mode answers for the same rows: the same filter, the same
166+
presentation (a declared boolean reads back as `true` / `false`), and an
167+
unknown column refused with `INVALID_FIELD` / 400. A tenant-scoped call is
168+
refused (table above), because no remote read applies the tenant scope.
169+
- **`reclaimSpace()`** sends the statement local mode issues,
170+
`PRAGMA incremental_vacuum`, to the remote database. It returns free pages
171+
only on a database whose `auto_vacuum` mode is `INCREMENTAL`: local mode sets
172+
that mode when it connects, and remote mode does not. A server that refuses
173+
the statement answers `DATABASE_ERROR` / 500.
174+
- **`supportsRotation`** is `false`. The lifecycle service reads it, and for an
175+
object that declares `lifecycle.storage.strategy: 'rotation'` it then takes the
176+
path it has for a driver that cannot shard: an age-based reap bounded by the
177+
same `shards` × `unit` window, instead of rotating shard tables.
178+
- **`setFileColumnsMovedResolver()`** returns `false` ("not taken"). Remote
179+
mode writes media columns in the JSON encoding and never asks the resolver.
180+
- **`getSchemaSyncStats()`** answers `{ created: 0, existing: 0 }`, which the
181+
`IDataDriver` contract reads as "cannot say": remote schema sync does not
182+
count the tables it creates.
183+
153184
### Auto-Detection
154185

155186
Transport mode is automatically detected from the URL:
@@ -332,18 +363,27 @@ interface TursoDriverConfig {
332363

333364
## Capabilities
334365

335-
TursoDriver declares enhanced capabilities beyond the base SqlDriver:
336-
337-
| Capability | SqlDriver | TursoDriver (local) | TursoDriver (remote) |
338-
|:---|:---:|:---:|:---:|
339-
| FTS5 Full-Text Search | ❌ | ✅ | ✅ |
340-
| JSON1 Query | ❌ | ✅ | ✅ |
341-
| Common Table Expressions | ❌ | ✅ | ✅ |
342-
| Savepoints | ❌ | ✅ | ✅ |
343-
| Indexes | ❌ | ✅ | ✅ |
344-
| Connection Pooling | ✅ | ❌ (concurrency limits) | ❌ |
345-
| Embedded Replica Sync | — | ✅ | — |
346-
| Serverless/Edge | — | — | ✅ |
366+
The capabilities TursoDriver declares are its `supports` getter, and it
367+
declares only the flags the engine acts on: everything `SqlDriver` declares,
368+
plus `batchSchemaSync: true`. Remote mode also declares
369+
`transactionsUnsupported: true` and no native date bucketing
370+
(`queryDateGranularity: {}`). It declares nothing about the SQLite features in
371+
the table below. That table records what each mode was measured to do; the
372+
engine does not read it.
373+
374+
| Feature | Local and embedded replica | Remote |
375+
|:---|:---:|:---:|
376+
| FTS5, JSON1 and common table expressions, through `execute()` | ✅ | ✅ |
377+
| Declared indexes (`indexes`, and a field's `unique`) | ✅ | ✅ |
378+
| Savepoints inside a transaction | ✅ (Knex transactions) | ❌ (transactions are refused) |
379+
| Raw `SAVEPOINT` statements through `execute()` | ✅ | ✅ through a `file:` client; not measured against a server |
380+
| Window functions | ✅ (`findWithWindowFunctions()`, or `execute()`) | Through `execute()` only |
381+
| Schema introspection (`introspectSchema()`) | ✅ | ❌ (refused) |
382+
| Embedded replica sync | ✅ (replica) | — |
383+
| No native module (serverless / edge) | — | ✅ |
384+
385+
The remote column was measured through a libSQL `file:` client, which runs
386+
the same SQLite engine as a Turso server but not its network protocol.
347387

348388
## Plugin Registration
349389

0 commit comments

Comments
 (0)