@qualithm/arrow-flight-client 0.1.0
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 +190 -0
- package/README.md +258 -0
- package/dist/client/create-flight-client.d.ts +28 -0
- package/dist/client/create-flight-client.d.ts.map +1 -0
- package/dist/client/create-flight-client.js +29 -0
- package/dist/client/create-flight-client.js.map +1 -0
- package/dist/client/create-flight-sql-client.d.ts +28 -0
- package/dist/client/create-flight-sql-client.d.ts.map +1 -0
- package/dist/client/create-flight-sql-client.js +29 -0
- package/dist/client/create-flight-sql-client.js.map +1 -0
- package/dist/client/errors.d.ts +81 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +106 -0
- package/dist/client/errors.js.map +1 -0
- package/dist/client/flight-client.d.ts +161 -0
- package/dist/client/flight-client.d.ts.map +1 -0
- package/dist/client/flight-client.js +403 -0
- package/dist/client/flight-client.js.map +1 -0
- package/dist/client/flight-sql-client.d.ts +348 -0
- package/dist/client/flight-sql-client.d.ts.map +1 -0
- package/dist/client/flight-sql-client.js +689 -0
- package/dist/client/flight-sql-client.js.map +1 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +10 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/ipc.d.ts +131 -0
- package/dist/client/ipc.d.ts.map +1 -0
- package/dist/client/ipc.js +246 -0
- package/dist/client/ipc.js.map +1 -0
- package/dist/client/types.d.ts +153 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +26 -0
- package/dist/client/types.js.map +1 -0
- package/dist/gen/arrow/flight/FlightSql_pb.d.ts +3173 -0
- package/dist/gen/arrow/flight/FlightSql_pb.d.ts.map +1 -0
- package/dist/gen/arrow/flight/FlightSql_pb.js +2258 -0
- package/dist/gen/arrow/flight/FlightSql_pb.js.map +1 -0
- package/dist/gen/arrow/flight/Flight_pb.d.ts +1159 -0
- package/dist/gen/arrow/flight/Flight_pb.d.ts.map +1 -0
- package/dist/gen/arrow/flight/Flight_pb.js +397 -0
- package/dist/gen/arrow/flight/Flight_pb.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
2
|
+
import { ActionBeginTransactionRequestSchema, ActionBeginTransactionResultSchema, ActionClosePreparedStatementRequestSchema, ActionCreatePreparedStatementRequestSchema, ActionCreatePreparedStatementResultSchema, ActionEndTransactionRequest_EndTransaction, ActionEndTransactionRequestSchema, CommandGetCatalogsSchema, CommandGetDbSchemasSchema, CommandGetPrimaryKeysSchema, CommandGetTablesSchema, CommandGetTableTypesSchema, CommandPreparedStatementQuerySchema, CommandPreparedStatementUpdateSchema, CommandStatementQuerySchema, CommandStatementUpdateSchema, DoPutUpdateResultSchema } from "../gen/arrow/flight/FlightSql_pb.js";
|
|
3
|
+
import { FlightError } from "./errors.js";
|
|
4
|
+
import { FlightClient } from "./flight-client.js";
|
|
5
|
+
import { decodeFlightDataStream, decodeFlightDataToTable, encodeRecordBatchesToFlightData } from "./ipc.js";
|
|
6
|
+
/**
|
|
7
|
+
* Flight SQL action type identifiers.
|
|
8
|
+
*/
|
|
9
|
+
const FLIGHT_SQL_ACTIONS = {
|
|
10
|
+
CREATE_PREPARED_STATEMENT: "CreatePreparedStatement",
|
|
11
|
+
CLOSE_PREPARED_STATEMENT: "ClosePreparedStatement",
|
|
12
|
+
BEGIN_TRANSACTION: "BeginTransaction",
|
|
13
|
+
END_TRANSACTION: "EndTransaction"
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Flight SQL type URL prefix for protobuf Any encoding.
|
|
17
|
+
*/
|
|
18
|
+
const TYPE_URL_PREFIX = "type.googleapis.com/arrow.flight.protocol.sql";
|
|
19
|
+
/**
|
|
20
|
+
* Encodes a Flight SQL command as a protobuf Any message.
|
|
21
|
+
*
|
|
22
|
+
* Flight SQL commands must be wrapped in an Any message with a type_url
|
|
23
|
+
* that identifies the command type. The server uses this to dispatch
|
|
24
|
+
* to the correct handler.
|
|
25
|
+
*
|
|
26
|
+
* @param typeName - The command type name (e.g., "CommandStatementQuery")
|
|
27
|
+
* @param data - The serialised command bytes
|
|
28
|
+
* @returns The encoded Any message as Uint8Array
|
|
29
|
+
*/
|
|
30
|
+
function packAny(typeName, data) {
|
|
31
|
+
const typeUrl = `${TYPE_URL_PREFIX}.${typeName}`;
|
|
32
|
+
const typeUrlBytes = new TextEncoder().encode(typeUrl);
|
|
33
|
+
const typeUrlLen = typeUrlBytes.length;
|
|
34
|
+
const dataLen = data.length;
|
|
35
|
+
// Calculate varint sizes
|
|
36
|
+
const typeUrlVarIntSize = varIntSize(typeUrlLen);
|
|
37
|
+
const dataVarIntSize = varIntSize(dataLen);
|
|
38
|
+
// Total size: 1 (tag) + varint + typeUrl + 1 (tag) + varint + data
|
|
39
|
+
const totalSize = 1 + typeUrlVarIntSize + typeUrlLen + 1 + dataVarIntSize + dataLen;
|
|
40
|
+
const buffer = new Uint8Array(totalSize);
|
|
41
|
+
let offset = 0;
|
|
42
|
+
// Field 1: type_url (tag = 0x0a = field 1, wire type 2)
|
|
43
|
+
buffer[offset++] = 0x0a;
|
|
44
|
+
offset = writeVarInt(buffer, offset, typeUrlLen);
|
|
45
|
+
buffer.set(typeUrlBytes, offset);
|
|
46
|
+
offset += typeUrlLen;
|
|
47
|
+
// Field 2: value (tag = 0x12 = field 2, wire type 2)
|
|
48
|
+
buffer[offset++] = 0x12;
|
|
49
|
+
offset = writeVarInt(buffer, offset, dataLen);
|
|
50
|
+
buffer.set(data, offset);
|
|
51
|
+
return buffer;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Calculates the size of a varint encoding.
|
|
55
|
+
*/
|
|
56
|
+
function varIntSize(value) {
|
|
57
|
+
let size = 1;
|
|
58
|
+
while (value >= 0x80) {
|
|
59
|
+
value >>>= 7;
|
|
60
|
+
size++;
|
|
61
|
+
}
|
|
62
|
+
return size;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Writes a varint to a Uint8Array.
|
|
66
|
+
*/
|
|
67
|
+
function writeVarInt(buffer, offset, value) {
|
|
68
|
+
while (value >= 0x80) {
|
|
69
|
+
buffer[offset++] = (value & 0x7f) | 0x80;
|
|
70
|
+
value >>>= 7;
|
|
71
|
+
}
|
|
72
|
+
buffer[offset++] = value;
|
|
73
|
+
return offset;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Reads a varint from a Uint8Array.
|
|
77
|
+
* Returns [value, newOffset].
|
|
78
|
+
*/
|
|
79
|
+
function readVarInt(buffer, offset) {
|
|
80
|
+
let value = 0;
|
|
81
|
+
let shift = 0;
|
|
82
|
+
while (offset < buffer.length) {
|
|
83
|
+
const byte = buffer[offset++];
|
|
84
|
+
value |= (byte & 0x7f) << shift;
|
|
85
|
+
if ((byte & 0x80) === 0) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
shift += 7;
|
|
89
|
+
}
|
|
90
|
+
return [value, offset];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Unpacks a protobuf Any message and returns the inner value.
|
|
94
|
+
*
|
|
95
|
+
* The Any message format is:
|
|
96
|
+
* - Field 1 (type_url): string
|
|
97
|
+
* - Field 2 (value): bytes
|
|
98
|
+
*
|
|
99
|
+
* @param data - The Any-encoded message
|
|
100
|
+
* @returns The inner value bytes
|
|
101
|
+
*/
|
|
102
|
+
function unpackAny(data) {
|
|
103
|
+
let offset = 0;
|
|
104
|
+
while (offset < data.length) {
|
|
105
|
+
const tag = data[offset++];
|
|
106
|
+
const fieldNumber = tag >>> 3;
|
|
107
|
+
const wireType = tag & 0x07;
|
|
108
|
+
if (wireType === 2) {
|
|
109
|
+
// Length-delimited field
|
|
110
|
+
const [length, newOffset] = readVarInt(data, offset);
|
|
111
|
+
offset = newOffset;
|
|
112
|
+
if (fieldNumber === 2) {
|
|
113
|
+
// This is the value field - return it
|
|
114
|
+
return data.slice(offset, offset + length);
|
|
115
|
+
}
|
|
116
|
+
// Skip this field (e.g., type_url)
|
|
117
|
+
offset += length;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Unknown wire type - shouldn't happen for Any
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// No value field found - return original data (not wrapped in Any)
|
|
125
|
+
return data;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Arrow Flight SQL client for executing SQL queries and managing transactions.
|
|
129
|
+
*
|
|
130
|
+
* FlightSqlClient wraps FlightClient using composition, providing Flight SQL
|
|
131
|
+
* specific operations while delegating core Flight RPC to the underlying client.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* const client = new FlightSqlClient({ url: "https://flight.example.com:8815" })
|
|
136
|
+
*
|
|
137
|
+
* // Execute a query and get results as a Table
|
|
138
|
+
* const table = await client.query("SELECT * FROM users")
|
|
139
|
+
* console.log(`Got ${table.numRows} rows`)
|
|
140
|
+
*
|
|
141
|
+
* // Execute an update
|
|
142
|
+
* const result = await client.executeUpdate("INSERT INTO users (name) VALUES ('Alice')")
|
|
143
|
+
* console.log(`Inserted ${result.recordCount} rows`)
|
|
144
|
+
*
|
|
145
|
+
* // Use prepared statements
|
|
146
|
+
* const stmt = await client.prepare("SELECT * FROM users WHERE id = ?")
|
|
147
|
+
* // ... bind parameters and execute ...
|
|
148
|
+
* await client.closePreparedStatement(stmt)
|
|
149
|
+
*
|
|
150
|
+
* client.close()
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export class FlightSqlClient {
|
|
154
|
+
#flight;
|
|
155
|
+
/**
|
|
156
|
+
* Creates a new FlightSqlClient.
|
|
157
|
+
*
|
|
158
|
+
* @param optionsOrClient - Either FlightClientOptions to create a new FlightClient,
|
|
159
|
+
* or an existing FlightClient instance to wrap.
|
|
160
|
+
*/
|
|
161
|
+
constructor(optionsOrClient) {
|
|
162
|
+
// Duck-type check: FlightClient has getFlightInfo method, options don't
|
|
163
|
+
if (typeof optionsOrClient.getFlightInfo === "function" &&
|
|
164
|
+
typeof optionsOrClient.doGet === "function") {
|
|
165
|
+
this.#flight = optionsOrClient;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
this.#flight = new FlightClient(optionsOrClient);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The underlying Flight client.
|
|
173
|
+
* Use this for advanced Flight operations not covered by Flight SQL.
|
|
174
|
+
*/
|
|
175
|
+
get flight() {
|
|
176
|
+
return this.#flight;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The base URL of the Flight server.
|
|
180
|
+
*/
|
|
181
|
+
get url() {
|
|
182
|
+
return this.#flight.url;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Whether the client has been closed.
|
|
186
|
+
*/
|
|
187
|
+
get closed() {
|
|
188
|
+
return this.#flight.closed;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Close the client and release resources.
|
|
192
|
+
*/
|
|
193
|
+
close() {
|
|
194
|
+
this.#flight.close();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Authenticate with the server using configured credentials.
|
|
198
|
+
*/
|
|
199
|
+
async authenticate() {
|
|
200
|
+
return this.#flight.authenticate();
|
|
201
|
+
}
|
|
202
|
+
// ── SQL Query Execution ─────────────────────────────────────────────
|
|
203
|
+
/**
|
|
204
|
+
* Execute a SQL query and return results as an Arrow Table.
|
|
205
|
+
*
|
|
206
|
+
* This is a convenience method that combines getFlightInfo and doGet
|
|
207
|
+
* to fetch all query results into memory.
|
|
208
|
+
*
|
|
209
|
+
* @param query - SQL query string
|
|
210
|
+
* @param options - Query execution options
|
|
211
|
+
* @returns Arrow Table containing query results
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* const table = await client.query("SELECT * FROM users WHERE active = true")
|
|
216
|
+
* console.log(`Found ${table.numRows} active users`)
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
async query(query, options) {
|
|
220
|
+
const stream = this.queryStream(query, options);
|
|
221
|
+
return decodeFlightDataToTable(stream);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Execute a SQL query and return results as a stream of RecordBatches.
|
|
225
|
+
*
|
|
226
|
+
* Use this for large result sets to avoid loading all data into memory.
|
|
227
|
+
*
|
|
228
|
+
* @param query - SQL query string
|
|
229
|
+
* @param options - Query execution options
|
|
230
|
+
* @yields RecordBatch objects
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* for await (const batch of client.queryBatches("SELECT * FROM large_table")) {
|
|
235
|
+
* processBatch(batch)
|
|
236
|
+
* }
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
async *queryBatches(query, options) {
|
|
240
|
+
const stream = this.queryStream(query, options);
|
|
241
|
+
for await (const batch of decodeFlightDataStream(stream)) {
|
|
242
|
+
yield batch;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Execute a SQL query and return the raw FlightData stream.
|
|
247
|
+
*
|
|
248
|
+
* This is the lowest-level query method, useful when you need
|
|
249
|
+
* access to raw Flight data or custom decoding.
|
|
250
|
+
*
|
|
251
|
+
* @param query - SQL query string
|
|
252
|
+
* @param options - Query execution options
|
|
253
|
+
* @yields FlightData messages
|
|
254
|
+
*/
|
|
255
|
+
async *queryStream(query, options) {
|
|
256
|
+
// Create CommandStatementQuery
|
|
257
|
+
const command = create(CommandStatementQuerySchema, {
|
|
258
|
+
query,
|
|
259
|
+
transactionId: options?.transactionId
|
|
260
|
+
});
|
|
261
|
+
// Serialize command to bytes (wrapped in protobuf Any)
|
|
262
|
+
const cmdBytes = packAny("CommandStatementQuery", toBinary(CommandStatementQuerySchema, command));
|
|
263
|
+
// Get flight info for the query
|
|
264
|
+
const descriptor = { type: "cmd", cmd: cmdBytes };
|
|
265
|
+
const flightInfo = await this.#flight.getFlightInfo(descriptor);
|
|
266
|
+
// Fetch data from each endpoint
|
|
267
|
+
for (const endpoint of flightInfo.endpoint) {
|
|
268
|
+
if (!endpoint.ticket) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
// Yield FlightData from this endpoint
|
|
272
|
+
for await (const data of this.#flight.doGet(endpoint.ticket)) {
|
|
273
|
+
yield data;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Get FlightInfo for a SQL query without executing it.
|
|
279
|
+
*
|
|
280
|
+
* Use this to inspect the query plan, schema, or endpoints
|
|
281
|
+
* before fetching data.
|
|
282
|
+
*
|
|
283
|
+
* @param query - SQL query string
|
|
284
|
+
* @param options - Query execution options
|
|
285
|
+
* @returns FlightInfo describing the query results
|
|
286
|
+
*/
|
|
287
|
+
async getQueryInfo(query, options) {
|
|
288
|
+
const command = create(CommandStatementQuerySchema, {
|
|
289
|
+
query,
|
|
290
|
+
transactionId: options?.transactionId
|
|
291
|
+
});
|
|
292
|
+
const cmdBytes = packAny("CommandStatementQuery", toBinary(CommandStatementQuerySchema, command));
|
|
293
|
+
return this.#flight.getFlightInfo({ type: "cmd", cmd: cmdBytes });
|
|
294
|
+
}
|
|
295
|
+
// ── SQL Update Execution ────────────────────────────────────────────
|
|
296
|
+
/**
|
|
297
|
+
* Execute a SQL update statement (INSERT, UPDATE, DELETE, DDL).
|
|
298
|
+
*
|
|
299
|
+
* @param query - SQL update statement
|
|
300
|
+
* @param options - Update execution options
|
|
301
|
+
* @returns Update result with affected row count
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```ts
|
|
305
|
+
* const result = await client.executeUpdate("DELETE FROM users WHERE inactive = true")
|
|
306
|
+
* console.log(`Deleted ${result.recordCount} inactive users`)
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
async executeUpdate(query, options) {
|
|
310
|
+
// Create CommandStatementUpdate
|
|
311
|
+
const command = create(CommandStatementUpdateSchema, {
|
|
312
|
+
query,
|
|
313
|
+
transactionId: options?.transactionId
|
|
314
|
+
});
|
|
315
|
+
const cmdBytes = packAny("CommandStatementUpdate", toBinary(CommandStatementUpdateSchema, command));
|
|
316
|
+
// For updates, we use DoPut with the command as descriptor
|
|
317
|
+
// Create empty FlightData stream with descriptor
|
|
318
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
319
|
+
async function* emptyStream() {
|
|
320
|
+
yield {
|
|
321
|
+
flightDescriptor: {
|
|
322
|
+
type: 2, // CMD
|
|
323
|
+
cmd: cmdBytes,
|
|
324
|
+
path: []
|
|
325
|
+
},
|
|
326
|
+
dataHeader: new Uint8Array(),
|
|
327
|
+
dataBody: new Uint8Array(),
|
|
328
|
+
appMetadata: new Uint8Array()
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
// Execute DoPut and collect result
|
|
332
|
+
let updateResult;
|
|
333
|
+
for await (const result of this.#flight.doPut(emptyStream())) {
|
|
334
|
+
// Parse the result as DoPutUpdateResult
|
|
335
|
+
if (result.appMetadata.length > 0) {
|
|
336
|
+
updateResult = fromBinary(DoPutUpdateResultSchema, result.appMetadata);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
recordCount: updateResult?.recordCount ?? -1n
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
// ── Prepared Statements ─────────────────────────────────────────────
|
|
344
|
+
/**
|
|
345
|
+
* Prepare a SQL statement for repeated execution.
|
|
346
|
+
*
|
|
347
|
+
* @param query - SQL query with optional parameter placeholders
|
|
348
|
+
* @param transactionId - Optional transaction to associate with the statement
|
|
349
|
+
* @returns Prepared statement handle
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```ts
|
|
353
|
+
* const stmt = await client.prepare("SELECT * FROM users WHERE id = ?")
|
|
354
|
+
* // Use executePrepared() to execute with parameters
|
|
355
|
+
* await client.closePreparedStatement(stmt)
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
async prepare(query, transactionId) {
|
|
359
|
+
const request = create(ActionCreatePreparedStatementRequestSchema, {
|
|
360
|
+
query,
|
|
361
|
+
transactionId
|
|
362
|
+
});
|
|
363
|
+
const requestBytes = packAny("ActionCreatePreparedStatementRequest", toBinary(ActionCreatePreparedStatementRequestSchema, request));
|
|
364
|
+
let result;
|
|
365
|
+
for await (const response of this.#flight.doAction({
|
|
366
|
+
type: FLIGHT_SQL_ACTIONS.CREATE_PREPARED_STATEMENT,
|
|
367
|
+
body: requestBytes
|
|
368
|
+
})) {
|
|
369
|
+
result = fromBinary(ActionCreatePreparedStatementResultSchema, unpackAny(response.body));
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
if (!result) {
|
|
373
|
+
throw new FlightError("failed to create prepared statement: no response from server");
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
handle: result.preparedStatementHandle,
|
|
377
|
+
datasetSchema: result.datasetSchema,
|
|
378
|
+
parameterSchema: result.parameterSchema
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Execute a prepared query statement and return results as a Table.
|
|
383
|
+
*
|
|
384
|
+
* @param statement - Prepared statement from prepare()
|
|
385
|
+
* @returns Arrow Table containing query results
|
|
386
|
+
*/
|
|
387
|
+
async executePrepared(statement) {
|
|
388
|
+
const stream = this.executePreparedStream(statement);
|
|
389
|
+
return decodeFlightDataToTable(stream);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Execute a prepared query statement and return results as a stream.
|
|
393
|
+
*
|
|
394
|
+
* @param statement - Prepared statement from prepare()
|
|
395
|
+
* @yields FlightData messages
|
|
396
|
+
*/
|
|
397
|
+
async *executePreparedStream(statement) {
|
|
398
|
+
const command = create(CommandPreparedStatementQuerySchema, {
|
|
399
|
+
preparedStatementHandle: statement.handle
|
|
400
|
+
});
|
|
401
|
+
const cmdBytes = packAny("CommandPreparedStatementQuery", toBinary(CommandPreparedStatementQuerySchema, command));
|
|
402
|
+
const flightInfo = await this.#flight.getFlightInfo({ type: "cmd", cmd: cmdBytes });
|
|
403
|
+
for (const endpoint of flightInfo.endpoint) {
|
|
404
|
+
if (!endpoint.ticket) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
for await (const data of this.#flight.doGet(endpoint.ticket)) {
|
|
408
|
+
yield data;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Execute a prepared update statement and return affected row count.
|
|
414
|
+
*
|
|
415
|
+
* @param statement - Prepared statement from prepare()
|
|
416
|
+
* @param parameters - Optional parameter values as RecordBatches
|
|
417
|
+
* @returns Update result with affected row count
|
|
418
|
+
*/
|
|
419
|
+
async executePreparedUpdate(statement, parameters) {
|
|
420
|
+
const command = create(CommandPreparedStatementUpdateSchema, {
|
|
421
|
+
preparedStatementHandle: statement.handle
|
|
422
|
+
});
|
|
423
|
+
const cmdBytes = packAny("CommandPreparedStatementUpdate", toBinary(CommandPreparedStatementUpdateSchema, command));
|
|
424
|
+
// If parameters provided, send them via DoPut
|
|
425
|
+
// Otherwise send empty stream
|
|
426
|
+
let dataStream;
|
|
427
|
+
if (parameters) {
|
|
428
|
+
// We need a schema to encode - for now we'll collect batches and use first batch's schema
|
|
429
|
+
const batchArray = [];
|
|
430
|
+
for await (const batch of parameters) {
|
|
431
|
+
batchArray.push(batch);
|
|
432
|
+
}
|
|
433
|
+
if (batchArray.length > 0) {
|
|
434
|
+
const { schema } = batchArray[0];
|
|
435
|
+
// Wrap encoded data with descriptor
|
|
436
|
+
async function* withDescriptor(encoded) {
|
|
437
|
+
let first = true;
|
|
438
|
+
for await (const data of encoded) {
|
|
439
|
+
if (first) {
|
|
440
|
+
yield {
|
|
441
|
+
...data,
|
|
442
|
+
flightDescriptor: { type: 2, cmd: cmdBytes, path: [] }
|
|
443
|
+
};
|
|
444
|
+
first = false;
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
yield data;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
dataStream = withDescriptor(encodeRecordBatchesToFlightData(batchArray, schema));
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
dataStream = this.#createEmptyStream(cmdBytes);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
dataStream = this.#createEmptyStream(cmdBytes);
|
|
459
|
+
}
|
|
460
|
+
let updateResult;
|
|
461
|
+
for await (const result of this.#flight.doPut(dataStream)) {
|
|
462
|
+
if (result.appMetadata.length > 0) {
|
|
463
|
+
updateResult = fromBinary(DoPutUpdateResultSchema, result.appMetadata);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
recordCount: updateResult?.recordCount ?? -1n
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Close a prepared statement and release server resources.
|
|
472
|
+
*
|
|
473
|
+
* @param statement - Prepared statement to close
|
|
474
|
+
*/
|
|
475
|
+
async closePreparedStatement(statement) {
|
|
476
|
+
const request = create(ActionClosePreparedStatementRequestSchema, {
|
|
477
|
+
preparedStatementHandle: statement.handle
|
|
478
|
+
});
|
|
479
|
+
const requestBytes = packAny("ActionClosePreparedStatementRequest", toBinary(ActionClosePreparedStatementRequestSchema, request));
|
|
480
|
+
// Fire and forget - no response expected
|
|
481
|
+
for await (const _ of this.#flight.doAction({
|
|
482
|
+
type: FLIGHT_SQL_ACTIONS.CLOSE_PREPARED_STATEMENT,
|
|
483
|
+
body: requestBytes
|
|
484
|
+
})) {
|
|
485
|
+
// Consume stream
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// ── Transactions ────────────────────────────────────────────────────
|
|
489
|
+
/**
|
|
490
|
+
* Begin a new transaction.
|
|
491
|
+
*
|
|
492
|
+
* @returns Transaction handle
|
|
493
|
+
*
|
|
494
|
+
* @example
|
|
495
|
+
* ```ts
|
|
496
|
+
* const txn = await client.beginTransaction()
|
|
497
|
+
* try {
|
|
498
|
+
* await client.executeUpdate("INSERT INTO users ...", { transactionId: txn.id })
|
|
499
|
+
* await client.executeUpdate("INSERT INTO logs ...", { transactionId: txn.id })
|
|
500
|
+
* await client.commit(txn)
|
|
501
|
+
* } catch (e) {
|
|
502
|
+
* await client.rollback(txn)
|
|
503
|
+
* throw e
|
|
504
|
+
* }
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
async beginTransaction() {
|
|
508
|
+
const request = create(ActionBeginTransactionRequestSchema, {});
|
|
509
|
+
const requestBytes = packAny("ActionBeginTransactionRequest", toBinary(ActionBeginTransactionRequestSchema, request));
|
|
510
|
+
let result;
|
|
511
|
+
for await (const response of this.#flight.doAction({
|
|
512
|
+
type: FLIGHT_SQL_ACTIONS.BEGIN_TRANSACTION,
|
|
513
|
+
body: requestBytes
|
|
514
|
+
})) {
|
|
515
|
+
result = fromBinary(ActionBeginTransactionResultSchema, unpackAny(response.body));
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
if (!result) {
|
|
519
|
+
throw new FlightError("failed to begin transaction: no response from server");
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
id: result.transactionId
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Commit a transaction.
|
|
527
|
+
*
|
|
528
|
+
* @param transaction - Transaction to commit
|
|
529
|
+
*/
|
|
530
|
+
async commit(transaction) {
|
|
531
|
+
await this.#endTransaction(transaction, ActionEndTransactionRequest_EndTransaction.COMMIT);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Roll back a transaction.
|
|
535
|
+
*
|
|
536
|
+
* @param transaction - Transaction to roll back
|
|
537
|
+
*/
|
|
538
|
+
async rollback(transaction) {
|
|
539
|
+
await this.#endTransaction(transaction, ActionEndTransactionRequest_EndTransaction.ROLLBACK);
|
|
540
|
+
}
|
|
541
|
+
// ── Database Metadata ───────────────────────────────────────────────
|
|
542
|
+
/**
|
|
543
|
+
* Get the list of catalogs in the database.
|
|
544
|
+
*
|
|
545
|
+
* @returns Arrow Table with catalog_name column
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```ts
|
|
549
|
+
* const catalogs = await client.getCatalogs()
|
|
550
|
+
* for (const row of catalogs) {
|
|
551
|
+
* console.log(row.catalog_name)
|
|
552
|
+
* }
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
async getCatalogs() {
|
|
556
|
+
const command = create(CommandGetCatalogsSchema, {});
|
|
557
|
+
const cmdBytes = packAny("CommandGetCatalogs", toBinary(CommandGetCatalogsSchema, command));
|
|
558
|
+
return this.#executeMetadataQuery(cmdBytes);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Get the list of database schemas.
|
|
562
|
+
*
|
|
563
|
+
* @param options - Filter options
|
|
564
|
+
* @returns Arrow Table with catalog_name and db_schema_name columns
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* ```ts
|
|
568
|
+
* const schemas = await client.getDbSchemas({ catalog: "my_catalog" })
|
|
569
|
+
* ```
|
|
570
|
+
*/
|
|
571
|
+
async getDbSchemas(options) {
|
|
572
|
+
const command = create(CommandGetDbSchemasSchema, {
|
|
573
|
+
catalog: options?.catalog,
|
|
574
|
+
dbSchemaFilterPattern: options?.dbSchemaFilterPattern
|
|
575
|
+
});
|
|
576
|
+
const cmdBytes = packAny("CommandGetDbSchemas", toBinary(CommandGetDbSchemasSchema, command));
|
|
577
|
+
return this.#executeMetadataQuery(cmdBytes);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Get the list of tables.
|
|
581
|
+
*
|
|
582
|
+
* @param options - Filter options
|
|
583
|
+
* @returns Arrow Table with catalog_name, db_schema_name, table_name, table_type columns
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```ts
|
|
587
|
+
* const tables = await client.getTables({ tableTypes: ["TABLE", "VIEW"] })
|
|
588
|
+
* ```
|
|
589
|
+
*/
|
|
590
|
+
async getTables(options) {
|
|
591
|
+
const command = create(CommandGetTablesSchema, {
|
|
592
|
+
catalog: options?.catalog,
|
|
593
|
+
dbSchemaFilterPattern: options?.dbSchemaFilterPattern,
|
|
594
|
+
tableNameFilterPattern: options?.tableNameFilterPattern,
|
|
595
|
+
tableTypes: options?.tableTypes ?? [],
|
|
596
|
+
includeSchema: options?.includeSchema ?? false
|
|
597
|
+
});
|
|
598
|
+
const cmdBytes = packAny("CommandGetTables", toBinary(CommandGetTablesSchema, command));
|
|
599
|
+
return this.#executeMetadataQuery(cmdBytes);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Get the list of table types supported by the server.
|
|
603
|
+
*
|
|
604
|
+
* @returns Arrow Table with table_type column
|
|
605
|
+
*
|
|
606
|
+
* @example
|
|
607
|
+
* ```ts
|
|
608
|
+
* const tableTypes = await client.getTableTypes()
|
|
609
|
+
* // Common types: TABLE, VIEW, SYSTEM TABLE
|
|
610
|
+
* ```
|
|
611
|
+
*/
|
|
612
|
+
async getTableTypes() {
|
|
613
|
+
const command = create(CommandGetTableTypesSchema, {});
|
|
614
|
+
const cmdBytes = packAny("CommandGetTableTypes", toBinary(CommandGetTableTypesSchema, command));
|
|
615
|
+
return this.#executeMetadataQuery(cmdBytes);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Get the primary keys for a table.
|
|
619
|
+
*
|
|
620
|
+
* @param table - Table name (required)
|
|
621
|
+
* @param options - Additional filter options
|
|
622
|
+
* @returns Arrow Table with catalog_name, db_schema_name, table_name, column_name, key_name, key_sequence columns
|
|
623
|
+
*
|
|
624
|
+
* @example
|
|
625
|
+
* ```ts
|
|
626
|
+
* const keys = await client.getPrimaryKeys("users", { catalog: "my_db" })
|
|
627
|
+
* ```
|
|
628
|
+
*/
|
|
629
|
+
async getPrimaryKeys(table, options) {
|
|
630
|
+
const command = create(CommandGetPrimaryKeysSchema, {
|
|
631
|
+
table,
|
|
632
|
+
catalog: options?.catalog,
|
|
633
|
+
dbSchema: options?.dbSchema
|
|
634
|
+
});
|
|
635
|
+
const cmdBytes = packAny("CommandGetPrimaryKeys", toBinary(CommandGetPrimaryKeysSchema, command));
|
|
636
|
+
return this.#executeMetadataQuery(cmdBytes);
|
|
637
|
+
}
|
|
638
|
+
// ── Private Helpers ─────────────────────────────────────────────────
|
|
639
|
+
/**
|
|
640
|
+
* Execute a metadata query and return the results as a Table.
|
|
641
|
+
*/
|
|
642
|
+
async #executeMetadataQuery(cmdBytes) {
|
|
643
|
+
const descriptor = { type: "cmd", cmd: cmdBytes };
|
|
644
|
+
const flightInfo = await this.#flight.getFlightInfo(descriptor);
|
|
645
|
+
// Collect all data from all endpoints
|
|
646
|
+
const allData = [];
|
|
647
|
+
for (const endpoint of flightInfo.endpoint) {
|
|
648
|
+
if (!endpoint.ticket) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
for await (const data of this.#flight.doGet(endpoint.ticket)) {
|
|
652
|
+
allData.push(data);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// Convert to async iterable for decoding
|
|
656
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
657
|
+
async function* toAsyncIterable(items) {
|
|
658
|
+
for (const item of items) {
|
|
659
|
+
yield item;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return decodeFlightDataToTable(toAsyncIterable(allData));
|
|
663
|
+
}
|
|
664
|
+
/** Commits or rolls back a transaction using EndTransaction action. */
|
|
665
|
+
async #endTransaction(transaction, action) {
|
|
666
|
+
const request = create(ActionEndTransactionRequestSchema, {
|
|
667
|
+
transactionId: transaction.id,
|
|
668
|
+
action
|
|
669
|
+
});
|
|
670
|
+
const requestBytes = packAny("ActionEndTransactionRequest", toBinary(ActionEndTransactionRequestSchema, request));
|
|
671
|
+
for await (const _ of this.#flight.doAction({
|
|
672
|
+
type: FLIGHT_SQL_ACTIONS.END_TRANSACTION,
|
|
673
|
+
body: requestBytes
|
|
674
|
+
})) {
|
|
675
|
+
// Consume stream
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/** Creates an empty FlightData stream with only a descriptor (for updates). */
|
|
679
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
680
|
+
async *#createEmptyStream(cmdBytes) {
|
|
681
|
+
yield {
|
|
682
|
+
flightDescriptor: { type: 2, cmd: cmdBytes, path: [] },
|
|
683
|
+
dataHeader: new Uint8Array(),
|
|
684
|
+
dataBody: new Uint8Array(),
|
|
685
|
+
appMetadata: new Uint8Array()
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
//# sourceMappingURL=flight-sql-client.js.map
|