lakeql 0.1.7 → 0.1.8

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/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { createParquetLake, readParquetMetadata, memoryStore, writePartitionedParquet, broadcastJoin, createOutputManifest, partitionedParquetOutputEntries, fingerprint, writeOutputManifest } from './chunk-RZL45ZSN.js';
2
+ import { createParquetLake, readParquetMetadata, memoryStore, writePartitionedParquet, broadcastJoin, createOutputManifest, partitionedParquetOutputEntries, fingerprint, writeOutputManifest } from './chunk-MXGEAVHL.js';
3
3
  import { __commonJS, __toESM, LakeqlError, matches, evaluate, isTimestampValue } from './chunk-TFD5RFKB.js';
4
4
  import { readFile, mkdir, writeFile } from 'fs/promises';
5
5
  import { dirname } from 'path';
@@ -1,4 +1,4 @@
1
- import { Lake, stableStringify, vectorValue, scalarVectorValue, batchFromColumns, withObjectStoreReadControls, throwIfAborted, readControlSignal, readParquetObjectBatches, readIcebergParquetDeletes } from './chunk-RZL45ZSN.js';
1
+ import { Lake, stableStringify, vectorValue, scalarVectorValue, batchFromColumns, withObjectStoreReadControls, throwIfAborted, readControlSignal, readParquetObjectBatches, readIcebergParquetDeletes } from './chunk-MXGEAVHL.js';
2
2
  import { LakeqlError, jsonSafeValue, matches } from './chunk-TFD5RFKB.js';
3
3
 
4
4
  // ../core/dist/in-memory.js
@@ -157,6 +157,92 @@ function projectPhysicalColumns(row, columns) {
157
157
  return out;
158
158
  }
159
159
 
160
+ // ../core/dist/object-store-json-cache.js
161
+ var ObjectStoreJsonCache = class {
162
+ store;
163
+ prefix;
164
+ ttlMs;
165
+ now;
166
+ constructor(options) {
167
+ this.store = options.store;
168
+ this.prefix = normalizeCachePrefix(options.prefix);
169
+ this.ttlMs = options.ttlMs;
170
+ this.now = options.now ?? Date.now;
171
+ }
172
+ async get(key) {
173
+ const path = this.pathFor(key);
174
+ const bytes = await this.store.get(path);
175
+ if (bytes === null)
176
+ return void 0;
177
+ let parsed;
178
+ try {
179
+ parsed = JSON.parse(new TextDecoder().decode(bytes));
180
+ } catch (cause) {
181
+ throw new LakeqlError("LAKEQL_CATALOG_ERROR", `Invalid JSON cache entry at ${path}`, {
182
+ path,
183
+ cause
184
+ });
185
+ }
186
+ if (!isStoredJsonCacheEntry(parsed)) {
187
+ throw new LakeqlError("LAKEQL_CATALOG_ERROR", `Invalid JSON cache entry at ${path}`, {
188
+ path
189
+ });
190
+ }
191
+ if (parsed.expiresAt !== void 0 && parsed.expiresAt <= this.now()) {
192
+ await this.store.delete(path);
193
+ return void 0;
194
+ }
195
+ const entry = { value: parsed.value };
196
+ if (parsed.expiresAt !== void 0)
197
+ entry.expiresAt = parsed.expiresAt;
198
+ return entry;
199
+ }
200
+ async set(key, entry) {
201
+ if (entry.value === void 0) {
202
+ throw new LakeqlError("LAKEQL_TYPE_ERROR", "JSON cache value is not JSON serializable", {
203
+ key
204
+ });
205
+ }
206
+ const stored = { value: entry.value };
207
+ if (entry.expiresAt !== void 0) {
208
+ stored.expiresAt = entry.expiresAt;
209
+ } else if (this.ttlMs !== void 0) {
210
+ stored.expiresAt = this.now() + this.ttlMs;
211
+ }
212
+ let json;
213
+ try {
214
+ json = JSON.stringify(stored);
215
+ } catch (cause) {
216
+ throw new LakeqlError("LAKEQL_TYPE_ERROR", "JSON cache value is not JSON serializable", {
217
+ key,
218
+ cause
219
+ });
220
+ }
221
+ await this.store.put(this.pathFor(key), new TextEncoder().encode(json), {
222
+ contentType: "application/json"
223
+ });
224
+ }
225
+ async delete(key) {
226
+ await this.store.delete(this.pathFor(key));
227
+ }
228
+ pathFor(key) {
229
+ return `${this.prefix}/${encodeURIComponent(key)}.json`;
230
+ }
231
+ };
232
+ function objectStoreJsonCache(options) {
233
+ return new ObjectStoreJsonCache(options);
234
+ }
235
+ function normalizeCachePrefix(prefix) {
236
+ const trimmed = prefix.replace(/^\/+|\/+$/g, "");
237
+ if (trimmed === "") {
238
+ throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "JSON cache prefix must be non-empty");
239
+ }
240
+ return trimmed;
241
+ }
242
+ function isStoredJsonCacheEntry(value) {
243
+ return typeof value === "object" && value !== null && "value" in value;
244
+ }
245
+
160
246
  // ../core/dist/vector-join.js
161
247
  function vectorHashJoin(left, right, options) {
162
248
  const normalized = validateVectorJoinOptions(left, right, options);
@@ -581,37 +667,61 @@ var IcebergRestCatalog = class {
581
667
  url;
582
668
  namespace;
583
669
  table;
584
- prefix;
670
+ explicitPrefix;
671
+ warehouse;
672
+ accessDelegation;
585
673
  token;
586
674
  fetchFn;
675
+ configPromise;
676
+ prefixPromise;
587
677
  constructor(options) {
588
678
  this.url = options.url;
589
679
  this.namespace = namespaceParts(options.namespace);
590
680
  this.table = requiredNonEmptyString(options.table, "table");
591
- this.prefix = catalogPrefixParts(options.prefix);
681
+ this.explicitPrefix = options.prefix === void 0 ? void 0 : catalogPrefixParts(options.prefix);
682
+ this.warehouse = options.warehouse === void 0 ? void 0 : requiredNonEmptyString(options.warehouse, "warehouse");
683
+ this.accessDelegation = options.accessDelegation?.join(",");
592
684
  this.token = options.token;
593
685
  this.fetchFn = options.fetch ?? fetch;
594
686
  }
595
- async loadTable(store) {
596
- const response = await this.requestJson(this.tableUrl(), { method: "GET" });
597
- if (!isRecord(response) || typeof response["metadata-location"] !== "string") {
598
- throw new LakeqlError("LAKEQL_CATALOG_ERROR", "Invalid Iceberg REST load table response", {
599
- url: this.tableUrl()
687
+ async loadTable(store, options = {}) {
688
+ const response = await this.loadTableResult();
689
+ const readControls = loadReadControls(options);
690
+ const controlledStore = withObjectStoreReadControls(store, readControls);
691
+ return new IcebergTable(controlledStore, response["metadata-location"], await hydrateMetadataManifests(controlledStore, response.metadata, readControls, options.cache));
692
+ }
693
+ async loadTableResult(options = {}) {
694
+ const url = await this.tableUrl(options.snapshots);
695
+ const headers = this.headers(false);
696
+ if (this.accessDelegation !== void 0)
697
+ headers.set("X-Iceberg-Access-Delegation", this.accessDelegation);
698
+ if (options.ifNoneMatch !== void 0)
699
+ headers.set("If-None-Match", options.ifNoneMatch);
700
+ const response = await this.requestJsonResponse(url, { method: "GET", headers });
701
+ return validateRestLoadTableResult(response.body, url, response.headers.get("etag"));
702
+ }
703
+ async loadConfig() {
704
+ if (this.configPromise === void 0) {
705
+ const url = this.configUrl();
706
+ this.configPromise = this.requestJson(url, { method: "GET" }).then((body) => validateRestCatalogConfig(body, url)).catch((cause) => {
707
+ this.configPromise = void 0;
708
+ throw cause;
600
709
  });
601
710
  }
602
- return new IcebergTable(store, response["metadata-location"], await hydrateMetadataManifests(store, validateMetadata(response.metadata)));
711
+ return await this.configPromise;
603
712
  }
604
713
  async listTables() {
605
- return validateListTablesResponse(await this.requestJson(this.namespaceTablesUrl(), {
714
+ return validateListTablesResponse(await this.requestJson(await this.namespaceTablesUrl(), {
606
715
  method: "GET"
607
716
  }));
608
717
  }
609
718
  async commitAppend(input) {
719
+ const tableUrl = await this.tableUrl();
610
720
  await input.store.put(input.manifestPath, new TextEncoder().encode(`${stableStringify(input.manifest)}
611
721
  `), { contentType: "application/json" });
612
722
  await input.store.put(input.nextMetadataPath, new TextEncoder().encode(`${JSON.stringify(input.metadata, null, 2)}
613
723
  `), { contentType: "application/json" });
614
- const response = await this.fetchFn(this.tableUrl(), {
724
+ const response = await this.fetchFn(tableUrl, {
615
725
  method: "POST",
616
726
  headers: this.headers(true),
617
727
  body: JSON.stringify({
@@ -641,7 +751,7 @@ var IcebergRestCatalog = class {
641
751
  return { committed: false };
642
752
  if (!response.ok) {
643
753
  throw new LakeqlError("LAKEQL_CATALOG_ERROR", "Iceberg REST table commit failed", {
644
- url: this.tableUrl(),
754
+ url: tableUrl,
645
755
  status: response.status,
646
756
  statusText: response.statusText
647
757
  });
@@ -653,9 +763,12 @@ var IcebergRestCatalog = class {
653
763
  };
654
764
  }
655
765
  async requestJson(url, init) {
766
+ return (await this.requestJsonResponse(url, init)).body;
767
+ }
768
+ async requestJsonResponse(url, init) {
656
769
  const response = await this.fetchFn(url, {
657
770
  ...init,
658
- headers: this.headers(init.body !== void 0)
771
+ headers: init.headers ?? this.headers(init.body !== void 0)
659
772
  });
660
773
  if (!response.ok) {
661
774
  throw new LakeqlError("LAKEQL_CATALOG_ERROR", "Iceberg REST catalog request failed", {
@@ -665,7 +778,7 @@ var IcebergRestCatalog = class {
665
778
  });
666
779
  }
667
780
  try {
668
- return await response.json();
781
+ return { body: await response.json(), headers: response.headers };
669
782
  } catch (cause) {
670
783
  throw new LakeqlError("LAKEQL_CATALOG_ERROR", "Iceberg REST catalog response is not JSON", {
671
784
  url,
@@ -681,14 +794,50 @@ var IcebergRestCatalog = class {
681
794
  headers.set("authorization", `Bearer ${this.token}`);
682
795
  return headers;
683
796
  }
684
- tableUrl() {
685
- return restCatalogUrl(this.url, [...this.namespaceTableSegments(), this.table]);
797
+ async tableUrl(snapshots) {
798
+ const url = new URL(restCatalogUrl(this.url, [...await this.namespaceTableSegments(), this.table]));
799
+ if (snapshots !== void 0)
800
+ url.searchParams.set("snapshots", snapshots);
801
+ return url.toString();
802
+ }
803
+ async namespaceTablesUrl() {
804
+ return restCatalogUrl(this.url, await this.namespaceTableSegments());
805
+ }
806
+ async namespaceTableSegments() {
807
+ return [
808
+ "v1",
809
+ ...await this.prefixParts(),
810
+ "namespaces",
811
+ this.namespace.join(""),
812
+ "tables"
813
+ ];
814
+ }
815
+ async prefixParts() {
816
+ if (this.explicitPrefix !== void 0)
817
+ return this.explicitPrefix;
818
+ if (this.warehouse === void 0)
819
+ return [];
820
+ if (this.prefixPromise === void 0)
821
+ this.prefixPromise = this.computePrefixParts();
822
+ return await this.prefixPromise;
686
823
  }
687
- namespaceTablesUrl() {
688
- return restCatalogUrl(this.url, this.namespaceTableSegments());
824
+ async computePrefixParts() {
825
+ try {
826
+ const config = await this.loadConfig();
827
+ const serverPrefix = config.overrides.prefix ?? config.defaults.prefix;
828
+ if (serverPrefix !== void 0 && serverPrefix.trim() !== "") {
829
+ return catalogPrefixParts(serverPrefix);
830
+ }
831
+ } catch {
832
+ return [this.warehouse];
833
+ }
834
+ return [this.warehouse];
689
835
  }
690
- namespaceTableSegments() {
691
- return ["v1", ...this.prefix, "namespaces", this.namespace.join(""), "tables"];
836
+ configUrl() {
837
+ const url = new URL(restCatalogUrl(this.url, ["v1", "config"]));
838
+ if (this.warehouse !== void 0)
839
+ url.searchParams.set("warehouse", this.warehouse);
840
+ return url.toString();
692
841
  }
693
842
  };
694
843
  function icebergRestCatalog(options) {
@@ -703,7 +852,7 @@ var IcebergUnsupportedCatalog = class {
703
852
  this.namespace = namespaceParts(namespace);
704
853
  this.table = requiredNonEmptyString(table, "table");
705
854
  }
706
- async loadTable(_store) {
855
+ async loadTable(_store, _options = {}) {
707
856
  throw this.unsupported("loadTable");
708
857
  }
709
858
  async listTables() {
@@ -743,7 +892,7 @@ async function loadIcebergTable(options) {
743
892
  throwIfAborted(readControls.signal);
744
893
  const text = new TextDecoder().decode(bytes);
745
894
  try {
746
- return new IcebergTable(store, options.metadataPath, await hydrateMetadataManifests(store, validateMetadata(JSON.parse(text)), readControls));
895
+ return new IcebergTable(store, options.metadataPath, await hydrateMetadataManifests(store, validateMetadata(JSON.parse(text)), readControls, options.cache));
747
896
  } catch (cause) {
748
897
  if (cause instanceof LakeqlError)
749
898
  throw cause;
@@ -761,10 +910,23 @@ async function loadIcebergTableFromObjectStore(options) {
761
910
  const versionHintPath = `${metadataPrefix}version-hint.text`;
762
911
  const hintedVersion = await readVersionHint(store, versionHintPath, readControls);
763
912
  const metadataPath = hintedVersion === void 0 ? await latestMetadataPathFromList(store, metadataPrefix, readControls) : `${metadataPrefix}v${hintedVersion}.metadata.json`;
764
- return await loadIcebergTable({ store, metadataPath, ...readControls });
913
+ return await loadIcebergTable({
914
+ store,
915
+ metadataPath,
916
+ ...readControls,
917
+ ...options.cache !== void 0 ? { cache: options.cache } : {}
918
+ });
765
919
  }
766
920
  async function loadIcebergTableFromRest(options) {
767
- return await icebergRestCatalog(options).loadTable(withObjectStoreReadControls(options.store, loadReadControls(options)));
921
+ const catalog = icebergRestCatalog(options);
922
+ const response = await catalog.loadTableResult();
923
+ const baseStore = options.store ?? (options.storeFactory === void 0 ? void 0 : await options.storeFactory({ ...response, catalog }));
924
+ if (baseStore === void 0) {
925
+ throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Iceberg REST table loading requires a store or storeFactory");
926
+ }
927
+ const readControls = loadReadControls(options);
928
+ const store = withObjectStoreReadControls(baseStore, readControls);
929
+ return new IcebergTable(store, response["metadata-location"], await hydrateMetadataManifests(store, response.metadata, readControls, options.cache));
768
930
  }
769
931
  function loadReadControls(options) {
770
932
  const controls = {};
@@ -919,23 +1081,81 @@ function metadataVersionHintPath(metadataPath) {
919
1081
  function supportsConditionalPut(store) {
920
1082
  return typeof store.conditionalPut === "function";
921
1083
  }
922
- async function hydrateMetadataManifests(store, metadata, controls = {}) {
1084
+ async function hydrateMetadataManifests(store, metadata, controls = {}, persistentCache) {
923
1085
  const hydrated = cloneMetadata(metadata);
924
- const tablePrefix = tableLocationObjectPrefix(hydrated.location);
925
- for (const snapshot of hydrated.snapshots) {
1086
+ const tableLocation = tableLocationRef(hydrated.location);
1087
+ const cache = {
1088
+ lists: /* @__PURE__ */ new Map(),
1089
+ manifests: /* @__PURE__ */ new Map(),
1090
+ tableCacheKey: icebergTableCacheKey(hydrated)
1091
+ };
1092
+ if (persistentCache !== void 0)
1093
+ cache.persistent = persistentCache;
1094
+ await Promise.all(hydrated.snapshots.map(async (snapshot) => {
926
1095
  throwIfAborted(controls.signal);
927
- const manifestReferences = snapshot.manifests ?? (snapshot["manifest-list"] !== void 0 ? await readManifestList(store, validateManifestSourcedPath(snapshot["manifest-list"], tablePrefix)) : []);
928
- const manifests = await Promise.all(manifestReferences.map(async (manifest) => {
929
- const manifestPath = validateManifestSourcedPath(manifest.path, tablePrefix);
930
- if (Array.isArray(manifest.files))
931
- return validateManifestPaths(manifest, manifestPath, tablePrefix);
932
- return await readManifest(store, manifestPath, tablePrefix);
933
- }));
1096
+ snapshot.manifests = mergeDeleteManifests(await hydrateSnapshotManifests(store, snapshot, tableLocation, cache, controls));
934
1097
  throwIfAborted(controls.signal);
935
- snapshot.manifests = mergeDeleteManifests(manifests);
936
- }
1098
+ }));
937
1099
  return hydrated;
938
1100
  }
1101
+ async function hydrateSnapshotManifests(store, snapshot, tableLocation, cache, controls) {
1102
+ const manifestReferences = snapshot.manifests ?? (snapshot["manifest-list"] !== void 0 ? await cachedManifestList(store, validateManifestSourcedPath(snapshot["manifest-list"], tableLocation), cache) : []);
1103
+ return await Promise.all(manifestReferences.map(async (manifest) => {
1104
+ throwIfAborted(controls.signal);
1105
+ const manifestPath = validateManifestSourcedPath(manifest.path, tableLocation);
1106
+ if (Array.isArray(manifest.files))
1107
+ return validateManifestPaths(manifest, manifestPath, tableLocation);
1108
+ return await cachedManifest(store, manifestPath, tableLocation, cache);
1109
+ }));
1110
+ }
1111
+ function cachedManifestList(store, path, cache) {
1112
+ const cached = cache.lists.get(path);
1113
+ if (cached !== void 0)
1114
+ return cached;
1115
+ const key = icebergMetadataCacheKey(cache, "manifest-list", path);
1116
+ const promise = readPersistentCache(cache.persistent, key, cloneManifestReferences).then(async (persistent) => {
1117
+ if (persistent !== void 0)
1118
+ return persistent;
1119
+ const manifests = await readManifestList(store, path);
1120
+ await cache.persistent?.set(key, { value: manifests.map(cloneManifestOrReference) });
1121
+ return manifests;
1122
+ }).catch((cause) => {
1123
+ cache.lists.delete(path);
1124
+ throw cause;
1125
+ });
1126
+ cache.lists.set(path, promise);
1127
+ return promise;
1128
+ }
1129
+ function cachedManifest(store, path, tableLocation, cache) {
1130
+ const cached = cache.manifests.get(path);
1131
+ if (cached !== void 0)
1132
+ return cached;
1133
+ const key = icebergMetadataCacheKey(cache, "manifest", path);
1134
+ const promise = readPersistentCache(cache.persistent, key, cloneManifest).then(async (persistent) => {
1135
+ if (persistent !== void 0)
1136
+ return validateManifestPaths(persistent, path, tableLocation);
1137
+ const manifest = await readManifest(store, path, tableLocation);
1138
+ await cache.persistent?.set(key, { value: cloneManifest(manifest) });
1139
+ return manifest;
1140
+ }).catch((cause) => {
1141
+ cache.manifests.delete(path);
1142
+ throw cause;
1143
+ });
1144
+ cache.manifests.set(path, promise);
1145
+ return promise;
1146
+ }
1147
+ async function readPersistentCache(cache, key, clone) {
1148
+ const entry = await cache?.get(key);
1149
+ if (entry === void 0)
1150
+ return void 0;
1151
+ return clone(entry.value);
1152
+ }
1153
+ function icebergTableCacheKey(metadata) {
1154
+ return `${metadata["table-uuid"]}:${metadata.location}`;
1155
+ }
1156
+ function icebergMetadataCacheKey(cache, kind, path) {
1157
+ return `iceberg:${cache.tableCacheKey}:${kind}:${path}`;
1158
+ }
939
1159
  function mergeDeleteManifests(manifests) {
940
1160
  const deleteFiles = manifests.flatMap((manifest) => manifest.deleteFiles ?? []);
941
1161
  const dataManifests = manifests.filter((manifest) => manifest.files.length > 0);
@@ -982,14 +1202,14 @@ async function readManifestList(store, path) {
982
1202
  });
983
1203
  }
984
1204
  }
985
- async function readManifest(store, path, tablePrefix = "") {
1205
+ async function readManifest(store, path, tableLocation = tableLocationRef("")) {
986
1206
  const bytes = await store.get(path);
987
1207
  if (!bytes) {
988
1208
  throw new LakeqlError("LAKEQL_OBJECT_NOT_FOUND", `No Iceberg manifest at ${path}`, { path });
989
1209
  }
990
1210
  try {
991
1211
  const manifest = avroObjectContainer(bytes) ? validateAvroManifest(await decodeAvroObjectContainer(bytes), path) : validateManifest(JSON.parse(new TextDecoder().decode(bytes)), path);
992
- return validateManifestPaths(manifest, path, tablePrefix);
1212
+ return validateManifestPaths(manifest, path, tableLocation);
993
1213
  } catch (cause) {
994
1214
  if (cause instanceof LakeqlError)
995
1215
  throw cause;
@@ -1058,7 +1278,7 @@ function validateAvroManifest(records, path) {
1058
1278
  const manifest = { path, files };
1059
1279
  if (deleteFiles.length > 0)
1060
1280
  manifest.deleteFiles = deleteFiles;
1061
- return validateManifestPaths(manifest, path);
1281
+ return manifest;
1062
1282
  }
1063
1283
  function avroDeleteContent(content, dataFile) {
1064
1284
  if (content === 1 && (String(dataFile.file_format).toLowerCase() === "puffin" || dataFile.content_offset !== void 0 || dataFile.content_size_in_bytes !== void 0)) {
@@ -1167,57 +1387,95 @@ function validateManifestContent(content, path) {
1167
1387
  content
1168
1388
  });
1169
1389
  }
1170
- function validateManifestPaths(manifest, path, tablePrefix = "") {
1171
- validateManifestSourcedPath(manifest.path, tablePrefix);
1172
- for (const file of manifest.files) {
1173
- validateManifestSourcedPath(file.path, tablePrefix);
1174
- for (const deleteFile of file.deleteFiles ?? []) {
1175
- validateManifestSourcedPath(deleteFile.path, tablePrefix);
1176
- }
1177
- }
1178
- for (const deleteFile of manifest.deleteFiles ?? []) {
1179
- validateManifestSourcedPath(deleteFile.path, tablePrefix);
1390
+ function validateManifestPaths(manifest, path, tableLocation = tableLocationRef("")) {
1391
+ validateManifestSourcedPath(manifest.path, tableLocation);
1392
+ return {
1393
+ ...manifest,
1394
+ path,
1395
+ files: manifest.files.map((file) => ({
1396
+ ...file,
1397
+ path: validateManifestSourcedPath(file.path, tableLocation),
1398
+ ...file.deleteFiles !== void 0 ? {
1399
+ deleteFiles: file.deleteFiles.map((deleteFile) => ({
1400
+ ...deleteFile,
1401
+ path: validateManifestSourcedPath(deleteFile.path, tableLocation)
1402
+ }))
1403
+ } : {}
1404
+ })),
1405
+ ...manifest.deleteFiles !== void 0 ? {
1406
+ deleteFiles: manifest.deleteFiles.map((deleteFile) => ({
1407
+ ...deleteFile,
1408
+ path: validateManifestSourcedPath(deleteFile.path, tableLocation)
1409
+ }))
1410
+ } : {}
1411
+ };
1412
+ }
1413
+ function validateManifestSourcedPath(path, tableLocation = tableLocationRef("")) {
1414
+ const normalized = normalizeManifestSourcedPath(path, tableLocation);
1415
+ validateRelativeObjectPath(normalized, path);
1416
+ if (tableLocation.prefix !== "" && normalized !== tableLocation.prefix && !normalized.startsWith(`${tableLocation.prefix}/`)) {
1417
+ throw new LakeqlError("LAKEQL_VALIDATION_ERROR", `Iceberg manifest path escapes table location: ${path}`, {
1418
+ path,
1419
+ tableLocation: tableLocation.prefix
1420
+ });
1180
1421
  }
1181
- return { ...manifest, path };
1422
+ return normalized;
1182
1423
  }
1183
- function validateManifestSourcedPath(path, tablePrefix = "") {
1424
+ function normalizeManifestSourcedPath(path, tableLocation) {
1184
1425
  if (/^(?:[a-z][a-z0-9+.-]*:)?\/\//iu.test(path) || path.startsWith("/")) {
1185
- throw new LakeqlError("LAKEQL_VALIDATION_ERROR", `Iceberg manifest path must be relative: ${path}`, {
1186
- path
1187
- });
1426
+ if (tableLocation.uriAuthority === void 0) {
1427
+ throw new LakeqlError("LAKEQL_VALIDATION_ERROR", `Iceberg manifest path must be relative: ${path}`, {
1428
+ path
1429
+ });
1430
+ }
1431
+ let url;
1432
+ try {
1433
+ url = new URL(path);
1434
+ } catch {
1435
+ throw new LakeqlError("LAKEQL_VALIDATION_ERROR", `Iceberg manifest path is invalid: ${path}`, {
1436
+ path
1437
+ });
1438
+ }
1439
+ const authority = `${url.protocol}//${url.host}`;
1440
+ if (authority !== tableLocation.uriAuthority) {
1441
+ throw new LakeqlError("LAKEQL_VALIDATION_ERROR", `Iceberg manifest path escapes table location: ${path}`, {
1442
+ path,
1443
+ tableLocation: tableLocation.uriAuthority
1444
+ });
1445
+ }
1446
+ return trimSlashes(decodeURIComponent(url.pathname));
1188
1447
  }
1448
+ return path;
1449
+ }
1450
+ function validateRelativeObjectPath(path, originalPath) {
1189
1451
  for (const segment of path.split("/")) {
1190
1452
  let decoded;
1191
1453
  try {
1192
1454
  decoded = decodeURIComponent(segment);
1193
1455
  } catch {
1194
- throw new LakeqlError("LAKEQL_VALIDATION_ERROR", `Iceberg manifest path is invalid: ${path}`, {
1195
- path
1456
+ throw new LakeqlError("LAKEQL_VALIDATION_ERROR", `Iceberg manifest path is invalid: ${originalPath}`, {
1457
+ path: originalPath
1196
1458
  });
1197
1459
  }
1198
1460
  if (decoded === "." || decoded === "..") {
1199
- throw new LakeqlError("LAKEQL_VALIDATION_ERROR", `Iceberg manifest path contains traversal: ${path}`, {
1200
- path
1461
+ throw new LakeqlError("LAKEQL_VALIDATION_ERROR", `Iceberg manifest path contains traversal: ${originalPath}`, {
1462
+ path: originalPath
1201
1463
  });
1202
1464
  }
1203
1465
  }
1204
- if (tablePrefix !== "" && path !== tablePrefix && !path.startsWith(`${tablePrefix}/`)) {
1205
- throw new LakeqlError("LAKEQL_VALIDATION_ERROR", `Iceberg manifest path escapes table location: ${path}`, {
1206
- path,
1207
- tableLocation: tablePrefix
1208
- });
1209
- }
1210
- return path;
1211
1466
  }
1212
- function tableLocationObjectPrefix(location) {
1467
+ function tableLocationRef(location) {
1213
1468
  const trimmed = trimTrailingSlash(location.trim());
1214
1469
  if (trimmed === "" || !trimmed.includes("/"))
1215
- return "";
1470
+ return { prefix: "" };
1216
1471
  if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(trimmed)) {
1217
1472
  const url = new URL(trimmed);
1218
- return trimSlashes(decodeURIComponent(url.pathname));
1473
+ return {
1474
+ prefix: trimSlashes(decodeURIComponent(url.pathname)),
1475
+ uriAuthority: `${url.protocol}//${url.host}`
1476
+ };
1219
1477
  }
1220
- return trimSlashes(trimmed);
1478
+ return { prefix: trimSlashes(trimmed) };
1221
1479
  }
1222
1480
  function trimSlashes(value) {
1223
1481
  return value.replace(/^\/+|\/+$/gu, "");
@@ -1433,6 +1691,9 @@ function cloneManifestOrReference(manifest) {
1433
1691
  }
1434
1692
  return cloneManifest(manifest);
1435
1693
  }
1694
+ function cloneManifestReferences(manifests) {
1695
+ return manifests.map(cloneManifestOrReference);
1696
+ }
1436
1697
  function snapshotManifests(snapshot) {
1437
1698
  return snapshot.manifests ?? [];
1438
1699
  }
@@ -1453,6 +1714,71 @@ function validateListTablesResponse(value) {
1453
1714
  return { namespace: identifier.namespace, name: identifier.name };
1454
1715
  });
1455
1716
  }
1717
+ function validateRestLoadTableResult(value, url, etag) {
1718
+ if (!isRecord(value) || typeof value["metadata-location"] !== "string") {
1719
+ throw new LakeqlError("LAKEQL_CATALOG_ERROR", "Invalid Iceberg REST load table response", {
1720
+ url
1721
+ });
1722
+ }
1723
+ return {
1724
+ "metadata-location": value["metadata-location"],
1725
+ metadata: validateMetadata(value.metadata),
1726
+ config: validateStringRecord(value.config, "Iceberg REST load table config"),
1727
+ "storage-credentials": validateStorageCredentials(value["storage-credentials"]),
1728
+ etag
1729
+ };
1730
+ }
1731
+ function validateRestCatalogConfig(value, url) {
1732
+ if (!isRecord(value)) {
1733
+ throw new LakeqlError("LAKEQL_CATALOG_ERROR", "Invalid Iceberg REST catalog config response", {
1734
+ url
1735
+ });
1736
+ }
1737
+ const config = {
1738
+ defaults: validateStringRecord(value.defaults, "Iceberg REST catalog defaults"),
1739
+ overrides: validateStringRecord(value.overrides, "Iceberg REST catalog overrides")
1740
+ };
1741
+ if (Array.isArray(value.endpoints) && value.endpoints.every((endpoint) => typeof endpoint === "string")) {
1742
+ config.endpoints = value.endpoints;
1743
+ }
1744
+ if (typeof value["idempotency-key-lifetime"] === "string") {
1745
+ config["idempotency-key-lifetime"] = value["idempotency-key-lifetime"];
1746
+ }
1747
+ return config;
1748
+ }
1749
+ function validateStorageCredentials(value) {
1750
+ if (value === void 0)
1751
+ return [];
1752
+ if (!Array.isArray(value)) {
1753
+ throw new LakeqlError("LAKEQL_CATALOG_ERROR", "Invalid Iceberg REST storage credentials");
1754
+ }
1755
+ return value.map((credential) => {
1756
+ if (!isRecord(credential) || typeof credential.prefix !== "string") {
1757
+ throw new LakeqlError("LAKEQL_CATALOG_ERROR", "Invalid Iceberg REST storage credential");
1758
+ }
1759
+ return {
1760
+ prefix: credential.prefix,
1761
+ config: validateStringRecord(credential.config, "Iceberg REST storage credential config")
1762
+ };
1763
+ });
1764
+ }
1765
+ function validateStringRecord(value, label) {
1766
+ if (value === void 0)
1767
+ return {};
1768
+ if (!isRecord(value)) {
1769
+ throw new LakeqlError("LAKEQL_CATALOG_ERROR", `${label} must be an object`);
1770
+ }
1771
+ const out = {};
1772
+ for (const [key, entry] of Object.entries(value)) {
1773
+ if (typeof entry !== "string") {
1774
+ throw new LakeqlError("LAKEQL_CATALOG_ERROR", `${label} values must be strings`, {
1775
+ key
1776
+ });
1777
+ }
1778
+ out[key] = entry;
1779
+ }
1780
+ return out;
1781
+ }
1456
1782
  function validateMetadata(value) {
1457
1783
  if (!isRecord(value))
1458
1784
  throw new LakeqlError("LAKEQL_CATALOG_ERROR", "Iceberg metadata must be an object");
@@ -1691,4 +2017,4 @@ async function* projectIcebergParquetBatches(store, table, path, partition, snap
1691
2017
  }
1692
2018
  }
1693
2019
 
1694
- export { IcebergRestCatalog, IcebergTable, IcebergUnsupportedCatalog, InMemoryRowsScanner, InMemoryRowsStore, ObjectStoreIcebergCommitCatalog, PACKAGE, applyIcebergDeletes, createInMemoryLake, icebergGlueCatalog, icebergNessieCatalog, icebergRestCatalog, inMemoryRowsScanner, loadIcebergTable, loadIcebergTableFromObjectStore, loadIcebergTableFromRest, loadTable, planFiles2 as planFiles, scanBatches, scanPlannedIcebergRows, scanRows, vectorHashJoin };
2020
+ export { IcebergRestCatalog, IcebergTable, IcebergUnsupportedCatalog, InMemoryRowsScanner, InMemoryRowsStore, ObjectStoreIcebergCommitCatalog, ObjectStoreJsonCache, PACKAGE, applyIcebergDeletes, createInMemoryLake, icebergGlueCatalog, icebergNessieCatalog, icebergRestCatalog, inMemoryRowsScanner, loadIcebergTable, loadIcebergTableFromObjectStore, loadIcebergTableFromRest, loadTable, objectStoreJsonCache, planFiles2 as planFiles, scanBatches, scanPlannedIcebergRows, scanRows, vectorHashJoin };