@gscdump/lakehouse 3.4.2 → 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 +143 -36
- package/dist/partition-prune.mjs +12 -1
- package/package.json +1 -1
- package/dist/manifest-cache-resolver.mjs +0 -50
package/dist/catalog.mjs
CHANGED
|
@@ -5,8 +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 {
|
|
9
|
-
import { buildManifestPartitionFilter } from "./partition-prune.mjs";
|
|
8
|
+
import { MULTI_MONTH_MANIFEST, buildManifestPartitionFilter, manifestMonthBucket } from "./partition-prune.mjs";
|
|
10
9
|
let icebirdWriteModule;
|
|
11
10
|
function useIcebirdWriteModule() {
|
|
12
11
|
icebirdWriteModule ??= import("./_virtual/node_modules/.pnpm/icebird@0.8.27_patch_hash=651b5cc1b513184638c98e21a0852c4559044ae60f8449fbe9d81a894ea5b4d7/node_modules/icebird/src/write/write.mjs");
|
|
@@ -55,8 +54,7 @@ async function connectIcebergCatalog(config, opts = {}) {
|
|
|
55
54
|
endpoint: config.s3.endpoint,
|
|
56
55
|
pathStyle: true
|
|
57
56
|
});
|
|
58
|
-
const
|
|
59
|
-
const resolver = withVerifiedWriterByteLengths(cachingResolver(baseResolver));
|
|
57
|
+
const resolver = withVerifiedWriterByteLengths(cachingResolver(s3Resolver));
|
|
60
58
|
return {
|
|
61
59
|
catalog,
|
|
62
60
|
resolver,
|
|
@@ -234,6 +232,7 @@ const SNAPSHOT_REF_TTL_MS = 18e5;
|
|
|
234
232
|
const RESOLVED_FILES_TTL_MS = 864e5;
|
|
235
233
|
const METADATA_TTL_MS = 864e5;
|
|
236
234
|
const MAX_CACHED_METADATA_BYTES = 2097152;
|
|
235
|
+
const MONTH_FILES_TTL_MS = 2592e6;
|
|
237
236
|
function snapshotRefKey(scope, namespace, table) {
|
|
238
237
|
return `lh-snapref\0${scope}\0${namespace}\0${table}`;
|
|
239
238
|
}
|
|
@@ -246,8 +245,11 @@ async function invalidateSnapshotRef(cache, namespace, table, cacheScope = "") {
|
|
|
246
245
|
function metadataRefKey(scope, namespace, table, snapshotId) {
|
|
247
246
|
return `lh-snapmeta\0${scope}\0${namespace}\0${table}\0${snapshotId}`;
|
|
248
247
|
}
|
|
248
|
+
function matchKeyOf(matches) {
|
|
249
|
+
return [...matches].map((m) => `${m.field}=${m.value}`).sort().join(",");
|
|
250
|
+
}
|
|
249
251
|
function resolvedFilesKey(scope, namespace, table, snapshotId, matches, range) {
|
|
250
|
-
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}`;
|
|
251
253
|
}
|
|
252
254
|
function monthsInRange(range) {
|
|
253
255
|
const [sy, sm] = range.start.split("-").map(Number);
|
|
@@ -269,6 +271,18 @@ function monthsSinceEpoch(ym) {
|
|
|
269
271
|
const [y, m] = ym.split("-").map(Number);
|
|
270
272
|
return (y - 1970) * 12 + (m - 1);
|
|
271
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
|
+
}
|
|
272
286
|
const DATE_BOUND_BYTES = 4;
|
|
273
287
|
function boundBytesForField(map, fieldId) {
|
|
274
288
|
if (map == null) return null;
|
|
@@ -346,11 +360,125 @@ async function loadSnapshotId(conn, namespace, table, cache, now) {
|
|
|
346
360
|
metadata
|
|
347
361
|
};
|
|
348
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
|
+
}
|
|
349
476
|
async function resolveIcebergDataFiles(conn, opts) {
|
|
350
477
|
const { namespace, table } = opts;
|
|
351
478
|
const profiler = opts.profiler;
|
|
352
479
|
const now = (opts.clock ?? Date.now)();
|
|
353
480
|
const wantedMonths = new Set(monthsInRange(opts.range).map(monthsSinceEpoch));
|
|
481
|
+
if (wantedMonths.size === 0) return [];
|
|
354
482
|
const endSnapshot = profiler?.start("iceberg.snapshot");
|
|
355
483
|
let { snapshotId, metadata } = await loadSnapshotId(conn, namespace, table, opts.cache, now);
|
|
356
484
|
endSnapshot?.({ cached: metadata == null && snapshotId != null });
|
|
@@ -375,49 +503,28 @@ async function resolveIcebergDataFiles(conn, opts) {
|
|
|
375
503
|
}
|
|
376
504
|
const endWalk = profiler?.start("iceberg.walk");
|
|
377
505
|
const partitionFilter = buildManifestPartitionFilter(opts.partitionSpec, opts.matches, wantedMonths);
|
|
378
|
-
const manifests = await icebergManifests({
|
|
379
|
-
metadata,
|
|
380
|
-
resolver: conn.resolver,
|
|
381
|
-
partitionFilter
|
|
382
|
-
});
|
|
383
506
|
const monthField = opts.partitionSpec.find((f) => f.transform === "month");
|
|
384
507
|
const monthFieldName = monthField?.name;
|
|
385
508
|
const dateFieldId = dateColumnFieldId(metadata, monthField?.sourceColumn);
|
|
386
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);
|
|
387
511
|
let boundsPruned = 0;
|
|
388
512
|
const out = [];
|
|
389
|
-
for (const
|
|
390
|
-
if (
|
|
391
|
-
const df = entry.data_file;
|
|
392
|
-
if (df.content !== 0) continue;
|
|
393
|
-
const part = df.partition;
|
|
394
|
-
let matchesAll = true;
|
|
395
|
-
for (const match of opts.matches) if (String(part[match.field]) !== String(match.value)) {
|
|
396
|
-
matchesAll = false;
|
|
397
|
-
break;
|
|
398
|
-
}
|
|
399
|
-
if (!matchesAll) continue;
|
|
400
|
-
if (monthFieldName) {
|
|
401
|
-
const month = part[monthFieldName];
|
|
402
|
-
if (typeof month !== "number" || !wantedMonths.has(month)) continue;
|
|
403
|
-
}
|
|
404
|
-
const dateBounds = dateFieldId == null ? null : readDataFileDateBounds(df, dateFieldId);
|
|
405
|
-
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)) {
|
|
406
515
|
boundsPruned++;
|
|
407
516
|
continue;
|
|
408
517
|
}
|
|
409
|
-
out.push(
|
|
410
|
-
filePath: df.file_path,
|
|
411
|
-
objectKey: stripBucket(df.file_path),
|
|
412
|
-
bytes: Number(df.file_size_in_bytes),
|
|
413
|
-
rowCount: Number(df.record_count),
|
|
414
|
-
...dateBounds ? { dateBounds } : {}
|
|
415
|
-
});
|
|
518
|
+
out.push(file);
|
|
416
519
|
}
|
|
520
|
+
out.sort((a, b) => a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0);
|
|
417
521
|
endWalk?.({
|
|
418
|
-
manifests:
|
|
522
|
+
manifests: walk.manifestsWalked,
|
|
419
523
|
files: out.length,
|
|
420
|
-
boundsPruned
|
|
524
|
+
boundsPruned,
|
|
525
|
+
monthsWanted: walk.monthsWanted,
|
|
526
|
+
monthsHit: walk.monthsHit,
|
|
527
|
+
manifestsWalked: walk.manifestsWalked
|
|
421
528
|
});
|
|
422
529
|
if (opts.cache) {
|
|
423
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
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { cacheGet, cachePut } from "./catalog-cache.mjs";
|
|
2
|
-
const MANIFEST_AVRO_PATH = /\/metadata\/[^/]+\.avro$/;
|
|
3
|
-
const MANIFEST_CACHE_TTL_MS = 6048e5;
|
|
4
|
-
const MAX_CACHED_MANIFEST_BYTES = 1048576;
|
|
5
|
-
const BASE64_CHUNK_BYTES = 32768;
|
|
6
|
-
function isManifestObjectPath(path) {
|
|
7
|
-
return MANIFEST_AVRO_PATH.test(path);
|
|
8
|
-
}
|
|
9
|
-
function manifestCacheKey(scope, path) {
|
|
10
|
-
return `lh-manifest\0${scope}\0${path}`;
|
|
11
|
-
}
|
|
12
|
-
function bytesToBase64(bytes) {
|
|
13
|
-
let binary = "";
|
|
14
|
-
for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK_BYTES) {
|
|
15
|
-
const chunk = bytes.subarray(offset, offset + BASE64_CHUNK_BYTES);
|
|
16
|
-
binary += String.fromCharCode(...chunk);
|
|
17
|
-
}
|
|
18
|
-
return btoa(binary);
|
|
19
|
-
}
|
|
20
|
-
function base64ToBytes(b64) {
|
|
21
|
-
const binary = atob(b64);
|
|
22
|
-
const bytes = new Uint8Array(binary.length);
|
|
23
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
24
|
-
return bytes;
|
|
25
|
-
}
|
|
26
|
-
function bufferedAsyncBuffer(bytes) {
|
|
27
|
-
return {
|
|
28
|
-
byteLength: bytes.byteLength,
|
|
29
|
-
slice(start = 0, end = bytes.byteLength) {
|
|
30
|
-
return bytes.buffer.slice(bytes.byteOffset + start, bytes.byteOffset + end);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
function wrapManifestCacheResolver(base, cache, scope, clock = Date.now) {
|
|
35
|
-
return {
|
|
36
|
-
...base,
|
|
37
|
-
async reader(path, byteLength) {
|
|
38
|
-
if (!isManifestObjectPath(path)) return base.reader(path, byteLength);
|
|
39
|
-
const now = clock();
|
|
40
|
-
const key = manifestCacheKey(scope, path);
|
|
41
|
-
const cached = await cacheGet(cache, key, now);
|
|
42
|
-
if (cached) return bufferedAsyncBuffer(base64ToBytes(cached.b64));
|
|
43
|
-
const ab = await base.reader(path, byteLength);
|
|
44
|
-
const bytes = new Uint8Array(await ab.slice(0, ab.byteLength));
|
|
45
|
-
if (bytes.byteLength <= MAX_CACHED_MANIFEST_BYTES) await cachePut(cache, key, { b64: bytesToBase64(bytes) }, MANIFEST_CACHE_TTL_MS, now);
|
|
46
|
-
return bufferedAsyncBuffer(bytes);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
export { wrapManifestCacheResolver };
|