Skip to content

Commit d616c8c

Browse files
committed
Merge origin/main into claude/issue-20493-ja-es-object-labels
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
2 parents 8f3e7ca + fb38607 commit d616c8c

57 files changed

Lines changed: 2337 additions & 284 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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `os migrate meta` guidance for the `field-*`, `export-*`, `api-*`, `dataset-*`, `hook-*` and `metadata-*` migration entries states each lesson in words instead of citing tracker numbers
6+
7+
Clause-②: no
8+
9+
The ADR-0087 semantic entries of the `field-*` family (the runtime `field` write door, the
10+
`maxLength` / `minLength` / `scale` / `precision` refusals, `scale` on a currency field,
11+
`multiple` on a type that holds one value, and predicates that read through a reference),
12+
the `export-*` family (the export permission axis, the eight constraint keys retired from
13+
`ExportFieldMeta` and the retired export-job API family), the `api-*` family (the runtime `api` write door,
14+
the split API entry and two duration keys renamed with their unit), the `dataset-*` family
15+
(the aggregate × field-type refusals and the nested-relation list refused at save), the
16+
`hook-*` family (the retired hook-session `roles` and the two `registerHook` refusals) and
17+
the `metadata-*` family (the retired customization protocol, the re-partitioned endpoint
18+
switches, the metadata-manager cache keys and the retired `additionalTypes`) are printed by
19+
`os migrate meta` as the header, `why:` and `verify:` lines of a manual change. Their text
20+
sent the reader to issue-tracker, decision-batch and ruling-record numbers — some of which
21+
no longer resolve, and some in another repository — for what a ruling, measurement or fix
22+
had decided; it now says what was decided, in the sentence being read. ADR ids are kept.
23+
24+
Text only: no entry id, `from` / `to`, conversion or matching logic changes, and the chain
25+
rewrites exactly what it rewrote before. One entry's `surface` (the header line of
26+
`dataset-measure-aggregate-field-type-refused`) drops the two tracker numbers it carried and
27+
names nothing else differently. The generated migration registry, `spec-changes.json` and
28+
the protocol upgrade guide carry the same text.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/driver-sql': minor
3+
'@objectstack/driver-turso': minor
4+
'@objectstack/driver-memory': minor
5+
'@objectstack/driver-mongodb': minor
6+
'@objectstack/formula': minor
7+
'@objectstack/objectql': minor
8+
'@objectstack/spec': minor
9+
---
10+
11+
feat(drivers,formula,objectql): the engine's filter faces answer the staged `$empty` operator (#20444)
12+
13+
Clause-②: yes (widening)
14+
15+
`$empty: true | false` is declared by `@objectstack/spec` (`FieldOperatorsSchema`) with a per-type meaning: a text-like field is empty when it is null or `''`, a multi-value field (multiselect, checkboxes, tags, or a select / radio / lookup / user / file / image with `multiple: true`) when it is null or `[]`, and every other type only when it is null. `$empty: false` is the exact complement. Until now every face in this list refused it (`INVALID_FILTER` / 400), except `matchesFilterCondition`, which answered `false` for every record. **A driver or evaluator called directly now answers it:**
16+
17+
- **By the field's declared type**, through the spec's one expansion (`expandEmptyOperator`): `driver-sql`'s filter compiler (and so `driver-sqlite-wasm` and `driver-turso`'s local transport, which inherit it), `driver-turso`'s remote transport, `driver-memory`'s query path (`find` / `count` / `update` / `delete`) and `driver-mongodb`'s `translateFilter` (its `find`, its aggregate `$match`). The declaration is the one each driver already receives — `initObjects` / `registerObjectMetadata` / `registerExternalObject` on the SQL family, `syncSchema` on the others. On SQL a multi-value field's empty list is tested as stored JSON per dialect (SQLite `json_array_length` behind a `json_valid` guard, PostgreSQL a `jsonb` comparison, MySQL `JSON_LENGTH`), never as an equality comparand.
18+
- **By value** — null, a missing value, `''` and `[]` are empty (`isEmptyFilterValue`) — on the faces that read no field declaration: `@objectstack/formula`'s `matchesFilterCondition` (the RLS write-side `check`), `driver-memory`'s reference matcher, and `@objectstack/objectql`'s `having` and per-aggregation `filter`. In `having`, a `count` or `sum` holding `0` is not empty.
19+
20+
**Refused, never guessed** (`INVALID_FILTER` / 400): `$empty` on a field whose declaration the driver does not hold (a table built outside its registration, a builtin column such as `id`, a field with no `type`, or `translateFilter` / `RemoteTransport` used standalone without a declaration), a multi-value field on a SQL dialect the driver does not model, and a flag that is not a boolean. `driver-memory`'s analytics (cube) face refuses `$empty` as an operator it cannot compile, as it does `$null`.
21+
22+
New optional API: `translateFilter(where, temporalKind?, valueShape?)` in `@objectstack/driver-mongodb` takes a declared-value-shape resolver (type `ValueShapeResolver`), and `buildAggregationPipeline` a `valueShape` option; `RemoteTransport.setDeclaredValueShapeResolver` in `@objectstack/driver-turso`, which `TursoDriver` wires. `@objectstack/spec`'s shared `FILTER_LOGIC_CASES` table gains seven `$empty` cases: a backend that runs it answers `$empty` or goes red, and its harness must declare the fixture's columns.
23+
24+
`$empty` stays staged: it is not in `FILTER_OPERATORS`, so the engine's front door still refuses it until the flip card adds it, and the view operators `is_empty` / `is_not_empty` still lower to `$null`.

‎docs/protocol-upgrade-guide.md‎

Lines changed: 10 additions & 10 deletions
Large diffs are not rendered by default.

‎packages/cli/test/migrate-meta-engine-guidance.test.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
* `os migrate meta` — the guidance it prints for the ADR-0087 semantic entries
55
* of the COVERED families (`engine-*`, `ui-*`, `plugin-*`, `driver-*`,
66
* `kernel-*`, `system-*`, `datasource-*`, `filter-*`, `action-*`, `data-*`,
7-
* `element-*`) states each lesson in words and carries no tracker number.
7+
* `element-*`, `field-*`, `export-*`, `api-*`, `dataset-*`, `hook-*`,
8+
* `metadata-*`) states each lesson in words and carries no tracker number.
89
*
910
* ## What this pins
1011
*
@@ -76,6 +77,7 @@ const TRACKER_ID = /#\d{4,5}\b/;
7677
const COVERED_PREFIXES = [
7778
'engine-', 'ui-', 'plugin-', 'driver-', 'kernel-', 'system-',
7879
'datasource-', 'filter-', 'action-', 'data-', 'element-',
80+
'field-', 'export-', 'api-', 'dataset-', 'hook-', 'metadata-',
7981
];
8082

8183
/**
@@ -89,12 +91,19 @@ const REWRITTEN = [
8991
'action-descriptor-resume-authority-default-flip',
9092
'action-engine-facade-find-query-envelope',
9193
'action-session-roles-to-positions',
94+
'api-assembled-entry-split',
95+
'api-error-retry-after-unit-in-key',
96+
'api-runtime-config-durations-unit-in-key',
97+
'api-runtime-create-withdrawn',
9298
'data-driver-find-stream-retired',
9399
'data-driver-query-omit-object',
94100
'data-engine-batch-retired',
95101
'data-field-changed-event-retired',
96102
'data-file-value-duration-unit-in-key',
97103
'data-nosql-query-options-timeout-unit-in-key',
104+
'dataset-filter-nested-relation-equality-array-refused-at-save',
105+
'dataset-measure-aggregate-field-type-refused',
106+
'dataset-measure-selecting-aggregate-field-type-refused',
98107
'datasource-config-inline-credential-refused',
99108
'datasource-config-mongo-options-credential-refused',
100109
'datasource-config-placeholder-refused',
@@ -118,6 +127,16 @@ const REWRITTEN = [
118127
'engine-find-formula-filter-refused',
119128
'engine-find-formula-order-by-refused',
120129
'engine-update-upsert-retired',
130+
'export-axis-opt-in',
131+
'export-field-meta-constraints-retired',
132+
'export-job-family-retired',
133+
'field-currency-scale-refused',
134+
'field-max-length-malformed-or-misplaced-refused',
135+
'field-min-length-malformed-or-misplaced-refused',
136+
'field-multiple-non-capable-type-refused',
137+
'field-predicate-reference-traversal-refused',
138+
'field-runtime-create-withdrawn',
139+
'field-scale-precision-integer-refused',
121140
'filter-between-blank-endpoint-refused',
122141
'filter-between-field-reference-endpoint-refused',
123142
'filter-comparand-types-and-widget-nested-slots-refused-at-save',
@@ -129,6 +148,9 @@ const REWRITTEN = [
129148
'filter-query-face-comparands-refused-at-save',
130149
'filter-regex-options-retired',
131150
'filter-text-operator-declared-type-refused',
151+
'hook-context-session-roles-retired',
152+
'hook-register-empty-object-target-refused',
153+
'hook-register-undispatched-lifecycle-event-refused',
132154
'kernel-compatibility-matrix-estimated-migration-time-unit-in-key',
133155
'kernel-context-preview-mode-retired',
134156
'kernel-event-bus-retention-unit-in-key',
@@ -138,6 +160,11 @@ const REWRITTEN = [
138160
'kernel-plugin-security-durations-unit-in-key',
139161
'kernel-runtime-config-timeout-unit-in-key',
140162
'kernel-startup-orchestrator-durations-unit-in-key',
163+
'metadata-customization-protocol-retired',
164+
'metadata-endpoints-switch-radius-repartitioned',
165+
'metadata-manager-config-cache-ttl-unit-in-key',
166+
'metadata-manager-config-inert-cache-keys-retired',
167+
'metadata-plugin-additional-types-retired',
141168
'plugin-activation-events-retired',
142169
'plugin-auto-restart-never-reinitialised',
143170
'plugin-manifest-contributes-dead-members-retired',

‎packages/drivers/driver-memory/src/filter-refusal.ts‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,13 @@ export const SUPPORTED_FIELD_OPERATORS: ReadonlySet<string> = new Set<string>([
364364
...FILTER_OPERATORS,
365365
'$like',
366366
'$ilike',
367+
// [#20444] The staged emptiness flag, admitted BY HAND for the reason the
368+
// `$like` paragraph above gives, and under its ordering rule: both arms land
369+
// with this entry — the reference matcher judges the stored value
370+
// (`isEmptyFilterValue`, the spec's reading for a face holding no field
371+
// declaration) and the live query path the field's DECLARED row
372+
// (`expandEmptyOperator`, from the declaration `syncSchema` recorded).
373+
'$empty',
367374
]);
368375

369376
/** The vocabulary as it appears in a refusal message, in declaration order. */
@@ -611,6 +618,42 @@ export function nonBooleanNullComparandError(field: string, value: unknown, path
611618
);
612619
}
613620

621+
/**
622+
* [#20444] A non-boolean `$empty` comparand. The leading sentence is
623+
* `driver-sql`'s `nonBooleanEmptyComparandError`, verbatim — one condition,
624+
* one wording (#5240).
625+
*/
626+
export function nonBooleanEmptyComparandError(field: string, value: unknown, path: string): Error {
627+
return unsupportedFilterError(
628+
`Operator "$empty" on field "${field}" requires a boolean comparand (true or false). ` +
629+
`Received ${describeFilterOperand(value)} (${safeShapePreview(value)}) at ${path}. ` +
630+
`@objectstack/spec FieldOperatorsSchema declares $empty as a boolean: true asks for the ` +
631+
`empty rows, false for their exact complement.`,
632+
);
633+
}
634+
635+
/**
636+
* [#20444] `$empty` on the live query path, aimed at a field this driver holds
637+
* no declaration for — an object never passed through `syncSchema`, a field
638+
* its schema does not name, or one declared with no `type`.
639+
*
640+
* What counts as empty is the field's DECLARED row of the ruled table, and the
641+
* live path reads it from the declaration rather than from a value, so without
642+
* one there is no answer to give: refused, never guessed. The reference matcher
643+
* (`memory-matcher.ts`) is the face that holds NO declarations at all, and it
644+
* judges the stored value instead — the spec's reading for such a face.
645+
*/
646+
export function undeclaredEmptyOperatorFieldError(field: string, path: string): Error {
647+
return unsupportedFilterError(
648+
`Operator "$empty" on field "${field}" at ${path} targets a field whose declaration this ` +
649+
`driver does not hold (no declared type — the object's schema was never synced, or does not ` +
650+
`declare the field). What counts as empty is the field's DECLARED row of the ruled table — ` +
651+
`null or '' for a text-like type, null or [] for a multi-value field, null only for every ` +
652+
`other type — so the operator is refused rather than guessed. Declare the field, or use ` +
653+
`"$null" for "has no value".`,
654+
);
655+
}
656+
614657
/**
615658
* [#5702] A RETIRED filter operator in a field constraint.
616659
*
@@ -895,6 +938,13 @@ function assertFieldConstraintShape(
895938
if (op === '$null' && typeof spec[op] !== 'boolean') {
896939
throw nonBooleanNullComparandError(field, spec[op], `${path}.$null`);
897940
}
941+
// [#20444] `$empty`'s comparand is a boolean by the same declaration
942+
// (`FieldOperatorsSchema`), refused on this walk for the same reason: both
943+
// faces of this package evaluate `true` / `false` exhaustively, so a third
944+
// value would land on whichever side each arm happens to default to.
945+
if (op === '$empty' && typeof spec[op] !== 'boolean') {
946+
throw nonBooleanEmptyComparandError(field, spec[op], `${path}.$empty`);
947+
}
898948
// [#16810] An ARRAY comparand on a single-value comparison — the operator
899949
// spelling of the implicit-equality position refused at the top of this
900950
// function, and the same cell `@objectstack/spec`'s comparand door leaves

0 commit comments

Comments
 (0)