spice-couchbase 2.0.46 → 2.0.48
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 +11 -7
- package/build/index.multi_get.test.js +327 -0
- package/build/utility.js +7 -12
- package/build/utility.test.js +23 -0
- package/package.json +1 -1
- package/src/index.js +7 -2
- package/src/index.multi_get.test.js +199 -0
- package/src/utility.js +6 -4
- package/src/utility.test.js +31 -0
package/build/index.js
CHANGED
|
@@ -115,10 +115,16 @@ module.exports = class Database {
|
|
|
115
115
|
var _this = this;
|
|
116
116
|
|
|
117
117
|
return _asyncToGenerator(function* () {
|
|
118
|
-
var
|
|
118
|
+
var connectOptions = {
|
|
119
119
|
username: config.username,
|
|
120
120
|
password: config.password
|
|
121
|
-
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (config.configProfile) {
|
|
124
|
+
connectOptions.configProfile = config.configProfile;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
var cluster = yield couchbase.connect(config.nodes, connectOptions);
|
|
122
128
|
var bucket = cluster.bucket(config.bucket);
|
|
123
129
|
var collection;
|
|
124
130
|
var scope_name = _this.scope_name || config.scope || "_default";
|
|
@@ -824,9 +830,7 @@ module.exports = class Database {
|
|
|
824
830
|
}); // 4. Execute the index creation if we got a valid statement
|
|
825
831
|
|
|
826
832
|
if (indexStatement) {
|
|
827
|
-
console.log("Executing index creation:", indexStatement);
|
|
828
833
|
var response = yield exec_scope.query(indexStatement);
|
|
829
|
-
console.log("Index creation response:", response);
|
|
830
834
|
}
|
|
831
835
|
} catch (e) {// Silently handle errors (index may already exist, etc.)
|
|
832
836
|
}
|
|
@@ -859,10 +863,10 @@ module.exports = class Database {
|
|
|
859
863
|
var endTime = process.hrtime.bigint(); // Calculate duration in microseconds
|
|
860
864
|
|
|
861
865
|
var durationInMilliseconds = (endTime - startTime) / BigInt(1000000); // optimize if response time is greater than 1 second
|
|
862
|
-
//if (durationInMilliseconds > 2000) {
|
|
863
|
-
|
|
864
|
-
_this19.optimize(query, exec_scope); //}
|
|
865
866
|
|
|
867
|
+
if (durationInMilliseconds > 2000) {
|
|
868
|
+
_this19.optimize(query, exec_scope);
|
|
869
|
+
}
|
|
866
870
|
|
|
867
871
|
return response;
|
|
868
872
|
})();
|
|
@@ -0,0 +1,327 @@
|
|
|
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 Database = require("./index");
|
|
10
|
+
|
|
11
|
+
var passed = 0;
|
|
12
|
+
var failed = 0;
|
|
13
|
+
|
|
14
|
+
function createDb(_ref) {
|
|
15
|
+
var {
|
|
16
|
+
lookupIn,
|
|
17
|
+
get
|
|
18
|
+
} = _ref;
|
|
19
|
+
var db = Object.create(Database.prototype);
|
|
20
|
+
var collection = {
|
|
21
|
+
lookupIn,
|
|
22
|
+
get
|
|
23
|
+
};
|
|
24
|
+
db.use_bucket_name = false;
|
|
25
|
+
db.collection = collection;
|
|
26
|
+
db.connection = collection;
|
|
27
|
+
return db;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function test(_x, _x2) {
|
|
31
|
+
return _test.apply(this, arguments);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _test() {
|
|
35
|
+
_test = _asyncToGenerator(function* (name, fn) {
|
|
36
|
+
try {
|
|
37
|
+
yield fn();
|
|
38
|
+
console.log("\u2713 ".concat(name));
|
|
39
|
+
passed++;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.log("\u2717 ".concat(name));
|
|
42
|
+
console.log(" Error: ".concat(error.message));
|
|
43
|
+
failed++;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return _test.apply(this, arguments);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function run() {
|
|
50
|
+
return _run.apply(this, arguments);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function _run() {
|
|
54
|
+
_run = _asyncToGenerator(function* () {
|
|
55
|
+
console.log("\n--- multi_get tests ---");
|
|
56
|
+
yield test("lookupIn success with dotted object path does not fallback to get",
|
|
57
|
+
/*#__PURE__*/
|
|
58
|
+
_asyncToGenerator(function* () {
|
|
59
|
+
var getCalls = 0;
|
|
60
|
+
var db = createDb({
|
|
61
|
+
lookupIn: function () {
|
|
62
|
+
var _lookupIn = _asyncToGenerator(function* () {
|
|
63
|
+
return {
|
|
64
|
+
content: [{
|
|
65
|
+
value: "Alice",
|
|
66
|
+
error: null
|
|
67
|
+
}]
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function lookupIn() {
|
|
72
|
+
return _lookupIn.apply(this, arguments);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return lookupIn;
|
|
76
|
+
}(),
|
|
77
|
+
get: function () {
|
|
78
|
+
var _get = _asyncToGenerator(function* () {
|
|
79
|
+
getCalls++;
|
|
80
|
+
return {
|
|
81
|
+
content: {
|
|
82
|
+
profile: {
|
|
83
|
+
name: "Alice"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function get() {
|
|
90
|
+
return _get.apply(this, arguments);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return get;
|
|
94
|
+
}()
|
|
95
|
+
});
|
|
96
|
+
var result = yield db.multi_get(["doc-1"], {
|
|
97
|
+
columns: ["profile.name"]
|
|
98
|
+
});
|
|
99
|
+
assert.deepStrictEqual(result, [{
|
|
100
|
+
id: "doc-1",
|
|
101
|
+
"profile.name": "Alice"
|
|
102
|
+
}]);
|
|
103
|
+
assert.strictEqual(getCalls, 0);
|
|
104
|
+
}));
|
|
105
|
+
yield test("array-mapped dotted path falls back and returns mapped array values",
|
|
106
|
+
/*#__PURE__*/
|
|
107
|
+
_asyncToGenerator(function* () {
|
|
108
|
+
var db = createDb({
|
|
109
|
+
lookupIn: function () {
|
|
110
|
+
var _lookupIn2 = _asyncToGenerator(function* () {
|
|
111
|
+
return {
|
|
112
|
+
content: [{
|
|
113
|
+
value: undefined,
|
|
114
|
+
error: new Error("path mismatch")
|
|
115
|
+
}]
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
function lookupIn() {
|
|
120
|
+
return _lookupIn2.apply(this, arguments);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return lookupIn;
|
|
124
|
+
}(),
|
|
125
|
+
get: function () {
|
|
126
|
+
var _get2 = _asyncToGenerator(function* () {
|
|
127
|
+
return {
|
|
128
|
+
content: {
|
|
129
|
+
tags: [{
|
|
130
|
+
name: "vip"
|
|
131
|
+
}, {
|
|
132
|
+
name: "beta"
|
|
133
|
+
}]
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
function get() {
|
|
139
|
+
return _get2.apply(this, arguments);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return get;
|
|
143
|
+
}()
|
|
144
|
+
});
|
|
145
|
+
var result = yield db.multi_get(["doc-1"], {
|
|
146
|
+
columns: ["tags.name"]
|
|
147
|
+
});
|
|
148
|
+
assert.deepStrictEqual(result, [{
|
|
149
|
+
id: "doc-1",
|
|
150
|
+
"tags.name": ["vip", "beta"]
|
|
151
|
+
}]);
|
|
152
|
+
}));
|
|
153
|
+
yield test("missing dotted fields return typed empties (object and array)",
|
|
154
|
+
/*#__PURE__*/
|
|
155
|
+
_asyncToGenerator(function* () {
|
|
156
|
+
var db = createDb({
|
|
157
|
+
lookupIn: function () {
|
|
158
|
+
var _lookupIn3 = _asyncToGenerator(function* () {
|
|
159
|
+
return {
|
|
160
|
+
content: [{
|
|
161
|
+
value: undefined,
|
|
162
|
+
error: new Error("path mismatch")
|
|
163
|
+
}]
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
function lookupIn() {
|
|
168
|
+
return _lookupIn3.apply(this, arguments);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return lookupIn;
|
|
172
|
+
}(),
|
|
173
|
+
get: function () {
|
|
174
|
+
var _get3 = _asyncToGenerator(function* () {
|
|
175
|
+
return {
|
|
176
|
+
content: {
|
|
177
|
+
profile: {},
|
|
178
|
+
tags: []
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
function get() {
|
|
184
|
+
return _get3.apply(this, arguments);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return get;
|
|
188
|
+
}()
|
|
189
|
+
});
|
|
190
|
+
var result = yield db.multi_get(["doc-1"], {
|
|
191
|
+
columns: ["profile.details", "tags.name"]
|
|
192
|
+
});
|
|
193
|
+
assert.deepStrictEqual(result, [{
|
|
194
|
+
id: "doc-1",
|
|
195
|
+
"profile.details": {},
|
|
196
|
+
"tags.name": []
|
|
197
|
+
}]);
|
|
198
|
+
}));
|
|
199
|
+
yield test("mixed key outcomes keep order and omit missing docs",
|
|
200
|
+
/*#__PURE__*/
|
|
201
|
+
_asyncToGenerator(function* () {
|
|
202
|
+
var db = createDb({
|
|
203
|
+
lookupIn: function () {
|
|
204
|
+
var _lookupIn4 = _asyncToGenerator(function* (key) {
|
|
205
|
+
if (key === "a") {
|
|
206
|
+
return {
|
|
207
|
+
content: [{
|
|
208
|
+
value: "Alice",
|
|
209
|
+
error: null
|
|
210
|
+
}]
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (key === "b") {
|
|
215
|
+
return {
|
|
216
|
+
content: [{
|
|
217
|
+
value: undefined,
|
|
218
|
+
error: new Error("path mismatch")
|
|
219
|
+
}]
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw new Error("not found");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
function lookupIn(_x3) {
|
|
227
|
+
return _lookupIn4.apply(this, arguments);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return lookupIn;
|
|
231
|
+
}(),
|
|
232
|
+
get: function () {
|
|
233
|
+
var _get4 = _asyncToGenerator(function* (key) {
|
|
234
|
+
if (key === "b") {
|
|
235
|
+
return {
|
|
236
|
+
content: {
|
|
237
|
+
tags: [{
|
|
238
|
+
name: "vip"
|
|
239
|
+
}, {
|
|
240
|
+
name: "beta"
|
|
241
|
+
}]
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
throw new Error("not found");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
function get(_x4) {
|
|
250
|
+
return _get4.apply(this, arguments);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return get;
|
|
254
|
+
}()
|
|
255
|
+
});
|
|
256
|
+
var result = yield db.multi_get(["a", "b", "c"], {
|
|
257
|
+
columns: ["tags.name"]
|
|
258
|
+
});
|
|
259
|
+
assert.deepStrictEqual(result, [{
|
|
260
|
+
id: "a",
|
|
261
|
+
"tags.name": "Alice"
|
|
262
|
+
}, {
|
|
263
|
+
id: "b",
|
|
264
|
+
"tags.name": ["vip", "beta"]
|
|
265
|
+
}]);
|
|
266
|
+
}));
|
|
267
|
+
yield test("non-column multi_get behavior remains unchanged",
|
|
268
|
+
/*#__PURE__*/
|
|
269
|
+
_asyncToGenerator(function* () {
|
|
270
|
+
var db = createDb({
|
|
271
|
+
lookupIn: function () {
|
|
272
|
+
var _lookupIn5 = _asyncToGenerator(function* () {
|
|
273
|
+
throw new Error("should not be called");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
function lookupIn() {
|
|
277
|
+
return _lookupIn5.apply(this, arguments);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return lookupIn;
|
|
281
|
+
}(),
|
|
282
|
+
get: function () {
|
|
283
|
+
var _get5 = _asyncToGenerator(function* (key) {
|
|
284
|
+
if (key === "missing") {
|
|
285
|
+
throw new Error("not found");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
value: {
|
|
290
|
+
type: "user",
|
|
291
|
+
name: key.toUpperCase()
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
function get(_x5) {
|
|
297
|
+
return _get5.apply(this, arguments);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return get;
|
|
301
|
+
}()
|
|
302
|
+
});
|
|
303
|
+
var result = yield db.multi_get(["a", "missing", "b"], {
|
|
304
|
+
keep_type: false
|
|
305
|
+
});
|
|
306
|
+
assert.deepStrictEqual(result, [{
|
|
307
|
+
name: "A",
|
|
308
|
+
id: "a"
|
|
309
|
+
}, {
|
|
310
|
+
name: "B",
|
|
311
|
+
id: "b"
|
|
312
|
+
}]);
|
|
313
|
+
}));
|
|
314
|
+
console.log("\nPassed: ".concat(passed));
|
|
315
|
+
console.log("Failed: ".concat(failed));
|
|
316
|
+
|
|
317
|
+
if (failed > 0) {
|
|
318
|
+
process.exitCode = 1;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
return _run.apply(this, arguments);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
run().catch(error => {
|
|
325
|
+
console.error(error);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
});
|
package/build/utility.js
CHANGED
|
@@ -323,22 +323,17 @@ function buildCoveringIndex(query, options) {
|
|
|
323
323
|
if (allFields.length === 0) {
|
|
324
324
|
// No fields to index besides deleted
|
|
325
325
|
return null;
|
|
326
|
-
} //
|
|
326
|
+
} // Build the field list with deleted INCLUDE MISSING first
|
|
327
327
|
|
|
328
328
|
|
|
329
|
-
var
|
|
330
|
-
|
|
329
|
+
var fieldList = ["`deleted` INCLUDE MISSING", ...allFields.map(f => "`".concat(f, "`"))].join(", "); // Generate unique index name based on bucket, scope, collection, and fields
|
|
330
|
+
// This ensures multiple queries covered by the same index get the same hash
|
|
331
331
|
|
|
332
|
-
var
|
|
332
|
+
var indexKey = "".concat(bucket, "|").concat(scope, "|").concat(collection, "|").concat(allFields.sort().join(","));
|
|
333
|
+
var hash = generateHash(indexKey);
|
|
334
|
+
var indexName = "adv_covered_".concat(hash); // Build the CREATE INDEX statement
|
|
333
335
|
|
|
334
|
-
var indexStatement = "CREATE INDEX `".concat(indexName, "` ON `default`:`").concat(bucket, "`.`").concat(scope, "`.`").concat(collection, "`(").concat(fieldList, ")");
|
|
335
|
-
|
|
336
|
-
console.log("[DEBUG buildCoveringIndex] Original query:", query);
|
|
337
|
-
console.log("[DEBUG buildCoveringIndex] WHERE fields:", filteredWhereFields);
|
|
338
|
-
console.log("[DEBUG buildCoveringIndex] GROUP BY fields:", filteredGroupByFields);
|
|
339
|
-
console.log("[DEBUG buildCoveringIndex] ORDER BY fields:", filteredOrderByFields);
|
|
340
|
-
console.log("[DEBUG buildCoveringIndex] SELECT fields:", selectFieldsFiltered);
|
|
341
|
-
console.log("[DEBUG buildCoveringIndex] Index statement:", indexStatement);
|
|
336
|
+
var indexStatement = "CREATE INDEX `".concat(indexName, "` ON `default`:`").concat(bucket, "`.`").concat(scope, "`.`").concat(collection, "`(").concat(fieldList, ")");
|
|
342
337
|
return indexStatement;
|
|
343
338
|
}
|
|
344
339
|
|
package/build/utility.test.js
CHANGED
|
@@ -586,6 +586,29 @@ test("consistent index name for same query", () => {
|
|
|
586
586
|
var result2 = buildCoveringIndex(query, defaultOptions);
|
|
587
587
|
assert.strictEqual(result1, result2);
|
|
588
588
|
});
|
|
589
|
+
test("same index name for different queries with same fields", () => {
|
|
590
|
+
// Two different queries that would be covered by the same index
|
|
591
|
+
var query1 = "SELECT name, email FROM users WHERE status = 'active'";
|
|
592
|
+
var query2 = "SELECT email, name FROM users WHERE status = 'pending'";
|
|
593
|
+
var result1 = buildCoveringIndex(query1, defaultOptions);
|
|
594
|
+
var result2 = buildCoveringIndex(query2, defaultOptions); // Extract index names
|
|
595
|
+
|
|
596
|
+
var name1 = result1.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
597
|
+
var name2 = result2.match(/CREATE INDEX `(adv_covered_\w+)`/)[1]; // Should have the same index name since they have the same fields
|
|
598
|
+
|
|
599
|
+
assert.strictEqual(name1, name2);
|
|
600
|
+
});
|
|
601
|
+
test("different index name for queries with different fields", () => {
|
|
602
|
+
var query1 = "SELECT name FROM users WHERE status = 'active'";
|
|
603
|
+
var query2 = "SELECT email FROM users WHERE type = 'admin'";
|
|
604
|
+
var result1 = buildCoveringIndex(query1, defaultOptions);
|
|
605
|
+
var result2 = buildCoveringIndex(query2, defaultOptions); // Extract index names
|
|
606
|
+
|
|
607
|
+
var name1 = result1.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
608
|
+
var name2 = result2.match(/CREATE INDEX `(adv_covered_\w+)`/)[1]; // Should have different index names since they have different fields
|
|
609
|
+
|
|
610
|
+
assert.notStrictEqual(name1, name2);
|
|
611
|
+
});
|
|
589
612
|
test("index includes all fields needed for covering", () => {
|
|
590
613
|
var query = "SELECT \n first_name, \n middle_name, \n last_name, \n status, \n constituency, \n division, \n description \n FROM subject \n WHERE (deleted = false OR deleted IS MISSING) \n AND created_at > '2024-01-01'";
|
|
591
614
|
var result = buildCoveringIndex(query, defaultOptions); // Verify structure matches the working example pattern
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -113,10 +113,15 @@ module.exports = class Database {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
async connect(connection_name, config) {
|
|
116
|
-
let
|
|
116
|
+
let connectOptions = {
|
|
117
117
|
username: config.username,
|
|
118
118
|
password: config.password,
|
|
119
|
-
}
|
|
119
|
+
};
|
|
120
|
+
if (config.configProfile) {
|
|
121
|
+
connectOptions.configProfile = config.configProfile;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let cluster = await couchbase.connect(config.nodes, connectOptions);
|
|
120
125
|
let bucket = cluster.bucket(config.bucket);
|
|
121
126
|
let collection;
|
|
122
127
|
let scope_name = this.scope_name || config.scope || "_default";
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("assert");
|
|
4
|
+
const Database = require("./index");
|
|
5
|
+
|
|
6
|
+
let passed = 0;
|
|
7
|
+
let failed = 0;
|
|
8
|
+
|
|
9
|
+
function createDb({ lookupIn, get }) {
|
|
10
|
+
const db = Object.create(Database.prototype);
|
|
11
|
+
const collection = {
|
|
12
|
+
lookupIn,
|
|
13
|
+
get,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
db.use_bucket_name = false;
|
|
17
|
+
db.collection = collection;
|
|
18
|
+
db.connection = collection;
|
|
19
|
+
|
|
20
|
+
return db;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function test(name, fn) {
|
|
24
|
+
try {
|
|
25
|
+
await fn();
|
|
26
|
+
console.log(`✓ ${name}`);
|
|
27
|
+
passed++;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.log(`✗ ${name}`);
|
|
30
|
+
console.log(` Error: ${error.message}`);
|
|
31
|
+
failed++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function run() {
|
|
36
|
+
console.log("\n--- multi_get tests ---");
|
|
37
|
+
|
|
38
|
+
await test("lookupIn success with dotted object path does not fallback to get", async () => {
|
|
39
|
+
let getCalls = 0;
|
|
40
|
+
const db = createDb({
|
|
41
|
+
lookupIn: async () => ({
|
|
42
|
+
content: [{ value: "Alice", error: null }],
|
|
43
|
+
}),
|
|
44
|
+
get: async () => {
|
|
45
|
+
getCalls++;
|
|
46
|
+
return { content: { profile: { name: "Alice" } } };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const result = await db.multi_get(["doc-1"], {
|
|
51
|
+
columns: ["profile.name"],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
assert.deepStrictEqual(result, [
|
|
55
|
+
{
|
|
56
|
+
id: "doc-1",
|
|
57
|
+
"profile.name": "Alice",
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
assert.strictEqual(getCalls, 0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await test("array-mapped dotted path falls back and returns mapped array values", async () => {
|
|
64
|
+
const db = createDb({
|
|
65
|
+
lookupIn: async () => ({
|
|
66
|
+
content: [{ value: undefined, error: new Error("path mismatch") }],
|
|
67
|
+
}),
|
|
68
|
+
get: async () => ({
|
|
69
|
+
content: {
|
|
70
|
+
tags: [{ name: "vip" }, { name: "beta" }],
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await db.multi_get(["doc-1"], {
|
|
76
|
+
columns: ["tags.name"],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
assert.deepStrictEqual(result, [
|
|
80
|
+
{
|
|
81
|
+
id: "doc-1",
|
|
82
|
+
"tags.name": ["vip", "beta"],
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await test("missing dotted fields return typed empties (object and array)", async () => {
|
|
88
|
+
const db = createDb({
|
|
89
|
+
lookupIn: async () => ({
|
|
90
|
+
content: [{ value: undefined, error: new Error("path mismatch") }],
|
|
91
|
+
}),
|
|
92
|
+
get: async () => ({
|
|
93
|
+
content: {
|
|
94
|
+
profile: {},
|
|
95
|
+
tags: [],
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const result = await db.multi_get(["doc-1"], {
|
|
101
|
+
columns: ["profile.details", "tags.name"],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
assert.deepStrictEqual(result, [
|
|
105
|
+
{
|
|
106
|
+
id: "doc-1",
|
|
107
|
+
"profile.details": {},
|
|
108
|
+
"tags.name": [],
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await test("mixed key outcomes keep order and omit missing docs", async () => {
|
|
114
|
+
const db = createDb({
|
|
115
|
+
lookupIn: async (key) => {
|
|
116
|
+
if (key === "a") {
|
|
117
|
+
return { content: [{ value: "Alice", error: null }] };
|
|
118
|
+
}
|
|
119
|
+
if (key === "b") {
|
|
120
|
+
return {
|
|
121
|
+
content: [{ value: undefined, error: new Error("path mismatch") }],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
throw new Error("not found");
|
|
125
|
+
},
|
|
126
|
+
get: async (key) => {
|
|
127
|
+
if (key === "b") {
|
|
128
|
+
return {
|
|
129
|
+
content: {
|
|
130
|
+
tags: [{ name: "vip" }, { name: "beta" }],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
throw new Error("not found");
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const result = await db.multi_get(["a", "b", "c"], {
|
|
139
|
+
columns: ["tags.name"],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
assert.deepStrictEqual(result, [
|
|
143
|
+
{
|
|
144
|
+
id: "a",
|
|
145
|
+
"tags.name": "Alice",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "b",
|
|
149
|
+
"tags.name": ["vip", "beta"],
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await test("non-column multi_get behavior remains unchanged", async () => {
|
|
155
|
+
const db = createDb({
|
|
156
|
+
lookupIn: async () => {
|
|
157
|
+
throw new Error("should not be called");
|
|
158
|
+
},
|
|
159
|
+
get: async (key) => {
|
|
160
|
+
if (key === "missing") {
|
|
161
|
+
throw new Error("not found");
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
value: {
|
|
165
|
+
type: "user",
|
|
166
|
+
name: key.toUpperCase(),
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const result = await db.multi_get(["a", "missing", "b"], {
|
|
173
|
+
keep_type: false,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
assert.deepStrictEqual(result, [
|
|
177
|
+
{
|
|
178
|
+
name: "A",
|
|
179
|
+
id: "a",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "B",
|
|
183
|
+
id: "b",
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
console.log(`\nPassed: ${passed}`);
|
|
189
|
+
console.log(`Failed: ${failed}`);
|
|
190
|
+
|
|
191
|
+
if (failed > 0) {
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
run().catch((error) => {
|
|
197
|
+
console.error(error);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
});
|
package/src/utility.js
CHANGED
|
@@ -342,16 +342,18 @@ function buildCoveringIndex(query, options) {
|
|
|
342
342
|
return null;
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
// Generate unique index name based on query hash
|
|
346
|
-
const hash = generateHash(query);
|
|
347
|
-
const indexName = `adv_covered_${hash}`;
|
|
348
|
-
|
|
349
345
|
// Build the field list with deleted INCLUDE MISSING first
|
|
350
346
|
const fieldList = [
|
|
351
347
|
"`deleted` INCLUDE MISSING",
|
|
352
348
|
...allFields.map((f) => `\`${f}\``),
|
|
353
349
|
].join(", ");
|
|
354
350
|
|
|
351
|
+
// Generate unique index name based on bucket, scope, collection, and fields
|
|
352
|
+
// This ensures multiple queries covered by the same index get the same hash
|
|
353
|
+
const indexKey = `${bucket}|${scope}|${collection}|${allFields.sort().join(",")}`;
|
|
354
|
+
const hash = generateHash(indexKey);
|
|
355
|
+
const indexName = `adv_covered_${hash}`;
|
|
356
|
+
|
|
355
357
|
// Build the CREATE INDEX statement
|
|
356
358
|
const indexStatement = `CREATE INDEX \`${indexName}\` ON \`default\`:\`${bucket}\`.\`${scope}\`.\`${collection}\`(${fieldList})`;
|
|
357
359
|
|
package/src/utility.test.js
CHANGED
|
@@ -706,6 +706,37 @@ test("consistent index name for same query", () => {
|
|
|
706
706
|
assert.strictEqual(result1, result2);
|
|
707
707
|
});
|
|
708
708
|
|
|
709
|
+
test("same index name for different queries with same fields", () => {
|
|
710
|
+
// Two different queries that would be covered by the same index
|
|
711
|
+
const query1 = "SELECT name, email FROM users WHERE status = 'active'";
|
|
712
|
+
const query2 = "SELECT email, name FROM users WHERE status = 'pending'";
|
|
713
|
+
|
|
714
|
+
const result1 = buildCoveringIndex(query1, defaultOptions);
|
|
715
|
+
const result2 = buildCoveringIndex(query2, defaultOptions);
|
|
716
|
+
|
|
717
|
+
// Extract index names
|
|
718
|
+
const name1 = result1.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
719
|
+
const name2 = result2.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
720
|
+
|
|
721
|
+
// Should have the same index name since they have the same fields
|
|
722
|
+
assert.strictEqual(name1, name2);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test("different index name for queries with different fields", () => {
|
|
726
|
+
const query1 = "SELECT name FROM users WHERE status = 'active'";
|
|
727
|
+
const query2 = "SELECT email FROM users WHERE type = 'admin'";
|
|
728
|
+
|
|
729
|
+
const result1 = buildCoveringIndex(query1, defaultOptions);
|
|
730
|
+
const result2 = buildCoveringIndex(query2, defaultOptions);
|
|
731
|
+
|
|
732
|
+
// Extract index names
|
|
733
|
+
const name1 = result1.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
734
|
+
const name2 = result2.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
735
|
+
|
|
736
|
+
// Should have different index names since they have different fields
|
|
737
|
+
assert.notStrictEqual(name1, name2);
|
|
738
|
+
});
|
|
739
|
+
|
|
709
740
|
test("index includes all fields needed for covering", () => {
|
|
710
741
|
const query = `SELECT
|
|
711
742
|
first_name,
|