recker 1.0.102 → 1.0.103

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.
@@ -1,13 +1,91 @@
1
1
  import { performance } from 'node:perf_hooks';
2
2
  import { connect as connectTcp } from 'node:net';
3
3
  import { connect as connectTls } from 'node:tls';
4
+ import * as grpc from '@grpc/grpc-js';
5
+ import protobuf from 'protobufjs';
4
6
  import { createClient } from '../core/client.js';
5
- import { AuthenticationError, ConfigurationError, ConnectionError, NotFoundError, ParseError, ProtocolError, UnsupportedError, ValidationError, } from '../core/errors.js';
7
+ import { AbortError, AuthenticationError, ConfigurationError, ConnectionError, NotFoundError, ParseError, ProtocolError, UnsupportedError, ValidationError, } from '../core/errors.js';
8
+ import { REDDB_PROTO } from './reddb-proto.js';
9
+ const PROTO_OBJECT_OPTIONS = {
10
+ longs: Number,
11
+ enums: String,
12
+ defaults: false,
13
+ arrays: true,
14
+ objects: true,
15
+ oneofs: true,
16
+ };
17
+ const parsedProto = protobuf.parse(REDDB_PROTO, { keepCase: true });
18
+ const protoRoot = parsedProto.root.resolveAll();
19
+ const protoService = protoRoot.lookupService('reddb.v1.RedDb');
20
+ const grpcDefinition = buildGrpcDefinition(protoService);
21
+ const GrpcRedDbClient = grpc.makeGenericClientConstructor(grpcDefinition, 'RedDb');
6
22
  const WIRE_MSG_QUERY = 0x01;
7
23
  const WIRE_MSG_RESULT = 0x02;
8
24
  const WIRE_MSG_ERROR = 0x03;
9
25
  const WIRE_MSG_BULK_INSERT = 0x04;
10
26
  const WIRE_MSG_BULK_OK = 0x05;
27
+ const WIRE_MSG_BULK_INSERT_BINARY = 0x06;
28
+ const WIRE_VAL_NULL = 0;
29
+ const WIRE_VAL_I64 = 1;
30
+ const WIRE_VAL_F64 = 2;
31
+ const WIRE_VAL_TEXT = 3;
32
+ const WIRE_VAL_BOOL = 4;
33
+ const WIRE_VAL_U64 = 5;
34
+ const QUERY_PROFILE = {
35
+ preferences: ['wire', 'grpc', 'http'],
36
+ };
37
+ const BATCH_PROFILE = {
38
+ preferences: ['wire', 'grpc', 'http'],
39
+ emulated: ['wire', 'http'],
40
+ };
41
+ const CONTROL_PROFILE = {
42
+ preferences: ['grpc', 'http'],
43
+ supported: { wire: false },
44
+ };
45
+ const ROW_SCAN_PROFILE = {
46
+ preferences: ['grpc', 'http'],
47
+ supported: { wire: false },
48
+ };
49
+ const ROW_BULK_PROFILE = {
50
+ preferences: ['wire', 'grpc', 'http'],
51
+ };
52
+ const ROW_MUTATION_PROFILE = {
53
+ preferences: ['grpc', 'http'],
54
+ supported: { wire: false },
55
+ };
56
+ const ENTITY_MUTATION_PROFILE = {
57
+ preferences: ['grpc', 'http'],
58
+ supported: { wire: false },
59
+ };
60
+ const VECTOR_SEARCH_PROFILE = {
61
+ preferences: ['grpc', 'http'],
62
+ supported: { wire: false },
63
+ };
64
+ const KV_HTTP_PROFILE = {
65
+ preferences: ['http'],
66
+ supported: { grpc: false, wire: false },
67
+ };
68
+ const KV_LIST_PROFILE = {
69
+ preferences: ['http', 'grpc', 'wire'],
70
+ };
71
+ function buildGrpcDefinition(service) {
72
+ const definition = {};
73
+ for (const method of service.methodsArray) {
74
+ definition[method.name] = {
75
+ path: `/${service.fullName.replace(/^\./, '')}/${method.name}`,
76
+ requestStream: false,
77
+ responseStream: false,
78
+ requestSerialize: (value) => Buffer.from(method.resolvedRequestType.encode(method.resolvedRequestType.fromObject(value)).finish()),
79
+ requestDeserialize: (value) => method.resolvedRequestType.toObject(method.resolvedRequestType.decode(value), PROTO_OBJECT_OPTIONS),
80
+ responseSerialize: (value) => Buffer.from(method.resolvedResponseType.encode(method.resolvedResponseType.fromObject(value)).finish()),
81
+ responseDeserialize: (value) => method.resolvedResponseType.toObject(method.resolvedResponseType.decode(value), PROTO_OBJECT_OPTIONS),
82
+ };
83
+ }
84
+ return definition;
85
+ }
86
+ function isPlainObject(value) {
87
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
88
+ }
11
89
  function isBufferLike(value) {
12
90
  return Buffer.isBuffer(value) || value instanceof Uint8Array;
13
91
  }
@@ -50,6 +128,32 @@ function parseWireAddress(address) {
50
128
  }
51
129
  return { host, port };
52
130
  }
131
+ function parseGrpcAddress(address) {
132
+ if (!address || !address.trim()) {
133
+ throw new ConfigurationError('grpcAddress must be a non-empty target string', {
134
+ configKey: 'grpcAddress',
135
+ });
136
+ }
137
+ const trimmed = address.trim();
138
+ if (trimmed.includes('://')) {
139
+ const url = new URL(trimmed);
140
+ const secure = url.protocol === 'grpcs:' || url.protocol === 'https:';
141
+ const port = Number(url.port || (secure ? 443 : 50051));
142
+ if (!url.hostname || !Number.isInteger(port) || port <= 0) {
143
+ throw new ConfigurationError(`Invalid grpcAddress: ${address}`, {
144
+ configKey: 'grpcAddress',
145
+ });
146
+ }
147
+ return {
148
+ target: `${url.hostname}:${port}`,
149
+ secure,
150
+ };
151
+ }
152
+ return {
153
+ target: trimmed,
154
+ secure: false,
155
+ };
156
+ }
53
157
  function escapeSqlIdentifier(value) {
54
158
  if (!value || !value.trim()) {
55
159
  throw new ValidationError('SQL identifier cannot be empty', {
@@ -59,12 +163,74 @@ function escapeSqlIdentifier(value) {
59
163
  }
60
164
  return `"${value.replace(/"/g, '""')}"`;
61
165
  }
166
+ function escapeSqlLiteral(value) {
167
+ return `'${value.replace(/'/g, "''")}'`;
168
+ }
169
+ function toSafeNumber(value) {
170
+ if (typeof value === 'number' && Number.isFinite(value)) {
171
+ return value;
172
+ }
173
+ if (typeof value === 'string' && value.trim()) {
174
+ const parsed = Number(value);
175
+ if (Number.isFinite(parsed)) {
176
+ return parsed;
177
+ }
178
+ }
179
+ return undefined;
180
+ }
181
+ function toPositiveInteger(value, field, minimum = 0) {
182
+ const parsed = toSafeNumber(value);
183
+ if (parsed === undefined || !Number.isInteger(parsed) || parsed < minimum) {
184
+ throw new ValidationError(`${field} must be an integer >= ${minimum}`, {
185
+ field,
186
+ value,
187
+ });
188
+ }
189
+ return parsed;
190
+ }
191
+ function parseJsonText(text, context) {
192
+ try {
193
+ return JSON.parse(text);
194
+ }
195
+ catch (error) {
196
+ throw new ParseError(`Failed to parse RedDB ${context}: ${error instanceof Error ? error.message : String(error)}`, { format: 'json' });
197
+ }
198
+ }
199
+ function parseMaybeJson(text) {
200
+ const trimmed = text.trim();
201
+ if (!trimmed) {
202
+ return null;
203
+ }
204
+ try {
205
+ return JSON.parse(trimmed);
206
+ }
207
+ catch {
208
+ return text;
209
+ }
210
+ }
211
+ function stringifyJson(value, context) {
212
+ try {
213
+ return JSON.stringify(value);
214
+ }
215
+ catch (error) {
216
+ throw new ValidationError(`Failed to serialize ${context} as JSON: ${error instanceof Error ? error.message : String(error)}`, { field: context, value });
217
+ }
218
+ }
219
+ function ensureNonEmptyString(value, field, message) {
220
+ if (!value || !value.trim()) {
221
+ throw new ValidationError(message || `${field} must be a non-empty string`, {
222
+ field,
223
+ value,
224
+ });
225
+ }
226
+ return value;
227
+ }
62
228
  function buildQueryStats(data) {
63
- if (!data || typeof data !== 'object') {
229
+ if (!isPlainObject(data)) {
64
230
  return {};
65
231
  }
66
232
  const queryData = data;
67
- const stats = queryData.result?.stats;
233
+ const stats = isPlainObject(queryData.result?.stats) ? queryData.result?.stats : undefined;
68
234
  return {
69
235
  recordCount: typeof queryData.record_count === 'number' ? queryData.record_count : undefined,
70
236
  affectedRows: typeof queryData.affected_rows === 'number' ? queryData.affected_rows : undefined,
@@ -91,24 +257,197 @@ async function mapWithConcurrency(items, concurrency, worker) {
91
257
  await Promise.all(Array.from({ length: safeConcurrency }, () => run()));
92
258
  return results;
93
259
  }
260
+ function normalizeQueryData(data) {
261
+ if (!isPlainObject(data)) {
262
+ return {};
263
+ }
264
+ const normalized = { ...data };
265
+ normalized.record_count = toSafeNumber(data.record_count);
266
+ normalized.affected_rows = toSafeNumber(data.affected_rows);
267
+ const resultJson = typeof data.result_json === 'string' ? parseMaybeJson(data.result_json) : undefined;
268
+ const sourceResult = isPlainObject(data.result) ? { ...data.result } : undefined;
269
+ const jsonResult = isPlainObject(resultJson) ? { ...resultJson } : undefined;
270
+ if (sourceResult || jsonResult || Array.isArray(data.columns)) {
271
+ normalized.result = {
272
+ ...(jsonResult || {}),
273
+ ...(sourceResult || {}),
274
+ };
275
+ }
276
+ if (normalized.result && Array.isArray(data.columns)) {
277
+ normalized.result.columns = data.columns.map((value) => String(value));
278
+ }
279
+ if (isPlainObject(normalized.result?.stats)) {
280
+ normalized.result.stats = {
281
+ ...normalized.result.stats,
282
+ rows_scanned: toSafeNumber(normalized.result.stats.rows_scanned),
283
+ exec_time_us: toSafeNumber(normalized.result.stats.exec_time_us),
284
+ nodes_scanned: toSafeNumber(normalized.result.stats.nodes_scanned),
285
+ edges_scanned: toSafeNumber(normalized.result.stats.edges_scanned),
286
+ };
287
+ }
288
+ return normalized;
289
+ }
290
+ function normalizeBatchQueryData(data) {
291
+ if (!isPlainObject(data) || !Array.isArray(data.results)) {
292
+ return { results: [] };
293
+ }
294
+ return {
295
+ ...data,
296
+ results: data.results.map((item) => normalizeQueryData(item)),
297
+ };
298
+ }
299
+ function normalizeScanData(data) {
300
+ if (!isPlainObject(data)) {
301
+ return {
302
+ collection: '',
303
+ total: 0,
304
+ next_offset: null,
305
+ items: [],
306
+ };
307
+ }
308
+ return {
309
+ ...data,
310
+ collection: typeof data.collection === 'string' ? data.collection : '',
311
+ total: toSafeNumber(data.total) ?? 0,
312
+ next_offset: toSafeNumber(data.next_offset) ?? null,
313
+ items: Array.isArray(data.items)
314
+ ? data.items.map((item) => ({
315
+ ...(isPlainObject(item) ? item : {}),
316
+ id: toSafeNumber(isPlainObject(item) ? item.id : undefined) ?? 0,
317
+ kind: isPlainObject(item) && typeof item.kind === 'string' ? item.kind : '',
318
+ collection: isPlainObject(item) && typeof item.collection === 'string' ? item.collection : '',
319
+ json: isPlainObject(item) && typeof item.json === 'string' ? item.json : '',
320
+ }))
321
+ : [],
322
+ };
323
+ }
324
+ function normalizeEntityData(data) {
325
+ if (!isPlainObject(data)) {
326
+ return {};
327
+ }
328
+ const entity = typeof data.entity_json === 'string'
329
+ ? parseMaybeJson(data.entity_json)
330
+ : data.entity;
331
+ return {
332
+ ...data,
333
+ id: toSafeNumber(data.id),
334
+ entity,
335
+ entity_json: typeof data.entity_json === 'string' ? data.entity_json : undefined,
336
+ };
337
+ }
338
+ function normalizeBulkEntityData(data) {
339
+ if (!isPlainObject(data)) {
340
+ return {};
341
+ }
342
+ return {
343
+ ...data,
344
+ count: toSafeNumber(data.count),
345
+ first_id: toSafeNumber(data.first_id),
346
+ items: Array.isArray(data.items) ? data.items.map((item) => normalizeEntityData(item)) : undefined,
347
+ };
348
+ }
349
+ function normalizeBulkInsertData(data) {
350
+ if (!isPlainObject(data)) {
351
+ return {};
352
+ }
353
+ return {
354
+ ...data,
355
+ count: toSafeNumber(data.count),
356
+ first_id: toSafeNumber(data.first_id),
357
+ };
358
+ }
359
+ function normalizeCollectionsData(data) {
360
+ if (Array.isArray(data)) {
361
+ return { collections: data.map((value) => String(value)) };
362
+ }
363
+ if (!isPlainObject(data)) {
364
+ return { collections: [] };
365
+ }
366
+ return {
367
+ ...data,
368
+ collections: Array.isArray(data.collections) ? data.collections.map((value) => String(value)) : [],
369
+ };
370
+ }
371
+ function normalizeHealthData(data) {
372
+ if (!isPlainObject(data)) {
373
+ return {};
374
+ }
375
+ return {
376
+ ...data,
377
+ checked_at_unix_ms: toSafeNumber(data.checked_at_unix_ms),
378
+ };
379
+ }
380
+ function normalizeStatsData(data) {
381
+ if (!isPlainObject(data)) {
382
+ return {};
383
+ }
384
+ return {
385
+ ...data,
386
+ collection_count: toSafeNumber(data.collection_count),
387
+ total_entities: toSafeNumber(data.total_entities),
388
+ total_memory_bytes: toSafeNumber(data.total_memory_bytes),
389
+ cross_ref_count: toSafeNumber(data.cross_ref_count),
390
+ active_connections: toSafeNumber(data.active_connections),
391
+ idle_connections: toSafeNumber(data.idle_connections),
392
+ total_checkouts: toSafeNumber(data.total_checkouts),
393
+ started_at_unix_ms: toSafeNumber(data.started_at_unix_ms),
394
+ };
395
+ }
396
+ function normalizePayloadValue(data) {
397
+ if (!isPlainObject(data)) {
398
+ return { payload: data };
399
+ }
400
+ const ok = typeof data.ok === 'boolean' ? data.ok : undefined;
401
+ const payload = typeof data.payload === 'string' ? parseMaybeJson(data.payload) : data.payload;
402
+ if (isPlainObject(payload)) {
403
+ return ok === undefined || Object.prototype.hasOwnProperty.call(payload, 'ok')
404
+ ? payload
405
+ : { ok, ...payload };
406
+ }
407
+ if (Array.isArray(payload)) {
408
+ return {
409
+ ok,
410
+ items: payload,
411
+ };
412
+ }
413
+ if (payload !== undefined) {
414
+ return {
415
+ ok,
416
+ payload,
417
+ };
418
+ }
419
+ return { ...data };
420
+ }
421
+ function normalizeOperationData(data, extras = {}) {
422
+ const base = isPlainObject(data) ? { ...data } : {};
423
+ for (const [key, value] of Object.entries(extras)) {
424
+ if (!Object.prototype.hasOwnProperty.call(base, key)) {
425
+ base[key] = value;
426
+ }
427
+ }
428
+ return base;
429
+ }
94
430
  class RedDbWireSocket {
95
431
  endpoint;
96
432
  tlsOptions;
97
433
  timeout;
434
+ connectTimeout;
435
+ keepAlive;
436
+ keepAliveInitialDelayMs;
98
437
  socket = null;
99
438
  connectPromise = null;
100
439
  pending = [];
101
440
  buffer = Buffer.alloc(0);
102
- constructor(address, tlsOptions, timeout) {
441
+ constructor(address, tlsOptions, timeout, connectTimeout, keepAlive, keepAliveInitialDelayMs) {
103
442
  this.endpoint = parseWireAddress(address);
104
443
  this.timeout = timeout;
444
+ this.connectTimeout = connectTimeout;
445
+ this.keepAlive = keepAlive;
446
+ this.keepAliveInitialDelayMs = keepAliveInitialDelayMs;
105
447
  this.tlsOptions = typeof tlsOptions === 'boolean'
106
448
  ? { enabled: tlsOptions }
107
449
  : { enabled: tlsOptions?.enabled ?? false, ...tlsOptions };
108
450
  }
109
- get available() {
110
- return this.socket !== null && !this.socket.destroyed;
111
- }
112
451
  async query(sql, timeout) {
113
452
  const response = await this.send(WIRE_MSG_QUERY, Buffer.from(sql, 'utf8'), timeout);
114
453
  if (response.type !== WIRE_MSG_RESULT) {
@@ -118,12 +457,7 @@ class RedDbWireSocket {
118
457
  phase: 'query',
119
458
  });
120
459
  }
121
- try {
122
- return JSON.parse(response.payload.toString('utf8'));
123
- }
124
- catch (error) {
125
- throw new ParseError(`Failed to parse RedDB wire query payload: ${error instanceof Error ? error.message : String(error)}`, { format: 'json' });
126
- }
460
+ return parseJsonText(response.payload.toString('utf8'), 'wire query payload');
127
461
  }
128
462
  async bulkInsert(collection, payloads, timeout) {
129
463
  const collectionBuffer = Buffer.from(collection, 'utf8');
@@ -139,23 +473,40 @@ class RedDbWireSocket {
139
473
  parts.push(lengthBuffer, jsonBuffer);
140
474
  }
141
475
  const response = await this.send(WIRE_MSG_BULK_INSERT, Buffer.concat(parts), timeout);
142
- if (response.type !== WIRE_MSG_BULK_OK) {
143
- throw new ProtocolError(`Unexpected wire bulk response type: ${response.type}`, {
144
- protocol: 'reddb',
145
- code: response.type,
146
- phase: 'bulkInsert',
147
- });
476
+ return decodeBulkOkResponse(response, 'bulkInsert');
477
+ }
478
+ async bulkInsertBinary(collection, fieldNames, rows, timeout) {
479
+ const collectionBuffer = Buffer.from(collection, 'utf8');
480
+ const header = Buffer.alloc(2 + collectionBuffer.length + 2);
481
+ header.writeUInt16LE(collectionBuffer.length, 0);
482
+ collectionBuffer.copy(header, 2);
483
+ header.writeUInt16LE(fieldNames.length, 2 + collectionBuffer.length);
484
+ const parts = [header];
485
+ for (const fieldName of fieldNames) {
486
+ const nameBuffer = Buffer.from(fieldName, 'utf8');
487
+ const nameHeader = Buffer.alloc(2);
488
+ nameHeader.writeUInt16LE(nameBuffer.length, 0);
489
+ parts.push(nameHeader, nameBuffer);
148
490
  }
149
- if (response.payload.length >= 8) {
150
- return Number(response.payload.readBigUInt64LE(0));
491
+ const rowHeader = Buffer.alloc(4);
492
+ rowHeader.writeUInt32LE(rows.length, 0);
493
+ parts.push(rowHeader);
494
+ for (const row of rows) {
495
+ if (row.length !== fieldNames.length) {
496
+ throw new ValidationError(`bulkInsertBinary row length ${row.length} does not match fieldNames length ${fieldNames.length}`, { field: 'rows', value: row });
497
+ }
498
+ for (const value of row) {
499
+ parts.push(encodeWireBinaryValue(value));
500
+ }
151
501
  }
152
- return 0;
502
+ const response = await this.send(WIRE_MSG_BULK_INSERT_BINARY, Buffer.concat(parts), timeout);
503
+ return decodeBulkOkResponse(response, 'bulkInsertBinary');
153
504
  }
154
505
  async close() {
155
506
  this.destroy();
156
507
  }
157
508
  async connect() {
158
- if (this.available) {
509
+ if (this.socket && !this.socket.destroyed) {
159
510
  return;
160
511
  }
161
512
  if (this.connectPromise) {
@@ -175,6 +526,7 @@ class RedDbWireSocket {
175
526
  cleanup();
176
527
  if (!socket.destroyed) {
177
528
  socket.setNoDelay(true);
529
+ socket.setKeepAlive(this.keepAlive, this.keepAliveInitialDelayMs);
178
530
  }
179
531
  this.socket = socket;
180
532
  socket.on('data', (chunk) => this.onData(chunk));
@@ -198,7 +550,7 @@ class RedDbWireSocket {
198
550
  host: this.endpoint.host,
199
551
  port: this.endpoint.port,
200
552
  }, onConnect);
201
- socket.setTimeout(this.timeout, () => {
553
+ socket.setTimeout(this.connectTimeout, () => {
202
554
  onError(new Error(`RedDB wire connection to ${this.endpoint.host}:${this.endpoint.port} timed out`));
203
555
  });
204
556
  socket.once('error', onError);
@@ -296,23 +648,298 @@ class RedDbWireSocket {
296
648
  }
297
649
  }
298
650
  }
651
+ class RedDbWirePool {
652
+ clients;
653
+ nextIndex = 0;
654
+ constructor(clients) {
655
+ if (clients.length === 0) {
656
+ throw new ConfigurationError('wire pool requires at least one client', {
657
+ configKey: 'wirePoolSize',
658
+ });
659
+ }
660
+ this.clients = clients;
661
+ }
662
+ async query(sql, timeout) {
663
+ return await this.select().query(sql, timeout);
664
+ }
665
+ async bulkInsert(collection, payloads, timeout) {
666
+ return await this.select().bulkInsert(collection, payloads, timeout);
667
+ }
668
+ async bulkInsertBinary(collection, fieldNames, rows, timeout) {
669
+ return await this.select().bulkInsertBinary(collection, fieldNames, rows, timeout);
670
+ }
671
+ async close() {
672
+ await Promise.all(this.clients.map((client) => client.close()));
673
+ }
674
+ select() {
675
+ const client = this.clients[this.nextIndex % this.clients.length];
676
+ this.nextIndex += 1;
677
+ return client;
678
+ }
679
+ }
680
+ function decodeBulkOkResponse(response, phase) {
681
+ if (response.type !== WIRE_MSG_BULK_OK) {
682
+ throw new ProtocolError(`Unexpected wire bulk response type: ${response.type}`, {
683
+ protocol: 'reddb',
684
+ code: response.type,
685
+ phase,
686
+ });
687
+ }
688
+ if (response.payload.length >= 8) {
689
+ return Number(response.payload.readBigUInt64LE(0));
690
+ }
691
+ return 0;
692
+ }
693
+ function encodeWireBinaryValue(value) {
694
+ if (value === null) {
695
+ return Buffer.from([WIRE_VAL_NULL]);
696
+ }
697
+ if (typeof value === 'string') {
698
+ const text = Buffer.from(value, 'utf8');
699
+ const buffer = Buffer.alloc(1 + 4 + text.length);
700
+ buffer[0] = WIRE_VAL_TEXT;
701
+ buffer.writeUInt32LE(text.length, 1);
702
+ text.copy(buffer, 5);
703
+ return buffer;
704
+ }
705
+ if (typeof value === 'boolean') {
706
+ return Buffer.from([WIRE_VAL_BOOL, value ? 1 : 0]);
707
+ }
708
+ if (typeof value === 'number') {
709
+ if (!Number.isFinite(value)) {
710
+ throw new ValidationError('bulkInsertBinary number values must be finite', {
711
+ field: 'rows',
712
+ value,
713
+ });
714
+ }
715
+ if (Number.isInteger(value)) {
716
+ const buffer = Buffer.alloc(1 + 8);
717
+ buffer[0] = WIRE_VAL_I64;
718
+ buffer.writeBigInt64LE(BigInt(value), 1);
719
+ return buffer;
720
+ }
721
+ const buffer = Buffer.alloc(1 + 8);
722
+ buffer[0] = WIRE_VAL_F64;
723
+ buffer.writeDoubleLE(value, 1);
724
+ return buffer;
725
+ }
726
+ if (typeof value === 'bigint') {
727
+ const maxU64 = (1n << 64n) - 1n;
728
+ const minI64 = -(1n << 63n);
729
+ const maxI64 = (1n << 63n) - 1n;
730
+ if (value >= 0 && value <= maxU64) {
731
+ const buffer = Buffer.alloc(1 + 8);
732
+ buffer[0] = WIRE_VAL_U64;
733
+ buffer.writeBigUInt64LE(value, 1);
734
+ return buffer;
735
+ }
736
+ if (value >= minI64 && value <= maxI64) {
737
+ const buffer = Buffer.alloc(1 + 8);
738
+ buffer[0] = WIRE_VAL_I64;
739
+ buffer.writeBigInt64LE(value, 1);
740
+ return buffer;
741
+ }
742
+ throw new ValidationError('bulkInsertBinary bigint values must fit within 64 bits', {
743
+ field: 'rows',
744
+ value: value.toString(),
745
+ });
746
+ }
747
+ if (isBufferLike(value)) {
748
+ throw new UnsupportedError('RedDB wire bulkInsertBinary does not support byte values; use the gRPC transport for blob payloads', { feature: 'reddb.wire.bulkInsertBinary.bytes' });
749
+ }
750
+ throw new ValidationError('Unsupported bulkInsertBinary value type', {
751
+ field: 'rows',
752
+ value,
753
+ });
754
+ }
755
+ class RedDbGrpcClient {
756
+ client;
757
+ timeout;
758
+ metadataEntries;
759
+ constructor(address, tlsOptions, timeout, metadataEntries, keepalive, channelOptions) {
760
+ const parsed = parseGrpcAddress(address);
761
+ const resolvedTls = typeof tlsOptions === 'boolean'
762
+ ? { enabled: tlsOptions }
763
+ : { enabled: tlsOptions?.enabled ?? parsed.secure, ...tlsOptions };
764
+ const rootCerts = typeof resolvedTls.ca === 'string' ? Buffer.from(resolvedTls.ca) : (resolvedTls.ca ?? null);
765
+ const credentials = resolvedTls.enabled
766
+ ? grpc.credentials.createSsl(rootCerts, null, null, {
767
+ rejectUnauthorized: resolvedTls.rejectUnauthorized ?? true,
768
+ })
769
+ : grpc.credentials.createInsecure();
770
+ const keepaliveOptions = {
771
+ 'grpc.keepalive_time_ms': keepalive?.timeMs ?? 30000,
772
+ 'grpc.keepalive_timeout_ms': keepalive?.timeoutMs ?? 10000,
773
+ 'grpc.keepalive_permit_without_calls': keepalive?.permitWithoutCalls === false ? 0 : 1,
774
+ 'grpc.http2.max_pings_without_data': keepalive?.maxPingsWithoutData ?? 0,
775
+ 'grpc.initial_reconnect_backoff_ms': keepalive?.initialReconnectBackoffMs ?? 250,
776
+ 'grpc.max_reconnect_backoff_ms': keepalive?.maxReconnectBackoffMs ?? 5000,
777
+ };
778
+ const options = {
779
+ ...keepaliveOptions,
780
+ ...(channelOptions || {}),
781
+ };
782
+ if (resolvedTls.servername) {
783
+ options['grpc.ssl_target_name_override'] = resolvedTls.servername;
784
+ options['grpc.default_authority'] = resolvedTls.servername;
785
+ }
786
+ this.client = new GrpcRedDbClient(parsed.target, credentials, options);
787
+ this.timeout = timeout;
788
+ this.metadataEntries = metadataEntries;
789
+ }
790
+ async unary(methodName, request, options) {
791
+ if (options.signal?.aborted) {
792
+ throw new AbortError();
793
+ }
794
+ return await new Promise((resolve, reject) => {
795
+ const metadata = new grpc.Metadata();
796
+ for (const [key, value] of this.metadataEntries) {
797
+ metadata.set(key, value);
798
+ }
799
+ let completed = false;
800
+ const cleanup = () => {
801
+ if (options.signal && onAbort) {
802
+ options.signal.removeEventListener('abort', onAbort);
803
+ }
804
+ };
805
+ const callback = (error, response) => {
806
+ if (completed) {
807
+ return;
808
+ }
809
+ completed = true;
810
+ cleanup();
811
+ if (error) {
812
+ reject(mapGrpcError(methodName, error, options.signal?.aborted ?? false));
813
+ return;
814
+ }
815
+ resolve(response);
816
+ };
817
+ const deadlineMs = options.timeout ?? this.timeout;
818
+ const callOptions = deadlineMs > 0 && Number.isFinite(deadlineMs)
819
+ ? { deadline: new Date(Date.now() + deadlineMs) }
820
+ : {};
821
+ const method = this.client[methodName];
822
+ const call = method.call(this.client, request, metadata, callOptions, callback);
823
+ const onAbort = options.signal
824
+ ? () => {
825
+ if (completed) {
826
+ return;
827
+ }
828
+ call.cancel();
829
+ }
830
+ : null;
831
+ if (options.signal && onAbort) {
832
+ options.signal.addEventListener('abort', onAbort, { once: true });
833
+ }
834
+ });
835
+ }
836
+ close() {
837
+ this.client.close();
838
+ }
839
+ }
840
+ function mapGrpcError(methodName, error, aborted) {
841
+ if (aborted || error.code === grpc.status.CANCELLED) {
842
+ return new AbortError(`RedDB gRPC ${methodName} was aborted`);
843
+ }
844
+ if (error.code === grpc.status.UNAUTHENTICATED) {
845
+ return new AuthenticationError(`RedDB gRPC ${methodName} unauthorized: ${error.message}`, {
846
+ authType: 'bearer',
847
+ });
848
+ }
849
+ if (error.code === grpc.status.NOT_FOUND) {
850
+ return new NotFoundError(`RedDB gRPC ${methodName} not found: ${error.message}`, {
851
+ resource: methodName,
852
+ });
853
+ }
854
+ if (error.code === grpc.status.DEADLINE_EXCEEDED || error.code === grpc.status.UNAVAILABLE) {
855
+ return new ConnectionError(`RedDB gRPC ${methodName} failed: ${error.message}`, {
856
+ code: String(error.code),
857
+ });
858
+ }
859
+ return new ProtocolError(`RedDB gRPC ${methodName} failed: ${error.message}`, {
860
+ protocol: 'reddb',
861
+ code: error.code,
862
+ phase: 'grpc',
863
+ retriable: error.code === grpc.status.RESOURCE_EXHAUSTED || error.code === grpc.status.INTERNAL,
864
+ });
865
+ }
299
866
  export class RedDbClient {
300
867
  baseUrl;
301
868
  httpClient;
869
+ grpcClient;
870
+ wireClient;
302
871
  defaultTransport;
303
872
  allowTransportFallback;
304
873
  timeout;
305
874
  batchConcurrency;
306
- wireClient;
875
+ operationTimeouts;
876
+ system = {
877
+ health: (options = {}) => this.executeSystemHealth('system.health', 'Health', '/health', options),
878
+ ready: (options = {}) => this.executeSystemHealth('system.ready', 'Ready', '/ready', options),
879
+ stats: (options = {}) => this.executeSystemStats(options),
880
+ };
881
+ sql = {
882
+ query: (query, options = {}) => this.executeSqlQuery('sql.query', query, options),
883
+ explain: (query, options = {}) => this.executeSqlExplain(query, options),
884
+ batch: (queries, options = {}) => this.executeSqlBatch(queries, options),
885
+ };
886
+ collections = {
887
+ list: (options = {}) => this.executeCollectionsList(options),
888
+ create: (options) => this.executeCollectionCreate(options),
889
+ describe: (options) => this.executeCollectionDescribe(options),
890
+ drop: (options) => this.executeCollectionDrop(options),
891
+ };
892
+ indexes = {
893
+ list: (options = {}) => this.executeIndexesList(options),
894
+ statuses: (options = {}) => this.executeIndexesStatuses(options),
895
+ create: (options) => this.executeIndexCreate(options),
896
+ enable: (options) => this.executeIndexToggle('indexes.enable', 'enable', true, options),
897
+ disable: (options) => this.executeIndexToggle('indexes.disable', 'disable', false, options),
898
+ warmup: (options) => this.executeIndexWarmup(options),
899
+ rebuild: (options = {}) => this.executeIndexesRebuild(options),
900
+ };
901
+ rows = {
902
+ scan: (request) => this.executeRowsScan(request),
903
+ create: (request) => this.executeRowCreate(request),
904
+ bulkCreate: (request) => this.executeRowsBulkCreate(request),
905
+ patch: (request) => this.executeRowPatch(request),
906
+ delete: (request) => this.executeRowDelete(request),
907
+ };
908
+ documents = {
909
+ create: (request) => this.executeEntityCreate('documents.create', 'CreateDocument', 'documents', request),
910
+ bulkCreate: (request) => this.executeBulkEntityCreate('documents.bulkCreate', 'BulkCreateDocuments', 'bulk/documents', request),
911
+ };
912
+ nodes = {
913
+ create: (request) => this.executeEntityCreate('nodes.create', 'CreateNode', 'nodes', request),
914
+ bulkCreate: (request) => this.executeBulkEntityCreate('nodes.bulkCreate', 'BulkCreateNodes', 'bulk/nodes', request),
915
+ };
916
+ edges = {
917
+ create: (request) => this.executeEntityCreate('edges.create', 'CreateEdge', 'edges', request),
918
+ bulkCreate: (request) => this.executeBulkEntityCreate('edges.bulkCreate', 'BulkCreateEdges', 'bulk/edges', request),
919
+ };
920
+ vectors = {
921
+ create: (request) => this.executeEntityCreate('vectors.create', 'CreateVector', 'vectors', request),
922
+ bulkCreate: (request) => this.executeBulkEntityCreate('vectors.bulkCreate', 'BulkCreateVectors', 'bulk/vectors', request),
923
+ bulkInsertBinary: (request) => this.executeVectorBulkInsertBinary(request),
924
+ similar: (request) => this.executeVectorSearch('vectors.similar', 'Similar', 'similar', request),
925
+ ivfSearch: (request) => this.executeVectorSearch('vectors.ivfSearch', 'IvfSearch', 'ivf/search', request),
926
+ };
927
+ kv = {
928
+ get: (request) => this.executeKvGet(request),
929
+ put: (request) => this.executeKvPut(request),
930
+ delete: (request) => this.executeKvDelete(request),
931
+ list: (request) => this.executeKvList(request),
932
+ };
307
933
  constructor(options = {}) {
308
934
  this.baseUrl = options.baseUrl;
309
935
  this.defaultTransport = options.transport ?? 'auto';
310
936
  this.allowTransportFallback = options.allowTransportFallback ?? true;
311
937
  this.timeout = options.timeout ?? 30000;
312
938
  this.batchConcurrency = Math.max(1, options.batchConcurrency ?? 8);
939
+ this.operationTimeouts = { ...(options.operationTimeouts || {}) };
313
940
  const headers = { ...(options.headers || {}) };
314
941
  if (options.authToken) {
315
- headers.Authorization = `Bearer ${options.authToken}`;
942
+ headers.authorization = `Bearer ${options.authToken}`;
316
943
  }
317
944
  if (options.writeToken) {
318
945
  headers['x-write-token'] = options.writeToken;
@@ -332,274 +959,710 @@ export class RedDbClient {
332
959
  else {
333
960
  this.httpClient = null;
334
961
  }
335
- this.wireClient = options.wireAddress
336
- ? new RedDbWireSocket(options.wireAddress, options.wireTls, this.timeout)
962
+ this.grpcClient = options.grpcAddress
963
+ ? new RedDbGrpcClient(options.grpcAddress, options.grpcTls, this.timeout, Object.entries(headers), options.grpcKeepalive, options.grpcOptions)
337
964
  : null;
965
+ if (options.wireAddress) {
966
+ const wirePoolSize = Math.max(1, options.wirePoolSize ?? 1);
967
+ const wireConnectTimeout = options.wireConnectTimeout ?? this.timeout;
968
+ const wireKeepAlive = options.wireKeepAlive ?? true;
969
+ const wireKeepAliveInitialDelayMs = options.wireKeepAliveInitialDelayMs ?? 30000;
970
+ const clients = Array.from({ length: wirePoolSize }, () => new RedDbWireSocket(options.wireAddress, options.wireTls, this.timeout, wireConnectTimeout, wireKeepAlive, wireKeepAliveInitialDelayMs));
971
+ this.wireClient = wirePoolSize === 1
972
+ ? clients[0]
973
+ : new RedDbWirePool(clients);
974
+ }
975
+ else {
976
+ this.wireClient = null;
977
+ }
338
978
  }
339
979
  getCapabilities() {
340
980
  const hasHttp = this.httpClient !== null;
981
+ const hasGrpc = this.grpcClient !== null;
341
982
  const hasWire = this.wireClient !== null;
342
983
  return {
343
984
  requestedTransport: this.defaultTransport,
344
985
  allowTransportFallback: this.allowTransportFallback,
345
986
  availableTransports: {
346
987
  http: hasHttp,
988
+ grpc: hasGrpc,
347
989
  wire: hasWire,
348
- grpc: false,
349
990
  },
350
- native: {
351
- query: hasHttp || hasWire,
352
- batchQuery: false,
353
- scan: hasHttp,
354
- bulkInsertRows: hasHttp || hasWire,
355
- bulkInsertBinary: false,
356
- stats: hasHttp,
357
- ensureCollection: hasHttp,
358
- ensureIndex: hasHttp || hasWire,
359
- warmupIndex: hasHttp,
991
+ namespaces: {
992
+ system: hasHttp || hasGrpc,
993
+ sql: hasHttp || hasGrpc || hasWire,
994
+ collections: hasHttp || hasGrpc,
995
+ indexes: hasHttp || hasGrpc || hasWire,
996
+ rows: hasHttp || hasGrpc || hasWire,
997
+ documents: hasHttp || hasGrpc,
998
+ nodes: hasHttp || hasGrpc,
999
+ edges: hasHttp || hasGrpc,
1000
+ vectors: hasHttp || hasGrpc || hasWire,
1001
+ kv: hasHttp || hasGrpc || hasWire,
360
1002
  },
361
- emulated: {
362
- batchQuery: hasHttp || hasWire,
363
- bulkInsertBinary: hasHttp || hasWire,
1003
+ features: {
1004
+ grpcNative: hasGrpc,
1005
+ wireSql: hasWire,
1006
+ wireBulkRows: hasWire,
1007
+ wireBinaryBulkScalars: hasWire,
1008
+ httpBinaryBulkEmulation: hasHttp,
1009
+ sqlBatchEmulationOverHttp: hasHttp,
1010
+ sqlBatchEmulationOverWire: hasWire,
1011
+ kvListViaSql: hasHttp || hasGrpc || hasWire,
364
1012
  },
365
1013
  };
366
1014
  }
367
- async query(query, options = {}) {
368
- if (!query || !query.trim()) {
369
- throw new ValidationError('Query must be a non-empty string', {
370
- field: 'query',
371
- value: query,
372
- });
1015
+ async close() {
1016
+ if (this.grpcClient) {
1017
+ this.grpcClient.close();
1018
+ }
1019
+ if (this.wireClient) {
1020
+ await this.wireClient.close();
373
1021
  }
374
- const resolution = this.resolveTransport('query', options.transport);
1022
+ }
1023
+ async executeSystemHealth(operation, grpcMethod, httpPath, options) {
1024
+ const requestOptions = this.withResolvedTimeout(operation, options);
1025
+ const resolution = this.resolveTransport(operation, CONTROL_PROFILE, requestOptions.transport);
375
1026
  const startedAt = Date.now();
376
1027
  const started = performance.now();
377
- const data = resolution.transport === 'wire'
378
- ? await this.runWireQuery(query, options)
379
- : await this.runHttpQuery('/query', {
380
- query,
381
- entity_types: options.entityTypes,
382
- capabilities: options.capabilities,
383
- }, options);
384
- return this.buildEnvelope('query', data, resolution, startedAt, started);
385
- }
386
- async explainQuery(query, options = {}) {
387
- if (!query || !query.trim()) {
388
- throw new ValidationError('Query must be a non-empty string', {
389
- field: 'query',
390
- value: query,
391
- });
392
- }
393
- const resolution = this.resolveTransport('explainQuery', options.transport);
1028
+ const data = resolution.transport === 'grpc'
1029
+ ? normalizeHealthData(await this.runGrpc(grpcMethod, {}, requestOptions))
1030
+ : normalizeHealthData(await this.runHttpGet(httpPath, requestOptions));
1031
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1032
+ }
1033
+ async executeSystemStats(options) {
1034
+ const operation = 'system.stats';
1035
+ const requestOptions = this.withResolvedTimeout(operation, options);
1036
+ const resolution = this.resolveTransport(operation, CONTROL_PROFILE, requestOptions.transport);
1037
+ const startedAt = Date.now();
1038
+ const started = performance.now();
1039
+ const data = resolution.transport === 'grpc'
1040
+ ? normalizeStatsData(await this.runGrpc('Stats', {}, requestOptions))
1041
+ : normalizeStatsData(await this.runHttpGet('/stats', requestOptions));
1042
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1043
+ }
1044
+ async executeSqlQuery(operation, query, options) {
1045
+ ensureNonEmptyString(query, 'query', 'Query must be a non-empty string');
1046
+ const requestOptions = this.withResolvedTimeout(operation, options);
1047
+ const resolution = this.resolveTransport(operation, QUERY_PROFILE, requestOptions.transport);
394
1048
  const startedAt = Date.now();
395
1049
  const started = performance.now();
396
1050
  const data = resolution.transport === 'wire'
397
- ? await this.runWireQuery(`EXPLAIN ${query}`, options)
398
- : await this.runHttpQuery('/query/explain', { query }, options);
399
- return this.buildEnvelope('explainQuery', data, resolution, startedAt, started);
1051
+ ? normalizeQueryData(await this.runWireQuery(query, requestOptions))
1052
+ : resolution.transport === 'grpc'
1053
+ ? normalizeQueryData(await this.runGrpc('Query', {
1054
+ query,
1055
+ entity_types: requestOptions.entityTypes ?? [],
1056
+ capabilities: requestOptions.capabilities ?? [],
1057
+ }, requestOptions))
1058
+ : normalizeQueryData(await this.runHttpPost('/query', {
1059
+ query,
1060
+ entity_types: requestOptions.entityTypes,
1061
+ capabilities: requestOptions.capabilities,
1062
+ }, requestOptions));
1063
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
400
1064
  }
401
- async mutateQuery(query, options = {}) {
402
- return this.query(query, options);
1065
+ async executeSqlExplain(query, options) {
1066
+ ensureNonEmptyString(query, 'query', 'Query must be a non-empty string');
1067
+ const operation = 'sql.explain';
1068
+ const requestOptions = this.withResolvedTimeout(operation, options);
1069
+ const resolution = this.resolveTransport(operation, QUERY_PROFILE, requestOptions.transport);
1070
+ const startedAt = Date.now();
1071
+ const started = performance.now();
1072
+ const data = resolution.transport === 'wire'
1073
+ ? normalizePayloadValue(await this.runWireQuery(`EXPLAIN ${query}`, requestOptions))
1074
+ : resolution.transport === 'grpc'
1075
+ ? normalizePayloadValue(await this.runGrpc('ExplainQuery', {
1076
+ query,
1077
+ entity_types: requestOptions.entityTypes ?? [],
1078
+ capabilities: requestOptions.capabilities ?? [],
1079
+ }, requestOptions))
1080
+ : normalizePayloadValue(await this.runHttpPost('/query/explain', {
1081
+ query,
1082
+ }, requestOptions));
1083
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
403
1084
  }
404
- async batchQuery(queries, options = {}) {
1085
+ async executeSqlBatch(queries, options) {
405
1086
  if (!Array.isArray(queries) || queries.length === 0) {
406
- throw new ValidationError('batchQuery requires a non-empty array of queries', {
1087
+ throw new ValidationError('batch requires a non-empty array of queries', {
407
1088
  field: 'queries',
408
1089
  value: queries,
409
1090
  });
410
1091
  }
411
- const resolution = this.resolveTransport('batchQuery', options.transport);
1092
+ const operation = 'sql.batch';
1093
+ const requestOptions = this.withResolvedTimeout(operation, options);
1094
+ const resolution = this.resolveTransport(operation, BATCH_PROFILE, requestOptions.transport);
412
1095
  const startedAt = Date.now();
413
1096
  const started = performance.now();
414
- const results = await mapWithConcurrency(queries, this.batchConcurrency, async (query) => {
415
- if (resolution.transport === 'wire') {
416
- return this.runWireQuery(query, options);
417
- }
418
- return this.runHttpQuery('/query', {
419
- query,
420
- entity_types: options.entityTypes,
421
- capabilities: options.capabilities,
422
- }, options);
423
- });
424
- return this.buildEnvelope('batchQuery', { results }, { ...resolution, emulated: true }, startedAt, started);
1097
+ let data;
1098
+ if (resolution.transport === 'grpc' && !resolution.emulated) {
1099
+ data = normalizeBatchQueryData(await this.runGrpc('BatchQuery', { queries }, requestOptions));
1100
+ }
1101
+ else {
1102
+ const concurrency = Math.max(1, requestOptions.concurrency ?? this.batchConcurrency);
1103
+ const results = await mapWithConcurrency(queries, concurrency, async (query) => {
1104
+ if (resolution.transport === 'wire') {
1105
+ return normalizeQueryData(await this.runWireQuery(query, requestOptions));
1106
+ }
1107
+ return normalizeQueryData(await this.runHttpPost('/query', {
1108
+ query,
1109
+ entity_types: requestOptions.entityTypes,
1110
+ capabilities: requestOptions.capabilities,
1111
+ }, requestOptions));
1112
+ });
1113
+ data = { results };
1114
+ }
1115
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1116
+ }
1117
+ async executeCollectionsList(options) {
1118
+ const operation = 'collections.list';
1119
+ const requestOptions = this.withResolvedTimeout(operation, options);
1120
+ const resolution = this.resolveTransport(operation, CONTROL_PROFILE, requestOptions.transport);
1121
+ const startedAt = Date.now();
1122
+ const started = performance.now();
1123
+ const data = resolution.transport === 'grpc'
1124
+ ? normalizeCollectionsData(await this.runGrpc('Collections', {}, requestOptions))
1125
+ : normalizeCollectionsData(await this.runHttpGet('/collections', requestOptions));
1126
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1127
+ }
1128
+ async executeCollectionCreate(options) {
1129
+ const name = ensureNonEmptyString(options.name, 'name', 'create requires a collection name');
1130
+ const operation = 'collections.create';
1131
+ const requestOptions = this.withResolvedTimeout(operation, options);
1132
+ const resolution = this.resolveTransport(operation, CONTROL_PROFILE, requestOptions.transport);
1133
+ const startedAt = Date.now();
1134
+ const started = performance.now();
1135
+ const payload = { name };
1136
+ if (requestOptions.ttl !== undefined)
1137
+ payload.ttl = requestOptions.ttl;
1138
+ if (requestOptions.ttlMs !== undefined)
1139
+ payload.ttl_ms = requestOptions.ttlMs;
1140
+ const data = resolution.transport === 'grpc'
1141
+ ? normalizePayloadValue(await this.runGrpc('CreateCollection', {
1142
+ payload_json: stringifyJson(payload, 'collection payload'),
1143
+ }, requestOptions))
1144
+ : normalizePayloadValue(await this.runHttpPost('/collections', payload, requestOptions));
1145
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1146
+ }
1147
+ async executeCollectionDescribe(options) {
1148
+ const name = ensureNonEmptyString(options.name, 'name', 'describe requires a collection name');
1149
+ const operation = 'collections.describe';
1150
+ const requestOptions = this.withResolvedTimeout(operation, options);
1151
+ const resolution = this.resolveTransport(operation, CONTROL_PROFILE, requestOptions.transport);
1152
+ const startedAt = Date.now();
1153
+ const started = performance.now();
1154
+ const data = resolution.transport === 'grpc'
1155
+ ? normalizePayloadValue(await this.runGrpc('DescribeCollection', {
1156
+ collection: name,
1157
+ }, requestOptions))
1158
+ : normalizePayloadValue(await this.runHttpGet(`/collections/${encodeURIComponent(name)}`, requestOptions));
1159
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1160
+ }
1161
+ async executeCollectionDrop(options) {
1162
+ const name = ensureNonEmptyString(options.name, 'name', 'drop requires a collection name');
1163
+ const operation = 'collections.drop';
1164
+ const requestOptions = this.withResolvedTimeout(operation, options);
1165
+ const resolution = this.resolveTransport(operation, CONTROL_PROFILE, requestOptions.transport);
1166
+ const startedAt = Date.now();
1167
+ const started = performance.now();
1168
+ const data = resolution.transport === 'grpc'
1169
+ ? normalizeOperationData(await this.runGrpc('DropCollection', {
1170
+ payload_json: stringifyJson({ name }, 'drop collection payload'),
1171
+ }, requestOptions), {
1172
+ dropped: name,
1173
+ message: `dropped collection ${name}`,
1174
+ })
1175
+ : normalizeOperationData(await this.runHttpDelete(`/collections/${encodeURIComponent(name)}`, requestOptions), {
1176
+ dropped: name,
1177
+ });
1178
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1179
+ }
1180
+ async executeIndexesList(options) {
1181
+ const operation = 'indexes.list';
1182
+ const requestOptions = this.withResolvedTimeout(operation, options);
1183
+ const resolution = this.resolveTransport(operation, CONTROL_PROFILE, requestOptions.transport);
1184
+ const startedAt = Date.now();
1185
+ const started = performance.now();
1186
+ const data = resolution.transport === 'grpc'
1187
+ ? normalizePayloadValue(await this.runGrpc('Indexes', {
1188
+ collection: requestOptions.collection ?? '',
1189
+ }, requestOptions))
1190
+ : normalizePayloadValue(await this.runHttpGet(requestOptions.collection
1191
+ ? `/collections/${encodeURIComponent(requestOptions.collection)}/indexes`
1192
+ : '/indexes', requestOptions));
1193
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
425
1194
  }
426
- async scan(params) {
427
- if (!params.collection || !params.collection.trim()) {
428
- throw new ValidationError('scan requires a collection name', {
429
- field: 'collection',
430
- value: params.collection,
1195
+ async executeIndexesStatuses(options) {
1196
+ const operation = 'indexes.statuses';
1197
+ const requestOptions = this.withResolvedTimeout(operation, options);
1198
+ const resolution = this.resolveTransport(operation, CONTROL_PROFILE, requestOptions.transport);
1199
+ const startedAt = Date.now();
1200
+ const started = performance.now();
1201
+ const data = resolution.transport === 'grpc'
1202
+ ? normalizePayloadValue(await this.runGrpc('IndexStatuses', {}, requestOptions))
1203
+ : normalizePayloadValue(await this.runHttpGet('/catalog/indexes/status', requestOptions));
1204
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1205
+ }
1206
+ async executeIndexCreate(options) {
1207
+ const name = ensureNonEmptyString(options.name, 'name', 'create requires an index name');
1208
+ const collection = ensureNonEmptyString(options.collection, 'collection', 'create requires a collection name');
1209
+ if (!Array.isArray(options.columns) || options.columns.length === 0) {
1210
+ throw new ValidationError('create requires at least one column', {
1211
+ field: 'columns',
1212
+ value: options.columns,
431
1213
  });
432
1214
  }
433
- const resolution = this.resolveTransport('scan', params.transport);
1215
+ const unique = options.unique ? 'UNIQUE ' : '';
1216
+ const method = options.method ? ` USING ${options.method}` : '';
1217
+ const columns = options.columns.map(escapeSqlIdentifier).join(', ');
1218
+ const query = `CREATE ${unique}INDEX IF NOT EXISTS ${escapeSqlIdentifier(name)} ON ${escapeSqlIdentifier(collection)} (${columns})${method}`;
1219
+ return await this.executeSqlQuery('indexes.create', query, options);
1220
+ }
1221
+ async executeIndexToggle(operation, pathAction, enabled, options) {
1222
+ const name = ensureNonEmptyString(options.name, 'name', `${pathAction} requires an index name`);
1223
+ const requestOptions = this.withResolvedTimeout(operation, options);
1224
+ const resolution = this.resolveTransport(operation, CONTROL_PROFILE, requestOptions.transport);
1225
+ const startedAt = Date.now();
1226
+ const started = performance.now();
1227
+ const data = resolution.transport === 'grpc'
1228
+ ? normalizePayloadValue(await this.runGrpc('SetIndexEnabled', { name, enabled }, requestOptions))
1229
+ : normalizePayloadValue(await this.runHttpPost(`/indexes/${encodeURIComponent(name)}/${pathAction}`, {}, requestOptions));
1230
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1231
+ }
1232
+ async executeIndexWarmup(options) {
1233
+ const name = ensureNonEmptyString(options.name, 'name', 'warmup requires an index name');
1234
+ const operation = 'indexes.warmup';
1235
+ const requestOptions = this.withResolvedTimeout(operation, options);
1236
+ const resolution = this.resolveTransport(operation, CONTROL_PROFILE, requestOptions.transport);
1237
+ const startedAt = Date.now();
1238
+ const started = performance.now();
1239
+ const data = resolution.transport === 'grpc'
1240
+ ? normalizePayloadValue(await this.runGrpc('WarmupIndex', { name }, requestOptions))
1241
+ : normalizePayloadValue(await this.runHttpPost(`/indexes/${encodeURIComponent(name)}/warmup`, {}, requestOptions));
1242
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1243
+ }
1244
+ async executeIndexesRebuild(options) {
1245
+ const operation = 'indexes.rebuild';
1246
+ const requestOptions = this.withResolvedTimeout(operation, options);
1247
+ const resolution = this.resolveTransport(operation, CONTROL_PROFILE, requestOptions.transport);
1248
+ const startedAt = Date.now();
1249
+ const started = performance.now();
1250
+ const data = resolution.transport === 'grpc'
1251
+ ? normalizePayloadValue(await this.runGrpc('RebuildIndexes', {
1252
+ collection: requestOptions.collection ?? '',
1253
+ }, requestOptions))
1254
+ : normalizePayloadValue(await this.runHttpPost(requestOptions.collection
1255
+ ? `/collections/${encodeURIComponent(requestOptions.collection)}/indexes/rebuild`
1256
+ : '/indexes/rebuild', {}, requestOptions));
1257
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1258
+ }
1259
+ async executeRowsScan(request) {
1260
+ const collection = ensureNonEmptyString(request.collection, 'collection', 'scan requires a collection name');
1261
+ const operation = 'rows.scan';
1262
+ const requestOptions = this.withResolvedTimeout(operation, request);
1263
+ const resolution = this.resolveTransport(operation, ROW_SCAN_PROFILE, requestOptions.transport);
434
1264
  const startedAt = Date.now();
435
1265
  const started = performance.now();
436
- const query = new URLSearchParams();
437
- query.set('offset', String(Math.max(0, params.offset ?? 0)));
438
- query.set('limit', String(Math.max(1, params.limit ?? 100)));
439
- const data = await this.runHttpGet(`/collections/${encodeURIComponent(params.collection)}/scan?${query.toString()}`, params);
440
- return this.buildEnvelope('scan', data, resolution, startedAt, started);
1266
+ const offset = Math.max(0, requestOptions.offset ?? 0);
1267
+ const limit = Math.max(1, requestOptions.limit ?? 100);
1268
+ const data = resolution.transport === 'grpc'
1269
+ ? normalizeScanData(await this.runGrpc('Scan', { collection, offset, limit }, requestOptions))
1270
+ : normalizeScanData(await this.runHttpGet(`/collections/${encodeURIComponent(collection)}/scan?offset=${offset}&limit=${limit}`, requestOptions));
1271
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
441
1272
  }
442
- async bulkInsertRows(params) {
443
- if (!params.collection || !params.collection.trim()) {
444
- throw new ValidationError('bulkInsertRows requires a collection name', {
445
- field: 'collection',
446
- value: params.collection,
1273
+ async executeRowCreate(request) {
1274
+ const collection = ensureNonEmptyString(request.collection, 'collection', 'create requires a collection name');
1275
+ if (!isPlainObject(request.payload) || !isPlainObject(request.payload.fields)) {
1276
+ throw new ValidationError('rows.create requires payload.fields', {
1277
+ field: 'payload',
1278
+ value: request.payload,
447
1279
  });
448
1280
  }
449
- if (!Array.isArray(params.items) || params.items.length === 0) {
450
- throw new ValidationError('bulkInsertRows requires a non-empty items array', {
1281
+ const operation = 'rows.create';
1282
+ const requestOptions = this.withResolvedTimeout(operation, request);
1283
+ const resolution = this.resolveTransport(operation, ROW_MUTATION_PROFILE, requestOptions.transport);
1284
+ const startedAt = Date.now();
1285
+ const started = performance.now();
1286
+ const data = resolution.transport === 'grpc'
1287
+ ? normalizeEntityData(await this.runGrpc('CreateRow', {
1288
+ collection,
1289
+ payload_json: stringifyJson(requestOptions.payload, 'row payload'),
1290
+ }, requestOptions))
1291
+ : normalizeEntityData(await this.runHttpPost(`/collections/${encodeURIComponent(collection)}/rows`, requestOptions.payload, requestOptions));
1292
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1293
+ }
1294
+ async executeRowsBulkCreate(request) {
1295
+ const collection = ensureNonEmptyString(request.collection, 'collection', 'bulkCreate requires a collection name');
1296
+ if (!Array.isArray(request.items) || request.items.length === 0) {
1297
+ throw new ValidationError('rows.bulkCreate requires a non-empty items array', {
451
1298
  field: 'items',
452
- value: params.items,
1299
+ value: request.items,
453
1300
  });
454
1301
  }
455
- const resolution = this.resolveTransport('bulkInsertRows', params.transport);
1302
+ for (const item of request.items) {
1303
+ if (!isPlainObject(item) || !isPlainObject(item.fields)) {
1304
+ throw new ValidationError('rows.bulkCreate items must contain a fields object', {
1305
+ field: 'items',
1306
+ value: item,
1307
+ });
1308
+ }
1309
+ }
1310
+ const operation = 'rows.bulkCreate';
1311
+ const requestOptions = this.withResolvedTimeout(operation, request);
1312
+ const resolution = this.resolveTransport(operation, ROW_BULK_PROFILE, requestOptions.transport);
456
1313
  const startedAt = Date.now();
457
1314
  const started = performance.now();
458
1315
  let data;
459
1316
  if (resolution.transport === 'wire') {
460
- const payloads = params.items.map((item) => JSON.stringify(item));
461
- const count = await this.runWireBulkInsert(params.collection, payloads, params);
462
- data = { ok: true, count };
1317
+ const count = await this.runWireBulkInsert(collection, requestOptions.items.map((item) => stringifyJson(item, 'row bulk item')), requestOptions);
1318
+ data = {
1319
+ ok: true,
1320
+ count,
1321
+ };
1322
+ }
1323
+ else if (resolution.transport === 'grpc') {
1324
+ data = normalizeBulkEntityData(await this.runGrpc('BulkCreateRows', {
1325
+ collection,
1326
+ payload_json: requestOptions.items.map((item) => stringifyJson(item, 'row bulk item')),
1327
+ }, requestOptions));
463
1328
  }
464
1329
  else {
465
- data = await this.runHttpQuery(`/collections/${encodeURIComponent(params.collection)}/bulk/rows`, { items: params.items }, params);
1330
+ data = normalizeBulkEntityData(await this.runHttpPost(`/collections/${encodeURIComponent(collection)}/bulk/rows`, { items: requestOptions.items }, requestOptions));
466
1331
  }
467
- return this.buildEnvelope('bulkInsertRows', data, resolution, startedAt, started);
1332
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
468
1333
  }
469
- async bulkInsertBinary(params) {
470
- if (!params.collection || !params.collection.trim()) {
471
- throw new ValidationError('bulkInsertBinary requires a collection name', {
472
- field: 'collection',
473
- value: params.collection,
1334
+ async executeRowPatch(request) {
1335
+ const collection = ensureNonEmptyString(request.collection, 'collection', 'patch requires a collection name');
1336
+ const id = toPositiveInteger(request.id, 'id', 0);
1337
+ if (!isPlainObject(request.payload)) {
1338
+ throw new ValidationError('patch requires a payload object', {
1339
+ field: 'payload',
1340
+ value: request.payload,
474
1341
  });
475
1342
  }
476
- if (!Array.isArray(params.fieldNames) || params.fieldNames.length === 0) {
477
- throw new ValidationError('bulkInsertBinary requires fieldNames', {
478
- field: 'fieldNames',
479
- value: params.fieldNames,
1343
+ const operation = 'rows.patch';
1344
+ const requestOptions = this.withResolvedTimeout(operation, request);
1345
+ const resolution = this.resolveTransport(operation, ROW_MUTATION_PROFILE, requestOptions.transport);
1346
+ const startedAt = Date.now();
1347
+ const started = performance.now();
1348
+ const data = resolution.transport === 'grpc'
1349
+ ? normalizeEntityData(await this.runGrpc('PatchEntity', {
1350
+ collection,
1351
+ id,
1352
+ payload_json: stringifyJson(requestOptions.payload, 'patch payload'),
1353
+ }, requestOptions))
1354
+ : normalizeEntityData(await this.runHttpPatch(`/collections/${encodeURIComponent(collection)}/entities/${id}`, requestOptions.payload, requestOptions));
1355
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1356
+ }
1357
+ async executeRowDelete(request) {
1358
+ const collection = ensureNonEmptyString(request.collection, 'collection', 'delete requires a collection name');
1359
+ const id = toPositiveInteger(request.id, 'id', 0);
1360
+ const operation = 'rows.delete';
1361
+ const requestOptions = this.withResolvedTimeout(operation, request);
1362
+ const resolution = this.resolveTransport(operation, ROW_MUTATION_PROFILE, requestOptions.transport);
1363
+ const startedAt = Date.now();
1364
+ const started = performance.now();
1365
+ const data = resolution.transport === 'grpc'
1366
+ ? normalizeOperationData(await this.runGrpc('DeleteEntity', { collection, id }, requestOptions), {
1367
+ deleted: true,
1368
+ id,
1369
+ })
1370
+ : normalizeOperationData(await this.runHttpDelete(`/collections/${encodeURIComponent(collection)}/entities/${id}`, requestOptions), {
1371
+ deleted: true,
1372
+ id,
480
1373
  });
481
- }
482
- if (!Array.isArray(params.rows) || params.rows.length === 0) {
483
- throw new ValidationError('bulkInsertBinary requires rows', {
484
- field: 'rows',
485
- value: params.rows,
1374
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1375
+ }
1376
+ async executeEntityCreate(operation, grpcMethod, httpAction, request) {
1377
+ const collection = ensureNonEmptyString(request.collection, 'collection', 'create requires a collection name');
1378
+ if (!isPlainObject(request.payload)) {
1379
+ throw new ValidationError('create requires a payload object', {
1380
+ field: 'payload',
1381
+ value: request.payload,
486
1382
  });
487
1383
  }
488
- const hasTrueBinaryValues = params.rows.some((row) => row.some((value) => isBufferLike(value)));
489
- if (hasTrueBinaryValues) {
490
- throw new UnsupportedError('bulkInsertBinary byte values require native gRPC support, which is not implemented yet', { feature: 'reddb.bulkInsertBinary.bytes' });
491
- }
492
- const items = params.rows.map((row) => ({
493
- fields: this.binaryRowToFields(params.fieldNames, row),
494
- }));
495
- const envelope = await this.bulkInsertRows({
496
- collection: params.collection,
497
- items,
498
- transport: params.transport,
499
- signal: params.signal,
500
- timeout: params.timeout,
501
- });
502
- return {
503
- ...envelope,
504
- emulated: true,
505
- metrics: {
506
- ...envelope.metrics,
507
- operation: 'bulkInsertBinary',
508
- emulated: true,
509
- },
510
- };
511
- }
512
- async stats(options = {}) {
513
- const resolution = this.resolveTransport('stats', options.transport);
1384
+ const requestOptions = this.withResolvedTimeout(operation, request);
1385
+ const resolution = this.resolveTransport(operation, ENTITY_MUTATION_PROFILE, requestOptions.transport);
514
1386
  const startedAt = Date.now();
515
1387
  const started = performance.now();
516
- const data = await this.runHttpGet('/stats', options);
517
- return this.buildEnvelope('stats', data, resolution, startedAt, started);
1388
+ const data = resolution.transport === 'grpc'
1389
+ ? normalizeEntityData(await this.runGrpc(grpcMethod, {
1390
+ collection,
1391
+ payload_json: stringifyJson(requestOptions.payload, `${operation} payload`),
1392
+ }, requestOptions))
1393
+ : normalizeEntityData(await this.runHttpPost(`/collections/${encodeURIComponent(collection)}/${httpAction}`, requestOptions.payload, requestOptions));
1394
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
518
1395
  }
519
- async ensureCollection(options) {
520
- if (!options.name || !options.name.trim()) {
521
- throw new ValidationError('ensureCollection requires a collection name', {
522
- field: 'name',
523
- value: options.name,
1396
+ async executeBulkEntityCreate(operation, grpcMethod, httpAction, request) {
1397
+ const collection = ensureNonEmptyString(request.collection, 'collection', 'bulkCreate requires a collection name');
1398
+ if (!Array.isArray(request.items) || request.items.length === 0) {
1399
+ throw new ValidationError('bulkCreate requires a non-empty items array', {
1400
+ field: 'items',
1401
+ value: request.items,
524
1402
  });
525
1403
  }
526
- const resolution = this.resolveTransport('ensureCollection', options.transport);
1404
+ for (const item of request.items) {
1405
+ if (!isPlainObject(item)) {
1406
+ throw new ValidationError('bulkCreate items must be objects', {
1407
+ field: 'items',
1408
+ value: item,
1409
+ });
1410
+ }
1411
+ }
1412
+ const requestOptions = this.withResolvedTimeout(operation, request);
1413
+ const resolution = this.resolveTransport(operation, ENTITY_MUTATION_PROFILE, requestOptions.transport);
527
1414
  const startedAt = Date.now();
528
1415
  const started = performance.now();
529
- const payload = { name: options.name };
530
- if (options.ttl !== undefined)
531
- payload.ttl = options.ttl;
532
- if (options.ttlMs !== undefined)
533
- payload.ttl_ms = options.ttlMs;
534
- const data = await this.runHttpQuery('/collections', payload, options);
535
- return this.buildEnvelope('ensureCollection', data, resolution, startedAt, started);
1416
+ const data = resolution.transport === 'grpc'
1417
+ ? normalizeBulkEntityData(await this.runGrpc(grpcMethod, {
1418
+ collection,
1419
+ payload_json: requestOptions.items.map((item) => stringifyJson(item, `${operation} item`)),
1420
+ }, requestOptions))
1421
+ : normalizeBulkEntityData(await this.runHttpPost(`/collections/${encodeURIComponent(collection)}/${httpAction}`, { items: requestOptions.items }, requestOptions));
1422
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
536
1423
  }
537
- async ensureIndex(options) {
538
- if (!options.name || !options.name.trim()) {
539
- throw new ValidationError('ensureIndex requires an index name', {
540
- field: 'name',
541
- value: options.name,
1424
+ async executeVectorBulkInsertBinary(request) {
1425
+ const collection = ensureNonEmptyString(request.collection, 'collection', 'bulkInsertBinary requires a collection name');
1426
+ if (!Array.isArray(request.fieldNames) || request.fieldNames.length === 0) {
1427
+ throw new ValidationError('bulkInsertBinary requires fieldNames', {
1428
+ field: 'fieldNames',
1429
+ value: request.fieldNames,
542
1430
  });
543
1431
  }
544
- if (!options.collection || !options.collection.trim()) {
545
- throw new ValidationError('ensureIndex requires a collection name', {
546
- field: 'collection',
547
- value: options.collection,
1432
+ if (!Array.isArray(request.rows) || request.rows.length === 0) {
1433
+ throw new ValidationError('bulkInsertBinary requires rows', {
1434
+ field: 'rows',
1435
+ value: request.rows,
548
1436
  });
549
1437
  }
550
- if (!Array.isArray(options.columns) || options.columns.length === 0) {
551
- throw new ValidationError('ensureIndex requires at least one column', {
552
- field: 'columns',
553
- value: options.columns,
554
- });
1438
+ const hasBytes = request.rows.some((row) => row.some((value) => isBufferLike(value)));
1439
+ const profile = hasBytes
1440
+ ? {
1441
+ preferences: ['grpc'],
1442
+ supported: { http: false, wire: false },
1443
+ }
1444
+ : {
1445
+ preferences: ['wire', 'grpc', 'http'],
1446
+ emulated: ['http'],
1447
+ };
1448
+ const operation = 'vectors.bulkInsertBinary';
1449
+ const requestOptions = this.withResolvedTimeout(operation, request);
1450
+ const resolution = this.resolveTransport(operation, profile, requestOptions.transport);
1451
+ const startedAt = Date.now();
1452
+ const started = performance.now();
1453
+ let data;
1454
+ if (resolution.transport === 'wire') {
1455
+ const count = await this.runWireBulkInsertBinary(collection, requestOptions.fieldNames, requestOptions.rows, requestOptions);
1456
+ data = {
1457
+ ok: true,
1458
+ count,
1459
+ };
555
1460
  }
556
- const unique = options.unique ? 'UNIQUE ' : '';
557
- const method = options.method ? ` USING ${options.method}` : '';
558
- const columns = options.columns.map(escapeSqlIdentifier).join(', ');
559
- const query = `CREATE ${unique}INDEX IF NOT EXISTS ${escapeSqlIdentifier(options.name)} ON ${escapeSqlIdentifier(options.collection)} (${columns})${method}`;
560
- return this.query(query, options);
1461
+ else if (resolution.transport === 'grpc') {
1462
+ data = normalizeBulkInsertData(await this.runGrpc('BulkInsertBinary', {
1463
+ collection,
1464
+ field_names: requestOptions.fieldNames,
1465
+ rows: requestOptions.rows.map((row) => ({
1466
+ values: row.map((value) => toGrpcBinaryValue(value)),
1467
+ })),
1468
+ }, requestOptions));
1469
+ }
1470
+ else {
1471
+ data = normalizeBulkInsertData(await this.runHttpPost(`/collections/${encodeURIComponent(collection)}/bulk/rows`, {
1472
+ items: requestOptions.rows.map((row) => ({
1473
+ fields: rowToScalarFields(requestOptions.fieldNames, row),
1474
+ })),
1475
+ }, requestOptions));
1476
+ }
1477
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
561
1478
  }
562
- async warmupIndex(options) {
563
- if (!options.name || !options.name.trim()) {
564
- throw new ValidationError('warmupIndex requires an index name', {
565
- field: 'name',
566
- value: options.name,
1479
+ async executeVectorSearch(operation, grpcMethod, httpAction, request) {
1480
+ const collection = ensureNonEmptyString(request.collection, 'collection', `${operation} requires a collection name`);
1481
+ if (!isPlainObject(request.payload)) {
1482
+ throw new ValidationError(`${operation} requires a payload object`, {
1483
+ field: 'payload',
1484
+ value: request.payload,
567
1485
  });
568
1486
  }
569
- const resolution = this.resolveTransport('warmupIndex', options.transport);
1487
+ const requestOptions = this.withResolvedTimeout(operation, request);
1488
+ const resolution = this.resolveTransport(operation, VECTOR_SEARCH_PROFILE, requestOptions.transport);
570
1489
  const startedAt = Date.now();
571
1490
  const started = performance.now();
572
- const data = await this.runHttpQuery(`/indexes/${encodeURIComponent(options.name)}/warmup`, {}, options);
573
- return this.buildEnvelope('warmupIndex', data, resolution, startedAt, started);
1491
+ const data = resolution.transport === 'grpc'
1492
+ ? normalizePayloadValue(await this.runGrpc(grpcMethod, {
1493
+ collection,
1494
+ payload_json: stringifyJson(requestOptions.payload, `${operation} payload`),
1495
+ }, requestOptions))
1496
+ : normalizePayloadValue(await this.runHttpPost(`/collections/${encodeURIComponent(collection)}/${httpAction}`, requestOptions.payload, requestOptions));
1497
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
574
1498
  }
575
- async close() {
576
- if (this.wireClient) {
577
- await this.wireClient.close();
1499
+ async executeKvGet(request) {
1500
+ const collection = ensureNonEmptyString(request.collection, 'collection', 'kv.get requires a collection name');
1501
+ const key = ensureNonEmptyString(request.key, 'key', 'kv.get requires a key');
1502
+ const operation = 'kv.get';
1503
+ const requestOptions = this.withResolvedTimeout(operation, request);
1504
+ const resolution = this.resolveTransport(operation, KV_HTTP_PROFILE, requestOptions.transport);
1505
+ const startedAt = Date.now();
1506
+ const started = performance.now();
1507
+ const data = normalizeOperationData(await this.runHttpGet(`/collections/${encodeURIComponent(collection)}/kvs/${encodeURIComponent(key)}`, requestOptions));
1508
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1509
+ }
1510
+ async executeKvPut(request) {
1511
+ const collection = ensureNonEmptyString(request.collection, 'collection', 'kv.put requires a collection name');
1512
+ const key = ensureNonEmptyString(request.key, 'key', 'kv.put requires a key');
1513
+ const operation = 'kv.put';
1514
+ const requestOptions = this.withResolvedTimeout(operation, request);
1515
+ const resolution = this.resolveTransport(operation, KV_HTTP_PROFILE, requestOptions.transport);
1516
+ const startedAt = Date.now();
1517
+ const started = performance.now();
1518
+ const data = normalizeEntityData(await this.runHttpPut(`/collections/${encodeURIComponent(collection)}/kvs/${encodeURIComponent(key)}`, { value: requestOptions.value }, requestOptions));
1519
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1520
+ }
1521
+ async executeKvDelete(request) {
1522
+ const collection = ensureNonEmptyString(request.collection, 'collection', 'kv.delete requires a collection name');
1523
+ const key = ensureNonEmptyString(request.key, 'key', 'kv.delete requires a key');
1524
+ const operation = 'kv.delete';
1525
+ const requestOptions = this.withResolvedTimeout(operation, request);
1526
+ const resolution = this.resolveTransport(operation, KV_HTTP_PROFILE, requestOptions.transport);
1527
+ const startedAt = Date.now();
1528
+ const started = performance.now();
1529
+ const data = normalizeOperationData(await this.runHttpDelete(`/collections/${encodeURIComponent(collection)}/kvs/${encodeURIComponent(key)}`, requestOptions), {
1530
+ key,
1531
+ deleted: true,
1532
+ });
1533
+ return this.buildEnvelope(operation, data, resolution, startedAt, started);
1534
+ }
1535
+ async executeKvList(request) {
1536
+ const collection = ensureNonEmptyString(request.collection, 'collection', 'kv.list requires a collection name');
1537
+ const limit = request.limit === undefined ? 100 : toPositiveInteger(request.limit, 'limit', 1);
1538
+ const offset = request.offset === undefined ? 0 : toPositiveInteger(request.offset, 'offset', 0);
1539
+ const order = request.order === 'desc' ? 'DESC' : 'ASC';
1540
+ const where = request.prefix
1541
+ ? ` WHERE key LIKE ${escapeSqlLiteral(`${request.prefix}%`)}`
1542
+ : '';
1543
+ const sql = `SELECT id, key, value FROM ${escapeSqlIdentifier(collection)}${where} ORDER BY key ${order} LIMIT ${limit} OFFSET ${offset}`;
1544
+ const queryEnvelope = await this.executeSqlQuery('kv.list', sql, {
1545
+ transport: request.transport,
1546
+ signal: request.signal,
1547
+ timeout: request.timeout,
1548
+ entityTypes: request.entityTypes,
1549
+ capabilities: request.capabilities,
1550
+ });
1551
+ const items = Array.isArray(queryEnvelope.data.result?.records)
1552
+ ? queryEnvelope.data.result.records.map((record) => ({
1553
+ ...record,
1554
+ id: toSafeNumber(record.id),
1555
+ key: typeof record.key === 'string' ? record.key : undefined,
1556
+ value: record.value,
1557
+ }))
1558
+ : [];
1559
+ return this.mapEnvelope(queryEnvelope, 'kv.list', {
1560
+ items,
1561
+ query: queryEnvelope.data,
1562
+ });
1563
+ }
1564
+ resolveOperationTimeout(operation, override) {
1565
+ if (override !== undefined) {
1566
+ return override;
578
1567
  }
1568
+ const bucket = this.operationTimeoutBucket(operation);
1569
+ const bucketTimeout = bucket ? this.operationTimeouts[bucket] : undefined;
1570
+ return bucketTimeout ?? this.timeout;
579
1571
  }
580
- binaryRowToFields(fieldNames, row) {
581
- if (row.length !== fieldNames.length) {
582
- throw new ValidationError(`bulkInsertBinary row length ${row.length} does not match fieldNames length ${fieldNames.length}`, { field: 'rows', value: row });
1572
+ withResolvedTimeout(operation, options) {
1573
+ if (options.timeout !== undefined) {
1574
+ return options;
583
1575
  }
584
- const fields = {};
585
- for (let index = 0; index < fieldNames.length; index++) {
586
- const fieldName = fieldNames[index];
587
- const value = row[index];
588
- if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
589
- fields[fieldName] = value;
590
- continue;
591
- }
592
- if (isBufferLike(value)) {
593
- throw new UnsupportedError('bulkInsertBinary emulation cannot safely serialize byte values without native gRPC support', { feature: 'reddb.bulkInsertBinary.bytes' });
1576
+ return {
1577
+ ...options,
1578
+ timeout: this.resolveOperationTimeout(operation),
1579
+ };
1580
+ }
1581
+ operationTimeoutBucket(operation) {
1582
+ if (operation.startsWith('system.'))
1583
+ return 'system';
1584
+ if (operation === 'sql.batch')
1585
+ return 'sqlBatch';
1586
+ if (operation.startsWith('sql.'))
1587
+ return 'sql';
1588
+ if (operation === 'rows.scan')
1589
+ return 'scan';
1590
+ if (operation.startsWith('collections.'))
1591
+ return 'ddl';
1592
+ if (operation.startsWith('indexes.'))
1593
+ return 'index';
1594
+ if (operation === 'vectors.similar' || operation === 'vectors.ivfSearch')
1595
+ return 'search';
1596
+ if (operation.startsWith('kv.'))
1597
+ return 'kv';
1598
+ if (operation === 'rows.bulkCreate' ||
1599
+ operation.endsWith('.bulkCreate') ||
1600
+ operation === 'vectors.bulkInsertBinary') {
1601
+ return 'bulk';
1602
+ }
1603
+ if (operation.startsWith('rows.') ||
1604
+ operation.endsWith('.create') ||
1605
+ operation.endsWith('.patch') ||
1606
+ operation.endsWith('.delete')) {
1607
+ return 'mutation';
1608
+ }
1609
+ return undefined;
1610
+ }
1611
+ resolveTransport(operation, profile, requestedTransport) {
1612
+ const requested = requestedTransport ?? this.defaultTransport;
1613
+ const available = {
1614
+ http: this.httpClient !== null,
1615
+ grpc: this.grpcClient !== null,
1616
+ wire: this.wireClient !== null,
1617
+ };
1618
+ const supported = {
1619
+ http: profile.supported?.http ?? true,
1620
+ grpc: profile.supported?.grpc ?? true,
1621
+ wire: profile.supported?.wire ?? true,
1622
+ };
1623
+ const canUse = (transport) => available[transport] && supported[transport];
1624
+ const choosePreferred = () => profile.preferences.find((transport) => canUse(transport));
1625
+ if (requested === 'auto') {
1626
+ const chosen = choosePreferred();
1627
+ if (!chosen) {
1628
+ throw new UnsupportedError(`No RedDB transport is configured for ${operation}`, {
1629
+ feature: `reddb.${operation}`,
1630
+ });
594
1631
  }
595
- throw new ValidationError(`Unsupported binary row value for field ${fieldName}`, {
596
- field: fieldName,
597
- value,
1632
+ return {
1633
+ transport: chosen,
1634
+ requestedTransport: requested,
1635
+ degradedFromRequestedTransport: false,
1636
+ emulated: profile.emulated?.includes(chosen) ?? false,
1637
+ };
1638
+ }
1639
+ if (canUse(requested)) {
1640
+ return {
1641
+ transport: requested,
1642
+ requestedTransport: requested,
1643
+ degradedFromRequestedTransport: false,
1644
+ emulated: profile.emulated?.includes(requested) ?? false,
1645
+ };
1646
+ }
1647
+ if (!this.allowTransportFallback) {
1648
+ throw new UnsupportedError(`Transport ${requested} cannot satisfy ${operation}`, {
1649
+ feature: `reddb.${operation}.${requested}`,
1650
+ });
1651
+ }
1652
+ const chosen = choosePreferred();
1653
+ if (!chosen) {
1654
+ throw new UnsupportedError(`No RedDB transport is configured for ${operation}`, {
1655
+ feature: `reddb.${operation}`,
598
1656
  });
599
1657
  }
600
- return fields;
1658
+ return {
1659
+ transport: chosen,
1660
+ requestedTransport: requested,
1661
+ degradedFromRequestedTransport: chosen !== requested,
1662
+ emulated: profile.emulated?.includes(chosen) ?? false,
1663
+ };
601
1664
  }
602
- async runHttpQuery(path, payload, options) {
1665
+ async runHttpPost(path, payload, options) {
603
1666
  if (!this.httpClient) {
604
1667
  throw new UnsupportedError('HTTP transport is not configured for this RedDB client', {
605
1668
  feature: 'reddb.http',
@@ -613,6 +1676,47 @@ export class RedDbClient {
613
1676
  });
614
1677
  return this.parseHttpResponse(response, path);
615
1678
  }
1679
+ async runHttpPut(path, payload, options) {
1680
+ if (!this.httpClient) {
1681
+ throw new UnsupportedError('HTTP transport is not configured for this RedDB client', {
1682
+ feature: 'reddb.http',
1683
+ });
1684
+ }
1685
+ const response = await this.httpClient.put(path, {
1686
+ json: payload,
1687
+ signal: options.signal,
1688
+ timeout: options.timeout ?? this.timeout,
1689
+ throwHttpErrors: false,
1690
+ });
1691
+ return this.parseHttpResponse(response, path);
1692
+ }
1693
+ async runHttpPatch(path, payload, options) {
1694
+ if (!this.httpClient) {
1695
+ throw new UnsupportedError('HTTP transport is not configured for this RedDB client', {
1696
+ feature: 'reddb.http',
1697
+ });
1698
+ }
1699
+ const response = await this.httpClient.patch(path, {
1700
+ json: payload,
1701
+ signal: options.signal,
1702
+ timeout: options.timeout ?? this.timeout,
1703
+ throwHttpErrors: false,
1704
+ });
1705
+ return this.parseHttpResponse(response, path);
1706
+ }
1707
+ async runHttpDelete(path, options) {
1708
+ if (!this.httpClient) {
1709
+ throw new UnsupportedError('HTTP transport is not configured for this RedDB client', {
1710
+ feature: 'reddb.http',
1711
+ });
1712
+ }
1713
+ const response = await this.httpClient.delete(path, {
1714
+ signal: options.signal,
1715
+ timeout: options.timeout ?? this.timeout,
1716
+ throwHttpErrors: false,
1717
+ });
1718
+ return this.parseHttpResponse(response, path);
1719
+ }
616
1720
  async runHttpGet(path, options) {
617
1721
  if (!this.httpClient) {
618
1722
  throw new UnsupportedError('HTTP transport is not configured for this RedDB client', {
@@ -656,6 +1760,14 @@ export class RedDbClient {
656
1760
  throw new ParseError(`Failed to parse RedDB HTTP response for ${path}: ${error instanceof Error ? error.message : String(error)}`, { format: 'json' });
657
1761
  }
658
1762
  }
1763
+ async runGrpc(methodName, request, options) {
1764
+ if (!this.grpcClient) {
1765
+ throw new UnsupportedError('gRPC transport is not configured for this RedDB client', {
1766
+ feature: 'reddb.grpc',
1767
+ });
1768
+ }
1769
+ return await this.grpcClient.unary(methodName, request, options);
1770
+ }
659
1771
  async runWireQuery(query, options) {
660
1772
  if (options.signal) {
661
1773
  throw new UnsupportedError('AbortSignal is not supported for RedDB wire operations', {
@@ -682,121 +1794,18 @@ export class RedDbClient {
682
1794
  }
683
1795
  return await this.wireClient.bulkInsert(collection, payloads, options.timeout ?? this.timeout);
684
1796
  }
685
- resolveTransport(operation, requestedTransport) {
686
- const requested = requestedTransport ?? this.defaultTransport;
687
- const hasHttp = this.httpClient !== null;
688
- const hasWire = this.wireClient !== null;
689
- const requireHttp = () => {
690
- if (hasHttp) {
691
- return {
692
- transport: 'http',
693
- requestedTransport: requested,
694
- degradedFromRequestedTransport: requested !== 'auto' && requested !== 'http',
695
- emulated: false,
696
- };
697
- }
698
- throw new UnsupportedError(`Operation ${operation} requires the RedDB HTTP API`, {
699
- feature: `reddb.${operation}.http`,
700
- });
701
- };
702
- const preferWireThenHttp = () => {
703
- if (hasWire) {
704
- return {
705
- transport: 'wire',
706
- requestedTransport: requested,
707
- degradedFromRequestedTransport: requested === 'grpc' || requested === 'http',
708
- emulated: false,
709
- };
710
- }
711
- if (hasHttp) {
712
- return {
713
- transport: 'http',
714
- requestedTransport: requested,
715
- degradedFromRequestedTransport: requested === 'grpc' || requested === 'wire',
716
- emulated: false,
717
- };
718
- }
719
- throw new UnsupportedError(`No RedDB transport is configured for ${operation}`, {
720
- feature: `reddb.${operation}`,
721
- });
722
- };
723
- const preferHttpThenWire = () => {
724
- if (hasHttp) {
725
- return {
726
- transport: 'http',
727
- requestedTransport: requested,
728
- degradedFromRequestedTransport: requested === 'grpc' || requested === 'wire',
729
- emulated: false,
730
- };
731
- }
732
- if (hasWire) {
733
- return {
734
- transport: 'wire',
735
- requestedTransport: requested,
736
- degradedFromRequestedTransport: requested === 'grpc' || requested === 'http',
737
- emulated: false,
738
- };
739
- }
740
- throw new UnsupportedError(`No RedDB transport is configured for ${operation}`, {
741
- feature: `reddb.${operation}`,
742
- });
743
- };
744
- const requiresHttp = new Set(['scan', 'stats', 'ensureCollection', 'warmupIndex']);
745
- const prefersWire = new Set(['query', 'mutateQuery', 'explainQuery', 'batchQuery', 'bulkInsertRows', 'bulkInsertBinary', 'ensureIndex']);
746
- if (requested === 'auto') {
747
- return prefersWire.has(operation) ? preferWireThenHttp() : requireHttp();
748
- }
749
- if (requested === 'http') {
750
- if (requiresHttp.has(operation)) {
751
- return requireHttp();
752
- }
753
- if (hasHttp) {
754
- return {
755
- transport: 'http',
756
- requestedTransport: requested,
757
- degradedFromRequestedTransport: false,
758
- emulated: false,
759
- };
760
- }
761
- if (this.allowTransportFallback && hasWire) {
762
- return {
763
- transport: 'wire',
764
- requestedTransport: requested,
765
- degradedFromRequestedTransport: true,
766
- emulated: false,
767
- };
768
- }
769
- throw new UnsupportedError(`HTTP transport is unavailable for ${operation}`, {
770
- feature: `reddb.${operation}.http`,
771
- });
772
- }
773
- if (requested === 'wire') {
774
- if (hasWire && !requiresHttp.has(operation)) {
775
- return {
776
- transport: 'wire',
777
- requestedTransport: requested,
778
- degradedFromRequestedTransport: false,
779
- emulated: false,
780
- };
781
- }
782
- if (this.allowTransportFallback && hasHttp) {
783
- return {
784
- transport: 'http',
785
- requestedTransport: requested,
786
- degradedFromRequestedTransport: true,
787
- emulated: false,
788
- };
789
- }
790
- throw new UnsupportedError(`Wire transport cannot satisfy ${operation}`, {
791
- feature: `reddb.${operation}.wire`,
1797
+ async runWireBulkInsertBinary(collection, fieldNames, rows, options) {
1798
+ if (options.signal) {
1799
+ throw new UnsupportedError('AbortSignal is not supported for RedDB wire operations', {
1800
+ feature: 'reddb.wire.signal',
792
1801
  });
793
1802
  }
794
- if (!this.allowTransportFallback) {
795
- throw new UnsupportedError(`gRPC transport is not implemented for ${operation}`, {
796
- feature: `reddb.${operation}.grpc`,
1803
+ if (!this.wireClient) {
1804
+ throw new UnsupportedError('Wire transport is not configured for this RedDB client', {
1805
+ feature: 'reddb.wire',
797
1806
  });
798
1807
  }
799
- return requiresHttp.has(operation) ? preferHttpThenWire() : preferWireThenHttp();
1808
+ return await this.wireClient.bulkInsertBinary(collection, fieldNames, rows, options.timeout ?? this.timeout);
800
1809
  }
801
1810
  buildEnvelope(operation, data, resolution, startedAt, started) {
802
1811
  const queryStats = buildQueryStats(data);
@@ -818,6 +1827,83 @@ export class RedDbClient {
818
1827
  },
819
1828
  };
820
1829
  }
1830
+ mapEnvelope(envelope, operation, data) {
1831
+ return {
1832
+ ...envelope,
1833
+ data,
1834
+ metrics: {
1835
+ ...envelope.metrics,
1836
+ operation,
1837
+ },
1838
+ };
1839
+ }
1840
+ }
1841
+ function toGrpcBinaryValue(value) {
1842
+ if (value === null) {
1843
+ return {};
1844
+ }
1845
+ if (typeof value === 'string') {
1846
+ return { text_value: value };
1847
+ }
1848
+ if (typeof value === 'boolean') {
1849
+ return { bool_value: value };
1850
+ }
1851
+ if (typeof value === 'number') {
1852
+ if (!Number.isFinite(value)) {
1853
+ throw new ValidationError('bulkInsertBinary number values must be finite', {
1854
+ field: 'rows',
1855
+ value,
1856
+ });
1857
+ }
1858
+ return Number.isInteger(value)
1859
+ ? { int_value: value }
1860
+ : { float_value: value };
1861
+ }
1862
+ if (typeof value === 'bigint') {
1863
+ return { int_value: value.toString() };
1864
+ }
1865
+ if (isBufferLike(value)) {
1866
+ return { blob_value: Buffer.from(value) };
1867
+ }
1868
+ throw new ValidationError('Unsupported bulkInsertBinary value type', {
1869
+ field: 'rows',
1870
+ value,
1871
+ });
1872
+ }
1873
+ function rowToScalarFields(fieldNames, row) {
1874
+ if (row.length !== fieldNames.length) {
1875
+ throw new ValidationError(`bulkInsertBinary row length ${row.length} does not match fieldNames length ${fieldNames.length}`, { field: 'rows', value: row });
1876
+ }
1877
+ const fields = {};
1878
+ for (let index = 0; index < fieldNames.length; index++) {
1879
+ const fieldName = fieldNames[index];
1880
+ const value = row[index];
1881
+ if (value === null ||
1882
+ typeof value === 'string' ||
1883
+ typeof value === 'number' ||
1884
+ typeof value === 'boolean' ||
1885
+ typeof value === 'bigint') {
1886
+ if (typeof value === 'bigint') {
1887
+ const numeric = Number(value);
1888
+ if (!Number.isSafeInteger(numeric)) {
1889
+ throw new UnsupportedError('bulkInsertBinary bigint values outside the safe integer range require gRPC transport', { feature: 'reddb.http.bulkInsertBinary.bigint' });
1890
+ }
1891
+ fields[fieldName] = numeric;
1892
+ }
1893
+ else {
1894
+ fields[fieldName] = value;
1895
+ }
1896
+ continue;
1897
+ }
1898
+ if (isBufferLike(value)) {
1899
+ throw new UnsupportedError('bulkInsertBinary byte values require gRPC transport; HTTP emulation would be lossy', { feature: 'reddb.http.bulkInsertBinary.bytes' });
1900
+ }
1901
+ throw new ValidationError(`Unsupported bulkInsertBinary value for field ${fieldName}`, {
1902
+ field: fieldName,
1903
+ value,
1904
+ });
1905
+ }
1906
+ return fields;
821
1907
  }
822
1908
  export function createRedDbClient(options = {}) {
823
1909
  return new RedDbClient(options);