@gscdump/lakehouse 0.1.0 → 0.35.1
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.
|
@@ -5,37 +5,6 @@ Apache-2.0, BlueOak-1.0.0, MIT
|
|
|
5
5
|
|
|
6
6
|
# Bundled Dependencies
|
|
7
7
|
|
|
8
|
-
## @netlify/blobs
|
|
9
|
-
|
|
10
|
-
License: MIT
|
|
11
|
-
By: Netlify Inc.
|
|
12
|
-
Repository: https://github.com/netlify/primitives
|
|
13
|
-
|
|
14
|
-
> Copyright (c) 2022 Netlify <team@netlify.com>
|
|
15
|
-
>
|
|
16
|
-
> MIT License
|
|
17
|
-
>
|
|
18
|
-
> Permission is hereby granted, free of charge, to any person obtaining
|
|
19
|
-
> a copy of this software and associated documentation files (the
|
|
20
|
-
> "Software"), to deal in the Software without restriction, including
|
|
21
|
-
> without limitation the rights to use, copy, modify, merge, publish,
|
|
22
|
-
> distribute, sublicense, and/or sell copies of the Software, and to
|
|
23
|
-
> permit persons to whom the Software is furnished to do so, subject to
|
|
24
|
-
> the following conditions:
|
|
25
|
-
>
|
|
26
|
-
> The above copyright notice and this permission notice shall be
|
|
27
|
-
> included in all copies or substantial portions of the Software.
|
|
28
|
-
>
|
|
29
|
-
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
30
|
-
> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
31
|
-
> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
32
|
-
> NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
33
|
-
> LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
34
|
-
> OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
35
|
-
> WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
36
|
-
|
|
37
|
-
---------------------------------------
|
|
38
|
-
|
|
39
8
|
## chokidar
|
|
40
9
|
|
|
41
10
|
License: MIT
|
|
@@ -63,6 +63,45 @@ interface ProvisionedCatalogRef {
|
|
|
63
63
|
bucket: string;
|
|
64
64
|
namespace: string;
|
|
65
65
|
}
|
|
66
|
+
interface EnableCatalogMaintenanceOptions {
|
|
67
|
+
accountId: string;
|
|
68
|
+
/** R2-Data-Catalog-scoped token (credential-set / maintenance-configs). */
|
|
69
|
+
catalogToken: string;
|
|
70
|
+
bucket: string;
|
|
71
|
+
/** See {@link CloudflareProvisionCreds.compactionCredentialToken}. */
|
|
72
|
+
compactionCredentialToken?: string;
|
|
73
|
+
/** Injectable fetch — defaults to the global. */
|
|
74
|
+
fetch?: typeof fetch;
|
|
75
|
+
/** Injectable warn sink for best-effort maintenance failures. Defaults to `console.warn`. */
|
|
76
|
+
onWarn?: (message: string) => void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Standalone credential-set + compaction/snapshot-expiry enable — the same two
|
|
80
|
+
* steps {@link selfProvisionCatalog} ends with, WITHOUT its bucket-create /
|
|
81
|
+
* CORS / catalog-enable side effects. For the "backfill maintenance onto an
|
|
82
|
+
* already-ready catalog" case (re-provision / admin-repair jobs), where
|
|
83
|
+
* re-running bucket-create or catalog-enable would be wasted CF calls, not
|
|
84
|
+
* just idempotent no-ops.
|
|
85
|
+
*
|
|
86
|
+
* R2-FIXES C3 note: this export postdates the version both nuxtseo's
|
|
87
|
+
* `provision-team-catalog.ts` and gscdump.com's `team-catalog-provisioner.ts`
|
|
88
|
+
* synced against (published 0.1.0 had no such entry point), so BOTH apps still
|
|
89
|
+
* hand-roll this exact sequence locally for their idempotent-backfill branch.
|
|
90
|
+
* Swap their local duplicates for this export on the next publish + bump.
|
|
91
|
+
*/
|
|
92
|
+
declare function enableCatalogMaintenance(opts: EnableCatalogMaintenanceOptions): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Generic `prefix + key` bucket-name normalization (R2-FIXES C3 TODO): lowercase,
|
|
95
|
+
* collapse non-`[a-z0-9-]` runs to a single hyphen, trim, clamp to the R2
|
|
96
|
+
* 63-char limit. Throws if the result isn't a valid bucket name.
|
|
97
|
+
*
|
|
98
|
+
* Neither app has switched over yet: nuxtseo's `teamCatalogBucketName` and
|
|
99
|
+
* gscdump.com's `teamCatalogBucket` predate this export AND encode slightly
|
|
100
|
+
* different schemes (gscdump.com additionally suffixes the partition-key
|
|
101
|
+
* encoding, e.g. `-int`) — see their own TODO(lakehouse@next) comments for the
|
|
102
|
+
* planned migration once a version carrying this export is published.
|
|
103
|
+
*/
|
|
104
|
+
declare function deriveBucketName(prefix: string, key: string): string;
|
|
66
105
|
interface SelfProvisionOptions {
|
|
67
106
|
creds: CloudflareProvisionCreds;
|
|
68
107
|
bucket: string;
|
|
@@ -121,4 +160,4 @@ declare function adoptCatalog(remote: RemoteCatalogRef | null, opts?: AdoptCatal
|
|
|
121
160
|
* pure validation/passthrough, no CF calls, no persistence.
|
|
122
161
|
*/
|
|
123
162
|
declare function suppliedCatalog(ref: ProvisionedCatalogRef): ProvisionedCatalogRef;
|
|
124
|
-
export { AdoptCatalogOptions, AdoptCatalogResult, type AllocateCatalogSiteIdOptions, type AllocateCatalogSiteIdResult, CloudflareProvisionCreds, ProvisionedCatalogRef, RemoteCatalogRef, SelfProvisionOptions, adoptCatalog, allocateCatalogSiteId, selfProvisionCatalog, suppliedCatalog };
|
|
163
|
+
export { AdoptCatalogOptions, AdoptCatalogResult, type AllocateCatalogSiteIdOptions, type AllocateCatalogSiteIdResult, CloudflareProvisionCreds, EnableCatalogMaintenanceOptions, ProvisionedCatalogRef, RemoteCatalogRef, SelfProvisionOptions, adoptCatalog, allocateCatalogSiteId, deriveBucketName, enableCatalogMaintenance, selfProvisionCatalog, suppliedCatalog };
|
|
@@ -95,6 +95,18 @@ async function ensureCredential(fetchImpl, accountId, catalogToken, credentialTo
|
|
|
95
95
|
onWarn(`[lakehouse-provisioning] credential-set failed for ${bucket} — compaction stays inert (credential absent): ${err instanceof Error ? err.message : String(err)}`);
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
|
+
async function enableCatalogMaintenance(opts) {
|
|
99
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
100
|
+
const onWarn = opts.onWarn ?? ((m) => console.warn(m));
|
|
101
|
+
if (opts.compactionCredentialToken) await ensureCredential(fetchImpl, opts.accountId, opts.catalogToken, opts.compactionCredentialToken, opts.bucket, onWarn);
|
|
102
|
+
await enableMaintenance(fetchImpl, opts.accountId, opts.catalogToken, opts.bucket, onWarn);
|
|
103
|
+
}
|
|
104
|
+
const MAX_BUCKET_NAME = 63;
|
|
105
|
+
function deriveBucketName(prefix, key) {
|
|
106
|
+
const bucket = `${prefix}${key}`.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, MAX_BUCKET_NAME).replace(/-+$/g, "");
|
|
107
|
+
if (!/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/.test(bucket)) throw new Error(`Cannot derive a valid R2 bucket name from prefix "${prefix}" + key "${key}" (got "${bucket}")`);
|
|
108
|
+
return bucket;
|
|
109
|
+
}
|
|
98
110
|
async function selfProvisionCatalog(opts) {
|
|
99
111
|
const { creds, bucket, namespace, corsOrigins } = opts;
|
|
100
112
|
const fetchImpl = opts.fetch ?? fetch;
|
|
@@ -138,4 +150,4 @@ function suppliedCatalog(ref) {
|
|
|
138
150
|
if (!ref.catalogUri || !ref.warehouse || !ref.bucket) throw new Error("suppliedCatalog: catalogUri, warehouse and bucket are all required");
|
|
139
151
|
return ref;
|
|
140
152
|
}
|
|
141
|
-
export { adoptCatalog, allocateCatalogSiteId, selfProvisionCatalog, suppliedCatalog };
|
|
153
|
+
export { adoptCatalog, allocateCatalogSiteId, deriveBucketName, enableCatalogMaintenance, selfProvisionCatalog, suppliedCatalog };
|
package/dist/unsafe-raw.d.mts
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
-
import { icebergAppendRetrying, isCommitRateLimited } from "./_chunks/catalog.mjs";
|
|
1
|
+
import { IcebergConnection, icebergAppendRetrying, isCommitRateLimited } from "./_chunks/catalog.mjs";
|
|
2
2
|
import { icebergAppend, icebergCreateTable, icebergDropTable, icebergManifests, restCatalogConnect, restCatalogCreateNamespace, restCatalogListTables, restCatalogLoadTable, s3SignedResolver } from "./_chunks/libs/icebird.mjs";
|
|
3
|
-
|
|
3
|
+
/** One listed object under a data prefix. */
|
|
4
|
+
interface SweepListedObject {
|
|
5
|
+
key: string;
|
|
6
|
+
uploaded: Date;
|
|
7
|
+
}
|
|
8
|
+
/** One page of a prefix listing. */
|
|
9
|
+
interface SweepListPage {
|
|
10
|
+
objects: readonly SweepListedObject[];
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
cursor?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Minimal storage client `sweepUncommittedOrphans` needs: list-by-prefix +
|
|
16
|
+
* batch delete against the warehouse bucket. Deliberately NOT an S3 SDK
|
|
17
|
+
* client shape — this interface is structurally satisfied by Cloudflare's
|
|
18
|
+
* native `R2Bucket` Workers binding (`env.R2_ANALYTICS.list({ prefix })` /
|
|
19
|
+
* `env.R2_ANALYTICS.delete(keys)`), so a caller running inside a Worker can
|
|
20
|
+
* pass the binding straight through with no adapter. Any other
|
|
21
|
+
* S3-compatible client can be wrapped to this shape.
|
|
22
|
+
*/
|
|
23
|
+
interface SweepStorageClient {
|
|
24
|
+
list: (options: {
|
|
25
|
+
prefix: string;
|
|
26
|
+
cursor?: string;
|
|
27
|
+
}) => Promise<SweepListPage>;
|
|
28
|
+
delete: (keys: string[]) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
interface SweepUncommittedOrphansOptions {
|
|
31
|
+
conn: IcebergConnection;
|
|
32
|
+
s3: SweepStorageClient;
|
|
33
|
+
/**
|
|
34
|
+
* Bare R2 bucket name backing `conn`'s warehouse. Every scanned table's
|
|
35
|
+
* `location` must resolve into this exact bucket; a table whose location
|
|
36
|
+
* points elsewhere is SKIPPED — never guessed at, never touched.
|
|
37
|
+
*/
|
|
38
|
+
bucket: string;
|
|
39
|
+
/**
|
|
40
|
+
* Restrict the sweep to these tables; defaults to every table currently
|
|
41
|
+
* in `conn.namespace` (`conn` already carries the namespace scope, so
|
|
42
|
+
* there's no separate `namespace` field here).
|
|
43
|
+
*/
|
|
44
|
+
tables?: readonly string[];
|
|
45
|
+
/** Only delete objects older than this many hours. Default 48. */
|
|
46
|
+
graceHours?: number;
|
|
47
|
+
/** Hard cap on deletions in one run. Default 500. */
|
|
48
|
+
maxDeletes?: number;
|
|
49
|
+
/** Compute the delete set but never call `s3.delete`. Default false. */
|
|
50
|
+
dryRun?: boolean;
|
|
51
|
+
/** Injectable clock (ms epoch). Defaults to `Date.now`. */
|
|
52
|
+
now?: () => number;
|
|
53
|
+
}
|
|
54
|
+
interface SweepUncommittedOrphansResult {
|
|
55
|
+
/** Tables whose retained snapshots were successfully walked. */
|
|
56
|
+
scannedTables: string[];
|
|
57
|
+
/** Tables skipped because their `location` didn't resolve into `bucket` — never scanned, never touched. */
|
|
58
|
+
skippedTables: string[];
|
|
59
|
+
/** Count of live (referenced-by-some-retained-snapshot) files found across `scannedTables`. */
|
|
60
|
+
liveFileCount: number;
|
|
61
|
+
/** Objects under a scanned table's data prefix with no snapshot reference anywhere, past `graceHours`. */
|
|
62
|
+
candidateCount: number;
|
|
63
|
+
/** Keys deleted (or, under `dryRun`, that WOULD be deleted). */
|
|
64
|
+
deleted: string[];
|
|
65
|
+
dryRun: boolean;
|
|
66
|
+
/** True when `candidateCount` exceeded `maxDeletes` — remaining orphans are left for the next run. */
|
|
67
|
+
cappedAtLimit: boolean;
|
|
68
|
+
/** Set when the sweep did nothing because the table scope listed zero tables (fail-closed — see class docs). */
|
|
69
|
+
reason?: 'zero-tables';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Sweep data files written under a table's `data/` prefix that were NEVER
|
|
73
|
+
* referenced by any snapshot the catalog still retains — a crashed/partial
|
|
74
|
+
* writer's leftovers. This is the one orphan class Cloudflare's own R2 Data
|
|
75
|
+
* Catalog snapshot-expiration GC does not cover.
|
|
76
|
+
*
|
|
77
|
+
* HARD SAFETY:
|
|
78
|
+
* - A listing failure (table load / manifest walk throwing) PROPAGATES.
|
|
79
|
+
* It is never caught and treated as "no live files" — that would make
|
|
80
|
+
* every real data file look orphaned. Callers must let this reject and
|
|
81
|
+
* must not proceed to delete on catch.
|
|
82
|
+
* - Zero tables in scope is treated as "couldn't determine what's live",
|
|
83
|
+
* not "nothing to protect" — the sweep no-ops with `reason: 'zero-tables'`
|
|
84
|
+
* rather than listing/deleting anything.
|
|
85
|
+
* - Deletions are capped at `maxDeletes` per run (default 500).
|
|
86
|
+
* - `dryRun` computes the exact candidate set without ever calling `s3.delete`.
|
|
87
|
+
* - Only ever lists/deletes keys under a scanned table's own
|
|
88
|
+
* `<table.location>/data/` prefix, and every candidate is re-checked to
|
|
89
|
+
* actually start with that prefix before being considered for deletion —
|
|
90
|
+
* never a bare bucket-root listing, never a key outside a data prefix.
|
|
91
|
+
*/
|
|
92
|
+
declare function sweepUncommittedOrphans(opts: SweepUncommittedOrphansOptions): Promise<SweepUncommittedOrphansResult>;
|
|
93
|
+
export { type SweepListPage, type SweepListedObject, type SweepStorageClient, type SweepUncommittedOrphansOptions, type SweepUncommittedOrphansResult, icebergAppend, icebergAppendRetrying, icebergCreateTable, icebergDropTable, icebergManifests, isCommitRateLimited, restCatalogConnect, restCatalogCreateNamespace, restCatalogListTables, restCatalogLoadTable, s3SignedResolver, sweepUncommittedOrphans };
|
package/dist/unsafe-raw.mjs
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
1
|
import { icebergAppend, icebergCreateTable, icebergDropTable, icebergManifests, restCatalogConnect, restCatalogCreateNamespace, restCatalogListTables, restCatalogLoadTable, s3SignedResolver } from "./_chunks/libs/icebird.mjs";
|
|
2
2
|
import { icebergAppendRetrying, isCommitRateLimited } from "./_chunks/catalog.mjs";
|
|
3
|
-
|
|
3
|
+
const DEFAULT_GRACE_HOURS = 48;
|
|
4
|
+
const DEFAULT_MAX_DELETES = 500;
|
|
5
|
+
const HOUR_MS = 3600 * 1e3;
|
|
6
|
+
function splitBucketKey(filePath) {
|
|
7
|
+
if (!filePath.startsWith("s3://")) return null;
|
|
8
|
+
const rest = filePath.slice(5);
|
|
9
|
+
const slash = rest.indexOf("/");
|
|
10
|
+
if (slash < 0) return null;
|
|
11
|
+
return {
|
|
12
|
+
bucket: rest.slice(0, slash),
|
|
13
|
+
key: rest.slice(slash + 1)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async function scanTable(conn, table, bucket) {
|
|
17
|
+
const { metadata } = await restCatalogLoadTable(conn.catalog, {
|
|
18
|
+
namespace: conn.namespace,
|
|
19
|
+
table
|
|
20
|
+
});
|
|
21
|
+
const location = splitBucketKey(String(metadata.location ?? ""));
|
|
22
|
+
if (!location || location.bucket !== bucket) return {
|
|
23
|
+
dataPrefix: null,
|
|
24
|
+
liveKeys: /* @__PURE__ */ new Set()
|
|
25
|
+
};
|
|
26
|
+
const dataPrefix = `${location.key.replace(/\/+$/, "")}/data/`;
|
|
27
|
+
const liveKeys = /* @__PURE__ */ new Set();
|
|
28
|
+
const snapshots = metadata.snapshots ?? [];
|
|
29
|
+
for (const snapshot of snapshots) {
|
|
30
|
+
const manifests = await icebergManifests({
|
|
31
|
+
metadata,
|
|
32
|
+
resolver: conn.resolver,
|
|
33
|
+
snapshotId: snapshot["snapshot-id"]
|
|
34
|
+
});
|
|
35
|
+
for (const manifest of manifests) for (const entry of manifest.entries) {
|
|
36
|
+
if (entry.status === 2) continue;
|
|
37
|
+
const filePath = entry.data_file?.file_path;
|
|
38
|
+
if (!filePath) continue;
|
|
39
|
+
const parsed = splitBucketKey(filePath);
|
|
40
|
+
if (parsed && parsed.bucket === bucket) liveKeys.add(parsed.key);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
dataPrefix,
|
|
45
|
+
liveKeys
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function listAllUnderPrefix(s3, prefix) {
|
|
49
|
+
const out = [];
|
|
50
|
+
let cursor;
|
|
51
|
+
do {
|
|
52
|
+
const page = await s3.list({
|
|
53
|
+
prefix,
|
|
54
|
+
cursor
|
|
55
|
+
});
|
|
56
|
+
out.push(...page.objects);
|
|
57
|
+
cursor = page.truncated ? page.cursor : void 0;
|
|
58
|
+
} while (cursor);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
async function sweepUncommittedOrphans(opts) {
|
|
62
|
+
const { conn, s3, bucket } = opts;
|
|
63
|
+
const graceHours = opts.graceHours ?? DEFAULT_GRACE_HOURS;
|
|
64
|
+
const maxDeletes = opts.maxDeletes ?? DEFAULT_MAX_DELETES;
|
|
65
|
+
const dryRun = opts.dryRun ?? false;
|
|
66
|
+
const graceCutoff = (opts.now ?? Date.now)() - graceHours * HOUR_MS;
|
|
67
|
+
const tables = opts.tables ?? (await restCatalogListTables(conn.catalog, { namespace: conn.namespace })).map((t) => t.name);
|
|
68
|
+
if (tables.length === 0) return {
|
|
69
|
+
scannedTables: [],
|
|
70
|
+
skippedTables: [],
|
|
71
|
+
liveFileCount: 0,
|
|
72
|
+
candidateCount: 0,
|
|
73
|
+
deleted: [],
|
|
74
|
+
dryRun,
|
|
75
|
+
cappedAtLimit: false,
|
|
76
|
+
reason: "zero-tables"
|
|
77
|
+
};
|
|
78
|
+
const scannedTables = [];
|
|
79
|
+
const skippedTables = [];
|
|
80
|
+
let liveFileCount = 0;
|
|
81
|
+
const candidates = [];
|
|
82
|
+
for (const table of tables) {
|
|
83
|
+
const { dataPrefix, liveKeys } = await scanTable(conn, table, bucket);
|
|
84
|
+
if (!dataPrefix) {
|
|
85
|
+
skippedTables.push(table);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
scannedTables.push(table);
|
|
89
|
+
liveFileCount += liveKeys.size;
|
|
90
|
+
const listed = await listAllUnderPrefix(s3, dataPrefix);
|
|
91
|
+
for (const obj of listed) {
|
|
92
|
+
if (!obj.key.startsWith(dataPrefix)) continue;
|
|
93
|
+
if (liveKeys.has(obj.key)) continue;
|
|
94
|
+
if (obj.uploaded.getTime() > graceCutoff) continue;
|
|
95
|
+
candidates.push(obj);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const candidateCount = candidates.length;
|
|
99
|
+
const toDelete = candidates.slice(0, maxDeletes);
|
|
100
|
+
const cappedAtLimit = candidateCount > maxDeletes;
|
|
101
|
+
if (!dryRun && toDelete.length > 0) await s3.delete(toDelete.map((o) => o.key));
|
|
102
|
+
return {
|
|
103
|
+
scannedTables,
|
|
104
|
+
skippedTables,
|
|
105
|
+
liveFileCount,
|
|
106
|
+
candidateCount,
|
|
107
|
+
deleted: toDelete.map((o) => o.key),
|
|
108
|
+
dryRun,
|
|
109
|
+
cappedAtLimit
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export { icebergAppend, icebergAppendRetrying, icebergCreateTable, icebergDropTable, icebergManifests, isCommitRateLimited, restCatalogConnect, restCatalogCreateNamespace, restCatalogListTables, restCatalogLoadTable, s3SignedResolver, sweepUncommittedOrphans };
|
package/package.json
CHANGED