spice-couchbase 2.0.44 → 2.0.46
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/build/index.js +189 -68
- package/build/utility.js +352 -0
- package/build/utility.test.js +784 -0
- package/package.json +2 -2
- package/src/index.js +173 -79
- package/src/utility.js +368 -0
- package/src/utility.test.js +986 -0
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spice-couchbase",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.46",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "node src/utility.test.js"
|
|
8
8
|
},
|
|
9
9
|
"author": "Chad Fraser <chad@sonover.com> (http://sonover.com)",
|
|
10
10
|
"license": "ISC",
|
package/src/index.js
CHANGED
|
@@ -19,11 +19,14 @@ if (!Promise.allSettled) {
|
|
|
19
19
|
.catch((reason) => ({
|
|
20
20
|
status: "rejected",
|
|
21
21
|
reason,
|
|
22
|
-
}))
|
|
23
|
-
)
|
|
22
|
+
})),
|
|
23
|
+
),
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Couchbase lookupIn has a limit of 16 operations per call
|
|
28
|
+
const LOOKUP_IN_MAX_SPECS = 16;
|
|
29
|
+
|
|
27
30
|
module.exports = class Database {
|
|
28
31
|
constructor(
|
|
29
32
|
connection,
|
|
@@ -33,7 +36,7 @@ module.exports = class Database {
|
|
|
33
36
|
scope,
|
|
34
37
|
query_scope = "cluster",
|
|
35
38
|
auto_create_collection,
|
|
36
|
-
} = {}
|
|
39
|
+
} = {},
|
|
37
40
|
) {
|
|
38
41
|
if (connection) {
|
|
39
42
|
this.connection_name = connection;
|
|
@@ -148,7 +151,7 @@ module.exports = class Database {
|
|
|
148
151
|
{ name: "scope_admin", bucket: this.db_config.bucket },
|
|
149
152
|
{ name: "data_reader", bucket: this.db_config.bucket },
|
|
150
153
|
],
|
|
151
|
-
}
|
|
154
|
+
},
|
|
152
155
|
);
|
|
153
156
|
} catch (e) {
|
|
154
157
|
console.log(e);
|
|
@@ -159,7 +162,7 @@ module.exports = class Database {
|
|
|
159
162
|
return (
|
|
160
163
|
(this.db_config.api_base ||
|
|
161
164
|
`http://${_.last(
|
|
162
|
-
_.first(this.db_config.nodes.split(",")).split("://")
|
|
165
|
+
_.first(this.db_config.nodes.split(",")).split("://"),
|
|
163
166
|
)}`) +
|
|
164
167
|
":" +
|
|
165
168
|
default_port
|
|
@@ -176,7 +179,7 @@ module.exports = class Database {
|
|
|
176
179
|
if (!this.checkGlobelConnectionObject(this.collection_name)) {
|
|
177
180
|
let collection_exists = await this.collectionExists(
|
|
178
181
|
this.collection_name,
|
|
179
|
-
this.scope_name
|
|
182
|
+
this.scope_name,
|
|
180
183
|
);
|
|
181
184
|
if (!collection_exists) {
|
|
182
185
|
await this.createCollection(this.collection_name, this.scope_name);
|
|
@@ -202,7 +205,7 @@ module.exports = class Database {
|
|
|
202
205
|
username: that.db_config.username,
|
|
203
206
|
password: that.db_config.password,
|
|
204
207
|
},
|
|
205
|
-
}
|
|
208
|
+
},
|
|
206
209
|
);
|
|
207
210
|
let scope = _.find(all_scopes.data.scopes, { name: scope_name });
|
|
208
211
|
let collectionSet = new Set();
|
|
@@ -245,7 +248,7 @@ module.exports = class Database {
|
|
|
245
248
|
async createPrimaryIndex(scope_name, col_name) {
|
|
246
249
|
try {
|
|
247
250
|
let response = await this.query(
|
|
248
|
-
`CREATE PRIMARY INDEX ON default:\`${this.db_config.bucket}\`.\`${scope_name}\`.\`${col_name}
|
|
251
|
+
`CREATE PRIMARY INDEX ON default:\`${this.db_config.bucket}\`.\`${scope_name}\`.\`${col_name}\``,
|
|
249
252
|
);
|
|
250
253
|
return response;
|
|
251
254
|
} catch (e) {
|
|
@@ -268,27 +271,39 @@ module.exports = class Database {
|
|
|
268
271
|
async get(doc_id, { columns = [] } = {}) {
|
|
269
272
|
try {
|
|
270
273
|
const useLookupIn = columns.length > 0;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
(this.use_bucket_name ? this.connection : this.collection).lookupIn(
|
|
274
|
-
doc_id,
|
|
275
|
-
columns.map((col) => couchbase.LookupInSpec.get(col))
|
|
276
|
-
)
|
|
277
|
-
: (this.use_bucket_name ? this.connection : this.collection).get(doc_id)
|
|
278
|
-
);
|
|
274
|
+
const collection =
|
|
275
|
+
this.use_bucket_name ? this.connection : this.collection;
|
|
279
276
|
|
|
280
|
-
if (useLookupIn) {
|
|
281
|
-
|
|
282
|
-
let value = { id: doc_id };
|
|
283
|
-
columns.forEach((col, colIndex) => {
|
|
284
|
-
if (response.content && response.content[colIndex]) {
|
|
285
|
-
value[col] = response.content[colIndex].value;
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
return value;
|
|
289
|
-
} else {
|
|
277
|
+
if (!useLookupIn) {
|
|
278
|
+
let response = await collection.get(doc_id);
|
|
290
279
|
return { id: doc_id, ...response.content };
|
|
291
280
|
}
|
|
281
|
+
|
|
282
|
+
// Batch columns into chunks of LOOKUP_IN_MAX_SPECS (16)
|
|
283
|
+
let value = { id: doc_id };
|
|
284
|
+
const columnBatches = _.chunk(columns, LOOKUP_IN_MAX_SPECS);
|
|
285
|
+
|
|
286
|
+
// Execute all batch requests in parallel
|
|
287
|
+
const batchPromises = columnBatches.map((batch) => {
|
|
288
|
+
const specs = batch.map((col) => couchbase.LookupInSpec.get(col));
|
|
289
|
+
return collection.lookupIn(doc_id, specs);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const batchResults = await Promise.allSettled(batchPromises);
|
|
293
|
+
|
|
294
|
+
// Merge results from all batches
|
|
295
|
+
batchResults.forEach((result, batchIndex) => {
|
|
296
|
+
if (result.status === "fulfilled" && result.value) {
|
|
297
|
+
const batch = columnBatches[batchIndex];
|
|
298
|
+
batch.forEach((col, colIndex) => {
|
|
299
|
+
if (result.value.content && result.value.content[colIndex]) {
|
|
300
|
+
value[col] = result.value.content[colIndex].value;
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return value;
|
|
292
307
|
} catch (e) {
|
|
293
308
|
console.error(e);
|
|
294
309
|
}
|
|
@@ -301,7 +316,7 @@ module.exports = class Database {
|
|
|
301
316
|
document,
|
|
302
317
|
{
|
|
303
318
|
expiry: expiry,
|
|
304
|
-
}
|
|
319
|
+
},
|
|
305
320
|
);
|
|
306
321
|
} catch (e) {
|
|
307
322
|
console.log(e.message, e, doc_id);
|
|
@@ -312,7 +327,7 @@ module.exports = class Database {
|
|
|
312
327
|
async touch(doc_id, expiry = 0) {
|
|
313
328
|
await (this.use_bucket_name ? this.connection : this.collection).touch(
|
|
314
329
|
doc_id,
|
|
315
|
-
expiry
|
|
330
|
+
expiry,
|
|
316
331
|
);
|
|
317
332
|
return await this.get(doc_id);
|
|
318
333
|
}
|
|
@@ -351,51 +366,83 @@ module.exports = class Database {
|
|
|
351
366
|
}
|
|
352
367
|
if (_.isArray(keys) && keys.length > 0) {
|
|
353
368
|
const useLookupIn = columns.length > 0;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
(this.use_bucket_name ? this.connection : this.collection).lookupIn(
|
|
357
|
-
key,
|
|
358
|
-
columns.map((col) => couchbase.LookupInSpec.get(col))
|
|
359
|
-
)
|
|
360
|
-
: (this.use_bucket_name ? this.connection : this.collection).get(key)
|
|
361
|
-
);
|
|
362
|
-
let returned_all = await Promise.allSettled(functions);
|
|
369
|
+
const collection =
|
|
370
|
+
this.use_bucket_name ? this.connection : this.collection;
|
|
363
371
|
|
|
364
|
-
|
|
372
|
+
if (!useLookupIn) {
|
|
373
|
+
// Regular get - no batching needed
|
|
374
|
+
let functions = _.map(keys, (key) => collection.get(key));
|
|
375
|
+
let returned_all = await Promise.allSettled(functions);
|
|
365
376
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
})
|
|
370
|
-
);
|
|
377
|
+
let responses = _.flatten(
|
|
378
|
+
_.map(returned_all, (returned_obj) => returned_obj?.value),
|
|
379
|
+
);
|
|
371
380
|
|
|
381
|
+
let final_responses = _.compact(
|
|
382
|
+
_.map(responses, (res, index) => {
|
|
383
|
+
if (res) {
|
|
384
|
+
let value = res.value;
|
|
385
|
+
if (!keep_type && value) {
|
|
386
|
+
delete value.type;
|
|
387
|
+
}
|
|
388
|
+
if (value) {
|
|
389
|
+
value.id = keys[index];
|
|
390
|
+
}
|
|
391
|
+
return value;
|
|
392
|
+
}
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
372
395
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
396
|
+
return final_responses;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// lookupIn with batching for columns > 16
|
|
400
|
+
const columnBatches = _.chunk(columns, LOOKUP_IN_MAX_SPECS);
|
|
401
|
+
|
|
402
|
+
// For each key, we need to fetch all column batches and merge the results
|
|
403
|
+
const keyPromises = _.map(keys, async (key) => {
|
|
404
|
+
try {
|
|
405
|
+
let value = { id: key };
|
|
406
|
+
|
|
407
|
+
// Execute all batch requests for this key in parallel
|
|
408
|
+
const batchPromises = columnBatches.map((batch) => {
|
|
409
|
+
const specs = batch.map((col) => couchbase.LookupInSpec.get(col));
|
|
410
|
+
return collection.lookupIn(key, specs);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const batchResults = await Promise.allSettled(batchPromises);
|
|
414
|
+
|
|
415
|
+
// Merge results from all batches
|
|
416
|
+
batchResults.forEach((result, batchIndex) => {
|
|
417
|
+
if (result.status === "fulfilled" && result.value) {
|
|
418
|
+
const batch = columnBatches[batchIndex];
|
|
419
|
+
batch.forEach((col, colIndex) => {
|
|
420
|
+
if (result.value.content && result.value.content[colIndex]) {
|
|
421
|
+
value[col] = result.value.content[colIndex].value;
|
|
383
422
|
}
|
|
384
423
|
});
|
|
385
|
-
} else {
|
|
386
|
-
// regular get returns the full document in value
|
|
387
|
-
value = res.value;
|
|
388
424
|
}
|
|
425
|
+
});
|
|
389
426
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
if (value) {
|
|
394
|
-
value.id = keys[index];
|
|
395
|
-
}
|
|
396
|
-
return value;
|
|
427
|
+
if (!keep_type && value) {
|
|
428
|
+
delete value.type;
|
|
397
429
|
}
|
|
398
|
-
|
|
430
|
+
|
|
431
|
+
return value;
|
|
432
|
+
} catch (e) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
let results = await Promise.allSettled(keyPromises);
|
|
438
|
+
let final_responses = _.compact(
|
|
439
|
+
_.map(results, (result) => {
|
|
440
|
+
if (result.status === "fulfilled" && result.value) {
|
|
441
|
+
// Only return if we got at least some data beyond just the id
|
|
442
|
+
return Object.keys(result.value).length > 1 ? result.value : null;
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}),
|
|
399
446
|
);
|
|
400
447
|
|
|
401
448
|
return final_responses;
|
|
@@ -463,7 +510,7 @@ module.exports = class Database {
|
|
|
463
510
|
order_by,
|
|
464
511
|
do_count = "false",
|
|
465
512
|
query_scope,
|
|
466
|
-
join = ""
|
|
513
|
+
join = "",
|
|
467
514
|
) {
|
|
468
515
|
const is_legacy =
|
|
469
516
|
this.scope_name !== "_default" && this.collection_name !== "_default";
|
|
@@ -558,7 +605,7 @@ module.exports = class Database {
|
|
|
558
605
|
let full_query = search_query + " AND type:" + type;
|
|
559
606
|
var query = that.SearchQuery.new(
|
|
560
607
|
that.full_text_index || full_text_index,
|
|
561
|
-
that.SearchQuery.queryString(full_query)
|
|
608
|
+
that.SearchQuery.queryString(full_query),
|
|
562
609
|
);
|
|
563
610
|
|
|
564
611
|
if (!limit) {
|
|
@@ -614,22 +661,69 @@ module.exports = class Database {
|
|
|
614
661
|
return response.rows;
|
|
615
662
|
}
|
|
616
663
|
|
|
664
|
+
/**
|
|
665
|
+
* Recursively checks if the query plan contains a Fetch operator
|
|
666
|
+
* @param {Object} plan - The EXPLAIN query plan
|
|
667
|
+
* @returns {boolean} - True if Fetch operator is found
|
|
668
|
+
*/
|
|
669
|
+
hasFetchOperator(plan) {
|
|
670
|
+
if (!plan || typeof plan !== "object") {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Check if this node is a Fetch operator
|
|
675
|
+
if (plan["#operator"] === "Fetch") {
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Recursively check all child properties
|
|
680
|
+
for (const key of Object.keys(plan)) {
|
|
681
|
+
const value = plan[key];
|
|
682
|
+
if (Array.isArray(value)) {
|
|
683
|
+
for (const item of value) {
|
|
684
|
+
if (this.hasFetchOperator(item)) {
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
} else if (typeof value === "object" && value !== null) {
|
|
689
|
+
if (this.hasFetchOperator(value)) {
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
|
|
617
698
|
async optimize(query, exec_scope) {
|
|
618
699
|
try {
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
await exec_scope.query(best_index);
|
|
700
|
+
const { buildCoveringIndex } = require("./utility");
|
|
701
|
+
|
|
702
|
+
// 1. Run EXPLAIN to get the query plan
|
|
703
|
+
const explainQuery = `EXPLAIN ${query}`;
|
|
704
|
+
const explainResponse = await exec_scope.query(explainQuery);
|
|
705
|
+
|
|
706
|
+
// 2. Check if query plan contains Fetch operator (not covered)
|
|
707
|
+
const plan = explainResponse?.rows?.[0];
|
|
708
|
+
if (!this.hasFetchOperator(plan)) {
|
|
709
|
+
// Query is already covered, no need to create index
|
|
710
|
+
return;
|
|
631
711
|
}
|
|
632
|
-
|
|
712
|
+
|
|
713
|
+
// 3. Build covering index from the query
|
|
714
|
+
const indexStatement = buildCoveringIndex(query, {
|
|
715
|
+
bucket: this.bucket,
|
|
716
|
+
scope: this.scope_name,
|
|
717
|
+
collection: this.collection_name,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// 4. Execute the index creation if we got a valid statement
|
|
721
|
+
if (indexStatement) {
|
|
722
|
+
const response = await exec_scope.query(indexStatement);
|
|
723
|
+
}
|
|
724
|
+
} catch (e) {
|
|
725
|
+
// Silently handle errors (index may already exist, etc.)
|
|
726
|
+
}
|
|
633
727
|
}
|
|
634
728
|
|
|
635
729
|
async query(query, query_scope = "cluster") {
|