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 +73 -17
- package/build/index.query-consistency.test.js +229 -0
- package/build/utility.js +23 -9
- package/package.json +3 -2
- package/src/index.js +88 -16
- package/src/index.query-consistency.test.js +154 -0
- package/src/utility.js +25 -8
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$
|
|
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$
|
|
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$
|
|
336
|
+
var _e$cause, _e$cause2;
|
|
287
337
|
|
|
288
|
-
if (e instanceof couchbase.IndexExistsError || (e === null || e === void 0 ? void 0 : (_e$
|
|
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
|
|
902
|
+
var query_options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
850
903
|
return _asyncToGenerator(function* () {
|
|
851
|
-
|
|
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 >
|
|
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
|
|
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
|
|
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 = "
|
|
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.
|
|
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 = [
|
|
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(
|
|
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(
|
|
704
|
-
|
|
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 =
|
|
778
|
-
|
|
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 >
|
|
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
|
|
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
|
-
|
|
315
|
-
|
|
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
|
|
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 = `
|
|
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})`;
|