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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "spice-couchbase",
3
- "version": "2.0.44",
3
+ "version": "2.0.46",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
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
- let response = await (
272
- useLookupIn ?
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
- // lookupIn returns content array with each spec result
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
- let functions = _.map(keys, (key) =>
355
- useLookupIn ?
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
- let responses = _.flatten(
367
- _.map(returned_all, (returned_obj) => {
368
- return returned_obj?.value;
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
- let final_responses = _.compact(
374
- _.map(responses, (res, index) => {
375
- if (res) {
376
- let value;
377
- if (useLookupIn) {
378
- // lookupIn returns content array with each spec result
379
- value = {};
380
- columns.forEach((col, colIndex) => {
381
- if (res.content && res.content[colIndex]) {
382
- value[col] = res.content[colIndex].value;
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
- if (!keep_type && value) {
391
- delete value.type;
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 options = {
620
- adhoc: true,
621
- };
622
- let advise_query = `ADVISE ${query}`;
623
- let response = await exec_scope.query(advise_query, options);
624
-
625
- let best_index = _.last(
626
- _.first(response?.rows || [])?.advice?.adviseinfo?.recommended_indexes
627
- ?.indexes || []
628
- )?.index_statement;
629
- if (best_index) {
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
- } catch (e) {}
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") {