From 3bbb1892a8d71068ecb306acc337646aa9b2119e Mon Sep 17 00:00:00 2001 From: HynLcc Date: Sat, 20 Jun 2026 01:17:28 +0800 Subject: [PATCH] Migrate table-create onto a new table-create lifecycle driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit table-create is a seedless, create-from-scratch family that no existing driver fits, so this adds a minimal framework/runners/table-create-lifecycle.ts modeled on field-add-lifecycle.ts but WITHOUT the seedReady/seedMode machinery (there is nothing to seed). The driver owns the repeated protocol: prepareFixture (allocate a mutable run accumulator) -> runPrimary (measured create + verify, mutating the fixture in place) -> buildResult (success + diagnostic-error paths) -> finally drop-created cleanup. The runner becomes a thin spec + delegator. Its result-assembly logic is lifted verbatim and re-pointed at the fixture, so the artifact stays byte-for-byte equivalent. Per-table createTable routing assertions and the single-engine check are preserved. Because there is no seed cache, this family never emits a seedHash, so the seedHash diff-mask dance the field-add family needs does not apply here — no diff-artifacts.mjs mask changes were required. Validation (local): two cases x v1,v2. - Baseline A vs B clean with existing masks (no seed cache, no new masks). - G1 baseline A vs candidate clean for all 4 case x engine artifacts. - Negative tests: semantic perturbation (verification fieldCount) fails; masked-field difference (tableId / *Ms) passes; sibling-of-masked semantic field (createdTables[].status) still fails (mask not over-broad). - Semantics: result=pass, routeMatched=true, v1 actualV2=false / v2=true, verification tables correct, error=null. - Trace refs preserved vs baseline (40 for 10x, 6 for 1k). Tracker: table-create -> Migrated; 22/35 -> 23/35 runner kinds, 29/55 -> 31/55 cases. Co-Authored-By: Claude --- framework/runners/table-create-lifecycle.ts | 113 ++++++ framework/runners/table-create.runner.ts | 391 +++++++++++--------- tasks/runner-migration-tracker.md | 52 +-- 3 files changed, 354 insertions(+), 202 deletions(-) create mode 100644 framework/runners/table-create-lifecycle.ts diff --git a/framework/runners/table-create-lifecycle.ts b/framework/runners/table-create-lifecycle.ts new file mode 100644 index 0000000..1479eae --- /dev/null +++ b/framework/runners/table-create-lifecycle.ts @@ -0,0 +1,113 @@ +import { PerfRunDiagnosticError } from "../types"; +import type { PerfCase, PerfRunContext, PerfRunResult } from "../types"; + +// The lifecycle skeleton shared by the table-create family: no seed, no reusable +// fixture — the measured operation CREATES entities from scratch inside one timed +// window and drops them on cleanup. It mirrors field-add-lifecycle's +// success/diagnostic/cleanup protocol but deliberately OMITS the +// seedReady/seedMode machinery, because a table-create case has nothing to seed: +// `prepareFixture` only allocates a mutable run accumulator (the created-entity +// list + its measurements) so the driver's buildResult and cleanup can read +// partial state even when the measured loop throws mid-way. +// +// Because there is no seed cache, this family never emits a seedHash — so the +// seedHash diff-mask dance the field-add family needs (mask the content-address +// so a behavior-preserving migration still passes the G1 diff) does NOT apply +// here. The migration is purely a re-shape of orchestration around the unchanged +// result-assembly logic. +// +// The driver owns the repeated protocol: +// prepareFixture(allocate) -> runPrimary(measured create + verify, mutating the +// fixture) -> build result (twice: diagnostic catch + success) -> finally +// drop-what-was-created cleanup. +// +// Scope note: table-create-family-shaped, not a universal driver. It assumes a +// seedless, create-from-scratch workload whose cleanup drops exactly what it +// created, and whose partial progress lives on a mutable fixture. A broader +// abstraction should wait for a second seedless family. + +export type TableCreateLifecyclePrepareArgs = { + perfCase: PerfCase; + context: PerfRunContext; + baseId: string; + config: TConfig; +}; + +export type TableCreateLifecycleBuildResultArgs = { + config: TConfig; + fixture?: TFixture; + primary?: TPrimary; + error?: unknown; +}; + +export type TableCreateLifecycleSpec = { + // Allocate the mutable run accumulator the measured operation fills in. + // table-create has no seed, so this does no I/O and must not throw; the + // returned fixture is the partial-state carrier buildResult and cleanup read + // when the measured operation fails part-way. + prepareFixture: ( + args: TableCreateLifecyclePrepareArgs, + ) => Promise; + // The measured operation: create the entities (trace-wrapped, with per-entity + // routing assertions) and verify them, mutating the fixture in place so + // partial progress survives a throw. Owns its own measurements/phases; the + // driver does not wrap it in a phase. + runPrimary: (args: { + perfCase: PerfCase; + context: PerfRunContext; + baseId: string; + fixture: TFixture; + config: TConfig; + }) => Promise; + // Assemble the artifact result from the (mutated) fixture. Called once on + // success and once inside the diagnostic-error path (with `error` set), so it + // must tolerate the partially-filled fixture left behind by a mid-run throw. + buildResult: ( + args: TableCreateLifecycleBuildResultArgs, + ) => PerfRunResult; + // Drop whatever the measured operation created. Runs in `finally`, so it must + // tolerate an undefined fixture (prepare failed) and a partially-filled one. + // `primaryAttempted` is true once the measured operation began. + cleanup: (args: { + baseId: string; + fixture: TFixture | undefined; + config: TConfig; + primaryAttempted: boolean; + }) => Promise; +}; + +export const runTableCreateLifecycle = async ( + perfCase: PerfCase, + context: PerfRunContext, + spec: TableCreateLifecycleSpec, +): Promise => { + const config = perfCase.config as unknown as TConfig; + const baseId = globalThis.testConfig.baseId; + let fixture: TFixture | undefined; + let primaryAttempted = false; + + try { + fixture = await spec.prepareFixture({ perfCase, context, baseId, config }); + let primary: TPrimary | undefined; + + try { + primaryAttempted = true; + primary = await spec.runPrimary({ + perfCase, + context, + baseId, + fixture, + config, + }); + } catch (error) { + throw new PerfRunDiagnosticError( + error instanceof Error ? error.message : String(error), + spec.buildResult({ config, fixture, primary, error }), + ); + } + + return spec.buildResult({ config, fixture, primary }); + } finally { + await spec.cleanup({ baseId, fixture, config, primaryAttempted }); + } +}; diff --git a/framework/runners/table-create.runner.ts b/framework/runners/table-create.runner.ts index fcf5fcc..6912112 100644 --- a/framework/runners/table-create.runner.ts +++ b/framework/runners/table-create.runner.ts @@ -16,7 +16,10 @@ import type { PerfRunResult, TableCreateCaseConfig, } from "../types"; -import { PerfRunDiagnosticError } from "../types"; +import { + runTableCreateLifecycle, + type TableCreateLifecycleSpec, +} from "./table-create-lifecycle"; import { pickTableLifecycleHeaders } from "./table-lifecycle.shared"; type Measurement = { @@ -208,197 +211,233 @@ const verifyCreatedTable = async ( }; }; -export const runTableCreateCase = async ( - perfCase: PerfCase, - context: PerfRunContext, -): Promise => { - const config = perfCase.config as TableCreateCaseConfig; - const baseId = globalThis.testConfig.baseId; - const runTag = `${context.engine}-${Date.now()}`; - const createdTables: CreatedTable[] = []; - let primaryMeasurement: Measurement | undefined; - let verifyMeasurement: Measurement | undefined; +// The mutable run accumulator. table-create has no seed and no reusable +// fixture, so "prepare" only allocates this object: the created-table list plus +// the primary/verify measurements. runPrimary fills it in place, and both +// buildResult and cleanup read it — even when the measured loop throws part-way, +// so the diagnostic artifact still carries the partially-created tables. +type TableCreateFixture = { + runTag: string; + createdTables: CreatedTable[]; + primaryMeasurement?: Measurement; + verifyMeasurement?: Measurement; +}; - const buildResult = (error?: unknown): PerfRunResult => { - const durations = createdTables.map((table) => table.durationMs); - const requestSummary = durations.length - ? summarizeDurations(durations) - : undefined; - const v2Headers = [ - ...new Set( - createdTables.map((table) => table.responseHeaders["x-teable-v2"]), - ), - ]; +// Unchanged result-assembly logic, lifted verbatim out of the old runner and +// re-pointed at the fixture so the artifact stays byte-for-byte equivalent. The +// driver calls it once on success and once inside the diagnostic-error path. +const buildTableCreateResult = ( + config: TableCreateCaseConfig, + fixture: TableCreateFixture, + error?: unknown, +): PerfRunResult => { + const { createdTables, primaryMeasurement, verifyMeasurement } = fixture; + const durations = createdTables.map((table) => table.durationMs); + const requestSummary = durations.length + ? summarizeDurations(durations) + : undefined; + const v2Headers = [ + ...new Set( + createdTables.map((table) => table.responseHeaders["x-teable-v2"]), + ), + ]; - return { - metrics: { - ...(primaryMeasurement - ? { [config.threshold.metric]: primaryMeasurement.durationMs } - : {}), - ...(requestSummary - ? { - createTableMinMs: requestSummary.minMs, - createTableP50Ms: requestSummary.p50Ms, - createTableP95Ms: requestSummary.p95Ms, - createTableMaxMs: requestSummary.maxMs, - } - : {}), - ...(verifyMeasurement - ? { createTablesVerifyMs: verifyMeasurement.durationMs } - : {}), - }, - thresholds: primaryMeasurement + return { + metrics: { + ...(primaryMeasurement + ? { [config.threshold.metric]: primaryMeasurement.durationMs } + : {}), + ...(requestSummary + ? { + createTableMinMs: requestSummary.minMs, + createTableP50Ms: requestSummary.p50Ms, + createTableP95Ms: requestSummary.p95Ms, + createTableMaxMs: requestSummary.maxMs, + } + : {}), + ...(verifyMeasurement + ? { createTablesVerifyMs: verifyMeasurement.durationMs } + : {}), + }, + thresholds: primaryMeasurement + ? [ + { + metric: config.threshold.metric, + max: getPrimaryThresholdMs(config.threshold.maxMs), + unit: "ms", + }, + ] + : [], + phases: [ + ...(primaryMeasurement ? [ { - metric: config.threshold.metric, - max: getPrimaryThresholdMs(config.threshold.maxMs), - unit: "ms", + name: primaryMeasurement.name, + durationMs: primaryMeasurement.durationMs, }, ] - : [], - phases: [ - ...(primaryMeasurement - ? [ - { - name: primaryMeasurement.name, - durationMs: primaryMeasurement.durationMs, - }, - ] - : []), - ...(verifyMeasurement - ? [ - { - name: verifyMeasurement.name, - durationMs: verifyMeasurement.durationMs, - }, - ] - : []), - ], - details: { - tableCount: config.tableCount, - fieldCount: config.fields.length, - emptyRecordsPayload: !config.inlineRecords, - inlineRecordCount: config.inlineRecords?.count ?? 0, - createdTables: createdTables.map((table) => ({ - index: table.index, - tableId: table.tableId, - tableName: table.tableName, - status: table.status, - durationMs: table.durationMs, - responseHeaders: table.responseHeaders, - routing: table.routing, - })), - routing: createdTables.length - ? { - routeMatched: createdTables.every( - (table) => table.routing.routeMatched === true, - ), - consistentEngine: v2Headers.length === 1, - requestedEngine: process.env.PERF_LAB_ENGINE ?? "local", - actualV2Header: v2Headers.length === 1 ? v2Headers[0] : undefined, - actualV2Headers: v2Headers, - feature: createdTables[0]?.responseHeaders["x-teable-v2-feature"], - reason: createdTables[0]?.responseHeaders["x-teable-v2-reason"], - } - : undefined, - verification: verifyMeasurement + : []), + ...(verifyMeasurement + ? [ + { + name: verifyMeasurement.name, + durationMs: verifyMeasurement.durationMs, + }, + ] + : []), + ], + details: { + tableCount: config.tableCount, + fieldCount: config.fields.length, + emptyRecordsPayload: !config.inlineRecords, + inlineRecordCount: config.inlineRecords?.count ?? 0, + createdTables: createdTables.map((table) => ({ + index: table.index, + tableId: table.tableId, + tableName: table.tableName, + status: table.status, + durationMs: table.durationMs, + responseHeaders: table.responseHeaders, + routing: table.routing, + })), + routing: createdTables.length + ? { + routeMatched: createdTables.every( + (table) => table.routing.routeMatched === true, + ), + consistentEngine: v2Headers.length === 1, + requestedEngine: process.env.PERF_LAB_ENGINE ?? "local", + actualV2Header: v2Headers.length === 1 ? v2Headers[0] : undefined, + actualV2Headers: v2Headers, + feature: createdTables[0]?.responseHeaders["x-teable-v2-feature"], + reason: createdTables[0]?.responseHeaders["x-teable-v2-reason"], + } + : undefined, + verification: verifyMeasurement + ? { + metric: "createTablesVerifyMs", + participatesInThreshold: false, + tables: verifyMeasurement.result, + } + : undefined, + error: + error instanceof Error ? { - metric: "createTablesVerifyMs", - participatesInThreshold: false, - tables: verifyMeasurement.result, + name: error.name, + message: error.message, } : undefined, - error: - error instanceof Error - ? { - name: error.name, - message: error.message, - } - : undefined, - }, - }; + }, }; +}; - try { - try { - primaryMeasurement = await measureAsync("createTablesTotal", async () => { - for (let index = 1; index <= config.tableCount; index += 1) { - const tableName = `${config.tableNamePrefix}-${runTag}-${padIndex( - index, - )}`; - const requestMeasurement = await withPerfTraceStep( - context, - perfCase, - `createTable-${padIndex(index)}`, - () => - measureAsync(`createTable-${padIndex(index)}`, () => - createOneTable(baseId, tableName, config), - ), - ); - const responseHeaders = pickTableLifecycleHeaders( - requestMeasurement.result.headers as Record, - ); - createdTables.push({ - index, - tableId: requestMeasurement.result.data.id, - tableName, - status: requestMeasurement.result.status, - durationMs: requestMeasurement.durationMs, - responseHeaders, - routing: assertEngineRouting(context, responseHeaders, { - feature: "createTable", - operation: "createTable", - }), - }); - } - }); - - verifyMeasurement = await withPerfTraceStep( - context, - perfCase, - "createTablesVerify", - () => - measureAsync("createTablesVerify", async () => { - const verifications: TableVerification[] = []; - for (const created of createdTables) { - verifications.push(await verifyCreatedTable(created, config)); - } - return verifications; - }), - ); - - const engines = new Set( - createdTables.map((table) => table.responseHeaders["x-teable-v2"]), - ); - if (engines.size > 1) { - throw new Error( - `createTable requests routed to mixed engines: ${[...engines].join( - ", ", - )}`, +const runTableCreatePrimary = async ( + perfCase: PerfCase, + context: PerfRunContext, + baseId: string, + fixture: TableCreateFixture, + config: TableCreateCaseConfig, +): Promise => { + fixture.primaryMeasurement = await measureAsync( + "createTablesTotal", + async () => { + for (let index = 1; index <= config.tableCount; index += 1) { + const tableName = `${config.tableNamePrefix}-${fixture.runTag}-${padIndex( + index, + )}`; + const requestMeasurement = await withPerfTraceStep( + context, + perfCase, + `createTable-${padIndex(index)}`, + () => + measureAsync(`createTable-${padIndex(index)}`, () => + createOneTable(baseId, tableName, config), + ), ); + const responseHeaders = pickTableLifecycleHeaders( + requestMeasurement.result.headers as Record, + ); + fixture.createdTables.push({ + index, + tableId: requestMeasurement.result.data.id, + tableName, + status: requestMeasurement.result.status, + durationMs: requestMeasurement.durationMs, + responseHeaders, + routing: assertEngineRouting(context, responseHeaders, { + feature: "createTable", + operation: "createTable", + }), + }); } - } catch (error) { - throw new PerfRunDiagnosticError( - error instanceof Error ? error.message : String(error), - buildResult(error), - ); - } + }, + ); - return buildResult(); - } finally { + fixture.verifyMeasurement = await withPerfTraceStep( + context, + perfCase, + "createTablesVerify", + () => + measureAsync("createTablesVerify", async () => { + const verifications: TableVerification[] = []; + for (const created of fixture.createdTables) { + verifications.push(await verifyCreatedTable(created, config)); + } + return verifications; + }), + ); + + const engines = new Set( + fixture.createdTables.map((table) => table.responseHeaders["x-teable-v2"]), + ); + if (engines.size > 1) { + throw new Error( + `createTable requests routed to mixed engines: ${[...engines].join(", ")}`, + ); + } +}; + +// table-create rides the table-create lifecycle as its first (and so far only) +// member: it allocates a fresh run accumulator, creates N tables in one measured +// `createTablesTotal` window (each table trace-wrapped with its own per-request +// routing assertion), verifies every created table, asserts a single routing +// engine across the batch, then drops the created tables on cleanup. There is no +// seed, so prepareFixture does no I/O and the family carries no seedHash. +const tableCreateSpec: TableCreateLifecycleSpec< + TableCreateCaseConfig, + TableCreateFixture, + void +> = { + prepareFixture: ({ context }) => + Promise.resolve({ + runTag: `${context.engine}-${Date.now()}`, + createdTables: [], + }), + runPrimary: ({ perfCase, context, baseId, fixture, config }) => + runTableCreatePrimary(perfCase, context, baseId, fixture, config), + buildResult: ({ config, fixture, error }) => + buildTableCreateResult(config, fixture as TableCreateFixture, error), + cleanup: async ({ baseId, fixture }) => { // CI execute jobs run on an isolated restored copy of the seed dump, so // the mutated database is simply discarded after the job. - if (!isExecuteDbIsolated()) { - for (const created of createdTables) { - try { - await permanentDeleteTable(baseId, created.tableId); - } catch (error) { - console.warn( - `Failed to cleanup created perf table ${created.tableId}`, - error, - ); - } + if (isExecuteDbIsolated() || !fixture) { + return; + } + for (const created of fixture.createdTables) { + try { + await permanentDeleteTable(baseId, created.tableId); + } catch (error) { + console.warn( + `Failed to cleanup created perf table ${created.tableId}`, + error, + ); } } - } + }, }; + +export const runTableCreateCase = ( + perfCase: PerfCase, + context: PerfRunContext, +): Promise => + runTableCreateLifecycle(perfCase, context, tableCreateSpec); diff --git a/tasks/runner-migration-tracker.md b/tasks/runner-migration-tracker.md index 214c0f2..ea2fa07 100644 --- a/tasks/runner-migration-tracker.md +++ b/tasks/runner-migration-tracker.md @@ -6,34 +6,35 @@ that uses it, because migrating a runner means re-verifying all of its cases. Status as of 2026-06-20 on `main`. -**Migrated: 22 / 35 runner kinds · 29 / 55 cases.** +**Migrated: 23 / 35 runner kinds · 31 / 55 cases.** ## Migrated (✅ on the driver) -| Runner kind | Driver / where | Cases | Verified | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| csv-import | `csv-import-lifecycle.ts` | 3 csv-import cases | ✅ v1+v2 pass (local) | -| field-delete | `field-delete-lifecycle.ts` | field-delete/mixed-10k-delete-19-fields | ✅ v1+v2 pass (local) | -| record-delete | `record-replay-lifecycle.ts` (no setup) | record-delete/delete-1k | ✅ v1+v2 pass (local) | -| record-delete-link | `table-link-lifecycle.ts` (single fixture) | record-delete/link-trash-1k | ✅ v1+v2 pass (local) | -| record-undo | `record-replay-lifecycle.ts` (delete setup) | record-undo/delete-1k | ✅ v1+v2 pass (local) | -| record-redo | `record-replay-lifecycle.ts` (delete+undo setup) | record-redo/delete-1k | ✅ v1+v2 pass (local) | -| table-delete | `table-lifecycle.ts` (sampled archive + restore-back) | table-delete/10k-20f | ✅ v1+v2 pass (local) | -| table-restore | `table-lifecycle.ts` (archive setup + sampled restore) | table-restore/10k-20f | ✅ v1+v2 pass (local) | -| table-delete-link | `table-link-lifecycle.ts` (sampled foreign-table delete) | table-delete/10k-20f-link-detach | ✅ v1+v2 pass (local) | -| table-restore-link | `table-link-lifecycle.ts` (sampled table restore) | table-restore/10k-20f-link-1k | ✅ v1+v2 pass (local) | -| selection-duplicate | `record-duplicate-lifecycle.ts` (stream block duplicate) | record-duplicate/grid-block-duplicate-1k | ✅ v1+v2 pass (local) | -| record-duplicate-single | `record-duplicate-lifecycle.ts` (sequential single dup) | record-duplicate/single-record-sequential-100 | ✅ v1+v2 pass (local) | -| record-update | `record-mutation-lifecycle.ts` (bulk update over seeded rows, record window + restore-or-delete) | record-update/mixed-1k-20fields-bulk-update | ✅ v1+v2 pass (local) | -| record-create | `record-mutation-lifecycle.ts` (bulk insert into empty seed, no window + delete-created-or-drop) | record-create/mixed-1k-20fields-bulk-create | ✅ v1+v2 pass (local) | -| record-reorder | `record-mutation-lifecycle.ts` (block reorder over seeded rows, record window + restore-order-or-drop) | record-reorder/10k-move-last-1k-to-front | ✅ v1+v2 pass (local) | -| field-convert | `field-convert-lifecycle.ts` (convert + readiness, keep-or-delete) | field-convert/10k-multi-select-to-text, field-convert/10k-text-to-formula | ✅ v1+v2 pass (local) | -| field-convert-link | `field-convert-lifecycle.ts` (link convert + readiness, delete host+foreign) | field-convert/10k-link-to-text, field-convert/10k-text-to-link | ✅ v1+v2 pass (local) | -| selection-clear | `record-mutation-lifecycle.ts` (clear-stream over seeded rows, no window, post-op verify, restore-or-delete) | selection-clear/flat-1k-20fields-cell-clear-stream | ✅ v1+v2 pass (local) | -| record-update-link | `record-mutation-lifecycle.ts` (bulk link-cell update over host + linked foreign fixture, no window) | record-update/1k-link-cells-bulk-update | ✅ v1+v2 pass (local) | -| conditional-lookup | `field-add-lifecycle.ts` (add a conditional lookup field over a seeded source + host pair, full-scan readiness, restore-or-delete) | lookup/conditional-10k | ✅ v1+v2 pass (local) | -| field-duplicate | `field-add-lifecycle.ts` (duplicate a lookup field over the conditional-lookup seed, backfill readiness, delete the copy or drop tables) | field-duplicate/conditional-lookup-10k | ✅ v1+v2 pass (local) | -| field-create | `field-add-lifecycle.ts` (add N fields over a seeded table, per-field routing + computed-backfill readiness, delete created fields or drop table) | field-create/10k-create-5-simple-fields, field-create/10k-create-5-formula-fields, field-create/mixed-10k-create-19-fields, field-create/single-select-1k-options | ✅ v1+v2 pass (local) | +| Runner kind | Driver / where | Cases | Verified | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| csv-import | `csv-import-lifecycle.ts` | 3 csv-import cases | ✅ v1+v2 pass (local) | +| field-delete | `field-delete-lifecycle.ts` | field-delete/mixed-10k-delete-19-fields | ✅ v1+v2 pass (local) | +| record-delete | `record-replay-lifecycle.ts` (no setup) | record-delete/delete-1k | ✅ v1+v2 pass (local) | +| record-delete-link | `table-link-lifecycle.ts` (single fixture) | record-delete/link-trash-1k | ✅ v1+v2 pass (local) | +| record-undo | `record-replay-lifecycle.ts` (delete setup) | record-undo/delete-1k | ✅ v1+v2 pass (local) | +| record-redo | `record-replay-lifecycle.ts` (delete+undo setup) | record-redo/delete-1k | ✅ v1+v2 pass (local) | +| table-delete | `table-lifecycle.ts` (sampled archive + restore-back) | table-delete/10k-20f | ✅ v1+v2 pass (local) | +| table-restore | `table-lifecycle.ts` (archive setup + sampled restore) | table-restore/10k-20f | ✅ v1+v2 pass (local) | +| table-delete-link | `table-link-lifecycle.ts` (sampled foreign-table delete) | table-delete/10k-20f-link-detach | ✅ v1+v2 pass (local) | +| table-restore-link | `table-link-lifecycle.ts` (sampled table restore) | table-restore/10k-20f-link-1k | ✅ v1+v2 pass (local) | +| selection-duplicate | `record-duplicate-lifecycle.ts` (stream block duplicate) | record-duplicate/grid-block-duplicate-1k | ✅ v1+v2 pass (local) | +| record-duplicate-single | `record-duplicate-lifecycle.ts` (sequential single dup) | record-duplicate/single-record-sequential-100 | ✅ v1+v2 pass (local) | +| record-update | `record-mutation-lifecycle.ts` (bulk update over seeded rows, record window + restore-or-delete) | record-update/mixed-1k-20fields-bulk-update | ✅ v1+v2 pass (local) | +| record-create | `record-mutation-lifecycle.ts` (bulk insert into empty seed, no window + delete-created-or-drop) | record-create/mixed-1k-20fields-bulk-create | ✅ v1+v2 pass (local) | +| record-reorder | `record-mutation-lifecycle.ts` (block reorder over seeded rows, record window + restore-order-or-drop) | record-reorder/10k-move-last-1k-to-front | ✅ v1+v2 pass (local) | +| field-convert | `field-convert-lifecycle.ts` (convert + readiness, keep-or-delete) | field-convert/10k-multi-select-to-text, field-convert/10k-text-to-formula | ✅ v1+v2 pass (local) | +| field-convert-link | `field-convert-lifecycle.ts` (link convert + readiness, delete host+foreign) | field-convert/10k-link-to-text, field-convert/10k-text-to-link | ✅ v1+v2 pass (local) | +| selection-clear | `record-mutation-lifecycle.ts` (clear-stream over seeded rows, no window, post-op verify, restore-or-delete) | selection-clear/flat-1k-20fields-cell-clear-stream | ✅ v1+v2 pass (local) | +| record-update-link | `record-mutation-lifecycle.ts` (bulk link-cell update over host + linked foreign fixture, no window) | record-update/1k-link-cells-bulk-update | ✅ v1+v2 pass (local) | +| conditional-lookup | `field-add-lifecycle.ts` (add a conditional lookup field over a seeded source + host pair, full-scan readiness, restore-or-delete) | lookup/conditional-10k | ✅ v1+v2 pass (local) | +| field-duplicate | `field-add-lifecycle.ts` (duplicate a lookup field over the conditional-lookup seed, backfill readiness, delete the copy or drop tables) | field-duplicate/conditional-lookup-10k | ✅ v1+v2 pass (local) | +| field-create | `field-add-lifecycle.ts` (add N fields over a seeded table, per-field routing + computed-backfill readiness, delete created fields or drop table) | field-create/10k-create-5-simple-fields, field-create/10k-create-5-formula-fields, field-create/mixed-10k-create-19-fields, field-create/single-select-1k-options | ✅ v1+v2 pass (local) | +| table-create | `table-create-lifecycle.ts` (seedless create-from-scratch: allocate a run accumulator, create N tables in one measured window with per-table routing, verify each, assert single engine, drop created tables on cleanup; no seed → no seedHash) | table-create/10x-20f-no-records, table-create/1x-20f-1k-records | ✅ v1+v2 pass (local) | ## Not migrated (⬜ legacy `*.runner.ts`) @@ -51,7 +52,6 @@ Status as of 2026-06-20 on `main`. | record-paste | 4 | record-paste/flat-10k-4fields-copy-paste, record-paste/flat-10k-20fields-copy-paste, record-paste/mixed-10k-20fields-complex-copy-paste, selection-paste/10k-expand-rows-and-fields-stream | | record-read | 2 | record-read/10k-50fields-10x1k-pages, record-read/10k-50fields-filter-sort-groupby-overhead | | record-update-attachment | 1 | record-update/attachment-insert-100 | -| table-create | 2 | table-create/10x-20f-no-records, table-create/1x-20f-1k-records | ## How migration proceeds