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.
- package/dist/clients/index.d.ts +1 -1
- package/dist/clients/index.js +1 -1
- package/dist/clients/reddb-proto.d.ts +1 -0
- package/dist/clients/reddb-proto.js +195 -0
- package/dist/clients/reddb.d.ts +306 -71
- package/dist/clients/reddb.js +1410 -324
- package/dist/protocols/index.d.ts +1 -1
- package/dist/protocols/index.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
package/dist/clients/reddb.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
336
|
-
? new
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
368
|
-
if (
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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 === '
|
|
378
|
-
? await this.
|
|
379
|
-
: await this.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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(
|
|
398
|
-
:
|
|
399
|
-
|
|
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
|
|
402
|
-
|
|
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
|
|
1085
|
+
async executeSqlBatch(queries, options) {
|
|
405
1086
|
if (!Array.isArray(queries) || queries.length === 0) {
|
|
406
|
-
throw new ValidationError('
|
|
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
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
|
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
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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:
|
|
1299
|
+
value: request.items,
|
|
453
1300
|
});
|
|
454
1301
|
}
|
|
455
|
-
const
|
|
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
|
|
461
|
-
|
|
462
|
-
|
|
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.
|
|
1330
|
+
data = normalizeBulkEntityData(await this.runHttpPost(`/collections/${encodeURIComponent(collection)}/bulk/rows`, { items: requestOptions.items }, requestOptions));
|
|
466
1331
|
}
|
|
467
|
-
return this.buildEnvelope(
|
|
1332
|
+
return this.buildEnvelope(operation, data, resolution, startedAt, started);
|
|
468
1333
|
}
|
|
469
|
-
async
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
|
489
|
-
|
|
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 =
|
|
517
|
-
|
|
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
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
|
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
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
return this.buildEnvelope(
|
|
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
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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 (!
|
|
545
|
-
throw new ValidationError('
|
|
546
|
-
field: '
|
|
547
|
-
value:
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
|
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 =
|
|
573
|
-
|
|
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
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
581
|
-
if (
|
|
582
|
-
|
|
1572
|
+
withResolvedTimeout(operation, options) {
|
|
1573
|
+
if (options.timeout !== undefined) {
|
|
1574
|
+
return options;
|
|
583
1575
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
|
1658
|
+
return {
|
|
1659
|
+
transport: chosen,
|
|
1660
|
+
requestedTransport: requested,
|
|
1661
|
+
degradedFromRequestedTransport: chosen !== requested,
|
|
1662
|
+
emulated: profile.emulated?.includes(chosen) ?? false,
|
|
1663
|
+
};
|
|
601
1664
|
}
|
|
602
|
-
async
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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.
|
|
795
|
-
throw new UnsupportedError(
|
|
796
|
-
feature:
|
|
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
|
|
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);
|