@gscdump/engine 0.40.2 → 1.0.0

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 (47) hide show
  1. package/README.md +4 -4
  2. package/dist/_chunks/duckdb.d.mts +2 -9
  3. package/dist/_chunks/engine.mjs +3 -7
  4. package/dist/_chunks/entities.mjs +1 -1
  5. package/dist/_chunks/index.d.mts +3 -8
  6. package/dist/_chunks/index2.d.mts +1 -2
  7. package/dist/_chunks/libs/hyparquet-compressors.mjs +1 -1
  8. package/dist/_chunks/libs/hyparquet.mjs +1 -63
  9. package/dist/_chunks/libs/icebird.d.mts +1 -81
  10. package/dist/_chunks/libs/icebird.mjs +4 -330
  11. package/dist/_chunks/parquet-plan.mjs +248 -3
  12. package/dist/_chunks/resolver.mjs +16 -2
  13. package/dist/_chunks/schema.d.mts +2 -2
  14. package/dist/_chunks/sink.d.mts +4 -5
  15. package/dist/_chunks/source.mjs +3 -3
  16. package/dist/_chunks/storage.d.mts +1 -43
  17. package/dist/_chunks/types.d.mts +8 -0
  18. package/dist/adapters/node.d.mts +1 -11
  19. package/dist/adapters/node.mjs +1 -1
  20. package/dist/analyzer/index.d.mts +1 -1
  21. package/dist/entities.d.mts +2 -10
  22. package/dist/entities.mjs +2 -2
  23. package/dist/iceberg/index.d.mts +2 -3
  24. package/dist/iceberg/index.mjs +12 -12
  25. package/dist/index.d.mts +83 -2
  26. package/dist/index.mjs +365 -6
  27. package/dist/period/index.d.mts +2 -2
  28. package/dist/period/index.mjs +1 -1
  29. package/dist/planner.mjs +1 -2
  30. package/dist/resolver/index.d.mts +2 -9
  31. package/dist/resolver/index.mjs +2 -2
  32. package/dist/rollups.d.mts +3 -20
  33. package/dist/rollups.mjs +5 -5
  34. package/dist/scope.d.mts +1 -2
  35. package/dist/scope.mjs +1 -1
  36. package/dist/source/index.d.mts +2 -2
  37. package/dist/source/index.mjs +2 -2
  38. package/dist/sql-bind.d.mts +1 -29
  39. package/dist/sql-bind.mjs +1 -1
  40. package/package.json +7 -22
  41. package/dist/_chunks/compaction.mjs +0 -251
  42. package/dist/adapters/r2-manifest.d.mts +0 -82
  43. package/dist/adapters/r2-manifest.mjs +0 -364
  44. package/dist/compaction-public.d.mts +0 -15
  45. package/dist/compaction-public.mjs +0 -5
  46. package/dist/snapshot.d.mts +0 -2
  47. package/dist/snapshot.mjs +0 -1
@@ -1,364 +0,0 @@
1
- import { inferSearchType } from "../_chunks/layout.mjs";
2
- import { engineErrorToException, engineErrors } from "../errors.mjs";
3
- import { matchesManifestEntryFilter, matchesSyncStateFilter, matchesWatermarkFilter } from "../_chunks/manifest-store-utils.mjs";
4
- import { err, ok, unwrapResult } from "gscdump/result";
5
- const SHARD_RE = /^u_[^/]+\/manifest\/(?<siteId>[^/]+)\/(?<table>[^/]+)\/HEAD$/;
6
- const CAS_BACKOFF_BASE_MS = 5;
7
- const CAS_BACKOFF_CAP_MS = 250;
8
- const SHARD_IO_CONCURRENCY = 8;
9
- const R2_DELETE_BATCH_SIZE = 1e3;
10
- async function casBackoff(attempt) {
11
- const ceil = Math.min(CAS_BACKOFF_CAP_MS, CAS_BACKOFF_BASE_MS * 2 ** attempt);
12
- await new Promise((resolve) => setTimeout(resolve, Math.random() * ceil));
13
- }
14
- async function mapWithConcurrency(items, concurrency, fn) {
15
- if (items.length === 0) return [];
16
- const workerCount = Math.max(1, Math.min(items.length, Math.floor(concurrency)));
17
- const results = Array.from({ length: items.length }, () => void 0);
18
- let nextIndex = 0;
19
- async function worker() {
20
- while (true) {
21
- const index = nextIndex++;
22
- if (index >= items.length) return;
23
- results[index] = await fn(items[index], index);
24
- }
25
- }
26
- await Promise.all(Array.from({ length: workerCount }, worker));
27
- return results;
28
- }
29
- function defaultSnapshotId() {
30
- return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
31
- }
32
- function shardPrefix(userId, siteId, table) {
33
- return `u_${userId}/manifest/${siteId}/${table}/`;
34
- }
35
- function headKey(userId, siteId, table) {
36
- return `${shardPrefix(userId, siteId, table)}HEAD`;
37
- }
38
- function snapshotKey(userId, siteId, table, snapshotId) {
39
- return `${shardPrefix(userId, siteId, table)}v${snapshotId}.json`;
40
- }
41
- function emptySnapshot() {
42
- return {
43
- version: 1,
44
- entries: [],
45
- watermarks: [],
46
- syncStates: []
47
- };
48
- }
49
- function shardScopesFromEntries(entries) {
50
- const out = /* @__PURE__ */ new Set();
51
- for (const e of entries) {
52
- if (e.siteId === void 0) throw new Error("R2 manifest store requires entries to carry siteId; cross-site entries are unshardable");
53
- out.add(`${e.siteId}\0${e.table}`);
54
- }
55
- return out;
56
- }
57
- function createR2ManifestStore(opts) {
58
- const { bucket, userId } = opts;
59
- const newSnapshotId = opts.newSnapshotId ?? defaultSnapshotId;
60
- const now = opts.now ?? (() => Date.now());
61
- const maxRetries = opts.maxRetries ?? 16;
62
- const onEvent = opts.onEvent;
63
- async function readShard(siteId, table) {
64
- const head = await bucket.get(headKey(userId, siteId, table));
65
- if (!head) return {
66
- snapshot: emptySnapshot(),
67
- headEtag: void 0
68
- };
69
- const snapshotId = (await head.text()).trim();
70
- if (!snapshotId) return {
71
- snapshot: emptySnapshot(),
72
- headEtag: head.etag
73
- };
74
- const snap = await bucket.get(snapshotKey(userId, siteId, table, snapshotId));
75
- if (!snap) return {
76
- snapshot: emptySnapshot(),
77
- headEtag: head.etag
78
- };
79
- const parsed = JSON.parse(await snap.text());
80
- if (parsed.version !== 1) throw new Error(`unsupported manifest snapshot version: ${parsed.version}`);
81
- return {
82
- snapshot: parsed,
83
- headEtag: head.etag
84
- };
85
- }
86
- async function writeShardResult(siteId, table, snapshot, headEtag, attempt) {
87
- const id = newSnapshotId();
88
- const snapKey = snapshotKey(userId, siteId, table, id);
89
- await bucket.put(snapKey, JSON.stringify(snapshot));
90
- const conditional = headEtag ? { onlyIf: { etagMatches: headEtag } } : { onlyIf: { etagDoesNotMatch: "*" } };
91
- return await bucket.put(headKey(userId, siteId, table), id, conditional) !== null ? ok(void 0) : err(engineErrors.manifestCasRoundLost(siteId, table, attempt));
92
- }
93
- async function mutateShardResult(siteId, table, mutate) {
94
- let attempt = 0;
95
- while (attempt < maxRetries) {
96
- onEvent?.({
97
- kind: "cas-attempt",
98
- siteId,
99
- table,
100
- attempt
101
- });
102
- const { snapshot, headEtag } = await readShard(siteId, table);
103
- await mutate(snapshot);
104
- const round = await writeShardResult(siteId, table, snapshot, headEtag, attempt);
105
- if (round.ok) {
106
- onEvent?.({
107
- kind: "cas-committed",
108
- siteId,
109
- table,
110
- attempts: attempt + 1
111
- });
112
- return round;
113
- }
114
- onEvent?.({
115
- kind: "cas-rejected",
116
- siteId,
117
- table,
118
- attempt
119
- });
120
- attempt++;
121
- if (attempt < maxRetries) await casBackoff(attempt);
122
- }
123
- return err(engineErrors.manifestCasExhausted(siteId, table, maxRetries));
124
- }
125
- async function mutateShard(siteId, table, mutate) {
126
- return unwrapResult(await mutateShardResult(siteId, table, mutate), engineErrorToException);
127
- }
128
- async function listShards(siteId) {
129
- const shards = [];
130
- let cursor;
131
- const prefix = siteId === void 0 ? `u_${userId}/manifest/` : `u_${userId}/manifest/${siteId}/`;
132
- do {
133
- const res = await bucket.list({
134
- prefix,
135
- cursor,
136
- limit: 1e3
137
- });
138
- for (const obj of res.objects) {
139
- const m = SHARD_RE.exec(obj.key);
140
- if (m?.groups) shards.push({
141
- siteId: m.groups.siteId,
142
- table: m.groups.table
143
- });
144
- }
145
- cursor = res.truncated ? res.cursor : void 0;
146
- } while (cursor);
147
- return shards;
148
- }
149
- async function shardsForFilter(filter) {
150
- if (filter.siteId !== void 0 && filter.table !== void 0) return [{
151
- siteId: filter.siteId,
152
- table: filter.table
153
- }];
154
- return (await listShards(filter.siteId)).filter((s) => (filter.siteId === void 0 || s.siteId === filter.siteId) && (filter.table === void 0 || s.table === filter.table));
155
- }
156
- function assertScopedUser(got, op) {
157
- if (got !== userId) throw new Error(`${op}: R2 manifest store is scoped to userId=${userId}, got ${got}`);
158
- }
159
- async function readEntriesAcrossShards(filter, includeRetired) {
160
- assertScopedUser(filter.userId, includeRetired ? "listAll" : "listLive");
161
- return (await mapWithConcurrency(await shardsForFilter(filter), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
162
- const { snapshot } = await readShard(siteId, table);
163
- const entries = [];
164
- for (const entry of snapshot.entries) {
165
- if (!includeRetired && entry.retiredAt !== void 0) continue;
166
- if (matchesManifestEntryFilter(entry, filter, { ignoreUserId: true })) entries.push(entry);
167
- }
168
- return entries;
169
- })).flat();
170
- }
171
- function groupBySiteTable(entries) {
172
- const out = /* @__PURE__ */ new Map();
173
- for (const e of entries) {
174
- const key = `${e.siteId}\0${e.table}`;
175
- if (!out.has(key)) out.set(key, []);
176
- out.get(key).push(e);
177
- }
178
- return out;
179
- }
180
- async function registerVersionsImpl(newEntries, superseding) {
181
- if (newEntries.length === 0 && (!superseding || superseding.length === 0)) return;
182
- const supersededAt = newEntries[0]?.createdAt ?? now();
183
- const byShard = /* @__PURE__ */ new Map();
184
- function bucket(entry, kind) {
185
- assertScopedUser(entry.userId, "registerVersions");
186
- if (entry.siteId === void 0) throw new Error("R2 manifest store requires entries to carry siteId");
187
- const key = `${entry.siteId}\0${entry.table}`;
188
- let bag = byShard.get(key);
189
- if (!bag) {
190
- bag = {
191
- newEntries: [],
192
- superseding: []
193
- };
194
- byShard.set(key, bag);
195
- }
196
- if (kind === "new") bag.newEntries.push(entry);
197
- else bag.superseding.push(entry);
198
- }
199
- for (const e of newEntries) bucket(e, "new");
200
- if (superseding) for (const e of superseding) bucket(e, "super");
201
- await mapWithConcurrency([...byShard], SHARD_IO_CONCURRENCY, async ([shardKey, { newEntries: news, superseding: supers }]) => {
202
- const [siteId, table] = shardKey.split("\0");
203
- await mutateShard(siteId, table, (snap) => {
204
- const byObjectKey = new Map(snap.entries.map((e) => [e.objectKey, e]));
205
- for (const s of supers) {
206
- const existing = byObjectKey.get(s.objectKey);
207
- if (existing && existing.retiredAt === void 0) byObjectKey.set(s.objectKey, {
208
- ...existing,
209
- retiredAt: supersededAt
210
- });
211
- }
212
- for (const n of news) byObjectKey.set(n.objectKey, n);
213
- snap.entries = Array.from(byObjectKey.values());
214
- });
215
- });
216
- }
217
- return {
218
- async listLive(filter) {
219
- return readEntriesAcrossShards(filter, false);
220
- },
221
- async listAll(filter) {
222
- return readEntriesAcrossShards(filter, true);
223
- },
224
- async registerVersion(entry, superseding) {
225
- return registerVersionsImpl([entry], superseding);
226
- },
227
- async registerVersions(entries, superseding) {
228
- shardScopesFromEntries(entries);
229
- return registerVersionsImpl(entries, superseding);
230
- },
231
- async listRetired(olderThan) {
232
- return (await mapWithConcurrency(await listShards(), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
233
- const { snapshot } = await readShard(siteId, table);
234
- const retired = [];
235
- for (const e of snapshot.entries) if (e.retiredAt !== void 0 && e.retiredAt <= olderThan) retired.push(e);
236
- return retired;
237
- })).flat();
238
- },
239
- async delete(toDelete) {
240
- await mapWithConcurrency([...groupBySiteTable(toDelete)], SHARD_IO_CONCURRENCY, async ([shardKey, entries]) => {
241
- const [siteId, table] = shardKey.split("\0");
242
- await mutateShard(siteId, table, (snap) => {
243
- const drop = new Set(entries.map((e) => e.objectKey));
244
- snap.entries = snap.entries.filter((e) => !drop.has(e.objectKey));
245
- });
246
- });
247
- },
248
- async getWatermarks(filter) {
249
- assertScopedUser(filter.userId, "getWatermarks");
250
- return (await mapWithConcurrency(await shardsForFilter(filter), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
251
- const { snapshot } = await readShard(siteId, table);
252
- const watermarks = [];
253
- for (const w of snapshot.watermarks) if (matchesWatermarkFilter(w, filter, { ignoreUserId: true })) watermarks.push(w);
254
- return watermarks;
255
- })).flat();
256
- },
257
- async bumpWatermark(scope, date, at) {
258
- assertScopedUser(scope.userId, "bumpWatermark");
259
- if (scope.siteId === void 0) throw new Error("R2 manifest store requires watermarks to carry siteId");
260
- const ts = at ?? now();
261
- const scopeSearchType = inferSearchType(scope);
262
- await mutateShard(scope.siteId, scope.table, (snap) => {
263
- const idx = snap.watermarks.findIndex((w) => w.userId === userId && w.siteId === scope.siteId && w.table === scope.table && inferSearchType(w) === scopeSearchType);
264
- if (idx === -1) {
265
- snap.watermarks.push({
266
- userId,
267
- siteId: scope.siteId,
268
- table: scope.table,
269
- ...scope.searchType !== void 0 ? { searchType: scope.searchType } : {},
270
- newestDateSynced: date,
271
- oldestDateSynced: date,
272
- lastSyncAt: ts
273
- });
274
- return;
275
- }
276
- const w = snap.watermarks[idx];
277
- const newest = date > w.newestDateSynced ? date : w.newestDateSynced;
278
- const oldest = date < w.oldestDateSynced ? date : w.oldestDateSynced;
279
- const lastSyncAt = ts > w.lastSyncAt ? ts : w.lastSyncAt;
280
- snap.watermarks[idx] = {
281
- ...w,
282
- newestDateSynced: newest,
283
- oldestDateSynced: oldest,
284
- lastSyncAt
285
- };
286
- });
287
- },
288
- async getSyncStates(filter) {
289
- assertScopedUser(filter.userId, "getSyncStates");
290
- return (await mapWithConcurrency(await shardsForFilter(filter), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
291
- const { snapshot } = await readShard(siteId, table);
292
- const states = [];
293
- for (const s of snapshot.syncStates) if (matchesSyncStateFilter(s, filter, { ignoreUserId: true })) states.push(s);
294
- return states;
295
- })).flat();
296
- },
297
- async setSyncState(scope, state, detail) {
298
- assertScopedUser(scope.userId, "setSyncState");
299
- if (scope.siteId === void 0) throw new Error("R2 manifest store requires sync states to carry siteId");
300
- const at = detail?.at ?? now();
301
- const scopeSearchType = inferSearchType(scope);
302
- await mutateShard(scope.siteId, scope.table, (snap) => {
303
- const idx = snap.syncStates.findIndex((s) => s.userId === userId && s.siteId === scope.siteId && s.table === scope.table && s.date === scope.date && inferSearchType(s) === scopeSearchType);
304
- if (idx === -1) {
305
- snap.syncStates.push({
306
- userId,
307
- siteId: scope.siteId,
308
- table: scope.table,
309
- date: scope.date,
310
- state,
311
- updatedAt: at,
312
- attempts: 1,
313
- error: detail?.error,
314
- ...scope.searchType !== void 0 ? { searchType: scope.searchType } : {}
315
- });
316
- return;
317
- }
318
- const prev = snap.syncStates[idx];
319
- const attempts = state === "inflight" && prev.state !== "inflight" ? prev.attempts + 1 : prev.attempts;
320
- const error = state === "done" ? void 0 : detail?.error ?? prev.error;
321
- snap.syncStates[idx] = {
322
- ...prev,
323
- state,
324
- updatedAt: at,
325
- attempts,
326
- error
327
- };
328
- });
329
- },
330
- async withLock(_scope, fn) {
331
- return fn();
332
- },
333
- async purgeTenant(filter) {
334
- if (filter.userId !== userId) throw new Error(`purgeTenant: store is scoped to userId=${userId}, got ${filter.userId}`);
335
- const purged = await mapWithConcurrency(await shardsForFilter({ siteId: filter.siteId }), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
336
- const { snapshot } = await readShard(siteId, table);
337
- const prefix = shardPrefix(userId, siteId, table);
338
- const keys = [];
339
- let cursor;
340
- do {
341
- const res = await bucket.list({
342
- prefix,
343
- cursor,
344
- limit: 1e3
345
- });
346
- for (const obj of res.objects) keys.push(obj.key);
347
- cursor = res.truncated ? res.cursor : void 0;
348
- } while (cursor);
349
- for (let offset = 0; offset < keys.length; offset += R2_DELETE_BATCH_SIZE) await bucket.delete(keys.slice(offset, offset + R2_DELETE_BATCH_SIZE));
350
- return {
351
- entriesRemoved: snapshot.entries.length,
352
- watermarksRemoved: snapshot.watermarks.length,
353
- syncStatesRemoved: snapshot.syncStates.length
354
- };
355
- });
356
- return {
357
- entriesRemoved: purged.reduce((sum, result) => sum + result.entriesRemoved, 0),
358
- watermarksRemoved: purged.reduce((sum, result) => sum + result.watermarksRemoved, 0),
359
- syncStatesRemoved: purged.reduce((sum, result) => sum + result.syncStatesRemoved, 0)
360
- };
361
- }
362
- };
363
- }
364
- export { createR2ManifestStore };
@@ -1,15 +0,0 @@
1
- import { dedupeOverlappingTiers, splitOverlappingTiers } from "./_chunks/storage.mjs";
2
- /**
3
- * Host-policy predicate: true once a table's live raw-daily count crosses the
4
- * engine's daily→weekly compaction gate. Wraps the internal threshold so hosts
5
- * decide "is compaction due?" without importing the constant or the counter.
6
- *
7
- * Pure — pass the entries the host already fetched (typically via its own cached
8
- * manifest store, so the hot-path check stays on the host's cache rather than
9
- * forcing an uncached read through the engine).
10
- */
11
- declare function isRawDailyCompactionDue(entries: ReadonlyArray<{
12
- tier?: string | null;
13
- partition: string;
14
- }>): boolean;
15
- export { dedupeOverlappingTiers, isRawDailyCompactionDue, splitOverlappingTiers };
@@ -1,5 +0,0 @@
1
- import { countRawDailies, dedupeOverlappingTiers, splitOverlappingTiers } from "./_chunks/compaction.mjs";
2
- function isRawDailyCompactionDue(entries) {
3
- return countRawDailies(entries) > 7;
4
- }
5
- export { dedupeOverlappingTiers, isRawDailyCompactionDue, splitOverlappingTiers };
@@ -1,2 +0,0 @@
1
- import { SnapshotIndex } from "./_chunks/snapshot.mjs";
2
- export { SnapshotIndex };
package/dist/snapshot.mjs DELETED
@@ -1 +0,0 @@
1
- export {};