@gscdump/lakehouse 3.4.1 → 3.4.3
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/catalog.mjs +147 -36
- package/dist/partition-prune.mjs +12 -1
- package/package.json +1 -1
package/dist/catalog.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { icebergManifests } from "./_virtual/node_modules/.pnpm/icebird@0.8.27_p
|
|
|
5
5
|
import { s3SignedResolver } from "./_virtual/node_modules/.pnpm/icebird@0.8.27_patch_hash=651b5cc1b513184638c98e21a0852c4559044ae60f8449fbe9d81a894ea5b4d7/node_modules/icebird/src/s3.mjs";
|
|
6
6
|
import { cacheGet, cachePut, reportCatalogCacheError } from "./catalog-cache.mjs";
|
|
7
7
|
import { toIcebergDayCount } from "./date.mjs";
|
|
8
|
-
import { buildManifestPartitionFilter } from "./partition-prune.mjs";
|
|
8
|
+
import { MULTI_MONTH_MANIFEST, buildManifestPartitionFilter, manifestMonthBucket } from "./partition-prune.mjs";
|
|
9
9
|
let icebirdWriteModule;
|
|
10
10
|
function useIcebirdWriteModule() {
|
|
11
11
|
icebirdWriteModule ??= import("./_virtual/node_modules/.pnpm/icebird@0.8.27_patch_hash=651b5cc1b513184638c98e21a0852c4559044ae60f8449fbe9d81a894ea5b4d7/node_modules/icebird/src/write/write.mjs");
|
|
@@ -46,18 +46,20 @@ async function connectIcebergCatalog(config, opts = {}) {
|
|
|
46
46
|
await cachePut(opts.cache, catalogConfigKey(config), toCache, CATALOG_CONFIG_TTL_MS, now);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
const
|
|
49
|
+
const cacheScope = catalogCacheScope(config);
|
|
50
|
+
const s3Resolver = s3SignedResolver({
|
|
50
51
|
accessKeyId: config.s3.accessKeyId,
|
|
51
52
|
secretAccessKey: config.s3.secretAccessKey,
|
|
52
53
|
region: config.s3.region ?? "auto",
|
|
53
54
|
endpoint: config.s3.endpoint,
|
|
54
55
|
pathStyle: true
|
|
55
|
-
})
|
|
56
|
+
});
|
|
57
|
+
const resolver = withVerifiedWriterByteLengths(cachingResolver(s3Resolver));
|
|
56
58
|
return {
|
|
57
59
|
catalog,
|
|
58
60
|
resolver,
|
|
59
61
|
namespace: config.namespace,
|
|
60
|
-
cacheScope
|
|
62
|
+
cacheScope
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
function withVerifiedWriterByteLengths(resolver) {
|
|
@@ -230,6 +232,7 @@ const SNAPSHOT_REF_TTL_MS = 18e5;
|
|
|
230
232
|
const RESOLVED_FILES_TTL_MS = 864e5;
|
|
231
233
|
const METADATA_TTL_MS = 864e5;
|
|
232
234
|
const MAX_CACHED_METADATA_BYTES = 2097152;
|
|
235
|
+
const MONTH_FILES_TTL_MS = 2592e6;
|
|
233
236
|
function snapshotRefKey(scope, namespace, table) {
|
|
234
237
|
return `lh-snapref\0${scope}\0${namespace}\0${table}`;
|
|
235
238
|
}
|
|
@@ -242,8 +245,11 @@ async function invalidateSnapshotRef(cache, namespace, table, cacheScope = "") {
|
|
|
242
245
|
function metadataRefKey(scope, namespace, table, snapshotId) {
|
|
243
246
|
return `lh-snapmeta\0${scope}\0${namespace}\0${table}\0${snapshotId}`;
|
|
244
247
|
}
|
|
248
|
+
function matchKeyOf(matches) {
|
|
249
|
+
return [...matches].map((m) => `${m.field}=${m.value}`).sort().join(",");
|
|
250
|
+
}
|
|
245
251
|
function resolvedFilesKey(scope, namespace, table, snapshotId, matches, range) {
|
|
246
|
-
return `lh-files2\0${scope}\0${namespace}\0${table}\0${snapshotId}\0${
|
|
252
|
+
return `lh-files2\0${scope}\0${namespace}\0${table}\0${snapshotId}\0${matchKeyOf(matches)}\0${range.start}..${range.end}`;
|
|
247
253
|
}
|
|
248
254
|
function monthsInRange(range) {
|
|
249
255
|
const [sy, sm] = range.start.split("-").map(Number);
|
|
@@ -265,6 +271,18 @@ function monthsSinceEpoch(ym) {
|
|
|
265
271
|
const [y, m] = ym.split("-").map(Number);
|
|
266
272
|
return (y - 1970) * 12 + (m - 1);
|
|
267
273
|
}
|
|
274
|
+
function ymFromMonthsSinceEpoch(value) {
|
|
275
|
+
const y = 1970 + Math.floor(value / 12);
|
|
276
|
+
const m = (value % 12 + 12) % 12 + 1;
|
|
277
|
+
return `${y}-${String(m).padStart(2, "0")}`;
|
|
278
|
+
}
|
|
279
|
+
function monthFilesKey(scope, namespace, table, matchKey, monthYm, manifestSetHash) {
|
|
280
|
+
return `lh-month\0${scope}\0${namespace}\0${table}\0${matchKey}\0${monthYm}\0${manifestSetHash}`;
|
|
281
|
+
}
|
|
282
|
+
async function sha256Hex(text) {
|
|
283
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
284
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
285
|
+
}
|
|
268
286
|
const DATE_BOUND_BYTES = 4;
|
|
269
287
|
function boundBytesForField(map, fieldId) {
|
|
270
288
|
if (map == null) return null;
|
|
@@ -342,11 +360,125 @@ async function loadSnapshotId(conn, namespace, table, cache, now) {
|
|
|
342
360
|
metadata
|
|
343
361
|
};
|
|
344
362
|
}
|
|
363
|
+
function toListedFile(entry, matches, monthFieldName, wantedMonths, dateFieldId) {
|
|
364
|
+
if (entry.status === 2) return null;
|
|
365
|
+
const df = entry.data_file;
|
|
366
|
+
if (df.content !== 0) return null;
|
|
367
|
+
const part = df.partition;
|
|
368
|
+
for (const match of matches) if (String(part[match.field]) !== String(match.value)) return null;
|
|
369
|
+
if (monthFieldName) {
|
|
370
|
+
const month = part[monthFieldName];
|
|
371
|
+
if (typeof month !== "number" || !wantedMonths.has(month)) return null;
|
|
372
|
+
}
|
|
373
|
+
const dateBounds = dateFieldId == null ? null : readDataFileDateBounds(df, dateFieldId);
|
|
374
|
+
return {
|
|
375
|
+
filePath: df.file_path,
|
|
376
|
+
objectKey: stripBucket(df.file_path),
|
|
377
|
+
bytes: Number(df.file_size_in_bytes),
|
|
378
|
+
rowCount: Number(df.record_count),
|
|
379
|
+
...dateBounds ? { dateBounds } : {}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
async function resolveViaFullWalk(conn, metadata, partitionFilter, matches, monthFieldName, wantedMonths, dateFieldId) {
|
|
383
|
+
const manifests = await icebergManifests({
|
|
384
|
+
metadata,
|
|
385
|
+
resolver: conn.resolver,
|
|
386
|
+
partitionFilter
|
|
387
|
+
});
|
|
388
|
+
const entries = [];
|
|
389
|
+
for (const m of manifests) for (const entry of m.entries) {
|
|
390
|
+
const file = toListedFile(entry, matches, monthFieldName, wantedMonths, dateFieldId);
|
|
391
|
+
if (file) entries.push(file);
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
entries,
|
|
395
|
+
manifestsWalked: manifests.length,
|
|
396
|
+
monthsWanted: 0,
|
|
397
|
+
monthsHit: 0
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
async function resolveViaMonthCache(conn, cache, metadata, partitionFilter, partitionSpec, matches, monthFieldName, wantedMonths, dateFieldId, scope, namespace, table, now) {
|
|
401
|
+
const matchKey = matchKeyOf(matches);
|
|
402
|
+
const manifestMonths = /* @__PURE__ */ new Map();
|
|
403
|
+
await icebergManifests({
|
|
404
|
+
metadata,
|
|
405
|
+
resolver: conn.resolver,
|
|
406
|
+
partitionFilter: (partitions, _specId, manifest) => {
|
|
407
|
+
try {
|
|
408
|
+
if (partitionFilter(partitions) === false) return false;
|
|
409
|
+
manifestMonths.set(manifest.manifest_path, manifestMonthBucket(partitionSpec, partitions));
|
|
410
|
+
} catch {
|
|
411
|
+
manifestMonths.set(manifest.manifest_path, MULTI_MONTH_MANIFEST);
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
const pathsByMonth = /* @__PURE__ */ new Map();
|
|
417
|
+
for (const [path, bucket] of manifestMonths) {
|
|
418
|
+
if (bucket === "multi" || !wantedMonths.has(bucket)) continue;
|
|
419
|
+
const list = pathsByMonth.get(bucket);
|
|
420
|
+
if (list) list.push(path);
|
|
421
|
+
else pathsByMonth.set(bucket, [path]);
|
|
422
|
+
}
|
|
423
|
+
const monthsWanted = pathsByMonth.size;
|
|
424
|
+
const monthKeys = /* @__PURE__ */ new Map();
|
|
425
|
+
await Promise.all([...pathsByMonth].map(async ([monthValue, paths]) => {
|
|
426
|
+
const hash = await sha256Hex([...paths].sort().join("\0"));
|
|
427
|
+
monthKeys.set(monthValue, monthFilesKey(scope, namespace, table, matchKey, ymFromMonthsSinceEpoch(monthValue), hash));
|
|
428
|
+
}));
|
|
429
|
+
const cachedByMonth = /* @__PURE__ */ new Map();
|
|
430
|
+
await Promise.all([...monthKeys].map(async ([monthValue, key]) => {
|
|
431
|
+
const cached = await cacheGet(cache, key, now);
|
|
432
|
+
if (cached !== void 0) cachedByMonth.set(monthValue, cached);
|
|
433
|
+
}));
|
|
434
|
+
const monthsHit = cachedByMonth.size;
|
|
435
|
+
const toWalk = /* @__PURE__ */ new Set();
|
|
436
|
+
for (const [path, bucket] of manifestMonths) if (bucket === "multi" || !cachedByMonth.has(bucket)) toWalk.add(path);
|
|
437
|
+
const entries = [];
|
|
438
|
+
for (const cached of cachedByMonth.values()) entries.push(...cached);
|
|
439
|
+
let manifestsWalked = 0;
|
|
440
|
+
if (toWalk.size > 0) {
|
|
441
|
+
const manifests = await icebergManifests({
|
|
442
|
+
metadata,
|
|
443
|
+
resolver: conn.resolver,
|
|
444
|
+
partitionFilter: (_partitions, _specId, manifest) => toWalk.has(manifest.manifest_path)
|
|
445
|
+
});
|
|
446
|
+
manifestsWalked = manifests.length;
|
|
447
|
+
const freshByMonth = /* @__PURE__ */ new Map();
|
|
448
|
+
for (const m of manifests) {
|
|
449
|
+
const bucket = manifestMonths.get(m.url);
|
|
450
|
+
if (bucket !== void 0 && bucket !== "multi" && !freshByMonth.has(bucket)) freshByMonth.set(bucket, []);
|
|
451
|
+
for (const entry of m.entries) {
|
|
452
|
+
const file = toListedFile(entry, matches, monthFieldName, wantedMonths, dateFieldId);
|
|
453
|
+
if (!file) continue;
|
|
454
|
+
entries.push(file);
|
|
455
|
+
if (bucket !== void 0 && bucket !== "multi") {
|
|
456
|
+
const list = freshByMonth.get(bucket);
|
|
457
|
+
if (list) list.push(file);
|
|
458
|
+
else freshByMonth.set(bucket, [file]);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const puts = [];
|
|
463
|
+
for (const [monthValue, files] of freshByMonth) {
|
|
464
|
+
const key = monthKeys.get(monthValue);
|
|
465
|
+
if (key) puts.push(cachePut(cache, key, files, MONTH_FILES_TTL_MS, now));
|
|
466
|
+
}
|
|
467
|
+
await Promise.all(puts);
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
entries,
|
|
471
|
+
manifestsWalked,
|
|
472
|
+
monthsWanted,
|
|
473
|
+
monthsHit
|
|
474
|
+
};
|
|
475
|
+
}
|
|
345
476
|
async function resolveIcebergDataFiles(conn, opts) {
|
|
346
477
|
const { namespace, table } = opts;
|
|
347
478
|
const profiler = opts.profiler;
|
|
348
479
|
const now = (opts.clock ?? Date.now)();
|
|
349
480
|
const wantedMonths = new Set(monthsInRange(opts.range).map(monthsSinceEpoch));
|
|
481
|
+
if (wantedMonths.size === 0) return [];
|
|
350
482
|
const endSnapshot = profiler?.start("iceberg.snapshot");
|
|
351
483
|
let { snapshotId, metadata } = await loadSnapshotId(conn, namespace, table, opts.cache, now);
|
|
352
484
|
endSnapshot?.({ cached: metadata == null && snapshotId != null });
|
|
@@ -371,49 +503,28 @@ async function resolveIcebergDataFiles(conn, opts) {
|
|
|
371
503
|
}
|
|
372
504
|
const endWalk = profiler?.start("iceberg.walk");
|
|
373
505
|
const partitionFilter = buildManifestPartitionFilter(opts.partitionSpec, opts.matches, wantedMonths);
|
|
374
|
-
const manifests = await icebergManifests({
|
|
375
|
-
metadata,
|
|
376
|
-
resolver: conn.resolver,
|
|
377
|
-
partitionFilter
|
|
378
|
-
});
|
|
379
506
|
const monthField = opts.partitionSpec.find((f) => f.transform === "month");
|
|
380
507
|
const monthFieldName = monthField?.name;
|
|
381
508
|
const dateFieldId = dateColumnFieldId(metadata, monthField?.sourceColumn);
|
|
382
509
|
const dayWindow = rangeDayWindow(opts.range);
|
|
510
|
+
const walk = opts.cache && monthFieldName ? await resolveViaMonthCache(conn, opts.cache, metadata, partitionFilter, opts.partitionSpec, opts.matches, monthFieldName, wantedMonths, dateFieldId, scope, namespace, table, now) : await resolveViaFullWalk(conn, metadata, partitionFilter, opts.matches, monthFieldName, wantedMonths, dateFieldId);
|
|
383
511
|
let boundsPruned = 0;
|
|
384
512
|
const out = [];
|
|
385
|
-
for (const
|
|
386
|
-
if (
|
|
387
|
-
const df = entry.data_file;
|
|
388
|
-
if (df.content !== 0) continue;
|
|
389
|
-
const part = df.partition;
|
|
390
|
-
let matchesAll = true;
|
|
391
|
-
for (const match of opts.matches) if (String(part[match.field]) !== String(match.value)) {
|
|
392
|
-
matchesAll = false;
|
|
393
|
-
break;
|
|
394
|
-
}
|
|
395
|
-
if (!matchesAll) continue;
|
|
396
|
-
if (monthFieldName) {
|
|
397
|
-
const month = part[monthFieldName];
|
|
398
|
-
if (typeof month !== "number" || !wantedMonths.has(month)) continue;
|
|
399
|
-
}
|
|
400
|
-
const dateBounds = dateFieldId == null ? null : readDataFileDateBounds(df, dateFieldId);
|
|
401
|
-
if (dateBounds && dayWindow && (dateBounds.maxDay < dayWindow.startDay || dateBounds.minDay > dayWindow.endDay)) {
|
|
513
|
+
for (const file of walk.entries) {
|
|
514
|
+
if (file.dateBounds && dayWindow && (file.dateBounds.maxDay < dayWindow.startDay || file.dateBounds.minDay > dayWindow.endDay)) {
|
|
402
515
|
boundsPruned++;
|
|
403
516
|
continue;
|
|
404
517
|
}
|
|
405
|
-
out.push(
|
|
406
|
-
filePath: df.file_path,
|
|
407
|
-
objectKey: stripBucket(df.file_path),
|
|
408
|
-
bytes: Number(df.file_size_in_bytes),
|
|
409
|
-
rowCount: Number(df.record_count),
|
|
410
|
-
...dateBounds ? { dateBounds } : {}
|
|
411
|
-
});
|
|
518
|
+
out.push(file);
|
|
412
519
|
}
|
|
520
|
+
out.sort((a, b) => a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0);
|
|
413
521
|
endWalk?.({
|
|
414
|
-
manifests:
|
|
522
|
+
manifests: walk.manifestsWalked,
|
|
415
523
|
files: out.length,
|
|
416
|
-
boundsPruned
|
|
524
|
+
boundsPruned,
|
|
525
|
+
monthsWanted: walk.monthsWanted,
|
|
526
|
+
monthsHit: walk.monthsHit,
|
|
527
|
+
manifestsWalked: walk.manifestsWalked
|
|
417
528
|
});
|
|
418
529
|
if (opts.cache) {
|
|
419
530
|
const freshKey = resolvedFilesKey(scope, namespace, table, snapshotId, opts.matches, opts.range);
|
package/dist/partition-prune.mjs
CHANGED
|
@@ -54,4 +54,15 @@ function buildManifestPartitionFilter(partitionSpec, matches, wantedMonths) {
|
|
|
54
54
|
return true;
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
const MULTI_MONTH_MANIFEST = "multi";
|
|
58
|
+
function manifestMonthBucket(partitionSpec, partitions) {
|
|
59
|
+
const monthFieldIndex = partitionSpec.findIndex((f) => f.transform === "month");
|
|
60
|
+
if (monthFieldIndex < 0 || !partitions || partitions.length <= monthFieldIndex) return MULTI_MONTH_MANIFEST;
|
|
61
|
+
const monthSummary = partitions[monthFieldIndex];
|
|
62
|
+
if (!monthSummary || monthSummary.lower_bound == null && monthSummary.upper_bound == null) return MULTI_MONTH_MANIFEST;
|
|
63
|
+
const lo = decodeMonthInt(monthSummary.lower_bound);
|
|
64
|
+
const hi = decodeMonthInt(monthSummary.upper_bound);
|
|
65
|
+
if (lo == null || hi == null || lo !== hi) return MULTI_MONTH_MANIFEST;
|
|
66
|
+
return lo;
|
|
67
|
+
}
|
|
68
|
+
export { MULTI_MONTH_MANIFEST, buildManifestPartitionFilter, manifestMonthBucket };
|
package/package.json
CHANGED