@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.
- package/dist/browser.js +998 -61
- package/dist/browser.js.map +24 -14
- package/dist/bun.js +1051 -66
- package/dist/bun.js.map +25 -15
- package/dist/common.d.ts +7 -0
- package/dist/common.d.ts.map +1 -1
- package/dist/migrations/IMigration.d.ts +57 -0
- package/dist/migrations/IMigration.d.ts.map +1 -0
- package/dist/migrations/MigrationRunner.d.ts +44 -0
- package/dist/migrations/MigrationRunner.d.ts.map +1 -0
- package/dist/migrations/TabularMigration.d.ts +85 -0
- package/dist/migrations/TabularMigration.d.ts.map +1 -0
- package/dist/migrations/TabularMigrationOrchestrator.d.ts +34 -0
- package/dist/migrations/TabularMigrationOrchestrator.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +11 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/runBackfill.d.ts +19 -0
- package/dist/migrations/runBackfill.d.ts.map +1 -0
- package/dist/node.js +1051 -66
- package/dist/node.js.map +25 -15
- package/dist/sql/Dialect.d.ts +26 -0
- package/dist/sql/Dialect.d.ts.map +1 -0
- package/dist/sql/PredicateBuilder.d.ts +30 -0
- package/dist/sql/PredicateBuilder.d.ts.map +1 -0
- package/dist/sql/PrefixDdl.d.ts +79 -0
- package/dist/sql/PrefixDdl.d.ts.map +1 -0
- package/dist/sql/index.d.ts +9 -0
- package/dist/sql/index.d.ts.map +1 -0
- package/dist/tabular/BaseSqlTabularStorage.d.ts +63 -2
- package/dist/tabular/BaseSqlTabularStorage.d.ts.map +1 -1
- package/dist/tabular/BaseTabularStorage.d.ts +111 -6
- package/dist/tabular/BaseTabularStorage.d.ts.map +1 -1
- package/dist/tabular/CachedTabularStorage.d.ts +38 -0
- package/dist/tabular/CachedTabularStorage.d.ts.map +1 -1
- package/dist/tabular/Cursor.d.ts +79 -0
- package/dist/tabular/Cursor.d.ts.map +1 -0
- package/dist/tabular/FsFolderTabularStorage.d.ts +5 -1
- package/dist/tabular/FsFolderTabularStorage.d.ts.map +1 -1
- package/dist/tabular/HuggingFaceTabularStorage.d.ts +26 -2
- package/dist/tabular/HuggingFaceTabularStorage.d.ts.map +1 -1
- package/dist/tabular/ITabularStorage.d.ts +203 -3
- package/dist/tabular/ITabularStorage.d.ts.map +1 -1
- package/dist/tabular/InMemoryTabularMigrationApplier.d.ts +39 -0
- package/dist/tabular/InMemoryTabularMigrationApplier.d.ts.map +1 -0
- package/dist/tabular/InMemoryTabularStorage.d.ts +6 -2
- package/dist/tabular/InMemoryTabularStorage.d.ts.map +1 -1
- package/dist/tabular/SharedInMemoryTabularStorage.d.ts +4 -1
- package/dist/tabular/SharedInMemoryTabularStorage.d.ts.map +1 -1
- package/dist/tabular/SqlTabularMigrationApplier.d.ts +53 -0
- package/dist/tabular/SqlTabularMigrationApplier.d.ts.map +1 -0
- package/dist/tabular/StorageError.d.ts.map +1 -1
- package/dist/tabular/TabularStorageRegistry.d.ts +13 -10
- package/dist/tabular/TabularStorageRegistry.d.ts.map +1 -1
- package/dist/tabular/TelemetryTabularStorage.d.ts +11 -1
- package/dist/tabular/TelemetryTabularStorage.d.ts.map +1 -1
- package/dist/tabular/sqlMigrationDdl.d.ts +11 -0
- package/dist/tabular/sqlMigrationDdl.d.ts.map +1 -0
- package/dist/vector/IVectorStorage.d.ts +61 -1
- package/dist/vector/IVectorStorage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/tabular/README.md +73 -0
- 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
|
|
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
|
|
412
|
+
let cursor;
|
|
201
413
|
while (true) {
|
|
202
|
-
const page = await this.
|
|
203
|
-
|
|
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
|
|
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
|
|
427
|
+
let cursor;
|
|
220
428
|
while (true) {
|
|
221
|
-
const page = await this.
|
|
222
|
-
if (
|
|
223
|
-
|
|
429
|
+
const page = await this.getPage({ limit: pageSize, cursor });
|
|
430
|
+
if (page.items.length > 0) {
|
|
431
|
+
yield page.items.slice();
|
|
224
432
|
}
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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,
|
|
1459
|
-
const repos =
|
|
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
|
-
|
|
1467
|
-
|
|
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=
|
|
3823
|
+
//# debugId=B16DB92363947DF464756E2164756E21
|