spice-couchbase 2.0.53 → 2.0.55

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 CHANGED
@@ -34,6 +34,56 @@ if (!Promise.allSettled) {
34
34
 
35
35
 
36
36
  var LOOKUP_IN_MAX_SPECS = 16;
37
+
38
+ function normalizeScanConsistency(value) {
39
+ if (value === true || value === QueryScanConsistency.RequestPlus || typeof value === "string" && ["true", "request_plus"].includes(value.toLowerCase())) {
40
+ return QueryScanConsistency.RequestPlus;
41
+ }
42
+
43
+ if (value === undefined || value === null || value === "" || value === false || value === QueryScanConsistency.NotBounded || typeof value === "string" && ["false", "not_bounded"].includes(value.toLowerCase())) {
44
+ return QueryScanConsistency.NotBounded;
45
+ }
46
+
47
+ throw new Error("Unsupported query scan consistency: ".concat(value));
48
+ }
49
+
50
+ function buildQueryOptions() {
51
+ var _queryOptions$consist, _queryOptions$scanWai;
52
+
53
+ var queryOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
54
+
55
+ if (typeof queryOptions !== "object" || Array.isArray(queryOptions)) {
56
+ queryOptions = {
57
+ scan_consistency: queryOptions
58
+ };
59
+ }
60
+
61
+ var options = {};
62
+ var consistentWith = (_queryOptions$consist = queryOptions.consistentWith) !== null && _queryOptions$consist !== void 0 ? _queryOptions$consist : queryOptions.consistent_with;
63
+
64
+ if (consistentWith !== undefined && consistentWith !== null) {
65
+ options.consistentWith = consistentWith;
66
+ } else {
67
+ var _queryOptions$scanCon;
68
+
69
+ options.scanConsistency = normalizeScanConsistency((_queryOptions$scanCon = queryOptions.scanConsistency) !== null && _queryOptions$scanCon !== void 0 ? _queryOptions$scanCon : queryOptions.scan_consistency);
70
+ }
71
+
72
+ var scanWait = (_queryOptions$scanWai = queryOptions.scanWait) !== null && _queryOptions$scanWai !== void 0 ? _queryOptions$scanWai : queryOptions.scan_wait;
73
+
74
+ if (scanWait !== undefined && scanWait !== null && scanWait !== "") {
75
+ var numericScanWait = Number(scanWait);
76
+
77
+ if (!Number.isFinite(numericScanWait) || numericScanWait < 0) {
78
+ throw new Error("Invalid query scan wait: ".concat(scanWait));
79
+ }
80
+
81
+ options.scanWait = numericScanWait;
82
+ }
83
+
84
+ return options;
85
+ }
86
+
37
87
  module.exports = class Database {
38
88
  constructor(connection) {
39
89
  var {
@@ -178,10 +228,10 @@ module.exports = class Database {
178
228
  }
179
229
 
180
230
  isCollectionAlreadyExistsError(error) {
181
- var _error$cause, _error$cause2, _error$context, _error$context2;
231
+ var _error$cause, _error$context, _error$context2, _error$cause2, _error$cause3;
182
232
 
183
233
  var message = (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : (_error$cause = error.cause) === null || _error$cause === void 0 ? void 0 : _error$cause.first_error_message) || (error === null || error === void 0 ? void 0 : (_error$context = error.context) === null || _error$context === void 0 ? void 0 : _error$context.first_error_message) || (error === null || error === void 0 ? void 0 : (_error$context2 = error.context) === null || _error$context2 === void 0 ? void 0 : _error$context2.http_response_body) || "";
184
- return error instanceof couchbase.CollectionExistsError || (error === null || error === void 0 ? void 0 : (_error$cause2 = error.cause) === null || _error$cause2 === void 0 ? void 0 : _error$cause2.first_error_code) === 12003 || (error === null || error === void 0 ? void 0 : (_error$cause2 = error.cause) === null || _error$cause2 === void 0 ? void 0 : _error$cause2.first_error_code) === 12027 || /collection .*already exists|already exists/i.test(message);
234
+ return error instanceof couchbase.CollectionExistsError || (error === null || error === void 0 ? void 0 : (_error$cause2 = error.cause) === null || _error$cause2 === void 0 ? void 0 : _error$cause2.first_error_code) === 12003 || (error === null || error === void 0 ? void 0 : (_error$cause3 = error.cause) === null || _error$cause3 === void 0 ? void 0 : _error$cause3.first_error_code) === 12027 || /collection .*already exists|already exists/i.test(message);
185
235
  }
186
236
 
187
237
  upsertCollection() {
@@ -283,9 +333,9 @@ module.exports = class Database {
283
333
  var response = yield _this7.query("CREATE PRIMARY INDEX ON ".concat(_this7.collectionPath(scope_name, col_name)));
284
334
  return response;
285
335
  } catch (e) {
286
- var _e$cause3, _e$cause4;
336
+ var _e$cause, _e$cause2;
287
337
 
288
- if (e instanceof couchbase.IndexExistsError || (e === null || e === void 0 ? void 0 : (_e$cause3 = e.cause) === null || _e$cause3 === void 0 ? void 0 : _e$cause3.first_error_code) === 4300 || /index .*already exists|index_exists/i.test((e === null || e === void 0 ? void 0 : e.message) || (e === null || e === void 0 ? void 0 : (_e$cause4 = e.cause) === null || _e$cause4 === void 0 ? void 0 : _e$cause4.first_error_message) || "")) {
338
+ if (e instanceof couchbase.IndexExistsError || (e === null || e === void 0 ? void 0 : (_e$cause = e.cause) === null || _e$cause === void 0 ? void 0 : _e$cause.first_error_code) === 4300 || /index .*already exists|index_exists/i.test((e === null || e === void 0 ? void 0 : e.message) || (e === null || e === void 0 ? void 0 : (_e$cause2 = e.cause) === null || _e$cause2 === void 0 ? void 0 : _e$cause2.first_error_message) || "")) {
289
339
  return;
290
340
  }
291
341
 
@@ -600,6 +650,7 @@ module.exports = class Database {
600
650
  var do_count = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : "false";
601
651
  var query_scope = arguments.length > 7 ? arguments[7] : undefined;
602
652
  var join = arguments.length > 8 && arguments[8] !== undefined ? arguments[8] : "";
653
+ var query_options = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : {};
603
654
  return _asyncToGenerator(function* () {
604
655
  var is_legacy = _this16.scope_name !== "_default" && _this16.collection_name !== "_default";
605
656
  var bucket = "`".concat(_this16.use_bucket_name ? _this16.bucket : _this16.collection_name, "`");
@@ -663,11 +714,11 @@ module.exports = class Database {
663
714
  var limit_clause = limit > 0 ? " LIMIT ".concat(limit) : "";
664
715
  var offset_clause = offset > 0 ? " OFFSET ".concat(offset) : "";
665
716
  query = query + order_by_clause + limit_clause + offset_clause;
666
- var active_query_space = query_scope || !_this16.use_bucket_name ? "scope" : "cluster";
667
- var queries = [_this16.getData(query, active_query_space)];
717
+ var active_query_space = query_scope || (!_this16.use_bucket_name ? "scope" : "cluster");
718
+ var queries = [_this16.getData(query, active_query_space, query_options)];
668
719
 
669
720
  if (do_count != "false") {
670
- queries.push(_this16.getCount(count_query, active_query_space));
721
+ queries.push(_this16.getCount(count_query, active_query_space, query_options));
671
722
  }
672
723
 
673
724
  var responses = yield Promise.all(queries);
@@ -754,8 +805,9 @@ module.exports = class Database {
754
805
  var _this17 = this;
755
806
 
756
807
  var query_scope = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "cluster";
808
+ var query_options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
757
809
  return _asyncToGenerator(function* () {
758
- var response = yield _this17.query(count_query_element, query_scope);
810
+ var response = yield _this17.query(count_query_element, query_scope, query_options);
759
811
  return response.rows[0].count;
760
812
  })();
761
813
  }
@@ -764,8 +816,9 @@ module.exports = class Database {
764
816
  var _this18 = this;
765
817
 
766
818
  var query_scope = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "cluster";
819
+ var query_options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
767
820
  return _asyncToGenerator(function* () {
768
- var response = yield _this18.query(query_element, query_scope);
821
+ var response = yield _this18.query(query_element, query_scope, query_options);
769
822
  return response.rows;
770
823
  })();
771
824
  }
@@ -843,12 +896,17 @@ module.exports = class Database {
843
896
  })();
844
897
  }
845
898
 
846
- query(query) {
899
+ query(query, query_scope) {
847
900
  var _this20 = this;
848
901
 
849
- var query_scope = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "cluster";
902
+ var query_options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
850
903
  return _asyncToGenerator(function* () {
851
- var scope = query_scope || _this20.query_scope;
904
+ if (query_scope && typeof query_scope === "object" && !Array.isArray(query_scope)) {
905
+ query_options = query_scope;
906
+ query_scope = undefined;
907
+ }
908
+
909
+ var scope = query_scope || _this20.query_scope || "cluster";
852
910
  var sanitizedQuery = query; //sanitization step to be implemented
853
911
 
854
912
  var exec_scope = null;
@@ -861,16 +919,14 @@ module.exports = class Database {
861
919
 
862
920
 
863
921
  var startTime = process.hrtime.bigint();
864
- var options = {
865
- scanConsistency: QueryScanConsistency.RequestPlus
866
- };
922
+ var options = buildQueryOptions(query_options);
867
923
  var response = yield exec_scope.query(sanitizedQuery, options); // End time
868
924
 
869
925
  var endTime = process.hrtime.bigint(); // Calculate duration in microseconds
870
926
 
871
927
  var durationInMilliseconds = (endTime - startTime) / BigInt(1000000); // optimize if response time is greater than 1 second
872
928
 
873
- if (durationInMilliseconds > 2000) {
929
+ if (durationInMilliseconds > 1000) {
874
930
  _this20.optimize(query, exec_scope);
875
931
  }
876
932
 
@@ -878,4 +934,4 @@ module.exports = class Database {
878
934
  })();
879
935
  }
880
936
 
881
- };
937
+ };
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+
3
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
4
+
5
+ function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
6
+
7
+ var assert = require("assert");
8
+
9
+ var {
10
+ QueryScanConsistency
11
+ } = require("couchbase");
12
+
13
+ var Database = require("./index");
14
+
15
+ var passed = 0;
16
+ var failed = 0;
17
+
18
+ function createQueryDb() {
19
+ var calls = [];
20
+ var db = Object.create(Database.prototype);
21
+ db.query_scope = "cluster";
22
+ db.cluster = {
23
+ query: function () {
24
+ var _query = _asyncToGenerator(function* (statement, options) {
25
+ calls.push({
26
+ target: "cluster",
27
+ statement,
28
+ options
29
+ });
30
+ return {
31
+ rows: []
32
+ };
33
+ });
34
+
35
+ function query(_x, _x2) {
36
+ return _query.apply(this, arguments);
37
+ }
38
+
39
+ return query;
40
+ }()
41
+ };
42
+ db.scope = {
43
+ query: function () {
44
+ var _query2 = _asyncToGenerator(function* (statement, options) {
45
+ calls.push({
46
+ target: "scope",
47
+ statement,
48
+ options
49
+ });
50
+ return {
51
+ rows: []
52
+ };
53
+ });
54
+
55
+ function query(_x3, _x4) {
56
+ return _query2.apply(this, arguments);
57
+ }
58
+
59
+ return query;
60
+ }()
61
+ };
62
+
63
+ db.optimize = () => {};
64
+
65
+ return {
66
+ db,
67
+ calls
68
+ };
69
+ }
70
+
71
+ function test(_x5, _x6) {
72
+ return _test.apply(this, arguments);
73
+ }
74
+
75
+ function _test() {
76
+ _test = _asyncToGenerator(function* (name, fn) {
77
+ try {
78
+ yield fn();
79
+ console.log("\u2713 ".concat(name));
80
+ passed += 1;
81
+ } catch (error) {
82
+ console.log("\u2717 ".concat(name));
83
+ console.log(" Error: ".concat(error.stack || error.message));
84
+ failed += 1;
85
+ }
86
+ });
87
+ return _test.apply(this, arguments);
88
+ }
89
+
90
+ function run() {
91
+ return _run.apply(this, arguments);
92
+ }
93
+
94
+ function _run() {
95
+ _run = _asyncToGenerator(function* () {
96
+ console.log("\n--- query consistency tests ---");
97
+ yield test("ordinary SQL++ queries default to not_bounded",
98
+ /*#__PURE__*/
99
+ _asyncToGenerator(function* () {
100
+ var {
101
+ db,
102
+ calls
103
+ } = createQueryDb();
104
+ yield db.query("SELECT 1");
105
+ assert.strictEqual(calls[0].target, "cluster");
106
+ assert.strictEqual(calls[0].options.scanConsistency, QueryScanConsistency.NotBounded);
107
+ }));
108
+ yield test("request_plus can be selected explicitly",
109
+ /*#__PURE__*/
110
+ _asyncToGenerator(function* () {
111
+ var {
112
+ db,
113
+ calls
114
+ } = createQueryDb();
115
+ yield db.query("SELECT 1", "scope", {
116
+ scan_consistency: "request_plus",
117
+ scan_wait: 2500
118
+ });
119
+ assert.strictEqual(calls[0].target, "scope");
120
+ assert.strictEqual(calls[0].options.scanConsistency, QueryScanConsistency.RequestPlus);
121
+ assert.strictEqual(calls[0].options.scanWait, 2500);
122
+ }));
123
+ yield test("legacy boolean consistency values remain supported",
124
+ /*#__PURE__*/
125
+ _asyncToGenerator(function* () {
126
+ var {
127
+ db,
128
+ calls
129
+ } = createQueryDb();
130
+ yield db.query("SELECT 1", "cluster", {
131
+ scan_consistency: true
132
+ });
133
+ assert.strictEqual(calls[0].options.scanConsistency, QueryScanConsistency.RequestPlus);
134
+ }));
135
+ yield test("mutation state is forwarded without a conflicting scan mode",
136
+ /*#__PURE__*/
137
+ _asyncToGenerator(function* () {
138
+ var {
139
+ db,
140
+ calls
141
+ } = createQueryDb();
142
+ var mutationState = {
143
+ token: "test-token"
144
+ };
145
+ yield db.query("SELECT 1", {
146
+ consistent_with: mutationState,
147
+ scan_wait: 1000
148
+ });
149
+ assert.strictEqual(calls[0].options.consistentWith, mutationState);
150
+ assert.strictEqual(calls[0].options.scanConsistency, undefined);
151
+ assert.strictEqual(calls[0].options.scanWait, 1000);
152
+ }));
153
+ yield test("search keeps query scope separate from consistency",
154
+ /*#__PURE__*/
155
+ _asyncToGenerator(function* () {
156
+ var db = Object.create(Database.prototype);
157
+ var calls = [];
158
+ db.use_bucket_name = true;
159
+ db.bucket = "documents";
160
+ db.scope_name = "_default";
161
+ db.collection_name = "_default";
162
+
163
+ db.getData =
164
+ /*#__PURE__*/
165
+ function () {
166
+ var _ref6 = _asyncToGenerator(function* (statement, scope, options) {
167
+ calls.push({
168
+ kind: "data",
169
+ statement,
170
+ scope,
171
+ options
172
+ });
173
+ return [];
174
+ });
175
+
176
+ return function (_x7, _x8, _x9) {
177
+ return _ref6.apply(this, arguments);
178
+ };
179
+ }();
180
+
181
+ db.getCount =
182
+ /*#__PURE__*/
183
+ function () {
184
+ var _ref7 = _asyncToGenerator(function* (statement, scope, options) {
185
+ calls.push({
186
+ kind: "count",
187
+ statement,
188
+ scope,
189
+ options
190
+ });
191
+ return 0;
192
+ });
193
+
194
+ return function (_x10, _x11, _x12) {
195
+ return _ref7.apply(this, arguments);
196
+ };
197
+ }();
198
+
199
+ var options = {
200
+ scan_consistency: "request_plus"
201
+ };
202
+ yield db.search("application", "", "", 10, 0, "created_at DESC", "true", "cluster", "", options);
203
+ assert.deepStrictEqual(calls.map(call => call.scope), ["cluster", "cluster"]);
204
+ assert.strictEqual(calls[0].options, options);
205
+ assert.strictEqual(calls[1].options, options);
206
+ }));
207
+ yield test("invalid consistency values fail before issuing a query",
208
+ /*#__PURE__*/
209
+ _asyncToGenerator(function* () {
210
+ var {
211
+ db,
212
+ calls
213
+ } = createQueryDb();
214
+ yield assert.rejects(() => db.query("SELECT 1", "cluster", {
215
+ scan_consistency: "eventual"
216
+ }), /Unsupported query scan consistency/);
217
+ assert.strictEqual(calls.length, 0);
218
+ }));
219
+ console.log("\nPassed: ".concat(passed));
220
+ console.log("Failed: ".concat(failed));
221
+ if (failed > 0) process.exitCode = 1;
222
+ });
223
+ return _run.apply(this, arguments);
224
+ }
225
+
226
+ run().catch(error => {
227
+ console.error(error);
228
+ process.exit(1);
229
+ });
package/build/utility.js CHANGED
@@ -284,8 +284,21 @@ function splitSelectClause(clause) {
284
284
 
285
285
  return parts;
286
286
  }
287
+
288
+ function hasQualifiedWildcardProjection(query) {
289
+ var selectMatch = query.match(/\bSELECT\b([\s\S]+?)\bFROM\b/i);
290
+
291
+ if (!selectMatch) {
292
+ return false;
293
+ }
294
+
295
+ return splitSelectClause(selectMatch[1].trim()).some(part => {
296
+ var trimmedPart = part.trim();
297
+ return /^\(\s*`?\w+`?\s*\)\s*\.\s*\*$/.test(trimmedPart) || /^`?\w+`?\s*\.\s*\*$/.test(trimmedPart);
298
+ });
299
+ }
287
300
  /**
288
- * Builds a covering index CREATE statement from a query
301
+ * Builds a covering or fallback non-covering index CREATE statement from a query
289
302
  * @param {string} query - The N1QL query to analyze
290
303
  * @param {Object} options - Configuration options
291
304
  * @param {string} options.bucket - The bucket name
@@ -298,19 +311,19 @@ function splitSelectClause(clause) {
298
311
  function buildCoveringIndex(query, options) {
299
312
  var {
300
313
  bucket,
301
- scope,
302
- collection
314
+ scope = "_default",
315
+ collection = "_default"
303
316
  } = options;
304
317
 
305
- if (!bucket || !scope || !collection) {
318
+ if (!bucket) {
306
319
  return null;
307
320
  } // Extract fields from SELECT clause
308
321
 
309
322
 
310
323
  var selectFields = extractSelectFields(query);
324
+ var isNonCoveringFallback = selectFields === null && hasQualifiedWildcardProjection(query);
311
325
 
312
- if (selectFields === null) {
313
- // SELECT * query - cannot create covering index
326
+ if (selectFields === null && !isNonCoveringFallback) {
314
327
  return null;
315
328
  } // Extract fields from WHERE clause
316
329
 
@@ -349,7 +362,7 @@ function buildCoveringIndex(query, options) {
349
362
  return true;
350
363
  }); // Filter SELECT fields (remove duplicates from WHERE, GROUP BY, and ORDER BY, remove deleted)
351
364
 
352
- var selectFieldsFiltered = selectFields.filter(f => {
365
+ var selectFieldsFiltered = (selectFields || []).filter(f => {
353
366
  var lower = f.toLowerCase();
354
367
 
355
368
  if (addedFields.has(lower) || lower === "deleted") {
@@ -373,9 +386,10 @@ function buildCoveringIndex(query, options) {
373
386
 
374
387
  var indexFieldsKey = allFields.map(field => formatIndexKey(field, orderDirectionMap)).sort().join(",");
375
388
  var orderFieldsKey = orderByIndexFields.map(field => formatIndexKey(field.name, orderDirectionMap)).join(",");
376
- var indexKey = "".concat(bucket, "|").concat(scope, "|").concat(collection, "|fields:").concat(indexFieldsKey, "|order:").concat(orderFieldsKey);
389
+ var indexMode = isNonCoveringFallback ? "noncovered" : "covered";
390
+ var indexKey = "".concat(bucket, "|").concat(scope, "|").concat(collection, "|mode:").concat(indexMode, "|fields:").concat(indexFieldsKey, "|order:").concat(orderFieldsKey);
377
391
  var hash = generateHash(indexKey);
378
- var indexName = "adv_covered_".concat(hash); // Build the CREATE INDEX statement
392
+ var indexName = "adv_".concat(indexMode, "_").concat(hash); // Build the CREATE INDEX statement
379
393
 
380
394
  var indexStatement = "CREATE INDEX `".concat(indexName, "` ON `default`:`").concat(bucket, "`.`").concat(scope, "`.`").concat(collection, "`(").concat(fieldList, ")");
381
395
  return indexStatement;
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "spice-couchbase",
3
- "version": "2.0.53",
3
+ "version": "2.0.55",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
7
- "test": "node src/utility.test.js"
7
+ "test": "node src/utility.test.js",
8
+ "test:query-consistency": "node src/index.query-consistency.test.js"
8
9
  },
9
10
  "author": "Chad Fraser <chad@sonover.com> (http://sonover.com)",
10
11
  "license": "ISC",
package/src/index.js CHANGED
@@ -25,6 +25,60 @@ if (!Promise.allSettled) {
25
25
  // Couchbase lookupIn has a limit of 16 operations per call
26
26
  const LOOKUP_IN_MAX_SPECS = 16;
27
27
 
28
+ function normalizeScanConsistency(value) {
29
+ if (
30
+ value === true ||
31
+ value === QueryScanConsistency.RequestPlus ||
32
+ (typeof value === "string" &&
33
+ ["true", "request_plus"].includes(value.toLowerCase()))
34
+ ) {
35
+ return QueryScanConsistency.RequestPlus;
36
+ }
37
+
38
+ if (
39
+ value === undefined ||
40
+ value === null ||
41
+ value === "" ||
42
+ value === false ||
43
+ value === QueryScanConsistency.NotBounded ||
44
+ (typeof value === "string" &&
45
+ ["false", "not_bounded"].includes(value.toLowerCase()))
46
+ ) {
47
+ return QueryScanConsistency.NotBounded;
48
+ }
49
+
50
+ throw new Error(`Unsupported query scan consistency: ${value}`);
51
+ }
52
+
53
+ function buildQueryOptions(queryOptions = {}) {
54
+ if (typeof queryOptions !== "object" || Array.isArray(queryOptions)) {
55
+ queryOptions = { scan_consistency: queryOptions };
56
+ }
57
+
58
+ const options = {};
59
+ const consistentWith =
60
+ queryOptions.consistentWith ?? queryOptions.consistent_with;
61
+
62
+ if (consistentWith !== undefined && consistentWith !== null) {
63
+ options.consistentWith = consistentWith;
64
+ } else {
65
+ options.scanConsistency = normalizeScanConsistency(
66
+ queryOptions.scanConsistency ?? queryOptions.scan_consistency,
67
+ );
68
+ }
69
+
70
+ const scanWait = queryOptions.scanWait ?? queryOptions.scan_wait;
71
+ if (scanWait !== undefined && scanWait !== null && scanWait !== "") {
72
+ const numericScanWait = Number(scanWait);
73
+ if (!Number.isFinite(numericScanWait) || numericScanWait < 0) {
74
+ throw new Error(`Invalid query scan wait: ${scanWait}`);
75
+ }
76
+ options.scanWait = numericScanWait;
77
+ }
78
+
79
+ return options;
80
+ }
81
+
28
82
  module.exports = class Database {
29
83
  constructor(
30
84
  connection,
@@ -249,9 +303,7 @@ module.exports = class Database {
249
303
  spice.db.collections.add(connection_name);
250
304
  return response;
251
305
  } catch (e) {
252
- if (
253
- this.isCollectionAlreadyExistsError(e)
254
- ) {
306
+ if (this.isCollectionAlreadyExistsError(e)) {
255
307
  spice.db.collections.add(connection_name);
256
308
  return;
257
309
  }
@@ -559,6 +611,7 @@ module.exports = class Database {
559
611
  do_count = "false",
560
612
  query_scope,
561
613
  join = "",
614
+ query_options = {},
562
615
  ) {
563
616
  const is_legacy =
564
617
  this.scope_name !== "_default" && this.collection_name !== "_default";
@@ -630,10 +683,14 @@ module.exports = class Database {
630
683
  query = query + order_by_clause + limit_clause + offset_clause;
631
684
 
632
685
  const active_query_space =
633
- query_scope || !this.use_bucket_name ? "scope" : "cluster";
634
- const queries = [this.getData(query, active_query_space)];
686
+ query_scope || (!this.use_bucket_name ? "scope" : "cluster");
687
+ const queries = [
688
+ this.getData(query, active_query_space, query_options),
689
+ ];
635
690
  if (do_count != "false") {
636
- queries.push(this.getCount(count_query, active_query_space));
691
+ queries.push(
692
+ this.getCount(count_query, active_query_space, query_options),
693
+ );
637
694
  }
638
695
  const responses = await Promise.all(queries);
639
696
 
@@ -700,12 +757,20 @@ module.exports = class Database {
700
757
  });
701
758
  }
702
759
 
703
- async getCount(count_query_element, query_scope = "cluster") {
704
- let response = await this.query(count_query_element, query_scope);
760
+ async getCount(
761
+ count_query_element,
762
+ query_scope = "cluster",
763
+ query_options = {},
764
+ ) {
765
+ let response = await this.query(
766
+ count_query_element,
767
+ query_scope,
768
+ query_options,
769
+ );
705
770
  return response.rows[0].count;
706
771
  }
707
- async getData(query_element, query_scope = "cluster") {
708
- let response = await this.query(query_element, query_scope);
772
+ async getData(query_element, query_scope = "cluster", query_options = {}) {
773
+ let response = await this.query(query_element, query_scope, query_options);
709
774
  return response.rows;
710
775
  }
711
776
 
@@ -774,8 +839,17 @@ module.exports = class Database {
774
839
  }
775
840
  }
776
841
 
777
- async query(query, query_scope = "cluster") {
778
- let scope = query_scope || this.query_scope;
842
+ async query(query, query_scope, query_options = {}) {
843
+ if (
844
+ query_scope &&
845
+ typeof query_scope === "object" &&
846
+ !Array.isArray(query_scope)
847
+ ) {
848
+ query_options = query_scope;
849
+ query_scope = undefined;
850
+ }
851
+
852
+ let scope = query_scope || this.query_scope || "cluster";
779
853
  const sanitizedQuery = query; //sanitization step to be implemented
780
854
  let exec_scope = null;
781
855
 
@@ -786,16 +860,14 @@ module.exports = class Database {
786
860
  }
787
861
  // Start time
788
862
  let startTime = process.hrtime.bigint();
789
- const options = {
790
- scanConsistency: QueryScanConsistency.RequestPlus,
791
- };
863
+ const options = buildQueryOptions(query_options);
792
864
  let response = await exec_scope.query(sanitizedQuery, options);
793
865
  // End time
794
866
  let endTime = process.hrtime.bigint();
795
867
  // Calculate duration in microseconds
796
868
  let durationInMilliseconds = (endTime - startTime) / BigInt(1000000);
797
869
  // optimize if response time is greater than 1 second
798
- if (durationInMilliseconds > 2000) {
870
+ if (durationInMilliseconds > 1000) {
799
871
  this.optimize(query, exec_scope);
800
872
  }
801
873
  return response;
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+
3
+ const assert = require("assert");
4
+ const { QueryScanConsistency } = require("couchbase");
5
+ const Database = require("./index");
6
+
7
+ let passed = 0;
8
+ let failed = 0;
9
+
10
+ function createQueryDb() {
11
+ const calls = [];
12
+ const db = Object.create(Database.prototype);
13
+ db.query_scope = "cluster";
14
+ db.cluster = {
15
+ query: async (statement, options) => {
16
+ calls.push({ target: "cluster", statement, options });
17
+ return { rows: [] };
18
+ },
19
+ };
20
+ db.scope = {
21
+ query: async (statement, options) => {
22
+ calls.push({ target: "scope", statement, options });
23
+ return { rows: [] };
24
+ },
25
+ };
26
+ db.optimize = () => {};
27
+ return { db, calls };
28
+ }
29
+
30
+ async function test(name, fn) {
31
+ try {
32
+ await fn();
33
+ console.log(`✓ ${name}`);
34
+ passed += 1;
35
+ } catch (error) {
36
+ console.log(`✗ ${name}`);
37
+ console.log(` Error: ${error.stack || error.message}`);
38
+ failed += 1;
39
+ }
40
+ }
41
+
42
+ async function run() {
43
+ console.log("\n--- query consistency tests ---");
44
+
45
+ await test("ordinary SQL++ queries default to not_bounded", async () => {
46
+ const { db, calls } = createQueryDb();
47
+
48
+ await db.query("SELECT 1");
49
+
50
+ assert.strictEqual(calls[0].target, "cluster");
51
+ assert.strictEqual(
52
+ calls[0].options.scanConsistency,
53
+ QueryScanConsistency.NotBounded,
54
+ );
55
+ });
56
+
57
+ await test("request_plus can be selected explicitly", async () => {
58
+ const { db, calls } = createQueryDb();
59
+
60
+ await db.query("SELECT 1", "scope", {
61
+ scan_consistency: "request_plus",
62
+ scan_wait: 2500,
63
+ });
64
+
65
+ assert.strictEqual(calls[0].target, "scope");
66
+ assert.strictEqual(
67
+ calls[0].options.scanConsistency,
68
+ QueryScanConsistency.RequestPlus,
69
+ );
70
+ assert.strictEqual(calls[0].options.scanWait, 2500);
71
+ });
72
+
73
+ await test("legacy boolean consistency values remain supported", async () => {
74
+ const { db, calls } = createQueryDb();
75
+
76
+ await db.query("SELECT 1", "cluster", { scan_consistency: true });
77
+
78
+ assert.strictEqual(
79
+ calls[0].options.scanConsistency,
80
+ QueryScanConsistency.RequestPlus,
81
+ );
82
+ });
83
+
84
+ await test("mutation state is forwarded without a conflicting scan mode", async () => {
85
+ const { db, calls } = createQueryDb();
86
+ const mutationState = { token: "test-token" };
87
+
88
+ await db.query("SELECT 1", {
89
+ consistent_with: mutationState,
90
+ scan_wait: 1000,
91
+ });
92
+
93
+ assert.strictEqual(calls[0].options.consistentWith, mutationState);
94
+ assert.strictEqual(calls[0].options.scanConsistency, undefined);
95
+ assert.strictEqual(calls[0].options.scanWait, 1000);
96
+ });
97
+
98
+ await test("search keeps query scope separate from consistency", async () => {
99
+ const db = Object.create(Database.prototype);
100
+ const calls = [];
101
+ db.use_bucket_name = true;
102
+ db.bucket = "documents";
103
+ db.scope_name = "_default";
104
+ db.collection_name = "_default";
105
+ db.getData = async (statement, scope, options) => {
106
+ calls.push({ kind: "data", statement, scope, options });
107
+ return [];
108
+ };
109
+ db.getCount = async (statement, scope, options) => {
110
+ calls.push({ kind: "count", statement, scope, options });
111
+ return 0;
112
+ };
113
+ const options = { scan_consistency: "request_plus" };
114
+
115
+ await db.search(
116
+ "application",
117
+ "",
118
+ "",
119
+ 10,
120
+ 0,
121
+ "created_at DESC",
122
+ "true",
123
+ "cluster",
124
+ "",
125
+ options,
126
+ );
127
+
128
+ assert.deepStrictEqual(
129
+ calls.map((call) => call.scope),
130
+ ["cluster", "cluster"],
131
+ );
132
+ assert.strictEqual(calls[0].options, options);
133
+ assert.strictEqual(calls[1].options, options);
134
+ });
135
+
136
+ await test("invalid consistency values fail before issuing a query", async () => {
137
+ const { db, calls } = createQueryDb();
138
+
139
+ await assert.rejects(
140
+ () => db.query("SELECT 1", "cluster", { scan_consistency: "eventual" }),
141
+ /Unsupported query scan consistency/,
142
+ );
143
+ assert.strictEqual(calls.length, 0);
144
+ });
145
+
146
+ console.log(`\nPassed: ${passed}`);
147
+ console.log(`Failed: ${failed}`);
148
+ if (failed > 0) process.exitCode = 1;
149
+ }
150
+
151
+ run().catch((error) => {
152
+ console.error(error);
153
+ process.exit(1);
154
+ });
package/src/utility.js CHANGED
@@ -293,8 +293,23 @@ function splitSelectClause(clause) {
293
293
  return parts;
294
294
  }
295
295
 
296
+ function hasQualifiedWildcardProjection(query) {
297
+ const selectMatch = query.match(/\bSELECT\b(.+?)\bFROM\b/is);
298
+ if (!selectMatch) {
299
+ return false;
300
+ }
301
+
302
+ return splitSelectClause(selectMatch[1].trim()).some((part) => {
303
+ const trimmedPart = part.trim();
304
+ return (
305
+ /^\(\s*`?\w+`?\s*\)\s*\.\s*\*$/.test(trimmedPart) ||
306
+ /^`?\w+`?\s*\.\s*\*$/.test(trimmedPart)
307
+ );
308
+ });
309
+ }
310
+
296
311
  /**
297
- * Builds a covering index CREATE statement from a query
312
+ * Builds a covering or fallback non-covering index CREATE statement from a query
298
313
  * @param {string} query - The N1QL query to analyze
299
314
  * @param {Object} options - Configuration options
300
315
  * @param {string} options.bucket - The bucket name
@@ -303,16 +318,17 @@ function splitSelectClause(clause) {
303
318
  * @returns {string|null} - The CREATE INDEX statement, or null if cannot build
304
319
  */
305
320
  function buildCoveringIndex(query, options) {
306
- const { bucket, scope, collection } = options;
321
+ const { bucket, scope = "_default", collection = "_default" } = options;
307
322
 
308
- if (!bucket || !scope || !collection) {
323
+ if (!bucket) {
309
324
  return null;
310
325
  }
311
326
 
312
327
  // Extract fields from SELECT clause
313
328
  const selectFields = extractSelectFields(query);
314
- if (selectFields === null) {
315
- // SELECT * query - cannot create covering index
329
+ const isNonCoveringFallback =
330
+ selectFields === null && hasQualifiedWildcardProjection(query);
331
+ if (selectFields === null && !isNonCoveringFallback) {
316
332
  return null;
317
333
  }
318
334
 
@@ -356,7 +372,7 @@ function buildCoveringIndex(query, options) {
356
372
  });
357
373
 
358
374
  // Filter SELECT fields (remove duplicates from WHERE, GROUP BY, and ORDER BY, remove deleted)
359
- const selectFieldsFiltered = selectFields.filter((f) => {
375
+ const selectFieldsFiltered = (selectFields || []).filter((f) => {
360
376
  const lower = f.toLowerCase();
361
377
  if (addedFields.has(lower) || lower === "deleted") {
362
378
  return false;
@@ -393,9 +409,10 @@ function buildCoveringIndex(query, options) {
393
409
  const orderFieldsKey = orderByIndexFields
394
410
  .map((field) => formatIndexKey(field.name, orderDirectionMap))
395
411
  .join(",");
396
- const indexKey = `${bucket}|${scope}|${collection}|fields:${indexFieldsKey}|order:${orderFieldsKey}`;
412
+ const indexMode = isNonCoveringFallback ? "noncovered" : "covered";
413
+ const indexKey = `${bucket}|${scope}|${collection}|mode:${indexMode}|fields:${indexFieldsKey}|order:${orderFieldsKey}`;
397
414
  const hash = generateHash(indexKey);
398
- const indexName = `adv_covered_${hash}`;
415
+ const indexName = `adv_${indexMode}_${hash}`;
399
416
 
400
417
  // Build the CREATE INDEX statement
401
418
  const indexStatement = `CREATE INDEX \`${indexName}\` ON \`default\`:\`${bucket}\`.\`${scope}\`.\`${collection}\`(${fieldList})`;