@qualithm/arrow-flight-sql-js 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -0
- package/README.md +433 -0
- package/dist/arrow.d.ts +65 -0
- package/dist/arrow.d.ts.map +1 -0
- package/dist/arrow.js +250 -0
- package/dist/arrow.js.map +1 -0
- package/dist/client.d.ts +416 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1087 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +128 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +181 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/index.d.ts +4 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/index.js +33 -0
- package/dist/generated/index.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics.d.ts +217 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +304 -0
- package/dist/metrics.js.map +1 -0
- package/dist/pool.d.ts +161 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +434 -0
- package/dist/pool.js.map +1 -0
- package/dist/proto.d.ts +168 -0
- package/dist/proto.d.ts.map +1 -0
- package/dist/proto.js +417 -0
- package/dist/proto.js.map +1 -0
- package/dist/query-builder.d.ts +1 -0
- package/dist/query-builder.d.ts.map +1 -0
- package/dist/query-builder.js +3 -0
- package/dist/query-builder.js.map +1 -0
- package/dist/retry.d.ts +92 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +212 -0
- package/dist/retry.js.map +1 -0
- package/dist/types.d.ts +325 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/package.json +82 -0
- package/proto/Flight.proto +645 -0
- package/proto/FlightSql.proto +1925 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,1087 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arrow Flight SQL Client
|
|
3
|
+
*
|
|
4
|
+
* A TypeScript client for communicating with Arrow Flight SQL servers.
|
|
5
|
+
* Modeled after the official Arrow Flight SQL clients (Java, C++, Go).
|
|
6
|
+
*/
|
|
7
|
+
import * as grpc from "@grpc/grpc-js";
|
|
8
|
+
import { RecordBatchReader } from "apache-arrow";
|
|
9
|
+
import { collectToTable, parseFlightData, tryParseSchema } from "./arrow";
|
|
10
|
+
import { AuthenticationError, ConnectionError, FlightSqlError } from "./errors";
|
|
11
|
+
import { getFlightServiceDefinition } from "./generated";
|
|
12
|
+
import { encodeActionClosePreparedStatementRequest, encodeActionCreatePreparedStatementRequest, encodeCommandGetCatalogs, encodeCommandGetDbSchemas, encodeCommandGetExportedKeys, encodeCommandGetImportedKeys, encodeCommandGetPrimaryKeys, encodeCommandGetTables, encodeCommandGetTableTypes, encodeCommandPreparedStatementQuery, encodeCommandStatementQuery, encodeCommandStatementUpdate, getBytesField, parseProtoFields } from "./proto";
|
|
13
|
+
// Default configuration values
|
|
14
|
+
const defaultConnectTimeoutMs = 30_000;
|
|
15
|
+
const defaultRequestTimeoutMs = 60_000;
|
|
16
|
+
/**
|
|
17
|
+
* Flight SQL client for executing queries and managing data with Arrow Flight SQL servers.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const client = new FlightSqlClient({
|
|
22
|
+
* host: "localhost",
|
|
23
|
+
* port: 50051,
|
|
24
|
+
* tls: false,
|
|
25
|
+
* auth: { type: "bearer", token: "my-token" }
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* await client.connect()
|
|
29
|
+
*
|
|
30
|
+
* const result = await client.execute("SELECT * FROM my_table")
|
|
31
|
+
* for await (const batch of result.stream()) {
|
|
32
|
+
* console.log(batch.numRows)
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* await client.close()
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class FlightSqlClient {
|
|
39
|
+
options;
|
|
40
|
+
grpcClient = null;
|
|
41
|
+
flightService = null;
|
|
42
|
+
authToken = null;
|
|
43
|
+
connected = false;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.options = {
|
|
46
|
+
...options,
|
|
47
|
+
tls: options.tls !== false,
|
|
48
|
+
port: options.port,
|
|
49
|
+
connectTimeoutMs: options.connectTimeoutMs ?? defaultConnectTimeoutMs,
|
|
50
|
+
requestTimeoutMs: options.requestTimeoutMs ?? defaultRequestTimeoutMs
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// ===========================================================================
|
|
54
|
+
// Connection Lifecycle
|
|
55
|
+
// ===========================================================================
|
|
56
|
+
/**
|
|
57
|
+
* Establish connection to the Flight SQL server and perform authentication.
|
|
58
|
+
*
|
|
59
|
+
* @throws {ConnectionError} If connection cannot be established
|
|
60
|
+
* @throws {AuthenticationError} If authentication fails
|
|
61
|
+
*/
|
|
62
|
+
async connect() {
|
|
63
|
+
if (this.connected) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
// Load the Flight service definition
|
|
68
|
+
const packageDef = await getFlightServiceDefinition();
|
|
69
|
+
// Navigate to arrow.flight.protocol.FlightService
|
|
70
|
+
const arrowPackage = packageDef.arrow;
|
|
71
|
+
const flightPackage = arrowPackage.flight;
|
|
72
|
+
const protocolPackage = flightPackage.protocol;
|
|
73
|
+
this.flightService = protocolPackage.FlightService;
|
|
74
|
+
// Create channel credentials
|
|
75
|
+
const credentials = this.options.credentials !== undefined ? this.options.credentials : this.createCredentials();
|
|
76
|
+
// Create the gRPC client
|
|
77
|
+
const address = `${this.options.host}:${String(this.options.port)}`;
|
|
78
|
+
this.grpcClient = new this.flightService(address, credentials, {
|
|
79
|
+
"grpc.max_receive_message_length": -1, // Unlimited
|
|
80
|
+
"grpc.max_send_message_length": -1,
|
|
81
|
+
"grpc.keepalive_time_ms": 30_000,
|
|
82
|
+
"grpc.keepalive_timeout_ms": 10_000
|
|
83
|
+
});
|
|
84
|
+
// Wait for the channel to be ready
|
|
85
|
+
await this.waitForReady();
|
|
86
|
+
// Perform authentication handshake if configured
|
|
87
|
+
if (this.options.auth && this.options.auth.type !== "none") {
|
|
88
|
+
await this.authenticate(this.options.auth);
|
|
89
|
+
}
|
|
90
|
+
this.connected = true;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
this.cleanup();
|
|
94
|
+
if (error instanceof FlightSqlError) {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
throw new ConnectionError(`Failed to connect to ${this.options.host}:${String(this.options.port)}`, {
|
|
98
|
+
cause: error
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Close the connection and release resources.
|
|
104
|
+
*/
|
|
105
|
+
close() {
|
|
106
|
+
this.cleanup();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Check if the client is connected.
|
|
110
|
+
*/
|
|
111
|
+
isConnected() {
|
|
112
|
+
return this.connected;
|
|
113
|
+
}
|
|
114
|
+
// ===========================================================================
|
|
115
|
+
// Flight SQL Query Operations
|
|
116
|
+
// ===========================================================================
|
|
117
|
+
/**
|
|
118
|
+
* Execute a SQL query and return a QueryResult for retrieving results.
|
|
119
|
+
*
|
|
120
|
+
* @param query - SQL query string
|
|
121
|
+
* @param options - Optional execution options
|
|
122
|
+
* @returns QueryResult with stream() and collect() methods
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* const result = await client.query("SELECT * FROM users")
|
|
127
|
+
*
|
|
128
|
+
* // Stream record batches
|
|
129
|
+
* for await (const batch of result.stream()) {
|
|
130
|
+
* console.log(batch.numRows)
|
|
131
|
+
* }
|
|
132
|
+
*
|
|
133
|
+
* // Or collect all into a table
|
|
134
|
+
* const table = await result.collect()
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
async query(query, options) {
|
|
138
|
+
this.ensureConnected();
|
|
139
|
+
// Build CommandStatementQuery with proper protobuf encoding
|
|
140
|
+
const command = encodeCommandStatementQuery(query, options?.transactionId);
|
|
141
|
+
// Create FlightDescriptor with CMD type
|
|
142
|
+
const descriptor = {
|
|
143
|
+
type: 2, // CMD
|
|
144
|
+
cmd: command
|
|
145
|
+
};
|
|
146
|
+
const flightInfo = await this.getFlightInfo(descriptor);
|
|
147
|
+
const schema = tryParseSchema(flightInfo.schema);
|
|
148
|
+
return new QueryResult(this, flightInfo, schema);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Execute a SQL query and return flight info for retrieving results.
|
|
152
|
+
* @deprecated Use query() instead for a more ergonomic API
|
|
153
|
+
*
|
|
154
|
+
* @param query - SQL query string
|
|
155
|
+
* @param options - Optional execution options
|
|
156
|
+
* @returns FlightInfo containing endpoints for data retrieval
|
|
157
|
+
*/
|
|
158
|
+
async execute(query, options) {
|
|
159
|
+
this.ensureConnected();
|
|
160
|
+
// Build CommandStatementQuery with proper protobuf encoding
|
|
161
|
+
const command = encodeCommandStatementQuery(query, options?.transactionId);
|
|
162
|
+
// Create FlightDescriptor with CMD type
|
|
163
|
+
const descriptor = {
|
|
164
|
+
type: 2, // CMD
|
|
165
|
+
cmd: command
|
|
166
|
+
};
|
|
167
|
+
return this.getFlightInfo(descriptor);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Execute a SQL update statement (INSERT, UPDATE, DELETE).
|
|
171
|
+
*
|
|
172
|
+
* @param query - SQL statement
|
|
173
|
+
* @param options - Optional execution options
|
|
174
|
+
* @returns Number of rows affected
|
|
175
|
+
*/
|
|
176
|
+
async executeUpdate(query, options) {
|
|
177
|
+
this.ensureConnected();
|
|
178
|
+
// Build CommandStatementUpdate with proper protobuf encoding
|
|
179
|
+
const command = encodeCommandStatementUpdate(query, options?.transactionId);
|
|
180
|
+
const descriptor = {
|
|
181
|
+
type: 2, // CMD
|
|
182
|
+
cmd: command
|
|
183
|
+
};
|
|
184
|
+
// Execute via DoPut and extract affected row count
|
|
185
|
+
const flightInfo = await this.getFlightInfo(descriptor);
|
|
186
|
+
// For updates, the server typically returns the count in the first endpoint
|
|
187
|
+
// We'll implement the full flow in a later milestone
|
|
188
|
+
return flightInfo.totalRecords;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Create a prepared statement for repeated execution.
|
|
192
|
+
*
|
|
193
|
+
* @param query - SQL query with optional parameter placeholders
|
|
194
|
+
* @param options - Optional prepared statement options
|
|
195
|
+
* @returns PreparedStatement that can be executed multiple times
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* const stmt = await client.prepare("SELECT * FROM users WHERE id = ?")
|
|
200
|
+
* const result = await stmt.executeQuery()
|
|
201
|
+
* await stmt.close()
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
async prepare(query, options) {
|
|
205
|
+
this.ensureConnected();
|
|
206
|
+
// Use CreatePreparedStatement action
|
|
207
|
+
const actionBody = encodeActionCreatePreparedStatementRequest(query, options?.transactionId);
|
|
208
|
+
const action = {
|
|
209
|
+
type: "CreatePreparedStatement",
|
|
210
|
+
body: actionBody
|
|
211
|
+
};
|
|
212
|
+
// Execute the action and parse the response
|
|
213
|
+
let handle;
|
|
214
|
+
let datasetSchema = null;
|
|
215
|
+
let parameterSchema = null;
|
|
216
|
+
for await (const result of this.doAction(action)) {
|
|
217
|
+
// Parse ActionCreatePreparedStatementResult
|
|
218
|
+
// Fields:
|
|
219
|
+
// 1: prepared_statement_handle (bytes)
|
|
220
|
+
// 2: dataset_schema (bytes) - Arrow IPC schema
|
|
221
|
+
// 3: parameter_schema (bytes) - Arrow IPC schema
|
|
222
|
+
const fields = parseProtoFields(result.body);
|
|
223
|
+
handle = getBytesField(fields, 1);
|
|
224
|
+
const datasetSchemaBytes = getBytesField(fields, 2);
|
|
225
|
+
const parameterSchemaBytes = getBytesField(fields, 3);
|
|
226
|
+
if (datasetSchemaBytes && datasetSchemaBytes.length > 0) {
|
|
227
|
+
datasetSchema = tryParseSchema(datasetSchemaBytes);
|
|
228
|
+
}
|
|
229
|
+
if (parameterSchemaBytes && parameterSchemaBytes.length > 0) {
|
|
230
|
+
parameterSchema = tryParseSchema(parameterSchemaBytes);
|
|
231
|
+
}
|
|
232
|
+
break; // Only expect one result
|
|
233
|
+
}
|
|
234
|
+
if (!handle) {
|
|
235
|
+
throw new FlightSqlError("Failed to create prepared statement: no handle returned");
|
|
236
|
+
}
|
|
237
|
+
return new PreparedStatement(this, handle, datasetSchema, parameterSchema);
|
|
238
|
+
}
|
|
239
|
+
// ===========================================================================
|
|
240
|
+
// Catalog Introspection
|
|
241
|
+
// ===========================================================================
|
|
242
|
+
/**
|
|
243
|
+
* Get the list of catalogs available on the server.
|
|
244
|
+
*
|
|
245
|
+
* @returns Array of catalog information
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```typescript
|
|
249
|
+
* const catalogs = await client.getCatalogs()
|
|
250
|
+
* for (const catalog of catalogs) {
|
|
251
|
+
* console.log(catalog.catalogName)
|
|
252
|
+
* }
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
async getCatalogs() {
|
|
256
|
+
this.ensureConnected();
|
|
257
|
+
const command = encodeCommandGetCatalogs();
|
|
258
|
+
const descriptor = {
|
|
259
|
+
type: 2, // CMD
|
|
260
|
+
cmd: command
|
|
261
|
+
};
|
|
262
|
+
const flightInfo = await this.getFlightInfo(descriptor);
|
|
263
|
+
return this.fetchCatalogResults(flightInfo, (row) => ({
|
|
264
|
+
catalogName: row["catalog_name"]
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get the list of schemas available in a catalog.
|
|
269
|
+
*
|
|
270
|
+
* @param catalog - Optional catalog name to filter by
|
|
271
|
+
* @param schemaFilterPattern - Optional SQL LIKE pattern to filter schema names
|
|
272
|
+
* @returns Array of schema information
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```typescript
|
|
276
|
+
* // Get all schemas
|
|
277
|
+
* const schemas = await client.getSchemas()
|
|
278
|
+
*
|
|
279
|
+
* // Get schemas in specific catalog
|
|
280
|
+
* const schemas = await client.getSchemas("my_catalog")
|
|
281
|
+
*
|
|
282
|
+
* // Get schemas matching pattern
|
|
283
|
+
* const schemas = await client.getSchemas(undefined, "public%")
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
async getSchemas(catalog, schemaFilterPattern) {
|
|
287
|
+
this.ensureConnected();
|
|
288
|
+
const command = encodeCommandGetDbSchemas(catalog, schemaFilterPattern);
|
|
289
|
+
const descriptor = {
|
|
290
|
+
type: 2, // CMD
|
|
291
|
+
cmd: command
|
|
292
|
+
};
|
|
293
|
+
const flightInfo = await this.getFlightInfo(descriptor);
|
|
294
|
+
return this.fetchCatalogResults(flightInfo, (row) => ({
|
|
295
|
+
catalogName: row["catalog_name"],
|
|
296
|
+
schemaName: row["db_schema_name"]
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Get the list of tables available.
|
|
301
|
+
*
|
|
302
|
+
* @param options - Filter options
|
|
303
|
+
* @returns Array of table information
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```typescript
|
|
307
|
+
* // Get all tables
|
|
308
|
+
* const tables = await client.getTables()
|
|
309
|
+
*
|
|
310
|
+
* // Get tables in specific catalog/schema
|
|
311
|
+
* const tables = await client.getTables({
|
|
312
|
+
* catalog: "my_catalog",
|
|
313
|
+
* schemaPattern: "public"
|
|
314
|
+
* })
|
|
315
|
+
*
|
|
316
|
+
* // Get only views
|
|
317
|
+
* const views = await client.getTables({ tableTypes: ["VIEW"] })
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
async getTables(options) {
|
|
321
|
+
this.ensureConnected();
|
|
322
|
+
const command = encodeCommandGetTables({
|
|
323
|
+
catalog: options?.catalog,
|
|
324
|
+
dbSchemaFilterPattern: options?.schemaPattern,
|
|
325
|
+
tableNameFilterPattern: options?.tablePattern,
|
|
326
|
+
tableTypes: options?.tableTypes,
|
|
327
|
+
includeSchema: options?.includeSchema
|
|
328
|
+
});
|
|
329
|
+
const descriptor = {
|
|
330
|
+
type: 2, // CMD
|
|
331
|
+
cmd: command
|
|
332
|
+
};
|
|
333
|
+
const flightInfo = await this.getFlightInfo(descriptor);
|
|
334
|
+
return this.fetchCatalogResults(flightInfo, (row) => {
|
|
335
|
+
const info = {
|
|
336
|
+
catalogName: row["catalog_name"],
|
|
337
|
+
schemaName: row["db_schema_name"],
|
|
338
|
+
tableName: row["table_name"],
|
|
339
|
+
tableType: row["table_type"]
|
|
340
|
+
};
|
|
341
|
+
// If includeSchema was requested, parse the table schema
|
|
342
|
+
const schemaBytes = row["table_schema"];
|
|
343
|
+
if (schemaBytes && schemaBytes.length > 0) {
|
|
344
|
+
info.schema = tryParseSchema(schemaBytes) ?? undefined;
|
|
345
|
+
}
|
|
346
|
+
return info;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get the list of table types supported by the server.
|
|
351
|
+
*
|
|
352
|
+
* @returns Array of table type names (e.g., "TABLE", "VIEW", "SYSTEM TABLE")
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* ```typescript
|
|
356
|
+
* const tableTypes = await client.getTableTypes()
|
|
357
|
+
* console.log(tableTypes) // [{ tableType: "TABLE" }, { tableType: "VIEW" }, ...]
|
|
358
|
+
* ```
|
|
359
|
+
*/
|
|
360
|
+
async getTableTypes() {
|
|
361
|
+
this.ensureConnected();
|
|
362
|
+
const command = encodeCommandGetTableTypes();
|
|
363
|
+
const descriptor = {
|
|
364
|
+
type: 2, // CMD
|
|
365
|
+
cmd: command
|
|
366
|
+
};
|
|
367
|
+
const flightInfo = await this.getFlightInfo(descriptor);
|
|
368
|
+
return this.fetchCatalogResults(flightInfo, (row) => ({
|
|
369
|
+
tableType: row["table_type"]
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Get the primary keys for a table.
|
|
374
|
+
*
|
|
375
|
+
* @param table - Table name
|
|
376
|
+
* @param catalog - Optional catalog name
|
|
377
|
+
* @param schema - Optional schema name
|
|
378
|
+
* @returns Array of primary key information
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* ```typescript
|
|
382
|
+
* const primaryKeys = await client.getPrimaryKeys("users")
|
|
383
|
+
* for (const pk of primaryKeys) {
|
|
384
|
+
* console.log(`${pk.columnName} (sequence: ${pk.keySequence})`)
|
|
385
|
+
* }
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
async getPrimaryKeys(table, catalog, schema) {
|
|
389
|
+
this.ensureConnected();
|
|
390
|
+
const command = encodeCommandGetPrimaryKeys(table, catalog, schema);
|
|
391
|
+
const descriptor = {
|
|
392
|
+
type: 2, // CMD
|
|
393
|
+
cmd: command
|
|
394
|
+
};
|
|
395
|
+
const flightInfo = await this.getFlightInfo(descriptor);
|
|
396
|
+
return this.fetchCatalogResults(flightInfo, (row) => ({
|
|
397
|
+
catalogName: row["catalog_name"],
|
|
398
|
+
schemaName: row["db_schema_name"],
|
|
399
|
+
tableName: row["table_name"],
|
|
400
|
+
columnName: row["column_name"],
|
|
401
|
+
keySequence: row["key_sequence"],
|
|
402
|
+
keyName: row["key_name"]
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Get the foreign keys that reference a table's primary key (exported keys).
|
|
407
|
+
*
|
|
408
|
+
* @param table - Table name
|
|
409
|
+
* @param catalog - Optional catalog name
|
|
410
|
+
* @param schema - Optional schema name
|
|
411
|
+
* @returns Array of foreign key information
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* ```typescript
|
|
415
|
+
* // Find all tables that reference the "users" table
|
|
416
|
+
* const exportedKeys = await client.getExportedKeys("users")
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
async getExportedKeys(table, catalog, schema) {
|
|
420
|
+
this.ensureConnected();
|
|
421
|
+
const command = encodeCommandGetExportedKeys(table, catalog, schema);
|
|
422
|
+
const descriptor = {
|
|
423
|
+
type: 2, // CMD
|
|
424
|
+
cmd: command
|
|
425
|
+
};
|
|
426
|
+
const flightInfo = await this.getFlightInfo(descriptor);
|
|
427
|
+
return this.fetchForeignKeyResults(flightInfo);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Get the foreign keys in a table (imported keys).
|
|
431
|
+
*
|
|
432
|
+
* @param table - Table name
|
|
433
|
+
* @param catalog - Optional catalog name
|
|
434
|
+
* @param schema - Optional schema name
|
|
435
|
+
* @returns Array of foreign key information
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* ```typescript
|
|
439
|
+
* // Find all foreign keys in the "orders" table
|
|
440
|
+
* const importedKeys = await client.getImportedKeys("orders")
|
|
441
|
+
* ```
|
|
442
|
+
*/
|
|
443
|
+
async getImportedKeys(table, catalog, schema) {
|
|
444
|
+
this.ensureConnected();
|
|
445
|
+
const command = encodeCommandGetImportedKeys(table, catalog, schema);
|
|
446
|
+
const descriptor = {
|
|
447
|
+
type: 2, // CMD
|
|
448
|
+
cmd: command
|
|
449
|
+
};
|
|
450
|
+
const flightInfo = await this.getFlightInfo(descriptor);
|
|
451
|
+
return this.fetchForeignKeyResults(flightInfo);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Helper to fetch catalog results and map rows to typed objects
|
|
455
|
+
*/
|
|
456
|
+
async fetchCatalogResults(flightInfo, mapper) {
|
|
457
|
+
const results = [];
|
|
458
|
+
// Parse schema from flightInfo
|
|
459
|
+
const schema = tryParseSchema(flightInfo.schema);
|
|
460
|
+
if (!schema) {
|
|
461
|
+
return results;
|
|
462
|
+
}
|
|
463
|
+
for (const endpoint of flightInfo.endpoints) {
|
|
464
|
+
for await (const flightData of this.doGet(endpoint.ticket)) {
|
|
465
|
+
if (flightData.dataHeader && flightData.dataBody) {
|
|
466
|
+
const batch = parseFlightData(flightData.dataHeader, flightData.dataBody, schema);
|
|
467
|
+
if (batch) {
|
|
468
|
+
// Convert batch rows to objects
|
|
469
|
+
for (let i = 0; i < batch.numRows; i++) {
|
|
470
|
+
const row = {};
|
|
471
|
+
for (const field of schema.fields) {
|
|
472
|
+
const column = batch.getChild(field.name);
|
|
473
|
+
row[field.name] = column?.get(i);
|
|
474
|
+
}
|
|
475
|
+
results.push(mapper(row));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return results;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Helper to fetch foreign key results with the complex schema
|
|
485
|
+
*/
|
|
486
|
+
async fetchForeignKeyResults(flightInfo) {
|
|
487
|
+
return this.fetchCatalogResults(flightInfo, (row) => ({
|
|
488
|
+
pkCatalogName: row["pk_catalog_name"],
|
|
489
|
+
pkSchemaName: row["pk_db_schema_name"],
|
|
490
|
+
pkTableName: row["pk_table_name"],
|
|
491
|
+
pkColumnName: row["pk_column_name"],
|
|
492
|
+
fkCatalogName: row["fk_catalog_name"],
|
|
493
|
+
fkSchemaName: row["fk_db_schema_name"],
|
|
494
|
+
fkTableName: row["fk_table_name"],
|
|
495
|
+
fkColumnName: row["fk_column_name"],
|
|
496
|
+
keySequence: row["key_sequence"],
|
|
497
|
+
fkKeyName: row["fk_key_name"],
|
|
498
|
+
pkKeyName: row["pk_key_name"],
|
|
499
|
+
updateRule: row["update_rule"],
|
|
500
|
+
deleteRule: row["delete_rule"]
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
// ===========================================================================
|
|
504
|
+
// Core Flight Operations
|
|
505
|
+
// ===========================================================================
|
|
506
|
+
/**
|
|
507
|
+
* Get flight information for a descriptor.
|
|
508
|
+
*
|
|
509
|
+
* @param descriptor - Flight descriptor
|
|
510
|
+
* @returns FlightInfo with schema and endpoints
|
|
511
|
+
*/
|
|
512
|
+
async getFlightInfo(descriptor) {
|
|
513
|
+
this.ensureConnected();
|
|
514
|
+
return new Promise((resolve, reject) => {
|
|
515
|
+
const client = this.grpcClient;
|
|
516
|
+
const metadata = this.createRequestMetadata();
|
|
517
|
+
const request = this.serializeFlightDescriptor(descriptor);
|
|
518
|
+
client.getFlightInfo(request, metadata, (error, response) => {
|
|
519
|
+
if (error) {
|
|
520
|
+
reject(this.wrapGrpcError(error));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
resolve(this.parseFlightInfo(response));
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Get the schema for a flight descriptor without fetching data.
|
|
529
|
+
*
|
|
530
|
+
* @param descriptor - Flight descriptor
|
|
531
|
+
* @returns Schema result
|
|
532
|
+
*/
|
|
533
|
+
async getSchema(descriptor) {
|
|
534
|
+
this.ensureConnected();
|
|
535
|
+
return new Promise((resolve, reject) => {
|
|
536
|
+
const client = this.grpcClient;
|
|
537
|
+
const metadata = this.createRequestMetadata();
|
|
538
|
+
const request = this.serializeFlightDescriptor(descriptor);
|
|
539
|
+
client.getSchema(request, metadata, (error, response) => {
|
|
540
|
+
if (error) {
|
|
541
|
+
reject(this.wrapGrpcError(error));
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
resolve(this.parseSchemaResult(response));
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Retrieve data for a ticket as an async iterator of FlightData.
|
|
550
|
+
*
|
|
551
|
+
* @param ticket - Ticket from FlightInfo endpoint
|
|
552
|
+
* @yields FlightData chunks containing dataHeader and dataBody
|
|
553
|
+
*/
|
|
554
|
+
async *doGet(ticket) {
|
|
555
|
+
this.ensureConnected();
|
|
556
|
+
const client = this.grpcClient;
|
|
557
|
+
const metadata = this.createRequestMetadata();
|
|
558
|
+
const request = { ticket: ticket.ticket };
|
|
559
|
+
const stream = client.doGet(request, metadata);
|
|
560
|
+
try {
|
|
561
|
+
for await (const data of this.wrapStream(stream)) {
|
|
562
|
+
const flightData = data;
|
|
563
|
+
yield flightData;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
stream.cancel();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Upload Arrow data to the server.
|
|
572
|
+
*
|
|
573
|
+
* @param descriptor - Flight descriptor describing the data
|
|
574
|
+
* @param dataStream - Async iterable of FlightData messages
|
|
575
|
+
* @returns Async iterator of PutResult messages
|
|
576
|
+
*/
|
|
577
|
+
async *doPut(descriptor, dataStream) {
|
|
578
|
+
this.ensureConnected();
|
|
579
|
+
const client = this.grpcClient;
|
|
580
|
+
const requestMetadata = this.createRequestMetadata();
|
|
581
|
+
// Create bidirectional stream
|
|
582
|
+
const stream = client.doPut(requestMetadata);
|
|
583
|
+
// Use object wrapper to track errors (allows TypeScript to understand mutation)
|
|
584
|
+
const errorState = { error: null };
|
|
585
|
+
stream.on("error", (err) => {
|
|
586
|
+
errorState.error = err;
|
|
587
|
+
});
|
|
588
|
+
// Send the first message with the descriptor
|
|
589
|
+
const firstData = await this.getFirstFromIterable(dataStream);
|
|
590
|
+
if (firstData) {
|
|
591
|
+
stream.write({
|
|
592
|
+
flightDescriptor: this.serializeFlightDescriptor(descriptor),
|
|
593
|
+
dataHeader: firstData.dataHeader,
|
|
594
|
+
dataBody: firstData.dataBody,
|
|
595
|
+
appMetadata: firstData.appMetadata
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
// Send remaining data
|
|
599
|
+
for await (const data of dataStream) {
|
|
600
|
+
if (errorState.error) {
|
|
601
|
+
throw this.wrapGrpcError(errorState.error);
|
|
602
|
+
}
|
|
603
|
+
stream.write({
|
|
604
|
+
dataHeader: data.dataHeader,
|
|
605
|
+
dataBody: data.dataBody,
|
|
606
|
+
appMetadata: data.appMetadata
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
// Signal end of writing
|
|
610
|
+
stream.end();
|
|
611
|
+
// Read responses
|
|
612
|
+
try {
|
|
613
|
+
for await (const result of this.wrapStream(stream)) {
|
|
614
|
+
const putResult = result;
|
|
615
|
+
yield { appMetadata: putResult.appMetadata };
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
if (errorState.error) {
|
|
620
|
+
throw this.wrapGrpcError(errorState.error);
|
|
621
|
+
}
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Helper to get the first item from an async iterable without consuming the rest
|
|
627
|
+
*/
|
|
628
|
+
async getFirstFromIterable(iterable) {
|
|
629
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
630
|
+
const result = await iterator.next();
|
|
631
|
+
return result.done ? undefined : result.value;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Execute an action on the server.
|
|
635
|
+
*
|
|
636
|
+
* @param action - Action to execute
|
|
637
|
+
* @returns Async iterator of results
|
|
638
|
+
*/
|
|
639
|
+
async *doAction(action) {
|
|
640
|
+
this.ensureConnected();
|
|
641
|
+
const client = this.grpcClient;
|
|
642
|
+
const metadata = this.createRequestMetadata();
|
|
643
|
+
const request = { type: action.type, body: action.body };
|
|
644
|
+
const stream = client.doAction(request, metadata);
|
|
645
|
+
try {
|
|
646
|
+
for await (const result of this.wrapStream(stream)) {
|
|
647
|
+
yield result;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
stream.cancel();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* List available action types.
|
|
656
|
+
*
|
|
657
|
+
* @returns Array of available action types
|
|
658
|
+
*/
|
|
659
|
+
async listActions() {
|
|
660
|
+
this.ensureConnected();
|
|
661
|
+
const client = this.grpcClient;
|
|
662
|
+
const metadata = this.createRequestMetadata();
|
|
663
|
+
const stream = client.listActions({}, metadata);
|
|
664
|
+
const actions = [];
|
|
665
|
+
for await (const action of this.wrapStream(stream)) {
|
|
666
|
+
actions.push(action);
|
|
667
|
+
}
|
|
668
|
+
return actions;
|
|
669
|
+
}
|
|
670
|
+
// ===========================================================================
|
|
671
|
+
// Private: Authentication
|
|
672
|
+
// ===========================================================================
|
|
673
|
+
async authenticate(auth) {
|
|
674
|
+
switch (auth.type) {
|
|
675
|
+
case "none":
|
|
676
|
+
return;
|
|
677
|
+
case "bearer":
|
|
678
|
+
// For bearer tokens, we just store the token to include in metadata
|
|
679
|
+
this.authToken = auth.token;
|
|
680
|
+
return;
|
|
681
|
+
case "basic": {
|
|
682
|
+
// Perform handshake with basic auth
|
|
683
|
+
const result = await this.handshake(auth.username, auth.password);
|
|
684
|
+
// Extract bearer token from response payload if present
|
|
685
|
+
const payloadStr = new TextDecoder().decode(result.payload);
|
|
686
|
+
if (payloadStr) {
|
|
687
|
+
this.authToken = payloadStr;
|
|
688
|
+
}
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async handshake(username, password) {
|
|
694
|
+
return new Promise((resolve, reject) => {
|
|
695
|
+
const client = this.grpcClient;
|
|
696
|
+
const stream = client.handshake();
|
|
697
|
+
// Build BasicAuth payload
|
|
698
|
+
const authPayload = this.encodeBasicAuth(username, password);
|
|
699
|
+
stream.on("data", (response) => {
|
|
700
|
+
resolve({
|
|
701
|
+
protocolVersion: BigInt(response.protocolVersion ?? "0"),
|
|
702
|
+
payload: response.payload ?? new Uint8Array()
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
stream.on("error", (error) => {
|
|
706
|
+
reject(new AuthenticationError("Handshake failed", { cause: error }));
|
|
707
|
+
});
|
|
708
|
+
stream.on("end", () => {
|
|
709
|
+
// Stream ended without response
|
|
710
|
+
});
|
|
711
|
+
// Send handshake request
|
|
712
|
+
stream.write({
|
|
713
|
+
protocolVersion: "1",
|
|
714
|
+
payload: authPayload
|
|
715
|
+
});
|
|
716
|
+
stream.end();
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
encodeBasicAuth(username, password) {
|
|
720
|
+
// Simple encoding: "username:password" as UTF-8 bytes
|
|
721
|
+
// The actual Flight protocol uses protobuf BasicAuth message
|
|
722
|
+
const encoder = new TextEncoder();
|
|
723
|
+
return encoder.encode(`${username}:${password}`);
|
|
724
|
+
}
|
|
725
|
+
// ===========================================================================
|
|
726
|
+
// Private: gRPC Helpers
|
|
727
|
+
// ===========================================================================
|
|
728
|
+
createCredentials() {
|
|
729
|
+
if (this.options.tls) {
|
|
730
|
+
return grpc.credentials.createSsl();
|
|
731
|
+
}
|
|
732
|
+
return grpc.credentials.createInsecure();
|
|
733
|
+
}
|
|
734
|
+
async waitForReady() {
|
|
735
|
+
return new Promise((resolve, reject) => {
|
|
736
|
+
const deadline = Date.now() + this.options.connectTimeoutMs;
|
|
737
|
+
const client = this.grpcClient;
|
|
738
|
+
if (!client) {
|
|
739
|
+
reject(new ConnectionError("gRPC client not initialized"));
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
client.waitForReady(deadline, (error) => {
|
|
743
|
+
if (error) {
|
|
744
|
+
reject(new ConnectionError("Connection timeout", { cause: error }));
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
resolve();
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
createRequestMetadata() {
|
|
753
|
+
const metadata = new grpc.Metadata();
|
|
754
|
+
// Add auth token if present
|
|
755
|
+
if (this.authToken) {
|
|
756
|
+
metadata.set("authorization", `Bearer ${this.authToken}`);
|
|
757
|
+
}
|
|
758
|
+
// Add custom metadata from options
|
|
759
|
+
if (this.options.metadata) {
|
|
760
|
+
for (const [key, value] of Object.entries(this.options.metadata)) {
|
|
761
|
+
metadata.set(key, value);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return metadata;
|
|
765
|
+
}
|
|
766
|
+
cleanup() {
|
|
767
|
+
if (this.grpcClient) {
|
|
768
|
+
this.grpcClient.close();
|
|
769
|
+
this.grpcClient = null;
|
|
770
|
+
}
|
|
771
|
+
this.flightService = null;
|
|
772
|
+
this.authToken = null;
|
|
773
|
+
this.connected = false;
|
|
774
|
+
}
|
|
775
|
+
ensureConnected() {
|
|
776
|
+
if (!this.connected) {
|
|
777
|
+
throw new ConnectionError("Client is not connected. Call connect() first.");
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
wrapGrpcError(error) {
|
|
781
|
+
const message = error.details || error.message;
|
|
782
|
+
switch (error.code) {
|
|
783
|
+
case grpc.status.UNAUTHENTICATED:
|
|
784
|
+
return new AuthenticationError(message, { cause: error });
|
|
785
|
+
case grpc.status.UNAVAILABLE:
|
|
786
|
+
case grpc.status.DEADLINE_EXCEEDED:
|
|
787
|
+
return new ConnectionError(message, { cause: error });
|
|
788
|
+
default:
|
|
789
|
+
return new FlightSqlError(message, { cause: error });
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// ===========================================================================
|
|
793
|
+
// Private: Streaming Helpers
|
|
794
|
+
// ===========================================================================
|
|
795
|
+
/**
|
|
796
|
+
* Wraps a gRPC stream to convert errors to FlightSqlError types.
|
|
797
|
+
* gRPC streams are already async iterable, so we just need error handling.
|
|
798
|
+
*/
|
|
799
|
+
async *wrapStream(stream) {
|
|
800
|
+
try {
|
|
801
|
+
for await (const data of stream) {
|
|
802
|
+
yield data;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
catch (error) {
|
|
806
|
+
throw this.wrapGrpcError(error);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
// ===========================================================================
|
|
810
|
+
// Private: Serialization Helpers
|
|
811
|
+
// ===========================================================================
|
|
812
|
+
serializeFlightDescriptor(descriptor) {
|
|
813
|
+
return {
|
|
814
|
+
type: descriptor.type,
|
|
815
|
+
cmd: descriptor.cmd,
|
|
816
|
+
path: descriptor.path
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
parseFlightInfo(response) {
|
|
820
|
+
const info = response;
|
|
821
|
+
return {
|
|
822
|
+
schema: info.schema ?? new Uint8Array(),
|
|
823
|
+
flightDescriptor: info.flightDescriptor
|
|
824
|
+
? {
|
|
825
|
+
type: info.flightDescriptor.type,
|
|
826
|
+
cmd: info.flightDescriptor.cmd,
|
|
827
|
+
path: info.flightDescriptor.path
|
|
828
|
+
}
|
|
829
|
+
: undefined,
|
|
830
|
+
endpoints: (info.endpoint ?? []).map((ep) => ({
|
|
831
|
+
ticket: { ticket: ep.ticket?.ticket ?? new Uint8Array() },
|
|
832
|
+
locations: (ep.location ?? []).map((loc) => ({ uri: loc.uri })),
|
|
833
|
+
expirationTime: ep.expirationTime
|
|
834
|
+
? new Date(Number(ep.expirationTime.seconds) * 1000 + ep.expirationTime.nanos / 1_000_000)
|
|
835
|
+
: undefined,
|
|
836
|
+
appMetadata: ep.appMetadata
|
|
837
|
+
})),
|
|
838
|
+
totalRecords: BigInt(info.totalRecords ?? "-1"),
|
|
839
|
+
totalBytes: BigInt(info.totalBytes ?? "-1"),
|
|
840
|
+
ordered: info.ordered ?? false,
|
|
841
|
+
appMetadata: info.appMetadata
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
parseSchemaResult(response) {
|
|
845
|
+
const result = response;
|
|
846
|
+
return {
|
|
847
|
+
schema: result.schema ?? new Uint8Array()
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
// ============================================================================
|
|
852
|
+
// QueryResult
|
|
853
|
+
// ============================================================================
|
|
854
|
+
/**
|
|
855
|
+
* Result of a query execution with methods to stream or collect data.
|
|
856
|
+
*
|
|
857
|
+
* @example
|
|
858
|
+
* ```typescript
|
|
859
|
+
* const result = await client.query("SELECT * FROM users")
|
|
860
|
+
*
|
|
861
|
+
* // Option 1: Stream record batches (memory efficient)
|
|
862
|
+
* for await (const batch of result.stream()) {
|
|
863
|
+
* console.log(`Batch with ${batch.numRows} rows`)
|
|
864
|
+
* }
|
|
865
|
+
*
|
|
866
|
+
* // Option 2: Collect all data into a table
|
|
867
|
+
* const table = await result.collect()
|
|
868
|
+
* console.log(`Total rows: ${table.numRows}`)
|
|
869
|
+
* ```
|
|
870
|
+
*/
|
|
871
|
+
export class QueryResult {
|
|
872
|
+
client;
|
|
873
|
+
info;
|
|
874
|
+
parsedSchema;
|
|
875
|
+
constructor(client, flightInfo, schema) {
|
|
876
|
+
this.client = client;
|
|
877
|
+
this.info = flightInfo;
|
|
878
|
+
this.parsedSchema = schema;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Get the FlightInfo for this query result
|
|
882
|
+
*/
|
|
883
|
+
get flightInfo() {
|
|
884
|
+
return this.info;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Get the Arrow schema for this query result
|
|
888
|
+
* May be null if schema could not be parsed
|
|
889
|
+
*/
|
|
890
|
+
get schema() {
|
|
891
|
+
return this.parsedSchema;
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Get the total number of records (if known)
|
|
895
|
+
* Returns -1 if unknown
|
|
896
|
+
*/
|
|
897
|
+
get totalRecords() {
|
|
898
|
+
return this.info.totalRecords;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Stream record batches from all endpoints.
|
|
902
|
+
* This is memory-efficient for large result sets.
|
|
903
|
+
*
|
|
904
|
+
* Flight SQL streams data as:
|
|
905
|
+
* 1. Schema message (dataHeader only)
|
|
906
|
+
* 2. RecordBatch messages (dataHeader + dataBody)
|
|
907
|
+
*
|
|
908
|
+
* We collect all messages and parse them as a complete IPC stream.
|
|
909
|
+
*/
|
|
910
|
+
async *stream() {
|
|
911
|
+
if (!this.parsedSchema) {
|
|
912
|
+
throw new FlightSqlError("Cannot stream results: schema not available");
|
|
913
|
+
}
|
|
914
|
+
for (const endpoint of this.info.endpoints) {
|
|
915
|
+
// Collect all FlightData messages for this endpoint
|
|
916
|
+
const framedParts = [];
|
|
917
|
+
for await (const flightData of this.client.doGet(endpoint.ticket)) {
|
|
918
|
+
if (flightData.dataHeader && flightData.dataHeader.length > 0) {
|
|
919
|
+
const framed = this.frameAsIPC(flightData.dataHeader, flightData.dataBody || new Uint8Array(0));
|
|
920
|
+
framedParts.push(framed);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (framedParts.length === 0) {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
// Concatenate all framed messages into a complete IPC stream
|
|
927
|
+
const fullStream = this.concatArrays(framedParts);
|
|
928
|
+
// Parse batches from the full stream
|
|
929
|
+
try {
|
|
930
|
+
const reader = RecordBatchReader.from(fullStream);
|
|
931
|
+
for (const batch of reader) {
|
|
932
|
+
yield batch;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
catch {
|
|
936
|
+
// If parsing fails, continue to next endpoint
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Frame raw flatbuffer bytes with IPC continuation marker and length prefix
|
|
942
|
+
*/
|
|
943
|
+
frameAsIPC(header, body) {
|
|
944
|
+
const continuationToken = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
|
|
945
|
+
const metadataLength = new Uint8Array(4);
|
|
946
|
+
new DataView(metadataLength.buffer).setInt32(0, header.length, true);
|
|
947
|
+
// Calculate padding for 8-byte alignment
|
|
948
|
+
const headerPadding = (8 - ((header.length + 4 + 4) % 8)) % 8;
|
|
949
|
+
const padding = new Uint8Array(headerPadding);
|
|
950
|
+
const totalLength = continuationToken.length +
|
|
951
|
+
metadataLength.length +
|
|
952
|
+
header.length +
|
|
953
|
+
padding.length +
|
|
954
|
+
body.length;
|
|
955
|
+
const result = new Uint8Array(totalLength);
|
|
956
|
+
let offset = 0;
|
|
957
|
+
result.set(continuationToken, offset);
|
|
958
|
+
offset += continuationToken.length;
|
|
959
|
+
result.set(metadataLength, offset);
|
|
960
|
+
offset += metadataLength.length;
|
|
961
|
+
result.set(header, offset);
|
|
962
|
+
offset += header.length;
|
|
963
|
+
result.set(padding, offset);
|
|
964
|
+
offset += padding.length;
|
|
965
|
+
result.set(body, offset);
|
|
966
|
+
return result;
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Concatenate multiple Uint8Arrays
|
|
970
|
+
*/
|
|
971
|
+
concatArrays(arrays) {
|
|
972
|
+
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
973
|
+
const result = new Uint8Array(totalLength);
|
|
974
|
+
let offset = 0;
|
|
975
|
+
for (const arr of arrays) {
|
|
976
|
+
result.set(arr, offset);
|
|
977
|
+
offset += arr.length;
|
|
978
|
+
}
|
|
979
|
+
return result;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Collect all data from all endpoints into a single Table.
|
|
983
|
+
* Warning: This loads the entire result set into memory.
|
|
984
|
+
*/
|
|
985
|
+
async collect() {
|
|
986
|
+
const batches = [];
|
|
987
|
+
for await (const batch of this.stream()) {
|
|
988
|
+
batches.push(batch);
|
|
989
|
+
}
|
|
990
|
+
if (!this.parsedSchema) {
|
|
991
|
+
throw new FlightSqlError("Cannot collect results: schema not available");
|
|
992
|
+
}
|
|
993
|
+
return collectToTable(this.streamBatches(batches), this.parsedSchema);
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Helper to convert batches array to async generator
|
|
997
|
+
*/
|
|
998
|
+
*streamBatches(batches) {
|
|
999
|
+
for (const batch of batches) {
|
|
1000
|
+
yield batch;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
// ============================================================================
|
|
1005
|
+
// PreparedStatement
|
|
1006
|
+
// ============================================================================
|
|
1007
|
+
/**
|
|
1008
|
+
* A prepared statement that can be executed multiple times with different parameters.
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* ```typescript
|
|
1012
|
+
* const stmt = await client.prepare("SELECT * FROM users WHERE id = ?")
|
|
1013
|
+
*
|
|
1014
|
+
* // Execute with parameters
|
|
1015
|
+
* const result = await stmt.execute()
|
|
1016
|
+
* const table = await result.collect()
|
|
1017
|
+
*
|
|
1018
|
+
* // Clean up
|
|
1019
|
+
* await stmt.close()
|
|
1020
|
+
* ```
|
|
1021
|
+
*/
|
|
1022
|
+
export class PreparedStatement {
|
|
1023
|
+
client;
|
|
1024
|
+
handle;
|
|
1025
|
+
datasetSchema;
|
|
1026
|
+
parameterSchema;
|
|
1027
|
+
closed = false;
|
|
1028
|
+
constructor(client, handle, datasetSchema, parameterSchema) {
|
|
1029
|
+
this.client = client;
|
|
1030
|
+
this.handle = handle;
|
|
1031
|
+
this.datasetSchema = datasetSchema;
|
|
1032
|
+
this.parameterSchema = parameterSchema;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Get the schema of the result set
|
|
1036
|
+
*/
|
|
1037
|
+
get resultSchema() {
|
|
1038
|
+
return this.datasetSchema;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Get the schema of the parameters
|
|
1042
|
+
*/
|
|
1043
|
+
get parametersSchema() {
|
|
1044
|
+
return this.parameterSchema;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Check if the prepared statement is closed
|
|
1048
|
+
*/
|
|
1049
|
+
get isClosed() {
|
|
1050
|
+
return this.closed;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Execute the prepared statement as a query
|
|
1054
|
+
*/
|
|
1055
|
+
async executeQuery() {
|
|
1056
|
+
if (this.closed) {
|
|
1057
|
+
throw new FlightSqlError("PreparedStatement is closed");
|
|
1058
|
+
}
|
|
1059
|
+
const command = encodeCommandPreparedStatementQuery(this.handle);
|
|
1060
|
+
const descriptor = {
|
|
1061
|
+
type: 2, // CMD
|
|
1062
|
+
cmd: command
|
|
1063
|
+
};
|
|
1064
|
+
const flightInfo = await this.client.getFlightInfo(descriptor);
|
|
1065
|
+
const schema = tryParseSchema(flightInfo.schema) ?? this.datasetSchema;
|
|
1066
|
+
return new QueryResult(this.client, flightInfo, schema);
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Close the prepared statement and release server resources
|
|
1070
|
+
*/
|
|
1071
|
+
async close() {
|
|
1072
|
+
if (this.closed) {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const actionBody = encodeActionClosePreparedStatementRequest(this.handle);
|
|
1076
|
+
const action = {
|
|
1077
|
+
type: "ClosePreparedStatement",
|
|
1078
|
+
body: actionBody
|
|
1079
|
+
};
|
|
1080
|
+
// Execute the close action
|
|
1081
|
+
for await (const result of this.client.doAction(action)) {
|
|
1082
|
+
void result; // Consume the results
|
|
1083
|
+
}
|
|
1084
|
+
this.closed = true;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
//# sourceMappingURL=client.js.map
|