Skip to content

fix(database): resolve a table update through its type to the modified count - #65

Merged
Upd4ting merged 1 commit into
campaign/integrationfrom
fix/update-result-type
Nov 2, 2026
Merged

Upd4ting merged 1 commit into
campaign/integrationfrom
fix/update-result-type

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Why

dms burns down its guardrail ratchet. Its row packages/interface-dms/src/auth/db/models/sessions.model.ts:27 (no-unsafe-assignment) comes from reading the result of a table update through its type, Awaited<ReturnType<ReturnType<SessionModel["table"]["update"]>["run"]>>. That type resolves to any, although update returns Query<number>. The goal is to type that write result honestly, so the consumer clears the row without a cast or a directive.

Cause and change

SingleSelection.update and Selection.update have two overloads. A type-level read (ReturnType<typeof x.update>) takes the last overload, here (document: (val: ValueProxy<T>) => U) => ExtractType<U> extends DeepPartial<T> ? Query<number> : never. U is not inferable there, so it resolves to unknown, the conditional is unresolved and the result reads as any.

The overloads are reordered: the updater overload first, the plain document overload (Query<number>) last. The two JSDoc blocks are split to match. No signature text changes.

Behaviour change

None at runtime. Calls resolve the same overload as before: a function argument matches only the updater overload, an object argument only the document overload. The .d.ts differs only by the order of the two update lines (api-diff below). A caller that reads ReturnType<typeof x.update> now gets Query<number>, so await ...run() is number instead of any. A caller that relied on any there and did not handle number fails to compile; the consumers were proven (see below).

Tests

src/tests/update_result.test.ts pins the types with Exact<A, B>, a new test helper that is false when either side is any. Before the change tsc --noEmit fails on both type assertions (Type 'true' is not assignable to type 'false'); after it, it passes. pnpm test: 212 passing.

Evidence

  • pnpm run checks (GITHUB_BASE_REF=campaign/integration): Checks: 8 passed, 0 failed.
  • api-diff against origin/campaign/integration: unchanged runtime entries (block below); the type diff is the reordering of the two update lines in Selection and SingleSelection only. Suppressions unchanged.
  • Consumer tgz proofs follow in a comment.

api-diff output

API: differences against origin/campaign/integration (committed HEAD).
diff --git a/base/_root/exports/root.d.ts b/head/_root/exports/root.d.ts
index 1cdef82..0fa7cd4 100644
--- a/base/_root/exports/root.d.ts
+++ b/head/_root/exports/root.d.ts
@@ -440,16 +440,16 @@ export declare class SchemaInstance extends StagedObject {
export declare class Selection extends Stream {
delete(): Query;
replace(document: DeepPartial & object): Query;

  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type SelectionKey = boolean | number | string;
    export declare class SingleSelection extends Datum {
    changes(): Query<Changes[]>;
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type StageCall = QueryStageMap[Name]["args"] extends [
    ] ? undefined extends QueryStageMap[Name]["options"] ? [
    diff --git a/base/_root/exports/subpath/schema.d.ts b/head/_root/exports/subpath/schema.d.ts
    index e0af7f7..0b74e8f 100644
    --- a/base/_root/exports/subpath/schema.d.ts
    +++ b/head/_root/exports/subpath/schema.d.ts
    @@ -427,16 +427,16 @@ export declare const Schemas: RegisteringProxy<(name: string, def: SchemaDefinit
    declare class Selection extends Stream {
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type SelectionKey = boolean | number | string;
    declare class SingleSelection extends Datum {
    changes(): Query<Changes[]>;
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type StageCall = QueryStageMap[Name]["args"] extends [
    ] ? undefined extends QueryStageMap[Name]["options"] ? [
    diff --git a/base/_root/exports/subpath/selection.d.ts b/head/_root/exports/subpath/selection.d.ts
    index 1ac66c8..7e04c3c 100644
    --- a/base/_root/exports/subpath/selection.d.ts
    +++ b/head/_root/exports/subpath/selection.d.ts
    @@ -383,16 +383,16 @@ interface Recorded<Options, Args extends readonly unknown[]> {
    export declare class Selection extends Stream {
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type SelectionKey = boolean | number | string;
    export declare class SingleSelection extends Datum {
    changes(): Query<Changes[]>;
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type StageCall = QueryStageMap[Name]["args"] extends [
    ] ? undefined extends QueryStageMap[Name]["options"] ? [
    diff --git a/base/_root/exports/subpath/staged-query.d.ts b/head/_root/exports/subpath/staged-query.d.ts
    index 66eee0c..b7a0d6f 100644
    --- a/base/_root/exports/subpath/staged-query.d.ts
    +++ b/head/_root/exports/subpath/staged-query.d.ts
    @@ -428,16 +428,16 @@ export declare class SchemaInstance extends StagedObject {
    export declare class Selection extends Stream {
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    export type SelectionKey = boolean | number | string;
    export declare class SingleSelection extends Datum {
    changes(): Query<Changes[]>;
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type StageCall = QueryStageMap[Name]["args"] extends [
    ] ? undefined extends QueryStageMap[Name]["options"] ? [
    diff --git a/base/_root/main.d.ts b/head/_root/main.d.ts
    index 1cdef82..0fa7cd4 100644
    --- a/base/_root/main.d.ts
    +++ b/head/_root/main.d.ts
    @@ -440,16 +440,16 @@ export declare class SchemaInstance extends StagedObject {
    export declare class Selection extends Stream {
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type SelectionKey = boolean | number | string;
    export declare class SingleSelection extends Datum {
    changes(): Query<Changes[]>;
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type StageCall = QueryStageMap[Name]["args"] extends [
    ] ? undefined extends QueryStageMap[Name]["options"] ? [
    diff --git a/base/_root/types.d.ts b/head/_root/types.d.ts
    index 1cdef82..0fa7cd4 100644
    --- a/base/_root/types.d.ts
    +++ b/head/_root/types.d.ts
    @@ -440,16 +440,16 @@ export declare class SchemaInstance extends StagedObject {
    export declare class Selection extends Stream {
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type SelectionKey = boolean | number | string;
    export declare class SingleSelection extends Datum {
    changes(): Query<Changes[]>;
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type StageCall = QueryStageMap[Name]["args"] extends [
    ] ? undefined extends QueryStageMap[Name]["options"] ? [
    diff --git a/base/_root/typesVersions/%2A/schema/0.d.ts b/head/_root/typesVersions/%2A/schema/0.d.ts
    index e0af7f7..0b74e8f 100644
    --- a/base/_root/typesVersions/%2A/schema/0.d.ts
    +++ b/head/_root/typesVersions/%2A/schema/0.d.ts
    @@ -427,16 +427,16 @@ export declare const Schemas: RegisteringProxy<(name: string, def: SchemaDefinit
    declare class Selection extends Stream {
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type SelectionKey = boolean | number | string;
    declare class SingleSelection extends Datum {
    changes(): Query<Changes[]>;
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type StageCall = QueryStageMap[Name]["args"] extends [
    ] ? undefined extends QueryStageMap[Name]["options"] ? [
    diff --git a/base/_root/typesVersions/%2A/selection/0.d.ts b/head/_root/typesVersions/%2A/selection/0.d.ts
    index 1ac66c8..7e04c3c 100644
    --- a/base/_root/typesVersions/%2A/selection/0.d.ts
    +++ b/head/_root/typesVersions/%2A/selection/0.d.ts
    @@ -383,16 +383,16 @@ interface Recorded<Options, Args extends readonly unknown[]> {
    export declare class Selection extends Stream {
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type SelectionKey = boolean | number | string;
    export declare class SingleSelection extends Datum {
    changes(): Query<Changes[]>;
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type StageCall = QueryStageMap[Name]["args"] extends [
    ] ? undefined extends QueryStageMap[Name]["options"] ? [
    diff --git a/base/_root/typesVersions/%2A/staged-query/0.d.ts b/head/_root/typesVersions/%2A/staged-query/0.d.ts
    index 66eee0c..b7a0d6f 100644
    --- a/base/_root/typesVersions/%2A/staged-query/0.d.ts
    +++ b/head/_root/typesVersions/%2A/staged-query/0.d.ts
    @@ -428,16 +428,16 @@ export declare class SchemaInstance extends StagedObject {
    export declare class Selection extends Stream {
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    export type SelectionKey = boolean | number | string;
    export declare class SingleSelection extends Datum {
    changes(): Query<Changes[]>;
    delete(): Query;
    replace(document: DeepPartial & object): Query;
  • update(document: DeepPartial & object): Query;
    update(document: (val: ValueProxy) => U): ExtractType extends DeepPartial ? Query : never;
  • update(document: DeepPartial & object): Query;
    }
    type StageCall = QueryStageMap[Name]["args"] extends [
    ] ? undefined extends QueryStageMap[Name]["options"] ? [
    API: skipped .agents/skills/verify-interface, private.
    Runtime API: 1 packages, 11 entries unchanged against origin/campaign/integration (committed HEAD).
    No behavioural probe ran.

Suppressions in authored code files (origin/campaign/integration and committed HEAD). A lint directive counts once per rule it names; * is a directive naming none.

Suppressions base HEAD
Lint suppressions, all rules 7 7
Lint suppressions, @typescript-eslint/no-unused-expressions 1 1
Lint suppressions, anti-slop/no-chained-type-assertions 3 3
Lint suppressions, typescript/no-unnecessary-type-parameters 1 1
Lint suppressions, typescript/no-unused-vars 1 1
Lint suppressions, unicorn/no-thenable 1 1
@ts-expect-error 2 2
@ts-ignore 0 0
SAFETY: comments 5 5
Lint config rule-offs, all rules 0 0

Correction (verifier, after landing)

Not every call resolves the same overload as before. A call whose argument is typed any, such as table.update(anyValue), now resolves to never, so .run() on it fails to compile (TS2339). No proven consumer does this. It goes in the release notes as a compile-time break for any-typed writes.

…d count

Reading the result of `update` through its type (`ReturnType<typeof table.update>`) took the last overload, whose conditional return resolved to `any`. The overloads are reordered so that the document overload, which returns `Query<number>`, is the one a type-level read sees. Calls type exactly as before.
@Upd4ting
Upd4ting marked this pull request as ready for review November 2, 2026 21:13
@Upd4ting
Upd4ting merged commit 062f88a into campaign/integration Nov 2, 2026
7 checks passed
@Upd4ting
Upd4ting deleted the fix/update-result-type branch November 2, 2026 21:13
Upd4ting added a commit to AntelopeJS/dms that referenced this pull request Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant