@workglow/storage 0.2.30 → 0.2.32

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 (62) hide show
  1. package/dist/browser.js +998 -61
  2. package/dist/browser.js.map +24 -14
  3. package/dist/bun.js +1051 -66
  4. package/dist/bun.js.map +25 -15
  5. package/dist/common.d.ts +7 -0
  6. package/dist/common.d.ts.map +1 -1
  7. package/dist/migrations/IMigration.d.ts +57 -0
  8. package/dist/migrations/IMigration.d.ts.map +1 -0
  9. package/dist/migrations/MigrationRunner.d.ts +44 -0
  10. package/dist/migrations/MigrationRunner.d.ts.map +1 -0
  11. package/dist/migrations/TabularMigration.d.ts +85 -0
  12. package/dist/migrations/TabularMigration.d.ts.map +1 -0
  13. package/dist/migrations/TabularMigrationOrchestrator.d.ts +34 -0
  14. package/dist/migrations/TabularMigrationOrchestrator.d.ts.map +1 -0
  15. package/dist/migrations/index.d.ts +11 -0
  16. package/dist/migrations/index.d.ts.map +1 -0
  17. package/dist/migrations/runBackfill.d.ts +19 -0
  18. package/dist/migrations/runBackfill.d.ts.map +1 -0
  19. package/dist/node.js +1051 -66
  20. package/dist/node.js.map +25 -15
  21. package/dist/sql/Dialect.d.ts +26 -0
  22. package/dist/sql/Dialect.d.ts.map +1 -0
  23. package/dist/sql/PredicateBuilder.d.ts +30 -0
  24. package/dist/sql/PredicateBuilder.d.ts.map +1 -0
  25. package/dist/sql/PrefixDdl.d.ts +79 -0
  26. package/dist/sql/PrefixDdl.d.ts.map +1 -0
  27. package/dist/sql/index.d.ts +9 -0
  28. package/dist/sql/index.d.ts.map +1 -0
  29. package/dist/tabular/BaseSqlTabularStorage.d.ts +63 -2
  30. package/dist/tabular/BaseSqlTabularStorage.d.ts.map +1 -1
  31. package/dist/tabular/BaseTabularStorage.d.ts +111 -6
  32. package/dist/tabular/BaseTabularStorage.d.ts.map +1 -1
  33. package/dist/tabular/CachedTabularStorage.d.ts +38 -0
  34. package/dist/tabular/CachedTabularStorage.d.ts.map +1 -1
  35. package/dist/tabular/Cursor.d.ts +79 -0
  36. package/dist/tabular/Cursor.d.ts.map +1 -0
  37. package/dist/tabular/FsFolderTabularStorage.d.ts +5 -1
  38. package/dist/tabular/FsFolderTabularStorage.d.ts.map +1 -1
  39. package/dist/tabular/HuggingFaceTabularStorage.d.ts +26 -2
  40. package/dist/tabular/HuggingFaceTabularStorage.d.ts.map +1 -1
  41. package/dist/tabular/ITabularStorage.d.ts +203 -3
  42. package/dist/tabular/ITabularStorage.d.ts.map +1 -1
  43. package/dist/tabular/InMemoryTabularMigrationApplier.d.ts +39 -0
  44. package/dist/tabular/InMemoryTabularMigrationApplier.d.ts.map +1 -0
  45. package/dist/tabular/InMemoryTabularStorage.d.ts +6 -2
  46. package/dist/tabular/InMemoryTabularStorage.d.ts.map +1 -1
  47. package/dist/tabular/SharedInMemoryTabularStorage.d.ts +4 -1
  48. package/dist/tabular/SharedInMemoryTabularStorage.d.ts.map +1 -1
  49. package/dist/tabular/SqlTabularMigrationApplier.d.ts +53 -0
  50. package/dist/tabular/SqlTabularMigrationApplier.d.ts.map +1 -0
  51. package/dist/tabular/StorageError.d.ts.map +1 -1
  52. package/dist/tabular/TabularStorageRegistry.d.ts +13 -10
  53. package/dist/tabular/TabularStorageRegistry.d.ts.map +1 -1
  54. package/dist/tabular/TelemetryTabularStorage.d.ts +11 -1
  55. package/dist/tabular/TelemetryTabularStorage.d.ts.map +1 -1
  56. package/dist/tabular/sqlMigrationDdl.d.ts +11 -0
  57. package/dist/tabular/sqlMigrationDdl.d.ts.map +1 -0
  58. package/dist/vector/IVectorStorage.d.ts +61 -1
  59. package/dist/vector/IVectorStorage.d.ts.map +1 -1
  60. package/package.json +3 -3
  61. package/src/tabular/README.md +73 -0
  62. package/src/vector/README.md +79 -0
package/dist/bun.js CHANGED
@@ -28,7 +28,7 @@ class StorageEmptyCriteriaError extends StorageValidationError {
28
28
  class StorageInvalidLimitError extends StorageValidationError {
29
29
  static type = "StorageInvalidLimitError";
30
30
  constructor(limit) {
31
- super(`Query limit must be greater than 0, got ${limit}`);
31
+ super(`Query limit must be a positive integer, got ${limit}`);
32
32
  }
33
33
  }
34
34
 
@@ -61,8 +61,214 @@ class CoveringIndexMissingError extends StorageError {
61
61
  }
62
62
  }
63
63
 
64
+ // src/tabular/Cursor.ts
65
+ var CURSOR_VERSION = 1;
66
+ var MAX_CURSOR_LENGTH = 8 * 1024;
67
+ function encodeCursor(payload) {
68
+ const json = JSON.stringify(payload);
69
+ let base64;
70
+ if (typeof Buffer !== "undefined") {
71
+ base64 = Buffer.from(json, "utf8").toString("base64");
72
+ } else {
73
+ const bytes = new TextEncoder().encode(json);
74
+ let binary = "";
75
+ const CHUNK = 32768;
76
+ for (let i = 0;i < bytes.length; i += CHUNK) {
77
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
78
+ }
79
+ base64 = btoa(binary);
80
+ }
81
+ let trimEnd = base64.length;
82
+ while (trimEnd > 0 && base64.charCodeAt(trimEnd - 1) === 61) {
83
+ trimEnd--;
84
+ }
85
+ const urlSafe = base64.slice(0, trimEnd).replace(/\+/g, "-").replace(/\//g, "_");
86
+ if (urlSafe.length > MAX_CURSOR_LENGTH) {
87
+ throw new StorageValidationError(`Encoded cursor exceeds maximum length (${urlSafe.length} > ${MAX_CURSOR_LENGTH})`);
88
+ }
89
+ return urlSafe;
90
+ }
91
+ function decodeCursor(cursor) {
92
+ if (typeof cursor !== "string" || cursor.length === 0) {
93
+ throw new StorageValidationError("Cursor must be a non-empty string");
94
+ }
95
+ if (cursor.length > MAX_CURSOR_LENGTH) {
96
+ throw new StorageValidationError(`Cursor exceeds maximum length (${cursor.length} > ${MAX_CURSOR_LENGTH})`);
97
+ }
98
+ const padded = cursor.replace(/-/g, "+").replace(/_/g, "/");
99
+ const padding = padded.length % 4 === 0 ? "" : "=".repeat(4 - padded.length % 4);
100
+ let json;
101
+ try {
102
+ if (typeof Buffer !== "undefined") {
103
+ json = Buffer.from(padded + padding, "base64").toString("utf8");
104
+ } else {
105
+ const binary = atob(padded + padding);
106
+ const bytes = new Uint8Array(binary.length);
107
+ for (let i = 0;i < binary.length; i++)
108
+ bytes[i] = binary.charCodeAt(i);
109
+ json = new TextDecoder().decode(bytes);
110
+ }
111
+ } catch {
112
+ throw new StorageValidationError("Cursor is not valid base64url");
113
+ }
114
+ let parsed;
115
+ try {
116
+ parsed = JSON.parse(json);
117
+ } catch {
118
+ throw new StorageValidationError("Cursor payload is not valid JSON");
119
+ }
120
+ const p = parsed;
121
+ if (!p || typeof p !== "object" || p.v !== CURSOR_VERSION || !Array.isArray(p.c) || !Array.isArray(p.n) || !Array.isArray(p.d) || p.n.length !== p.c.length || p.n.length !== p.d.length || !p.n.every((name) => typeof name === "string") || !p.d.every((dir) => dir === "a" || dir === "d") || !p.c.every((v) => v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean")) {
122
+ throw new StorageValidationError(`Cursor format is unsupported (expected v${CURSOR_VERSION})`);
123
+ }
124
+ return p;
125
+ }
126
+ function assertCursorMatches(payload, effectiveOrder) {
127
+ if (payload.n.length !== effectiveOrder.length) {
128
+ throw new StorageValidationError(`Cursor has ${payload.n.length} component(s); request expects ${effectiveOrder.length}`);
129
+ }
130
+ for (let i = 0;i < effectiveOrder.length; i++) {
131
+ if (payload.n[i] !== effectiveOrder[i].column) {
132
+ throw new StorageValidationError(`Cursor column ${i} is "${payload.n[i]}"; request expects "${effectiveOrder[i].column}"`);
133
+ }
134
+ const expected = effectiveOrder[i].direction === "ASC" ? "a" : "d";
135
+ if (payload.d[i] !== expected) {
136
+ throw new StorageValidationError(`Cursor column "${effectiveOrder[i].column}" was minted for ${payload.d[i] === "a" ? "ASC" : "DESC"}; request expects ${effectiveOrder[i].direction}`);
137
+ }
138
+ }
139
+ }
140
+ // src/migrations/MigrationRunner.ts
141
+ var MIGRATIONS_TABLE = "_storage_migrations";
142
+ function sortMigrations(migrations) {
143
+ return [...migrations].sort((a, b) => {
144
+ if (a.component !== b.component)
145
+ return a.component < b.component ? -1 : 1;
146
+ return a.version - b.version;
147
+ });
148
+ }
149
+ // src/migrations/TabularMigrationOrchestrator.ts
150
+ async function runTabularMigrations(applier, defaultComponent, migrations, options = {}) {
151
+ if (migrations.length === 0)
152
+ return;
153
+ await applier.ensureBookkeeping();
154
+ const byComponent = new Map;
155
+ for (const m of migrations) {
156
+ const c = m.component ?? defaultComponent;
157
+ let bucket = byComponent.get(c);
158
+ if (!bucket) {
159
+ bucket = [];
160
+ byComponent.set(c, bucket);
161
+ }
162
+ bucket.push(m);
163
+ }
164
+ for (const [component, group] of byComponent) {
165
+ const sorted = [...group].sort((a, b) => a.version - b.version);
166
+ const applied = await applier.appliedVersions(component);
167
+ const fresh = options.freshTable ?? !await applier.tableExists();
168
+ if (applied.size === 0 && fresh) {
169
+ await applier.markAllApplied(component, sorted.map((m) => ({ version: m.version, description: m.description })));
170
+ for (const m of sorted) {
171
+ options.onProgress?.({
172
+ component,
173
+ version: m.version,
174
+ phase: "completed",
175
+ description: m.description,
176
+ fraction: 1
177
+ });
178
+ }
179
+ continue;
180
+ }
181
+ for (const m of sorted) {
182
+ if (applied.has(m.version))
183
+ continue;
184
+ options.onProgress?.({
185
+ component,
186
+ version: m.version,
187
+ phase: "starting",
188
+ description: m.description
189
+ });
190
+ try {
191
+ await applier.applyMigration(component, m.version, m.description, m.ops, (fraction) => {
192
+ options.onProgress?.({
193
+ component,
194
+ version: m.version,
195
+ phase: "running",
196
+ description: m.description,
197
+ fraction
198
+ });
199
+ });
200
+ options.onProgress?.({
201
+ component,
202
+ version: m.version,
203
+ phase: "completed",
204
+ description: m.description,
205
+ fraction: 1
206
+ });
207
+ } catch (err) {
208
+ options.onProgress?.({
209
+ component,
210
+ version: m.version,
211
+ phase: "failed",
212
+ description: m.description,
213
+ error: err
214
+ });
215
+ throw err;
216
+ }
217
+ }
218
+ }
219
+ }
220
+ // src/migrations/runBackfill.ts
221
+ async function runBackfill(storage, batchSize, transform) {
222
+ let cursor;
223
+ while (true) {
224
+ const page = await storage.getPage({ limit: batchSize, cursor });
225
+ for (const row of page.items) {
226
+ const out = await transform(row);
227
+ if (out === row)
228
+ continue;
229
+ if (out === undefined) {
230
+ await storage.delete(row);
231
+ } else {
232
+ await storage.put(out);
233
+ }
234
+ }
235
+ if (!page.nextCursor)
236
+ break;
237
+ cursor = page.nextCursor;
238
+ }
239
+ }
64
240
  // src/tabular/BaseTabularStorage.ts
65
241
  var TABULAR_REPOSITORY = createServiceToken("storage.tabularRepository");
242
+ function toCursorValue(value) {
243
+ if (value === null || value === undefined)
244
+ return null;
245
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
246
+ return value;
247
+ }
248
+ if (value instanceof Date)
249
+ return value.toISOString();
250
+ if (typeof value === "bigint") {
251
+ throw new StorageValidationError("bigint values are not supported as cursor keys \u2014 string-encoded bigints don't sort numerically. Use a number column or override `getPage` for this storage.");
252
+ }
253
+ throw new StorageValidationError(`Cannot encode value of type ${typeof value} into a pagination cursor; use a key column with a primitive type.`);
254
+ }
255
+ function compareKeyValues(a, b) {
256
+ const an = toCursorValue(a);
257
+ const bn = toCursorValue(b);
258
+ if (an === null && bn === null)
259
+ return 0;
260
+ if (an === null)
261
+ return -1;
262
+ if (bn === null)
263
+ return 1;
264
+ const av = typeof an === "boolean" ? Number(an) : an;
265
+ const bv = typeof bn === "boolean" ? Number(bn) : bn;
266
+ if (av < bv)
267
+ return -1;
268
+ if (av > bv)
269
+ return 1;
270
+ return 0;
271
+ }
66
272
 
67
273
  class BaseTabularStorage {
68
274
  schema;
@@ -71,12 +277,18 @@ class BaseTabularStorage {
71
277
  indexes;
72
278
  primaryKeySchema;
73
279
  valueSchema;
280
+ tabularMigrations;
281
+ migrationComponent = "tabular:unnamed";
74
282
  autoGeneratedKeyName = null;
75
283
  autoGeneratedKeyStrategy = null;
76
284
  clientProvidedKeys;
77
- constructor(schema, primaryKeyNames, indexes = [], clientProvidedKeys = "if-missing") {
285
+ constructor(schema, primaryKeyNames, indexes = [], clientProvidedKeys = "if-missing", tabularMigrations, migrationName) {
78
286
  this.schema = schema;
79
287
  this.primaryKeyNames = primaryKeyNames;
288
+ this.tabularMigrations = tabularMigrations;
289
+ if (migrationName) {
290
+ this.migrationComponent = `tabular:${migrationName}`;
291
+ }
80
292
  this.clientProvidedKeys = clientProvidedKeys;
81
293
  const primaryKeyProps = {};
82
294
  const valueProps = {};
@@ -197,37 +409,160 @@ class BaseTabularStorage {
197
409
  if (pageSize <= 0) {
198
410
  throw new RangeError(`pageSize must be greater than 0, got ${pageSize}`);
199
411
  }
200
- let offset = 0;
412
+ let cursor;
201
413
  while (true) {
202
- const page = await this.getBulk(offset, pageSize);
203
- if (!page || page.length === 0) {
204
- break;
205
- }
206
- for (const entity of page) {
414
+ const page = await this.getPage({ limit: pageSize, cursor });
415
+ for (const entity of page.items) {
207
416
  yield entity;
208
417
  }
209
- if (page.length < pageSize) {
418
+ if (!page.nextCursor || page.items.length === 0)
210
419
  break;
211
- }
212
- offset += pageSize;
420
+ cursor = page.nextCursor;
213
421
  }
214
422
  }
215
423
  async* pages(pageSize = 100) {
216
424
  if (pageSize <= 0) {
217
425
  throw new RangeError(`pageSize must be greater than 0, got ${pageSize}`);
218
426
  }
219
- let offset = 0;
427
+ let cursor;
220
428
  while (true) {
221
- const page = await this.getBulk(offset, pageSize);
222
- if (!page || page.length === 0) {
223
- break;
429
+ const page = await this.getPage({ limit: pageSize, cursor });
430
+ if (page.items.length > 0) {
431
+ yield page.items.slice();
224
432
  }
225
- yield page;
226
- if (page.length < pageSize) {
433
+ if (!page.nextCursor || page.items.length === 0)
227
434
  break;
435
+ cursor = page.nextCursor;
436
+ }
437
+ }
438
+ async getPage(request = {}) {
439
+ return this.runPage(undefined, request);
440
+ }
441
+ async queryPage(criteria, request = {}) {
442
+ return this.runPage(criteria, request);
443
+ }
444
+ async runPage(criteria, request) {
445
+ this.validatePageRequest(request);
446
+ const limit = request.limit ?? 100;
447
+ const pkColumns = this.primaryKeyColumns();
448
+ const orderBy = request.orderBy;
449
+ const effectiveOrderBy = this.buildEffectiveOrderBy(orderBy, pkColumns);
450
+ const effectiveOrderForCursor = effectiveOrderBy.map((o) => ({
451
+ column: String(o.column),
452
+ direction: o.direction
453
+ }));
454
+ let cursorPayload;
455
+ if (request.cursor !== undefined) {
456
+ cursorPayload = decodeCursor(request.cursor);
457
+ assertCursorMatches(cursorPayload, effectiveOrderForCursor);
458
+ }
459
+ const pkCol = pkColumns[0];
460
+ const userCriteria = criteria ?? {};
461
+ const userTouchesPk = pkColumns.length === 1 && Object.prototype.hasOwnProperty.call(userCriteria, pkCol);
462
+ const canPushKeyset = pkColumns.length === 1 && effectiveOrderBy.length === 1 && effectiveOrderBy[0].column === pkCol && !userTouchesPk;
463
+ let queryCriteria = userCriteria;
464
+ const useFallback = !canPushKeyset;
465
+ if (cursorPayload && canPushKeyset) {
466
+ const direction = effectiveOrderBy[0].direction;
467
+ const op = direction === "ASC" ? ">" : "<";
468
+ const lastPk = cursorPayload.c[0];
469
+ const keysetCondition = {
470
+ value: lastPk,
471
+ operator: op
472
+ };
473
+ queryCriteria = {
474
+ ...userCriteria,
475
+ [pkCol]: keysetCondition
476
+ };
477
+ } else if (cursorPayload && useFallback) {
478
+ const leading = effectiveOrderBy[0];
479
+ const leadingCol = leading.column;
480
+ const leadingCursor = cursorPayload.c[0];
481
+ const userTouchesLeading = Object.prototype.hasOwnProperty.call(userCriteria, leadingCol);
482
+ if (leading.direction === "ASC" && leadingCursor !== null && !userTouchesLeading) {
483
+ const leadingCondition = {
484
+ value: leadingCursor,
485
+ operator: ">="
486
+ };
487
+ queryCriteria = {
488
+ ...userCriteria,
489
+ [leadingCol]: leadingCondition
490
+ };
491
+ }
492
+ }
493
+ const fetchLimit = useFallback ? undefined : limit;
494
+ const queryOptions = {
495
+ orderBy: effectiveOrderBy,
496
+ ...fetchLimit !== undefined ? { limit: fetchLimit } : {}
497
+ };
498
+ let rows;
499
+ let forcedFallback = false;
500
+ if (Object.keys(queryCriteria).length === 0) {
501
+ rows = await this.getAll(queryOptions);
502
+ } else {
503
+ try {
504
+ rows = await this.query(queryCriteria, queryOptions);
505
+ } catch (err) {
506
+ const userHadNoCriteria = !criteria || Object.keys(criteria).length === 0;
507
+ if (err instanceof StorageUnsupportedError && userHadNoCriteria) {
508
+ rows = await this.getAll({ orderBy: effectiveOrderBy });
509
+ forcedFallback = true;
510
+ } else {
511
+ throw err;
512
+ }
513
+ }
514
+ }
515
+ let items = rows ?? [];
516
+ if (useFallback || forcedFallback) {
517
+ items = this.sortInMemory(items.slice(), effectiveOrderBy);
518
+ if (cursorPayload) {
519
+ items = this.applyKeysetFilter(items, cursorPayload, effectiveOrderBy, pkColumns);
520
+ }
521
+ }
522
+ if (items.length > limit) {
523
+ items = items.slice(0, limit);
524
+ }
525
+ const nextCursor = items.length === limit ? this.buildCursor(items[items.length - 1], effectiveOrderBy) : undefined;
526
+ return { items, nextCursor };
527
+ }
528
+ buildEffectiveOrderBy(orderBy, pkColumns) {
529
+ const result = orderBy ? orderBy.slice() : [];
530
+ const seen = new Set(result.map((o) => o.column));
531
+ for (const pk of pkColumns) {
532
+ if (!seen.has(pk)) {
533
+ result.push({ column: pk, direction: "ASC" });
228
534
  }
229
- offset += pageSize;
230
535
  }
536
+ return result;
537
+ }
538
+ sortInMemory(rows, orderBy) {
539
+ rows.sort((a, b) => {
540
+ for (const { column, direction } of orderBy) {
541
+ const cmp = compareKeyValues(a[column], b[column]);
542
+ if (cmp !== 0)
543
+ return direction === "ASC" ? cmp : -cmp;
544
+ }
545
+ return 0;
546
+ });
547
+ return rows;
548
+ }
549
+ applyKeysetFilter(rows, cursor, effectiveOrderBy, _pkColumns) {
550
+ return rows.filter((row) => {
551
+ for (let i = 0;i < effectiveOrderBy.length; i++) {
552
+ const { column, direction } = effectiveOrderBy[i];
553
+ const cmp = compareKeyValues(row[column], cursor.c[i]);
554
+ if (cmp === 0)
555
+ continue;
556
+ return direction === "ASC" ? cmp > 0 : cmp < 0;
557
+ }
558
+ return false;
559
+ });
560
+ }
561
+ buildCursor(row, effectiveOrderBy) {
562
+ const n = effectiveOrderBy.map((spec) => String(spec.column));
563
+ const d = effectiveOrderBy.map((spec) => spec.direction === "ASC" ? "a" : "d");
564
+ const c = effectiveOrderBy.map((spec) => toCursorValue(row[spec.column]));
565
+ return encodeCursor({ v: 1, n, d, c });
231
566
  }
232
567
  subscribeToChanges(_callback, _options) {
233
568
  throw new Error(`subscribeToChanges is not implemented for ${this.constructor.name}. ` + `All concrete repository implementations should override this method.`);
@@ -255,17 +590,7 @@ class BaseTabularStorage {
255
590
  }
256
591
  }
257
592
  }
258
- if (options?.orderBy) {
259
- const validDirections = ["ASC", "DESC"];
260
- for (const { column, direction } of options.orderBy) {
261
- if (!(column in this.schema.properties)) {
262
- throw new StorageInvalidColumnError(String(column));
263
- }
264
- if (!validDirections.includes(direction)) {
265
- throw new StorageValidationError(`Invalid sort direction "${direction}". Must be "ASC" or "DESC"`);
266
- }
267
- }
268
- }
593
+ this.validateOrderBy(options?.orderBy);
269
594
  }
270
595
  validateGetAllOptions(options) {
271
596
  if (!options)
@@ -276,17 +601,31 @@ class BaseTabularStorage {
276
601
  if (options.offset !== undefined && options.offset < 0) {
277
602
  throw new StorageValidationError(`Query offset must be non-negative, got ${options.offset}`);
278
603
  }
279
- if (options.orderBy) {
280
- const validDirections = ["ASC", "DESC"];
281
- for (const { column, direction } of options.orderBy) {
282
- if (!(column in this.schema.properties)) {
283
- throw new StorageInvalidColumnError(String(column));
284
- }
285
- if (!validDirections.includes(direction)) {
286
- throw new StorageValidationError(`Invalid sort direction "${direction}". Must be "ASC" or "DESC"`);
287
- }
604
+ this.validateOrderBy(options.orderBy);
605
+ }
606
+ validateOrderBy(orderBy) {
607
+ if (!orderBy)
608
+ return;
609
+ const validDirections = ["ASC", "DESC"];
610
+ for (const { column, direction } of orderBy) {
611
+ if (typeof column !== "string") {
612
+ throw new StorageInvalidColumnError(String(column));
613
+ }
614
+ if (!(column in this.schema.properties)) {
615
+ throw new StorageInvalidColumnError(String(column));
616
+ }
617
+ if (!validDirections.includes(direction)) {
618
+ throw new StorageValidationError(`Invalid sort direction "${direction}". Must be "ASC" or "DESC"`);
619
+ }
620
+ }
621
+ }
622
+ validatePageRequest(request) {
623
+ if (request.limit !== undefined) {
624
+ if (!Number.isInteger(request.limit) || request.limit <= 0) {
625
+ throw new StorageInvalidLimitError(request.limit);
288
626
  }
289
627
  }
628
+ this.validateOrderBy(request.orderBy);
290
629
  }
291
630
  validateSelect(options) {
292
631
  if (!options.select || options.select.length === 0) {
@@ -406,6 +745,21 @@ class BaseTabularStorage {
406
745
  generateKeyValue(columnName, strategy) {
407
746
  throw new Error(`generateKeyValue not implemented for ${this.constructor.name}. ` + `Column: ${columnName}, Strategy: ${strategy}`);
408
747
  }
748
+ async withTransaction(fn) {
749
+ return await fn(this);
750
+ }
751
+ getMigrationApplier() {
752
+ return null;
753
+ }
754
+ async applyTabularMigrations(options) {
755
+ if (!this.tabularMigrations || this.tabularMigrations.length === 0)
756
+ return;
757
+ const applier = this.getMigrationApplier();
758
+ if (!applier) {
759
+ throw new Error(`${this.constructor.name} declared migrations but has no migration applier wired up.`);
760
+ }
761
+ await runTabularMigrations(applier, this.migrationComponent, this.tabularMigrations, options);
762
+ }
409
763
  async setupDatabase() {}
410
764
  destroy() {}
411
765
  async[Symbol.asyncDispose]() {
@@ -422,8 +776,8 @@ class BaseSqlTabularStorage extends BaseTabularStorage {
422
776
  _valColsCache = new Map;
423
777
  _pkColListCache = new Map;
424
778
  _valColListCache = new Map;
425
- constructor(table = "tabular_store", schema, primaryKeyNames, indexes = [], clientProvidedKeys = "if-missing") {
426
- super(schema, primaryKeyNames, indexes, clientProvidedKeys);
779
+ constructor(table = "tabular_store", schema, primaryKeyNames, indexes = [], clientProvidedKeys = "if-missing", tabularMigrations, migrationName) {
780
+ super(schema, primaryKeyNames, indexes, clientProvidedKeys, tabularMigrations, migrationName ?? table);
427
781
  this.table = table;
428
782
  this.validateTableAndSchema();
429
783
  }
@@ -613,6 +967,90 @@ class BaseSqlTabularStorage extends BaseTabularStorage {
613
967
  throw new Error(`Duplicate keys found in schemas: ${duplicates.join(", ")}`);
614
968
  }
615
969
  }
970
+ buildKeysetWhere(effectiveOrderBy, cursorValues, quote, placeholder, startIndex) {
971
+ if (effectiveOrderBy.length !== cursorValues.length) {
972
+ throw new Error(`Keyset arity mismatch: ${effectiveOrderBy.length} order columns vs ${cursorValues.length} cursor values`);
973
+ }
974
+ const clauses = [];
975
+ const params = [];
976
+ let idx = startIndex;
977
+ for (let i = 0;i < effectiveOrderBy.length; i++) {
978
+ const parts = [];
979
+ for (let j = 0;j < i; j++) {
980
+ const colExpr2 = `${quote}${String(effectiveOrderBy[j].column)}${quote}`;
981
+ if (cursorValues[j] === null) {
982
+ parts.push(`${colExpr2} IS NULL`);
983
+ } else {
984
+ parts.push(`${colExpr2} = ${placeholder(idx)}`);
985
+ params.push(cursorValues[j]);
986
+ idx++;
987
+ }
988
+ }
989
+ const colExpr = `${quote}${String(effectiveOrderBy[i].column)}${quote}`;
990
+ const v = cursorValues[i];
991
+ const dir = effectiveOrderBy[i].direction;
992
+ if (v === null) {
993
+ parts.push(dir === "ASC" ? `${colExpr} IS NOT NULL` : `1 = 0`);
994
+ } else {
995
+ if (dir === "ASC") {
996
+ parts.push(`${colExpr} > ${placeholder(idx)}`);
997
+ } else {
998
+ parts.push(`(${colExpr} < ${placeholder(idx)} OR ${colExpr} IS NULL)`);
999
+ }
1000
+ params.push(v);
1001
+ idx++;
1002
+ }
1003
+ clauses.push(`(${parts.join(" AND ")})`);
1004
+ }
1005
+ return { whereClause: clauses.join(" OR "), params, nextIndex: idx };
1006
+ }
1007
+ async runSqlPage(criteria, request, dialect) {
1008
+ this.validatePageRequest(request);
1009
+ const limit = request.limit ?? 100;
1010
+ const pkColumns = this.primaryKeyColumns();
1011
+ const orderBy = request.orderBy;
1012
+ const effectiveOrderBy = this.buildEffectiveOrderBy(orderBy, pkColumns);
1013
+ const effectiveOrderForCursor = effectiveOrderBy.map((o) => ({
1014
+ column: String(o.column),
1015
+ direction: o.direction
1016
+ }));
1017
+ let cursorPayload;
1018
+ if (request.cursor !== undefined) {
1019
+ cursorPayload = decodeCursor(request.cursor);
1020
+ assertCursorMatches(cursorPayload, effectiveOrderForCursor);
1021
+ }
1022
+ const params = [];
1023
+ let paramIdx = 1;
1024
+ const whereClauses = [];
1025
+ if (criteria && Object.keys(criteria).length > 0) {
1026
+ const built = dialect.buildSearchWhere(criteria, paramIdx);
1027
+ whereClauses.push(built.whereClause);
1028
+ params.push(...built.params);
1029
+ paramIdx = built.nextIndex;
1030
+ }
1031
+ if (cursorPayload) {
1032
+ const cursorValues = cursorPayload.c.map((v, i) => this.jsToSqlValue(String(effectiveOrderBy[i].column), v));
1033
+ const built = this.buildKeysetWhere(effectiveOrderBy, cursorValues, dialect.quote, dialect.placeholder, paramIdx);
1034
+ whereClauses.push(`(${built.whereClause})`);
1035
+ params.push(...built.params);
1036
+ paramIdx = built.nextIndex;
1037
+ }
1038
+ const q = dialect.quote;
1039
+ const orderByClause = effectiveOrderBy.map((o) => {
1040
+ const nulls = o.direction === "ASC" ? "NULLS FIRST" : "NULLS LAST";
1041
+ return `${q}${String(o.column)}${q} ${o.direction} ${nulls}`;
1042
+ }).join(", ");
1043
+ let sql = `SELECT * FROM ${q}${this.table}${q}`;
1044
+ if (whereClauses.length > 0) {
1045
+ sql += ` WHERE ${whereClauses.join(" AND ")}`;
1046
+ }
1047
+ sql += ` ORDER BY ${orderByClause}`;
1048
+ sql += ` LIMIT ${dialect.placeholder(paramIdx)}`;
1049
+ params.push(limit);
1050
+ const items = await dialect.executeSelect(sql, params);
1051
+ const nextCursor = items.length === limit ? this.buildCursor(items[items.length - 1], effectiveOrderBy) : undefined;
1052
+ return { items, nextCursor };
1053
+ }
616
1054
  }
617
1055
  // src/tabular/CachedTabularStorage.ts
618
1056
  import { createServiceToken as createServiceToken3, getLogger } from "@workglow/util";
@@ -620,6 +1058,55 @@ import { createServiceToken as createServiceToken3, getLogger } from "@workglow/
620
1058
  // src/tabular/InMemoryTabularStorage.ts
621
1059
  import { createServiceToken as createServiceToken2, makeFingerprint as makeFingerprint2, uuid4 } from "@workglow/util";
622
1060
 
1061
+ // src/tabular/InMemoryTabularMigrationApplier.ts
1062
+ class InMemoryTabularMigrationApplier {
1063
+ storage;
1064
+ storeName;
1065
+ applied = new Map;
1066
+ constructor(storage, storeName) {
1067
+ this.storage = storage;
1068
+ this.storeName = storeName;
1069
+ }
1070
+ async ensureBookkeeping() {}
1071
+ async appliedVersions(component) {
1072
+ return new Set(this.applied.get(component) ?? []);
1073
+ }
1074
+ async tableExists() {
1075
+ return await this.storage.size() > 0;
1076
+ }
1077
+ async markAllApplied(component, versions) {
1078
+ if (versions.length === 0)
1079
+ return;
1080
+ let set = this.applied.get(component);
1081
+ if (!set) {
1082
+ set = new Set;
1083
+ this.applied.set(component, set);
1084
+ }
1085
+ for (const v of versions)
1086
+ set.add(v.version);
1087
+ await this.persist();
1088
+ }
1089
+ async applyMigration(component, version, _description, ops, onProgress) {
1090
+ let processed = 0;
1091
+ const total = Math.max(ops.length, 1);
1092
+ for (const op of ops) {
1093
+ if (op.kind === "backfill") {
1094
+ await runBackfill(this.storage, op.batchSize ?? 500, op.transform);
1095
+ }
1096
+ processed++;
1097
+ onProgress?.(processed / total);
1098
+ }
1099
+ let set = this.applied.get(component);
1100
+ if (!set) {
1101
+ set = new Set;
1102
+ this.applied.set(component, set);
1103
+ }
1104
+ set.add(version);
1105
+ await this.persist();
1106
+ }
1107
+ async persist() {}
1108
+ }
1109
+
623
1110
  // src/tabular/coveringIndexPicker.ts
624
1111
  function pickCoveringIndex(input) {
625
1112
  const { table, indexes, criteriaColumns, orderByColumns, selectColumns, primaryKeyColumns } = input;
@@ -680,10 +1167,17 @@ class InMemoryTabularStorage extends BaseTabularStorage {
680
1167
  values = new Map;
681
1168
  autoIncrementCounter = 0;
682
1169
  _lastPutWasInsert = false;
683
- constructor(schema, primaryKeyNames, indexes = [], clientProvidedKeys = "if-missing") {
684
- super(schema, primaryKeyNames, indexes, clientProvidedKeys);
1170
+ constructor(schema, primaryKeyNames, indexes = [], clientProvidedKeys = "if-missing", tabularMigrations, migrationName = "inmemory") {
1171
+ super(schema, primaryKeyNames, indexes, clientProvidedKeys, tabularMigrations, migrationName);
1172
+ }
1173
+ async setupDatabase() {
1174
+ if (this.tabularMigrations && this.tabularMigrations.length > 0) {
1175
+ await this.applyTabularMigrations();
1176
+ }
1177
+ }
1178
+ getMigrationApplier() {
1179
+ return new InMemoryTabularMigrationApplier(this, "inmemory");
685
1180
  }
686
- async setupDatabase() {}
687
1181
  generateKeyValue(columnName, strategy) {
688
1182
  if (strategy === "autoincrement") {
689
1183
  return ++this.autoIncrementCounter;
@@ -1065,6 +1559,7 @@ class CachedTabularStorage extends BaseTabularStorage {
1065
1559
  await this.cache.deleteAll();
1066
1560
  }
1067
1561
  async getAll(options) {
1562
+ this.validateGetAllOptions(options);
1068
1563
  await this.initializeCache();
1069
1564
  let results = await this.cache.getAll();
1070
1565
  if (!results || results.length === 0) {
@@ -1099,6 +1594,14 @@ class CachedTabularStorage extends BaseTabularStorage {
1099
1594
  await this.durable.deleteSearch(criteria);
1100
1595
  await this.cache.deleteSearch(criteria);
1101
1596
  }
1597
+ async withTransaction(fn) {
1598
+ await this.initializeCache();
1599
+ try {
1600
+ return await this.durable.withTransaction(fn);
1601
+ } finally {
1602
+ await this.invalidateCache();
1603
+ }
1604
+ }
1102
1605
  async invalidateCache() {
1103
1606
  await this.cache.deleteAll();
1104
1607
  this.cacheInitialized = false;
@@ -1122,6 +1625,14 @@ class CachedTabularStorage extends BaseTabularStorage {
1122
1625
  callback(change);
1123
1626
  }, options);
1124
1627
  }
1628
+ async setupDatabase() {
1629
+ await this.durable.setupDatabase();
1630
+ await this.cache.setupDatabase();
1631
+ }
1632
+ getMigrationApplier() {
1633
+ const inner = this.durable;
1634
+ return inner.getMigrationApplier?.() ?? null;
1635
+ }
1125
1636
  destroy() {
1126
1637
  this.durable.destroy();
1127
1638
  this.cache.destroy();
@@ -1130,6 +1641,21 @@ class CachedTabularStorage extends BaseTabularStorage {
1130
1641
  // src/tabular/HuggingFaceTabularStorage.ts
1131
1642
  import { createServiceToken as createServiceToken4 } from "@workglow/util";
1132
1643
  var HF_TABULAR_REPOSITORY = createServiceToken4("storage.tabularRepository.huggingface");
1644
+ var HF_OFFSET_CURSOR_NAME = "hfOffset";
1645
+ function encodeOffsetCursor(offset) {
1646
+ return encodeCursor({ v: 1, n: [HF_OFFSET_CURSOR_NAME], d: ["a"], c: [offset] });
1647
+ }
1648
+ function decodeOffsetCursor(cursor) {
1649
+ const payload = decodeCursor(cursor);
1650
+ if (payload.n[0] !== HF_OFFSET_CURSOR_NAME) {
1651
+ throw new StorageValidationError("Cursor was not produced by HuggingFaceTabularStorage");
1652
+ }
1653
+ const value = payload.c[0];
1654
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
1655
+ throw new StorageValidationError("Invalid HuggingFace pagination cursor");
1656
+ }
1657
+ return value;
1658
+ }
1133
1659
 
1134
1660
  class HuggingFaceTabularStorage extends BaseTabularStorage {
1135
1661
  dataset;
@@ -1137,8 +1663,8 @@ class HuggingFaceTabularStorage extends BaseTabularStorage {
1137
1663
  split;
1138
1664
  token;
1139
1665
  baseUrl;
1140
- constructor(dataset, config, split, schema, primaryKeyNames, options) {
1141
- super(schema, primaryKeyNames, options?.indexes ?? [], "never");
1666
+ constructor(dataset, config, split, schema, primaryKeyNames, options, tabularMigrations) {
1667
+ super(schema, primaryKeyNames, options?.indexes ?? [], "never", tabularMigrations, `hf:${dataset}/${config}/${split}`);
1142
1668
  this.dataset = dataset;
1143
1669
  this.config = config;
1144
1670
  this.split = split;
@@ -1190,6 +1716,12 @@ class HuggingFaceTabularStorage extends BaseTabularStorage {
1190
1716
  }
1191
1717
  }
1192
1718
  }
1719
+ if (this.tabularMigrations && this.tabularMigrations.length > 0) {
1720
+ await this.applyTabularMigrations();
1721
+ }
1722
+ }
1723
+ getMigrationApplier() {
1724
+ return new InMemoryTabularMigrationApplier(this, `hf:${this.dataset}/${this.config}/${this.split}`);
1193
1725
  }
1194
1726
  async get(key) {
1195
1727
  const keyObj = this.separateKeyValueFromCombined({ ...key }).key;
@@ -1273,6 +1805,34 @@ class HuggingFaceTabularStorage extends BaseTabularStorage {
1273
1805
  }
1274
1806
  return entities;
1275
1807
  }
1808
+ async getPage(request = {}) {
1809
+ this.validatePageRequest(request);
1810
+ const limit = request.limit ?? 100;
1811
+ if (request.orderBy && request.orderBy.length > 0) {
1812
+ throw new StorageUnsupportedError("orderBy in getPage", "HuggingFaceTabularStorage");
1813
+ }
1814
+ const HF_PAGE_CAP = 100;
1815
+ let offset = request.cursor ? decodeOffsetCursor(request.cursor) : 0;
1816
+ const items = [];
1817
+ let endOfDataset = false;
1818
+ while (items.length < limit) {
1819
+ const remaining = limit - items.length;
1820
+ const chunkSize = Math.min(remaining, HF_PAGE_CAP);
1821
+ const rows = await this.getBulk(offset, chunkSize) ?? [];
1822
+ if (rows.length === 0) {
1823
+ endOfDataset = true;
1824
+ break;
1825
+ }
1826
+ items.push(...rows);
1827
+ offset += rows.length;
1828
+ if (rows.length < chunkSize) {
1829
+ endOfDataset = true;
1830
+ break;
1831
+ }
1832
+ }
1833
+ const nextCursor = endOfDataset ? undefined : encodeOffsetCursor(offset);
1834
+ return { items, nextCursor };
1835
+ }
1276
1836
  async size() {
1277
1837
  const data = await this.fetchApi("/size", {});
1278
1838
  return data.size.num_rows;
@@ -1444,34 +2004,190 @@ import {
1444
2004
  registerInputResolver
1445
2005
  } from "@workglow/util";
1446
2006
  var TABULAR_REPOSITORIES = createServiceToken5("storage.tabular.repositories");
1447
- globalServiceRegistry.registerIfAbsent(TABULAR_REPOSITORIES, () => new Map, true);
1448
- function getGlobalTabularRepositories() {
1449
- return globalServiceRegistry.get(TABULAR_REPOSITORIES);
2007
+ function getGlobalTabularRepositories(registry = globalServiceRegistry) {
2008
+ if (!registry.has(TABULAR_REPOSITORIES)) {
2009
+ registerTabularStorageDefaults(registry);
2010
+ }
2011
+ return registry.get(TABULAR_REPOSITORIES);
1450
2012
  }
1451
- function registerTabularRepository(id, repository) {
1452
- const repos = getGlobalTabularRepositories();
2013
+ function registerTabularRepository(id, repository, registry = globalServiceRegistry) {
2014
+ const repos = getGlobalTabularRepositories(registry);
1453
2015
  repos.set(id, repository);
1454
2016
  }
1455
- function getTabularRepository(id) {
1456
- return getGlobalTabularRepositories().get(id);
2017
+ function getTabularRepository(id, registry = globalServiceRegistry) {
2018
+ return getGlobalTabularRepositories(registry).get(id);
1457
2019
  }
1458
- function resolveRepositoryFromRegistry(id, format, registry) {
1459
- const repos = registry.has(TABULAR_REPOSITORIES) ? registry.get(TABULAR_REPOSITORIES) : getGlobalTabularRepositories();
2020
+ function resolveRepositoryFromRegistry(id, _format, registry) {
2021
+ const repos = getGlobalTabularRepositories(registry);
1460
2022
  const repo = repos.get(id);
1461
2023
  if (!repo) {
1462
2024
  throw new Error(`Tabular storage "${id}" not found in registry`);
1463
2025
  }
1464
2026
  return repo;
1465
2027
  }
1466
- registerInputResolver("storage:tabular", resolveRepositoryFromRegistry);
1467
- registerInputCompactor("storage:tabular", (value, _format, registry) => {
1468
- const repos = registry.has(TABULAR_REPOSITORIES) ? registry.get(TABULAR_REPOSITORIES) : getGlobalTabularRepositories();
2028
+ function compactTabularRepository(value, _format, registry) {
2029
+ const repos = getGlobalTabularRepositories(registry);
1469
2030
  for (const [id, repo] of repos) {
1470
2031
  if (repo === value)
1471
2032
  return id;
1472
2033
  }
1473
2034
  return;
1474
- });
2035
+ }
2036
+ function registerTabularStorageDefaults(registry = globalServiceRegistry) {
2037
+ registry.registerIfAbsent(TABULAR_REPOSITORIES, () => new Map, true);
2038
+ registerInputResolver("storage:tabular", resolveRepositoryFromRegistry, registry);
2039
+ registerInputCompactor("storage:tabular", compactTabularRepository, registry);
2040
+ }
2041
+ registerTabularStorageDefaults();
2042
+ // src/sql/Dialect.ts
2043
+ var SqliteDialect = {
2044
+ name: "sqlite",
2045
+ quoteId(id) {
2046
+ return "`" + id.replace(/`/g, "``") + "`";
2047
+ },
2048
+ placeholder(_index) {
2049
+ return "?";
2050
+ }
2051
+ };
2052
+ var PostgresDialect = {
2053
+ name: "postgres",
2054
+ quoteId(id) {
2055
+ return '"' + id.replace(/"/g, '""') + '"';
2056
+ },
2057
+ placeholder(index) {
2058
+ return `$${index}`;
2059
+ }
2060
+ };
2061
+
2062
+ // src/tabular/sqlMigrationDdl.ts
2063
+ function selectDialect(name) {
2064
+ return name === "sqlite" ? SqliteDialect : PostgresDialect;
2065
+ }
2066
+ function buildAddColumnSql(dialect, table, column, sqlType, nullable, hasDefault = false, defaultLiteralSql) {
2067
+ const d = selectDialect(dialect);
2068
+ let sql = `ALTER TABLE ${d.quoteId(table)} ADD COLUMN ${d.quoteId(column)} ${sqlType}`;
2069
+ if (!nullable)
2070
+ sql += " NOT NULL";
2071
+ if (hasDefault && defaultLiteralSql !== undefined) {
2072
+ sql += ` DEFAULT ${defaultLiteralSql}`;
2073
+ }
2074
+ return sql;
2075
+ }
2076
+ function buildDropColumnSql(dialect, table, column) {
2077
+ const d = selectDialect(dialect);
2078
+ return `ALTER TABLE ${d.quoteId(table)} DROP COLUMN ${d.quoteId(column)}`;
2079
+ }
2080
+ function buildRenameColumnSql(dialect, table, from, to) {
2081
+ const d = selectDialect(dialect);
2082
+ return `ALTER TABLE ${d.quoteId(table)} RENAME COLUMN ${d.quoteId(from)} TO ${d.quoteId(to)}`;
2083
+ }
2084
+ function buildAddIndexSql(dialect, table, indexName, columns, unique) {
2085
+ const d = selectDialect(dialect);
2086
+ const cols = columns.map((c) => d.quoteId(c)).join(", ");
2087
+ return `CREATE ${unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ` + `${d.quoteId(indexName)} ON ${d.quoteId(table)} (${cols})`;
2088
+ }
2089
+ function buildDropIndexSql(dialect, indexName) {
2090
+ const d = selectDialect(dialect);
2091
+ return `DROP INDEX IF EXISTS ${d.quoteId(indexName)}`;
2092
+ }
2093
+ // src/tabular/SqlTabularMigrationApplier.ts
2094
+ class SqlTabularMigrationApplier {
2095
+ async ensureBookkeeping() {
2096
+ await this.executeSql(this.bookkeepingDdl());
2097
+ }
2098
+ async appliedVersions(component) {
2099
+ return this.queryAppliedVersions(component);
2100
+ }
2101
+ async tableExists() {
2102
+ return this.probeTableExists();
2103
+ }
2104
+ async markAllApplied(component, versions) {
2105
+ if (versions.length === 0)
2106
+ return;
2107
+ for (const v of versions) {
2108
+ await this.recordApplied(component, v.version, v.description);
2109
+ }
2110
+ }
2111
+ async applyMigration(component, version, description, ops, onProgress) {
2112
+ const storage = this.storage();
2113
+ await storage.withTransaction(async (tx) => {
2114
+ let processed = 0;
2115
+ const total = Math.max(ops.length, 1);
2116
+ for (const op of ops) {
2117
+ await this.applyOp(op, tx);
2118
+ processed++;
2119
+ onProgress?.(processed / total);
2120
+ }
2121
+ await this.recordAppliedTx(component, version, description, tx);
2122
+ });
2123
+ }
2124
+ async applyOp(op, tx) {
2125
+ switch (op.kind) {
2126
+ case "addColumn": {
2127
+ const sqlType = this.mapTypeToSQL(op.schema);
2128
+ const nullable = this.isNullableSchema(op.schema);
2129
+ const hasDefault = op.default !== undefined;
2130
+ const sql = buildAddColumnSql(this.dialectName(), this.table(), op.name, sqlType, nullable, hasDefault, hasDefault ? this.literalSql(op.default) : undefined);
2131
+ await this.executeSqlTx(sql, tx);
2132
+ return;
2133
+ }
2134
+ case "dropColumn": {
2135
+ await this.executeSqlTx(buildDropColumnSql(this.dialectName(), this.table(), op.name), tx);
2136
+ return;
2137
+ }
2138
+ case "renameColumn": {
2139
+ await this.executeSqlTx(buildRenameColumnSql(this.dialectName(), this.table(), op.from, op.to), tx);
2140
+ return;
2141
+ }
2142
+ case "addIndex": {
2143
+ await this.executeSqlTx(buildAddIndexSql(this.dialectName(), this.table(), op.name, op.columns, op.unique ?? false), tx);
2144
+ return;
2145
+ }
2146
+ case "dropIndex": {
2147
+ await this.executeSqlTx(buildDropIndexSql(this.dialectName(), op.name), tx);
2148
+ return;
2149
+ }
2150
+ case "backfill": {
2151
+ await runBackfill(tx, op.batchSize ?? 500, op.transform);
2152
+ return;
2153
+ }
2154
+ }
2155
+ }
2156
+ literalSql(value) {
2157
+ if (value === null)
2158
+ return "NULL";
2159
+ if (typeof value === "string")
2160
+ return `'${value.replace(/'/g, "''")}'`;
2161
+ if (typeof value === "number") {
2162
+ if (!Number.isFinite(value)) {
2163
+ throw new Error(`Unsupported numeric default for tabular migration: ${value} (must be finite)`);
2164
+ }
2165
+ return String(value);
2166
+ }
2167
+ if (typeof value === "boolean") {
2168
+ return this.dialectName() === "sqlite" ? value ? "1" : "0" : value ? "TRUE" : "FALSE";
2169
+ }
2170
+ throw new Error(`Unsupported default value for tabular migration: ${typeof value} (${String(value)})`);
2171
+ }
2172
+ bookkeepingDdl() {
2173
+ if (this.dialectName() === "sqlite") {
2174
+ return `CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
2175
+ component TEXT NOT NULL,
2176
+ version INTEGER NOT NULL,
2177
+ description TEXT,
2178
+ applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
2179
+ PRIMARY KEY (component, version)
2180
+ )`;
2181
+ }
2182
+ return `CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
2183
+ component TEXT NOT NULL,
2184
+ version INTEGER NOT NULL,
2185
+ description TEXT,
2186
+ applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
2187
+ PRIMARY KEY (component, version)
2188
+ )`;
2189
+ }
2190
+ }
1475
2191
  // src/tabular/TelemetryTabularStorage.ts
1476
2192
  import { traced } from "@workglow/util";
1477
2193
 
@@ -1512,6 +2228,12 @@ class TelemetryTabularStorage {
1512
2228
  getBulk(offset, limit) {
1513
2229
  return traced("workglow.storage.tabular.getBulk", this.storageName, () => this.inner.getBulk(offset, limit));
1514
2230
  }
2231
+ getPage(request) {
2232
+ return traced("workglow.storage.tabular.getPage", this.storageName, () => this.inner.getPage(request));
2233
+ }
2234
+ queryPage(criteria, request) {
2235
+ return traced("workglow.storage.tabular.queryPage", this.storageName, () => this.inner.queryPage(criteria, request));
2236
+ }
1515
2237
  query(criteria, options) {
1516
2238
  return traced("workglow.storage.tabular.query", this.storageName, () => this.inner.query(criteria, options));
1517
2239
  }
@@ -1527,9 +2249,19 @@ class TelemetryTabularStorage {
1527
2249
  subscribeToChanges(callback, options) {
1528
2250
  return this.inner.subscribeToChanges(callback, options);
1529
2251
  }
2252
+ withTransaction(fn) {
2253
+ return traced("workglow.storage.tabular.withTransaction", this.storageName, () => this.inner.withTransaction((innerTx) => {
2254
+ const txWrapper = new TelemetryTabularStorage(this.storageName, innerTx);
2255
+ return fn(txWrapper);
2256
+ }));
2257
+ }
1530
2258
  setupDatabase() {
1531
2259
  return this.inner.setupDatabase();
1532
2260
  }
2261
+ getMigrationApplier() {
2262
+ const inner = this.inner;
2263
+ return inner.getMigrationApplier?.() ?? null;
2264
+ }
1533
2265
  destroy() {
1534
2266
  return this.inner.destroy();
1535
2267
  }
@@ -2010,6 +2742,176 @@ class PollingSubscriptionManager {
2010
2742
  this.initializing = false;
2011
2743
  }
2012
2744
  }
2745
+ // src/sql/PredicateBuilder.ts
2746
+ function buildSearchWhere(dialect, criteria, schemaProps, convertValue, startIndex = 1) {
2747
+ const conditions = [];
2748
+ const params = [];
2749
+ let paramIndex = startIndex;
2750
+ for (const column of Object.keys(criteria)) {
2751
+ if (!(column in schemaProps)) {
2752
+ throw new Error(`Schema must have a "${String(column)}" field to use it in search criteria`);
2753
+ }
2754
+ const criterion = criteria[column];
2755
+ let operator = "=";
2756
+ let value;
2757
+ if (isSearchCondition(criterion)) {
2758
+ operator = criterion.operator;
2759
+ value = criterion.value;
2760
+ } else {
2761
+ value = criterion;
2762
+ }
2763
+ conditions.push(`${dialect.quoteId(String(column))} ${operator} ${dialect.placeholder(paramIndex)}`);
2764
+ params.push(convertValue(column, value));
2765
+ paramIndex++;
2766
+ }
2767
+ return {
2768
+ whereClause: conditions.join(" AND "),
2769
+ params
2770
+ };
2771
+ }
2772
+ // src/sql/PrefixDdl.ts
2773
+ var SAFE_IDENTIFIER = /^[a-zA-Z][a-zA-Z0-9_]*$/;
2774
+ var SQL_RESERVED_WORDS = new Set([
2775
+ "all",
2776
+ "alter",
2777
+ "and",
2778
+ "as",
2779
+ "asc",
2780
+ "between",
2781
+ "by",
2782
+ "case",
2783
+ "check",
2784
+ "column",
2785
+ "constraint",
2786
+ "create",
2787
+ "cross",
2788
+ "current",
2789
+ "default",
2790
+ "delete",
2791
+ "desc",
2792
+ "distinct",
2793
+ "drop",
2794
+ "else",
2795
+ "end",
2796
+ "exists",
2797
+ "false",
2798
+ "for",
2799
+ "foreign",
2800
+ "from",
2801
+ "full",
2802
+ "function",
2803
+ "grant",
2804
+ "group",
2805
+ "having",
2806
+ "in",
2807
+ "index",
2808
+ "inner",
2809
+ "insert",
2810
+ "into",
2811
+ "is",
2812
+ "join",
2813
+ "key",
2814
+ "left",
2815
+ "like",
2816
+ "limit",
2817
+ "natural",
2818
+ "not",
2819
+ "null",
2820
+ "offset",
2821
+ "on",
2822
+ "or",
2823
+ "order",
2824
+ "outer",
2825
+ "primary",
2826
+ "references",
2827
+ "returning",
2828
+ "right",
2829
+ "select",
2830
+ "set",
2831
+ "table",
2832
+ "then",
2833
+ "true",
2834
+ "union",
2835
+ "unique",
2836
+ "update",
2837
+ "user",
2838
+ "using",
2839
+ "values",
2840
+ "view",
2841
+ "when",
2842
+ "where",
2843
+ "with"
2844
+ ]);
2845
+ function assertPrefixesSafe(prefixes) {
2846
+ for (const p of prefixes) {
2847
+ if (!SAFE_IDENTIFIER.test(p.name)) {
2848
+ throw new Error(`Prefix column name must start with a letter and contain only letters, digits, and underscores, got: ${p.name}`);
2849
+ }
2850
+ if (SQL_RESERVED_WORDS.has(p.name.toLowerCase())) {
2851
+ throw new Error(`Prefix column name "${p.name}" is a reserved SQL keyword. Pick a different identifier (e.g. "${p.name}_id").`);
2852
+ }
2853
+ }
2854
+ }
2855
+ function assertPrefixValuesPresent(prefixes, prefixValues) {
2856
+ for (const p of prefixes) {
2857
+ const v = prefixValues[p.name];
2858
+ if (v === undefined || v === null) {
2859
+ throw new Error(`Missing prefix value for column "${p.name}". Every prefix declared in \`prefixes\` ` + `must have a corresponding entry in \`prefixValues\`.`);
2860
+ }
2861
+ }
2862
+ }
2863
+ function prefixColumnType(dialect, type) {
2864
+ if (type === "uuid") {
2865
+ return dialect.name === "postgres" ? "UUID" : "TEXT";
2866
+ }
2867
+ return "INTEGER";
2868
+ }
2869
+ function buildPrefixColumnsSql(dialect, prefixes) {
2870
+ if (prefixes.length === 0)
2871
+ return "";
2872
+ assertPrefixesSafe(prefixes);
2873
+ return prefixes.map((p) => `${p.name} ${prefixColumnType(dialect, p.type)} NOT NULL`).join(`,
2874
+ `) + `,
2875
+ `;
2876
+ }
2877
+ function getPrefixColumnNames(prefixes) {
2878
+ return prefixes.map((p) => p.name);
2879
+ }
2880
+ function getPrefixIndexPrefix(prefixes) {
2881
+ if (prefixes.length === 0)
2882
+ return "";
2883
+ assertPrefixesSafe(prefixes);
2884
+ return prefixes.map((p) => p.name).join(", ") + ", ";
2885
+ }
2886
+ function getPrefixIndexSuffix(prefixes) {
2887
+ if (prefixes.length === 0)
2888
+ return "";
2889
+ assertPrefixesSafe(prefixes);
2890
+ return "_" + prefixes.map((p) => p.name).join("_");
2891
+ }
2892
+ function buildPrefixWhereClause(dialect, prefixes, prefixValues, startParam = 1) {
2893
+ if (prefixes.length === 0)
2894
+ return { conditions: "", params: [] };
2895
+ assertPrefixesSafe(prefixes);
2896
+ assertPrefixValuesPresent(prefixes, prefixValues);
2897
+ const conditions = prefixes.map((p, i) => `${p.name} = ${dialect.placeholder(startParam + i)}`).join(" AND ");
2898
+ const params = prefixes.map((p) => prefixValues[p.name]);
2899
+ return { conditions: " AND " + conditions, params };
2900
+ }
2901
+ function getPrefixParamValues(prefixes, prefixValues) {
2902
+ if (prefixes.length === 0)
2903
+ return [];
2904
+ assertPrefixValuesPresent(prefixes, prefixValues);
2905
+ return prefixes.map((p) => prefixValues[p.name]);
2906
+ }
2907
+ function buildPrefixInsertFragments(dialect, prefixes, startParam = 1) {
2908
+ if (prefixes.length === 0)
2909
+ return { columns: "", placeholders: "" };
2910
+ assertPrefixesSafe(prefixes);
2911
+ const columns = prefixes.map((p) => p.name).join(", ") + ", ";
2912
+ const placeholders = prefixes.map((_, i) => dialect.placeholder(startParam + i)).join(", ") + ", ";
2913
+ return { columns, placeholders };
2914
+ }
2013
2915
  // src/vector/InMemoryVectorStorage.ts
2014
2916
  import { cosineSimilarity } from "@workglow/util/schema";
2015
2917
 
@@ -2284,12 +3186,51 @@ import { mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
2284
3186
  import path from "path";
2285
3187
  var FS_FOLDER_TABULAR_REPOSITORY = createServiceToken8("storage.tabularRepository.fsFolder");
2286
3188
 
3189
+ class FsFolderMigrationApplier extends InMemoryTabularMigrationApplier {
3190
+ folderPath;
3191
+ loaded = false;
3192
+ constructor(storage, folderPath) {
3193
+ super(storage, "fsfolder");
3194
+ this.folderPath = folderPath;
3195
+ }
3196
+ async ensureBookkeeping() {
3197
+ await this.load();
3198
+ }
3199
+ async appliedVersions(component) {
3200
+ await this.load();
3201
+ return new Set(this.applied.get(component) ?? []);
3202
+ }
3203
+ async load() {
3204
+ if (this.loaded)
3205
+ return;
3206
+ const file = `${this.folderPath}/_storage_migrations.json`;
3207
+ try {
3208
+ const text = await readFile(file, "utf8");
3209
+ const parsed = JSON.parse(text);
3210
+ for (const [c, vs] of Object.entries(parsed)) {
3211
+ this.applied.set(c, new Set(vs));
3212
+ }
3213
+ } catch (err) {
3214
+ if (err?.code !== "ENOENT")
3215
+ throw err;
3216
+ }
3217
+ this.loaded = true;
3218
+ }
3219
+ async persist() {
3220
+ const file = `${this.folderPath}/_storage_migrations.json`;
3221
+ const out = {};
3222
+ for (const [c, vs] of this.applied)
3223
+ out[c] = [...vs].sort((a, b) => a - b);
3224
+ await writeFile(file, JSON.stringify(out, null, 2));
3225
+ }
3226
+ }
3227
+
2287
3228
  class FsFolderTabularStorage extends BaseTabularStorage {
2288
3229
  folderPath;
2289
3230
  autoIncrementCounter = 0;
2290
3231
  pollingManager = null;
2291
- constructor(folderPath, schema, primaryKeyNames, indexes = [], clientProvidedKeys = "if-missing") {
2292
- super(schema, primaryKeyNames, indexes, clientProvidedKeys);
3232
+ constructor(folderPath, schema, primaryKeyNames, indexes = [], clientProvidedKeys = "if-missing", tabularMigrations) {
3233
+ super(schema, primaryKeyNames, indexes, clientProvidedKeys, tabularMigrations, "fsfolder");
2293
3234
  this.folderPath = path.join(folderPath);
2294
3235
  }
2295
3236
  async setupDirectory() {
@@ -2302,6 +3243,15 @@ class FsFolderTabularStorage extends BaseTabularStorage {
2302
3243
  } catch {}
2303
3244
  }
2304
3245
  }
3246
+ async setupDatabase() {
3247
+ await this.setupDirectory();
3248
+ if (this.tabularMigrations && this.tabularMigrations.length > 0) {
3249
+ await this.applyTabularMigrations();
3250
+ }
3251
+ }
3252
+ getMigrationApplier() {
3253
+ return new FsFolderMigrationApplier(this, this.folderPath);
3254
+ }
2305
3255
  generateKeyValue(columnName, strategy) {
2306
3256
  if (strategy === "autoincrement") {
2307
3257
  return ++this.autoIncrementCounter;
@@ -2379,7 +3329,7 @@ class FsFolderTabularStorage extends BaseTabularStorage {
2379
3329
  await this.setupDirectory();
2380
3330
  try {
2381
3331
  const files = await readdir(this.folderPath);
2382
- const jsonFiles = files.filter((file) => file.endsWith(".json"));
3332
+ const jsonFiles = files.filter((file) => file.endsWith(".json") && !file.startsWith("_"));
2383
3333
  if (jsonFiles.length === 0) {
2384
3334
  return;
2385
3335
  }
@@ -2409,13 +3359,13 @@ class FsFolderTabularStorage extends BaseTabularStorage {
2409
3359
  async size() {
2410
3360
  await this.setupDirectory();
2411
3361
  const files = await readdir(this.folderPath);
2412
- const jsonFiles = files.filter((file) => file.endsWith(".json"));
3362
+ const jsonFiles = files.filter((file) => file.endsWith(".json") && !file.startsWith("_"));
2413
3363
  return jsonFiles.length;
2414
3364
  }
2415
3365
  async getBulk(offset, limit) {
2416
3366
  await this.setupDirectory();
2417
3367
  const files = await readdir(this.folderPath);
2418
- const jsonFiles = files.filter((file) => file.endsWith(".json"));
3368
+ const jsonFiles = files.filter((file) => file.endsWith(".json") && !file.startsWith("_"));
2419
3369
  if (jsonFiles.length === 0) {
2420
3370
  return;
2421
3371
  }
@@ -2604,8 +3554,8 @@ class SharedInMemoryTabularStorage extends BaseTabularStorage {
2604
3554
  isInitialized = false;
2605
3555
  syncInProgress = false;
2606
3556
  pendingMessages = [];
2607
- constructor(channelName = "tabular_store", schema, primaryKeyNames, indexes = [], clientProvidedKeys = "if-missing") {
2608
- super(schema, primaryKeyNames, indexes, clientProvidedKeys);
3557
+ constructor(channelName = "tabular_store", schema, primaryKeyNames, indexes = [], clientProvidedKeys = "if-missing", tabularMigrations) {
3558
+ super(schema, primaryKeyNames, indexes, clientProvidedKeys, tabularMigrations, channelName);
2609
3559
  this.channelName = channelName;
2610
3560
  this.inMemoryRepo = new InMemoryTabularStorage(schema, primaryKeyNames, indexes, clientProvidedKeys);
2611
3561
  this.setupEventForwarding();
@@ -2729,6 +3679,12 @@ class SharedInMemoryTabularStorage extends BaseTabularStorage {
2729
3679
  return;
2730
3680
  this.isInitialized = true;
2731
3681
  await this.syncFromOtherTabs();
3682
+ if (this.tabularMigrations && this.tabularMigrations.length > 0) {
3683
+ await this.applyTabularMigrations();
3684
+ }
3685
+ }
3686
+ getMigrationApplier() {
3687
+ return new InMemoryTabularMigrationApplier(this, this.channelName);
2732
3688
  }
2733
3689
  async put(value) {
2734
3690
  const result = await this.inMemoryRepo.put(value);
@@ -2786,13 +3742,36 @@ class SharedInMemoryTabularStorage extends BaseTabularStorage {
2786
3742
  }
2787
3743
  }
2788
3744
  export {
3745
+ sortMigrations,
3746
+ runTabularMigrations,
3747
+ runBackfill,
3748
+ registerTabularStorageDefaults,
2789
3749
  registerTabularRepository,
3750
+ prefixColumnType,
2790
3751
  pickCoveringIndex,
2791
3752
  isSearchCondition,
2792
3753
  getVectorProperty,
2793
3754
  getTabularRepository,
3755
+ getPrefixParamValues,
3756
+ getPrefixIndexSuffix,
3757
+ getPrefixIndexPrefix,
3758
+ getPrefixColumnNames,
2794
3759
  getMetadataProperty,
2795
3760
  getGlobalTabularRepositories,
3761
+ encodeCursor,
3762
+ decodeCursor,
3763
+ buildSearchWhere,
3764
+ buildRenameColumnSql,
3765
+ buildPrefixWhereClause,
3766
+ buildPrefixInsertFragments,
3767
+ buildPrefixColumnsSql,
3768
+ buildDropIndexSql,
3769
+ buildDropColumnSql,
3770
+ buildAddIndexSql,
3771
+ buildAddColumnSql,
3772
+ assertPrefixesSafe,
3773
+ assertPrefixValuesPresent,
3774
+ assertCursorMatches,
2796
3775
  TelemetryVectorStorage,
2797
3776
  TelemetryTabularStorage,
2798
3777
  TelemetryKvStorage,
@@ -2804,17 +3783,23 @@ export {
2804
3783
  StorageInvalidColumnError,
2805
3784
  StorageError,
2806
3785
  StorageEmptyCriteriaError,
3786
+ SqliteDialect,
3787
+ SqlTabularMigrationApplier,
2807
3788
  SharedInMemoryTabularStorage,
2808
3789
  SHARED_IN_MEMORY_TABULAR_REPOSITORY,
3790
+ PostgresDialect,
2809
3791
  PollingSubscriptionManager,
3792
+ MIGRATIONS_TABLE,
2810
3793
  MEMORY_TABULAR_REPOSITORY,
2811
3794
  MEMORY_KV_REPOSITORY,
3795
+ MAX_CURSOR_LENGTH,
2812
3796
  LazyEncryptedCredentialStore,
2813
3797
  KvViaTabularStorage,
2814
3798
  KvStorage,
2815
3799
  KV_REPOSITORY,
2816
3800
  InMemoryVectorStorage,
2817
3801
  InMemoryTabularStorage,
3802
+ InMemoryTabularMigrationApplier,
2818
3803
  InMemoryKvStorage,
2819
3804
  HybridSubscriptionManager,
2820
3805
  HuggingFaceTabularStorage,
@@ -2835,4 +3820,4 @@ export {
2835
3820
  BaseSqlTabularStorage
2836
3821
  };
2837
3822
 
2838
- //# debugId=C90CE55156F0F23964756E2164756E21
3823
+ //# debugId=B16DB92363947DF464756E2164756E21