Repository navigation
feat(staged-query)!: drop casts, thenable queries and caller-typed schema lookups - #91
Merged
Merged
Conversation
…hema lookups Remove every suppression guardrails.suppressions.json still allowed on published declarations, by fixing the code each one hid: - cast() on Datum, Stream and ValueProxy returns a new object of that class with the same stages, through a StagedObject constructor form `new X(undefined, previous)` that copies the stages without recording one. No type assertion is left. - Query<T> declares what it yields, `QueryYield<T>`, so the build-only entry tells Query<string> from Query<number> and ExtractType reads T back from a Datum or Stream; QueryExecution types its cursor items from it. - Queries are no longer thenable: `then` is gone and QueryExecution no longer extends PromiseLike. The conformance suite, docs, README, the shipped skill and the verify scenarios run queries with run(). - Schema.get(id) takes no type arguments and returns what the registry holds, Schema<any, unknown> | undefined. - The protected callfunc takes one or two typed StagedConstructors and calls func without a tuple cast; group passes the argument class its mapper declares. The suppression budget drops to 0 for every rule and SAFETY comment; the 2 @ts-expect-error in atomic_mutation.test.ts stay for #90. BREAKING CHANGE: `await query` no longer runs a query; call run(), or read it with cursor() or for await. cast() returns a new object instead of its receiver, and on a Table, Selection or SingleSelection returns a plain Stream or Datum. Schema.get() takes no type arguments and types the schema's definition unknown. The build-only Query<A> is no longer assignable to Query<B>. The protected StagedObject#callfunc takes one or two typed constructors. UPGRADING.md has before and after snippets.
Upd4ting
added a commit
that referenced
this pull request
Nov 13, 2026
## Why #91 is a breaking change, so it has to ship as 0.4.0: database modules cap their range below the next minor. This repo's release workflow takes no `release-version` input. Dispatching it with one fails with `Unexpected inputs provided`, and the automatic increment would publish the breaking change as a patch. This PR adds the optional input and passes it to `AntelopeJS/.github/.github/workflows/release-npm-public.yml@v1`, the same way tooling-configs' `release.yml` does. ## What `.github/workflows/release.yml` gets the optional string input `release-version`, which it passes to the reusable workflow. When it is empty, the automatic increment applies as before. ## Proof Before, dispatching with `-f release-version=0.4.0`: ``` Unexpected inputs provided ``` After, the file is byte-identical to tooling-configs' `release.yml`: ``` $ gh api repos/AntelopeJS/tooling-configs/contents/.github/workflows/release.yml -q .content | base64 -d | diff - .github/workflows/release.yml && echo identical-to-tooling-configs identical-to-tooling-configs ``` The reusable workflow at `v1` declares the input and checks it: ``` release-version: description: Exact stable version; empty uses the automatic increment default: "" type: string ... test "$CHANNEL" = latest && [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || { echo "release-version must be a stable version on the latest channel." >&2 ``` `pnpm run checks`: `Checks: 11 passed, 0 failed.` I did not dispatch a test run, because it would publish a release.
Upd4ting
added a commit
that referenced
this pull request
Nov 13, 2026
## Why A user upgrading `@antelopejs/interface-database` to 0.4.0 should learn from its release notes which changes break their code and how to adapt. Today the `## v0.4.0` section of `CHANGELOG.md` and the v0.4.0 GitHub release name #91 only as "Drop casts, thenable queries and caller-typed schema lookups". Its upgrade notes live in `UPGRADING.md`. The release commit (7268503) does not copy them, so this PR does, as AGENTS.md asks. ## What This adds a `### Upgrade notes` block under `## v0.4.0` in `CHANGELOG.md`, right after the compare link, where the v0.3.0 block sits. It holds the `UPGRADING.md` v0.4.0 bullets verbatim, snippets included. It lists no implementer action: database modules implement nothing new, and the conformance suite in `dist/tests` already runs every query with `run()`. ## Pending step for Antony I have not edited the v0.4.0 GitHub release body. Paste the text under **Release notes to paste** into https://github.com/AntelopeJS/interface-database/releases/tag/v0.4.0. It is the current release body with the same upgrade-notes block after the compare link. ## Release notes to paste ````markdown [compare changes](v0.3.3...v0.4.0) ### Upgrade notes - **A query is no longer a promise.** `Query` has no `then()`, and `QueryExecution` no longer extends `PromiseLike`. `await query` gives back the query object and sends nothing to the provider. Run a query with `run()`, or read it with `cursor()` or `for await`. A query in `Promise.all([...])`, or returned from an `async` function, is no longer run either. TypeScript rejects a query passed where a `PromiseLike` is expected, and the `typescript/await-thenable` lint rule reports `await` on a query and a query passed to `Promise.all`, `Promise.race` or another promise aggregator. The conformance suite in `dist/tests` runs every query with `run()`. A database module implements nothing new. ```ts // Before const adults = await users.filter((u) => u.key("age").ge(18)); // After const adults = await users.filter((u) => u.key("age").ge(18)).run(); ``` - **`cast()` returns a new object.** `Datum#cast`, `Stream#cast` and `ValueProxy#cast` return a new object with the same stages, built by the new `StagedObject` constructor form `new X(undefined, previous)`, which records no stage. `cast()` on a `Table`, a `Selection` or a `SingleSelection` returns a plain `Stream` or `Datum`, the class its type already declared. Code that compared the result with its receiver, or relied on it keeping the subclass methods at runtime, such as a table's `insert` or `getAll`, keeps the receiver instead. ```ts // Before: the same object, still a Table at runtime table.cast<Row>() === table; // true // After: a new Stream with the same stages table.cast<Row>() === table; // false table.cast<Row>().build(); // equal to table.build() ``` - **`Schema.get()` takes no type arguments.** The registry keeps none, so `Schema.get(id)` returns `Schema<any, unknown> | undefined`: tables hold `any` documents, and the definition is typed `unknown`. A typed lookup such as `Schema.get<{ users: User }>(id)` fails to compile (TS2558). Query typed tables through the schema object the constructor returned. On the runtime entry, `Schema.list().get(id)` returns the schema with its definition typed `SchemaDefinition`. ```ts // Before const schema = Schema.get<{ users: User }>("myapp")!; // After: export the schema you define, and import it export const schema = new Schema<{ users: User }>("myapp", definition); ``` - **The build-only `Query<T>` keeps its result type.** Loaded from `@antelopejs/interface-database/staged-query` alone, `Query<T>` now uses `T`, so `ExtractType` reads the result type of a `Datum` or a `Stream` a mapper returns, and a `Query<string>` is no longer assignable to a `Query<number>`. Code that relied on that assignment fails to compile. ```ts // Before, build-only entry: compiled, typed unknown const q: Query<number> = schema.createInstance(); // Query<string> // After: TS2322 ``` - **`StagedObject#callfunc` checks its argument classes.** The protected method takes one or two `StagedConstructor`s, and each must build the parameter type `func` declares. A subclass that passed `ValueProxy<unknown>` for a parameter typed `ValueProxy<T>` passes `ValueProxy<T>`. ```ts // Before this.callfunc(mapper, Stream<T>, ValueProxy<unknown>); // After this.callfunc( mapper, Stream<T>, ValueProxy<K extends keyof T ? T[K] : unknown>, ); ``` ### 🚀 Enhancements - **staged-query:**⚠️ Drop casts, thenable queries and caller-typed schema lookups ([#91](#91)) ### 💅 Refactors - **tests:** Drop the no-unused-expressions suppression ([#88](#88)) ### 📖 Documentation - **readme:** List the mysql and postgres backends among the implementations ([#89](#89)) ### ✅ Tests - **atomic:** Move off-contract refusal checks into an untyped spec ([#90](#90)) ### 🤖 CI - **release:** Accept an exact release-version on dispatch ([#92](#92)) ####⚠️ Breaking Changes - **staged-query:**⚠️ Drop casts, thenable queries and caller-typed schema lookups ([#91](#91)) ### ❤️ Contributors - Antony Rizzitelli ([@Upd4ting](http://github.com/Upd4ting)) ```` ## Proof The inserted block matches `UPGRADING.md`'s v0.4.0 block line for line: ``` $ diff <(grep -v '^$' notes-from-UPGRADING.md) <(grep -v '^$' block-in-CHANGELOG.md) && echo verbatim verbatim ``` CHANGELOG diff: ````diff diff --git a/CHANGELOG.md b/CHANGELOG.md index bb189cc..d22fdb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,57 @@ [compare changes](v0.3.3...v0.4.0) +### Upgrade notes + +- **A query is no longer a promise.** `Query` has no `then()`, and `QueryExecution` no longer extends `PromiseLike`. `await query` gives back the query object and sends nothing to the provider. Run a query with `run()`, or read it with `cursor()` or `for await`. A query in `Promise.all([...])`, or returned from an `async` function, is no longer run either. TypeScript rejects a query passed where a `PromiseLike` is expected, and the `typescript/await-thenable` lint rule reports `await` on a query and a query passed to `Promise.all`, `Promise.race` or another promise aggregator. The conformance suite in `dist/tests` runs every query with `run()`. A database module implements nothing new. + + ```ts + // Before + const adults = await users.filter((u) => u.key("age").ge(18)); + // After + const adults = await users.filter((u) => u.key("age").ge(18)).run(); + ``` + +- **`cast()` returns a new object.** `Datum#cast`, `Stream#cast` and `ValueProxy#cast` return a new object with the same stages, built by the new `StagedObject` constructor form `new X(undefined, previous)`, which records no stage. `cast()` on a `Table`, a `Selection` or a `SingleSelection` returns a plain `Stream` or `Datum`, the class its type already declared. Code that compared the result with its receiver, or relied on it keeping the subclass methods at runtime, such as a table's `insert` or `getAll`, keeps the receiver instead. + + ```ts + // Before: the same object, still a Table at runtime + table.cast<Row>() === table; // true + // After: a new Stream with the same stages + table.cast<Row>() === table; // false + table.cast<Row>().build(); // equal to table.build() + ``` + +- **`Schema.get()` takes no type arguments.** The registry keeps none, so `Schema.get(id)` returns `Schema<any, unknown> | undefined`: tables hold `any` documents, and the definition is typed `unknown`. A typed lookup such as `Schema.get<{ users: User }>(id)` fails to compile (TS2558). Query typed tables through the schema object the constructor returned. On the runtime entry, `Schema.list().get(id)` returns the schema with its definition typed `SchemaDefinition`. + + ```ts + // Before + const schema = Schema.get<{ users: User }>("myapp")!; + // After: export the schema you define, and import it + export const schema = new Schema<{ users: User }>("myapp", definition); + ``` + +- **The build-only `Query<T>` keeps its result type.** Loaded from `@antelopejs/interface-database/staged-query` alone, `Query<T>` now uses `T`, so `ExtractType` reads the result type of a `Datum` or a `Stream` a mapper returns, and a `Query<string>` is no longer assignable to a `Query<number>`. Code that relied on that assignment fails to compile. + + ```ts + // Before, build-only entry: compiled, typed unknown + const q: Query<number> = schema.createInstance(); // Query<string> + // After: TS2322 + ``` + +- **`StagedObject#callfunc` checks its argument classes.** The protected method takes one or two `StagedConstructor`s, and each must build the parameter type `func` declares. A subclass that passed `ValueProxy<unknown>` for a parameter typed `ValueProxy<T>` passes `ValueProxy<T>`. + + ```ts + // Before + this.callfunc(mapper, Stream<T>, ValueProxy<unknown>); + // After + this.callfunc( + mapper, + Stream<T>, + ValueProxy<K extends keyof T ? T[K] : unknown>, + ); + ``` + ### 🚀 Enhancements - **staged-query:**⚠️ Drop casts, thenable queries and caller-typed schema lookups ([#91](#91)) ```` `pnpm run checks`: ``` Checks: 11 passed, 0 failed. ``` The CI docs check reads only `README.md` and `docs/`, so this PR does not change its inputs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Antony wants the lint in this repo to hold with no escape hatches (ANT-1103). #88 and #90 removed every suppression that could go without a published change. The rest sat on published signatures, and Antony approved one breaking PR for them. With it, every ceiling in
guardrails.suppressions.jsonis 0: nooxlint-disable,@ts-expect-errororSAFETY:comment is left anywhere in the repo. It must ship as 0.4.0, because database modules cap their range below the next minor.Breaking changes
Each item is in
UPGRADING.mdunder## v0.4.0, with a before and after snippet.thenis gone fromQuery.prototype, andQueryExecutionno longer extendsPromiseLike.await querygives back the query object and runs nothing. Callers use.run(),cursor()orfor await. This removes theunicorn/no-thenabledirective. A type-aware codemod moved everyawait <query>insrc/tests,docs/,README.md,skills/and.agents/skills/verify-interfaceto.run(), and lint (typescript/await-thenable) reports none left.cast<U>()returns a new object with the same stages onDatum,StreamandValueProxy. It used to returnthisthroughas unknown as. A new constructor overload,new X(undefined, previous), copiesprevious's stages and records none. This removes 3SAFETY:comments and 1anti-slop/no-chained-type-assertionsdirective. On aTable,SelectionorSingleSelection,cast()now returns a plainStreamorDatum, the class its type already declared.Schema.get(id)takes no type arguments and returnsSchema<any, unknown> | undefined, which is what the registry holds. This removes 1SAFETY:comment. Docs that calledSchema.get<{ users: User }>(…)now use the schema object the constructor returned.callfunctakes 1 or 2 typed constructors (two overloads) and callsfuncwithout a tuple cast. This removes 1SAFETY:comment. The recorded stages are byte-identical, with argument ids still allocated a, then b.Stream#groupnow passes theValueProxytype argument its mapper declares, which only changes the type.Query<T>now usesT. This removes thetypescript/no-unused-varsandtypescript/no-unnecessary-type-parametersdirectives, and fixes a real type hole. Loaded fromstaged-queryalone,Query<string>was assignable toQuery<number>, andExtractType<Datum<number>>wasunknown. The class declaresdeclare protected readonly [YIELDS]?: QueryYield<T>.The
Query<T>member, for the adversarial checkIt is a declared-only member:
declareemits nothing, and no instance ever has it. In the build-only entry, a query holds stages and never a value of typeT. So any member usingTis declared, not stored. Two reviewers (Opus and Sonnet) each judged it, and both found it a real fix, not a brand. They ran the type probe added toscripts/test-staged-query.cjsagainst main's build, and all four assertions fail there. Neither found a fix without a member:Datum<infer A>/Stream<infer A>inExtractTypestill leavesTunused, and loses bareQuery<T>.QueryExecution<T>into the build-only entry would give it arun()it does not have.Tcannot be dropped, because declaration merging withsrc/query.tsneeds the same type parameters.The key is
YIELDS, adeclare const YIELDS: unique symbolno other module can name, so a subclass property namedyieldsdoes not collide with it.No
SAFETY:comment is added.Proof
New tests at
origin/main(55853ef), with the head's test files copied in:At head:
pnpm run checks:Checks: 11 passed, 0 failed.tsc -p docs/tsconfig.json) is clean.flowchart LR subgraph Before A1[await query] -->|then| R1[RunQuery] C1["cast()"] -->|as unknown as| S1[same object] end subgraph After A2[await query] --> Q2[the query object, nothing runs] B2["query.run()"] --> R2[RunQuery] C2["cast()"] -->|"new X(undefined, this)"| S2[new object, same stages] endverify-interface
A thread that did not write the change ran
.agents/skills/verify-interfaceat 452d96c. All 30 journeys the harness registers pass. Thequery-executionevidence:minor-bumpandevery-functiondid not run.features/smoke.mddeliberately leaves both unregistered in this repo. The only later commit, b95e426, keepsStagedObject.lengthat 2 and moves the argument helper to module scope.pnpm run checksandpnpm test:staged-querycover it, and both pass.API diff
antelopejs-api-diff origin/mainexits 1 here, as expected for a breaking PR. Its full output is 137 KB, more than twice GitHub's 65,536-character body limit, so it is not pasted. To reproduce:pnpm exec antelopejs-api-diff origin/mainat the head of this PR. Its closing stamp then readsbase=55853ef… head=b95e426… lines=3837 digest=c2620097c6c135f4. What it reports:typesVersionsfile):Query<T>gains the[YIELDS]member and the newQueryYield<T>interface.QueryExecution<T>dropsPromiseLike<T>and typescursor()fromQueryYield<T>["item"].Schema.getloses its type parameters.StagedObjectgains the(undefined, previous)constructor overload.callfuncbecomes two overloads.thenis removed fromQuery.prototype, in the root,queryandmainentries.StagedObject.prototype.callfunc.lengthgoes from 1 to 0.StagedObject.lengthstays 2, and no static is added.Lint suppressions, all rules4 → 0 andSAFETY:comments 5 → 0. Every other row was already 0.Downstream
Every module or app that awaits a query must call
.run()once it moves to 0.4.0. From reading their source, these also stop compiling:getTenantSchema(): SchemareturningSchema.get(…).Object.hasOwn(schema.definition, …), and its console'sSchema.get<unknown, SchemaDefLike>.UPGRADING.mdpoints root callers toSchema.list().get(id), which stays typed withSchemaDefinition.