Skip to content

Commit e826e77

Browse files
refactor(driver-sql): put SqlDriver into facade form with the sql-driver codemod (sql-driver.ts 18,334 -> 10,979, body-identical) (#30139)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB2J1rGmLUVawKQeMT3jK9KS)_ Part of #30105 ## 维护者速读 - 改了什么:给 `scripts/codemod-split-sql-driver.mjs`(#29520 的试点脚本)加上 `engine.ts` / `protocol.ts` 已有的门面层,并重新生成 `sql-driver.ts`。`SqlDriver` 成为门面:158 个方法体移入 21 个无状态 facet 类(静态方法,`this` 是一个具名 host),类上各留一行 `.call(xHost.of(this), …)` 委派;私有方法的文档块随方法体移走(21 个),公开和受保护方法的文档块留在门面;状态全部留在门面。生成文件未手改。 - 为什么改:#29465 大文件拆分,裁决 6632500982 把门面形态(6624258595)扩展到本文件;`.d.ts` 门槛按 6624345766 取「公开面相等」。 - 风险与回滚:纯搬移。158/158 个方法体逐字节等于 base,撤销拆分逐字节重建 base 的 `sql-driver.ts`;两次 `--write` 输出树 sha256 相同。公开面 `.d.ts` 相等(对照抓到)。driver-sql 测试 base 与 head 都是 3,859 通过;turso、sqlite-wasm 两个子类驱动计数不变。回滚即 revert。 - 先看:① 「Facade reading」一节的 28 条例外(其中哪些是 codemod 限制、需另立卡);② codemod 的 `CLASS_HEAD_SECTION` 和新加的 `logDurabilityFailure` pin;③ `check-tenant-chokepoint.mjs` 新登记的 4 个模块。 - 两分钟验证:`pnpm install --frozen-lockfile` 后运行 `node scripts/codemod-split-sql-driver.mjs --verify --base 21510bb`,预期 `158/158 moved bodies identical to base`、`sql-driver.ts 18334 -> 10979 lines`,退出码 0;`--self-test` 67 例通过;`node scripts/check-facade-delegation-only.mjs` 退出码 0。 - 需要你做的:改动超过 3,000 行(+4,990 / −3,121,含生成文件),需您批准后落地,并在您定的窗口冻结 `sql-driver.ts`;本 PR 保持草稿。 Clause-②: no Metadata-protocol: none ## What changes - **`scripts/codemod-split-sql-driver.mjs`** gains the facade layer `scripts/codemod-split-engine.mjs` carries (#30012, #30083): - Each section module's moved bodies become the static methods of one stateless facet class (`sql-driver-crud.ts` → `CrudFacet`), at class indentation, unedited. Their `this` is one named host interface typed AS the class members (`SqlDriver['x']`), so a private member is reached like a public one. - The stub on the class keeps its modifiers and signature byte-for-byte (only `async` is dropped) and makes one call: `return CrudFacet.x.call(crudHost.of(this), …)`. Each host is built once per driver by a factory the class's static block installs into `sql-driver-facet-hosts.ts` (types-only module). Methods go as closures called on the driver at call time, so a subclass override or an instance spy is still the one reached; `getBuilder` goes as a bound getter (`BOUND_HOST_METHODS`) so `check-tenant-chokepoint` still counts the binding in the moved body; fields and accessors go as getters, assigned fields with a setter. - Private statics the moved bodies read go through `SqlDriverStatics`, which a facet module imports AS `SqlDriver` (private-static-through-host, 6624174693). - Docblock ownership (6624345766 item 1): a private method's docblock moves with its body; a public or protected one stays on the facade and the facet member carries a one-line placeholder pointing back. - The members above the first banner form one more section, the class head (`CLASS_HEAD_SECTION` → `sql-driver-class-head.ts`). Three sections whose hand-series seam exists already take a `-section` suffix; `sql-driver-deferred-ddl.ts` and `sql-driver-transactions-section.ts` are new. - No import prune: driver-sql compiles without `noUnusedLocals`, so an import only a moved body reads stays, as the pilot left them. - The pilot's `--dts` byte-equality reading is dropped: a facade cannot meet it by construction (the bundled `.d.ts` prints every private member). The bar is now the public-surface reading below, taken outside `scripts/` (6441032442 item 3). - `parameter-shadows` now fires only for a parameter named like the stub's facet class or host variable (it freed 4 methods). - One new `PATH_READER_PINS` row, `logDurabilityFailure` (see readers below). - `--self-test`: 67 cases in 5 pinned batteries (class-head section, the bound getter and two banner refusals added; the import-prune battery removed). - **The generated split** is `--write` on the unsplit `sql-driver.ts` of `21510bba` (the parent of the pilot's landing), unedited. 158/254 methods move into 21 facet modules; `sql-driver.ts` goes 18,334 → 10,979 lines (12,460 on `main`). The leaf `sql-driver-top-level.ts` is byte-identical to `main`. `index.ts` re-exports none of the new modules. - **`scripts/facade-delegation-registry.json`**: the `sql-driver.ts` / `SqlDriver` facade row (card #30105) and 28 exception rows, all carded #30020. - **By-path readers** (6597555122 item 3): - `check-durability-degradation-log-level` judges the two MySQL widening catches that stay on the class (`migrateMysqlDatetimeColumns`, `migrateMysqlTimeColumns`) by following `this.logDurabilityFailure(…)` to its body in the same file. A one-call stub there cannot be followed, so both read "swallowed, no log" (measured red). The helper is pinned to stay rather than widening the gate's same-file helper walk across files; the gate's readings are then equal to base. - `check-tenant-chokepoint`'s `DECLARED_WATCH_HINTS` names the four new `sql-driver-*.ts` modules (its self-test requires every scanned file to be declared). - Every other reader of `sql-driver.ts` by path (the `packages/cli` generate pins, `check-keyed-text-bounds`, `sql-driver-compile-refusal-seam.test.ts`, the liveness and checklist anchors, the ADR anchors) reads text the codemod keeps on the class through its existing pin and needle tables, and passes unchanged. - **No changeset**, as #29598 and #30012 (both `skip-changeset`): no published behaviour or surface changes. ## Tests No test file changes. The suites below ran at base `f68b47ca` and at head; the counts are equal. | Suite | Base | Head | |:--|:--|:--| | driver-sql (`vitest run`) | 238 files passed, 12 skipped; 3,859 passed, 216 skipped | the same | | driver-turso (subclasses `SqlDriver`) | 96 files; 2,496 passed, 34 skipped | 96 files; 2,496 passed, 34 skipped | | driver-sqlite-wasm (subclasses `SqlDriver`) | 40 files; 750 passed | 40 files; 750 passed | | cli by-path pins (9 files: generate-* pins, `clean.operator-text-21418`) | 9 files; 131 passed | 9 files; 131 passed | | types / metadata / metadata-protocol by-path tests, spec `check-liveness.test` | 22 / 11 / 28 / 74 passed | 22 / 11 / 28 / 74 passed | ## Acceptance notes - Phase 0: this PR stays a draft. It is over the 3,000-line human-merge threshold, so it lands on a maintainer approval, after `sql-driver.ts` is frozen for a window. At landing the script runs on the latest `main`'s unsplit text (regenerate, never rebase, ruling A on #29465). - Codemod-limit exception rows the seat may card separately (the rest are state-owning, #30020): the five accessors (the codemod moves methods, not accessors); `rotateShards` / `ensureRotation` (a computed parameter default stays in the byte-for-byte signature); the path-reader pins (`ensureShardTable`, `initObjects`, `migrateMysqlDatetimeColumns`, `migrateMysqlTimeColumns`, `resolveWithheldFilterRefusal`, `createAuditTimestampColumn`, `varcharColumnChars`, `createColumn`, `logDurabilityFailure`), which move once their readers read the facet modules; and the `field-call: knex`, `inferred-return-type` and `comment-in-parameters` stays. - `dts-public.mjs`'s built-in `--private-control` fails on this dist: it deletes the first `private x;` line but leaves that member's docblock, which then attaches to the next (public) member. Dropping the whole private member with its docblock passes (reading below). ## Verification | Reading | Base | Head | |:--|:--|:--| | `sql-driver.ts` lines | 18,334 (`21510bba`) · 12,460 (`main`) | **10,979** | | Byte-identity (`--write`, and `--verify --base 21510bb`, exit 0) | | 158/158 bodies, 127/127 top-level declarations identical; undoing the split rebuilds the base byte-for-byte | | Determinism | | two `--write` runs: output tree sha256 `eab675141cad9efdb5217f65f044a246474c973777c575d9f5ee00b8c63d8154` both times; regeneration reproduces the committed tree | | `driver-sql typecheck` | exit 0 | exit 0 | | `check:facade-delegation-only` (+ `--self-test` 36) | | exit 0: `SqlDriver` 352 members, 244 delegations, 80 fields, 28 named exceptions | | `check:tenant-chokepoint` | 24 bindings, 21 read doors, 3 insert builders, 4 tenant predicates, 0 D8 arms, 34 files | the same readings, 38 files | | `check:durability-log-level` | 51 seams (9 propagating), 101 read seams (13 discriminated) | the same; the output is byte-identical | | `pnpm build` (whole repo, 119 tasks) | | exit 0 | | `pnpm lint` | | exit 0 | | `dispatch-gates --commands` (67) | | 67 run, every exit 0; `--ran`: 67 derived, 0 NOT-MEASURED, 0 UNRUN | ### `.d.ts`: ruling 6624345766 item 1, "public surface EQUAL" Read with `dts-public.mjs` (project files `issue-29985/dts-public.mjs`, sha256 `211b32062250606e658acd7fb8880e0079b4043121fb7b970c658b4b57bf5020`, the reading #29985 used). driver-sql's `.d.ts` build is deterministic on both sides (two builds each, byte-identical), so one pair is read. | Reading | `index.d.ts` | `index.d.mts` | |:--|:--|:--| | raw (sha256 base `69d9162d…` / head `fac28979…`) | differs (private members) | differs | | private members cut, union order, second normalisation | **equal** (106 / 106 statements) | **equal** | | declared surface | equal | equal | - The control that edits one public docblock is caught. - Dropping one whole private member (`fileColumnsMovedDeclared` with its docblock) passes. - The facet classes and hosts do not appear in the `.d.ts`. ### Facade reading | Facet | Members | Docblocks moved | |:--|--:|--:| | ClassHead | 12 | 6 | | Lifecycle | 11 | 6 | | Crud | 7 | 2 | | SequenceSemantics | 23 | 3 | | BulkOperations | 7 | 0 | | TransactionsSection | 1 | 0 | | Aggregation | 3 | 0 | | Distinct | 1 | 0 | | WindowFunctions | 1 | 0 | | QueryPlan | 2 | 0 | | SchemaSyncSection | 3 | 0 | | Rotation | 9 | 0 | | DeferredDdl | 3 | 0 | | ManagedDrift | 8 | 0 | | InternalHelpers | 7 | 0 | | FilterHelpers | 30 | 3 | | FieldMapping | 6 | 0 | | WindowFunctionBuilder | 1 | 0 | | ColumnCreation | 14 | 1 | | DatabaseHelpers | 7 | 0 | | SqliteSerialisation | 2 | 0 | | **Total** | **158** | **21** | The 28 exception rows (all #30020): | Reason | Members | |:--|:--| | state: constructor logic, field / static initializers holding arrows | `constructor`, `logger`, `static DIALECT_CONNECT_TIMEOUT` | | accessor (codemod moves methods only) | `get supports`, `get insertReturnsStoredRows`, `get dateGranularityCapabilities`, `get supportsWalJournal`, `get dialectName` | | a computed parameter default | `rotateShards`, `ensureRotation` | | path-reader | `logDurabilityFailure`, `ensureShardTable`, `initObjects`, `migrateMysqlDatetimeColumns`, `migrateMysqlTimeColumns`, `resolveWithheldFilterRefusal`, `createAuditTimestampColumn`, `varcharColumnChars`, `createColumn` | | field-call: knex | `listPhysicalColumns`, `getBuilder`, `explainUnkeyableTextColumn`, `explainHashShadowDuplicate` | | inferred-return-type | `applyFilters`, `applyFilterCondition`, `ensureDatabaseExists`, `createDatabase` | | comment-in-parameters | `assertOperatorAppliesToColumn` | The 78 hand-series stubs stay as they were (their modules are the hand series', not facets) and read as delegations. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzhBZia3VxBbmZ7wCkS --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 07595c0 commit e826e77

26 files changed

Lines changed: 4990 additions & 3121 deletions

‎packages/drivers/driver-sql/src/sql-driver-aggregation.ts‎

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,64 +2,76 @@
22

33
/**
44
* [#29520 codemod] The "Aggregation" section of `SqlDriver`: the bodies of
5-
* `aggregate`, `foldEmptyAggregateAnswers` moved out of `sql-driver.ts` whole
6-
* by `scripts/codemod-split-sql-driver.mjs`.
5+
* `aggregate`, `aggregandColumnClassOf`, `foldEmptyAggregateAnswers` moved out
6+
* of `sql-driver.ts` whole by `scripts/codemod-split-sql-driver.mjs`.
77
*
8-
* A MOVE, not a redesign. Each function below is the body of the `SqlDriver`
8+
* A MOVE, not a redesign. {@link AggregationFacet} is a stateless facet of `SqlDriver`
9+
* (ruling 6624345766): each static method of it is the body of the `SqlDriver`
910
* method of the same name exactly as it stood: not re-indented, not
10-
* formatted, not edited. The method stays on the class with its docblock and
11-
* signature, so the published `dist/*.d.ts`, an instance spy and a subclass
12-
* override are what they were; the docblock there is the contract.
11+
* formatted, not edited. The method stays on the class as a one-call stub with
12+
* its signature, so the published surface, an instance spy and a subclass
13+
* override are what they were. A public or protected method keeps its
14+
* docblock on the class, the contract; a private method's docblock moved here
15+
* with its body. The state stays on `SqlDriver`.
1316
*
1417
* The bodies reach the class through {@link AggregationHost}: exactly the
15-
* `SqlDriver` members they read. Each method hands its body one literal of the
16-
* members that body reads, plus a `.call`: methods go as closures that call
17-
* them on the driver at call time (the builder factory as a bound reference),
18-
* so a subclass override or an instance spy is still the one reached; fields
19-
* and accessors go as GETTERS, read when the body reads them, as they were on
20-
* the class.
18+
* `SqlDriver` members they read, private ones included. Every stub hands its
19+
* body, by a `.call`, the same host: `aggregationHost` in
20+
* `sql-driver-facet-hosts.ts`, built once per driver by the class's static
21+
* block. Methods go as closures that call them on the driver at call time, so a
22+
* subclass override or an instance spy is still the one reached; fields and
23+
* accessors go as GETTERS, read when the body reads them, as they were on the
24+
* class, and a field a body assigns goes with a SETTER that assigns it on the
25+
* driver.
2126
*
22-
* The module imports `sql-driver.ts` for types only, so the two modules form
23-
* no evaluation cycle. `index.ts` re-exports nothing from this module.
27+
* The module imports `sql-driver.ts` for types only, so the two modules form no
28+
* evaluation cycle. `index.ts` re-exports nothing from this module.
2429
*/
2530

2631
import { emptyGroupValueFor } from '@objectstack/spec/data';
2732
import type { DriverOptions } from '@objectstack/spec/data';
2833
import type { DriverQuery } from '@objectstack/spec/contracts';
2934
import { AGGREGATE_ANSWER_KIND, aggregandOperandSql } from '@objectstack/core';
35+
import type { AggregandColumnClass } from '@objectstack/core';
3036
import {
3137
refuseDateBucketedGroupBy, refuseDistinctAggregateWithoutField, unsupportedAggregationFilterError,
3238
} from './sql-driver-top-level.js';
3339
import type { ReadPresentationKind } from './sql-driver-top-level.js';
3440
import type { SqlDriver } from './sql-driver.js';
3541

3642
/**
37-
* The `SqlDriver` members the functions below read — every one of them, and
38-
* no other. Each is typed AS the class member (an indexed access on the
39-
* class), so a change to a member's signature on the class is a change here
40-
* too. `readonly` members are the getters.
43+
* The `SqlDriver` members the bodies of {@link AggregationFacet} read — every one of
44+
* them, and no other. Each is typed AS the class member (an indexed access on
45+
* the class), so a change to a member's signature on the class is a change
46+
* here too. `readonly` members are the getters; the others are fields a body
47+
* assigns.
4148
*/
4249
export interface AggregationHost {
4350
aggregandColumnClassOf: SqlDriver['aggregandColumnClassOf'];
4451
aggregateBackendFault: SqlDriver['aggregateBackendFault'];
4552
aliasIdentifierSql: SqlDriver['aliasIdentifierSql'];
4653
applyFilters: SqlDriver['applyFilters'];
4754
applyTenantScope: SqlDriver['applyTenantScope'];
55+
readonly booleanFields: SqlDriver['booleanFields'];
4856
buildDateBucketExpr: SqlDriver['buildDateBucketExpr'];
4957
coercionKey: SqlDriver['coercionKey'];
5058
readonly config: SqlDriver['config'];
5159
readonly dateGranularityCapabilities: SqlDriver['dateGranularityCapabilities'];
5260
readonly dialectName: SqlDriver['dialectName'];
5361
foldEmptyAggregateAnswers: SqlDriver['foldEmptyAggregateAnswers'];
62+
readonly fractionalNumericFields: SqlDriver['fractionalNumericFields'];
5463
getBuilder: SqlDriver['getBuilder'];
5564
readonly knex: SqlDriver['knex'];
5665
mapAggregateFunc: SqlDriver['mapAggregateFunc'];
66+
readonly numericFields: SqlDriver['numericFields'];
5767
presentReadColumns: SqlDriver['presentReadColumns'];
5868
readPresentationKind: SqlDriver['readPresentationKind'];
5969
}
6070

61-
/** The body of `SqlDriver#aggregate`; the method's docblock there is its contract. */
62-
export async function aggregate(this: Pick<AggregationHost, 'aggregandColumnClassOf' | 'aggregateBackendFault' | 'aliasIdentifierSql' | 'applyFilters' | 'applyTenantScope' | 'buildDateBucketExpr' | 'coercionKey' | 'config' | 'dateGranularityCapabilities' | 'dialectName' | 'foldEmptyAggregateAnswers' | 'getBuilder' | 'knex' | 'mapAggregateFunc' | 'presentReadColumns' | 'readPresentationKind'>, object: string, query: DriverQuery, options?: DriverOptions): Promise<Record<string, unknown>[]> {
71+
/** The bodies of the "Aggregation" section of `SqlDriver`: a stateless facet, called by the class's stubs. */
72+
export class AggregationFacet {
73+
/** The body of `SqlDriver#aggregate`; the method's docblock there is its contract. */
74+
static async aggregate(this: Pick<AggregationHost, 'aggregandColumnClassOf' | 'aggregateBackendFault' | 'aliasIdentifierSql' | 'applyFilters' | 'applyTenantScope' | 'buildDateBucketExpr' | 'coercionKey' | 'config' | 'dateGranularityCapabilities' | 'dialectName' | 'foldEmptyAggregateAnswers' | 'getBuilder' | 'knex' | 'mapAggregateFunc' | 'presentReadColumns' | 'readPresentationKind'>, object: string, query: DriverQuery, options?: DriverOptions): Promise<Record<string, unknown>[]> {
6375
const builder = this.getBuilder(object, options);
6476
this.applyTenantScope(builder, object, options);
6577

@@ -331,8 +343,17 @@ export async function aggregate(this: Pick<AggregationHost, 'aggregandColumnClas
331343
return this.presentReadColumns(this.foldEmptyAggregateAnswers(rows, foldedOutput), presentedOutput);
332344
}
333345

334-
/** The body of `SqlDriver#foldEmptyAggregateAnswers`; the method's docblock there is its contract. */
335-
export function foldEmptyAggregateAnswers(rows: any, identities: Map<string, number>): any {
346+
/** The body of `SqlDriver#aggregandColumnClassOf`; the method's docblock there is its contract. */
347+
static aggregandColumnClassOf(this: Pick<AggregationHost, 'booleanFields' | 'fractionalNumericFields' | 'numericFields'>, table: string | null, field: string): AggregandColumnClass | undefined {
348+
if (table === null) return undefined;
349+
if (this.booleanFields[table]?.includes(field)) return 'boolean';
350+
if (this.fractionalNumericFields[table]?.includes(field)) return 'fractional';
351+
if (this.numericFields[table]?.includes(field)) return 'integral';
352+
return undefined;
353+
}
354+
355+
/** The body of `SqlDriver#foldEmptyAggregateAnswers`; the method's docblock there is its contract. */
356+
static foldEmptyAggregateAnswers(rows: any, identities: Map<string, number>): any {
336357
if (identities.size === 0 || !Array.isArray(rows)) return rows;
337358
for (const row of rows) {
338359
if (!row || typeof row !== 'object') continue;
@@ -342,3 +363,4 @@ export function foldEmptyAggregateAnswers(rows: any, identities: Map<string, num
342363
}
343364
return rows;
344365
}
366+
}

‎packages/drivers/driver-sql/src/sql-driver-bulk-operations.ts‎

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,31 @@
22

33
/**
44
* [#29520 codemod] The "Bulk & Batch Operations" section of `SqlDriver`: the
5-
* bodies of `bulkCreate`, `bulkUpdate`, `updateMany`, `rotatedUpdateById`,
6-
* `count` moved out of `sql-driver.ts` whole by
7-
* `scripts/codemod-split-sql-driver.mjs`.
5+
* bodies of `bulkCreate`, `bulkUpdate`, `bulkDelete`, `updateMany`,
6+
* `deleteMany`, `rotatedUpdateById`, `count` moved out of `sql-driver.ts` whole
7+
* by `scripts/codemod-split-sql-driver.mjs`.
88
*
9-
* A MOVE, not a redesign. Each function below is the body of the `SqlDriver`
9+
* A MOVE, not a redesign. {@link BulkOperationsFacet} is a stateless facet of `SqlDriver`
10+
* (ruling 6624345766): each static method of it is the body of the `SqlDriver`
1011
* method of the same name exactly as it stood: not re-indented, not
11-
* formatted, not edited. The method stays on the class with its docblock and
12-
* signature, so the published `dist/*.d.ts`, an instance spy and a subclass
13-
* override are what they were; the docblock there is the contract.
12+
* formatted, not edited. The method stays on the class as a one-call stub with
13+
* its signature, so the published surface, an instance spy and a subclass
14+
* override are what they were. A public or protected method keeps its
15+
* docblock on the class, the contract; a private method's docblock moved here
16+
* with its body. The state stays on `SqlDriver`.
1417
*
1518
* The bodies reach the class through {@link BulkOperationsHost}: exactly the
16-
* `SqlDriver` members they read. Each method hands its body one literal of the
17-
* members that body reads, plus a `.call`: methods go as closures that call
18-
* them on the driver at call time (the builder factory as a bound reference),
19-
* so a subclass override or an instance spy is still the one reached; fields
20-
* and accessors go as GETTERS, read when the body reads them, as they were on
21-
* the class.
19+
* `SqlDriver` members they read, private ones included. Every stub hands its
20+
* body, by a `.call`, the same host: `bulkOperationsHost` in
21+
* `sql-driver-facet-hosts.ts`, built once per driver by the class's static
22+
* block. Methods go as closures that call them on the driver at call time, so a
23+
* subclass override or an instance spy is still the one reached; fields and
24+
* accessors go as GETTERS, read when the body reads them, as they were on the
25+
* class, and a field a body assigns goes with a SETTER that assigns it on the
26+
* driver.
2227
*
23-
* The module imports `sql-driver.ts` for types only, so the two modules form
24-
* no evaluation cycle. `index.ts` re-exports nothing from this module.
28+
* The module imports `sql-driver.ts` for types only, so the two modules form no
29+
* evaluation cycle. `index.ts` re-exports nothing from this module.
2530
*/
2631

2732
import type { DriverOptions } from '@objectstack/spec/data';
@@ -33,10 +38,11 @@ import type { AutoNumberReservation } from './sql-driver-top-level.js';
3338
import type { SqlDriver } from './sql-driver.js';
3439

3540
/**
36-
* The `SqlDriver` members the functions below read — every one of them, and
37-
* no other. Each is typed AS the class member (an indexed access on the
38-
* class), so a change to a member's signature on the class is a change here
39-
* too. `readonly` members are the getters.
41+
* The `SqlDriver` members the bodies of {@link BulkOperationsFacet} read — every one of
42+
* them, and no other. Each is typed AS the class member (an indexed access on
43+
* the class), so a change to a member's signature on the class is a change
44+
* here too. `readonly` members are the getters; the others are fields a body
45+
* assigns.
4046
*/
4147
export interface BulkOperationsHost {
4248
applyFilters: SqlDriver['applyFilters'];
@@ -67,8 +73,10 @@ export interface BulkOperationsHost {
6773
updatedAtStampIsPresumed: SqlDriver['updatedAtStampIsPresumed'];
6874
}
6975

70-
/** The body of `SqlDriver#bulkCreate`; the method's docblock there is its contract. */
71-
export async function bulkCreate(this: Pick<BulkOperationsHost, 'applyWriteColumnMap' | 'auditMissingTenant' | 'autoNumberCounterKey' | 'collidingAutoNumberReservations' | 'fillAutoNumberFields' | 'formatInput' | 'formatOutput' | 'getBuilder' | 'injectTenantOnInsert' | 'insertReturnsStoredRows' | 'readBackInsertedRows' | 'resyncSequenceToDataMax' | 'rotationWriteTarget' | 'stampInsertTimestamps'>, object: string, data: any[], options?: DriverOptions): Promise<Record<string, unknown>[]> {
76+
/** The bodies of the "Bulk & Batch Operations" section of `SqlDriver`: a stateless facet, called by the class's stubs. */
77+
export class BulkOperationsFacet {
78+
/** The body of `SqlDriver#bulkCreate`; the method's docblock there is its contract. */
79+
static async bulkCreate(this: Pick<BulkOperationsHost, 'applyWriteColumnMap' | 'auditMissingTenant' | 'autoNumberCounterKey' | 'collidingAutoNumberReservations' | 'fillAutoNumberFields' | 'formatInput' | 'formatOutput' | 'getBuilder' | 'injectTenantOnInsert' | 'insertReturnsStoredRows' | 'readBackInsertedRows' | 'resyncSequenceToDataMax' | 'rotationWriteTarget' | 'stampInsertTimestamps'>, object: string, data: any[], options?: DriverOptions): Promise<Record<string, unknown>[]> {
7280
// Same client-side id assignment as create() (id/_id normalization,
7381
// nanoid fallback when neither is supplied) — a row missing an id must
7482
// not be silently inserted with a NULL primary key just because the
@@ -225,8 +233,8 @@ export async function bulkCreate(this: Pick<BulkOperationsHost, 'applyWriteColum
225233
}
226234
}
227235

228-
/** The body of `SqlDriver#bulkUpdate`; the method's docblock there is its contract. */
229-
export async function bulkUpdate(this: Pick<BulkOperationsHost, 'knex' | 'update'>, object: string, updates: Array<{ id: string | number; data: Record<string, any> }>, options?: DriverOptions): Promise<Record<string, unknown>[]> {
236+
/** The body of `SqlDriver#bulkUpdate`; the method's docblock there is its contract. */
237+
static async bulkUpdate(this: Pick<BulkOperationsHost, 'knex' | 'update'>, object: string, updates: Array<{ id: string | number; data: Record<string, any> }>, options?: DriverOptions): Promise<Record<string, unknown>[]> {
230238
// An empty batch issues no statement, exactly as before: a BEGIN/COMMIT
231239
// pair — and the pool checkout behind it — buys nothing for zero rows.
232240
if (updates.length === 0) return [];
@@ -247,8 +255,18 @@ export async function bulkUpdate(this: Pick<BulkOperationsHost, 'knex' | 'update
247255
return runner.transaction(async (trx) => applyAll({ ...options, transaction: trx }));
248256
}
249257

250-
/** The body of `SqlDriver#updateMany`; the method's docblock there is its contract. */
251-
export async function updateMany(this: Pick<BulkOperationsHost, 'applyFilters' | 'applyTenantScope' | 'applyWriteColumnMap' | 'auditMissingTenant' | 'formatInput' | 'getBuilder' | 'keepSuppliedUpdatedAt' | 'rotationShardsOf' | 'stampsUpdatedAt' | 'updateWithPresumedTimestamp' | 'updatedAtStamp' | 'updatedAtStampIsPresumed'>, object: string, query: DriverQuery, data: any, options?: DriverOptions): Promise<number> {
258+
/** The body of `SqlDriver#bulkDelete`; the method's docblock there is its contract. */
259+
static async bulkDelete(this: Pick<BulkOperationsHost, 'applyTenantScope' | 'auditMissingTenant' | 'getBuilder' | 'rotationShardsOf'>, object: string, ids: Array<string | number>, options?: DriverOptions): Promise<void> {
260+
this.auditMissingTenant(object, 'bulkDelete', options);
261+
for (const target of this.rotationShardsOf(object) ?? [object]) {
262+
const builder = this.getBuilder(target, options).whereIn('id', ids);
263+
this.applyTenantScope(builder, object, options);
264+
await builder.delete();
265+
}
266+
}
267+
268+
/** The body of `SqlDriver#updateMany`; the method's docblock there is its contract. */
269+
static async updateMany(this: Pick<BulkOperationsHost, 'applyFilters' | 'applyTenantScope' | 'applyWriteColumnMap' | 'auditMissingTenant' | 'formatInput' | 'getBuilder' | 'keepSuppliedUpdatedAt' | 'rotationShardsOf' | 'stampsUpdatedAt' | 'updateWithPresumedTimestamp' | 'updatedAtStamp' | 'updatedAtStampIsPresumed'>, object: string, query: DriverQuery, data: any, options?: DriverOptions): Promise<number> {
252270
this.auditMissingTenant(object, 'updateMany', options);
253271

254272
// `data ?? {}` keeps the nullish tolerance this door already had on the
@@ -289,8 +307,21 @@ export async function updateMany(this: Pick<BulkOperationsHost, 'applyFilters' |
289307
return total;
290308
}
291309

292-
/** The body of `SqlDriver#rotatedUpdateById`; the method's docblock there is its contract. */
293-
export async function rotatedUpdateById(this: Pick<BulkOperationsHost, 'applyTenantScope' | 'applyWriteColumnMap' | 'formatInput' | 'formatOutput' | 'getBuilder' | 'keepSuppliedUpdatedAt' | 'stampsUpdatedAt' | 'updatedAtStamp'>, object: string, shards: string[], id: string | number, data: Record<string, any>, options?: DriverOptions): Promise<Record<string, unknown> | null> {
310+
/** The body of `SqlDriver#deleteMany`; the method's docblock there is its contract. */
311+
static async deleteMany(this: Pick<BulkOperationsHost, 'applyFilters' | 'applyTenantScope' | 'auditMissingTenant' | 'getBuilder' | 'rotationShardsOf'>, object: string, query: DriverQuery, options?: DriverOptions): Promise<number> {
312+
this.auditMissingTenant(object, 'deleteMany', options);
313+
let total = 0;
314+
for (const target of this.rotationShardsOf(object) ?? [object]) {
315+
const builder = this.getBuilder(target, options);
316+
this.applyTenantScope(builder, object, options);
317+
if (query.where) this.applyFilters(builder, query.where, options);
318+
total += (await builder.delete()) || 0;
319+
}
320+
return total;
321+
}
322+
323+
/** The body of `SqlDriver#rotatedUpdateById`; the method's docblock there is its contract. */
324+
static async rotatedUpdateById(this: Pick<BulkOperationsHost, 'applyTenantScope' | 'applyWriteColumnMap' | 'formatInput' | 'formatOutput' | 'getBuilder' | 'keepSuppliedUpdatedAt' | 'stampsUpdatedAt' | 'updatedAtStamp'>, object: string, shards: string[], id: string | number, data: Record<string, any>, options?: DriverOptions): Promise<Record<string, unknown> | null> {
294325
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, data));
295326
// [commit 479fba50d] One definition of the decision, shared with {@link update}. No
296327
// fallback is threaded here, and that is a property of the path rather than
@@ -319,8 +350,8 @@ export async function rotatedUpdateById(this: Pick<BulkOperationsHost, 'applyTen
319350
return null;
320351
}
321352

322-
/** The body of `SqlDriver#count`; the method's docblock there is its contract. */
323-
export async function count(this: Pick<BulkOperationsHost, 'applyFilters' | 'applyTenantScope' | 'backendStatementFault' | 'getBuilder' | 'unresolvableFilterColumnRefusal'>, object: string, query?: DriverQuery, options?: DriverOptions): Promise<number> {
353+
/** The body of `SqlDriver#count`; the method's docblock there is its contract. */
354+
static async count(this: Pick<BulkOperationsHost, 'applyFilters' | 'applyTenantScope' | 'backendStatementFault' | 'getBuilder' | 'unresolvableFilterColumnRefusal'>, object: string, query?: DriverQuery, options?: DriverOptions): Promise<number> {
324355
const builder = this.getBuilder(object, options);
325356
this.applyTenantScope(builder, object, options);
326357

@@ -352,3 +383,4 @@ export async function count(this: Pick<BulkOperationsHost, 'applyFilters' | 'app
352383
}
353384
return 0;
354385
}
386+
}

0 commit comments

Comments
 (0)