Skip to content

Commit 95b97ef

Browse files
committed
Merge origin/main 810d42b into claude/issue-19278-shard-hash-carries-closure
Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude <noreply@anthropic.com>
2 parents 9df0b71 + 810d42b commit 95b97ef

55 files changed

Lines changed: 3483 additions & 277 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/driver-sql': patch
3+
---
4+
5+
driver-sql refusals, drift reports and log lines no longer cite tracker numbers; each states the reason in words
6+
7+
Clause-②: no
8+
9+
Many messages the SQL driver shows to authors and operators ended with an issue-tracker number where
10+
the reason belonged. The number goes, and where the sentence did not already say what was decided, it
11+
now does:
12+
13+
- Filter refusals (`INVALID_FILTER`): the withheld-detail wording ("withheld from the message; the full
14+
diagnostic is in the server log"), the JSON-column, zero-operator, `$null` / `$exists`, undefined
15+
comparand and unknown-combinator refusals, and the filter-array refusal.
16+
- Schema and index messages: the `reference_to` DDL refusal (the FOREIGN KEY DDL that key used to gate
17+
is retired, because it could never fire for a spec-conformant lookup), the MySQL TEXT-key and
18+
row-size explanations, the hash-shadow UNIQUE messages, and the `os migrate plan` drift entries.
19+
- The NULL-safe UNIQUE messages now say why rows without an organization were never constrained: SQL
20+
UNIQUE is NULL-distinct.
21+
- Boot log lines for the SQLite datetime, time and json canonicalisation and the MySQL `TIMESTAMP` /
22+
`TIME` widening now say what the conversion is for.
23+
24+
Text only: no error code, field name, status or behaviour changes. Three aggregate refusals keep their
25+
citation for now, because a test in `@objectstack/driver-turso` compares them byte for byte with the
26+
Turso remote transport's copies; they change together with those copies.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/sdui-parser': minor
3+
---
4+
5+
`@objectstack/sdui-parser` now reads one base-prop list, ported from objectui's `SDUI_BASE_PROPS` at the console pin `db11afd4967c` (objectui#11008, #11044). Both `validateTree` and the generated JSX types (`generateDts`'s `SduiBaseProps`) are driven by it.
6+
7+
- On every node, whatever the component declares: `bind`, `hidden`, `visibleWhen`, `hiddenOn`, `testId` are newly accepted. They no longer draw `unknown-prop`, and the generated types accept them as attributes.
8+
- Only on a type whose registration declares no input of that name: `name`, `label`, `description`, `placeholder`, `data`, `ariaLabel` are newly accepted. A type that declares one keeps its declared type check and its declared attribute type; its generated interface `Omit`s that key from `SduiBaseProps`.
9+
10+
Effect for consumers: `os validate` stops warning `unknown-prop` on those keys, and a `.tsx` page that authors them now type-checks against `generateDts` output where it was a TypeScript error before. Measured on the tracked `sdui.manifest.json` (107 components), no component declares any of the five every-node keys, and every declared where-undeclared key is checked as before, so no diagnostic of error severity is removed for that manifest. The wider type surface is why this is a minor, not a patch.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os migrate meta` converts an object built with `ObjectSchema.create(…)` instead of stopping at load when the object carries a retired key
6+
7+
Clause-②: no
8+
9+
`os migrate meta` reads a config the current schema refuses, so it can rewrite the
10+
retired keys in it. It did that for artifacts built with a `define*` helper and for
11+
plain object literals. It did not do it for artifacts built with a factory such as
12+
`ObjectSchema.create(…)`, which validates when it is called. An object like this:
13+
14+
```ts
15+
ObjectSchema.create({
16+
name: 'ticket',
17+
fields: { title: { type: 'text' } },
18+
tenancy: { enabled: true, organizationField: 'organization_id' },
19+
})
20+
```
21+
22+
stopped `os migrate meta --from 17` at load with exit 1 and a raw JSON array of
23+
validation issues. The message in that array told the author to run
24+
`os migrate meta --from 17`.
25+
26+
The command now loads it, applies the conversion (here
27+
`object-tenancy-organization-field-removed`), and reports `schemaValid` for the
28+
migrated stack, exactly as it does for the same object written as a plain literal.
29+
This covers the five factories in `@objectstack/spec` that validate when called:
30+
`ObjectSchema.create` (`@objectstack/spec/data`) and `App.create`,
31+
`Dashboard.create`, `Report.create` and `Action.create` (`@objectstack/spec/ui`).
32+
The other `create` factories spec exports return their argument unchanged and
33+
never refused anything, so nothing changes for them.
34+
35+
A schema problem the migration cannot fix is still reported: it is listed among
36+
the refusals under the verdict, and `schemaValid` is `false`. A check that only
37+
the factory makes when it is called, such as `ObjectSchema.create` refusing a
38+
`managedBy: 'system-data'` object that grants no create, edit or delete, is not
39+
part of the stack schema. It is reported on the stderr line described below and
40+
does not change `schemaValid`, the same as `defineStack`'s own call-time checks.
41+
`os validate` still refuses it.
42+
43+
While the config loads, `os migrate meta` prints one stderr line for each
44+
artifact the current schema refused. A raw validation error on that line is now
45+
printed as a block, for example `ObjectSchema.create validation failed (1 issue):`
46+
followed by one `✗ path: message` line per issue, instead of a raw JSON array.
47+
This also applies to `define*` helpers that throw a raw validation error, such
48+
as `defineAgent`.
49+
50+
Nothing else changes. `os validate`, `os build` and every other command still
51+
refuse the retired key at load, with the same message. `ObjectSchema.create` and
52+
the other factories stay strict everywhere outside `os migrate meta`. The keys
53+
of the `--json` payload are unchanged, and a run whose migrated stack does not
54+
parse still exits 0.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/driver-sql': patch
3+
---
4+
5+
fix(driver-sql): the first boot of a new database no longer prints a `DATABASE_ERROR` for `sys_migration` (#20768)
6+
7+
On the first boot of a new database, the SQL driver printed this line once, on its warn channel (stderr by default):
8+
9+
```text
10+
[sql-driver] DATABASE_ERROR — the backend refused a read on 'sys_migration' (SQLITE_ERROR) ... no such table: sys_migration
11+
```
12+
13+
Nothing was wrong. At the start of its first schema sync, before it creates any table, the driver asks whether this deployment's file columns have moved (the ADR-0104 media-arm resolver). The resolver the engine supplies answers by reading `sys_migration`. On a new database that table does not exist yet, so the read is refused and the answer is "not moved", which is correct for an empty store.
14+
15+
The driver now asks that question inside an async scope. Inside it, a read refused because its own target table does not exist goes to the logger's `debug` channel instead of `warn`. The default logger has no `debug`, so the line is not printed. The logger shape gains an optional `debug`. The refusal is still thrown to the resolver, and the resolver's answer is the same as before.
16+
17+
What still warns:
18+
19+
- every other refusal inside that scope, such as a malformed statement on a table that exists, or a missing table named by another relation (a view over a dropped table);
20+
- a missing table read anywhere else, as before.
21+
22+
The missing-table check is the shared `isMissingTableError` from `@objectstack/types`, which `@objectstack/metadata/errors` re-exports. There is nothing to migrate.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/objectql": minor
3+
---
4+
5+
fix(objectql)!: a `groupBy` on a structured-JSON field is refused with `INVALID_FIELD` / 400 at the engine's `aggregate`, on every driver
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of a grouping TARGET at the engine's aggregate door: a groupBy entry naming a declared json, composite, repeater, record, location, address or vector field. No authorable key, spelling, export or stored shape moves (the door module is internal; `@objectstack/objectql` exports nothing new and nothing less, and `EngineAggregateOptions` / `QuerySchema.groupBy` keep parsing the entry), and no stored row is read or rewritten. The grouping had no shared meaning to preserve (one merged group on the in-memory driver, one group per serialized document on SQLite, a 500 on PostgreSQL), and which scalar part of the document a caller meant to group on is not something a ledger entry can rewrite. The other categories are closed on facts: the package publishes (not `unpublished`); no ADR-0087 id covers a grouping target (not `registered` / `already-registered`); and the change is runtime behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
10+
11+
**BREAKING**: this narrows what `aggregate` accepts as a grouping target. A `groupBy` entry that names a declared field of the structured-JSON class (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`) is refused by the engine before any driver is asked. Both entry spellings are judged, the field name and the `{ field }` object, a `dateGranularity` bucket included. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
12+
13+
**What an author sees now.** `400 INVALID_FIELD`, naming the position (`groupBy[0]`, or `groupBy[0].field` for the object form), the field and its declared type, saying the query was not run, and naming the route inside the first 500 characters the REST door keeps: group by a field that stores one scalar value, storing the part of the document you group on in a field of its own. The thrown error carries `field`, `fields`, `object` and `param: 'groupBy'`.
14+
15+
**Why a refusal.** The drivers share no meaning for a JSON document as a group key. Measured through `POST /api/v1/data/:object/query` over three rows with different documents under the grouped field: the in-memory driver answered 200 with one group holding every row, SQLite answered 200 with one group per serialized document, and PostgreSQL answered 500 `DATABASE_ERROR`. A `vector` field split the same three ways, and a date bucket over a `json` field answered one `null` bucket on memory and SQLite and 500 on PostgreSQL. No producer that groups by a structured-JSON field was found (no dataset, cube, view grouping or `groupBy` in the example apps names one), so no meaning is defined for it here.
16+
17+
**Who is affected.** A caller of `engine.aggregate` or of the REST query door that grouped by such a field on the in-memory driver or on SQLite and read the merged or per-serialization groups as real ones. On PostgreSQL the same query was already a 500. The analytics service's aggregate path (a cube query the native-SQL strategy declines, such as a time dimension with a granularity, or any cube query on the in-memory driver) reaches the engine and answers this refusal too.
18+
19+
**Unchanged.** A `groupBy` on any other type (`text`, `number`, a `multiple: true` select, a file field), a structured-JSON field as an AGGREGATED column (`count`, `count_distinct`, `min`, `max`), and an undeclared name, which the REST door answers `INVALID_FIELD` as unknown before the engine is reached.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/objectql': patch
4+
'@objectstack/plugin-security': minor
5+
---
6+
7+
feat(spec, objectql, plugin-security): one shared filter lowering, run once at the engine and RLS seams (ADR-0053 D-D1, amended)
8+
9+
Clause-②: yes
10+
11+
`@objectstack/spec/data` exports `lowerFilterCondition(filter, options?)` and its `FilterLoweringOptions` type. It is not exported from the package root entry. It is a pure `FilterCondition → FilterCondition` rewrite that applies three rules once:
12+
13+
- `$between` becomes `$gte` its minimum and `$lte` its maximum.
14+
- A `$lte` whose comparand is a bare `YYYY-MM-DD` day becomes `$lt` the next day, in the calendar-string domain. On the last supported day (`9999-12-31`) a lone `$lte` becomes `{ $null: false }`, and a `$between` keeps only its minimum.
15+
- The NULL-polarity guards the drivers already compile. A `$ne` of a value, a `$nin` or a `$notContains` holds for a row with no value. Every leaf of a `$not` operand is made total.
16+
17+
The rewrite is copy-on-write, idempotent and never refuses. A node it rewrites keeps its filter-subtree provenance mark. With `options.isDatetimeColumn` (a typed seam), the first two rules change only a declared `datetime` column. Without it they apply to every column.
18+
19+
As ADR-0053 D-D1 (amended 2026-09-30) requires, the seams now run it once, after the comparand doors and after filter-token resolution:
20+
21+
- **`@objectstack/objectql`** runs it on every filter position, typed by the object's declared fields. That covers `where` on `find`, `findOne`, `count`, `update` and `delete`, and `aggregate`'s `where`, `aggregations[i].filter` and `having`. `having` is typed by the aggregated row's columns, so `max` of a `datetime` field counts as a `datetime`. Drivers receive the lowered filter. A date macro such as `{today}` is resolved before the lowering reads it.
22+
- **`@objectstack/plugin-security`** runs it on every compiled RLS policy filter (`using` and `check`), right after the two comparand faces. `SecurityPlugin` now hands the compile seam the object's declared `datetime` columns (`RlsFieldGuard.datetime`). A guard without that set treats no column as `datetime`.
23+
24+
Row answers stay the same on every driver. Each driver keeps its own copy of these rules, and every copy gives the same answer on lowered input. One result changes. The engine evaluates `aggregate`'s `aggregations[i].filter` and `having` itself, and that evaluator now treats a row or group with no value the way every driver's `where` already does. It no longer counts such a row in a `$between` on a `datetime` column. It now keeps such a row under a `$not` over an ordering such as `$lt`.
25+
26+
Nothing is removed or renamed, and there is nothing to migrate.

0 commit comments

Comments
 (0)