@loaders.gl/scan 5.0.0-alpha.3 → 5.0.0-alpha.5

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/src/hudi.ts CHANGED
@@ -23,9 +23,13 @@ import type {
23
23
 
24
24
  /** A physical Parquet base file recorded by a Hudi snapshot descriptor. */
25
25
  export type HudiBaseFile = Readonly<{
26
+ /** Path or URL of the Parquet base file. */
26
27
  path: string;
28
+ /** Compressed file size in bytes, when recorded by the descriptor. */
27
29
  size?: number;
30
+ /** Number of records in the base file, when recorded by the descriptor. */
28
31
  numRecords?: number;
32
+ /** Partition column values associated with the base file. */
29
33
  partitionValues?: Readonly<Record<string, unknown>>;
30
34
  }>;
31
35
 
@@ -139,7 +143,7 @@ export class HudiTableSource
139
143
  private async loadDescriptor(signal?: AbortSignal): Promise<HudiSnapshotDescriptor> {
140
144
  const text =
141
145
  typeof this.data === 'string'
142
- ? await (await this.fetch(this.data, {headers: this.options.hudi?.headers, signal})).text()
146
+ ? await (await this.fetch(this.url, {headers: this.options.hudi?.headers, signal})).text()
143
147
  : await this.data.text();
144
148
  let descriptor: unknown;
145
149
  try {
@@ -154,6 +158,37 @@ export class HudiTableSource
154
158
  if (!Array.isArray(files)) {
155
159
  throw new Error('Hudi snapshot descriptor must contain a files array');
156
160
  }
161
+ for (const [index, file] of files.entries()) {
162
+ if (!file || typeof file !== 'object' || Array.isArray(file)) {
163
+ throw new Error(`Hudi snapshot file at index ${index} must be an object`);
164
+ }
165
+ const value = file as Record<string, unknown>;
166
+ if (typeof value.path !== 'string' || !value.path) {
167
+ throw new Error(
168
+ `Hudi snapshot file at index ${index} must contain a non-empty string path`
169
+ );
170
+ }
171
+ if (
172
+ value.size !== undefined &&
173
+ (typeof value.size !== 'number' || !Number.isFinite(value.size))
174
+ ) {
175
+ throw new Error(`Hudi snapshot file at index ${index} size must be a finite number`);
176
+ }
177
+ if (
178
+ value.numRecords !== undefined &&
179
+ (typeof value.numRecords !== 'number' || !Number.isFinite(value.numRecords))
180
+ ) {
181
+ throw new Error(`Hudi snapshot file at index ${index} numRecords must be a finite number`);
182
+ }
183
+ if (
184
+ value.partitionValues !== undefined &&
185
+ (!value.partitionValues ||
186
+ typeof value.partitionValues !== 'object' ||
187
+ Array.isArray(value.partitionValues))
188
+ ) {
189
+ throw new Error(`Hudi snapshot file at index ${index} partitionValues must be an object`);
190
+ }
191
+ }
157
192
  const value = descriptor as HudiSnapshotDescriptor;
158
193
  if (
159
194
  value.tableType &&
@@ -41,16 +41,19 @@ export type ScanEngine = ScanBackend & {
41
41
  queryAsync: (sourceTable: ArrowTable, options?: ArrowQueryOptions) => Promise<ArrowTable>;
42
42
  };
43
43
 
44
- const scanBackendLoaders = new Map<ScanBackendName, ScanBackendLoader>();
44
+ const scanBackends = new Map<ScanBackendName, ScanBackend | ScanBackendLoader>();
45
45
 
46
46
  const arrowScanBackend: ScanBackend = Object.freeze({
47
47
  name: 'arrow',
48
48
  query: queryArrowTable,
49
- queryAsync: async (sourceTable, options) => queryArrowTable(sourceTable, options),
49
+ queryAsync: (sourceTable, options) =>
50
+ Promise.resolve().then(queryArrowTable.bind(null, sourceTable, options)),
50
51
  explain: explainArrowTableQuery
51
52
  });
52
53
 
53
- scanBackendLoaders.set('arrow', () => arrowScanBackend);
54
+ // Store the built-in backend directly. Apart from avoiding needless indirection, this keeps
55
+ // coverage stable when browser shards transform this shared module independently.
56
+ scanBackends.set('arrow', arrowScanBackend);
54
57
 
55
58
  /**
56
59
  * Registers a lazy scan backend without adding it to the default bundle.
@@ -63,7 +66,7 @@ export function registerScanBackend(name: ScanBackendName, loader: ScanBackendLo
63
66
  if (!name || !loader) {
64
67
  throw new Error('A scan backend name and loader are required');
65
68
  }
66
- scanBackendLoaders.set(name, loader);
69
+ scanBackends.set(name, loader);
67
70
  }
68
71
 
69
72
  /**
@@ -74,13 +77,14 @@ export function registerScanBackend(name: ScanBackendName, loader: ScanBackendLo
74
77
  */
75
78
  export async function createScanEngine(options: ScanEngineOptions = {}): Promise<ScanEngine> {
76
79
  const backendName = options.backend || 'arrow';
77
- const loader = scanBackendLoaders.get(backendName);
78
- if (!loader) {
80
+ const registeredBackend = scanBackends.get(backendName);
81
+ if (!registeredBackend) {
79
82
  throw new Error(
80
83
  `Scan backend "${backendName}" is not registered. The built-in backend is "arrow".`
81
84
  );
82
85
  }
83
- const backend = await loader();
86
+ const backend =
87
+ typeof registeredBackend === 'function' ? await registeredBackend() : registeredBackend;
84
88
  if (backend.name !== backendName) {
85
89
  throw new Error(
86
90
  `Scan backend loader returned "${backend.name}" while "${backendName}" was requested.`