@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.
Files changed (149) hide show
  1. package/README.md +232 -0
  2. package/dist/ai-provider/assertions/capabilityHonesty.d.ts +7 -0
  3. package/dist/ai-provider/assertions/dispose.d.ts +7 -0
  4. package/dist/ai-provider/assertions/inferAdvertisesRegistered.d.ts +15 -0
  5. package/dist/ai-provider/assertions/inferServesInferred.d.ts +20 -0
  6. package/dist/ai-provider/assertions/pricingMatchesModality.d.ts +36 -0
  7. package/dist/ai-provider/assertions/registryCoverage.d.ts +13 -0
  8. package/dist/ai-provider/assertions/sessionReuse.d.ts +7 -0
  9. package/dist/ai-provider/assertions/signalHonoring.d.ts +7 -0
  10. package/dist/ai-provider/assertions/structuredGeneration.d.ts +7 -0
  11. package/dist/ai-provider/assertions/textGenerationSmoke.d.ts +7 -0
  12. package/dist/ai-provider/assertions/toolCallAccumulator.d.ts +7 -0
  13. package/dist/ai-provider/assertions/toolCallMultiTurn.d.ts +7 -0
  14. package/dist/ai-provider/assertions/usageNormalization.d.ts +61 -0
  15. package/dist/ai-provider/fixtures.d.ts +8 -0
  16. package/dist/ai-provider/runAiProviderConformance.d.ts +7 -0
  17. package/dist/ai-provider/types.d.ts +75 -0
  18. package/dist/ai-provider.d.ts +25 -0
  19. package/dist/ai-provider.js +876 -0
  20. package/dist/ai-provider.js.map +26 -0
  21. package/dist/browser-context/ConformanceMockContext.d.ts +54 -0
  22. package/dist/browser-context/assertions/ariaRoundTrip.d.ts +7 -0
  23. package/dist/browser-context/assertions/capabilityHonesty.d.ts +7 -0
  24. package/dist/browser-context/assertions/itExpectFail.d.ts +8 -0
  25. package/dist/browser-context/assertions/networkIntrospection.d.ts +7 -0
  26. package/dist/browser-context/assertions/tabsLifecycle.d.ts +7 -0
  27. package/dist/browser-context/fixtures.d.ts +8 -0
  28. package/dist/browser-context/runIBrowserContextConformance.d.ts +7 -0
  29. package/dist/browser-context/types.d.ts +48 -0
  30. package/dist/browser-context.d.ts +18 -0
  31. package/dist/browser-context.js +567 -0
  32. package/dist/browser-context.js.map +17 -0
  33. package/dist/creditExhaustedSkip.d.ts +39 -0
  34. package/dist/creditExhaustedSkip.js +163 -0
  35. package/dist/creditExhaustedSkip.js.map +10 -0
  36. package/dist/entitlement-profile/assertions/denialShape.d.ts +13 -0
  37. package/dist/entitlement-profile/assertions/dispose.d.ts +16 -0
  38. package/dist/entitlement-profile/assertions/hierarchyHonoring.d.ts +11 -0
  39. package/dist/entitlement-profile/assertions/optionalNeverDenied.d.ts +11 -0
  40. package/dist/entitlement-profile/assertions/requestEntitlementShape.d.ts +12 -0
  41. package/dist/entitlement-profile/assertions/resourceScoping.d.ts +16 -0
  42. package/dist/entitlement-profile/assertions/subscribeGrant.d.ts +11 -0
  43. package/dist/entitlement-profile/assertions/subscribeReload.d.ts +11 -0
  44. package/dist/entitlement-profile/assertions/subscribeRevocation.d.ts +12 -0
  45. package/dist/entitlement-profile/assertions/surfaceCoverage.d.ts +7 -0
  46. package/dist/entitlement-profile/assertions/unsubscribeIdempotent.d.ts +7 -0
  47. package/dist/entitlement-profile/fixtures.d.ts +21 -0
  48. package/dist/entitlement-profile/runEntitlementProfileConformance.d.ts +7 -0
  49. package/dist/entitlement-profile/types.d.ts +45 -0
  50. package/dist/entitlement-profile.d.ts +23 -0
  51. package/dist/entitlement-profile.js +436 -0
  52. package/dist/entitlement-profile.js.map +22 -0
  53. package/dist/human-connector/MockHumanConnector.d.ts +29 -0
  54. package/dist/human-connector/assertions/abort.d.ts +7 -0
  55. package/dist/human-connector/assertions/capabilityHonesty.d.ts +7 -0
  56. package/dist/human-connector/assertions/concurrentIsolation.d.ts +7 -0
  57. package/dist/human-connector/assertions/multiTurnFollowUp.d.ts +7 -0
  58. package/dist/human-connector/assertions/notifyDisplayFastResolve.d.ts +7 -0
  59. package/dist/human-connector/assertions/roundtrip.d.ts +7 -0
  60. package/dist/human-connector/fixtures.d.ts +8 -0
  61. package/dist/human-connector/runHumanConnectorConformance.d.ts +9 -0
  62. package/dist/human-connector/types.d.ts +91 -0
  63. package/dist/human-connector.d.ts +19 -0
  64. package/dist/human-connector.js +843 -0
  65. package/dist/human-connector.js.map +20 -0
  66. package/dist/itExpectFail.d.ts +15 -0
  67. package/dist/itExpectFail.js +180 -0
  68. package/dist/itExpectFail.js.map +11 -0
  69. package/dist/storage-migrations/assertions/appliesAndRecords.d.ts +7 -0
  70. package/dist/storage-migrations/assertions/concurrentRunsSerialize.d.ts +18 -0
  71. package/dist/storage-migrations/assertions/ensureBookkeepingIdempotent.d.ts +7 -0
  72. package/dist/storage-migrations/assertions/failedMigrationLeavesNoPartialSchema.d.ts +14 -0
  73. package/dist/storage-migrations/assertions/failedMigrationNotRecorded.d.ts +7 -0
  74. package/dist/storage-migrations/assertions/idempotentRun.d.ts +7 -0
  75. package/dist/storage-migrations/assertions/incrementalApplication.d.ts +7 -0
  76. package/dist/storage-migrations/runMigrationRunnerContract.d.ts +9 -0
  77. package/dist/storage-migrations/types.d.ts +78 -0
  78. package/dist/storage-migrations.d.ts +18 -0
  79. package/dist/storage-migrations.js +473 -0
  80. package/dist/storage-migrations.js.map +20 -0
  81. package/dist/tabular-migrations/assertions/addAndDropIndex.d.ts +7 -0
  82. package/dist/tabular-migrations/assertions/addColumn.d.ts +7 -0
  83. package/dist/tabular-migrations/assertions/backfill.d.ts +7 -0
  84. package/dist/tabular-migrations/assertions/dropColumn.d.ts +7 -0
  85. package/dist/tabular-migrations/assertions/failedMigrationNotRecorded.d.ts +7 -0
  86. package/dist/tabular-migrations/assertions/freshDbFastPath.d.ts +7 -0
  87. package/dist/tabular-migrations/assertions/incrementalApplication.d.ts +7 -0
  88. package/dist/tabular-migrations/assertions/renameColumn.d.ts +7 -0
  89. package/dist/tabular-migrations/runTabularMigrationContract.d.ts +8 -0
  90. package/dist/tabular-migrations/types.d.ts +28 -0
  91. package/dist/tabular-migrations.d.ts +19 -0
  92. package/dist/tabular-migrations.js +257 -0
  93. package/dist/tabular-migrations.js.map +18 -0
  94. package/dist/tabular-storage/assertions/countMatchesQuery.d.ts +7 -0
  95. package/dist/tabular-storage/assertions/guardParity.d.ts +25 -0
  96. package/dist/tabular-storage/assertions/inListCriterion.d.ts +14 -0
  97. package/dist/tabular-storage/assertions/joinBoundedLeftRead.d.ts +29 -0
  98. package/dist/tabular-storage/assertions/joinPushdown.d.ts +13 -0
  99. package/dist/tabular-storage/assertions/notInListCriterion.d.ts +15 -0
  100. package/dist/tabular-storage/assertions/strategyParity.d.ts +31 -0
  101. package/dist/tabular-storage/assertions/subscribeToChanges.d.ts +21 -0
  102. package/dist/tabular-storage/assertions/vectorColumnFormat.d.ts +7 -0
  103. package/dist/tabular-storage/assertions/withConnectionTransaction.d.ts +7 -0
  104. package/dist/tabular-storage/assertions/withTransactionRollback.d.ts +7 -0
  105. package/dist/tabular-storage/fixtures.d.ts +33 -0
  106. package/dist/tabular-storage/joinFixtures.d.ts +66 -0
  107. package/dist/tabular-storage/runTabularJoinContract.d.ts +24 -0
  108. package/dist/tabular-storage/runTabularStorageContract.d.ts +9 -0
  109. package/dist/tabular-storage/types.d.ts +102 -0
  110. package/dist/tabular-storage.d.ts +25 -0
  111. package/dist/tabular-storage.js +1477 -0
  112. package/dist/tabular-storage.js.map +27 -0
  113. package/dist/vector-storage/assertions/dimensionValidation.d.ts +17 -0
  114. package/dist/vector-storage/assertions/legacyEncoding.d.ts +16 -0
  115. package/dist/vector-storage/assertions/metadataFilter.d.ts +16 -0
  116. package/dist/vector-storage/assertions/searchOptions.d.ts +16 -0
  117. package/dist/vector-storage/assertions/shared.d.ts +23 -0
  118. package/dist/vector-storage/assertions/similarityRanking.d.ts +18 -0
  119. package/dist/vector-storage/assertions/similaritySearchEvent.d.ts +16 -0
  120. package/dist/vector-storage/assertions/vectorRoundTrip.d.ts +17 -0
  121. package/dist/vector-storage/runVectorStorageContract.d.ts +20 -0
  122. package/dist/vector-storage/types.d.ts +87 -0
  123. package/dist/vector-storage.d.ts +19 -0
  124. package/dist/vector-storage.js +412 -0
  125. package/dist/vector-storage.js.map +21 -0
  126. package/dist/web-search/assertions/capabilityAgreement.d.ts +23 -0
  127. package/dist/web-search/assertions/credentialNaming.d.ts +21 -0
  128. package/dist/web-search/assertions/dateFiltering.d.ts +22 -0
  129. package/dist/web-search/assertions/domainNormalization.d.ts +24 -0
  130. package/dist/web-search/assertions/maxResultsCeiling.d.ts +22 -0
  131. package/dist/web-search/assertions/reportedQuery.d.ts +18 -0
  132. package/dist/web-search/assertions/signalThreading.d.ts +20 -0
  133. package/dist/web-search/fixtures.d.ts +40 -0
  134. package/dist/web-search/runWebSearchProviderConformance.d.ts +24 -0
  135. package/dist/web-search/types.d.ts +79 -0
  136. package/dist/web-search.d.ts +19 -0
  137. package/dist/web-search.js +560 -0
  138. package/dist/web-search.js.map +18 -0
  139. package/dist/worker-proxy/assertions/backlogOrdering.d.ts +7 -0
  140. package/dist/worker-proxy/assertions/disposeTerminatesWorker.d.ts +8 -0
  141. package/dist/worker-proxy/assertions/errorPropagation.d.ts +7 -0
  142. package/dist/worker-proxy/assertions/providerCallHelpers.d.ts +23 -0
  143. package/dist/worker-proxy/browserOnlyStub.d.ts +7 -0
  144. package/dist/worker-proxy/runWorkerProxyBoundary.d.ts +7 -0
  145. package/dist/worker-proxy/types.d.ts +47 -0
  146. package/dist/worker-proxy.d.ts +16 -0
  147. package/dist/worker-proxy.js +389 -0
  148. package/dist/worker-proxy.js.map +17 -0
  149. package/package.json +120 -0
@@ -0,0 +1,1477 @@
1
+ // src/tabular-storage/assertions/countMatchesQuery.ts
2
+ import { afterEach, beforeEach, describe, expect, it as it2 } from "vitest";
3
+
4
+ // src/creditExhaustedSkip.ts
5
+ import { it as vitestIt } from "vitest";
6
+ var BILLING_CODES = new Set([
7
+ "insufficient_quota",
8
+ "billing_hard_limit_reached",
9
+ "insufficient_credits",
10
+ "insufficient_balance"
11
+ ]);
12
+ var CREDIT_MESSAGE = /insufficient credits|not enough credits|no credits remaining|out of credits|insufficient[\s_-]balance|balance is (?:too low|insufficient)|exceeded[\s\S]{0,80}credits|monthly included credits|payment required|exceeded your current quota|check your plan and billing|billing_hard_limit/i;
13
+ var HTTP_402_MESSAGE = /\bHTTP\/?\d?(?:\.\d)?\s*402\b|\bstatus(?:\s*code)?\s*[:=]\s*402\b|\berror code:?\s*402\b|(?:^|:\s+)402\s+[a-z]/im;
14
+ function isCreditExhaustedError(err) {
15
+ const seen = new Set;
16
+ let current = err;
17
+ while (current !== undefined && current !== null && !seen.has(current)) {
18
+ seen.add(current);
19
+ if (matchesCreditExhaustion(current))
20
+ return true;
21
+ current = causeOf(current);
22
+ }
23
+ return false;
24
+ }
25
+ function matchesCreditExhaustion(err) {
26
+ if (typeof err === "string")
27
+ return messageLooksLikeCreditExhaustion(err);
28
+ if (!err || typeof err !== "object")
29
+ return false;
30
+ const rec = err;
31
+ const status = numericStatus(rec);
32
+ if (status === 402)
33
+ return true;
34
+ const code = stringish(rec.code) ?? stringish(nested(rec.error, "code"));
35
+ const type = stringish(rec.type) ?? stringish(nested(rec.error, "type"));
36
+ if (code && BILLING_CODES.has(code))
37
+ return true;
38
+ if (type && BILLING_CODES.has(type))
39
+ return true;
40
+ const message = [
41
+ stringish(rec.message),
42
+ stringish(nested(rec.error, "message")),
43
+ stringifyUnknown(err)
44
+ ].filter((part) => typeof part === "string").join(`
45
+ `);
46
+ return messageLooksLikeCreditExhaustion(message);
47
+ }
48
+ function messageLooksLikeCreditExhaustion(message) {
49
+ return CREDIT_MESSAGE.test(message) || HTTP_402_MESSAGE.test(message);
50
+ }
51
+ function numericStatus(rec) {
52
+ for (const key of ["status", "statusCode"]) {
53
+ const value = rec[key];
54
+ if (typeof value === "number" && Number.isFinite(value))
55
+ return value;
56
+ }
57
+ const nestedStatus = nested(rec.error, "status");
58
+ if (typeof nestedStatus === "number" && Number.isFinite(nestedStatus))
59
+ return nestedStatus;
60
+ return;
61
+ }
62
+ function nested(value, key) {
63
+ if (!value || typeof value !== "object")
64
+ return;
65
+ return value[key];
66
+ }
67
+ function stringish(value) {
68
+ return typeof value === "string" && value.length > 0 ? value : undefined;
69
+ }
70
+ function stringifyUnknown(err) {
71
+ if (err instanceof Error)
72
+ return err.message;
73
+ return String(err);
74
+ }
75
+ function causeOf(err) {
76
+ if (!err || typeof err !== "object")
77
+ return;
78
+ return err.cause;
79
+ }
80
+ function creditSkipNote(err) {
81
+ const message = err instanceof Error ? err.message : String(err);
82
+ return `skipped: provider out of credits (${message})`;
83
+ }
84
+ var SKIP_OVERRIDE_ENV = "WORKGLOW_CREDIT_EXHAUSTED_SKIP";
85
+ function isTruthyFlag(value) {
86
+ return !/^(?:0|false|off|no)$/i.test(value.trim());
87
+ }
88
+ function creditExhaustionSkipsTest(env = process.env) {
89
+ const override = env[SKIP_OVERRIDE_ENV];
90
+ if (override !== undefined && override.trim() !== "")
91
+ return isTruthyFlag(override);
92
+ for (const key of ["CI", "GITHUB_ACTIONS"]) {
93
+ const value = env[key];
94
+ if (value !== undefined && value.trim() !== "" && isTruthyFlag(value))
95
+ return true;
96
+ }
97
+ return false;
98
+ }
99
+ function reportLocalCreditFailure(err) {
100
+ const message = err instanceof Error ? err.message : String(err);
101
+ console.error(`provider out of credits (${message})
102
+ ` + ` Failing because this is not CI. Top the account up, or set ${SKIP_OVERRIDE_ENV}=1 to skip instead.`);
103
+ }
104
+ async function runWithCreditSkip(ctx, fn) {
105
+ try {
106
+ return await fn();
107
+ } catch (err) {
108
+ if (isCreditExhaustedError(err)) {
109
+ if (!creditExhaustionSkipsTest()) {
110
+ reportLocalCreditFailure(err);
111
+ throw err;
112
+ }
113
+ ctx.skip(creditSkipNote(err));
114
+ return;
115
+ }
116
+ throw err;
117
+ }
118
+ }
119
+ function wrapTestBodyForCreditSkip(fn) {
120
+ return async function wrapped(...args) {
121
+ try {
122
+ return await fn.apply(this, args);
123
+ } catch (err) {
124
+ if (!isCreditExhaustedError(err))
125
+ throw err;
126
+ if (!creditExhaustionSkipsTest()) {
127
+ reportLocalCreditFailure(err);
128
+ throw err;
129
+ }
130
+ const ctx = args[0];
131
+ if (ctx && typeof ctx.skip === "function") {
132
+ ctx.skip(creditSkipNote(err));
133
+ return;
134
+ }
135
+ console.warn(creditSkipNote(err));
136
+ return;
137
+ }
138
+ };
139
+ }
140
+ function wrapCallable(fn) {
141
+ return new Proxy(fn, {
142
+ apply(target, thisArg, argArray) {
143
+ const wrapped = argArray.map((arg) => typeof arg === "function" ? wrapTestBodyForCreditSkip(arg) : arg);
144
+ const result = Reflect.apply(target, thisArg, wrapped);
145
+ return typeof result === "function" ? wrapCallable(result) : result;
146
+ },
147
+ get(target, prop, receiver) {
148
+ const value = Reflect.get(target, prop, receiver);
149
+ if (typeof value === "function") {
150
+ return wrapCallable(value.bind(target));
151
+ }
152
+ return value;
153
+ }
154
+ });
155
+ }
156
+ var it = wrapCallable(vitestIt);
157
+
158
+ // src/itExpectFail.ts
159
+ var itExpectFail = (name, fn, timeout) => {
160
+ const native = it.fails;
161
+ if (typeof native === "function") {
162
+ native(name, fn, timeout);
163
+ return;
164
+ }
165
+ it(`${name} [expected-fail]`, async () => {
166
+ let passed = false;
167
+ try {
168
+ await fn();
169
+ passed = true;
170
+ } catch (err) {
171
+ if (isCreditExhaustedError(err))
172
+ throw err;
173
+ }
174
+ if (passed) {
175
+ throw new Error(`Test "${name}" was marked as expected-fail but passed. Remove its name from opts.expectedFailures.`);
176
+ }
177
+ }, timeout);
178
+ };
179
+
180
+ // src/tabular-storage/assertions/countMatchesQuery.ts
181
+ function countMatchesQueryBlock(opts) {
182
+ const expectFails = new Set(opts.expectedFailures ?? []);
183
+ const itImpl = expectFails.has("countMatchesQuery") ? itExpectFail : it2;
184
+ describe.skipIf(!opts.capabilities.supportsQuery)("countMatchesQuery", () => {
185
+ let storage;
186
+ beforeEach(async () => {
187
+ storage = await opts.createStorage();
188
+ await storage.setupDatabase?.();
189
+ });
190
+ afterEach(async () => {
191
+ await storage.deleteAll();
192
+ storage.destroy?.();
193
+ await opts.releaseStorage?.(storage);
194
+ });
195
+ itImpl("count(predicate) equals query(predicate).length for every predicate", async () => {
196
+ const rows = [
197
+ { name: "a", type: "t", option: "x", success: true },
198
+ { name: "b", type: "t", option: "x", success: true },
199
+ { name: "c", type: "t", option: "x", success: true },
200
+ { name: "d", type: "t", option: "y", success: true },
201
+ { name: "e", type: "t", option: "y", success: true },
202
+ { name: "f", type: "t", option: "z", success: true }
203
+ ];
204
+ for (const row of rows) {
205
+ await storage.put(row);
206
+ }
207
+ const expected = { x: 3, y: 2, z: 1, missing: 0 };
208
+ for (const [option, expectedCount] of Object.entries(expected)) {
209
+ const counted = await storage.count({ option });
210
+ const queried = await storage.query({ option }) ?? [];
211
+ expect(counted, `count({option:"${option}"}) disagrees with query({option:"${option}"}).length`).toBe(queried.length);
212
+ expect(counted).toBe(expectedCount);
213
+ }
214
+ const totalCount = await storage.count();
215
+ const allRows = await storage.getAll() ?? [];
216
+ expect(totalCount).toBe(allRows.length);
217
+ expect(totalCount).toBe(rows.length);
218
+ }, opts.timeout);
219
+ });
220
+ }
221
+ // src/tabular-storage/assertions/guardParity.ts
222
+ import { afterEach as afterEach2, beforeEach as beforeEach2, describe as describe2, expect as expect2, it as it3 } from "vitest";
223
+ async function outcomeOf(call) {
224
+ try {
225
+ await call();
226
+ return { kind: "returned" };
227
+ } catch (error) {
228
+ return {
229
+ kind: "threw",
230
+ name: error instanceof Error ? error.constructor.name : typeof error,
231
+ message: error instanceof Error ? error.message : String(error)
232
+ };
233
+ }
234
+ }
235
+ var CASES = [
236
+ {
237
+ method: "getAll",
238
+ label: "a limit of zero",
239
+ run: (t) => t.getAll({ limit: 0 })
240
+ },
241
+ {
242
+ method: "getAll",
243
+ label: "a negative offset",
244
+ run: (t) => t.getAll({ offset: -1 })
245
+ },
246
+ {
247
+ method: "getPage",
248
+ label: "a limit of zero",
249
+ run: (t) => t.getPage({ limit: 0 })
250
+ },
251
+ {
252
+ method: "count",
253
+ label: "empty criteria",
254
+ run: (t) => t.count({})
255
+ },
256
+ {
257
+ method: "query",
258
+ label: "an operator outside the allow-list",
259
+ run: (t) => t.query({ name: { operator: "regex", value: "a" } })
260
+ },
261
+ {
262
+ method: "query",
263
+ label: "a column that is not in the schema",
264
+ run: (t) => t.query({ nope: "a" })
265
+ },
266
+ {
267
+ method: "query",
268
+ label: "a list operator whose value is not an array",
269
+ run: (t) => t.query({ name: { operator: "in", value: "a" } })
270
+ },
271
+ {
272
+ method: "queryPage",
273
+ label: "empty criteria",
274
+ run: (t) => t.queryPage({}, { limit: 5 })
275
+ },
276
+ {
277
+ method: "queryIndex",
278
+ label: "an empty select",
279
+ run: (t) => t.queryIndex({ name: "a" }, { select: [] })
280
+ },
281
+ {
282
+ method: "queryIndex",
283
+ label: "a select column that is not in the schema",
284
+ run: (t) => t.queryIndex({ name: "a" }, { select: ["nope"] })
285
+ },
286
+ {
287
+ method: "deleteSearch",
288
+ label: "empty criteria",
289
+ run: (t) => t.deleteSearch({})
290
+ },
291
+ {
292
+ method: "deleteSearch",
293
+ label: "criteria that exclude nothing",
294
+ run: (t) => t.deleteSearch({ option: { operator: "not-in", value: [] } })
295
+ },
296
+ {
297
+ method: "updateWhere",
298
+ label: "a patch that rewrites a primary-key column",
299
+ run: (t) => t.updateWhere({ name: "guard", type: "parity" }, { name: "moved" })
300
+ },
301
+ {
302
+ method: "join",
303
+ label: "a join column that is not in the schema",
304
+ run: (t) => t.join({ type: "inner", on: [{ left: "nope", right: "name" }] }, t)
305
+ }
306
+ ];
307
+ var UNGUARDED = {
308
+ put: "an entity is coerced against the schema, not rejected by a guard",
309
+ putBulk: "as `put`",
310
+ get: "a primary key, and a malformed one reads as a miss on every backend",
311
+ getBulk: "as `get`",
312
+ delete: "as `get`",
313
+ deleteAll: "takes no arguments",
314
+ size: "takes no arguments",
315
+ getOffsetPage: "takes two numbers and the SQL backends validate neither"
316
+ };
317
+ function publicNameOf(internal) {
318
+ const match = /^_(.+)Internal$/.exec(internal);
319
+ return match?.[1];
320
+ }
321
+ function routedMethods(storage) {
322
+ const found = new Set;
323
+ for (let proto = Object.getPrototypeOf(storage);proto !== null && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
324
+ for (const key of Object.getOwnPropertyNames(proto)) {
325
+ const name = publicNameOf(key);
326
+ if (name !== undefined && typeof storage[key] === "function")
327
+ found.add(name);
328
+ }
329
+ }
330
+ return [...found].sort();
331
+ }
332
+ function guardParityBlock(opts) {
333
+ const expectFails = new Set(opts.expectedFailures ?? []);
334
+ const itImpl = expectFails.has("guardParity") ? itExpectFail : it3;
335
+ describe2.skipIf(!opts.capabilities.supportsTransactions || !opts.capabilities.supportsQuery)("guardParity", () => {
336
+ let storage;
337
+ beforeEach2(async () => {
338
+ storage = await opts.createStorage();
339
+ await storage.setupDatabase?.();
340
+ await storage.put({ name: "guard", type: "parity", option: "base", success: true });
341
+ });
342
+ afterEach2(async () => {
343
+ await storage.deleteAll();
344
+ storage.destroy?.();
345
+ await opts.releaseStorage?.(storage);
346
+ });
347
+ for (const testCase of CASES) {
348
+ itImpl(`${testCase.method} rejects ${testCase.label} the same way inside a transaction`, async () => {
349
+ const direct = await outcomeOf(() => testCase.run(storage));
350
+ let inside;
351
+ await storage.withTransaction(async (tx) => {
352
+ inside = await outcomeOf(() => testCase.run(tx));
353
+ });
354
+ expect2(inside).toEqual(direct);
355
+ }, opts.timeout);
356
+ }
357
+ itImpl("deleteSearch with empty criteria deletes nothing on either path", async () => {
358
+ await storage.deleteSearch({});
359
+ await storage.withTransaction(async (tx) => {
360
+ await tx.deleteSearch({});
361
+ });
362
+ expect2(await storage.get({ name: "guard", type: "parity" })).toBeDefined();
363
+ }, opts.timeout);
364
+ itImpl("runs the public method on the transaction path, not only its implementation", async () => {
365
+ const emitted = [];
366
+ const listener = (keys) => {
367
+ emitted.push(keys);
368
+ };
369
+ storage.on("getBulk", listener);
370
+ try {
371
+ await storage.withTransaction(async (tx) => {
372
+ await tx.getBulk([{ name: "guard", type: "parity" }]);
373
+ });
374
+ } finally {
375
+ storage.off("getBulk", listener);
376
+ }
377
+ expect2(emitted).toEqual([[{ name: "guard", type: "parity" }]]);
378
+ }, opts.timeout);
379
+ itImpl("states, for every method with a private implementation, what its guard is", async () => {
380
+ const covered = new Set(CASES.map((testCase) => testCase.method));
381
+ const unstated = routedMethods(storage).filter((method) => !covered.has(method) && UNGUARDED[method] === undefined);
382
+ expect2(unstated).toEqual([]);
383
+ }, opts.timeout);
384
+ });
385
+ }
386
+ // src/tabular-storage/assertions/inListCriterion.ts
387
+ import { afterEach as afterEach3, beforeEach as beforeEach3, describe as describe3, expect as expect3, it as it4 } from "vitest";
388
+ function inListCriterionBlock(opts) {
389
+ const expectFails = new Set(opts.expectedFailures ?? []);
390
+ const itImpl = expectFails.has("inListCriterion") ? itExpectFail : it4;
391
+ describe3.skipIf(!opts.capabilities.supportsQuery)("inListCriterion", () => {
392
+ let storage;
393
+ const rows = [
394
+ { name: "a", type: "t", option: "x", success: true },
395
+ { name: "b", type: "t", option: "y", success: true },
396
+ { name: "c", type: "t", option: "z", success: false },
397
+ { name: "d", type: "u", option: "x", success: false }
398
+ ];
399
+ beforeEach3(async () => {
400
+ storage = await opts.createStorage();
401
+ await storage.setupDatabase?.();
402
+ for (const row of rows)
403
+ await storage.put(row);
404
+ });
405
+ afterEach3(async () => {
406
+ await storage.deleteAll();
407
+ storage.destroy?.();
408
+ await opts.releaseStorage?.(storage);
409
+ });
410
+ const names = (found) => found.map((r) => r.name).sort();
411
+ itImpl("matches every listed value and nothing else", async () => {
412
+ const found = await storage.query({ option: { value: ["x", "z"], operator: "in" } }) ?? [];
413
+ expect3(names(found)).toEqual(["a", "c", "d"]);
414
+ }, opts.timeout);
415
+ itImpl("ANDs with the other criteria columns", async () => {
416
+ const found = await storage.query({ option: { value: ["x", "z"], operator: "in" }, type: "t" }) ?? [];
417
+ expect3(names(found)).toEqual(["a", "c"]);
418
+ }, opts.timeout);
419
+ itImpl("ignores values that match no row, and never duplicates a row for a repeated value", async () => {
420
+ const found = await storage.query({ option: { value: ["x", "nope", "x"], operator: "in" } }) ?? [];
421
+ expect3(names(found)).toEqual(["a", "d"]);
422
+ }, opts.timeout);
423
+ itImpl("matches nothing for an empty list", async () => {
424
+ const found = await storage.query({ option: { value: [], operator: "in" } }) ?? [];
425
+ expect3(found).toEqual([]);
426
+ expect3(await storage.count({ option: { value: [], operator: "in" } })).toBe(0);
427
+ }, opts.timeout);
428
+ itImpl("count agrees with query for an in-list", async () => {
429
+ const criteria = { option: { value: ["x", "z"], operator: "in" } };
430
+ const queried = await storage.query(criteria) ?? [];
431
+ expect3(await storage.count(criteria)).toBe(queried.length);
432
+ }, opts.timeout);
433
+ itImpl("deleteSearch removes exactly the listed rows", async () => {
434
+ await storage.deleteSearch({ option: { value: ["x", "z"], operator: "in" } });
435
+ const left = await storage.getAll() ?? [];
436
+ expect3(names(left)).toEqual(["b"]);
437
+ }, opts.timeout);
438
+ itImpl("deleteSearch with an empty list deletes nothing", async () => {
439
+ await storage.deleteSearch({ option: { value: [], operator: "in" } });
440
+ const left = await storage.getAll() ?? [];
441
+ expect3(left.length).toBe(rows.length);
442
+ }, opts.timeout);
443
+ itImpl("matches on a boolean column", async () => {
444
+ const found = await storage.query({ success: { value: [false], operator: "in" } }) ?? [];
445
+ expect3(names(found)).toEqual(["c", "d"]);
446
+ }, opts.timeout);
447
+ });
448
+ }
449
+ // src/tabular-storage/assertions/joinBoundedLeftRead.ts
450
+ import { afterEach as afterEach4, beforeEach as beforeEach4, describe as describe4, expect as expect4, it as it5, vi } from "vitest";
451
+ function limitsOf(spy) {
452
+ return spy.mock.calls.map((call) => call[call.length - 1]?.limit);
453
+ }
454
+ function joinBoundedLeftReadBlock(createPosts, createAuthors, seed, opts) {
455
+ const on = [{ left: "author_id", right: "id" }];
456
+ describe4.skipIf(opts.expectSqlPushdown !== false)("join bounded left read", () => {
457
+ let posts;
458
+ let authors;
459
+ beforeEach4(async () => {
460
+ posts = await createPosts();
461
+ authors = await createAuthors();
462
+ await posts.setupDatabase?.();
463
+ await authors.setupDatabase?.();
464
+ await seed(posts, authors);
465
+ });
466
+ afterEach4(async () => {
467
+ await posts.deleteAll();
468
+ await authors.deleteAll();
469
+ posts.destroy?.();
470
+ authors.destroy?.();
471
+ vi.restoreAllMocks();
472
+ });
473
+ it5("bounds the left read when no order is asked for", async () => {
474
+ const leftRead = vi.spyOn(posts, "getAll");
475
+ const rows = await posts.join({ type: "left", on, limit: 2 }, authors);
476
+ expect4(rows).toHaveLength(2);
477
+ expect4(limitsOf(leftRead)).toEqual([2]);
478
+ }, opts.timeout);
479
+ it5("bounds the left read when the left query can serve the order", async () => {
480
+ const leftRead = vi.spyOn(posts, "getAll");
481
+ const rows = await posts.join({
482
+ type: "left",
483
+ on,
484
+ orderBy: [{ side: "left", column: "views", direction: "DESC" }],
485
+ limit: 2
486
+ }, authors);
487
+ expect4(rows.map((r) => r.left.views)).toEqual([10, 9]);
488
+ expect4(limitsOf(leftRead)).toEqual([2]);
489
+ }, opts.timeout);
490
+ it5("counts offset toward the bound rather than ignoring it", async () => {
491
+ const leftRead = vi.spyOn(posts, "getAll");
492
+ const rows = await posts.join({
493
+ type: "left",
494
+ on,
495
+ orderBy: [{ side: "left", column: "views", direction: "DESC" }],
496
+ offset: 2,
497
+ limit: 2
498
+ }, authors);
499
+ expect4(rows.map((r) => r.left.views)).toEqual([7, 5]);
500
+ expect4(limitsOf(leftRead)).toEqual([4]);
501
+ }, opts.timeout);
502
+ it5("widens the left read when an inner join drops too many rows", async () => {
503
+ const leftRead = vi.spyOn(posts, "getAll");
504
+ const rows = await posts.join({ type: "inner", on, limit: 4 }, authors);
505
+ expect4(rows).toHaveLength(4);
506
+ const limits = limitsOf(leftRead);
507
+ expect4(limits.length).toBeGreaterThan(1);
508
+ expect4(limits[0]).toBe(4);
509
+ expect4(limits[1]).toBeGreaterThan(4);
510
+ }, opts.timeout);
511
+ it5("reads the whole left side when the joined order needs a sort", async () => {
512
+ const leftRead = vi.spyOn(posts, "getAll");
513
+ const rows = await posts.join({
514
+ type: "left",
515
+ on,
516
+ orderBy: [
517
+ { side: "right", column: "name", direction: "ASC" },
518
+ { side: "left", column: "id", direction: "ASC" }
519
+ ],
520
+ limit: 2
521
+ }, authors);
522
+ expect4(rows.map((r) => r.left.id)).toEqual(["p4", "p5"]);
523
+ expect4(limitsOf(leftRead)).toEqual([undefined]);
524
+ }, opts.timeout);
525
+ it5("reads the whole left side when the order names a nullable left column", async () => {
526
+ const leftRead = vi.spyOn(posts, "getAll");
527
+ const rows = await posts.join({
528
+ type: "left",
529
+ on,
530
+ orderBy: [
531
+ { side: "left", column: "author_id", direction: "ASC" },
532
+ { side: "left", column: "id", direction: "ASC" }
533
+ ],
534
+ limit: 2
535
+ }, authors);
536
+ expect4(rows.map((r) => r.left.id)).toEqual(["p5", "p1"]);
537
+ expect4(limitsOf(leftRead)).toEqual([undefined]);
538
+ }, opts.timeout);
539
+ it5("bounds the filtered left read the same way", async () => {
540
+ const leftQuery = vi.spyOn(posts, "query");
541
+ const rows = await posts.join({ type: "left", on, where: { left: { tenant: "t1" } }, limit: 2 }, authors);
542
+ expect4(rows).toHaveLength(2);
543
+ expect4(limitsOf(leftQuery)).toEqual([2]);
544
+ }, opts.timeout);
545
+ });
546
+ }
547
+ // src/tabular-storage/assertions/joinPushdown.ts
548
+ import { InMemoryTabularStorage } from "@workglow/storage";
549
+ import { afterEach as afterEach5, beforeEach as beforeEach5, describe as describe5, expect as expect5, it as it6, vi as vi2 } from "vitest";
550
+
551
+ // src/tabular-storage/fixtures.ts
552
+ var CompoundPrimaryKeyNames = ["name", "type"];
553
+ var CompoundSchema = {
554
+ type: "object",
555
+ properties: {
556
+ name: { type: "string" },
557
+ type: { type: "string" },
558
+ option: { type: "string" },
559
+ success: { type: "boolean" }
560
+ },
561
+ required: ["name", "type", "option", "success"],
562
+ additionalProperties: false
563
+ };
564
+
565
+ // src/tabular-storage/assertions/joinPushdown.ts
566
+ function joinPushdownBlock(opts) {
567
+ const expectFails = new Set(opts.expectedFailures ?? []);
568
+ const itImpl = expectFails.has("joinPushdown") ? itExpectFail : it6;
569
+ describe5.skipIf(!opts.capabilities.supportsQuery || !opts.createSiblingStorage)("joinPushdown", () => {
570
+ let primary;
571
+ let sibling;
572
+ beforeEach5(async () => {
573
+ primary = await opts.createStorage();
574
+ await primary.setupDatabase?.();
575
+ sibling = await opts.createSiblingStorage(primary);
576
+ await sibling.setupDatabase?.();
577
+ await primary.putBulk([
578
+ { name: "a", type: "t", option: "x", success: true },
579
+ { name: "b", type: "t", option: "y", success: false }
580
+ ]);
581
+ await sibling.putBulk([
582
+ { name: "a", type: "u", option: "p", success: true },
583
+ { name: "c", type: "u", option: "q", success: true }
584
+ ]);
585
+ });
586
+ afterEach5(async () => {
587
+ await primary.deleteAll();
588
+ await sibling.deleteAll();
589
+ primary.destroy?.();
590
+ sibling.destroy?.();
591
+ await opts.releaseStorage?.(primary);
592
+ await opts.releaseStorage?.(sibling);
593
+ vi2.restoreAllMocks();
594
+ });
595
+ itImpl("runs a same-connection join without querying the right side", async () => {
596
+ const rightQuery = vi2.spyOn(sibling, "query");
597
+ const rows = await primary.join({
598
+ type: "left",
599
+ on: [{ left: "name", right: "name" }],
600
+ orderBy: [{ side: "left", column: "name", direction: "ASC" }]
601
+ }, sibling);
602
+ expect5(rightQuery).not.toHaveBeenCalled();
603
+ expect5(rows.map((r) => `${r.left.name}:${r.right?.type ?? "-"}`)).toEqual(["a:u", "b:-"]);
604
+ }, opts.timeout);
605
+ itImpl("falls back to the hash join for a right side on another connection", async () => {
606
+ const elsewhere = new InMemoryTabularStorage(CompoundSchema, CompoundPrimaryKeyNames);
607
+ await elsewhere.putBulk([{ name: "b", type: "v", option: "z", success: true }]);
608
+ const rightQuery = vi2.spyOn(elsewhere, "query");
609
+ const rows = await primary.join({
610
+ type: "left",
611
+ on: [{ left: "name", right: "name" }],
612
+ orderBy: [{ side: "left", column: "name", direction: "ASC" }]
613
+ }, elsewhere);
614
+ expect5(rightQuery).toHaveBeenCalled();
615
+ expect5(rows.map((r) => `${r.left.name}:${r.right?.type ?? "-"}`)).toEqual(["a:-", "b:v"]);
616
+ }, opts.timeout);
617
+ });
618
+ }
619
+ // src/tabular-storage/assertions/notInListCriterion.ts
620
+ import { afterEach as afterEach6, beforeEach as beforeEach6, describe as describe6, expect as expect6, it as it7 } from "vitest";
621
+ function notInListCriterionBlock(opts) {
622
+ const expectFails = new Set(opts.expectedFailures ?? []);
623
+ const itImpl = expectFails.has("notInListCriterion") ? itExpectFail : it7;
624
+ describe6.skipIf(!opts.capabilities.supportsQuery)("notInListCriterion", () => {
625
+ let storage;
626
+ const rows = [
627
+ { name: "a", type: "t", option: "x", success: true },
628
+ { name: "b", type: "t", option: "y", success: true },
629
+ { name: "c", type: "t", option: "z", success: false },
630
+ { name: "d", type: "u", option: "x", success: false }
631
+ ];
632
+ beforeEach6(async () => {
633
+ storage = await opts.createStorage();
634
+ await storage.setupDatabase?.();
635
+ for (const row of rows)
636
+ await storage.put(row);
637
+ });
638
+ afterEach6(async () => {
639
+ await storage.deleteAll();
640
+ storage.destroy?.();
641
+ await opts.releaseStorage?.(storage);
642
+ });
643
+ const names = (found) => found.map((r) => r.name).sort();
644
+ itImpl("excludes every listed value and keeps the rest", async () => {
645
+ const found = await storage.query({ option: { value: ["x", "z"], operator: "not-in" } }) ?? [];
646
+ expect6(names(found)).toEqual(["b"]);
647
+ }, opts.timeout);
648
+ itImpl("is the exact complement of the same in-list", async () => {
649
+ const included = await storage.query({ option: { value: ["x"], operator: "in" } }) ?? [];
650
+ const excluded = await storage.query({ option: { value: ["x"], operator: "not-in" } }) ?? [];
651
+ expect6([...names(included), ...names(excluded)].sort()).toEqual(["a", "b", "c", "d"]);
652
+ }, opts.timeout);
653
+ itImpl("ANDs with the other criteria columns", async () => {
654
+ const found = await storage.query({ option: { value: ["x"], operator: "not-in" }, type: "t" }) ?? [];
655
+ expect6(names(found)).toEqual(["b", "c"]);
656
+ }, opts.timeout);
657
+ itImpl("ignores values that match no row, and tolerates a repeated value", async () => {
658
+ const found = await storage.query({ option: { value: ["x", "nope", "x"], operator: "not-in" } }) ?? [];
659
+ expect6(names(found)).toEqual(["b", "c"]);
660
+ }, opts.timeout);
661
+ itImpl("matches everything for an empty list", async () => {
662
+ const found = await storage.query({ option: { value: [], operator: "not-in" } }) ?? [];
663
+ expect6(names(found)).toEqual(["a", "b", "c", "d"]);
664
+ expect6(await storage.count({ option: { value: [], operator: "not-in" } })).toBe(rows.length);
665
+ }, opts.timeout);
666
+ itImpl("count agrees with query for a not-in list", async () => {
667
+ const criteria = { option: { value: ["x", "z"], operator: "not-in" } };
668
+ const queried = await storage.query(criteria) ?? [];
669
+ expect6(await storage.count(criteria)).toBe(queried.length);
670
+ }, opts.timeout);
671
+ itImpl("deleteSearch removes exactly the unlisted rows", async () => {
672
+ await storage.deleteSearch({ option: { value: ["x", "z"], operator: "not-in" } });
673
+ const left = await storage.getAll() ?? [];
674
+ expect6(names(left)).toEqual(["a", "c", "d"]);
675
+ }, opts.timeout);
676
+ itImpl("deleteSearch refuses criteria that reduce to an empty exclusion", async () => {
677
+ await expect6(storage.deleteSearch({ option: { value: [], operator: "not-in" } })).rejects.toThrow(/delete the whole table/);
678
+ const left = await storage.getAll() ?? [];
679
+ expect6(names(left)).toEqual(["a", "b", "c", "d"]);
680
+ }, opts.timeout);
681
+ itImpl("deleteSearch still runs when another column narrows the empty exclusion", async () => {
682
+ await storage.deleteSearch({
683
+ type: "u",
684
+ option: { value: [], operator: "not-in" }
685
+ });
686
+ const left = await storage.getAll() ?? [];
687
+ expect6(names(left)).toEqual(["a", "b", "c"]);
688
+ }, opts.timeout);
689
+ itImpl("matches on a boolean column", async () => {
690
+ const found = await storage.query({ success: { value: [false], operator: "not-in" } }) ?? [];
691
+ expect6(names(found)).toEqual(["a", "b"]);
692
+ }, opts.timeout);
693
+ itImpl("matches nothing when the list contains null", async () => {
694
+ const found = await storage.query({
695
+ option: { value: ["x", null], operator: "not-in" }
696
+ }) ?? [];
697
+ expect6(found).toEqual([]);
698
+ }, opts.timeout);
699
+ describe6.skipIf(!opts.capabilities.supportsTransactions)("through a transaction handle", () => {
700
+ itImpl("refuses an empty exclusion there too", async () => {
701
+ await expect6(storage.withTransaction(async (tx) => {
702
+ await tx.deleteSearch({ option: { value: [], operator: "not-in" } });
703
+ })).rejects.toThrow(/delete the whole table/);
704
+ const left = await storage.getAll() ?? [];
705
+ expect6(names(left)).toEqual(["a", "b", "c", "d"]);
706
+ }, opts.timeout);
707
+ itImpl("keeps empty criteria a no-op there too", async () => {
708
+ await storage.withTransaction(async (tx) => {
709
+ await tx.deleteSearch({});
710
+ });
711
+ const left = await storage.getAll() ?? [];
712
+ expect6(names(left)).toEqual(["a", "b", "c", "d"]);
713
+ }, opts.timeout);
714
+ });
715
+ });
716
+ }
717
+ // src/tabular-storage/assertions/strategyParity.ts
718
+ import { BaseTabularStorage } from "@workglow/storage";
719
+ import { describe as describe7, expect as expect7, it as it8 } from "vitest";
720
+ var AXES = {
721
+ type: [
722
+ { label: "inner", value: "inner" },
723
+ { label: "left", value: "left" }
724
+ ],
725
+ on: [
726
+ { label: "one key", value: [{ left: "author_id", right: "id" }] },
727
+ {
728
+ label: "a compound key",
729
+ value: [
730
+ { left: "tenant", right: "tenant" },
731
+ { left: "author_id", right: "id" }
732
+ ]
733
+ }
734
+ ],
735
+ where: [
736
+ { label: "no filter", value: undefined },
737
+ { label: "a left filter", value: { left: { views: { value: 5, operator: ">" } } } },
738
+ { label: "a right filter", value: { right: { name: "Ann" } } },
739
+ { label: "a null right filter", value: { right: { country: null } } },
740
+ {
741
+ label: "both filters",
742
+ value: { left: { tenant: "t1" }, right: { name: "Ann" } }
743
+ }
744
+ ],
745
+ orderBy: [
746
+ { label: "no order", value: undefined },
747
+ {
748
+ label: "a left order",
749
+ value: [{ side: "left", column: "views", direction: "DESC" }]
750
+ },
751
+ {
752
+ label: "a nullable left order",
753
+ value: [
754
+ { side: "left", column: "author_id", direction: "ASC" },
755
+ { side: "left", column: "id", direction: "ASC" }
756
+ ]
757
+ },
758
+ {
759
+ label: "a right order",
760
+ value: [
761
+ { side: "right", column: "name", direction: "ASC" },
762
+ { side: "left", column: "id", direction: "ASC" }
763
+ ]
764
+ },
765
+ {
766
+ label: "a descending right order",
767
+ value: [
768
+ { side: "right", column: "name", direction: "DESC" },
769
+ { side: "left", column: "id", direction: "DESC" }
770
+ ]
771
+ }
772
+ ],
773
+ limit: [
774
+ { label: "no limit", value: undefined },
775
+ { label: "a limit of 2", value: 2 },
776
+ { label: "a limit of 50", value: 50 }
777
+ ],
778
+ offset: [
779
+ { label: "no offset", value: undefined },
780
+ { label: "an offset of 1", value: 1 },
781
+ { label: "an offset past the end", value: 99 }
782
+ ]
783
+ };
784
+ var BASE = {
785
+ type: AXES.type[0].value,
786
+ on: AXES.on[0].value
787
+ };
788
+ function buildSpecs() {
789
+ const specs = [];
790
+ const seen = new Set;
791
+ const add = (label, spec) => {
792
+ const key = JSON.stringify(spec);
793
+ if (seen.has(key))
794
+ return;
795
+ seen.add(key);
796
+ specs.push({ label, spec });
797
+ };
798
+ for (const key of Object.keys(AXES)) {
799
+ for (const variant of AXES[key]) {
800
+ add(variant.label, { ...BASE, [key]: variant.value });
801
+ }
802
+ }
803
+ for (const type of AXES.type) {
804
+ for (const order of AXES.orderBy) {
805
+ for (const limit of AXES.limit) {
806
+ for (const offset of AXES.offset) {
807
+ add(`${type.label} join, ${order.label}, ${limit.label}, ${offset.label}`, {
808
+ ...BASE,
809
+ type: type.value,
810
+ orderBy: order.value,
811
+ limit: limit.value,
812
+ offset: offset.value
813
+ });
814
+ }
815
+ }
816
+ }
817
+ }
818
+ return specs;
819
+ }
820
+ var SPECS = buildSpecs();
821
+ function multisetRemainder(part, whole) {
822
+ const available = new Map;
823
+ for (const row of whole)
824
+ available.set(row, (available.get(row) ?? 0) + 1);
825
+ const unaccounted = [];
826
+ for (const row of part) {
827
+ const left = available.get(row) ?? 0;
828
+ if (left === 0)
829
+ unaccounted.push(row);
830
+ else
831
+ available.set(row, left - 1);
832
+ }
833
+ return unaccounted;
834
+ }
835
+ function windowLength(total, spec) {
836
+ const remaining = Math.max(0, total - (spec.offset ?? 0));
837
+ return spec.limit === undefined ? remaining : Math.min(spec.limit, remaining);
838
+ }
839
+ function shape(rows) {
840
+ return rows.map((row) => `${row.left.id}:${row.right?.id ?? "-"}`);
841
+ }
842
+ function sortRows(rows) {
843
+ return [...rows].sort((a, b) => `${a.left.id}:${a.right?.id ?? "-"}`.localeCompare(`${b.left.id}:${b.right?.id ?? "-"}`));
844
+ }
845
+ function comparisonFor(spec) {
846
+ if ((spec.orderBy?.length ?? 0) > 0)
847
+ return "ordered";
848
+ return spec.limit === undefined && spec.offset === undefined ? "set" : "window";
849
+ }
850
+ var viaHashJoin = BaseTabularStorage.prototype.join;
851
+ function joinStrategyParityBlock(createPosts, createAuthors, seed, opts) {
852
+ describe7.skipIf(opts.expectSqlPushdown !== true)("join strategy parity", () => {
853
+ it8("states, for every field of a join spec, what the two strategies agree about", () => {
854
+ const empty = Object.keys(AXES).filter((key) => AXES[key].length === 0);
855
+ expect7(empty).toEqual([]);
856
+ expect7(SPECS.length).toBeGreaterThan(AXES.orderBy.length * AXES.limit.length);
857
+ }, opts.timeout);
858
+ for (const { label, spec } of SPECS) {
859
+ it8(`the pushdown and the hash join agree on ${label}`, async () => {
860
+ const posts = await createPosts();
861
+ const authors = await createAuthors();
862
+ try {
863
+ await posts.setupDatabase?.();
864
+ await authors.setupDatabase?.();
865
+ await seed(posts, authors);
866
+ const pushedDown = await posts.join(spec, authors);
867
+ const hashed = await viaHashJoin.call(posts, spec, authors);
868
+ switch (comparisonFor(spec)) {
869
+ case "ordered": {
870
+ expect7(shape(pushedDown)).toEqual(shape(hashed));
871
+ expect7(pushedDown).toEqual(hashed);
872
+ break;
873
+ }
874
+ case "set": {
875
+ expect7(shape(pushedDown).sort()).toEqual(shape(hashed).sort());
876
+ expect7(sortRows(pushedDown)).toEqual(sortRows(hashed));
877
+ break;
878
+ }
879
+ case "window": {
880
+ const whole = shape(await posts.join({ ...spec, limit: undefined, offset: undefined }, authors));
881
+ const expectedLength = windowLength(whole.length, spec);
882
+ expect7(shape(pushedDown)).toHaveLength(expectedLength);
883
+ expect7(shape(hashed)).toHaveLength(expectedLength);
884
+ expect7(multisetRemainder(shape(pushedDown), whole)).toEqual([]);
885
+ expect7(multisetRemainder(shape(hashed), whole)).toEqual([]);
886
+ break;
887
+ }
888
+ }
889
+ } finally {
890
+ await posts.deleteAll();
891
+ await authors.deleteAll();
892
+ posts.destroy?.();
893
+ authors.destroy?.();
894
+ }
895
+ }, opts.timeout);
896
+ }
897
+ });
898
+ }
899
+ // src/tabular-storage/assertions/subscribeToChanges.ts
900
+ import { sleep } from "@workglow/util";
901
+ import { afterEach as afterEach7, beforeEach as beforeEach7, describe as describe8, expect as expect8, it as it9 } from "vitest";
902
+ function subscribeToChangesBlock(opts) {
903
+ if (!opts.capabilities.supportsSubscriptions) {
904
+ describe8.skip("subscribeToChanges", () => {
905
+ it9("skipped: capabilities.supportsSubscriptions === false", () => {});
906
+ });
907
+ return;
908
+ }
909
+ const expectFails = new Set(opts.expectedFailures ?? []);
910
+ const itImpl = expectFails.has("subscribeToChanges") ? itExpectFail : it9;
911
+ const usesPolling = opts.usesPolling;
912
+ const pollingIntervalMs = opts.pollingIntervalMs ?? 1;
913
+ const waitTime = usesPolling ? Math.max(pollingIntervalMs * 8, 200) : 50;
914
+ const initWaitTime = usesPolling ? Math.max(pollingIntervalMs * 10, 300) : 10;
915
+ if (usesPolling) {
916
+ describe8("subscribeToChanges.polling", () => {
917
+ let storage;
918
+ beforeEach7(async () => {
919
+ storage = await opts.createStorage();
920
+ await storage.setupDatabase?.();
921
+ });
922
+ afterEach7(async () => {
923
+ await storage.deleteAll();
924
+ storage.destroy?.();
925
+ await opts.releaseStorage?.(storage);
926
+ });
927
+ itImpl("observes every write (set equality + count, order unspecified)", async () => {
928
+ const changes = [];
929
+ const unsubscribe = storage.subscribeToChanges((change) => changes.push(change), {
930
+ pollingIntervalMs
931
+ });
932
+ await sleep(initWaitTime);
933
+ await storage.put({ name: "t1", type: "s1", option: "v1", success: true });
934
+ await storage.put({ name: "t2", type: "s2", option: "v2", success: false });
935
+ await storage.put({ name: "t3", type: "s3", option: "v3", success: true });
936
+ await sleep(waitTime);
937
+ const writeEvents = changes.filter((c) => c.type === "INSERT" || c.type === "UPDATE");
938
+ expect8(writeEvents.length).toBe(3);
939
+ const options = writeEvents.map((e) => e.new?.option).sort((a, b) => String(a).localeCompare(String(b)));
940
+ expect8(options).toEqual(["v1", "v2", "v3"]);
941
+ unsubscribe();
942
+ }, opts.timeout);
943
+ });
944
+ return;
945
+ }
946
+ describe8("subscribeToChanges.eventDriven", () => {
947
+ let storage;
948
+ beforeEach7(async () => {
949
+ storage = await opts.createStorage();
950
+ await storage.setupDatabase?.();
951
+ });
952
+ afterEach7(async () => {
953
+ await storage.deleteAll();
954
+ storage.destroy?.();
955
+ await opts.releaseStorage?.(storage);
956
+ });
957
+ itImpl("fires exactly once per write in commit order", async () => {
958
+ const changes = [];
959
+ const unsubscribe = storage.subscribeToChanges((change) => changes.push(change), {
960
+ pollingIntervalMs
961
+ });
962
+ await sleep(initWaitTime);
963
+ await storage.put({ name: "t1", type: "s1", option: "v1", success: true });
964
+ await storage.put({ name: "t2", type: "s2", option: "v2", success: false });
965
+ await storage.put({ name: "t3", type: "s3", option: "v3", success: true });
966
+ await sleep(waitTime);
967
+ const writeEvents = changes.filter((c) => c.type === "INSERT" || c.type === "UPDATE");
968
+ expect8(writeEvents.length).toBe(3);
969
+ expect8(writeEvents[0].new?.option).toBe("v1");
970
+ expect8(writeEvents[1].new?.option).toBe("v2");
971
+ expect8(writeEvents[2].new?.option).toBe("v3");
972
+ unsubscribe();
973
+ }, opts.timeout);
974
+ });
975
+ }
976
+ // src/tabular-storage/assertions/vectorColumnFormat.ts
977
+ import { afterEach as afterEach8, beforeEach as beforeEach8, describe as describe9, expect as expect9, it as it10 } from "vitest";
978
+ function vectorColumnFormatBlock(opts) {
979
+ const expectFails = new Set(opts.expectedFailures ?? []);
980
+ const itImpl = expectFails.has("vectorColumnFormat") ? itExpectFail : it10;
981
+ describe9.skipIf(!opts.capabilities.supportsVectorColumns)("vectorColumnFormat", () => {
982
+ let storage;
983
+ beforeEach8(async () => {
984
+ if (!opts.createVectorStorage) {
985
+ throw new Error("createVectorStorage is required when capabilities.supportsVectorColumns is true");
986
+ }
987
+ storage = await opts.createVectorStorage();
988
+ await storage.setupDatabase?.();
989
+ });
990
+ afterEach8(async () => {
991
+ await storage.deleteAll();
992
+ storage.destroy?.();
993
+ await opts.releaseStorage?.(storage);
994
+ });
995
+ itImpl("round-trip preserves Float32Array type and values", async () => {
996
+ const original = new Float32Array([0.1, 0.2, 0.3]);
997
+ await storage.put({ id: "v1", embedding: original });
998
+ const retrieved = await storage.get({ id: "v1" });
999
+ expect9(retrieved).toBeDefined();
1000
+ const embedding = retrieved?.embedding;
1001
+ expect9(embedding).toBeInstanceOf(Float32Array);
1002
+ expect9(embedding[0]).toBeCloseTo(0.1, 5);
1003
+ expect9(embedding[1]).toBeCloseTo(0.2, 5);
1004
+ expect9(embedding[2]).toBeCloseTo(0.3, 5);
1005
+ }, opts.timeout);
1006
+ });
1007
+ }
1008
+ // src/tabular-storage/assertions/withConnectionTransaction.ts
1009
+ import {
1010
+ ConnectionReentryError,
1011
+ withConnectionTransaction
1012
+ } from "@workglow/storage";
1013
+ import { afterEach as afterEach9, beforeEach as beforeEach9, describe as describe10, expect as expect10, it as it11 } from "vitest";
1014
+ function withConnectionTransactionBlock(opts) {
1015
+ const expectFails = new Set(opts.expectedFailures ?? []);
1016
+ const itImpl = expectFails.has("withConnectionTransaction") ? itExpectFail : it11;
1017
+ describe10.skipIf(!opts.capabilities.supportsTransactions || opts.createSiblingStorage === undefined)("withConnectionTransaction", () => {
1018
+ let primary;
1019
+ let sibling;
1020
+ let outsider;
1021
+ beforeEach9(async () => {
1022
+ primary = await opts.createStorage();
1023
+ await primary.setupDatabase?.();
1024
+ sibling = await opts.createSiblingStorage(primary);
1025
+ await sibling.setupDatabase?.();
1026
+ outsider = await opts.createSiblingStorage(primary);
1027
+ await outsider.setupDatabase?.();
1028
+ });
1029
+ afterEach9(async () => {
1030
+ await outsider?.deleteAll();
1031
+ outsider?.destroy?.();
1032
+ await sibling?.deleteAll();
1033
+ sibling?.destroy?.();
1034
+ await primary?.deleteAll();
1035
+ primary?.destroy?.();
1036
+ await opts.releaseStorage?.(primary);
1037
+ });
1038
+ itImpl("commits writes across two tables", async () => {
1039
+ await withConnectionTransaction([primary, sibling], async () => {
1040
+ await primary.put({ name: "from-a", type: "x", option: "va", success: true });
1041
+ await sibling.put({ name: "from-b", type: "x", option: "vb", success: true });
1042
+ });
1043
+ expect10(await primary.get({ name: "from-a", type: "x" })).toMatchObject({ option: "va" });
1044
+ expect10(await sibling.get({ name: "from-b", type: "x" })).toMatchObject({ option: "vb" });
1045
+ }, opts.timeout);
1046
+ itImpl("rolls back both tables when the callback throws", async () => {
1047
+ await primary.put({ name: "kept", type: "x", option: "base", success: true });
1048
+ await expect10(withConnectionTransaction([primary, sibling], async () => {
1049
+ await primary.put({ name: "from-a", type: "x", option: "va", success: true });
1050
+ await sibling.put({ name: "from-b", type: "x", option: "vb", success: true });
1051
+ throw new Error("forced rollback");
1052
+ })).rejects.toThrow("forced rollback");
1053
+ expect10(await primary.get({ name: "kept", type: "x" })).toBeDefined();
1054
+ expect10(await primary.get({ name: "from-a", type: "x" })).toBeUndefined();
1055
+ expect10(await sibling.get({ name: "from-b", type: "x" })).toBeUndefined();
1056
+ }, opts.timeout);
1057
+ itImpl("does not refuse an unrelated concurrent caller", async () => {
1058
+ let releaseBody = () => {};
1059
+ const bodyCanFinish = new Promise((resolve) => {
1060
+ releaseBody = resolve;
1061
+ });
1062
+ let signalStarted = () => {};
1063
+ const bodyStarted = new Promise((resolve) => {
1064
+ signalStarted = resolve;
1065
+ });
1066
+ const txPromise = withConnectionTransaction([primary, sibling], async () => {
1067
+ await primary.put({ name: "enlisted", type: "x", option: "ok", success: true });
1068
+ signalStarted();
1069
+ await bodyCanFinish;
1070
+ });
1071
+ await bodyStarted;
1072
+ let outsiderError;
1073
+ const concurrent = outsider.put({ name: "concurrent", type: "x", option: "yes", success: true }).catch((err) => {
1074
+ outsiderError = err;
1075
+ });
1076
+ releaseBody();
1077
+ await txPromise;
1078
+ await concurrent;
1079
+ expect10(outsiderError).toBeUndefined();
1080
+ expect10(await outsider.get({ name: "concurrent", type: "x" })).toBeDefined();
1081
+ expect10(await primary.get({ name: "enlisted", type: "x" })).toBeDefined();
1082
+ }, opts.timeout);
1083
+ itImpl("does not refuse a concurrent transaction whose participant set merely differs", async () => {
1084
+ let releaseFirst = () => {};
1085
+ const firstCanFinish = new Promise((resolve) => {
1086
+ releaseFirst = resolve;
1087
+ });
1088
+ let signalStarted = () => {};
1089
+ const firstStarted = new Promise((resolve) => {
1090
+ signalStarted = resolve;
1091
+ });
1092
+ const first = withConnectionTransaction([primary, sibling], async () => {
1093
+ await primary.put({ name: "first", type: "x", option: "one", success: true });
1094
+ signalStarted();
1095
+ await firstCanFinish;
1096
+ });
1097
+ await firstStarted;
1098
+ let secondError;
1099
+ const second = withConnectionTransaction([sibling, outsider], async () => {
1100
+ await outsider.put({ name: "second", type: "x", option: "two", success: true });
1101
+ }).catch((err) => {
1102
+ secondError = err;
1103
+ });
1104
+ releaseFirst();
1105
+ await first;
1106
+ await second;
1107
+ expect10(secondError).toBeUndefined();
1108
+ expect10(await primary.get({ name: "first", type: "x" })).toBeDefined();
1109
+ expect10(await outsider.get({ name: "second", type: "x" })).toBeDefined();
1110
+ }, opts.timeout);
1111
+ itImpl("does not show uncommitted rows to a concurrent reader", async () => {
1112
+ await primary.put({ name: "iso", type: "x", option: "before", success: true });
1113
+ let releaseBody = () => {};
1114
+ const bodyCanFinish = new Promise((resolve) => {
1115
+ releaseBody = resolve;
1116
+ });
1117
+ let signalStarted = () => {};
1118
+ const bodyStarted = new Promise((resolve) => {
1119
+ signalStarted = resolve;
1120
+ });
1121
+ let rollbackError;
1122
+ const txPromise = withConnectionTransaction([primary, sibling], async () => {
1123
+ await primary.put({ name: "iso", type: "x", option: "dirty", success: true });
1124
+ signalStarted();
1125
+ await bodyCanFinish;
1126
+ throw new Error("forced rollback");
1127
+ }).catch((err) => {
1128
+ rollbackError = err;
1129
+ });
1130
+ await bodyStarted;
1131
+ const readPromise = primary.get({ name: "iso", type: "x" });
1132
+ await new Promise((resolve) => setTimeout(resolve, 25));
1133
+ releaseBody();
1134
+ await txPromise;
1135
+ expect10(rollbackError?.message).toBe("forced rollback");
1136
+ expect10(await readPromise).toMatchObject({ option: "before" });
1137
+ expect10(await primary.get({ name: "iso", type: "x" })).toMatchObject({ option: "before" });
1138
+ }, opts.timeout);
1139
+ itImpl("does not show uncommitted rows to a concurrent pushed-down join", async () => {
1140
+ await primary.put({ name: "join-iso", type: "j", option: "before", success: true });
1141
+ await sibling.put({ name: "join-key", type: "j", option: "rhs", success: true });
1142
+ let releaseBody = () => {};
1143
+ const bodyCanFinish = new Promise((resolve) => {
1144
+ releaseBody = resolve;
1145
+ });
1146
+ let signalStarted = () => {};
1147
+ const bodyStarted = new Promise((resolve) => {
1148
+ signalStarted = resolve;
1149
+ });
1150
+ let rollbackError;
1151
+ const txPromise = withConnectionTransaction([primary, sibling], async () => {
1152
+ await primary.put({ name: "join-iso", type: "j", option: "dirty", success: true });
1153
+ signalStarted();
1154
+ await bodyCanFinish;
1155
+ throw new Error("forced rollback");
1156
+ }).catch((err) => {
1157
+ rollbackError = err;
1158
+ });
1159
+ await bodyStarted;
1160
+ const joinPromise = primary.join({ type: "inner", on: [{ left: "type", right: "type" }] }, sibling);
1161
+ await new Promise((resolve) => setTimeout(resolve, 25));
1162
+ releaseBody();
1163
+ await txPromise;
1164
+ expect10(rollbackError?.message).toBe("forced rollback");
1165
+ const rows = await joinPromise;
1166
+ expect10(rows).toHaveLength(1);
1167
+ expect10(rows[0].left).toMatchObject({ name: "join-iso", option: "before" });
1168
+ expect10(rows[0].right).toMatchObject({ name: "join-key", option: "rhs" });
1169
+ }, opts.timeout);
1170
+ itImpl("throws sibling-op for a storage that was not enlisted", async () => {
1171
+ let error;
1172
+ await withConnectionTransaction([primary, sibling], async () => {
1173
+ await primary.put({ name: "enlisted", type: "x", option: "ok", success: true });
1174
+ try {
1175
+ await outsider.put({ name: "outsider", type: "x", option: "no", success: true });
1176
+ } catch (err) {
1177
+ error = err;
1178
+ }
1179
+ });
1180
+ expect10(error).toBeInstanceOf(ConnectionReentryError);
1181
+ expect10(error.mode).toBe("sibling-op");
1182
+ expect10(await outsider.get({ name: "outsider", type: "x" })).toBeUndefined();
1183
+ expect10(await primary.get({ name: "enlisted", type: "x" })).toBeDefined();
1184
+ }, opts.timeout);
1185
+ });
1186
+ }
1187
+ // src/tabular-storage/assertions/withTransactionRollback.ts
1188
+ import { afterEach as afterEach10, beforeEach as beforeEach10, describe as describe11, expect as expect11, it as it12 } from "vitest";
1189
+ function withTransactionRollbackBlock(opts) {
1190
+ const expectFails = new Set(opts.expectedFailures ?? []);
1191
+ const itImpl = expectFails.has("withTransactionRollback") ? itExpectFail : it12;
1192
+ describe11.skipIf(!opts.capabilities.supportsTransactions)("withTransactionRollback", () => {
1193
+ let storage;
1194
+ beforeEach10(async () => {
1195
+ storage = await opts.createStorage();
1196
+ await storage.setupDatabase?.();
1197
+ });
1198
+ afterEach10(async () => {
1199
+ await storage.deleteAll();
1200
+ storage.destroy?.();
1201
+ await opts.releaseStorage?.(storage);
1202
+ });
1203
+ itImpl("rolls back writes when callback throws", async () => {
1204
+ const baseline = { name: "before", type: "tx", option: "base", success: true };
1205
+ await storage.put(baseline);
1206
+ await expect11(storage.withTransaction(async (tx) => {
1207
+ await tx.put({ name: "inside", type: "tx", option: "intra", success: false });
1208
+ throw new Error("forced rollback");
1209
+ })).rejects.toBeDefined();
1210
+ const intraRow = await storage.get({ name: "inside", type: "tx" });
1211
+ expect11(intraRow).toBeUndefined();
1212
+ const beforeRow = await storage.get({ name: "before", type: "tx" });
1213
+ expect11(beforeRow).toBeDefined();
1214
+ expect11(beforeRow?.option).toBe("base");
1215
+ }, opts.timeout);
1216
+ });
1217
+ }
1218
+ // src/tabular-storage/joinFixtures.ts
1219
+ var AuthorPrimaryKeyNames = ["id"];
1220
+ var AuthorSchema = {
1221
+ type: "object",
1222
+ properties: {
1223
+ id: { type: "string" },
1224
+ tenant: { type: "string" },
1225
+ name: { type: "string" },
1226
+ country: { anyOf: [{ type: "string" }, { type: "null" }] }
1227
+ },
1228
+ required: ["id", "tenant", "name", "country"],
1229
+ additionalProperties: false
1230
+ };
1231
+ var PostPrimaryKeyNames = ["id"];
1232
+ var PostSchema = {
1233
+ type: "object",
1234
+ properties: {
1235
+ id: { type: "string" },
1236
+ tenant: { type: "string" },
1237
+ author_id: { anyOf: [{ type: "string" }, { type: "null" }] },
1238
+ title: { type: "string" },
1239
+ views: { type: "number" }
1240
+ },
1241
+ required: ["id", "tenant", "author_id", "title", "views"],
1242
+ additionalProperties: false
1243
+ };
1244
+ var JOIN_FIXTURE_AUTHORS = [
1245
+ { id: "a1", tenant: "t1", name: "Ann", country: "US" },
1246
+ { id: "a2", tenant: "t1", name: "Bob", country: null },
1247
+ { id: "a3", tenant: "t2", name: "Cid", country: "FR" }
1248
+ ];
1249
+ var JOIN_FIXTURE_POSTS = [
1250
+ { id: "p1", tenant: "t1", author_id: "a1", title: "one", views: 10 },
1251
+ { id: "p2", tenant: "t1", author_id: "a1", title: "two", views: 5 },
1252
+ { id: "p3", tenant: "t1", author_id: "a2", title: "three", views: 7 },
1253
+ { id: "p4", tenant: "t1", author_id: "zz", title: "orphan", views: 1 },
1254
+ { id: "p5", tenant: "t1", author_id: null, title: "anon", views: 3 },
1255
+ { id: "p6", tenant: "t2", author_id: "a1", title: "cross", views: 9 }
1256
+ ];
1257
+ async function seedJoinFixtures(posts, authors) {
1258
+ await authors.putBulk(JOIN_FIXTURE_AUTHORS);
1259
+ await posts.putBulk(JOIN_FIXTURE_POSTS);
1260
+ }
1261
+ // src/tabular-storage/runTabularJoinContract.ts
1262
+ import { StorageInvalidColumnError, StorageValidationError } from "@workglow/storage";
1263
+ import { afterEach as afterEach11, beforeEach as beforeEach11, describe as describe12, expect as expect12, it as it13, vi as vi3 } from "vitest";
1264
+ var pairs = (rows) => rows.map((r) => `${r.left.id}:${r.right?.id ?? "-"}`);
1265
+ function runTabularJoinContract(createPosts, createAuthors, opts = {}) {
1266
+ const on = [{ left: "author_id", right: "id" }];
1267
+ describe12(`join (${opts.expectSqlPushdown ? "SQL pushdown" : "hash join"})`, () => {
1268
+ let posts;
1269
+ let authors;
1270
+ beforeEach11(async () => {
1271
+ posts = await createPosts();
1272
+ authors = await createAuthors();
1273
+ await posts.setupDatabase?.();
1274
+ await authors.setupDatabase?.();
1275
+ await seedJoinFixtures(posts, authors);
1276
+ });
1277
+ afterEach11(async () => {
1278
+ await posts.deleteAll();
1279
+ await authors.deleteAll();
1280
+ posts.destroy?.();
1281
+ authors.destroy?.();
1282
+ vi3.restoreAllMocks();
1283
+ });
1284
+ it13.skipIf(opts.expectSqlPushdown === undefined)("takes the expected execution path", async () => {
1285
+ const rightQuery = vi3.spyOn(authors, "query");
1286
+ await posts.join({ type: "inner", on }, authors);
1287
+ if (opts.expectSqlPushdown) {
1288
+ expect12(rightQuery).not.toHaveBeenCalled();
1289
+ } else {
1290
+ expect12(rightQuery).toHaveBeenCalled();
1291
+ }
1292
+ }, opts.timeout);
1293
+ it13("inner join pairs every match and drops orphans and null keys", async () => {
1294
+ const rows = await posts.join({ type: "inner", on }, authors);
1295
+ expect12(pairs(rows).sort()).toEqual(["p1:a1", "p2:a1", "p3:a2", "p6:a1"]);
1296
+ const row = rows.find((r) => r.left.id === "p1");
1297
+ expect12(row?.left).toEqual(JOIN_FIXTURE_POSTS[0]);
1298
+ expect12(row?.right).toEqual(JOIN_FIXTURE_AUTHORS[0]);
1299
+ }, opts.timeout);
1300
+ it13("left join keeps orphans and null keys with right undefined", async () => {
1301
+ const rows = await posts.join({ type: "left", on }, authors);
1302
+ expect12(pairs(rows).sort()).toEqual(["p1:a1", "p2:a1", "p3:a2", "p4:-", "p5:-", "p6:a1"]);
1303
+ expect12(rows.find((r) => r.left.id === "p4")?.right).toBeUndefined();
1304
+ expect12(rows.find((r) => r.left.id === "p5")?.right).toBeUndefined();
1305
+ }, opts.timeout);
1306
+ it13("a right-side filter under a left join does not drop unmatched left rows", async () => {
1307
+ const rows = await posts.join({ type: "left", on, where: { right: { name: "Ann" } } }, authors);
1308
+ expect12(pairs(rows).sort()).toEqual(["p1:a1", "p2:a1", "p3:-", "p4:-", "p5:-", "p6:a1"]);
1309
+ }, opts.timeout);
1310
+ it13("a right-side filter under an inner join narrows the result", async () => {
1311
+ const rows = await posts.join({ type: "inner", on, where: { right: { country: null } } }, authors);
1312
+ expect12(pairs(rows)).toEqual(["p3:a2"]);
1313
+ }, opts.timeout);
1314
+ it13("a left-side filter narrows the left rows", async () => {
1315
+ const rows = await posts.join({ type: "left", on, where: { left: { views: { value: 5, operator: ">" } } } }, authors);
1316
+ expect12(pairs(rows).sort()).toEqual(["p1:a1", "p3:a2", "p6:a1"]);
1317
+ }, opts.timeout);
1318
+ it13("joins on a compound key", async () => {
1319
+ const rows = await posts.join({
1320
+ type: "inner",
1321
+ on: [
1322
+ { left: "tenant", right: "tenant" },
1323
+ { left: "author_id", right: "id" }
1324
+ ]
1325
+ }, authors);
1326
+ expect12(pairs(rows).sort()).toEqual(["p1:a1", "p2:a1", "p3:a2"]);
1327
+ }, opts.timeout);
1328
+ it13("orders by a left column", async () => {
1329
+ const rows = await posts.join({ type: "left", on, orderBy: [{ side: "left", column: "views", direction: "DESC" }] }, authors);
1330
+ expect12(rows.map((r) => r.left.views)).toEqual([10, 9, 7, 5, 3, 1]);
1331
+ }, opts.timeout);
1332
+ it13("orders by a right column with unmatched rows first under ASC", async () => {
1333
+ const rows = await posts.join({
1334
+ type: "left",
1335
+ on,
1336
+ orderBy: [
1337
+ { side: "right", column: "name", direction: "ASC" },
1338
+ { side: "left", column: "id", direction: "ASC" }
1339
+ ]
1340
+ }, authors);
1341
+ expect12(pairs(rows)).toEqual(["p4:-", "p5:-", "p1:a1", "p2:a1", "p6:a1", "p3:a2"]);
1342
+ }, opts.timeout);
1343
+ it13("orders by a right column with unmatched rows last under DESC", async () => {
1344
+ const rows = await posts.join({
1345
+ type: "left",
1346
+ on,
1347
+ orderBy: [
1348
+ { side: "right", column: "name", direction: "DESC" },
1349
+ { side: "left", column: "id", direction: "DESC" }
1350
+ ]
1351
+ }, authors);
1352
+ expect12(pairs(rows)).toEqual(["p3:a2", "p6:a1", "p2:a1", "p1:a1", "p5:-", "p4:-"]);
1353
+ }, opts.timeout);
1354
+ it13("applies limit and offset to the joined rows", async () => {
1355
+ const rows = await posts.join({
1356
+ type: "inner",
1357
+ on,
1358
+ orderBy: [{ side: "left", column: "id", direction: "ASC" }],
1359
+ offset: 1,
1360
+ limit: 2
1361
+ }, authors);
1362
+ expect12(pairs(rows)).toEqual(["p2:a1", "p3:a2"]);
1363
+ }, opts.timeout);
1364
+ it13("offset without limit skips rows", async () => {
1365
+ const rows = await posts.join({
1366
+ type: "inner",
1367
+ on,
1368
+ orderBy: [{ side: "left", column: "id", direction: "ASC" }],
1369
+ offset: 3
1370
+ }, authors);
1371
+ expect12(pairs(rows)).toEqual(["p6:a1"]);
1372
+ }, opts.timeout);
1373
+ it13("honours a caller's own criterion on the join column", async () => {
1374
+ const rows = await posts.join({ type: "inner", on, where: { right: { id: "a2" } } }, authors);
1375
+ expect12(pairs(rows)).toEqual(["p3:a2"]);
1376
+ }, opts.timeout);
1377
+ it13("returns [] rather than undefined when nothing matches", async () => {
1378
+ const rows = await posts.join({ type: "inner", on, where: { left: { title: "no such title" } } }, authors);
1379
+ expect12(rows).toEqual([]);
1380
+ }, opts.timeout);
1381
+ it13("joins many distinct keys", async () => {
1382
+ const n = 1100;
1383
+ await authors.putBulk(Array.from({ length: n }, (_, i) => ({
1384
+ id: `b${i}`,
1385
+ tenant: "t9",
1386
+ name: `n${i}`,
1387
+ country: null
1388
+ })));
1389
+ await posts.putBulk(Array.from({ length: n }, (_, i) => ({
1390
+ id: `q${i}`,
1391
+ tenant: "t9",
1392
+ author_id: `b${i}`,
1393
+ title: `t${i}`,
1394
+ views: i
1395
+ })));
1396
+ const rows = await posts.join({ type: "inner", on, where: { left: { tenant: "t9" } } }, authors);
1397
+ expect12(rows).toHaveLength(n);
1398
+ expect12(rows.every((r) => r.right.id === r.left.author_id)).toBe(true);
1399
+ }, opts.timeout);
1400
+ it13("rejects an unknown column on either side before running", async () => {
1401
+ await expect12(posts.join({ type: "inner", on: [{ left: "nope", right: "id" }] }, authors)).rejects.toBeInstanceOf(StorageInvalidColumnError);
1402
+ await expect12(posts.join({ type: "inner", on: [{ left: "author_id", right: "nope" }] }, authors)).rejects.toBeInstanceOf(StorageInvalidColumnError);
1403
+ await expect12(posts.join({ type: "inner", on, orderBy: [{ side: "right", column: "views", direction: "ASC" }] }, authors)).rejects.toBeInstanceOf(StorageInvalidColumnError);
1404
+ }, opts.timeout);
1405
+ it13("rejects a forged join type, side, direction, limit or offset", async () => {
1406
+ await expect12(posts.join({ type: "cross; DROP TABLE x", on }, authors)).rejects.toBeInstanceOf(StorageValidationError);
1407
+ await expect12(posts.join({ type: "inner", on, orderBy: [{ side: "x", column: "id", direction: "ASC" }] }, authors)).rejects.toBeInstanceOf(StorageValidationError);
1408
+ await expect12(posts.join({
1409
+ type: "inner",
1410
+ on,
1411
+ orderBy: [{ side: "left", column: "id", direction: "ASC; --" }]
1412
+ }, authors)).rejects.toBeInstanceOf(StorageValidationError);
1413
+ await expect12(posts.join({ type: "inner", on, limit: 0 }, authors)).rejects.toBeInstanceOf(StorageValidationError);
1414
+ await expect12(posts.join({ type: "inner", on, offset: -1 }, authors)).rejects.toBeInstanceOf(StorageValidationError);
1415
+ await expect12(posts.join({ type: "inner", on: [] }, authors)).rejects.toBeInstanceOf(StorageValidationError);
1416
+ }, opts.timeout);
1417
+ });
1418
+ joinBoundedLeftReadBlock(createPosts, createAuthors, seedJoinFixtures, opts);
1419
+ joinStrategyParityBlock(createPosts, createAuthors, seedJoinFixtures, opts);
1420
+ }
1421
+ // src/tabular-storage/runTabularStorageContract.ts
1422
+ import { describe as describe13 } from "vitest";
1423
+
1424
+ // src/tabular-storage/types.ts
1425
+ var VectorItemPrimaryKeyNames = ["id"];
1426
+ var VectorItemSchema = {
1427
+ type: "object",
1428
+ properties: {
1429
+ id: { type: "string" },
1430
+ embedding: { type: "array", format: "TypedArray:Float32Array" }
1431
+ },
1432
+ required: ["id", "embedding"],
1433
+ additionalProperties: false
1434
+ };
1435
+
1436
+ // src/tabular-storage/runTabularStorageContract.ts
1437
+ function runTabularStorageContract(opts) {
1438
+ describe13.skipIf(opts.skip)(`Tabular storage contract: ${opts.name}`, () => {
1439
+ subscribeToChangesBlock(opts);
1440
+ vectorColumnFormatBlock(opts);
1441
+ withTransactionRollbackBlock(opts);
1442
+ withConnectionTransactionBlock(opts);
1443
+ countMatchesQueryBlock(opts);
1444
+ inListCriterionBlock(opts);
1445
+ notInListCriterionBlock(opts);
1446
+ joinPushdownBlock(opts);
1447
+ guardParityBlock(opts);
1448
+ });
1449
+ }
1450
+ export {
1451
+ AuthorPrimaryKeyNames,
1452
+ AuthorSchema,
1453
+ CompoundPrimaryKeyNames,
1454
+ CompoundSchema,
1455
+ JOIN_FIXTURE_AUTHORS,
1456
+ JOIN_FIXTURE_POSTS,
1457
+ PostPrimaryKeyNames,
1458
+ PostSchema,
1459
+ VectorItemPrimaryKeyNames,
1460
+ VectorItemSchema,
1461
+ countMatchesQueryBlock,
1462
+ guardParityBlock,
1463
+ inListCriterionBlock,
1464
+ joinBoundedLeftReadBlock,
1465
+ joinPushdownBlock,
1466
+ joinStrategyParityBlock,
1467
+ notInListCriterionBlock,
1468
+ runTabularJoinContract,
1469
+ runTabularStorageContract,
1470
+ seedJoinFixtures,
1471
+ subscribeToChangesBlock,
1472
+ vectorColumnFormatBlock,
1473
+ withConnectionTransactionBlock,
1474
+ withTransactionRollbackBlock
1475
+ };
1476
+
1477
+ //# debugId=FCE7FDB7D993910B64756E2164756E21