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