@loaders.gl/scan 5.0.0-alpha.10
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/LICENSE +52 -0
- package/README.md +219 -0
- package/dist/delta.cjs +25568 -0
- package/dist/delta.cjs.map +6 -0
- package/dist/delta.d.ts +11 -0
- package/dist/delta.d.ts.map +1 -0
- package/dist/delta.js +12 -0
- package/dist/delta.js.map +1 -0
- package/dist/dist.min.js +18535 -0
- package/dist/federated-table-scan-source.d.ts +116 -0
- package/dist/federated-table-scan-source.d.ts.map +1 -0
- package/dist/federated-table-scan-source.js +691 -0
- package/dist/federated-table-scan-source.js.map +1 -0
- package/dist/hudi.cjs +25405 -0
- package/dist/hudi.cjs.map +6 -0
- package/dist/hudi.d.ts +58 -0
- package/dist/hudi.d.ts.map +1 -0
- package/dist/hudi.js +147 -0
- package/dist/hudi.js.map +1 -0
- package/dist/iceberg.cjs +27662 -0
- package/dist/iceberg.cjs.map +6 -0
- package/dist/iceberg.d.ts +12 -0
- package/dist/iceberg.d.ts.map +1 -0
- package/dist/iceberg.js +12 -0
- package/dist/iceberg.js.map +1 -0
- package/dist/index.cjs +3540 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/scan-engine.d.ts +44 -0
- package/dist/scan-engine.d.ts.map +1 -0
- package/dist/scan-engine.js +51 -0
- package/dist/scan-engine.js.map +1 -0
- package/dist/scan-query.d.ts +24 -0
- package/dist/scan-query.d.ts.map +1 -0
- package/dist/scan-query.js +5 -0
- package/dist/scan-query.js.map +1 -0
- package/dist/vector-table-scan-source.d.ts +92 -0
- package/dist/vector-table-scan-source.d.ts.map +1 -0
- package/dist/vector-table-scan-source.js +220 -0
- package/dist/vector-table-scan-source.js.map +1 -0
- package/package.json +66 -0
- package/src/delta.ts +18 -0
- package/src/federated-table-scan-source.ts +991 -0
- package/src/hudi.ts +235 -0
- package/src/iceberg.ts +36 -0
- package/src/index.ts +97 -0
- package/src/scan-engine.ts +100 -0
- package/src/scan-query.ts +27 -0
- package/src/vector-table-scan-source.ts +300 -0
package/src/hudi.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
CoreAPI,
|
|
7
|
+
ScanFragment,
|
|
8
|
+
ScanFragmentProvider,
|
|
9
|
+
ScanQueryMetadata,
|
|
10
|
+
ScanQueryMetadataOptions,
|
|
11
|
+
TableScanSource
|
|
12
|
+
} from '@loaders.gl/loader-utils';
|
|
13
|
+
import {createScanQueryMetadata, DataSource} from '@loaders.gl/loader-utils';
|
|
14
|
+
import {ParquetDatasetSource} from '@loaders.gl/parquet/parquet-dataset-source';
|
|
15
|
+
import {PARQUET_TABLE_QUERY_CAPABILITIES} from '@loaders.gl/parquet/parquet-source-capabilities';
|
|
16
|
+
import type {
|
|
17
|
+
ParquetDatasetBatch,
|
|
18
|
+
ParquetDatasetExplain,
|
|
19
|
+
ParquetDatasetReadOptions,
|
|
20
|
+
ParquetPredicate,
|
|
21
|
+
ParquetDatasetSourceOptions
|
|
22
|
+
} from '@loaders.gl/parquet/parquet-source-types';
|
|
23
|
+
|
|
24
|
+
/** A physical Parquet base file recorded by a Hudi snapshot descriptor. */
|
|
25
|
+
export type HudiBaseFile = Readonly<{
|
|
26
|
+
/** Path or URL of the Parquet base file. */
|
|
27
|
+
path: string;
|
|
28
|
+
/** Compressed file size in bytes, when recorded by the descriptor. */
|
|
29
|
+
size?: number;
|
|
30
|
+
/** Number of records in the base file, when recorded by the descriptor. */
|
|
31
|
+
numRecords?: number;
|
|
32
|
+
/** Partition column values associated with the base file. */
|
|
33
|
+
partitionValues?: Readonly<Record<string, unknown>>;
|
|
34
|
+
}>;
|
|
35
|
+
|
|
36
|
+
/** Minimal, portable descriptor used by the Hudi proof of concept. */
|
|
37
|
+
export type HudiSnapshotDescriptor = Readonly<{
|
|
38
|
+
tableType?: 'COPY_ON_WRITE' | 'MERGE_ON_READ';
|
|
39
|
+
basePath?: string;
|
|
40
|
+
completedInstant?: string;
|
|
41
|
+
files: readonly HudiBaseFile[];
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
/** Options for reading a Hudi snapshot descriptor and its Parquet base files. */
|
|
45
|
+
export type HudiSourceOptions = ParquetDatasetSourceOptions & {
|
|
46
|
+
hudi?: {
|
|
47
|
+
baseUrl?: string;
|
|
48
|
+
headers?: HeadersInit;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Query options accepted by a Hudi snapshot scan. */
|
|
53
|
+
export type HudiScanOptions = ParquetDatasetReadOptions;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Minimal read-only Hudi Copy-on-Write source.
|
|
57
|
+
*
|
|
58
|
+
* The POC consumes a small JSON snapshot descriptor containing the selected base files. It does
|
|
59
|
+
* not claim to decode Hudi log files, merge-on-read records, or incremental timeline queries.
|
|
60
|
+
*/
|
|
61
|
+
export class HudiTableSource
|
|
62
|
+
extends DataSource<string | Blob, HudiSourceOptions>
|
|
63
|
+
implements
|
|
64
|
+
TableScanSource<ParquetDatasetBatch, ParquetPredicate>,
|
|
65
|
+
ScanFragmentProvider<ParquetPredicate>
|
|
66
|
+
{
|
|
67
|
+
private descriptorPromise: Promise<HudiSnapshotDescriptor> | null = null;
|
|
68
|
+
|
|
69
|
+
/** Creates a source from a Hudi snapshot descriptor URL or Blob. */
|
|
70
|
+
constructor(data: string | Blob, options: HudiSourceOptions = {}, coreApi?: CoreAPI) {
|
|
71
|
+
super(data, options, undefined, coreApi);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Loads and validates the POC snapshot descriptor. */
|
|
75
|
+
async getDescriptor(signal?: AbortSignal): Promise<HudiSnapshotDescriptor> {
|
|
76
|
+
if (!this.descriptorPromise) {
|
|
77
|
+
this.descriptorPromise = this.loadDescriptor(signal).catch(error => {
|
|
78
|
+
this.descriptorPromise = null;
|
|
79
|
+
throw error;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return this.descriptorPromise;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Discovers the Copy-on-Write Parquet base files in the selected snapshot. */
|
|
86
|
+
async getScanFragments(options: HudiScanOptions = {}): Promise<readonly ScanFragment[]> {
|
|
87
|
+
const descriptor = await this.getDescriptor(options.signal);
|
|
88
|
+
if (descriptor.tableType === 'MERGE_ON_READ') {
|
|
89
|
+
throw new Error('Hudi Merge-on-Read snapshots are not supported by this POC');
|
|
90
|
+
}
|
|
91
|
+
return Object.freeze(
|
|
92
|
+
descriptor.files.map(file =>
|
|
93
|
+
Object.freeze({
|
|
94
|
+
id: file.path,
|
|
95
|
+
uri: this.resolveDataFile(file.path, descriptor.basePath),
|
|
96
|
+
partitionValues: file.partitionValues,
|
|
97
|
+
byteLength: file.size,
|
|
98
|
+
rowCount: file.numRecords,
|
|
99
|
+
metadata: {completedInstant: descriptor.completedInstant}
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Discovers schema and snapshot statistics without decoding every row. */
|
|
106
|
+
async getQueryMetadata(options: ScanQueryMetadataOptions = {}): Promise<ScanQueryMetadata> {
|
|
107
|
+
const fragments = await this.getScanFragments(options);
|
|
108
|
+
if (!fragments.length) throw new Error('Hudi snapshot contains no Parquet base files');
|
|
109
|
+
const dataset = this.createDataset(fragments);
|
|
110
|
+
const schema = await dataset.getSchema({signal: options.signal});
|
|
111
|
+
return createScanQueryMetadata({
|
|
112
|
+
sourceType: 'hudi',
|
|
113
|
+
queryType: 'table',
|
|
114
|
+
execution: {status: 'supported', method: 'read'},
|
|
115
|
+
name: this.data instanceof Blob ? undefined : this.data,
|
|
116
|
+
schema,
|
|
117
|
+
capabilities: {table: PARQUET_TABLE_QUERY_CAPABILITIES},
|
|
118
|
+
statistics: {
|
|
119
|
+
rowCount: fragments.reduce((total, fragment) => total + Number(fragment.rowCount || 0), 0),
|
|
120
|
+
byteLength: fragments.reduce(
|
|
121
|
+
(total, fragment) => total + Number(fragment.byteLength || 0),
|
|
122
|
+
0
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Explains the Hudi snapshot and delegated Parquet physical plan. */
|
|
129
|
+
async getScanPlan(options: HudiScanOptions = {}): Promise<ParquetDatasetExplain> {
|
|
130
|
+
return this.createDataset(await this.getScanFragments(options)).getScanPlan(options);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Reads the selected Hudi snapshot through the shared Parquet executor. */
|
|
134
|
+
read(options: HudiScanOptions = {}): AsyncIterable<ParquetDatasetBatch> {
|
|
135
|
+
return this.createDatasetFromOptions(options).read(options);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Alias for `read()` used by scan-aware consumers. */
|
|
139
|
+
scan(options: HudiScanOptions = {}): AsyncIterable<ParquetDatasetBatch> {
|
|
140
|
+
return this.read(options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async loadDescriptor(signal?: AbortSignal): Promise<HudiSnapshotDescriptor> {
|
|
144
|
+
const text =
|
|
145
|
+
typeof this.data === 'string'
|
|
146
|
+
? await (await this.fetch(this.url, {headers: this.options.hudi?.headers, signal})).text()
|
|
147
|
+
: await this.data.text();
|
|
148
|
+
let descriptor: unknown;
|
|
149
|
+
try {
|
|
150
|
+
descriptor = JSON.parse(text);
|
|
151
|
+
} catch {
|
|
152
|
+
throw new Error('Hudi snapshot descriptor must be valid JSON');
|
|
153
|
+
}
|
|
154
|
+
const files =
|
|
155
|
+
descriptor && typeof descriptor === 'object'
|
|
156
|
+
? (descriptor as {files?: unknown}).files
|
|
157
|
+
: undefined;
|
|
158
|
+
if (!Array.isArray(files)) {
|
|
159
|
+
throw new Error('Hudi snapshot descriptor must contain a files array');
|
|
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
|
+
}
|
|
192
|
+
const value = descriptor as HudiSnapshotDescriptor;
|
|
193
|
+
if (
|
|
194
|
+
value.tableType &&
|
|
195
|
+
value.tableType !== 'COPY_ON_WRITE' &&
|
|
196
|
+
value.tableType !== 'MERGE_ON_READ'
|
|
197
|
+
) {
|
|
198
|
+
throw new Error(`Unsupported Hudi table type: ${String(value.tableType)}`);
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private createDataset(fragments: readonly ScanFragment[]): ParquetDatasetSource {
|
|
204
|
+
return new ParquetDatasetSource(
|
|
205
|
+
fragments.map(fragment => ({
|
|
206
|
+
data: fragment.uri!,
|
|
207
|
+
id: fragment.id,
|
|
208
|
+
partitions: fragment.partitionValues as never,
|
|
209
|
+
metadata: fragment.metadata
|
|
210
|
+
})),
|
|
211
|
+
this.options
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private createDatasetFromOptions(options: HudiScanOptions): ParquetDatasetSource {
|
|
216
|
+
return new ParquetDatasetSource(
|
|
217
|
+
() =>
|
|
218
|
+
this.getScanFragments(options).then(fragments =>
|
|
219
|
+
fragments.map(fragment => ({
|
|
220
|
+
data: fragment.uri!,
|
|
221
|
+
id: fragment.id,
|
|
222
|
+
partitions: fragment.partitionValues as never,
|
|
223
|
+
metadata: fragment.metadata
|
|
224
|
+
}))
|
|
225
|
+
),
|
|
226
|
+
this.options
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private resolveDataFile(path: string, basePath?: string): string {
|
|
231
|
+
const baseUrl = this.options.hudi?.baseUrl || basePath;
|
|
232
|
+
if (baseUrl) return new URL(path, baseUrl).toString();
|
|
233
|
+
return path;
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/iceberg.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Incubating Iceberg table-format adapters.
|
|
7
|
+
*
|
|
8
|
+
* This entry point is intentionally separate from the scan package root. The adapter remains
|
|
9
|
+
* backed by the Parquet implementation while its public API and planning semantics mature.
|
|
10
|
+
*/
|
|
11
|
+
export {IcebergTableSource} from '@loaders.gl/parquet/iceberg-table-source';
|
|
12
|
+
export type {
|
|
13
|
+
IcebergScanOptions,
|
|
14
|
+
IcebergSourceOptions
|
|
15
|
+
} from '@loaders.gl/parquet/iceberg-table-source';
|
|
16
|
+
export {IcebergRestCatalog} from '@loaders.gl/parquet/iceberg-rest-catalog';
|
|
17
|
+
export type {
|
|
18
|
+
IcebergRestCatalogOptions,
|
|
19
|
+
IcebergRestTable,
|
|
20
|
+
IcebergRestTableIdentifier
|
|
21
|
+
} from '@loaders.gl/parquet/iceberg-rest-catalog';
|
|
22
|
+
export type {
|
|
23
|
+
IcebergBoundingBox,
|
|
24
|
+
IcebergDataFile,
|
|
25
|
+
IcebergDeleteFile,
|
|
26
|
+
IcebergManifestFile,
|
|
27
|
+
IcebergParquetFile,
|
|
28
|
+
IcebergPartitionSpec,
|
|
29
|
+
IcebergScanPlan,
|
|
30
|
+
IcebergSchema,
|
|
31
|
+
IcebergSnapshot,
|
|
32
|
+
IcebergSnapshotReference,
|
|
33
|
+
IcebergSpatialFilter,
|
|
34
|
+
IcebergTableMetadata,
|
|
35
|
+
IcebergTableSourceOptions
|
|
36
|
+
} from '@loaders.gl/parquet/iceberg-types';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
export {createScanEngine, registerScanBackend} from './scan-engine';
|
|
6
|
+
// The low-level executor lives in loader-utils so format packages can reuse it without importing
|
|
7
|
+
// this optional module. It is re-exported here as the scan package's common batch adapter.
|
|
8
|
+
export {
|
|
9
|
+
executeTableScanBatches,
|
|
10
|
+
filterTableBatch,
|
|
11
|
+
makeTableScanBatch,
|
|
12
|
+
projectTableBatch,
|
|
13
|
+
projectTableSchema,
|
|
14
|
+
truncateTableBatch
|
|
15
|
+
} from '@loaders.gl/loader-utils';
|
|
16
|
+
export type {TableScanBatchReader, TableScanBatchOperators} from '@loaders.gl/loader-utils';
|
|
17
|
+
export type {
|
|
18
|
+
ScanBackend,
|
|
19
|
+
ScanBackendLoader,
|
|
20
|
+
ScanBackendName,
|
|
21
|
+
ScanEngine,
|
|
22
|
+
ScanEngineOptions
|
|
23
|
+
} from './scan-engine';
|
|
24
|
+
|
|
25
|
+
// Re-export the query vocabulary from one optional entry point for applications. Format adapters
|
|
26
|
+
// should continue importing the lightweight contracts from @loaders.gl/loader-utils.
|
|
27
|
+
export {parseSQLPredicate} from '@loaders.gl/sql';
|
|
28
|
+
export type {ArrowQueryOptions, SQLPredicate} from '@loaders.gl/sql';
|
|
29
|
+
export type {
|
|
30
|
+
RelationalAggregate,
|
|
31
|
+
RelationalExpression,
|
|
32
|
+
RelationalOrderKey,
|
|
33
|
+
TableQueryOptions
|
|
34
|
+
} from '@loaders.gl/loader-utils';
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
createScanQueryMetadata,
|
|
38
|
+
intersectPointCloudBounds,
|
|
39
|
+
selectPointCloudScanTiles,
|
|
40
|
+
validatePointCloudQueryOptions
|
|
41
|
+
} from '@loaders.gl/loader-utils';
|
|
42
|
+
export type {
|
|
43
|
+
CreateScanQueryMetadataOptions,
|
|
44
|
+
PointCloudQueryBounds,
|
|
45
|
+
PointCloudQueryCapabilities,
|
|
46
|
+
PointCloudQueryOptions,
|
|
47
|
+
PointCloudScanChildrenLoader,
|
|
48
|
+
PointCloudScanReadOptions,
|
|
49
|
+
PointCloudScanSource,
|
|
50
|
+
PointCloudScanTile,
|
|
51
|
+
RasterQueryCapabilities,
|
|
52
|
+
RasterQueryOptions,
|
|
53
|
+
ScanBounds,
|
|
54
|
+
ScanColumnMetadata,
|
|
55
|
+
ScanColumnRole,
|
|
56
|
+
ScanExecutionMethod,
|
|
57
|
+
ScanExecutionTelemetry,
|
|
58
|
+
ScanExecutionTelemetryCallback,
|
|
59
|
+
ScanExecutionTelemetryStatus,
|
|
60
|
+
ScanSourceExecutionTelemetry,
|
|
61
|
+
ScanExecutionSupport,
|
|
62
|
+
ScanQueryCapabilities,
|
|
63
|
+
ScanQueryMetadata,
|
|
64
|
+
ScanQueryMetadataOptions,
|
|
65
|
+
ScanQueryMetadataProvider,
|
|
66
|
+
ScanRasterLevel,
|
|
67
|
+
ScanSourceStatistics,
|
|
68
|
+
ScanSpatialMetadata,
|
|
69
|
+
TableScanReadOptions,
|
|
70
|
+
TableScanSource
|
|
71
|
+
} from '@loaders.gl/loader-utils';
|
|
72
|
+
export type {ScanQuery} from './scan-query';
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
FEDERATED_TABLE_QUERY_CAPABILITIES,
|
|
76
|
+
FederatedTableScanSource
|
|
77
|
+
} from './federated-table-scan-source';
|
|
78
|
+
export type {
|
|
79
|
+
FederatedTableBatch,
|
|
80
|
+
FederatedTableBatchProvenance,
|
|
81
|
+
FederatedTableScanExplain,
|
|
82
|
+
FederatedTableScanSourceOptions,
|
|
83
|
+
FederatedTableSchemaPolicy,
|
|
84
|
+
FederatedTableSourceEntry,
|
|
85
|
+
FederatedTableSourceExplain
|
|
86
|
+
} from './federated-table-scan-source';
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
AddressedVectorTableScanSource,
|
|
90
|
+
VectorFeatureTableScanSource,
|
|
91
|
+
VectorTileTableScanSource
|
|
92
|
+
} from './vector-table-scan-source';
|
|
93
|
+
export type {
|
|
94
|
+
VectorFeatureTableScanSourceOptions,
|
|
95
|
+
VectorTableScanSourceOptions,
|
|
96
|
+
VectorTileTableScanSourceOptions
|
|
97
|
+
} from './vector-table-scan-source';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {ArrowTable} from '@loaders.gl/schema';
|
|
6
|
+
import {
|
|
7
|
+
explainArrowTableQuery,
|
|
8
|
+
queryArrowTable,
|
|
9
|
+
type ArrowQueryOptions,
|
|
10
|
+
type SQLPredicate
|
|
11
|
+
} from '@loaders.gl/sql';
|
|
12
|
+
import type {TableQueryExplain} from '@loaders.gl/loader-utils';
|
|
13
|
+
|
|
14
|
+
/** Names accepted by the optional scan engine factory. */
|
|
15
|
+
export type ScanBackendName = 'arrow' | (string & {});
|
|
16
|
+
|
|
17
|
+
/** A backend implementation for the Arrow table query surface. */
|
|
18
|
+
export type ScanBackend = Readonly<{
|
|
19
|
+
/** Stable backend name reported by the engine. */
|
|
20
|
+
name: ScanBackendName;
|
|
21
|
+
/** Executes a query over one in-memory Arrow table. */
|
|
22
|
+
query(sourceTable: ArrowTable, options?: ArrowQueryOptions): ArrowTable;
|
|
23
|
+
/** Optional asynchronous execution hook for backends that compile or schedule work. */
|
|
24
|
+
queryAsync?: (sourceTable: ArrowTable, options?: ArrowQueryOptions) => Promise<ArrowTable>;
|
|
25
|
+
/** Explains a query without evaluating table rows. */
|
|
26
|
+
explain(sourceTable: ArrowTable, options?: ArrowQueryOptions): TableQueryExplain<SQLPredicate>;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
/** Lazy loader used to register an optional scan backend. */
|
|
30
|
+
export type ScanBackendLoader = () => ScanBackend | Promise<ScanBackend>;
|
|
31
|
+
|
|
32
|
+
/** Options for creating a scan engine. */
|
|
33
|
+
export type ScanEngineOptions = Readonly<{
|
|
34
|
+
/** Backend to use. Arrow is the built-in reference backend. */
|
|
35
|
+
backend?: ScanBackendName;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
/** Stable application-facing scan engine returned by {@link createScanEngine}. */
|
|
39
|
+
export type ScanEngine = ScanBackend & {
|
|
40
|
+
/** Executes a query asynchronously, allowing backends to schedule remote or compiled work. */
|
|
41
|
+
queryAsync: (sourceTable: ArrowTable, options?: ArrowQueryOptions) => Promise<ArrowTable>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const scanBackends = new Map<ScanBackendName, ScanBackend | ScanBackendLoader>();
|
|
45
|
+
|
|
46
|
+
const arrowScanBackend: ScanBackend = Object.freeze({
|
|
47
|
+
name: 'arrow',
|
|
48
|
+
query: queryArrowTable,
|
|
49
|
+
queryAsync: (sourceTable, options) =>
|
|
50
|
+
Promise.resolve().then(queryArrowTable.bind(null, sourceTable, options)),
|
|
51
|
+
explain: explainArrowTableQuery
|
|
52
|
+
});
|
|
53
|
+
|
|
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);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Registers a lazy scan backend without adding it to the default bundle.
|
|
60
|
+
*
|
|
61
|
+
* This is the extension point for future DuckDB and GPU implementations. The backend package can
|
|
62
|
+
* register a dynamic loader from application code while the public import remains
|
|
63
|
+
* `@loaders.gl/scan`.
|
|
64
|
+
*/
|
|
65
|
+
export function registerScanBackend(name: ScanBackendName, loader: ScanBackendLoader): void {
|
|
66
|
+
if (!name || !loader) {
|
|
67
|
+
throw new Error('A scan backend name and loader are required');
|
|
68
|
+
}
|
|
69
|
+
scanBackends.set(name, loader);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Creates the configured scan engine.
|
|
74
|
+
*
|
|
75
|
+
* The factory is asynchronous even for Arrow so optional backends can be loaded lazily later
|
|
76
|
+
* without changing the application-facing API.
|
|
77
|
+
*/
|
|
78
|
+
export async function createScanEngine(options: ScanEngineOptions = {}): Promise<ScanEngine> {
|
|
79
|
+
const backendName = options.backend || 'arrow';
|
|
80
|
+
const registeredBackend = scanBackends.get(backendName);
|
|
81
|
+
if (!registeredBackend) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Scan backend "${backendName}" is not registered. The built-in backend is "arrow".`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const backend =
|
|
87
|
+
typeof registeredBackend === 'function' ? await registeredBackend() : registeredBackend;
|
|
88
|
+
if (backend.name !== backendName) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Scan backend loader returned "${backend.name}" while "${backendName}" was requested.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const query = backend.query.bind(backend);
|
|
94
|
+
const explain = backend.explain.bind(backend);
|
|
95
|
+
const queryAsync = backend.queryAsync
|
|
96
|
+
? backend.queryAsync.bind(backend)
|
|
97
|
+
: async (sourceTable: ArrowTable, queryOptions?: ArrowQueryOptions) =>
|
|
98
|
+
backend.query.call(backend, sourceTable, queryOptions);
|
|
99
|
+
return Object.freeze({...backend, query, explain, queryAsync});
|
|
100
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Source-neutral values emitted by a scan query editor.
|
|
7
|
+
*
|
|
8
|
+
* This type is deliberately smaller than any one format's executor options. Applications can
|
|
9
|
+
* pass it between metadata-driven controls and adapters, which may normalize or ignore values
|
|
10
|
+
* that are not supported by a particular source family.
|
|
11
|
+
*/
|
|
12
|
+
export type ScanQuery = Readonly<{
|
|
13
|
+
/** Output columns; an empty selection means all columns. */
|
|
14
|
+
columns?: readonly string[];
|
|
15
|
+
/** Maximum number of rows, features, or points to return. */
|
|
16
|
+
limit?: number;
|
|
17
|
+
/** Optional source-coordinate bounding box in minX, minY, maxX, maxY order. */
|
|
18
|
+
boundingBox?: readonly [number, number, number, number];
|
|
19
|
+
/** Optional raster overview or point-cloud hierarchy level. */
|
|
20
|
+
level?: number;
|
|
21
|
+
/** Optional point-cloud minimum hierarchy level. */
|
|
22
|
+
minimumLevel?: number;
|
|
23
|
+
/** Optional point-cloud maximum hierarchy level. */
|
|
24
|
+
maximumLevel?: number;
|
|
25
|
+
/** Optional point-cloud target spacing. */
|
|
26
|
+
targetSpacing?: number;
|
|
27
|
+
}>;
|