Skip to content

Commit bfca0ef

Browse files
refactor(driver-sql): the durability gate and the compile-refusal enumeration read the sql-driver facade, so the codemod moves four more members (#30147 phase 2b) (#30306)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBWMEECkfNfe2VjW9rMJG3n7)_ Part of #30147 (phase 2b of 2a-2d: two by-path readers learn the facade form, so the codemod moves four more members. 2c, 2d and the phase 3 measurement stay open on the card.) ## 维护者速读 - 改了什么:`check-durability-degradation-log-level.mjs` 学会跨文件跟随 sql-driver 的 facade(facet 模块里的 `this.x()` 调到 `SqlDriver#x`,再穿过 stub 到 facet 方法体);`sql-driver-compile-refusal-seam.test.ts` 改为读取全部 `sql-driver-*.ts` 模块。codemod 因此去掉 1 个 pin 和 4 个 needle,重新生成后又有 4 个成员变成委托,登记表 73 → 69。 - 为什么改:这两个读者只认 `sql-driver.ts` 本文件,codemod 只好把这些方法留在类上;读者学会新形态后,codemod 才能把它们移走(#30147,裁决 6597555122 第 3 条)。 - 风险与回滚:方法体逐字节不变(175/175),公开 `.d.ts` 与 base 相同;durability 门禁在 base 和 head 上读到同样的 52 个 seam、同样的判定,只有两个 seam 的位置从 `sql-driver.ts` 换到 `sql-driver-rotation.ts`。回滚即 revert。 - 先看:门禁里新增的 `FACADE_LOG_HOP` / `createFacadeLogHop` 和 `collectLoggedLevels` 里的 6 行接入;测试里 `sources` 的改法。 - 两分钟验证:`node scripts/check-durability-degradation-log-level.mjs --self-test`(新增 10 行通过)和 `node scripts/codemod-split-sql-driver.mjs --verify --base 21510bb`(`175/175`,退出 0)。 - 需要你做的:无,席位审阅并落地。 Clause-②: no Metadata-protocol: none ## What changes Two readers took `sql-driver.ts` text by path, so the codemod pinned four members on the class. Each reader now reads the facade form, and the pins go. **`scripts/check-durability-degradation-log-level.mjs`.** Its catch side follows a log helper to its body by name, in the same file. A catch the codemod moves into a facet module calls `this.logDurabilityFailure(…)`, which that module does not declare. Once `logDurabilityFailure` itself moves, the method the call reaches is a one-call stub. Either way the catch read "swallowed, no log": with the pins removed and no reader change, the gate reds the two `runWideningAlters()` catches in `sql-driver-rotation.ts`. `FACADE_LOG_HOP` follows exactly the two shapes the codemod writes: - HOST CALL: `this.x(…)` in a `sql-driver-*.ts` module, inside a function whose `this` is the module's own host interface (bare or through a `Pick` naming `x`), whose member `x` is typed `T['x']` with `T` the `SqlDriver` class imported from `./sql-driver.js` (directly or as `SqlDriverClass`), resolves to `SqlDriver#x` in `sql-driver.ts`. That is one hop, the same hop the call made before the move. - STUB: a `SqlDriver` method whose last statement returns `<X>Facet.x.call(<y>Host.of(this), …)` is read as the facet's static `x`. This costs no depth. Every other shape stays unresolved, as before. A log found across the hop is reported at the line of the call in the file being judged. The hop is handed in by `run()` to the log-level rule only. The read-seam rule's scan roots do not include `driver-sql`, so the two rules still agree on every file they both read. **`packages/drivers/driver-sql/src/sql-driver-compile-refusal-seam.test.ts`.** The enumeration read five named files. It now reads every `sql-driver-*.ts` module in the directory. A facet's static `x` is named `SqlDriver.x`, because it is that method's body. The positive control also asserts that the five files it used to read, plus one facet module, are still among those it reads. The pinned sets are unchanged. **Codemod (`scripts/codemod-split-sql-driver.mjs`).** These entries are gone: - the `logDurabilityFailure` pin; - the `this.runWideningAlters(` needle; - the three `CLASS_PATH_READER_NEEDLES` rows (the table stays, empty, with its comment rewritten). The tree is regenerated from `21510bba`, so `logDurabilityFailure`, `migrateMysqlDatetimeColumns`, `migrateMysqlTimeColumns` and `resolveWithheldFilterRefusal` move. `sql-driver.ts` goes from 10,717 to 10,624 lines. **Registry.** The four rows are deleted, so 73 rows become 69. | card | before | after | |:--|--:|--:| | #30020 | 17 | 17 | | #30054 | 16 | 16 | | #30055 | 6 | 6 | | #30147 | 34 | 30 (sql-driver.ts 12 → 8) | | total | 73 | 69 | ## Tests - The durability gate's self-test gains 10 rows, each driven through `analyzeSourceFile` the way `run()` drives it, over multi-file fixtures: - the hop reaches a loud body; - the same hop to a `warn` body is judged at that level; - with no hop the moved catch reads swallowed; - the same files in another directory, a member typed otherwise, a member the `Pick` omits and a class type imported from elsewhere each resolve nowhere; - a routed module's `SqlDriverClass` alias resolves; - a `sql-driver.ts` catch follows the stub; - a stub handing anything but `<y>Host.of(this)` is not followed. The `selfTest` partition floor goes 70 → 80 and the roster total 137 → 147. - The compile-refusal test keeps its 115 cases. Reading the old five files over the new tree reds "only three functions build the envelope WITHOUT the seam". Measured before the widening. - No other test file changes. The suites below ran at base `9b8a5867` 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`) | 97 files; 2,514 passed, 34 skipped | the same | | driver-sqlite-wasm (subclasses `SqlDriver`) | 40 files; 750 passed | the same | | cli by-path pins (9 files) | 9 files; 116 passed | the same | | types / metadata / metadata-protocol by-path tests, spec `check-liveness.test` | 53 / 11 / 33 / 74 passed | the same | ## Acceptance notes - `check-durability-degradation-log-level.mjs` is a by-path reader, not the facade gate. The facade gate script and its self-test are untouched. - No changeset, as #30139 and #30249 (`skip-changeset`): no published behaviour or surface changes. ## Verification Head `d3ca391c`, base `origin/main` `9b8a5867`. <details><summary>Proof readings</summary> - Byte-identity: `node scripts/codemod-split-sql-driver.mjs --verify --base 21510bb` exit 0, "175/175 moved bodies identical to base (body, parameters, return type, async); 127/127 moved top-level declarations identical to base; sql-driver.ts 18334 -> 10624 lines; undoing the split does rebuild the base byte-for-byte". - Determinism: two `--write` runs from the unsplit input give tree sha256 `5044055d6e0d47825283c7fe8d3c539a823909573a382434b0cda80e1b76326c` both times. - Durability gate, `--list` at base and head: 52 seams, the same verdicts. The only change is the location of the two `runWideningAlters()` seams, from `sql-driver.ts:5187` / `:5281` to `sql-driver-rotation.ts:327` / `:373`, both "recovers on one branch, loud (error … via logDurabilityFailure())". Without the hop, head reds those two. - Public `.d.ts`: `dts-public.mjs` over the base and head `dist` exit 0, `pass: true`, both files raw-equal (`index.d.ts` sha256 `92e2cfb2…` on both sides). - Typecheck: `pnpm --filter @objectstack/driver-sql typecheck` exit 0. - `check-tenant-chokepoint`: 24/21/3/4/0 across 39 files at base and head; `--self-test` exit 0. - Facade gate exit 0, "3 facade file(s) hold delegations only, 69 named exception(s)"; `--self-test` 36 exit 0. - `node scripts/pm/dispatch-gates.mjs --commands`: 74 derived commands, each run, all exit 0; `--ran` reconciles 74 derived, 74 run, 0 NOT-MEASURED. Also run: the codemod, durability, `check-tenant-chokepoint` and facade gate `--self-test`s, `check:lean-entry-closure`, eslint over the changed files, all exit 0. - Changed lines 740 (+542 / -198), under the 3,000 threshold. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzj4B2cbnRLVdgtLcDZ)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent c1240a3 commit bfca0ef

8 files changed

Lines changed: 542 additions & 198 deletions

‎packages/drivers/driver-sql/src/sql-driver-class-head.ts‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
/**
44
* [#29520 codemod] The "Class head: dialect probes, logger, configuration"
5-
* section of `SqlDriver`: the bodies of `logger`, `emissionClients`,
6-
* `clientSpelling`, `insertReturnsStoredRows`, `aliasIdentifierSql`,
7-
* `dateGranularityCapabilities`, `dateBucketSql`, `knexConfigFor`,
8-
* `withConnectBound`, `withUtcSession`, `withMysqlCalendarDayAsText`,
9-
* `withPostgresCalendarDayAsText`, `installQueryTiming`, `assertSchemaMutable`,
10-
* `assertDialectEmits` moved out of `sql-driver.ts` whole by
11-
* `scripts/codemod-split-sql-driver.mjs`.
5+
* section of `SqlDriver`: the bodies of `logger`, `logDurabilityFailure`,
6+
* `emissionClients`, `clientSpelling`, `insertReturnsStoredRows`,
7+
* `aliasIdentifierSql`, `dateGranularityCapabilities`, `dateBucketSql`,
8+
* `knexConfigFor`, `withConnectBound`, `withUtcSession`,
9+
* `withMysqlCalendarDayAsText`, `withPostgresCalendarDayAsText`,
10+
* `installQueryTiming`, `assertSchemaMutable`, `assertDialectEmits` moved out
11+
* of `sql-driver.ts` whole by `scripts/codemod-split-sql-driver.mjs`.
1212
*
1313
* A MOVE, not a redesign. {@link ClassHeadFacet} is a stateless facet of `SqlDriver`
1414
* (ruling 6624345766): each static method of it is the body of the `SqlDriver`
@@ -65,6 +65,7 @@ export interface ClassHeadHost {
6565
readonly isPostgres: SqlDriverClass['isPostgres'];
6666
readonly isSqlite: SqlDriverClass['isSqlite'];
6767
readonly knex: SqlDriverClass['knex'];
68+
readonly logger: SqlDriverClass['logger'];
6869
openedEmptyInMemory: SqlDriverClass['openedEmptyInMemory'];
6970
readonly schemaMode: SqlDriverClass['schemaMode'];
7071
}
@@ -79,6 +80,12 @@ export class ClassHeadFacet {
7980
};
8081
}
8182

83+
/** The body of `SqlDriver#logDurabilityFailure`; the method's docblock there is its contract. */
84+
static logDurabilityFailure(this: Pick<ClassHeadHost, 'logger'>, msg: string, meta?: any): void {
85+
if (this.logger.error) this.logger.error(msg, meta);
86+
else this.logger.warn(msg, meta);
87+
}
88+
8289
/** The body of `SqlDriver#emissionClients`; the method's docblock there is its contract. */
8390
static emissionClients(): string[] {
8491
return [

‎packages/drivers/driver-sql/src/sql-driver-compile-refusal-seam.test.ts‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
* fault module `sql-driver-read-exit-fault.ts` (#25521 cut 15) and the
3333
* predicate-emitter module `sql-driver-predicate-emitters.ts` (#25521 cut 16)
3434
* and the generated top-level leaf `sql-driver-top-level.ts` (#29520 codemod),
35+
* and, since #30147, every other `sql-driver-*.ts` module beside it (a
36+
* facet member is named as the `SqlDriver` method whose body it is),
3537
* and lists every function that builds an `INVALID_FILTER` error. Only three may call
3638
* `unsupportedFilterError` directly (the seam itself, the `'author'`
3739
* re-issue, and the column refusal raised from a dialect error AFTER the
@@ -48,7 +50,7 @@
4850
*/
4951

5052
import { describe, it, expect, beforeEach } from 'vitest';
51-
import { readFileSync } from 'node:fs';
53+
import { readdirSync, readFileSync } from 'node:fs';
5254
import ts from 'typescript';
5355
import { SqlDriver } from './index.js';
5456
import { markFilterSubtreeProvenance, type FilterCondition } from '@objectstack/spec/data';
@@ -275,12 +277,12 @@ const DOORS: readonly Door[] = [
275277
// ── Half 1: the enumeration ───────────────────────────────────────────────────
276278

277279
/**
278-
* Every function in `sql-driver.ts`, `sql-driver-cross-field-comparison.ts`,
279-
* `sql-driver-read-exit-fault.ts` and `sql-driver-predicate-emitters.ts` that builds an `INVALID_FILTER` error, by the route it takes. Read with the TypeScript parser, not a regex, so a call
280+
* Every function in `sql-driver.ts` and every `sql-driver-*.ts` module beside it that builds an `INVALID_FILTER` error, by the route it takes. Read with the TypeScript parser, not a regex, so a call
280281
* split across lines, an inline `throw`, or a builder written as a method or an
281282
* arrow cannot slip past the count.
282283
*/
283284
function enumerateRefusalBuilders(): {
285+
files: string[];
284286
directCallers: Set<string>;
285287
seamCallers: Set<string>;
286288
codeWriters: Set<string>;
@@ -293,13 +295,14 @@ function enumerateRefusalBuilders(): {
293295
// `$empty` refusals; the scan reads it too.
294296
// [#29520 codemod] The top-level declarations ahead of the class moved to a
295297
// generated leaf, with the refusal builders among them; the scan reads it too.
296-
const sources = [
297-
['sql-driver.ts', readFileSync(new URL('./sql-driver.ts', import.meta.url), 'utf8')],
298-
['sql-driver-cross-field-comparison.ts', readFileSync(new URL('./sql-driver-cross-field-comparison.ts', import.meta.url), 'utf8')],
299-
['sql-driver-read-exit-fault.ts', readFileSync(new URL('./sql-driver-read-exit-fault.ts', import.meta.url), 'utf8')],
300-
['sql-driver-predicate-emitters.ts', readFileSync(new URL('./sql-driver-predicate-emitters.ts', import.meta.url), 'utf8')],
301-
['sql-driver-top-level.ts', readFileSync(new URL('./sql-driver-top-level.ts', import.meta.url), 'utf8')],
302-
] as const;
298+
// [#30147] The codemod moves a class method's body into a facet class
299+
// (`export class <X>Facet`) in any section module, so the scan reads every
300+
// `sql-driver-*.ts` module, not a list a move can outrun, and names a facet's
301+
// static `x` as `SqlDriver.x`: it is that method's body.
302+
const files = readdirSync(new URL('.', import.meta.url))
303+
.filter((f) => /^sql-driver(-[a-z0-9-]+)?\.ts$/.test(f) && !f.endsWith('.test.ts'))
304+
.sort();
305+
const sources = files.map((f) => [f, readFileSync(new URL(`./${f}`, import.meta.url), 'utf8')] as const);
303306
const directCallers = new Set<string>();
304307
const seamCallers = new Set<string>();
305308
const codeWriters = new Set<string>();
@@ -310,7 +313,7 @@ function enumerateRefusalBuilders(): {
310313
if (ts.isMethodDeclaration(at) && ts.isIdentifier(at.name)) {
311314
const owner = at.parent;
312315
const cls = ts.isClassDeclaration(owner) && owner.name ? owner.name.text : '?';
313-
return `${cls}.${at.name.text}`;
316+
return `${/^[A-Z][A-Za-z0-9]*Facet$/.test(cls) ? 'SqlDriver' : cls}.${at.name.text}`;
314317
}
315318
if ((ts.isArrowFunction(at) || ts.isFunctionExpression(at)) && ts.isVariableDeclaration(at.parent)) {
316319
const decl = at.parent;
@@ -357,7 +360,7 @@ function enumerateRefusalBuilders(): {
357360
ts.forEachChild(node, visit);
358361
};
359362
for (const [name, source] of sources) visit(ts.createSourceFile(name, source, ts.ScriptTarget.Latest, true));
360-
return { directCallers, seamCallers, codeWriters };
363+
return { files, directCallers, seamCallers, codeWriters };
361364
}
362365

363366
describe('[#20039] the filter-compile refusal class is closed — enumeration', () => {
@@ -376,6 +379,12 @@ describe('[#20039] the filter-compile refusal class is closed — enumeration',
376379
expect(found.seamCallers, builder).toContain(builder);
377380
}
378381
expect(found.directCallers).toContain('unresolvableFilterColumnError');
382+
// [#30147] The modules the builders were found in before the scan read
383+
// every module, and one facet module, are among those it reads.
384+
for (const f of ['sql-driver.ts', 'sql-driver-cross-field-comparison.ts', 'sql-driver-read-exit-fault.ts',
385+
'sql-driver-predicate-emitters.ts', 'sql-driver-top-level.ts', 'sql-driver-filter-helpers.ts']) {
386+
expect(found.files, `the scan lost ${f}`).toContain(f);
387+
}
379388
});
380389

381390
it('only three functions build the envelope WITHOUT the seam, none of them on the compile path', () => {

‎packages/drivers/driver-sql/src/sql-driver-filter-helpers.ts‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
* `temporalFilterColumnSql`, `isJsonColumn`, `jsonColumnFieldClass`,
1212
* `isNonTextColumn`, `applyTextOperatorOverNonTextColumn`,
1313
* `declaredValueShape`, `assertOperatorAppliesToColumn`, `applyFilters`,
14-
* `logWithheldFilterDiagnostic`, `withWithheldFilterLog`, `compileFilters`,
15-
* `applyContainsLike`, `declaredFieldsFor`, `applyFalseConstant` moved out of
16-
* `sql-driver.ts` whole by `scripts/codemod-split-sql-driver.mjs`.
14+
* `resolveWithheldFilterRefusal`, `logWithheldFilterDiagnostic`,
15+
* `withWithheldFilterLog`, `compileFilters`, `applyContainsLike`,
16+
* `declaredFieldsFor`, `applyFalseConstant` moved out of `sql-driver.ts` whole
17+
* by `scripts/codemod-split-sql-driver.mjs`.
1718
*
1819
* A MOVE, not a redesign. {@link FilterHelpersFacet} is a stateless facet of `SqlDriver`
1920
* (ruling 6624345766): each static method of it is the body of the `SqlDriver`
@@ -38,16 +39,18 @@
3839
* evaluation cycle. `index.ts` re-exports nothing from this module.
3940
*/
4041

42+
import { resolveFilterSubtreeProvenance } from '@objectstack/spec/data';
4143
import type { DriverOptions, ValueShapeFieldDef } from '@objectstack/spec/data';
4244
import { JSON_COLUMN_INCOMPATIBLE_OPERATORS, findMisplacedReadableIds, presentAsNumber, refuseReadableIdsOnce, temporalStorageForm } from '@objectstack/core';
4345
import type { JsonColumnFieldClass } from '@objectstack/core';
4446
import { StorageNameMapping } from '@objectstack/spec/system';
4547
import type { Knex } from 'knex';
4648
import {
47-
AUDIT_TIMESTAMP_COLUMNS, SQLITE_TIME_EXPR_REFS, WITHHELD_FILTER_LOGGED, assertCompilableComparand,
48-
assertDefinedComparands, canonicalTimeOfDay, canonicalUtcDatetime, emptyFieldConstraintError,
49-
filterArrayReachedDriverError, isEmptyFieldConstraint, jsonColumnOperatorError, mysqlDatetimeLiteral,
50-
normalizeSqliteDatetimeOutput, presentAuditTimestampOutput, refusalSubtree, withheldFilterDiagnosticOf,
49+
AUDIT_TIMESTAMP_COLUMNS, SQLITE_TIME_EXPR_REFS, WITHHELD_FILTER_AUTHOR_TEXT, WITHHELD_FILTER_LOGGED,
50+
WITHHELD_FILTER_SUBTREE, assertCompilableComparand, assertDefinedComparands, canonicalTimeOfDay,
51+
canonicalUtcDatetime, emptyFieldConstraintError, filterArrayReachedDriverError, isEmptyFieldConstraint,
52+
jsonColumnOperatorError, mysqlDatetimeLiteral, normalizeSqliteDatetimeOutput, presentAuditTimestampOutput,
53+
refusalSubtree, unsupportedFilterError, withheldFilterDiagnosticOf,
5154
} from './sql-driver-top-level.js';
5255
import type { ReadPresentationKind } from './sql-driver-top-level.js';
5356
import type { SqlDriver } from './sql-driver.js';
@@ -79,6 +82,7 @@ export interface FilterHelpersHost {
7982
readonly isSqlite: SqlDriver['isSqlite'];
8083
jsonColumnFieldClass: SqlDriver['jsonColumnFieldClass'];
8184
readonly jsonFields: SqlDriver['jsonFields'];
85+
logWithheldFilterDiagnostic: SqlDriver['logWithheldFilterDiagnostic'];
8286
readonly logger: SqlDriver['logger'];
8387
readonly managedObjectFields: SqlDriver['managedObjectFields'];
8488
readonly mediaFields: SqlDriver['mediaFields'];
@@ -430,6 +434,22 @@ export class FilterHelpersFacet {
430434
}
431435
}
432436

437+
/** The body of `SqlDriver#resolveWithheldFilterRefusal`; the method's docblock there is its contract. */
438+
static resolveWithheldFilterRefusal(this: Pick<FilterHelpersHost, 'logWithheldFilterDiagnostic'>, err: unknown, rootFilter: unknown): unknown {
439+
const diagnostic = withheldFilterDiagnosticOf(err);
440+
if (diagnostic === null) return err;
441+
const carrier = err as Record<symbol, unknown>;
442+
const subtree = carrier[WITHHELD_FILTER_SUBTREE];
443+
const provenance =
444+
subtree === undefined ? null : resolveFilterSubtreeProvenance(rootFilter, subtree);
445+
if (provenance === 'author') {
446+
const authorText = carrier[WITHHELD_FILTER_AUTHOR_TEXT];
447+
return unsupportedFilterError(typeof authorText === 'string' ? authorText : diagnostic);
448+
}
449+
this.logWithheldFilterDiagnostic(err);
450+
return err;
451+
}
452+
433453
/** The body of `SqlDriver#logWithheldFilterDiagnostic`; the method's docblock there is its contract. */
434454
static logWithheldFilterDiagnostic(this: Pick<FilterHelpersHost, 'logger'>, err: unknown): void {
435455
const diagnostic = withheldFilterDiagnosticOf(err);

0 commit comments

Comments
 (0)