@gscdump/engine 0.40.1 → 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.
- package/README.md +4 -4
- package/dist/_chunks/duckdb.d.mts +2 -9
- package/dist/_chunks/engine.mjs +3 -7
- package/dist/_chunks/entities.mjs +1 -1
- package/dist/_chunks/index.d.mts +3 -8
- package/dist/_chunks/index2.d.mts +1 -2
- package/dist/_chunks/libs/hyparquet-compressors.mjs +1 -1
- package/dist/_chunks/libs/hyparquet.mjs +1 -63
- package/dist/_chunks/libs/icebird.d.mts +1 -81
- package/dist/_chunks/libs/icebird.mjs +4 -330
- package/dist/_chunks/parquet-plan.mjs +248 -3
- package/dist/_chunks/resolver.mjs +16 -2
- package/dist/_chunks/schema.d.mts +2 -2
- package/dist/_chunks/sink.d.mts +4 -5
- package/dist/_chunks/source.mjs +3 -3
- package/dist/_chunks/storage.d.mts +1 -43
- package/dist/_chunks/types.d.mts +8 -0
- package/dist/adapters/node.d.mts +1 -11
- package/dist/adapters/node.mjs +1 -1
- package/dist/analyzer/index.d.mts +1 -1
- package/dist/entities.d.mts +2 -10
- package/dist/entities.mjs +2 -2
- package/dist/iceberg/index.d.mts +2 -3
- package/dist/iceberg/index.mjs +12 -12
- package/dist/index.d.mts +83 -2
- package/dist/index.mjs +365 -6
- package/dist/period/index.d.mts +2 -2
- package/dist/period/index.mjs +1 -1
- package/dist/planner.mjs +1 -2
- package/dist/resolver/index.d.mts +2 -9
- package/dist/resolver/index.mjs +2 -2
- package/dist/rollups.d.mts +3 -20
- package/dist/rollups.mjs +5 -5
- package/dist/scope.d.mts +1 -2
- package/dist/scope.mjs +1 -1
- package/dist/source/index.d.mts +2 -2
- package/dist/source/index.mjs +2 -2
- package/dist/sql-bind.d.mts +1 -29
- package/dist/sql-bind.mjs +1 -1
- package/package.json +7 -22
- package/dist/_chunks/compaction.mjs +0 -251
- package/dist/adapters/r2-manifest.d.mts +0 -82
- package/dist/adapters/r2-manifest.mjs +0 -364
- package/dist/compaction-public.d.mts +0 -15
- package/dist/compaction-public.mjs +0 -5
- package/dist/snapshot.d.mts +0 -2
- package/dist/snapshot.mjs +0 -1
package/README.md
CHANGED
|
@@ -27,7 +27,6 @@ Optional peers (install only what your runtime needs):
|
|
|
27
27
|
| `@gscdump/engine/contracts` | `StorageEngine`, `Row`, `TableName`, `WriteCtx`, `ManifestEntry`, ... |
|
|
28
28
|
| `@gscdump/engine/schema` | `SCHEMAS`, `allTables`, `inferTable`, column metadata. |
|
|
29
29
|
| `@gscdump/engine/planner` | `resolveToSQL`, `enumeratePartitions`, partition planning. |
|
|
30
|
-
| `@gscdump/engine/snapshot` | `SnapshotIndex` contract for hot/cold snapshot files. |
|
|
31
30
|
| `@gscdump/engine/ingest` | GSC row → storage row helpers (`createRowAccumulator`, `transformGscRow`). |
|
|
32
31
|
| `@gscdump/engine/sql` | SQL literal binding helpers (`bindLiterals`, `formatLiteral`). |
|
|
33
32
|
| `@gscdump/engine/sql-fragments` | Reusable SQL fragments shared across analyzers. |
|
|
@@ -40,8 +39,7 @@ Optional peers (install only what your runtime needs):
|
|
|
40
39
|
| `@gscdump/engine/filesystem` | Node-only `DataSource` + `ManifestStore` adapters. |
|
|
41
40
|
| `@gscdump/engine/hyparquet` | Pure-JS `ParquetCodec`. |
|
|
42
41
|
| `@gscdump/engine/r2` | Cloudflare R2 `DataSource` (structurally typed against `R2Bucket`). |
|
|
43
|
-
| `@gscdump/engine/
|
|
44
|
-
| `@gscdump/engine/iceberg` | Edge-safe GSC Iceberg schema/catalog/append sink. |
|
|
42
|
+
| `@gscdump/engine/iceberg` | Edge-safe GSC dataset schemas, catalog wrappers, and append sink. Generic Iceberg APIs live in `@gscdump/lakehouse`. |
|
|
45
43
|
| `@gscdump/engine/sink-node` | Node-only Iceberg overwrite/delete recovery writer. |
|
|
46
44
|
| `@gscdump/engine/source` | Query-source contracts and factories. |
|
|
47
45
|
|
|
@@ -58,9 +56,11 @@ Optional peers (install only what your runtime needs):
|
|
|
58
56
|
## Related
|
|
59
57
|
|
|
60
58
|
- [`gscdump`](../gscdump) — REST client + query builder (edge-safe peer dep).
|
|
61
|
-
- [`@gscdump/analysis`](../analysis) — analyzers; consumes
|
|
59
|
+
- [`@gscdump/analysis`](../analysis) — analyzers; consumes storage through `AnalysisQuerySource` adapters.
|
|
62
60
|
- [`@gscdump/engine-duckdb-wasm`](../engine-duckdb-wasm) — DuckDB-WASM browser adapter.
|
|
63
61
|
- [`@gscdump/engine-sqlite`](../engine-sqlite) — SQLite / D1 adapter.
|
|
62
|
+
- [`@gscdump/lakehouse`](../lakehouse) — dataset-agnostic Iceberg catalog and
|
|
63
|
+
dataset registry APIs.
|
|
64
64
|
- [`@gscdump/cli`](../cli) — CLI wrapping engine + analysis.
|
|
65
65
|
|
|
66
66
|
## License
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ParquetCodec, QueryExecutor, Row
|
|
1
|
+
import { ParquetCodec, QueryExecutor, Row } from "./storage.mjs";
|
|
2
2
|
interface DuckDBHandle {
|
|
3
3
|
query: (sql: string, params?: unknown[]) => Promise<Row[]>;
|
|
4
4
|
registerFileBuffer: (name: string, bytes: Uint8Array) => Promise<void>;
|
|
@@ -24,11 +24,4 @@ interface DuckDBReadOptions {
|
|
|
24
24
|
}
|
|
25
25
|
declare function createDuckDBCodec(factory: DuckDBFactory, options?: DuckDBReadOptions): ParquetCodec;
|
|
26
26
|
declare function createDuckDBExecutor(factory: DuckDBFactory, options?: DuckDBReadOptions): QueryExecutor;
|
|
27
|
-
|
|
28
|
-
* Canonical "empty-file" SELECT clause for a table. Codecs that need to
|
|
29
|
-
* emit a schema-correct empty Parquet can wrap this in:
|
|
30
|
-
* `COPY (SELECT * FROM <clause> WHERE FALSE) TO '<key>' (FORMAT PARQUET, COMPRESSION ZSTD)`
|
|
31
|
-
* to satisfy the ParquetCodec empty-rows invariant.
|
|
32
|
-
*/
|
|
33
|
-
declare function canonicalEmptyParquetSchema(table: TableName): string;
|
|
34
|
-
export { DuckDBFactory, DuckDBHandle, canonicalEmptyParquetSchema, createDuckDBCodec, createDuckDBExecutor };
|
|
27
|
+
export { DuckDBFactory, DuckDBHandle, createDuckDBCodec, createDuckDBExecutor };
|
package/dist/_chunks/engine.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { coerceRows } from "./coerce.mjs";
|
|
2
1
|
import { dayPartition, hourPartition, inferSearchType, objectKey, tenantPrefix } from "./layout.mjs";
|
|
2
|
+
import { coerceRows } from "./coerce.mjs";
|
|
3
3
|
import { SCHEMAS, TABLE_METADATA, currentSchemaVersion, dateColumnsFor, dedupeByNaturalKey } from "./schema.mjs";
|
|
4
|
-
import { compactTieredImpl, dedupeOverlappingTiers, splitOverlappingTiers } from "./
|
|
4
|
+
import { compactTieredImpl, compileLogicalQueryPlan, dedupeOverlappingTiers, splitOverlappingTiers, substituteNamedFiles } from "./parquet-plan.mjs";
|
|
5
5
|
import { dateReplaceClause as dateReplaceClause$1 } from "../sql-fragments.mjs";
|
|
6
|
-
import { compileLogicalQueryPlan, substituteNamedFiles } from "./parquet-plan.mjs";
|
|
7
6
|
import { sqlEscape } from "../sql-bind.mjs";
|
|
8
7
|
import { encodeJsonBigintSafe } from "@gscdump/lakehouse";
|
|
9
8
|
import { buildLogicalPlan } from "gscdump/query/plan";
|
|
@@ -187,9 +186,6 @@ function createDuckDBExecutor(factory, options = {}) {
|
|
|
187
186
|
function emptyTableSchema(table) {
|
|
188
187
|
return `(FROM (VALUES ${placeholderValues(table)}) t(${columnList(table)}))`;
|
|
189
188
|
}
|
|
190
|
-
function canonicalEmptyParquetSchema(table) {
|
|
191
|
-
return emptyTableSchema(table);
|
|
192
|
-
}
|
|
193
189
|
function dateReplaceClause(table) {
|
|
194
190
|
if (!table) return "";
|
|
195
191
|
return dateReplaceClause$1(dateColumnsFor(table), "string");
|
|
@@ -732,4 +728,4 @@ function createStorageEngine(opts) {
|
|
|
732
728
|
readObject: (key) => dataSource.read(key)
|
|
733
729
|
};
|
|
734
730
|
}
|
|
735
|
-
export { MAX_DAY_BYTES,
|
|
731
|
+
export { MAX_DAY_BYTES, createDuckDBCodec, createDuckDBExecutor, createStorageEngine };
|
|
@@ -1052,4 +1052,4 @@ function createEmptyTypesStore(opts) {
|
|
|
1052
1052
|
}
|
|
1053
1053
|
};
|
|
1054
1054
|
}
|
|
1055
|
-
export {
|
|
1055
|
+
export { INSPECTION_HISTORY_MAX_BYTES, buildQueryDimRecords, createEmptyTypesStore, createIndexingMetadataStore, createInspectionStore, createQueryDimStore, createSitemapStore, emptyTypesKey, hashUrl, hashUrlList, indexingMetadataIndexKey, inspectionBaseKey, inspectionEventKey, inspectionEventsPrefix, inspectionHistoryPrefix, inspectionHistoryShardKey, inspectionIndexKey, inspectionParquetKey, queryDimMetaKey, queryDimParquetKey, sitemapHistoryKey, sitemapIndexKey, sitemapUrlsDeltaKey, sitemapUrlsIndexKey, sitemapUrlsIndexPrefix };
|
package/dist/_chunks/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Row, SearchType as SearchType$1, StorageEngine, TenantCtx } from "./storage.mjs";
|
|
2
|
-
import { EngineError } from "./errors.mjs";
|
|
3
2
|
import { AnalysisParams, AnalysisResult } from "./analysis-types.mjs";
|
|
4
3
|
import { ResolverAdapter } from "./types.mjs";
|
|
5
|
-
import { AnalysisQuerySource, AnalysisSourceKind, AnalyzerRegistry,
|
|
4
|
+
import { AnalysisQuerySource, AnalysisSourceKind, AnalyzerRegistry, QueryRow, SourceCapabilities } from "./registry.mjs";
|
|
5
|
+
import { EngineError } from "./errors.mjs";
|
|
6
6
|
import "./contracts.mjs";
|
|
7
7
|
import { PlannerCapabilities } from "gscdump/query/plan";
|
|
8
8
|
import { BuilderState } from "gscdump/query";
|
|
@@ -44,11 +44,6 @@ declare class AttachedTableMissingError extends Error {
|
|
|
44
44
|
readonly engineError: EngineError;
|
|
45
45
|
constructor(missing: readonly string[]);
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Swap `read_parquet({{KEY}}, union_by_name = true)` for `<schema>.<table>`.
|
|
49
|
-
* Tolerates whitespace variation. Preserves the rest of the SQL verbatim.
|
|
50
|
-
*/
|
|
51
|
-
declare function rewriteForTableSource(sql: string, schema: string, fileSets: Record<string, FileSet>): string;
|
|
52
47
|
declare function createAttachedTableSource(runner: AttachedTableRunner, options: AttachedTableSourceOptions): AnalysisQuerySource;
|
|
53
48
|
interface CreateSqlQuerySourceOptions<TKey extends string> {
|
|
54
49
|
/** Debug-only identifier surfaced on the source for error messages. */
|
|
@@ -106,4 +101,4 @@ declare function runAnalyzerWithEngine(deps: {
|
|
|
106
101
|
engine: StorageEngine;
|
|
107
102
|
}, ctx: TenantCtx, params: AnalysisParams, registry: AnalyzerRegistry): Promise<AnalysisResult>;
|
|
108
103
|
declare function queryRows<TRow = QueryRow>(source: AnalysisQuerySource, state: BuilderState): Promise<TRow[]>;
|
|
109
|
-
export { AttachedTableMissingError, AttachedTableRunner, AttachedTableSourceOptions, CreateSqlQuerySourceOptions, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, createAttachedTableSource, createEngineQuerySource, createSqlQuerySource, queryRows,
|
|
104
|
+
export { AttachedTableMissingError, AttachedTableRunner, AttachedTableSourceOptions, CreateSqlQuerySourceOptions, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, createAttachedTableSource, createEngineQuerySource, createSqlQuerySource, queryRows, runAnalyzerWithEngine };
|
|
@@ -26,7 +26,6 @@ interface ComparisonPeriod {
|
|
|
26
26
|
previous: AnalysisPeriod;
|
|
27
27
|
}
|
|
28
28
|
declare function defaultEndDate(): string;
|
|
29
|
-
declare function defaultStartDate(): string;
|
|
30
29
|
declare function periodOf(params: AnalysisParams): AnalysisPeriod;
|
|
31
30
|
declare function comparisonOf(params: AnalysisParams): ComparisonPeriod;
|
|
32
31
|
declare function resolveWindow(opts: ResolveWindowOptions): ResolvedWindow;
|
|
@@ -51,4 +50,4 @@ type DateRowShape = Record<string, unknown> & {
|
|
|
51
50
|
* once. Existing dates keep all their rows (grouped timeseries safe).
|
|
52
51
|
*/
|
|
53
52
|
declare function padTimeseries<T extends DateRowShape = DateRowShape>(rows: readonly T[], options: PadTimeseriesOptions<T>): T[];
|
|
54
|
-
export { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate,
|
|
53
|
+
export { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, padTimeseries, periodOf, resolveWindow };
|
|
@@ -913,68 +913,6 @@ function equals(a, b, strict = true) {
|
|
|
913
913
|
for (const k of aKeys) if (!equals(a[k], b[k], strict)) return false;
|
|
914
914
|
return true;
|
|
915
915
|
}
|
|
916
|
-
async function byteLengthFromUrlUsingGet(url, requestInit = {}, fetchFn = globalThis.fetch) {
|
|
917
|
-
const controller = new AbortController();
|
|
918
|
-
const headers = new Headers(requestInit.headers);
|
|
919
|
-
headers.set("Range", "bytes=0-0");
|
|
920
|
-
const res = await fetchFn(url, {
|
|
921
|
-
...requestInit,
|
|
922
|
-
headers,
|
|
923
|
-
signal: controller.signal
|
|
924
|
-
});
|
|
925
|
-
if (!res.ok) throw new Error(`fetch with range failed ${res.status}`);
|
|
926
|
-
if (res.status === 206) {
|
|
927
|
-
const contentRange = res.headers.get("Content-Range");
|
|
928
|
-
if (!contentRange) throw new Error("missing content-range header");
|
|
929
|
-
const match = contentRange.match(/bytes \d+-\d+\/(\d+)/);
|
|
930
|
-
if (!match) throw new Error(`invalid content-range header: ${contentRange}`);
|
|
931
|
-
return parseInt(match[1]);
|
|
932
|
-
}
|
|
933
|
-
if (res.status === 200) {
|
|
934
|
-
const contentLength = res.headers.get("Content-Length");
|
|
935
|
-
controller.abort();
|
|
936
|
-
if (contentLength) return parseInt(contentLength);
|
|
937
|
-
}
|
|
938
|
-
throw new Error("server does not support range requests and missing content-length");
|
|
939
|
-
}
|
|
940
|
-
async function byteLengthFromUrl(url, requestInit, customFetch) {
|
|
941
|
-
const fetch = customFetch ?? globalThis.fetch;
|
|
942
|
-
const res = await fetch(url, {
|
|
943
|
-
...requestInit,
|
|
944
|
-
method: "HEAD"
|
|
945
|
-
});
|
|
946
|
-
if (res.status === 403) return byteLengthFromUrlUsingGet(url, requestInit, fetch);
|
|
947
|
-
if (!res.ok) throw new Error(`fetch head failed ${res.status}`);
|
|
948
|
-
const length = res.headers.get("Content-Length");
|
|
949
|
-
if (!length) return byteLengthFromUrlUsingGet(url, requestInit, fetch);
|
|
950
|
-
return parseInt(length);
|
|
951
|
-
}
|
|
952
|
-
async function asyncBufferFromUrl({ url, byteLength, requestInit, fetch: customFetch }) {
|
|
953
|
-
if (!url) throw new Error("missing url");
|
|
954
|
-
const fetch = customFetch ?? globalThis.fetch;
|
|
955
|
-
byteLength ??= await byteLengthFromUrl(url, requestInit, fetch);
|
|
956
|
-
let buffer = void 0;
|
|
957
|
-
const init = requestInit || {};
|
|
958
|
-
return {
|
|
959
|
-
byteLength,
|
|
960
|
-
async slice(start, end) {
|
|
961
|
-
if (buffer) return buffer.then((buffer) => buffer.slice(start, end));
|
|
962
|
-
const headers = new Headers(init.headers);
|
|
963
|
-
const endStr = end === void 0 ? "" : end - 1;
|
|
964
|
-
headers.set("Range", `bytes=${start}-${endStr}`);
|
|
965
|
-
const res = await fetch(url, {
|
|
966
|
-
...init,
|
|
967
|
-
headers
|
|
968
|
-
});
|
|
969
|
-
if (!res.ok || !res.body) throw new Error(`fetch failed ${res.status}`);
|
|
970
|
-
if (res.status === 200) {
|
|
971
|
-
buffer = res.arrayBuffer();
|
|
972
|
-
return buffer.then((buffer) => buffer.slice(start, end));
|
|
973
|
-
} else if (res.status === 206) return res.arrayBuffer();
|
|
974
|
-
else throw new Error(`fetch received unexpected status code ${res.status}`);
|
|
975
|
-
}
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
916
|
function flatten(chunks) {
|
|
979
917
|
if (!chunks) return [];
|
|
980
918
|
if (chunks.length === 1) return chunks[0];
|
|
@@ -2655,4 +2593,4 @@ function toJson(obj) {
|
|
|
2655
2593
|
return obj;
|
|
2656
2594
|
}
|
|
2657
2595
|
new TextDecoder();
|
|
2658
|
-
export { BoundaryOrders, CompressionCodecs, ConvertedTypes, EdgeInterpolationAlgorithms, Encodings, FieldRepetitionTypes, PageTypes, ParquetTypes,
|
|
2596
|
+
export { BoundaryOrders, CompressionCodecs, ConvertedTypes, EdgeInterpolationAlgorithms, Encodings, FieldRepetitionTypes, PageTypes, ParquetTypes, getMaxDefinitionLevel, getSchemaPath, hashParquetValue, isListLike, isMapLike, parquetReadObjects, snappyUncompress, toJson };
|
|
@@ -42,11 +42,6 @@ interface FileCatalog {
|
|
|
42
42
|
conditionalCommits?: boolean;
|
|
43
43
|
}
|
|
44
44
|
type Catalog = RestCatalogContext | FileCatalog;
|
|
45
|
-
interface LoadTableResponse {
|
|
46
|
-
metadataLocation?: string;
|
|
47
|
-
metadata: TableMetadata;
|
|
48
|
-
config: Record<string, string>;
|
|
49
|
-
}
|
|
50
45
|
interface TableMetadata {
|
|
51
46
|
'format-version': number;
|
|
52
47
|
'table-uuid': string;
|
|
@@ -210,42 +205,12 @@ interface Manifest {
|
|
|
210
205
|
// key_metadata?: unknown
|
|
211
206
|
first_row_id?: bigint | number;
|
|
212
207
|
}
|
|
213
|
-
interface ManifestEntry {
|
|
214
|
-
status: 0 | 1 | 2; // 0=existing, 1=added, 2=deleted
|
|
215
|
-
snapshot_id?: bigint;
|
|
216
|
-
sequence_number?: bigint;
|
|
217
|
-
file_sequence_number?: bigint;
|
|
218
|
-
partition_spec_id?: number;
|
|
219
|
-
data_file: DataFile;
|
|
220
|
-
}
|
|
221
208
|
interface FieldSummary {
|
|
222
209
|
contains_null: boolean;
|
|
223
210
|
contains_nan?: boolean | null;
|
|
224
211
|
lower_bound?: Uint8Array | null;
|
|
225
212
|
upper_bound?: Uint8Array | null;
|
|
226
213
|
}
|
|
227
|
-
interface DataFile {
|
|
228
|
-
content: 0 | 1 | 2; // 0=data, 1=position_delete, 2=equality_delete
|
|
229
|
-
file_path: string;
|
|
230
|
-
file_format: 'avro' | 'orc' | 'parquet' | 'puffin';
|
|
231
|
-
partition: Record<string, unknown>; // keyed by partition-field name in Avro
|
|
232
|
-
record_count: bigint;
|
|
233
|
-
file_size_in_bytes: bigint;
|
|
234
|
-
column_sizes?: Record<number, bigint>;
|
|
235
|
-
value_counts?: Record<number, bigint>;
|
|
236
|
-
null_value_counts?: Record<number, bigint>;
|
|
237
|
-
nan_value_counts?: Record<number, bigint>;
|
|
238
|
-
lower_bounds?: Record<number, unknown>;
|
|
239
|
-
upper_bounds?: Record<number, unknown>;
|
|
240
|
-
// key_metadata?: string
|
|
241
|
-
split_offsets?: bigint[];
|
|
242
|
-
equality_ids?: number[];
|
|
243
|
-
sort_order_id?: number;
|
|
244
|
-
first_row_id?: bigint | number;
|
|
245
|
-
referenced_data_file?: string;
|
|
246
|
-
content_offset?: bigint;
|
|
247
|
-
content_size_in_bytes?: bigint;
|
|
248
|
-
}
|
|
249
214
|
/**
|
|
250
215
|
* Create a new table. REST: delegates to the catalog's create endpoint.
|
|
251
216
|
* File: writes the initial `v1.metadata.json` and `version-hint.text` under
|
|
@@ -276,49 +241,4 @@ declare function icebergCreateTable({ catalog, namespace, table, tableUrl, schem
|
|
|
276
241
|
formatVersion?: 2 | 3;
|
|
277
242
|
stageCreate?: boolean;
|
|
278
243
|
}): Promise<TableMetadata>;
|
|
279
|
-
|
|
280
|
-
* Load a single table. Returns the inline TableMetadata, the metadata
|
|
281
|
-
* file location, and any per-table config the server returned.
|
|
282
|
-
*
|
|
283
|
-
* The returned `metadata` and `metadata.location` can be passed directly
|
|
284
|
-
* into `icebergRead({ tableUrl: metadata.location, metadata })`.
|
|
285
|
-
*
|
|
286
|
-
* @param {RestCatalogContext} ctx
|
|
287
|
-
* @param {object} options
|
|
288
|
-
* @param {string | string[]} options.namespace
|
|
289
|
-
* @param {string} options.table
|
|
290
|
-
* @returns {Promise<LoadTableResponse>}
|
|
291
|
-
*/
|
|
292
|
-
declare function restCatalogLoadTable(ctx: RestCatalogContext, { namespace, table }: {
|
|
293
|
-
namespace: string | string[];
|
|
294
|
-
table: string;
|
|
295
|
-
}): Promise<LoadTableResponse>;
|
|
296
|
-
type ManifestList = {
|
|
297
|
-
url: string;
|
|
298
|
-
entries: ManifestEntry[];
|
|
299
|
-
}[];
|
|
300
|
-
/**
|
|
301
|
-
* Predicate applied to each manifest's manifest-list `partitions` field-summary
|
|
302
|
-
* array before entry fetch. Return `false` to skip the manifest's entry fetch.
|
|
303
|
-
*/
|
|
304
|
-
type ManifestPartitionFilter = (partitions: Manifest['partitions'], partitionSpecId: number, manifest: Manifest) => boolean;
|
|
305
|
-
/**
|
|
306
|
-
* Returns manifest entries for a snapshot. Defaults to the current snapshot;
|
|
307
|
-
* pass `snapshotId` to time-travel to a prior snapshot in the metadata's
|
|
308
|
-
* snapshot log.
|
|
309
|
-
*
|
|
310
|
-
* @import {Resolver, TableMetadata, Manifest, ManifestEntry} from '../src/types.js'
|
|
311
|
-
* @typedef {{ url: string, entries: ManifestEntry[] }[]} ManifestList
|
|
312
|
-
* @param {object} options
|
|
313
|
-
* @param {TableMetadata} options.metadata
|
|
314
|
-
* @param {Resolver} [options.resolver]
|
|
315
|
-
* @param {number | bigint} [options.snapshotId] - Optional snapshot id; defaults to `current-snapshot-id`.
|
|
316
|
-
* @returns {Promise<ManifestList>}
|
|
317
|
-
*/
|
|
318
|
-
declare function icebergManifests({ metadata, resolver, snapshotId, partitionFilter }: {
|
|
319
|
-
metadata: TableMetadata;
|
|
320
|
-
resolver?: Resolver;
|
|
321
|
-
snapshotId?: number | bigint;
|
|
322
|
-
partitionFilter?: ManifestPartitionFilter;
|
|
323
|
-
}): Promise<ManifestList>;
|
|
324
|
-
export { icebergCreateTable, icebergManifests, restCatalogLoadTable };
|
|
244
|
+
export { icebergCreateTable };
|
|
@@ -1,189 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
function readZigZag(reader) {
|
|
5
|
-
let result = 0;
|
|
6
|
-
let shift = 0;
|
|
7
|
-
while (true) {
|
|
8
|
-
const byte = reader.view.getUint8(reader.offset++);
|
|
9
|
-
result |= (byte & 127) << shift;
|
|
10
|
-
if (!(byte & 128)) return result >>> 1 ^ -(result & 1);
|
|
11
|
-
shift += 7;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
function readZigZagBigInt(reader) {
|
|
15
|
-
let result = 0n;
|
|
16
|
-
let shift = 0n;
|
|
17
|
-
while (true) {
|
|
18
|
-
const byte = reader.view.getUint8(reader.offset++);
|
|
19
|
-
result |= BigInt(byte & 127) << shift;
|
|
20
|
-
if (!(byte & 128)) return result >> 1n ^ -(result & 1n);
|
|
21
|
-
shift += 7n;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function readAvroString(reader) {
|
|
25
|
-
const length = readZigZag(reader);
|
|
26
|
-
const bytes = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, length);
|
|
27
|
-
reader.offset += length;
|
|
28
|
-
return new TextDecoder().decode(bytes);
|
|
29
|
-
}
|
|
30
|
-
function avroMetadata(reader) {
|
|
31
|
-
if (reader.view.getUint32(reader.offset) !== 1331849729) throw new Error("avro invalid magic bytes");
|
|
32
|
-
reader.offset += 4;
|
|
33
|
-
const metadata = {};
|
|
34
|
-
let mapCount = readZigZag(reader);
|
|
35
|
-
while (mapCount !== 0) {
|
|
36
|
-
if (mapCount < 0) {
|
|
37
|
-
mapCount = -mapCount;
|
|
38
|
-
readZigZag(reader);
|
|
39
|
-
}
|
|
40
|
-
for (let i = 0; i < mapCount; i++) {
|
|
41
|
-
const key = readAvroString(reader);
|
|
42
|
-
metadata[key] = readAvroString(reader);
|
|
43
|
-
}
|
|
44
|
-
mapCount = readZigZag(reader);
|
|
45
|
-
}
|
|
46
|
-
metadata["avro.schema"] = JSON.parse(metadata["avro.schema"]);
|
|
47
|
-
if (metadata["schema"]) metadata["schema"] = JSON.parse(metadata["schema"]);
|
|
48
|
-
if (metadata["iceberg.schema"]) metadata["iceberg.schema"] = JSON.parse(metadata["iceberg.schema"]);
|
|
49
|
-
const syncMarker = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, 16);
|
|
50
|
-
reader.offset += 16;
|
|
51
|
-
return {
|
|
52
|
-
metadata,
|
|
53
|
-
syncMarker
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function avroRead({ reader, metadata, syncMarker }) {
|
|
57
|
-
const blocks = [];
|
|
58
|
-
while (reader.offset < reader.view.byteLength) {
|
|
59
|
-
let recordCount = readZigZag(reader);
|
|
60
|
-
if (recordCount === 0) break;
|
|
61
|
-
if (recordCount < 0) recordCount = -recordCount;
|
|
62
|
-
const blockSize = readZigZag(reader);
|
|
63
|
-
let data = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, blockSize);
|
|
64
|
-
reader.offset += blockSize;
|
|
65
|
-
const blockSync = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, 16);
|
|
66
|
-
reader.offset += 16;
|
|
67
|
-
for (let i = 0; i < 16; i++) if (blockSync[i] !== syncMarker[i]) throw new Error("sync marker does not match");
|
|
68
|
-
const codec = metadata["avro.codec"];
|
|
69
|
-
if (codec === "deflate") data = gunzip(data);
|
|
70
|
-
else if (codec !== "null") throw new Error(`unsupported codec: ${codec}`);
|
|
71
|
-
const { fields } = metadata["avro.schema"];
|
|
72
|
-
const dataReader = {
|
|
73
|
-
view: new DataView(data.buffer, data.byteOffset, data.byteLength),
|
|
74
|
-
offset: 0
|
|
75
|
-
};
|
|
76
|
-
for (let i = 0; i < recordCount; i++) {
|
|
77
|
-
const obj = {};
|
|
78
|
-
for (const field of fields) {
|
|
79
|
-
const value = readType(dataReader, field.type);
|
|
80
|
-
obj[field.name] = value;
|
|
81
|
-
}
|
|
82
|
-
blocks.push(obj);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return blocks;
|
|
86
|
-
}
|
|
87
|
-
function readType(reader, type) {
|
|
88
|
-
if (type === "null") return;
|
|
89
|
-
else if (Array.isArray(type)) return readType(reader, type[readZigZag(reader)]);
|
|
90
|
-
else if (typeof type === "object" && type.type === "record") {
|
|
91
|
-
const obj = {};
|
|
92
|
-
for (const subField of type.fields) obj[subField.name] = readType(reader, subField.type);
|
|
93
|
-
return obj;
|
|
94
|
-
} else if (typeof type === "object" && type.type === "array") {
|
|
95
|
-
const arr = [];
|
|
96
|
-
while (true) {
|
|
97
|
-
let count = readZigZag(reader);
|
|
98
|
-
if (count === 0) break;
|
|
99
|
-
if (count < 0) {
|
|
100
|
-
count = -count;
|
|
101
|
-
readZigZag(reader);
|
|
102
|
-
}
|
|
103
|
-
for (let i = 0; i < count; i++) arr.push(readType(reader, type.items));
|
|
104
|
-
}
|
|
105
|
-
return arr;
|
|
106
|
-
} else if (typeof type === "object" && type.type === "map") {
|
|
107
|
-
const map = {};
|
|
108
|
-
while (true) {
|
|
109
|
-
let count = readZigZag(reader);
|
|
110
|
-
if (count === 0) break;
|
|
111
|
-
if (count < 0) {
|
|
112
|
-
count = -count;
|
|
113
|
-
readZigZag(reader);
|
|
114
|
-
}
|
|
115
|
-
for (let i = 0; i < count; i++) {
|
|
116
|
-
const key = readType(reader, "string");
|
|
117
|
-
map[key] = readType(reader, type.values);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return map;
|
|
121
|
-
} else if (typeof type === "object" && type.type === "enum") return type.symbols[readZigZag(reader)];
|
|
122
|
-
else if (typeof type === "object" && "logicalType" in type && type.logicalType) if (type.logicalType === "date" && type.type === "int") {
|
|
123
|
-
const value = readZigZag(reader);
|
|
124
|
-
return /* @__PURE__ */ new Date(value * 864e5);
|
|
125
|
-
} else if (type.logicalType === "time-millis" && type.type === "int") return readZigZag(reader);
|
|
126
|
-
else if (type.logicalType === "time-micros" && type.type === "long") return readZigZagBigInt(reader);
|
|
127
|
-
else if (type.logicalType === "timestamp-millis" && type.type === "long") {
|
|
128
|
-
const value = readZigZagBigInt(reader);
|
|
129
|
-
return new Date(Number(value));
|
|
130
|
-
} else if (type.logicalType === "timestamp-micros" && type.type === "long") {
|
|
131
|
-
const value = readZigZagBigInt(reader);
|
|
132
|
-
return new Date(Number(value / 1000n));
|
|
133
|
-
} else if (type.logicalType === "timestamp-nanos" && type.type === "long") {
|
|
134
|
-
const value = readZigZagBigInt(reader);
|
|
135
|
-
return new Date(Number(value / 1000000n));
|
|
136
|
-
} else if (type.logicalType === "decimal" && "precision" in type) {
|
|
137
|
-
const bytes = type.type === "fixed" ? readFixed(reader, type.size) : readType(reader, type.type);
|
|
138
|
-
const factor = 10 ** -(type.scale || 0);
|
|
139
|
-
return parseDecimal(bytes) * factor;
|
|
140
|
-
} else if (type.logicalType === "uuid" && type.type === "fixed" && type.size === 16) return bytesToUuid(readFixed(reader, 16));
|
|
141
|
-
else {
|
|
142
|
-
console.warn(`unknown logical type: ${type.logicalType}`);
|
|
143
|
-
return type.type === "fixed" ? readFixed(reader, type.size) : readType(reader, type.type);
|
|
144
|
-
}
|
|
145
|
-
else if (typeof type === "object" && type.type === "fixed") return readFixed(reader, type.size);
|
|
146
|
-
else if (type === "boolean") {
|
|
147
|
-
const value = reader.view.getUint8(reader.offset) === 1;
|
|
148
|
-
reader.offset++;
|
|
149
|
-
return value;
|
|
150
|
-
} else if (type === "int") return readZigZag(reader);
|
|
151
|
-
else if (type === "long") return readZigZagBigInt(reader);
|
|
152
|
-
else if (type === "float") {
|
|
153
|
-
const value = reader.view.getFloat32(reader.offset, true);
|
|
154
|
-
reader.offset += 4;
|
|
155
|
-
return value;
|
|
156
|
-
} else if (type === "double") {
|
|
157
|
-
const value = reader.view.getFloat64(reader.offset, true);
|
|
158
|
-
reader.offset += 8;
|
|
159
|
-
return value;
|
|
160
|
-
} else if (type === "bytes") {
|
|
161
|
-
const length = readZigZag(reader);
|
|
162
|
-
const bytes = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, length);
|
|
163
|
-
reader.offset += length;
|
|
164
|
-
return bytes;
|
|
165
|
-
} else if (type === "string") {
|
|
166
|
-
const length = readZigZag(reader);
|
|
167
|
-
const bytes = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, length);
|
|
168
|
-
const text = new TextDecoder().decode(bytes);
|
|
169
|
-
reader.offset += length;
|
|
170
|
-
return text;
|
|
171
|
-
} else if (typeof type === "object" && typeof type.type === "string") return readType(reader, type.type);
|
|
172
|
-
else throw new Error(`unsupported type: ${JSON.stringify(type)}`);
|
|
173
|
-
}
|
|
174
|
-
function readFixed(reader, size) {
|
|
175
|
-
const bytes = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, size);
|
|
176
|
-
reader.offset += size;
|
|
177
|
-
return bytes;
|
|
178
|
-
}
|
|
179
|
-
function bytesToUuid(bytes) {
|
|
180
|
-
let hex = "";
|
|
181
|
-
for (let i = 0; i < 16; i++) {
|
|
182
|
-
hex += bytes[i].toString(16).padStart(2, "0");
|
|
183
|
-
if (i === 3 || i === 5 || i === 7 || i === 9) hex += "-";
|
|
184
|
-
}
|
|
185
|
-
return hex;
|
|
186
|
-
}
|
|
1
|
+
import "./hyparquet.mjs";
|
|
2
|
+
import "./hyparquet-writer.mjs";
|
|
3
|
+
import "./hyparquet-compressors.mjs";
|
|
187
4
|
function uuid4() {
|
|
188
5
|
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
|
|
189
6
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
@@ -191,72 +8,6 @@ function uuid4() {
|
|
|
191
8
|
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
192
9
|
});
|
|
193
10
|
}
|
|
194
|
-
function translateS3Url(url) {
|
|
195
|
-
if (url.startsWith("s3a://") || url.startsWith("s3://")) {
|
|
196
|
-
const rest = url.slice(url.indexOf("://") + 3);
|
|
197
|
-
const slashIndex = rest.indexOf("/");
|
|
198
|
-
if (slashIndex === -1) throw new Error("Invalid S3 URL, missing \"/\" after bucket");
|
|
199
|
-
return `https://${rest.slice(0, slashIndex)}.s3.amazonaws.com${rest.slice(slashIndex)}`;
|
|
200
|
-
}
|
|
201
|
-
return url;
|
|
202
|
-
}
|
|
203
|
-
function urlResolver({ requestInit } = {}) {
|
|
204
|
-
return {
|
|
205
|
-
reader(url, byteLength) {
|
|
206
|
-
return asyncBufferFromUrl({
|
|
207
|
-
url: translateS3Url(url),
|
|
208
|
-
byteLength,
|
|
209
|
-
requestInit
|
|
210
|
-
});
|
|
211
|
-
},
|
|
212
|
-
writer(url, options) {
|
|
213
|
-
const w = new ByteWriter();
|
|
214
|
-
w.finish = async function() {
|
|
215
|
-
const target = translateS3Url(url);
|
|
216
|
-
const body = w.getBytes().slice();
|
|
217
|
-
const headers = {};
|
|
218
|
-
if (requestInit?.headers) new Headers(requestInit.headers).forEach((v, k) => {
|
|
219
|
-
headers[k] = v;
|
|
220
|
-
});
|
|
221
|
-
if (options?.ifNoneMatch) headers["If-None-Match"] = options.ifNoneMatch;
|
|
222
|
-
const res = await fetch(target, {
|
|
223
|
-
...requestInit,
|
|
224
|
-
method: "PUT",
|
|
225
|
-
headers,
|
|
226
|
-
body
|
|
227
|
-
});
|
|
228
|
-
if (!res.ok) {
|
|
229
|
-
const err = /* @__PURE__ */ new Error(`PUT ${url}: ${res.status} ${res.statusText}`);
|
|
230
|
-
err.status = res.status;
|
|
231
|
-
throw err;
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
return w;
|
|
235
|
-
},
|
|
236
|
-
async deleter(url) {
|
|
237
|
-
const res = await fetch(translateS3Url(url), {
|
|
238
|
-
...requestInit,
|
|
239
|
-
method: "DELETE"
|
|
240
|
-
});
|
|
241
|
-
if (!res.ok && res.status !== 404) throw new Error(`DELETE ${url}: ${res.status} ${res.statusText}`);
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
async function fetchAvroRecords(url, resolver, byteLength) {
|
|
246
|
-
const lengthHint = byteLength !== void 0 && Number.isFinite(byteLength) ? byteLength : void 0;
|
|
247
|
-
const ab = await resolver.reader(url, lengthHint);
|
|
248
|
-
const buffer = await ab.slice(0, ab.byteLength);
|
|
249
|
-
const reader = {
|
|
250
|
-
view: new DataView(buffer),
|
|
251
|
-
offset: 0
|
|
252
|
-
};
|
|
253
|
-
const { metadata, syncMarker } = await avroMetadata(reader);
|
|
254
|
-
return await avroRead({
|
|
255
|
-
reader,
|
|
256
|
-
metadata,
|
|
257
|
-
syncMarker
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
11
|
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
261
12
|
function parseIcebergJson(text) {
|
|
262
13
|
let i = 0;
|
|
@@ -403,14 +154,6 @@ function stringifyIcebergJson(value, indent = 2) {
|
|
|
403
154
|
}
|
|
404
155
|
return serialize(value, 0);
|
|
405
156
|
}
|
|
406
|
-
async function restCatalogLoadTable(ctx, { namespace, table }) {
|
|
407
|
-
const body = parseIcebergJson(await (await restFetch(ctx, `namespaces/${encodeNamespace(namespace)}/tables/${encodeURIComponent(table)}`)).text());
|
|
408
|
-
return {
|
|
409
|
-
metadataLocation: body["metadata-location"],
|
|
410
|
-
metadata: body.metadata,
|
|
411
|
-
config: body.config ?? {}
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
157
|
async function restCatalogCreateTable(ctx, { namespace, table, schema, location, partitionSpec, writeOrder, stageCreate, properties }) {
|
|
415
158
|
const ns = encodeNamespace(namespace);
|
|
416
159
|
const body = {
|
|
@@ -765,75 +508,6 @@ function maxPartitionFieldId(partitionFields = []) {
|
|
|
765
508
|
for (const pf of partitionFields) if (max < pf["field-id"]) max = pf["field-id"];
|
|
766
509
|
return max;
|
|
767
510
|
}
|
|
768
|
-
async function icebergManifests({ metadata, resolver, snapshotId, partitionFilter }) {
|
|
769
|
-
resolver ??= urlResolver();
|
|
770
|
-
const rawTarget = snapshotId ?? metadata["current-snapshot-id"];
|
|
771
|
-
if (rawTarget == null || rawTarget < 0) throw new Error("No current snapshot id found in table metadata");
|
|
772
|
-
const targetId = BigInt(rawTarget);
|
|
773
|
-
const snapshot = metadata.snapshots?.find((s) => BigInt(s["snapshot-id"]) === targetId);
|
|
774
|
-
if (!snapshot) throw new Error(`Snapshot ${rawTarget} not found in metadata`);
|
|
775
|
-
let manifests = [];
|
|
776
|
-
if (snapshot["manifest-list"]) {
|
|
777
|
-
const manifestListUrl = snapshot["manifest-list"];
|
|
778
|
-
manifests = await fetchAvroRecords(manifestListUrl, resolver);
|
|
779
|
-
} else if (snapshot.manifests) manifests = snapshot.manifests;
|
|
780
|
-
else throw new Error("No manifest information found in snapshot");
|
|
781
|
-
if (partitionFilter) manifests = manifests.filter((manifest) => {
|
|
782
|
-
let keep = true;
|
|
783
|
-
try {
|
|
784
|
-
keep = partitionFilter(manifest.partitions, manifest.partition_spec_id ?? 0, manifest) !== false;
|
|
785
|
-
} catch {
|
|
786
|
-
keep = true;
|
|
787
|
-
}
|
|
788
|
-
return keep;
|
|
789
|
-
});
|
|
790
|
-
return await fetchManifests(manifests, resolver);
|
|
791
|
-
}
|
|
792
|
-
const MANIFEST_FETCH_CONCURRENCY = 8;
|
|
793
|
-
async function fetchOneManifest(manifest, resolver) {
|
|
794
|
-
const url = manifest.manifest_path;
|
|
795
|
-
const entries = await fetchAvroRecords(url, resolver, Number(manifest.manifest_length));
|
|
796
|
-
for (const entry of entries) {
|
|
797
|
-
entry.partition_spec_id = manifest.partition_spec_id ?? 0;
|
|
798
|
-
if (entry.sequence_number === void 0) entry.sequence_number = manifest.sequence_number ?? 0n;
|
|
799
|
-
if (entry.status === 1) {
|
|
800
|
-
if (entry.sequence_number === void 0) entry.sequence_number = manifest.sequence_number;
|
|
801
|
-
if (entry.file_sequence_number === void 0) entry.file_sequence_number = manifest.sequence_number;
|
|
802
|
-
} else if (entry.sequence_number === void 0 || entry.file_sequence_number === void 0) throw new Error("iceberg manifest entry missing sequence number");
|
|
803
|
-
}
|
|
804
|
-
assignFirstRowIds(manifest, entries);
|
|
805
|
-
return {
|
|
806
|
-
url,
|
|
807
|
-
entries
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
async function fetchManifests(manifests, resolver) {
|
|
811
|
-
const results = new Array(manifests.length);
|
|
812
|
-
let next = 0;
|
|
813
|
-
async function worker() {
|
|
814
|
-
while (next < manifests.length) {
|
|
815
|
-
const i = next++;
|
|
816
|
-
results[i] = await fetchOneManifest(manifests[i], resolver);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
const poolSize = Math.min(MANIFEST_FETCH_CONCURRENCY, manifests.length);
|
|
820
|
-
const workers = [];
|
|
821
|
-
for (let w = 0; w < poolSize; w++) workers.push(worker());
|
|
822
|
-
await Promise.all(workers);
|
|
823
|
-
return results;
|
|
824
|
-
}
|
|
825
|
-
function assignFirstRowIds(manifest, entries) {
|
|
826
|
-
if (manifest.content !== 0 || manifest.first_row_id == null) return;
|
|
827
|
-
let nextFirstRowId = BigInt(manifest.first_row_id);
|
|
828
|
-
for (const entry of entries) {
|
|
829
|
-
const dataFile = entry.data_file;
|
|
830
|
-
if (dataFile.content !== 0) continue;
|
|
831
|
-
if (dataFile.first_row_id == null) {
|
|
832
|
-
dataFile.first_row_id = nextFirstRowId;
|
|
833
|
-
nextFirstRowId += BigInt(dataFile.record_count);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
511
|
Object.freeze({
|
|
838
512
|
maxAttempts: 50,
|
|
839
513
|
initialMs: 50,
|
|
@@ -887,4 +561,4 @@ new TextEncoder();
|
|
|
887
561
|
return () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
888
562
|
})();
|
|
889
563
|
new TextEncoder();
|
|
890
|
-
export { icebergCreateTable
|
|
564
|
+
export { icebergCreateTable };
|