@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.
Files changed (49) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +433 -0
  3. package/dist/arrow.d.ts +65 -0
  4. package/dist/arrow.d.ts.map +1 -0
  5. package/dist/arrow.js +250 -0
  6. package/dist/arrow.js.map +1 -0
  7. package/dist/client.d.ts +416 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +1087 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/errors.d.ts +128 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +181 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/generated/index.d.ts +4 -0
  16. package/dist/generated/index.d.ts.map +1 -0
  17. package/dist/generated/index.js +33 -0
  18. package/dist/generated/index.js.map +1 -0
  19. package/dist/index.d.ts +40 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +43 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/metrics.d.ts +217 -0
  24. package/dist/metrics.d.ts.map +1 -0
  25. package/dist/metrics.js +304 -0
  26. package/dist/metrics.js.map +1 -0
  27. package/dist/pool.d.ts +161 -0
  28. package/dist/pool.d.ts.map +1 -0
  29. package/dist/pool.js +434 -0
  30. package/dist/pool.js.map +1 -0
  31. package/dist/proto.d.ts +168 -0
  32. package/dist/proto.d.ts.map +1 -0
  33. package/dist/proto.js +417 -0
  34. package/dist/proto.js.map +1 -0
  35. package/dist/query-builder.d.ts +1 -0
  36. package/dist/query-builder.d.ts.map +1 -0
  37. package/dist/query-builder.js +3 -0
  38. package/dist/query-builder.js.map +1 -0
  39. package/dist/retry.d.ts +92 -0
  40. package/dist/retry.d.ts.map +1 -0
  41. package/dist/retry.js +212 -0
  42. package/dist/retry.js.map +1 -0
  43. package/dist/types.d.ts +325 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +18 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +82 -0
  48. package/proto/Flight.proto +645 -0
  49. 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