@gscdump/lakehouse 7.1.1 → 7.2.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.
- package/README.md +30 -2
- package/dist/_virtual/node_modules/.pnpm/icebird@0.8.30_patch_hash=a400e343a199b1ca18601f96348011745ac6518fc07ffa71d5adfdd2ccc7d14f/node_modules/icebird/src/write/manifest.mjs +1 -1
- package/dist/_virtual/node_modules/.pnpm/icebird@0.8.30_patch_hash=a400e343a199b1ca18601f96348011745ac6518fc07ffa71d5adfdd2ccc7d14f/node_modules/icebird/src/write/rewrite.mjs +1 -1
- package/dist/_virtual/node_modules/.pnpm/icebird@0.8.30_patch_hash=a400e343a199b1ca18601f96348011745ac6518fc07ffa71d5adfdd2ccc7d14f/node_modules/icebird/src/write/stage-deletion-vector.mjs +1 -1
- package/dist/_virtual/node_modules/.pnpm/icebird@0.8.30_patch_hash=a400e343a199b1ca18601f96348011745ac6518fc07ffa71d5adfdd2ccc7d14f/node_modules/icebird/src/write/stage-position-delete.mjs +1 -1
- package/dist/_virtual/node_modules/.pnpm/icebird@0.8.30_patch_hash=a400e343a199b1ca18601f96348011745ac6518fc07ffa71d5adfdd2ccc7d14f/node_modules/icebird/src/write/stage.mjs +1 -1
- package/dist/_virtual/node_modules/.pnpm/icebird@0.8.30_patch_hash=a400e343a199b1ca18601f96348011745ac6518fc07ffa71d5adfdd2ccc7d14f/node_modules/icebird/src/write/write.mjs +2 -2
- package/dist/maintenance.d.mts +2 -1
- package/dist/maintenance.mjs +2 -1
- package/dist/manifest-merge.d.mts +34 -0
- package/dist/manifest-merge.mjs +245 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,8 +18,8 @@ Node.js 22 or newer is required.
|
|
|
18
18
|
- `@gscdump/lakehouse/bigint`: bigint-safe JSON helpers without catalog or
|
|
19
19
|
Node dependencies.
|
|
20
20
|
- `@gscdump/lakehouse/schema`: lightweight shared schema constants.
|
|
21
|
-
- `@gscdump/lakehouse/maintenance`: stable commit classification
|
|
22
|
-
cleanup workflows.
|
|
21
|
+
- `@gscdump/lakehouse/maintenance`: stable commit classification, manifest
|
|
22
|
+
merging, and orphan cleanup workflows.
|
|
23
23
|
- `@gscdump/lakehouse/provisioning`: adopt, allocate, or provision R2 Data
|
|
24
24
|
Catalog resources.
|
|
25
25
|
- `@gscdump/lakehouse/unsafe-raw`: explicit escape hatch for package adapters
|
|
@@ -66,6 +66,34 @@ Older writers cannot recognize the new content ID format.
|
|
|
66
66
|
|
|
67
67
|
Batch appends already require an explicit `appendId` and retain that behavior.
|
|
68
68
|
|
|
69
|
+
## Manifest merging
|
|
70
|
+
|
|
71
|
+
Use `mergeIcebergManifests(connection, table, options)` from `@gscdump/lakehouse/maintenance`
|
|
72
|
+
to reduce current metadata reads for append-only v2 tables.
|
|
73
|
+
The connection supplies the exact catalog and namespace.
|
|
74
|
+
The operation preserves DATA files, their metadata and sequence numbers, and historical append snapshots and manifests.
|
|
75
|
+
It groups entries by partition specification and writes encoded manifests up to four MiB.
|
|
76
|
+
It bounds aggregate input and staged output to eight MiB each, with ten thousand live entries.
|
|
77
|
+
Options can lower these limits.
|
|
78
|
+
|
|
79
|
+
`Merged` reports the snapshot ID, manifest counts, and file count.
|
|
80
|
+
`Unchanged` avoids writes when merging cannot reduce the manifest count.
|
|
81
|
+
`Conflict` leaves a concurrent UUID or main-snapshot change intact.
|
|
82
|
+
Reload the current head before a later attempt.
|
|
83
|
+
`Err` reports unsupported formats, deletes, invalid inventory, exceeded limits, or changed external authority.
|
|
84
|
+
Infrastructure errors propagate.
|
|
85
|
+
Non-null DATA metadata fields unsupported by the v2 writer, including encryption metadata, produce `invalid-inventory`.
|
|
86
|
+
|
|
87
|
+
Supply `confirmAuthority` to recheck external authority immediately before the catalog commit.
|
|
88
|
+
Returning false prevents that commit; callback exceptions propagate.
|
|
89
|
+
This callback does not create an atomic transaction with an external database.
|
|
90
|
+
Supply `cache` to invalidate the captured namespace and table's cached snapshot pointer after success.
|
|
91
|
+
|
|
92
|
+
Failed or uncertain commits retain staged metadata.
|
|
93
|
+
An uncertain response may follow a successful commit, so deleting staged files could break the table.
|
|
94
|
+
The operation never expires historical snapshots or deletes historical manifests.
|
|
95
|
+
Keep those objects while authenticated receipts or append retries still need them.
|
|
96
|
+
|
|
69
97
|
## License
|
|
70
98
|
|
|
71
99
|
[MIT](../../LICENSE)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { partitionAvroSchema, partitionSpecJson, partitionToAvroRecord } from "./partition.mjs";
|
|
2
1
|
import { avroWrite } from "../avro/avro.write.mjs";
|
|
2
|
+
import { partitionAvroSchema, partitionSpecJson, partitionToAvroRecord } from "./partition.mjs";
|
|
3
3
|
function manifestEntrySchema(schema, partitionSpec, formatVersion, manifestContent = 0) {
|
|
4
4
|
const dataFileFields = [
|
|
5
5
|
{
|
|
@@ -3,9 +3,9 @@ import { uuid4 } from "../utils.mjs";
|
|
|
3
3
|
import { fetchDeleteMaps } from "../fetch.mjs";
|
|
4
4
|
import { icebergManifests, splitManifestEntries } from "../manifest.mjs";
|
|
5
5
|
import { groupByPartition } from "./partition.mjs";
|
|
6
|
+
import { writeDataManifest, writeExistingDataManifest } from "./manifest.mjs";
|
|
6
7
|
import { computeColumnStats } from "./stats.mjs";
|
|
7
8
|
import { buildPartitionSummaries, buildSnapshotUpdate, currentSnapshot, loadPriorManifests } from "./snapshot.mjs";
|
|
8
|
-
import { writeDataManifest, writeExistingDataManifest } from "./manifest.mjs";
|
|
9
9
|
import { writeParquet } from "./parquet.mjs";
|
|
10
10
|
import { buildSortComparator } from "./sort.mjs";
|
|
11
11
|
import { checkWriteFormat, newSnapshotId, resolveParquetCodec } from "./stage.mjs";
|
|
@@ -4,8 +4,8 @@ import { writeDeletionVector } from "../puffin/deletion-vector.mjs";
|
|
|
4
4
|
import { writePuffinFile } from "../puffin/puffin.mjs";
|
|
5
5
|
import { fetchDeleteMaps } from "../fetch.mjs";
|
|
6
6
|
import { validateSchemaForVersion } from "../schema.mjs";
|
|
7
|
-
import { buildPartitionSummaries, buildSnapshotUpdate, currentSnapshot, loadPriorManifests } from "./snapshot.mjs";
|
|
8
7
|
import { writeDeleteManifest, writeExistingDeleteManifest } from "./manifest.mjs";
|
|
8
|
+
import { buildPartitionSummaries, buildSnapshotUpdate, currentSnapshot, loadPriorManifests } from "./snapshot.mjs";
|
|
9
9
|
import { checkWriteFormat, newSnapshotId } from "./stage.mjs";
|
|
10
10
|
import { findDataFileEntries, loadManifestEntries, partitionTupleKey } from "./stage-position-delete.mjs";
|
|
11
11
|
async function icebergStageDeletionVector({ tableUrl, metadata, deletes, resolver }) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { bytesToHex, uuid4 } from "../utils.mjs";
|
|
2
2
|
import { fetchAvroRecords } from "../fetch.mjs";
|
|
3
3
|
import { validateSchemaForVersion } from "../schema.mjs";
|
|
4
|
-
import { buildPartitionSummaries, buildSnapshotUpdate, currentSnapshot } from "./snapshot.mjs";
|
|
5
4
|
import { writeDeleteManifest } from "./manifest.mjs";
|
|
5
|
+
import { buildPartitionSummaries, buildSnapshotUpdate, currentSnapshot } from "./snapshot.mjs";
|
|
6
6
|
import { checkWriteFormat, newSnapshotId, resolveParquetCodec } from "./stage.mjs";
|
|
7
7
|
import { writePositionDeleteFile } from "./delete-file.mjs";
|
|
8
8
|
async function icebergStagePositionDelete({ tableUrl, metadata, deletes, resolver }) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { uuid4 } from "../utils.mjs";
|
|
2
2
|
import { validateSchemaForVersion } from "../schema.mjs";
|
|
3
3
|
import { groupByPartition } from "./partition.mjs";
|
|
4
|
+
import { writeDataManifest } from "./manifest.mjs";
|
|
4
5
|
import { computeColumnStats } from "./stats.mjs";
|
|
5
6
|
import { buildPartitionSummaries, buildSnapshotUpdate, currentSnapshot, normalizeCurrentSnapshotId } from "./snapshot.mjs";
|
|
6
7
|
import { applyUpdates } from "./commit.mjs";
|
|
7
|
-
import { writeDataManifest } from "./manifest.mjs";
|
|
8
8
|
import { writeParquet } from "./parquet.mjs";
|
|
9
9
|
import { buildSortComparator } from "./sort.mjs";
|
|
10
10
|
async function prepareAppend({ tableUrl, metadata, records, resolver, sortOrderId, snapshotId }) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { restCatalogCreateTable, restCatalogDropTable, restCatalogLoadTable, restCatalogUpdateTable } from "../catalog/rest.mjs";
|
|
2
|
+
import { applyUpdates, fileCatalogCommit } from "./commit.mjs";
|
|
3
|
+
import { icebergStageAppend, icebergStageExpireSnapshots, icebergStageSetRef, icebergStageUpdateSchema, prepareAppend, stageSnapshotForAppend, stageSnapshotForAppendBatches } from "./stage.mjs";
|
|
2
4
|
import { loadLatestFileCatalogMetadata } from "../metadata.mjs";
|
|
3
5
|
import { loadTable } from "../catalog/loadTable.mjs";
|
|
4
6
|
import { icebergCreate } from "../create.mjs";
|
|
5
|
-
import { applyUpdates, fileCatalogCommit } from "./commit.mjs";
|
|
6
|
-
import { icebergStageAppend, icebergStageExpireSnapshots, icebergStageSetRef, icebergStageUpdateSchema, prepareAppend, stageSnapshotForAppend, stageSnapshotForAppendBatches } from "./stage.mjs";
|
|
7
7
|
import { icebergStagePositionDelete } from "./stage-position-delete.mjs";
|
|
8
8
|
import { icebergStageDeletionVector } from "./stage-deletion-vector.mjs";
|
|
9
9
|
import { icebergStageRewrite } from "./rewrite.mjs";
|
package/dist/maintenance.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { isCommitRateLimited, isCommitServerError, isCommitTransient } from "./catalog.mjs";
|
|
2
|
+
import { MergeIcebergManifestsOptions, MergeIcebergManifestsResult, mergeIcebergManifests } from "./manifest-merge.mjs";
|
|
2
3
|
import { SweepListPage, SweepListedObject, SweepStorageClient, SweepUncommittedOrphansOptions, SweepUncommittedOrphansResult, sweepUncommittedOrphans } from "./orphan-sweep.mjs";
|
|
3
|
-
export { type SweepListPage, type SweepListedObject, type SweepStorageClient, type SweepUncommittedOrphansOptions, type SweepUncommittedOrphansResult, isCommitRateLimited, isCommitServerError, isCommitTransient, sweepUncommittedOrphans };
|
|
4
|
+
export { type MergeIcebergManifestsOptions, type MergeIcebergManifestsResult, type SweepListPage, type SweepListedObject, type SweepStorageClient, type SweepUncommittedOrphansOptions, type SweepUncommittedOrphansResult, isCommitRateLimited, isCommitServerError, isCommitTransient, mergeIcebergManifests, sweepUncommittedOrphans };
|
package/dist/maintenance.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { isCommitRateLimited, isCommitServerError, isCommitTransient } from "./catalog.mjs";
|
|
2
|
+
import { mergeIcebergManifests } from "./manifest-merge.mjs";
|
|
2
3
|
import { sweepUncommittedOrphans } from "./orphan-sweep.mjs";
|
|
3
|
-
export { isCommitRateLimited, isCommitServerError, isCommitTransient, sweepUncommittedOrphans };
|
|
4
|
+
export { isCommitRateLimited, isCommitServerError, isCommitTransient, mergeIcebergManifests, sweepUncommittedOrphans };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CatalogCache } from "./catalog-cache.mjs";
|
|
2
|
+
import { IcebergConnection } from "./catalog.mjs";
|
|
3
|
+
export interface MergeIcebergManifestsOptions {
|
|
4
|
+
/** Lower the four-MiB encoded manifest limit. */
|
|
5
|
+
maxManifestBytes?: number;
|
|
6
|
+
/** Lower the eight-MiB aggregate metadata input/output limit. */
|
|
7
|
+
maxInputBytes?: number;
|
|
8
|
+
/** Lower the ten-thousand live-entry limit. */
|
|
9
|
+
maxEntries?: number;
|
|
10
|
+
/** Invalidate this reader cache after a successful commit. */
|
|
11
|
+
cache?: CatalogCache;
|
|
12
|
+
/** Recheck external authority immediately before the catalog CAS. Exceptions propagate. */
|
|
13
|
+
confirmAuthority?: () => Promise<boolean>;
|
|
14
|
+
}
|
|
15
|
+
export type MergeIcebergManifestsResult = {
|
|
16
|
+
_tag: 'Merged';
|
|
17
|
+
snapshotId: string;
|
|
18
|
+
manifestsBefore: number;
|
|
19
|
+
manifestsAfter: number;
|
|
20
|
+
files: number;
|
|
21
|
+
} | {
|
|
22
|
+
_tag: 'Unchanged';
|
|
23
|
+
} | {
|
|
24
|
+
_tag: 'Conflict';
|
|
25
|
+
} | {
|
|
26
|
+
_tag: 'Err';
|
|
27
|
+
reason: 'unsupported-format' | 'unsupported-deletes' | 'invalid-inventory' | 'metadata-limit' | 'authority-changed';
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Merge current append-only v2 manifests without reading or rewriting DATA files.
|
|
31
|
+
* Historical snapshots and their immutable manifests remain available to append retries.
|
|
32
|
+
* Conflicts leave staged metadata unreferenced. Never delete it after an ambiguous commit response.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mergeIcebergManifests(conn: IcebergConnection, table: string, options?: MergeIcebergManifestsOptions): Promise<MergeIcebergManifestsResult>;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { restCatalogLoadTable, restCatalogUpdateTable } from "./_virtual/node_modules/.pnpm/icebird@0.8.30_patch_hash=a400e343a199b1ca18601f96348011745ac6518fc07ffa71d5adfdd2ccc7d14f/node_modules/icebird/src/catalog/rest.mjs";
|
|
2
|
+
import { ByteWriter } from "./_virtual/node_modules/.pnpm/hyparquet-writer@0.16.10_patch_hash=d26d002aede61ad95dd633d6557b56897cdcbc45dfc3a5d93fa0e9075ad5425f/node_modules/hyparquet-writer/src/bytewriter.mjs";
|
|
3
|
+
import "./_virtual/node_modules/.pnpm/hyparquet-writer@0.16.10_patch_hash=d26d002aede61ad95dd633d6557b56897cdcbc45dfc3a5d93fa0e9075ad5425f/node_modules/hyparquet-writer/src/node.mjs";
|
|
4
|
+
import { uuid4 } from "./_virtual/node_modules/.pnpm/icebird@0.8.30_patch_hash=a400e343a199b1ca18601f96348011745ac6518fc07ffa71d5adfdd2ccc7d14f/node_modules/icebird/src/utils.mjs";
|
|
5
|
+
import { icebergManifests } from "./_virtual/node_modules/.pnpm/icebird@0.8.30_patch_hash=a400e343a199b1ca18601f96348011745ac6518fc07ffa71d5adfdd2ccc7d14f/node_modules/icebird/src/manifest.mjs";
|
|
6
|
+
import { invalidateSnapshotRef } from "./catalog.mjs";
|
|
7
|
+
import { writeExistingDataManifest } from "./_virtual/node_modules/.pnpm/icebird@0.8.30_patch_hash=a400e343a199b1ca18601f96348011745ac6518fc07ffa71d5adfdd2ccc7d14f/node_modules/icebird/src/write/manifest.mjs";
|
|
8
|
+
import { buildPartitionSummaries, buildSnapshotUpdate, loadPriorManifests } from "./_virtual/node_modules/.pnpm/icebird@0.8.30_patch_hash=a400e343a199b1ca18601f96348011745ac6518fc07ffa71d5adfdd2ccc7d14f/node_modules/icebird/src/write/snapshot.mjs";
|
|
9
|
+
import { newSnapshotId } from "./_virtual/node_modules/.pnpm/icebird@0.8.30_patch_hash=a400e343a199b1ca18601f96348011745ac6518fc07ffa71d5adfdd2ccc7d14f/node_modules/icebird/src/write/stage.mjs";
|
|
10
|
+
const MAX_MANIFEST_BYTES = 4194304;
|
|
11
|
+
const MAX_INPUT_BYTES = 8388608;
|
|
12
|
+
const MAX_ENTRIES = 1e4;
|
|
13
|
+
const PRESERVED_DATA_FIELDS = /* @__PURE__ */ new Set([
|
|
14
|
+
"content",
|
|
15
|
+
"file_path",
|
|
16
|
+
"file_format",
|
|
17
|
+
"partition",
|
|
18
|
+
"record_count",
|
|
19
|
+
"file_size_in_bytes",
|
|
20
|
+
"column_sizes",
|
|
21
|
+
"value_counts",
|
|
22
|
+
"null_value_counts",
|
|
23
|
+
"nan_value_counts",
|
|
24
|
+
"lower_bounds",
|
|
25
|
+
"upper_bounds",
|
|
26
|
+
"split_offsets",
|
|
27
|
+
"sort_order_id"
|
|
28
|
+
]);
|
|
29
|
+
async function mergeIcebergManifests(conn, table, options = {}) {
|
|
30
|
+
const limit = (value, ceiling) => {
|
|
31
|
+
if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0 || value > ceiling)) throw new RangeError(`Maintenance limit must be between 1 and ${ceiling}`);
|
|
32
|
+
return value ?? ceiling;
|
|
33
|
+
};
|
|
34
|
+
const maxManifestBytes = limit(options.maxManifestBytes, MAX_MANIFEST_BYTES);
|
|
35
|
+
const maxInputBytes = limit(options.maxInputBytes, MAX_INPUT_BYTES);
|
|
36
|
+
const maxEntries = limit(options.maxEntries, MAX_ENTRIES);
|
|
37
|
+
if (!conn.resolver.writer) throw new Error("Manifest merging requires a metadata writer");
|
|
38
|
+
const { metadata } = await restCatalogLoadTable(conn.catalog, {
|
|
39
|
+
namespace: conn.namespace,
|
|
40
|
+
table
|
|
41
|
+
});
|
|
42
|
+
if (metadata["format-version"] !== 2) return {
|
|
43
|
+
_tag: "Err",
|
|
44
|
+
reason: "unsupported-format"
|
|
45
|
+
};
|
|
46
|
+
if (metadata["current-snapshot-id"] == null || metadata["current-snapshot-id"] < 0) return { _tag: "Unchanged" };
|
|
47
|
+
const overflow = /* @__PURE__ */ new Error("Metadata merge byte limit exceeded");
|
|
48
|
+
let inputBytes = 0;
|
|
49
|
+
const resolver = {
|
|
50
|
+
...conn.resolver,
|
|
51
|
+
reader: async (path) => {
|
|
52
|
+
const file = await conn.resolver.reader(path);
|
|
53
|
+
if (!Number.isSafeInteger(file.byteLength) || file.byteLength < 0 || file.byteLength > MAX_MANIFEST_BYTES || inputBytes + file.byteLength > maxInputBytes) throw overflow;
|
|
54
|
+
inputBytes += file.byteLength;
|
|
55
|
+
return {
|
|
56
|
+
byteLength: file.byteLength,
|
|
57
|
+
slice: async (start, end) => {
|
|
58
|
+
const bytes = await file.slice(start, end);
|
|
59
|
+
if (bytes.byteLength !== (end ?? file.byteLength) - start) throw new Error("Metadata object length changed while reading");
|
|
60
|
+
return bytes;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const lists = /* @__PURE__ */ new Map();
|
|
66
|
+
const prior = await loadPriorManifests(metadata, resolver).catch((error) => {
|
|
67
|
+
if (error === overflow) return null;
|
|
68
|
+
throw error;
|
|
69
|
+
});
|
|
70
|
+
if (!prior) return {
|
|
71
|
+
_tag: "Err",
|
|
72
|
+
reason: "metadata-limit"
|
|
73
|
+
};
|
|
74
|
+
if (prior.some((manifest) => manifest.content !== 0 || manifest.deleted_files_count > 0)) return {
|
|
75
|
+
_tag: "Err",
|
|
76
|
+
reason: "unsupported-deletes"
|
|
77
|
+
};
|
|
78
|
+
if (prior.length < 2) return { _tag: "Unchanged" };
|
|
79
|
+
const snapshot = metadata.snapshots?.find((s) => BigInt(s["snapshot-id"]) === BigInt(metadata["current-snapshot-id"]));
|
|
80
|
+
if (snapshot?.["manifest-list"]) lists.set(snapshot["manifest-list"], Promise.resolve(prior));
|
|
81
|
+
const inventory = await icebergManifests({
|
|
82
|
+
metadata,
|
|
83
|
+
resolver,
|
|
84
|
+
manifestCache: {
|
|
85
|
+
lists,
|
|
86
|
+
entries: /* @__PURE__ */ new Map()
|
|
87
|
+
}
|
|
88
|
+
}).catch((error) => {
|
|
89
|
+
if (error === overflow) return null;
|
|
90
|
+
throw error;
|
|
91
|
+
});
|
|
92
|
+
if (!inventory) return {
|
|
93
|
+
_tag: "Err",
|
|
94
|
+
reason: "metadata-limit"
|
|
95
|
+
};
|
|
96
|
+
const groups = /* @__PURE__ */ new Map();
|
|
97
|
+
const paths = /* @__PURE__ */ new Set();
|
|
98
|
+
for (const manifest of inventory) for (const entry of manifest.entries) {
|
|
99
|
+
if (entry.status === 2 || entry.data_file.content !== 0) return {
|
|
100
|
+
_tag: "Err",
|
|
101
|
+
reason: "unsupported-deletes"
|
|
102
|
+
};
|
|
103
|
+
if (Object.entries(entry.data_file).some(([field, value]) => value != null && !PRESERVED_DATA_FIELDS.has(field))) return {
|
|
104
|
+
_tag: "Err",
|
|
105
|
+
reason: "invalid-inventory"
|
|
106
|
+
};
|
|
107
|
+
if (entry.snapshot_id == null || entry.sequence_number == null || entry.file_sequence_number == null || entry.partition_spec_id == null || paths.has(entry.data_file.file_path) || !metadata["partition-specs"].some((spec) => spec["spec-id"] === entry.partition_spec_id)) return {
|
|
108
|
+
_tag: "Err",
|
|
109
|
+
reason: "invalid-inventory"
|
|
110
|
+
};
|
|
111
|
+
paths.add(entry.data_file.file_path);
|
|
112
|
+
if (paths.size > maxEntries) return {
|
|
113
|
+
_tag: "Err",
|
|
114
|
+
reason: "metadata-limit"
|
|
115
|
+
};
|
|
116
|
+
const group = groups.get(entry.partition_spec_id) ?? [];
|
|
117
|
+
group.push(entry);
|
|
118
|
+
groups.set(entry.partition_spec_id, group);
|
|
119
|
+
}
|
|
120
|
+
const schema = metadata.schemas.find((schema) => schema["schema-id"] === metadata["current-schema-id"]);
|
|
121
|
+
if (!schema) return {
|
|
122
|
+
_tag: "Err",
|
|
123
|
+
reason: "invalid-inventory"
|
|
124
|
+
};
|
|
125
|
+
const planned = [];
|
|
126
|
+
let outputBytes = 0;
|
|
127
|
+
function boundedWriter() {
|
|
128
|
+
const writer = new ByteWriter(Math.min(1024, maxManifestBytes));
|
|
129
|
+
const ensure = writer.ensure.bind(writer);
|
|
130
|
+
writer.ensure = (size) => {
|
|
131
|
+
if (writer.offset + size > maxManifestBytes) throw overflow;
|
|
132
|
+
ensure(size);
|
|
133
|
+
};
|
|
134
|
+
return writer;
|
|
135
|
+
}
|
|
136
|
+
async function encode(entries, specId) {
|
|
137
|
+
const writer = boundedWriter();
|
|
138
|
+
const spec = metadata["partition-specs"].find((spec) => spec["spec-id"] === specId);
|
|
139
|
+
if (!await Promise.resolve().then(() => writeExistingDataManifest({
|
|
140
|
+
writer,
|
|
141
|
+
schema,
|
|
142
|
+
partitionSpec: spec,
|
|
143
|
+
entries,
|
|
144
|
+
formatVersion: 2
|
|
145
|
+
})).then(() => true, (error) => {
|
|
146
|
+
if (error === overflow) return false;
|
|
147
|
+
throw error;
|
|
148
|
+
})) {
|
|
149
|
+
if (entries.length < 2) return false;
|
|
150
|
+
const middle = Math.floor(entries.length / 2);
|
|
151
|
+
return await encode(entries.slice(0, middle), specId) && await encode(entries.slice(middle), specId);
|
|
152
|
+
}
|
|
153
|
+
if (outputBytes + writer.offset > maxInputBytes) return false;
|
|
154
|
+
outputBytes += writer.offset;
|
|
155
|
+
planned.push({
|
|
156
|
+
bytes: new Uint8Array(writer.getBuffer()),
|
|
157
|
+
entries,
|
|
158
|
+
specId
|
|
159
|
+
});
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
for (const [specId, entries] of groups) if (!await encode(entries, specId)) return {
|
|
163
|
+
_tag: "Err",
|
|
164
|
+
reason: "metadata-limit"
|
|
165
|
+
};
|
|
166
|
+
if (planned.length >= prior.length || planned.length === 0) return { _tag: "Unchanged" };
|
|
167
|
+
const snapshotId = newSnapshotId(metadata);
|
|
168
|
+
const sequenceNumber = BigInt(metadata["last-sequence-number"]) + 1n;
|
|
169
|
+
const manifestUuid = uuid4();
|
|
170
|
+
const manifests = [];
|
|
171
|
+
for (const [index, plan] of planned.entries()) {
|
|
172
|
+
const path = `${metadata.location}/metadata/${manifestUuid}-merge-${index}.avro`;
|
|
173
|
+
const writer = conn.resolver.writer(path);
|
|
174
|
+
writer.appendBytes(plan.bytes);
|
|
175
|
+
await writer.finish();
|
|
176
|
+
const spec = metadata["partition-specs"].find((spec) => spec["spec-id"] === plan.specId);
|
|
177
|
+
manifests.push({
|
|
178
|
+
manifest_path: path,
|
|
179
|
+
manifest_length: BigInt(plan.bytes.byteLength),
|
|
180
|
+
partition_spec_id: plan.specId,
|
|
181
|
+
content: 0,
|
|
182
|
+
sequence_number: sequenceNumber,
|
|
183
|
+
min_sequence_number: plan.entries.reduce((min, entry) => entry.sequence_number < min ? entry.sequence_number : min, sequenceNumber),
|
|
184
|
+
added_snapshot_id: snapshotId,
|
|
185
|
+
added_files_count: 0,
|
|
186
|
+
existing_files_count: plan.entries.length,
|
|
187
|
+
deleted_files_count: 0,
|
|
188
|
+
added_rows_count: 0n,
|
|
189
|
+
existing_rows_count: plan.entries.reduce((sum, entry) => sum + entry.data_file.record_count, 0n),
|
|
190
|
+
deleted_rows_count: 0n,
|
|
191
|
+
partitions: buildPartitionSummaries(plan.entries.map((entry) => entry.data_file.partition), schema, spec)
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const listWriter = boundedWriter();
|
|
195
|
+
const staged = await buildSnapshotUpdate({
|
|
196
|
+
tableUrl: metadata.location,
|
|
197
|
+
metadata,
|
|
198
|
+
resolver: {
|
|
199
|
+
...resolver,
|
|
200
|
+
writer: () => listWriter
|
|
201
|
+
},
|
|
202
|
+
snapshotId,
|
|
203
|
+
sequenceNumber,
|
|
204
|
+
manifestUuid,
|
|
205
|
+
timestampMs: Date.now(),
|
|
206
|
+
formatVersion: 2,
|
|
207
|
+
priorManifests: prior,
|
|
208
|
+
skipPriorManifestPaths: new Set(prior.map((manifest) => manifest.manifest_path)),
|
|
209
|
+
newManifests: manifests,
|
|
210
|
+
summary: { operation: "replace" },
|
|
211
|
+
writtenFiles: manifests.map((manifest) => manifest.manifest_path)
|
|
212
|
+
}).catch((error) => {
|
|
213
|
+
if (error === overflow) return null;
|
|
214
|
+
throw error;
|
|
215
|
+
});
|
|
216
|
+
if (!staged || outputBytes + listWriter.offset > maxInputBytes) return {
|
|
217
|
+
_tag: "Err",
|
|
218
|
+
reason: "metadata-limit"
|
|
219
|
+
};
|
|
220
|
+
const persistedList = conn.resolver.writer(staged.snapshot["manifest-list"]);
|
|
221
|
+
persistedList.appendBytes(listWriter.getBytes());
|
|
222
|
+
await persistedList.finish();
|
|
223
|
+
if (options.confirmAuthority && !await options.confirmAuthority()) return {
|
|
224
|
+
_tag: "Err",
|
|
225
|
+
reason: "authority-changed"
|
|
226
|
+
};
|
|
227
|
+
if (!await restCatalogUpdateTable(conn.catalog, {
|
|
228
|
+
namespace: conn.namespace,
|
|
229
|
+
table,
|
|
230
|
+
requirements: staged.requirements,
|
|
231
|
+
updates: staged.updates
|
|
232
|
+
}).then(() => true, (error) => {
|
|
233
|
+
if (typeof error === "object" && error !== null && "status" in error && (error.status === 409 || error.status === 412)) return false;
|
|
234
|
+
throw error;
|
|
235
|
+
})) return { _tag: "Conflict" };
|
|
236
|
+
if (options.cache) await invalidateSnapshotRef(options.cache, conn.namespace, table, conn.cacheScope);
|
|
237
|
+
return {
|
|
238
|
+
_tag: "Merged",
|
|
239
|
+
snapshotId: String(snapshotId),
|
|
240
|
+
manifestsBefore: prior.length,
|
|
241
|
+
manifestsAfter: manifests.length,
|
|
242
|
+
files: paths.size
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
export { mergeIcebergManifests };
|
package/package.json
CHANGED