@workglow/test-contract 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +232 -0
- package/dist/ai-provider/assertions/capabilityHonesty.d.ts +7 -0
- package/dist/ai-provider/assertions/dispose.d.ts +7 -0
- package/dist/ai-provider/assertions/inferAdvertisesRegistered.d.ts +15 -0
- package/dist/ai-provider/assertions/inferServesInferred.d.ts +20 -0
- package/dist/ai-provider/assertions/pricingMatchesModality.d.ts +36 -0
- package/dist/ai-provider/assertions/registryCoverage.d.ts +13 -0
- package/dist/ai-provider/assertions/sessionReuse.d.ts +7 -0
- package/dist/ai-provider/assertions/signalHonoring.d.ts +7 -0
- package/dist/ai-provider/assertions/structuredGeneration.d.ts +7 -0
- package/dist/ai-provider/assertions/textGenerationSmoke.d.ts +7 -0
- package/dist/ai-provider/assertions/toolCallAccumulator.d.ts +7 -0
- package/dist/ai-provider/assertions/toolCallMultiTurn.d.ts +7 -0
- package/dist/ai-provider/assertions/usageNormalization.d.ts +61 -0
- package/dist/ai-provider/fixtures.d.ts +8 -0
- package/dist/ai-provider/runAiProviderConformance.d.ts +7 -0
- package/dist/ai-provider/types.d.ts +75 -0
- package/dist/ai-provider.d.ts +25 -0
- package/dist/ai-provider.js +876 -0
- package/dist/ai-provider.js.map +26 -0
- package/dist/browser-context/ConformanceMockContext.d.ts +54 -0
- package/dist/browser-context/assertions/ariaRoundTrip.d.ts +7 -0
- package/dist/browser-context/assertions/capabilityHonesty.d.ts +7 -0
- package/dist/browser-context/assertions/itExpectFail.d.ts +8 -0
- package/dist/browser-context/assertions/networkIntrospection.d.ts +7 -0
- package/dist/browser-context/assertions/tabsLifecycle.d.ts +7 -0
- package/dist/browser-context/fixtures.d.ts +8 -0
- package/dist/browser-context/runIBrowserContextConformance.d.ts +7 -0
- package/dist/browser-context/types.d.ts +48 -0
- package/dist/browser-context.d.ts +18 -0
- package/dist/browser-context.js +567 -0
- package/dist/browser-context.js.map +17 -0
- package/dist/creditExhaustedSkip.d.ts +39 -0
- package/dist/creditExhaustedSkip.js +163 -0
- package/dist/creditExhaustedSkip.js.map +10 -0
- package/dist/entitlement-profile/assertions/denialShape.d.ts +13 -0
- package/dist/entitlement-profile/assertions/dispose.d.ts +16 -0
- package/dist/entitlement-profile/assertions/hierarchyHonoring.d.ts +11 -0
- package/dist/entitlement-profile/assertions/optionalNeverDenied.d.ts +11 -0
- package/dist/entitlement-profile/assertions/requestEntitlementShape.d.ts +12 -0
- package/dist/entitlement-profile/assertions/resourceScoping.d.ts +16 -0
- package/dist/entitlement-profile/assertions/subscribeGrant.d.ts +11 -0
- package/dist/entitlement-profile/assertions/subscribeReload.d.ts +11 -0
- package/dist/entitlement-profile/assertions/subscribeRevocation.d.ts +12 -0
- package/dist/entitlement-profile/assertions/surfaceCoverage.d.ts +7 -0
- package/dist/entitlement-profile/assertions/unsubscribeIdempotent.d.ts +7 -0
- package/dist/entitlement-profile/fixtures.d.ts +21 -0
- package/dist/entitlement-profile/runEntitlementProfileConformance.d.ts +7 -0
- package/dist/entitlement-profile/types.d.ts +45 -0
- package/dist/entitlement-profile.d.ts +23 -0
- package/dist/entitlement-profile.js +436 -0
- package/dist/entitlement-profile.js.map +22 -0
- package/dist/human-connector/MockHumanConnector.d.ts +29 -0
- package/dist/human-connector/assertions/abort.d.ts +7 -0
- package/dist/human-connector/assertions/capabilityHonesty.d.ts +7 -0
- package/dist/human-connector/assertions/concurrentIsolation.d.ts +7 -0
- package/dist/human-connector/assertions/multiTurnFollowUp.d.ts +7 -0
- package/dist/human-connector/assertions/notifyDisplayFastResolve.d.ts +7 -0
- package/dist/human-connector/assertions/roundtrip.d.ts +7 -0
- package/dist/human-connector/fixtures.d.ts +8 -0
- package/dist/human-connector/runHumanConnectorConformance.d.ts +9 -0
- package/dist/human-connector/types.d.ts +91 -0
- package/dist/human-connector.d.ts +19 -0
- package/dist/human-connector.js +843 -0
- package/dist/human-connector.js.map +20 -0
- package/dist/itExpectFail.d.ts +15 -0
- package/dist/itExpectFail.js +180 -0
- package/dist/itExpectFail.js.map +11 -0
- package/dist/storage-migrations/assertions/appliesAndRecords.d.ts +7 -0
- package/dist/storage-migrations/assertions/concurrentRunsSerialize.d.ts +18 -0
- package/dist/storage-migrations/assertions/ensureBookkeepingIdempotent.d.ts +7 -0
- package/dist/storage-migrations/assertions/failedMigrationLeavesNoPartialSchema.d.ts +14 -0
- package/dist/storage-migrations/assertions/failedMigrationNotRecorded.d.ts +7 -0
- package/dist/storage-migrations/assertions/idempotentRun.d.ts +7 -0
- package/dist/storage-migrations/assertions/incrementalApplication.d.ts +7 -0
- package/dist/storage-migrations/runMigrationRunnerContract.d.ts +9 -0
- package/dist/storage-migrations/types.d.ts +78 -0
- package/dist/storage-migrations.d.ts +18 -0
- package/dist/storage-migrations.js +473 -0
- package/dist/storage-migrations.js.map +20 -0
- package/dist/tabular-migrations/assertions/addAndDropIndex.d.ts +7 -0
- package/dist/tabular-migrations/assertions/addColumn.d.ts +7 -0
- package/dist/tabular-migrations/assertions/backfill.d.ts +7 -0
- package/dist/tabular-migrations/assertions/dropColumn.d.ts +7 -0
- package/dist/tabular-migrations/assertions/failedMigrationNotRecorded.d.ts +7 -0
- package/dist/tabular-migrations/assertions/freshDbFastPath.d.ts +7 -0
- package/dist/tabular-migrations/assertions/incrementalApplication.d.ts +7 -0
- package/dist/tabular-migrations/assertions/renameColumn.d.ts +7 -0
- package/dist/tabular-migrations/runTabularMigrationContract.d.ts +8 -0
- package/dist/tabular-migrations/types.d.ts +28 -0
- package/dist/tabular-migrations.d.ts +19 -0
- package/dist/tabular-migrations.js +257 -0
- package/dist/tabular-migrations.js.map +18 -0
- package/dist/tabular-storage/assertions/countMatchesQuery.d.ts +7 -0
- package/dist/tabular-storage/assertions/guardParity.d.ts +25 -0
- package/dist/tabular-storage/assertions/inListCriterion.d.ts +14 -0
- package/dist/tabular-storage/assertions/joinBoundedLeftRead.d.ts +29 -0
- package/dist/tabular-storage/assertions/joinPushdown.d.ts +13 -0
- package/dist/tabular-storage/assertions/notInListCriterion.d.ts +15 -0
- package/dist/tabular-storage/assertions/strategyParity.d.ts +31 -0
- package/dist/tabular-storage/assertions/subscribeToChanges.d.ts +21 -0
- package/dist/tabular-storage/assertions/vectorColumnFormat.d.ts +7 -0
- package/dist/tabular-storage/assertions/withConnectionTransaction.d.ts +7 -0
- package/dist/tabular-storage/assertions/withTransactionRollback.d.ts +7 -0
- package/dist/tabular-storage/fixtures.d.ts +33 -0
- package/dist/tabular-storage/joinFixtures.d.ts +66 -0
- package/dist/tabular-storage/runTabularJoinContract.d.ts +24 -0
- package/dist/tabular-storage/runTabularStorageContract.d.ts +9 -0
- package/dist/tabular-storage/types.d.ts +102 -0
- package/dist/tabular-storage.d.ts +25 -0
- package/dist/tabular-storage.js +1477 -0
- package/dist/tabular-storage.js.map +27 -0
- package/dist/vector-storage/assertions/dimensionValidation.d.ts +17 -0
- package/dist/vector-storage/assertions/legacyEncoding.d.ts +16 -0
- package/dist/vector-storage/assertions/metadataFilter.d.ts +16 -0
- package/dist/vector-storage/assertions/searchOptions.d.ts +16 -0
- package/dist/vector-storage/assertions/shared.d.ts +23 -0
- package/dist/vector-storage/assertions/similarityRanking.d.ts +18 -0
- package/dist/vector-storage/assertions/similaritySearchEvent.d.ts +16 -0
- package/dist/vector-storage/assertions/vectorRoundTrip.d.ts +17 -0
- package/dist/vector-storage/runVectorStorageContract.d.ts +20 -0
- package/dist/vector-storage/types.d.ts +87 -0
- package/dist/vector-storage.d.ts +19 -0
- package/dist/vector-storage.js +412 -0
- package/dist/vector-storage.js.map +21 -0
- package/dist/web-search/assertions/capabilityAgreement.d.ts +23 -0
- package/dist/web-search/assertions/credentialNaming.d.ts +21 -0
- package/dist/web-search/assertions/dateFiltering.d.ts +22 -0
- package/dist/web-search/assertions/domainNormalization.d.ts +24 -0
- package/dist/web-search/assertions/maxResultsCeiling.d.ts +22 -0
- package/dist/web-search/assertions/reportedQuery.d.ts +18 -0
- package/dist/web-search/assertions/signalThreading.d.ts +20 -0
- package/dist/web-search/fixtures.d.ts +40 -0
- package/dist/web-search/runWebSearchProviderConformance.d.ts +24 -0
- package/dist/web-search/types.d.ts +79 -0
- package/dist/web-search.d.ts +19 -0
- package/dist/web-search.js +560 -0
- package/dist/web-search.js.map +18 -0
- package/dist/worker-proxy/assertions/backlogOrdering.d.ts +7 -0
- package/dist/worker-proxy/assertions/disposeTerminatesWorker.d.ts +8 -0
- package/dist/worker-proxy/assertions/errorPropagation.d.ts +7 -0
- package/dist/worker-proxy/assertions/providerCallHelpers.d.ts +23 -0
- package/dist/worker-proxy/browserOnlyStub.d.ts +7 -0
- package/dist/worker-proxy/runWorkerProxyBoundary.d.ts +7 -0
- package/dist/worker-proxy/types.d.ts +47 -0
- package/dist/worker-proxy.d.ts +16 -0
- package/dist/worker-proxy.js +389 -0
- package/dist/worker-proxy.js.map +17 -0
- package/package.json +120 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularMigrationContractHandle, TabularMigrationContractOpts } from "../types";
|
|
7
|
+
export declare function freshDbFastPathBlock(opts: TabularMigrationContractOpts, getHandle: () => TabularMigrationContractHandle): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularMigrationContractHandle, TabularMigrationContractOpts } from "../types";
|
|
7
|
+
export declare function incrementalApplicationBlock(_opts: TabularMigrationContractOpts, getHandle: () => TabularMigrationContractHandle): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularMigrationContractHandle, TabularMigrationContractOpts } from "../types";
|
|
7
|
+
export declare function renameColumnBlock(_opts: TabularMigrationContractOpts, getHandle: () => TabularMigrationContractHandle): void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularMigrationContractOpts } from "./types";
|
|
7
|
+
export declare function runTabularMigrationContract(opts: TabularMigrationContractOpts): void;
|
|
8
|
+
export type { TabularMigrationContractHandle, TabularMigrationContractOpts } from "./types";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { AnyTabularStorage, ITabularMigration } from "@workglow/storage";
|
|
7
|
+
export interface TabularMigrationContractHandle {
|
|
8
|
+
/**
|
|
9
|
+
* Build a fresh storage at the given target schema, with the supplied
|
|
10
|
+
* migrations and an optional pre-populated set of rows. Calls
|
|
11
|
+
* `setupDatabase` before returning.
|
|
12
|
+
*
|
|
13
|
+
* The schema's properties at minimum include `id: string` (PK) plus
|
|
14
|
+
* whatever extra columns the test's migration ops mention.
|
|
15
|
+
*/
|
|
16
|
+
makeStorage(properties: Record<string, unknown>, migrations: ReadonlyArray<ITabularMigration>, preExistingRows?: ReadonlyArray<Record<string, unknown>>): Promise<AnyTabularStorage>;
|
|
17
|
+
dispose(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export interface TabularMigrationContractOpts {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly factory: () => Promise<TabularMigrationContractHandle>;
|
|
22
|
+
readonly skip?: boolean;
|
|
23
|
+
readonly timeout?: number;
|
|
24
|
+
/** True for SQL backends: DDL is enforced; addColumn produces a real column. */
|
|
25
|
+
readonly enforcesDdl: boolean;
|
|
26
|
+
/** True for FsFolder, IDB, SQLite-on-disk: bookkeeping survives restart. */
|
|
27
|
+
readonly persistentBookkeeping?: boolean;
|
|
28
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Conformance suite for tabular schema migrations — add, drop, rename and
|
|
8
|
+
* backfill against a live table.
|
|
9
|
+
*/
|
|
10
|
+
export * from "./tabular-migrations/runTabularMigrationContract";
|
|
11
|
+
export * from "./tabular-migrations/types";
|
|
12
|
+
export * from "./tabular-migrations/assertions/addAndDropIndex";
|
|
13
|
+
export * from "./tabular-migrations/assertions/addColumn";
|
|
14
|
+
export * from "./tabular-migrations/assertions/backfill";
|
|
15
|
+
export * from "./tabular-migrations/assertions/dropColumn";
|
|
16
|
+
export * from "./tabular-migrations/assertions/failedMigrationNotRecorded";
|
|
17
|
+
export * from "./tabular-migrations/assertions/freshDbFastPath";
|
|
18
|
+
export * from "./tabular-migrations/assertions/incrementalApplication";
|
|
19
|
+
export * from "./tabular-migrations/assertions/renameColumn";
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// src/tabular-migrations/runTabularMigrationContract.ts
|
|
2
|
+
import { afterAll, beforeAll, describe } from "vitest";
|
|
3
|
+
|
|
4
|
+
// src/tabular-migrations/assertions/addAndDropIndex.ts
|
|
5
|
+
import { expect, it } from "vitest";
|
|
6
|
+
function addAndDropIndexBlock(_opts, getHandle) {
|
|
7
|
+
it("addIndex / dropIndex: query still returns correct rows", async () => {
|
|
8
|
+
const storage = await getHandle().makeStorage({ id: { type: "string" }, name: { type: "string" } }, [
|
|
9
|
+
{
|
|
10
|
+
version: 1,
|
|
11
|
+
ops: [
|
|
12
|
+
{ kind: "addIndex", name: "idx_name", columns: ["name"] },
|
|
13
|
+
{ kind: "dropIndex", name: "idx_name" }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
], [
|
|
17
|
+
{ id: "u1", name: "alice" },
|
|
18
|
+
{ id: "u2", name: "bob" }
|
|
19
|
+
]);
|
|
20
|
+
const rows = await storage.query({ name: "alice" }) ?? [];
|
|
21
|
+
expect(rows.map((r) => r.id)).toEqual(["u1"]);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/tabular-migrations/assertions/addColumn.ts
|
|
26
|
+
import { expect as expect2, it as it2 } from "vitest";
|
|
27
|
+
function addColumnBlock(_opts, getHandle) {
|
|
28
|
+
it2("addColumn: existing rows survive; new column readable", async () => {
|
|
29
|
+
const storage = await getHandle().makeStorage({
|
|
30
|
+
id: { type: "string" },
|
|
31
|
+
name: { type: "string" },
|
|
32
|
+
archived: { anyOf: [{ type: "boolean" }, { type: "null" }] }
|
|
33
|
+
}, [
|
|
34
|
+
{
|
|
35
|
+
version: 1,
|
|
36
|
+
ops: [
|
|
37
|
+
{
|
|
38
|
+
kind: "addColumn",
|
|
39
|
+
name: "archived",
|
|
40
|
+
schema: { anyOf: [{ type: "boolean" }, { type: "null" }] }
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
], [{ id: "u1", name: "alice" }]);
|
|
45
|
+
const row = await storage.get({ id: "u1" });
|
|
46
|
+
expect2(row).toBeDefined();
|
|
47
|
+
expect2(row.id).toBe("u1");
|
|
48
|
+
expect2(row.archived ?? null).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/tabular-migrations/assertions/backfill.ts
|
|
53
|
+
import { expect as expect3, it as it3 } from "vitest";
|
|
54
|
+
function backfillBlock(_opts, getHandle) {
|
|
55
|
+
it3("backfill: every existing row is rewritten", async () => {
|
|
56
|
+
const rows = Array.from({ length: 30 }, (_, i) => ({ id: `r${i}`, n: i }));
|
|
57
|
+
const storage = await getHandle().makeStorage({ id: { type: "string" }, n: { type: "integer" } }, [
|
|
58
|
+
{
|
|
59
|
+
version: 1,
|
|
60
|
+
ops: [
|
|
61
|
+
{
|
|
62
|
+
kind: "backfill",
|
|
63
|
+
batchSize: 7,
|
|
64
|
+
transform: (row) => ({ ...row, n: row.n * 2 })
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
], rows);
|
|
69
|
+
const all = await storage.getAll() ?? [];
|
|
70
|
+
const r3 = all.find((r) => r.id === "r3");
|
|
71
|
+
expect3(r3).toBeDefined();
|
|
72
|
+
expect3(r3.n).toBe(6);
|
|
73
|
+
expect3(all.length).toBe(30);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/tabular-migrations/assertions/dropColumn.ts
|
|
78
|
+
import { expect as expect4, it as it4 } from "vitest";
|
|
79
|
+
function dropColumnBlock(_opts, getHandle) {
|
|
80
|
+
it4("dropColumn: column removed; existing rows preserved (sans column)", async () => {
|
|
81
|
+
const storage = await getHandle().makeStorage({ id: { type: "string" }, name: { type: "string" } }, [
|
|
82
|
+
{
|
|
83
|
+
version: 1,
|
|
84
|
+
ops: [{ kind: "dropColumn", name: "removed_field" }]
|
|
85
|
+
}
|
|
86
|
+
], [{ id: "u1", name: "alice", removed_field: "old" }]);
|
|
87
|
+
const row = await storage.get({ id: "u1" });
|
|
88
|
+
expect4(row).toBeDefined();
|
|
89
|
+
expect4(row.name).toBe("alice");
|
|
90
|
+
expect4(row.removed_field).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/tabular-migrations/assertions/failedMigrationNotRecorded.ts
|
|
95
|
+
import { expect as expect5, it as it5 } from "vitest";
|
|
96
|
+
function failedMigrationNotRecordedBlock(_opts, getHandle) {
|
|
97
|
+
it5("a failed migration is not recorded; subsequent run can succeed", async () => {
|
|
98
|
+
let failOnFirst = true;
|
|
99
|
+
const migrations = [
|
|
100
|
+
{
|
|
101
|
+
version: 1,
|
|
102
|
+
ops: [
|
|
103
|
+
{
|
|
104
|
+
kind: "backfill",
|
|
105
|
+
transform: (row) => {
|
|
106
|
+
if (failOnFirst) {
|
|
107
|
+
failOnFirst = false;
|
|
108
|
+
throw new Error("boom");
|
|
109
|
+
}
|
|
110
|
+
return row;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
await expect5(getHandle().makeStorage({ id: { type: "string" } }, migrations, [{ id: "u1" }])).rejects.toThrow(/boom/);
|
|
117
|
+
const storage = await getHandle().makeStorage({ id: { type: "string" } }, migrations, [
|
|
118
|
+
{ id: "u1" }
|
|
119
|
+
]);
|
|
120
|
+
expect5(await storage.size()).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/tabular-migrations/assertions/freshDbFastPath.ts
|
|
125
|
+
import { expect as expect6, it as it6 } from "vitest";
|
|
126
|
+
function freshDbFastPathBlock(opts, getHandle) {
|
|
127
|
+
it6("fresh DB: backfill is NOT executed; bookkeeping marks all applied", async () => {
|
|
128
|
+
let transformCalls = 0;
|
|
129
|
+
const storage = await getHandle().makeStorage({ id: { type: "string" }, n: { type: "integer" } }, [
|
|
130
|
+
{
|
|
131
|
+
version: 1,
|
|
132
|
+
ops: [
|
|
133
|
+
{
|
|
134
|
+
kind: "backfill",
|
|
135
|
+
transform: (row) => {
|
|
136
|
+
transformCalls++;
|
|
137
|
+
return row;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
], []);
|
|
143
|
+
expect6(transformCalls).toBe(0);
|
|
144
|
+
expect6(await storage.size()).toBe(0);
|
|
145
|
+
});
|
|
146
|
+
if (opts.enforcesDdl) {
|
|
147
|
+
it6("fresh DB: addColumn against target-schema does NOT execute (fast path)", async () => {
|
|
148
|
+
const storage = await getHandle().makeStorage({
|
|
149
|
+
id: { type: "string" },
|
|
150
|
+
name: { type: "string" },
|
|
151
|
+
archived: { anyOf: [{ type: "boolean" }, { type: "null" }] }
|
|
152
|
+
}, [
|
|
153
|
+
{
|
|
154
|
+
version: 1,
|
|
155
|
+
ops: [
|
|
156
|
+
{
|
|
157
|
+
kind: "addColumn",
|
|
158
|
+
name: "archived",
|
|
159
|
+
schema: { anyOf: [{ type: "boolean" }, { type: "null" }] }
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
], []);
|
|
164
|
+
expect6(await storage.size()).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/tabular-migrations/assertions/incrementalApplication.ts
|
|
170
|
+
import { expect as expect7, it as it7 } from "vitest";
|
|
171
|
+
function incrementalApplicationBlock(_opts, getHandle) {
|
|
172
|
+
it7("running migrations twice is idempotent (each version applied once)", async () => {
|
|
173
|
+
let backfillCalls = 0;
|
|
174
|
+
const migrations = [
|
|
175
|
+
{
|
|
176
|
+
version: 1,
|
|
177
|
+
ops: [
|
|
178
|
+
{
|
|
179
|
+
kind: "backfill",
|
|
180
|
+
transform: (row) => {
|
|
181
|
+
backfillCalls++;
|
|
182
|
+
return row;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
];
|
|
188
|
+
await getHandle().makeStorage({ id: { type: "string" }, n: { type: "integer" } }, migrations, [
|
|
189
|
+
{ id: "r1", n: 1 }
|
|
190
|
+
]);
|
|
191
|
+
const firstCount = backfillCalls;
|
|
192
|
+
await getHandle().makeStorage({ id: { type: "string" }, n: { type: "integer" } }, migrations, [
|
|
193
|
+
{ id: "r1", n: 1 }
|
|
194
|
+
]);
|
|
195
|
+
expect7(backfillCalls).toBe(firstCount);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/tabular-migrations/assertions/renameColumn.ts
|
|
200
|
+
import { expect as expect8, it as it8 } from "vitest";
|
|
201
|
+
function renameColumnBlock(_opts, getHandle) {
|
|
202
|
+
it8("renameColumn: data preserved under new name", async () => {
|
|
203
|
+
const storage = await getHandle().makeStorage({ id: { type: "string" }, state: { type: "string" } }, [
|
|
204
|
+
{
|
|
205
|
+
version: 1,
|
|
206
|
+
ops: [{ kind: "renameColumn", from: "status", to: "state" }]
|
|
207
|
+
}
|
|
208
|
+
], [{ id: "u1", status: "active" }]);
|
|
209
|
+
const row = await storage.get({ id: "u1" });
|
|
210
|
+
expect8(row).toBeDefined();
|
|
211
|
+
expect8(row.state).toBe("active");
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/tabular-migrations/runTabularMigrationContract.ts
|
|
216
|
+
function runTabularMigrationContract(opts) {
|
|
217
|
+
describe.skipIf(opts.skip)(`Tabular migration contract: ${opts.name}`, () => {
|
|
218
|
+
let handle;
|
|
219
|
+
const getHandle = () => {
|
|
220
|
+
if (!handle)
|
|
221
|
+
throw new Error("handle not initialized");
|
|
222
|
+
return handle;
|
|
223
|
+
};
|
|
224
|
+
beforeAll(async () => {
|
|
225
|
+
handle = await opts.factory();
|
|
226
|
+
}, opts.timeout);
|
|
227
|
+
afterAll(async () => {
|
|
228
|
+
if (handle)
|
|
229
|
+
await handle.dispose();
|
|
230
|
+
});
|
|
231
|
+
if (opts.enforcesDdl) {
|
|
232
|
+
addColumnBlock(opts, getHandle);
|
|
233
|
+
dropColumnBlock(opts, getHandle);
|
|
234
|
+
renameColumnBlock(opts, getHandle);
|
|
235
|
+
addAndDropIndexBlock(opts, getHandle);
|
|
236
|
+
}
|
|
237
|
+
backfillBlock(opts, getHandle);
|
|
238
|
+
freshDbFastPathBlock(opts, getHandle);
|
|
239
|
+
if (opts.persistentBookkeeping !== false) {
|
|
240
|
+
incrementalApplicationBlock(opts, getHandle);
|
|
241
|
+
failedMigrationNotRecordedBlock(opts, getHandle);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
export {
|
|
246
|
+
addAndDropIndexBlock,
|
|
247
|
+
addColumnBlock,
|
|
248
|
+
backfillBlock,
|
|
249
|
+
dropColumnBlock,
|
|
250
|
+
failedMigrationNotRecordedBlock,
|
|
251
|
+
freshDbFastPathBlock,
|
|
252
|
+
incrementalApplicationBlock,
|
|
253
|
+
renameColumnBlock,
|
|
254
|
+
runTabularMigrationContract
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
//# debugId=FAE6293E721A4E2664756E2164756E21
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/tabular-migrations/runTabularMigrationContract.ts", "../src/tabular-migrations/assertions/addAndDropIndex.ts", "../src/tabular-migrations/assertions/addColumn.ts", "../src/tabular-migrations/assertions/backfill.ts", "../src/tabular-migrations/assertions/dropColumn.ts", "../src/tabular-migrations/assertions/failedMigrationNotRecorded.ts", "../src/tabular-migrations/assertions/freshDbFastPath.ts", "../src/tabular-migrations/assertions/incrementalApplication.ts", "../src/tabular-migrations/assertions/renameColumn.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { afterAll, beforeAll, describe } from \"vitest\";\n\nimport { addAndDropIndexBlock } from \"./assertions/addAndDropIndex\";\nimport { addColumnBlock } from \"./assertions/addColumn\";\nimport { backfillBlock } from \"./assertions/backfill\";\nimport { dropColumnBlock } from \"./assertions/dropColumn\";\nimport { failedMigrationNotRecordedBlock } from \"./assertions/failedMigrationNotRecorded\";\nimport { freshDbFastPathBlock } from \"./assertions/freshDbFastPath\";\nimport { incrementalApplicationBlock } from \"./assertions/incrementalApplication\";\nimport { renameColumnBlock } from \"./assertions/renameColumn\";\nimport type { TabularMigrationContractHandle, TabularMigrationContractOpts } from \"./types\";\n\nexport function runTabularMigrationContract(opts: TabularMigrationContractOpts): void {\n describe.skipIf(opts.skip)(`Tabular migration contract: ${opts.name}`, () => {\n let handle: TabularMigrationContractHandle | undefined;\n const getHandle = (): TabularMigrationContractHandle => {\n if (!handle) throw new Error(\"handle not initialized\");\n return handle;\n };\n\n beforeAll(async () => {\n handle = await opts.factory();\n }, opts.timeout);\n afterAll(async () => {\n if (handle) await handle.dispose();\n });\n\n if (opts.enforcesDdl) {\n addColumnBlock(opts, getHandle);\n dropColumnBlock(opts, getHandle);\n renameColumnBlock(opts, getHandle);\n addAndDropIndexBlock(opts, getHandle);\n }\n backfillBlock(opts, getHandle);\n freshDbFastPathBlock(opts, getHandle);\n if (opts.persistentBookkeeping !== false) {\n incrementalApplicationBlock(opts, getHandle);\n failedMigrationNotRecordedBlock(opts, getHandle);\n }\n });\n}\n\nexport type { TabularMigrationContractHandle, TabularMigrationContractOpts } from \"./types\";\n",
|
|
6
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { expect, it } from \"vitest\";\nimport type { TabularMigrationContractHandle, TabularMigrationContractOpts } from \"../types\";\n\nexport function addAndDropIndexBlock(\n _opts: TabularMigrationContractOpts,\n getHandle: () => TabularMigrationContractHandle\n): void {\n it(\"addIndex / dropIndex: query still returns correct rows\", async () => {\n const storage = await getHandle().makeStorage(\n { id: { type: \"string\" }, name: { type: \"string\" } },\n [\n {\n version: 1,\n ops: [\n { kind: \"addIndex\", name: \"idx_name\", columns: [\"name\"] },\n { kind: \"dropIndex\", name: \"idx_name\" },\n ],\n },\n ],\n [\n { id: \"u1\", name: \"alice\" },\n { id: \"u2\", name: \"bob\" },\n ]\n );\n const rows = (await storage.query({ name: \"alice\" })) ?? [];\n expect(rows.map((r) => (r as { id: string }).id)).toEqual([\"u1\"]);\n });\n}\n",
|
|
7
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { expect, it } from \"vitest\";\nimport type { TabularMigrationContractHandle, TabularMigrationContractOpts } from \"../types\";\n\nexport function addColumnBlock(\n _opts: TabularMigrationContractOpts,\n getHandle: () => TabularMigrationContractHandle\n): void {\n it(\"addColumn: existing rows survive; new column readable\", async () => {\n const storage = await getHandle().makeStorage(\n {\n id: { type: \"string\" },\n name: { type: \"string\" },\n archived: { anyOf: [{ type: \"boolean\" }, { type: \"null\" }] },\n },\n [\n {\n version: 1,\n ops: [\n {\n kind: \"addColumn\",\n name: \"archived\",\n schema: { anyOf: [{ type: \"boolean\" }, { type: \"null\" }] },\n },\n ],\n },\n ],\n [{ id: \"u1\", name: \"alice\" }]\n );\n const row = (await storage.get({ id: \"u1\" })) as\n | { id: string; name: string; archived?: boolean | null }\n | undefined;\n expect(row).toBeDefined();\n expect(row!.id).toBe(\"u1\");\n expect(row!.archived ?? null).toBeNull();\n });\n}\n",
|
|
8
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { expect, it } from \"vitest\";\nimport type { TabularMigrationContractHandle, TabularMigrationContractOpts } from \"../types\";\n\nexport function backfillBlock(\n _opts: TabularMigrationContractOpts,\n getHandle: () => TabularMigrationContractHandle\n): void {\n it(\"backfill: every existing row is rewritten\", async () => {\n const rows = Array.from({ length: 30 }, (_, i) => ({ id: `r${i}`, n: i }));\n const storage = await getHandle().makeStorage(\n { id: { type: \"string\" }, n: { type: \"integer\" } },\n [\n {\n version: 1,\n ops: [\n {\n kind: \"backfill\",\n batchSize: 7,\n transform: (row) => ({ ...row, n: (row.n as number) * 2 }),\n },\n ],\n },\n ],\n rows\n );\n const all = (await storage.getAll()) ?? [];\n const r3 = all.find((r) => (r as { id: string }).id === \"r3\") as { n: number } | undefined;\n expect(r3).toBeDefined();\n expect(r3!.n).toBe(6);\n expect(all.length).toBe(30);\n });\n}\n",
|
|
9
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { expect, it } from \"vitest\";\nimport type { TabularMigrationContractHandle, TabularMigrationContractOpts } from \"../types\";\n\nexport function dropColumnBlock(\n _opts: TabularMigrationContractOpts,\n getHandle: () => TabularMigrationContractHandle\n): void {\n it(\"dropColumn: column removed; existing rows preserved (sans column)\", async () => {\n const storage = await getHandle().makeStorage(\n { id: { type: \"string\" }, name: { type: \"string\" } },\n [\n {\n version: 1,\n ops: [{ kind: \"dropColumn\", name: \"removed_field\" }],\n },\n ],\n [{ id: \"u1\", name: \"alice\", removed_field: \"old\" }]\n );\n const row = (await storage.get({ id: \"u1\" })) as\n | { id: string; name: string; removed_field?: string }\n | undefined;\n expect(row).toBeDefined();\n expect(row!.name).toBe(\"alice\");\n expect((row as Record<string, unknown>).removed_field).toBeUndefined();\n });\n}\n",
|
|
10
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { expect, it } from \"vitest\";\nimport type { TabularMigrationContractHandle, TabularMigrationContractOpts } from \"../types\";\n\nexport function failedMigrationNotRecordedBlock(\n _opts: TabularMigrationContractOpts,\n getHandle: () => TabularMigrationContractHandle\n): void {\n it(\"a failed migration is not recorded; subsequent run can succeed\", async () => {\n let failOnFirst = true;\n const migrations = [\n {\n version: 1,\n ops: [\n {\n kind: \"backfill\" as const,\n transform: (row: Record<string, unknown>) => {\n if (failOnFirst) {\n failOnFirst = false;\n throw new Error(\"boom\");\n }\n return row;\n },\n },\n ],\n },\n ];\n await expect(\n getHandle().makeStorage({ id: { type: \"string\" } }, migrations, [{ id: \"u1\" }])\n ).rejects.toThrow(/boom/);\n\n const storage = await getHandle().makeStorage({ id: { type: \"string\" } }, migrations, [\n { id: \"u1\" },\n ]);\n expect(await storage.size()).toBe(1);\n });\n}\n",
|
|
11
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { expect, it } from \"vitest\";\nimport type { TabularMigrationContractHandle, TabularMigrationContractOpts } from \"../types\";\n\nexport function freshDbFastPathBlock(\n opts: TabularMigrationContractOpts,\n getHandle: () => TabularMigrationContractHandle\n): void {\n it(\"fresh DB: backfill is NOT executed; bookkeeping marks all applied\", async () => {\n let transformCalls = 0;\n const storage = await getHandle().makeStorage(\n { id: { type: \"string\" }, n: { type: \"integer\" } },\n [\n {\n version: 1,\n ops: [\n {\n kind: \"backfill\",\n transform: (row) => {\n transformCalls++;\n return row;\n },\n },\n ],\n },\n ],\n [] // no pre-existing rows -> fresh DB\n );\n expect(transformCalls).toBe(0);\n expect(await storage.size()).toBe(0);\n });\n\n // Regression test for the fresh-DB fast path on SQL backends. If the\n // orchestrator's freshTable signal is wired up correctly, addColumn for\n // a column that already exists in the target schema must NOT run as a\n // real ALTER TABLE — which would throw \"duplicate column\" — because\n // the migration is recorded as already-applied without executing ops.\n // Only meaningful on backends that enforce DDL (i.e. SQL ones).\n if (opts.enforcesDdl) {\n it(\"fresh DB: addColumn against target-schema does NOT execute (fast path)\", async () => {\n // The target schema already includes `archived`. A v1 migration that\n // would `ALTER TABLE … ADD COLUMN archived` should be marked applied\n // without running on a brand-new DB.\n const storage = await getHandle().makeStorage(\n {\n id: { type: \"string\" },\n name: { type: \"string\" },\n archived: { anyOf: [{ type: \"boolean\" }, { type: \"null\" }] },\n },\n [\n {\n version: 1,\n ops: [\n {\n kind: \"addColumn\",\n name: \"archived\",\n schema: { anyOf: [{ type: \"boolean\" }, { type: \"null\" }] },\n },\n ],\n },\n ],\n [] // no pre-existing rows -> fresh DB\n );\n // If the fast path is broken, makeStorage above throws on the\n // duplicate-column error and we never reach here. Reaching this\n // assertion is itself the proof.\n expect(await storage.size()).toBe(0);\n });\n }\n}\n",
|
|
12
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { expect, it } from \"vitest\";\nimport type { TabularMigrationContractHandle, TabularMigrationContractOpts } from \"../types\";\n\nexport function incrementalApplicationBlock(\n _opts: TabularMigrationContractOpts,\n getHandle: () => TabularMigrationContractHandle\n): void {\n it(\"running migrations twice is idempotent (each version applied once)\", async () => {\n let backfillCalls = 0;\n const migrations = [\n {\n version: 1,\n ops: [\n {\n kind: \"backfill\" as const,\n transform: (row: Record<string, unknown>) => {\n backfillCalls++;\n return row;\n },\n },\n ],\n },\n ];\n await getHandle().makeStorage({ id: { type: \"string\" }, n: { type: \"integer\" } }, migrations, [\n { id: \"r1\", n: 1 },\n ]);\n const firstCount = backfillCalls;\n await getHandle().makeStorage({ id: { type: \"string\" }, n: { type: \"integer\" } }, migrations, [\n { id: \"r1\", n: 1 },\n ]);\n expect(backfillCalls).toBe(firstCount);\n });\n}\n",
|
|
13
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { expect, it } from \"vitest\";\nimport type { TabularMigrationContractHandle, TabularMigrationContractOpts } from \"../types\";\n\nexport function renameColumnBlock(\n _opts: TabularMigrationContractOpts,\n getHandle: () => TabularMigrationContractHandle\n): void {\n it(\"renameColumn: data preserved under new name\", async () => {\n const storage = await getHandle().makeStorage(\n { id: { type: \"string\" }, state: { type: \"string\" } },\n [\n {\n version: 1,\n ops: [{ kind: \"renameColumn\", from: \"status\", to: \"state\" }],\n },\n ],\n [{ id: \"u1\", status: \"active\" }]\n );\n const row = (await storage.get({ id: \"u1\" })) as { id: string; state: string } | undefined;\n expect(row).toBeDefined();\n expect(row!.state).toBe(\"active\");\n });\n}\n"
|
|
14
|
+
],
|
|
15
|
+
"mappings": "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",
|
|
16
|
+
"debugId": "FAE6293E721A4E2664756E2164756E21",
|
|
17
|
+
"names": []
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularStorageContractOpts } from "../types";
|
|
7
|
+
export declare function countMatchesQueryBlock(opts: TabularStorageContractOpts): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularStorageContractOpts } from "../types";
|
|
7
|
+
/**
|
|
8
|
+
* A guard on a public method is a guard on the transaction path.
|
|
9
|
+
*
|
|
10
|
+
* It was not always. `withTransaction` used to hand its callback a Proxy that
|
|
11
|
+
* resolved `tx.foo(...)` to `_fooInternal(...)` by name, skipping the public
|
|
12
|
+
* method, so a check written on `foo` was silently absent from `tx.foo`. That
|
|
13
|
+
* cost three fixes — `tx.deleteSearch({})` turning from a no-op into a `DELETE
|
|
14
|
+
* FROM t WHERE` syntax error and criteria excluding nothing into a truncated
|
|
15
|
+
* table; `tx.updateWhere` rewriting a row's identity where the same call
|
|
16
|
+
* outside a transaction threw; `tx.getBulk([])` reaching DuckDB as `IN ()` —
|
|
17
|
+
* and each could only restate the guard in the private method.
|
|
18
|
+
*
|
|
19
|
+
* `createTxView` now leaves the public methods in place and neutralizes only
|
|
20
|
+
* the mutex, so `tx.foo(args)` runs `foo`. Parity is a property of the routing
|
|
21
|
+
* rather than of anyone's diligence, and this block is what says so out loud:
|
|
22
|
+
* every guard below is written once, on the public method, and asserted on both
|
|
23
|
+
* paths, over a set of methods derived from the object rather than written down.
|
|
24
|
+
*/
|
|
25
|
+
export declare function guardParityBlock(opts: TabularStorageContractOpts): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularStorageContractOpts } from "../types";
|
|
7
|
+
/**
|
|
8
|
+
* The `{ operator: "in" }` criterion, which every backend renders differently —
|
|
9
|
+
* expanded placeholders on SQLite/DuckDB, a single `= ANY($n)` array parameter
|
|
10
|
+
* on Postgres, a PostgREST `in.()` filter on Supabase, and an in-memory
|
|
11
|
+
* membership test elsewhere. That divergence is exactly why the semantics are
|
|
12
|
+
* pinned here rather than per backend.
|
|
13
|
+
*/
|
|
14
|
+
export declare function inListCriterionBlock(opts: TabularStorageContractOpts): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { AuthorStorage, PostStorage } from "../joinFixtures";
|
|
7
|
+
/**
|
|
8
|
+
* What the hash join reads off the left side, and when it is allowed to stop.
|
|
9
|
+
*
|
|
10
|
+
* `join`'s own docstring makes a promise with a condition attached: `limit` and
|
|
11
|
+
* `offset` apply to the joined rows, and the fallback "bounds the left read —
|
|
12
|
+
* and stops early — only when the joined order does not need a sort", meaning
|
|
13
|
+
* `orderBy` is absent or names left columns the left query can serve. An
|
|
14
|
+
* `orderBy` touching the right side is sorted in memory, and that sort needs
|
|
15
|
+
* every joined row, so such a join reads the whole left table however small its
|
|
16
|
+
* `limit`.
|
|
17
|
+
*
|
|
18
|
+
* Both halves matter and they fail in opposite directions. Losing the bound is
|
|
19
|
+
* a performance cliff a caller cannot see: over a remote backend a `limit: 2`
|
|
20
|
+
* join quietly puts the whole table on the wire. Applying it when the order is
|
|
21
|
+
* NOT already final is a wrong answer: the first N left rows in arbitrary order
|
|
22
|
+
* are sorted afterwards and returned as though they were the top N. Nothing
|
|
23
|
+
* else in this contract asserts either one — the pushdown is bounded by the
|
|
24
|
+
* database, so only the fallback can be caught getting this wrong.
|
|
25
|
+
*/
|
|
26
|
+
export declare function joinBoundedLeftReadBlock(createPosts: () => Promise<PostStorage>, createAuthors: () => Promise<AuthorStorage>, seed: (posts: PostStorage, authors: AuthorStorage) => Promise<void>, opts: {
|
|
27
|
+
readonly expectSqlPushdown?: boolean;
|
|
28
|
+
readonly timeout?: number;
|
|
29
|
+
}): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularStorageContractOpts } from "../types";
|
|
7
|
+
/**
|
|
8
|
+
* Which path a join takes. A backend that can share a connection handle must
|
|
9
|
+
* run a same-handle join as one statement — proven by the right side's `query`
|
|
10
|
+
* never being called — and must fall back to the hash join, which does call
|
|
11
|
+
* it, when the right side lives elsewhere.
|
|
12
|
+
*/
|
|
13
|
+
export declare function joinPushdownBlock(opts: TabularStorageContractOpts): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularStorageContractOpts } from "../types";
|
|
7
|
+
/**
|
|
8
|
+
* The `{ operator: "not-in" }` criterion. Pinned here for the same reason its
|
|
9
|
+
* complement is — `NOT IN (…)` on SQLite/DuckDB, `<> ALL($n)` on Postgres, a
|
|
10
|
+
* PostgREST `not.in.()` filter on Supabase, a JS predicate elsewhere — plus one
|
|
11
|
+
* of its own: an exclusion filter that silently matches nothing looks like an
|
|
12
|
+
* empty result set rather than a bug, so the empty-list and null cases need a
|
|
13
|
+
* test that says which way they are supposed to fall.
|
|
14
|
+
*/
|
|
15
|
+
export declare function notInListCriterionBlock(opts: TabularStorageContractOpts): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { AuthorStorage, PostStorage } from "../joinFixtures";
|
|
7
|
+
/**
|
|
8
|
+
* The two join strategies answer the same question the same way.
|
|
9
|
+
*
|
|
10
|
+
* `ITabularStorage.join` has one documented semantics and two implementations —
|
|
11
|
+
* a single `JOIN` statement when both tables share a connection, and an
|
|
12
|
+
* application-side hash join otherwise — and which one runs is decided by the
|
|
13
|
+
* planner, not by the caller. So a caller cannot tell them apart, and any
|
|
14
|
+
* disagreement between them is a wrong answer that no error reports and that
|
|
15
|
+
* changes with where the right-hand storage happens to live.
|
|
16
|
+
*
|
|
17
|
+
* That has already happened once: a pushdown chosen against a right side
|
|
18
|
+
* enlisted in a transaction it could not see returned zero rows from an inner
|
|
19
|
+
* join where the same call through the fallback returned the row. The block
|
|
20
|
+
* that covered `join` at the time stayed green throughout, because it asserted
|
|
21
|
+
* which path ran and never compared what the two paths returned.
|
|
22
|
+
*
|
|
23
|
+
* This asserts the second thing. It runs only where both strategies are
|
|
24
|
+
* reachable — a pair the planner pushes down — and compares them over specs
|
|
25
|
+
* derived from the option space rather than a list, so an option added to
|
|
26
|
+
* `JoinSpec` is covered by construction.
|
|
27
|
+
*/
|
|
28
|
+
export declare function joinStrategyParityBlock(createPosts: () => Promise<PostStorage>, createAuthors: () => Promise<AuthorStorage>, seed: (posts: PostStorage, authors: AuthorStorage) => Promise<void>, opts: {
|
|
29
|
+
readonly expectSqlPushdown?: boolean;
|
|
30
|
+
readonly timeout?: number;
|
|
31
|
+
}): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularStorageContractOpts } from "../types";
|
|
7
|
+
/**
|
|
8
|
+
* Subscription contract. Selection between the two blocks below is driven by
|
|
9
|
+
* `opts.usesPolling`:
|
|
10
|
+
*
|
|
11
|
+
* - `usesPolling: false` → `subscribeToChanges.eventDriven` — strict commit
|
|
12
|
+
* order (event-driven subscriptions like Postgres LISTEN/NOTIFY or the
|
|
13
|
+
* in-memory broadcast bus emit one event per write, in write order).
|
|
14
|
+
* - `usesPolling: true` → `subscribeToChanges.polling` — set equality plus
|
|
15
|
+
* event count (polling-based subscriptions diff a snapshot and have no
|
|
16
|
+
* way to preserve commit order).
|
|
17
|
+
*
|
|
18
|
+
* When `capabilities.supportsSubscriptions` is true, `usesPolling` is
|
|
19
|
+
* required on the contract opts; the type guarantees this at compile time.
|
|
20
|
+
*/
|
|
21
|
+
export declare function subscribeToChangesBlock(opts: TabularStorageContractOpts): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularStorageContractOpts } from "../types";
|
|
7
|
+
export declare function vectorColumnFormatBlock(opts: TabularStorageContractOpts): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularStorageContractOpts } from "../types";
|
|
7
|
+
export declare function withConnectionTransactionBlock(opts: TabularStorageContractOpts): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { TabularStorageContractOpts } from "../types";
|
|
7
|
+
export declare function withTransactionRollbackBlock(opts: TabularStorageContractOpts): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The row shape every tabular assertion in this contract reads and writes.
|
|
8
|
+
*
|
|
9
|
+
* A compound primary key (`name` + `type`) rather than a single column, because
|
|
10
|
+
* a backend that gets one key right and the pair wrong is the common failure —
|
|
11
|
+
* cursor encoding, `getBulk` chunking and the join's tuple fingerprint all
|
|
12
|
+
* behave differently once the key has two parts.
|
|
13
|
+
*/
|
|
14
|
+
export declare const CompoundPrimaryKeyNames: readonly ["name", "type"];
|
|
15
|
+
export declare const CompoundSchema: {
|
|
16
|
+
readonly type: "object";
|
|
17
|
+
readonly properties: {
|
|
18
|
+
readonly name: {
|
|
19
|
+
readonly type: "string";
|
|
20
|
+
};
|
|
21
|
+
readonly type: {
|
|
22
|
+
readonly type: "string";
|
|
23
|
+
};
|
|
24
|
+
readonly option: {
|
|
25
|
+
readonly type: "string";
|
|
26
|
+
};
|
|
27
|
+
readonly success: {
|
|
28
|
+
readonly type: "boolean";
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
readonly required: readonly ["name", "type", "option", "success"];
|
|
32
|
+
readonly additionalProperties: false;
|
|
33
|
+
};
|