spice-couchbase 2.0.61 → 2.0.63
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/connectionHealth.js +573 -0
- package/build/connectionHealth.test.js +659 -0
- package/build/index.auto-optimize.test.js +204 -2
- package/build/index.js +341 -43
- package/build/utility.js +65 -0
- package/build/utility.test.js +49 -1
- package/package.json +3 -2
- package/src/connectionHealth.js +529 -0
- package/src/connectionHealth.test.js +412 -0
- package/src/index.auto-optimize.test.js +158 -2
- package/src/index.js +320 -55
- package/src/utility.js +63 -0
- package/src/utility.test.js +69 -0
package/build/index.js
CHANGED
|
@@ -24,6 +24,16 @@ var {
|
|
|
24
24
|
QueryScanConsistency
|
|
25
25
|
} = require("couchbase");
|
|
26
26
|
|
|
27
|
+
var {
|
|
28
|
+
parseResourceList,
|
|
29
|
+
extractStatementResources
|
|
30
|
+
} = require("./utility");
|
|
31
|
+
|
|
32
|
+
var {
|
|
33
|
+
createHealthMonitor,
|
|
34
|
+
openHandles
|
|
35
|
+
} = require("./connectionHealth");
|
|
36
|
+
|
|
27
37
|
if (!Promise.allSettled) {
|
|
28
38
|
Promise.allSettled = promises => Promise.all(promises.map((promise, i) => promise.then(value => ({
|
|
29
39
|
status: "fulfilled",
|
|
@@ -52,8 +62,55 @@ var optimizeState = {
|
|
|
52
62
|
inFlight: 0,
|
|
53
63
|
seen: new Set(),
|
|
54
64
|
cooldownUntil: 0
|
|
65
|
+
}; // Auto cover indexing is opt-in per resource. A resource is the collection a
|
|
66
|
+
// Database instance serves (scoped deployments) or the `type = '...'` literal
|
|
67
|
+
// legacy bucket mode adds to every statement; raw statements also count their
|
|
68
|
+
// FROM / JOIN keyspaces. Names are case-insensitive. A slow statement is
|
|
69
|
+
// optimised when, in order of precedence:
|
|
70
|
+
// 1. the call says so: query_options.auto_optimize (true / false)
|
|
71
|
+
// 2. the instance says so: constructor option or connection config
|
|
72
|
+
// `auto_optimize` (true / false)
|
|
73
|
+
// 3. the application policy: Database.setAutoOptimizePolicy(fn), where
|
|
74
|
+
// fn(resource, context) returns true / false, or
|
|
75
|
+
// undefined to defer to the static lists
|
|
76
|
+
// 4. a static list names a resource it touches: comma-separated
|
|
77
|
+
// SPICE_COUCHBASE_OPTIMIZE_RESOURCES (`*` = every resource, which is the
|
|
78
|
+
// pre-2.0.62 behaviour), or connection config / constructor option
|
|
79
|
+
// `auto_optimize_resources`.
|
|
80
|
+
|
|
81
|
+
var OPTIMIZE_ENABLED_RESOURCES = parseResourceList(process.env.SPICE_COUCHBASE_OPTIMIZE_RESOURCES);
|
|
82
|
+
var autoOptimizePolicy = null; // One health monitor per process, shared by every copy of this module that
|
|
83
|
+
// ends up loaded. See connectionHealth.js.
|
|
84
|
+
|
|
85
|
+
var HEALTH_MONITOR_KEY = Symbol.for("spice-couchbase.health-monitor");
|
|
86
|
+
var health = globalThis[HEALTH_MONITOR_KEY] || (globalThis[HEALTH_MONITOR_KEY] = createHealthMonitor()); // Cluster handles live on the `spice.database` globals, which a reconnect
|
|
87
|
+
// replaces. Instances read them at call time instead of copying them.
|
|
88
|
+
|
|
89
|
+
var HANDLE_GLOBALS = {
|
|
90
|
+
cluster: "clusters",
|
|
91
|
+
connection: "connections",
|
|
92
|
+
scope: "scopes",
|
|
93
|
+
bucket_handle: "buckets"
|
|
55
94
|
};
|
|
56
95
|
|
|
96
|
+
function globalHandle(kind, connection_name) {
|
|
97
|
+
var _spice, _spice$database, _spice$database$HANDL;
|
|
98
|
+
|
|
99
|
+
if (typeof spice === "undefined") return undefined;
|
|
100
|
+
return (_spice = spice) === null || _spice === void 0 ? void 0 : (_spice$database = _spice.database) === null || _spice$database === void 0 ? void 0 : (_spice$database$HANDL = _spice$database[HANDLE_GLOBALS[kind]]) === null || _spice$database$HANDL === void 0 ? void 0 : _spice$database$HANDL[connection_name];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function requestedAutoOptimize(query_options) {
|
|
104
|
+
var _query_options$auto_o;
|
|
105
|
+
|
|
106
|
+
if (!query_options || typeof query_options !== "object" || Array.isArray(query_options)) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
var value = (_query_options$auto_o = query_options.auto_optimize) !== null && _query_options$auto_o !== void 0 ? _query_options$auto_o : query_options.optimize;
|
|
111
|
+
return typeof value === "boolean" ? value : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
57
114
|
function shouldOptimize(query) {
|
|
58
115
|
if (!AUTO_OPTIMIZE_ENABLED) return false;
|
|
59
116
|
if (optimizeState.inFlight >= OPTIMIZE_MAX_INFLIGHT) return false;
|
|
@@ -124,19 +181,18 @@ module.exports = class Database {
|
|
|
124
181
|
collection,
|
|
125
182
|
scope,
|
|
126
183
|
query_scope = "cluster",
|
|
127
|
-
auto_create_collection
|
|
184
|
+
auto_create_collection,
|
|
185
|
+
auto_optimize,
|
|
186
|
+
auto_optimize_resources
|
|
128
187
|
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
129
188
|
|
|
130
189
|
if (connection) {
|
|
131
|
-
var
|
|
190
|
+
var _spice2, _spice2$db;
|
|
132
191
|
|
|
133
192
|
this.connection_name = connection;
|
|
134
|
-
this.connection = spice.database.connections[connection];
|
|
135
193
|
this.query_scope = query_scope;
|
|
136
|
-
this.cluster = spice.database.clusters[connection];
|
|
137
194
|
this.bucket = this.connection._scope._bucket._name;
|
|
138
195
|
this.db_config = spice.config.database.connections[connection];
|
|
139
|
-
this.collection_manager = spice.database.buckets[connection].collections();
|
|
140
196
|
this.auto_create_collection = auto_create_collection || (collection || spice.config.database.connections[connection].collection) != undefined && (collection || spice.config.database.connections[connection].collection) != "default" && (scope || spice.config.database.connections[connection].scope) != "_default";
|
|
141
197
|
this.collection_name = collection || spice.config.database.connections[connection].collection || "_default";
|
|
142
198
|
/* console.log(
|
|
@@ -147,14 +203,11 @@ module.exports = class Database {
|
|
|
147
203
|
);*/
|
|
148
204
|
|
|
149
205
|
this.scope_name = scope || spice.config.database.connections[connection].scope || "_default";
|
|
150
|
-
this.scope = spice.database.scopes[connection];
|
|
151
206
|
this.full_text_index = spice.config.database.connections[connection].full_text_index || full_text_index;
|
|
152
207
|
this.N1qlQuery = couchbase.N1qlQuery;
|
|
153
208
|
this.SearchQuery = couchbase.SearchQuery;
|
|
154
|
-
this.collection = this.scope.collection(this.collection_name);
|
|
155
|
-
this.userManager = this.getUserManager(connection);
|
|
156
209
|
|
|
157
|
-
if (!((
|
|
210
|
+
if (!((_spice2 = spice) === null || _spice2 === void 0 ? void 0 : (_spice2$db = _spice2.db) === null || _spice2$db === void 0 ? void 0 : _spice2$db.collections)) {
|
|
158
211
|
spice.db = spice.db || {};
|
|
159
212
|
spice.db.collections = new Set();
|
|
160
213
|
}
|
|
@@ -163,8 +216,255 @@ module.exports = class Database {
|
|
|
163
216
|
this.upsertCollection();
|
|
164
217
|
}
|
|
165
218
|
|
|
166
|
-
this.use_bucket_name = spice.config.database.connections[this.connection_name].scope == undefined;
|
|
219
|
+
this.use_bucket_name = spice.config.database.connections[this.connection_name].scope == undefined; // Auto cover indexing. See OPTIMIZE_ENABLED_RESOURCES for precedence.
|
|
220
|
+
|
|
221
|
+
var configured_auto_optimize = auto_optimize !== null && auto_optimize !== void 0 ? auto_optimize : this.db_config.auto_optimize;
|
|
222
|
+
this.auto_optimize = typeof configured_auto_optimize === "boolean" ? configured_auto_optimize : undefined;
|
|
223
|
+
this.enabled_resources = parseResourceList([...parseResourceList(this.db_config.auto_optimize_resources), ...parseResourceList(auto_optimize_resources)]);
|
|
224
|
+
}
|
|
225
|
+
} // Handles resolve from the globals on every access, so an instance created
|
|
226
|
+
// before a reconnect uses the new cluster afterwards. Assigning one pins it
|
|
227
|
+
// on that instance instead (tests, custom wiring).
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
pinHandle(kind, value) {
|
|
231
|
+
if (!Object.prototype.hasOwnProperty.call(this, "_pinned_handles")) {
|
|
232
|
+
Object.defineProperty(this, "_pinned_handles", {
|
|
233
|
+
value: {},
|
|
234
|
+
enumerable: false,
|
|
235
|
+
writable: true
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this._pinned_handles[kind] = value;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
pinnedHandle(kind) {
|
|
243
|
+
var pinned = this._pinned_handles;
|
|
244
|
+
return pinned && kind in pinned ? {
|
|
245
|
+
value: pinned[kind]
|
|
246
|
+
} : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
handle(kind) {
|
|
250
|
+
var pinned = this.pinnedHandle(kind);
|
|
251
|
+
return pinned ? pinned.value : globalHandle(kind, this.connection_name);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
get cluster() {
|
|
255
|
+
return this.handle("cluster");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
set cluster(value) {
|
|
259
|
+
this.pinHandle("cluster", value);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
get connection() {
|
|
263
|
+
return this.handle("connection");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
set connection(value) {
|
|
267
|
+
this.pinHandle("connection", value);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
get scope() {
|
|
271
|
+
return this.handle("scope");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
set scope(value) {
|
|
275
|
+
this.pinHandle("scope", value);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
get collection() {
|
|
279
|
+
var _this$_collection_cac;
|
|
280
|
+
|
|
281
|
+
var pinned = this.pinnedHandle("collection");
|
|
282
|
+
if (pinned) return pinned.value;
|
|
283
|
+
var scope = this.scope;
|
|
284
|
+
if (!scope) return undefined; // Collection handles are cheap but not free; cache one per scope object,
|
|
285
|
+
// which changes exactly when the connection is replaced.
|
|
286
|
+
|
|
287
|
+
if (((_this$_collection_cac = this._collection_cache) === null || _this$_collection_cac === void 0 ? void 0 : _this$_collection_cac.scope) !== scope) {
|
|
288
|
+
Object.defineProperty(this, "_collection_cache", {
|
|
289
|
+
value: {
|
|
290
|
+
scope,
|
|
291
|
+
collection: scope.collection(this.collection_name)
|
|
292
|
+
},
|
|
293
|
+
enumerable: false,
|
|
294
|
+
writable: true,
|
|
295
|
+
configurable: true
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return this._collection_cache.collection;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
set collection(value) {
|
|
303
|
+
this.pinHandle("collection", value);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
get collection_manager() {
|
|
307
|
+
var _this$handle;
|
|
308
|
+
|
|
309
|
+
var pinned = this.pinnedHandle("collection_manager");
|
|
310
|
+
if (pinned) return pinned.value;
|
|
311
|
+
return (_this$handle = this.handle("bucket_handle")) === null || _this$handle === void 0 ? void 0 : _this$handle.collections();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
set collection_manager(value) {
|
|
315
|
+
this.pinHandle("collection_manager", value);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
get userManager() {
|
|
319
|
+
var _this$cluster;
|
|
320
|
+
|
|
321
|
+
var pinned = this.pinnedHandle("userManager");
|
|
322
|
+
if (pinned) return pinned.value;
|
|
323
|
+
return (_this$cluster = this.cluster) === null || _this$cluster === void 0 ? void 0 : _this$cluster.users();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
set userManager(value) {
|
|
327
|
+
this.pinHandle("userManager", value);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Reports a finished operation to the health monitor. Only connectivity
|
|
331
|
+
* errors (timeouts, cancelled requests, service not available) count.
|
|
332
|
+
*/
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
recordOutcome(error) {
|
|
336
|
+
health.recordOutcome(this.connection_name, error);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
recordSettled(results) {
|
|
340
|
+
for (var result of results) {
|
|
341
|
+
if ((result === null || result === void 0 ? void 0 : result.status) === "rejected") this.recordOutcome(result.reason);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
observe(operation) {
|
|
346
|
+
var _this = this;
|
|
347
|
+
|
|
348
|
+
return _asyncToGenerator(function* () {
|
|
349
|
+
try {
|
|
350
|
+
return yield operation;
|
|
351
|
+
} catch (error) {
|
|
352
|
+
_this.recordOutcome(error);
|
|
353
|
+
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
})();
|
|
357
|
+
}
|
|
358
|
+
/** Per-connection health: healthy, reconnecting, generation, last error. */
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
static healthStatus() {
|
|
362
|
+
return health.status();
|
|
363
|
+
}
|
|
364
|
+
/** Probes a connection now and reconnects it if the probe fails. */
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
static checkHealth() {
|
|
368
|
+
var connection_name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "default";
|
|
369
|
+
return health.check(connection_name);
|
|
370
|
+
}
|
|
371
|
+
/** Opens a fresh cluster for a connection and swaps it in. */
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
static forceReconnect() {
|
|
375
|
+
var connection_name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "default";
|
|
376
|
+
return health.reconnect(connection_name);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Stops probing and reconnecting and closes replaced clusters still in
|
|
380
|
+
* their grace period. Call before closing `spice.database.clusters` on
|
|
381
|
+
* shutdown so a reconnect cannot race it.
|
|
382
|
+
*/
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
static stopHealthMonitor() {
|
|
386
|
+
return health.stop();
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Lets the application decide per resource whether slow statements may
|
|
390
|
+
* create covering indexes, e.g. from a per-resource setting an operator
|
|
391
|
+
* toggles at runtime. `policy(resource, { query, connection, collection })`
|
|
392
|
+
* returns true or false, or undefined to defer to the static lists. It is
|
|
393
|
+
* synchronous because it runs on the query path. Pass null to clear.
|
|
394
|
+
*/
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
static setAutoOptimizePolicy(policy) {
|
|
398
|
+
if (policy !== null && policy !== undefined && typeof policy !== "function") {
|
|
399
|
+
throw new TypeError("Auto-optimize policy must be a function or null");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
autoOptimizePolicy = policy || null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
static getAutoOptimizePolicy() {
|
|
406
|
+
return autoOptimizePolicy;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Resources a statement touches, plus this instance's collection: that is
|
|
410
|
+
* the keyspace `optimize()` would create the index on.
|
|
411
|
+
*/
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
statementResources(query) {
|
|
415
|
+
var resources = extractStatementResources(query);
|
|
416
|
+
|
|
417
|
+
if (this.collection_name && this.collection_name !== "_default") {
|
|
418
|
+
resources.add(String(this.collection_name).toLowerCase());
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return resources;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* True when a slow statement may create a covering index. See
|
|
425
|
+
* OPTIMIZE_ENABLED_RESOURCES for the order of precedence.
|
|
426
|
+
*/
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
shouldAutoOptimize(query) {
|
|
430
|
+
var query_options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
431
|
+
var requested = requestedAutoOptimize(query_options);
|
|
432
|
+
if (requested !== undefined) return requested;
|
|
433
|
+
if (typeof this.auto_optimize === "boolean") return this.auto_optimize;
|
|
434
|
+
var resources = this.statementResources(query);
|
|
435
|
+
|
|
436
|
+
if (typeof autoOptimizePolicy === "function" && resources.size > 0) {
|
|
437
|
+
var deferred = false;
|
|
438
|
+
|
|
439
|
+
for (var resource of resources) {
|
|
440
|
+
var verdict = void 0;
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
verdict = autoOptimizePolicy(resource, {
|
|
444
|
+
query,
|
|
445
|
+
connection: this.connection_name,
|
|
446
|
+
collection: this.collection_name
|
|
447
|
+
});
|
|
448
|
+
} catch (e) {
|
|
449
|
+
// A broken policy must not create indexes nobody asked for.
|
|
450
|
+
verdict = false;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (verdict === true) return true;
|
|
454
|
+
if (verdict === undefined || verdict === null) deferred = true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!deferred) return false;
|
|
167
458
|
}
|
|
459
|
+
|
|
460
|
+
var enabled = new Set([...OPTIMIZE_ENABLED_RESOURCES, ...(this.enabled_resources || [])]);
|
|
461
|
+
if (enabled.has("*")) return true;
|
|
462
|
+
|
|
463
|
+
for (var _resource of resources) {
|
|
464
|
+
if (enabled.has(_resource)) return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return false;
|
|
168
468
|
}
|
|
169
469
|
|
|
170
470
|
setGlobalConnectionObject(collection) {
|
|
@@ -189,25 +489,15 @@ module.exports = class Database {
|
|
|
189
489
|
}
|
|
190
490
|
|
|
191
491
|
connect(connection_name, config) {
|
|
192
|
-
var _this = this;
|
|
193
|
-
|
|
194
492
|
return _asyncToGenerator(function* () {
|
|
195
|
-
var
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
connectOptions.configProfile = config.configProfile;
|
|
202
|
-
}
|
|
493
|
+
var {
|
|
494
|
+
cluster,
|
|
495
|
+
bucket,
|
|
496
|
+
scope,
|
|
497
|
+
collection
|
|
498
|
+
} = yield openHandles(config); // Watch the connection from here on and replace it if it wedges.
|
|
203
499
|
|
|
204
|
-
|
|
205
|
-
var bucket = cluster.bucket(config.bucket);
|
|
206
|
-
var collection;
|
|
207
|
-
var scope_name = _this.scope_name || config.scope || "_default";
|
|
208
|
-
var collection_name = _this.collection_name || config.collection || "_default";
|
|
209
|
-
var scope = bucket.scope(scope_name);
|
|
210
|
-
collection = bucket.scope(scope_name).collection(collection_name || "_default");
|
|
500
|
+
health.register(connection_name, config);
|
|
211
501
|
return {
|
|
212
502
|
connection: connection_name,
|
|
213
503
|
bucket,
|
|
@@ -219,7 +509,7 @@ module.exports = class Database {
|
|
|
219
509
|
}
|
|
220
510
|
|
|
221
511
|
getUserManager(connection) {
|
|
222
|
-
return
|
|
512
|
+
return globalHandle("cluster", connection).users();
|
|
223
513
|
}
|
|
224
514
|
|
|
225
515
|
assignUserPermission() {
|
|
@@ -292,9 +582,9 @@ module.exports = class Database {
|
|
|
292
582
|
|
|
293
583
|
return _asyncToGenerator(function* () {
|
|
294
584
|
try {
|
|
295
|
-
var
|
|
585
|
+
var _spice3, _spice3$db, _spice3$db$collection;
|
|
296
586
|
|
|
297
|
-
if ((
|
|
587
|
+
if ((_spice3 = spice) === null || _spice3 === void 0 ? void 0 : (_spice3$db = _spice3.db) === null || _spice3$db === void 0 ? void 0 : (_spice3$db$collection = _spice3$db.collections) === null || _spice3$db$collection === void 0 ? void 0 : _spice3$db$collection.has(collection_name)) {
|
|
298
588
|
return true;
|
|
299
589
|
} else {
|
|
300
590
|
var response = yield _this4.cluster.query("SELECT RAW keyspace_id\n FROM system:keyspaces\n WHERE bucket_id = ?\n AND scope_id = ?\n AND keyspace_id = ?\n LIMIT 1", {
|
|
@@ -405,7 +695,7 @@ module.exports = class Database {
|
|
|
405
695
|
var collection = _this9.use_bucket_name ? _this9.connection : _this9.collection;
|
|
406
696
|
|
|
407
697
|
if (!useLookupIn) {
|
|
408
|
-
var response = yield collection.get(doc_id);
|
|
698
|
+
var response = yield _this9.observe(collection.get(doc_id));
|
|
409
699
|
return _objectSpread({
|
|
410
700
|
id: doc_id
|
|
411
701
|
}, response.content);
|
|
@@ -423,7 +713,10 @@ module.exports = class Database {
|
|
|
423
713
|
var specs = batch.map(col => couchbase.LookupInSpec.get(col));
|
|
424
714
|
return collection.lookupIn(doc_id, specs);
|
|
425
715
|
});
|
|
426
|
-
var batchResults = yield Promise.allSettled(batchPromises);
|
|
716
|
+
var batchResults = yield Promise.allSettled(batchPromises);
|
|
717
|
+
|
|
718
|
+
_this9.recordSettled(batchResults); // Merge results from all batches
|
|
719
|
+
|
|
427
720
|
|
|
428
721
|
batchResults.forEach((result, batchIndex) => {
|
|
429
722
|
if (result.status === "fulfilled" && result.value) {
|
|
@@ -447,9 +740,9 @@ module.exports = class Database {
|
|
|
447
740
|
|
|
448
741
|
return _asyncToGenerator(function* () {
|
|
449
742
|
try {
|
|
450
|
-
yield (_this10.use_bucket_name ? _this10.connection : _this10.collection).insert(doc_id, document, {
|
|
743
|
+
yield _this10.observe((_this10.use_bucket_name ? _this10.connection : _this10.collection).insert(doc_id, document, {
|
|
451
744
|
expiry: expiry
|
|
452
|
-
});
|
|
745
|
+
}));
|
|
453
746
|
} catch (e) {
|
|
454
747
|
console.log(e.message, e, doc_id);
|
|
455
748
|
}
|
|
@@ -463,7 +756,7 @@ module.exports = class Database {
|
|
|
463
756
|
|
|
464
757
|
var expiry = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
|
|
465
758
|
return _asyncToGenerator(function* () {
|
|
466
|
-
yield (_this11.use_bucket_name ? _this11.connection : _this11.collection).touch(doc_id, expiry);
|
|
759
|
+
yield _this11.observe((_this11.use_bucket_name ? _this11.connection : _this11.collection).touch(doc_id, expiry));
|
|
467
760
|
return yield _this11.get(doc_id);
|
|
468
761
|
})();
|
|
469
762
|
}
|
|
@@ -472,9 +765,9 @@ module.exports = class Database {
|
|
|
472
765
|
var _this12 = this;
|
|
473
766
|
|
|
474
767
|
return _asyncToGenerator(function* () {
|
|
475
|
-
var res = yield (_this12.use_bucket_name ? _this12.connection : _this12.collection).counter(doc_id, adjustment, {
|
|
768
|
+
var res = yield _this12.observe((_this12.use_bucket_name ? _this12.connection : _this12.collection).counter(doc_id, adjustment, {
|
|
476
769
|
initial: initial_value
|
|
477
|
-
});
|
|
770
|
+
}));
|
|
478
771
|
return res.value;
|
|
479
772
|
})();
|
|
480
773
|
}
|
|
@@ -483,9 +776,9 @@ module.exports = class Database {
|
|
|
483
776
|
var _this13 = this;
|
|
484
777
|
|
|
485
778
|
return _asyncToGenerator(function* () {
|
|
486
|
-
var res = yield (_this13.use_bucket_name ? _this13.connection : _this13.collection).replace(doc_id, document, {
|
|
779
|
+
var res = yield _this13.observe((_this13.use_bucket_name ? _this13.connection : _this13.collection).replace(doc_id, document, {
|
|
487
780
|
expiry: expiry
|
|
488
|
-
});
|
|
781
|
+
}));
|
|
489
782
|
return res.value;
|
|
490
783
|
})();
|
|
491
784
|
}
|
|
@@ -494,7 +787,7 @@ module.exports = class Database {
|
|
|
494
787
|
var _this14 = this;
|
|
495
788
|
|
|
496
789
|
return _asyncToGenerator(function* () {
|
|
497
|
-
var res = yield (_this14.use_bucket_name ? _this14.connection : _this14.collection).remove(doc_id);
|
|
790
|
+
var res = yield _this14.observe((_this14.use_bucket_name ? _this14.connection : _this14.collection).remove(doc_id));
|
|
498
791
|
return res.value;
|
|
499
792
|
})();
|
|
500
793
|
}
|
|
@@ -521,6 +814,8 @@ module.exports = class Database {
|
|
|
521
814
|
|
|
522
815
|
var returned_all = yield Promise.allSettled(functions);
|
|
523
816
|
|
|
817
|
+
_this15.recordSettled(returned_all);
|
|
818
|
+
|
|
524
819
|
var responses = _.flatten(_.map(returned_all, returned_obj => returned_obj === null || returned_obj === void 0 ? void 0 : returned_obj.value));
|
|
525
820
|
|
|
526
821
|
var _final_responses = _.compact(_.map(responses, (res, index) => {
|
|
@@ -559,7 +854,10 @@ module.exports = class Database {
|
|
|
559
854
|
var specs = batch.map(col => couchbase.LookupInSpec.get(col));
|
|
560
855
|
return collection.lookupIn(key, specs);
|
|
561
856
|
});
|
|
562
|
-
var batchResults = yield Promise.allSettled(batchPromises);
|
|
857
|
+
var batchResults = yield Promise.allSettled(batchPromises);
|
|
858
|
+
|
|
859
|
+
_this15.recordSettled(batchResults); // Merge results from all batches
|
|
860
|
+
|
|
563
861
|
|
|
564
862
|
batchResults.forEach((result, batchIndex) => {
|
|
565
863
|
if (result.status === "fulfilled" && result.value) {
|
|
@@ -986,14 +1284,14 @@ module.exports = class Database {
|
|
|
986
1284
|
|
|
987
1285
|
var startTime = process.hrtime.bigint();
|
|
988
1286
|
var options = buildQueryOptions(query_options);
|
|
989
|
-
var response = yield exec_scope.query(sanitizedQuery, options); // End time
|
|
1287
|
+
var response = yield _this20.observe(exec_scope.query(sanitizedQuery, options)); // End time
|
|
990
1288
|
|
|
991
1289
|
var endTime = process.hrtime.bigint(); // Calculate duration in microseconds
|
|
992
1290
|
|
|
993
1291
|
var durationInMilliseconds = (endTime - startTime) / BigInt(1000000); // Optimise slow statements in the background, subject to the guard rails
|
|
994
1292
|
// declared at the top of this file. Never awaited, never allowed to reject.
|
|
995
1293
|
|
|
996
|
-
if (durationInMilliseconds > SLOW_QUERY_MS && shouldOptimize(sanitizedQuery)) {
|
|
1294
|
+
if (durationInMilliseconds > SLOW_QUERY_MS && _this20.shouldAutoOptimize(sanitizedQuery, query_options) && shouldOptimize(sanitizedQuery)) {
|
|
997
1295
|
_this20.optimize(sanitizedQuery, exec_scope).catch(() => {});
|
|
998
1296
|
}
|
|
999
1297
|
|
package/build/utility.js
CHANGED
|
@@ -435,11 +435,76 @@ function indexSatisfiesCoveringDefinition(index, candidate) {
|
|
|
435
435
|
var existingFields = new Set(existingKeys.map(key => key.field));
|
|
436
436
|
return requiredKeys.slice(candidate.accessKeyCount).every(key => existingFields.has(key.field));
|
|
437
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Normalizes an exemption list into a Set of lower-case resource names.
|
|
440
|
+
* Accepts an array, a Set, or a comma-separated string (the env var form).
|
|
441
|
+
* Surrounding whitespace and backticks are ignored; empty entries are dropped.
|
|
442
|
+
* @param {string|string[]|Set<string>} value
|
|
443
|
+
* @returns {Set<string>}
|
|
444
|
+
*/
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
function parseResourceList(value) {
|
|
448
|
+
var names = new Set();
|
|
449
|
+
var items = [];
|
|
450
|
+
|
|
451
|
+
if (Array.isArray(value) || value instanceof Set) {
|
|
452
|
+
items = [...value];
|
|
453
|
+
} else if (typeof value === "string") {
|
|
454
|
+
items = value.split(",");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
for (var item of items) {
|
|
458
|
+
if (typeof item !== "string") continue;
|
|
459
|
+
var name = item.trim().replace(/^`+|`+$/g, "").trim().toLowerCase();
|
|
460
|
+
if (name) names.add(name);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return names;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Extracts the resource names a statement touches, lower-cased:
|
|
467
|
+
* - the last path segment of every FROM / JOIN keyspace, unquoted, so
|
|
468
|
+
* `default`:`bucket`.`scope`.`users` and users both yield "users";
|
|
469
|
+
* - every `type = '...'` literal, which is how legacy bucket-mode
|
|
470
|
+
* deployments distinguish resources that share one keyspace.
|
|
471
|
+
* Only simple identifiers are recognised; subqueries and expressions are skipped.
|
|
472
|
+
* @param {string} query
|
|
473
|
+
* @returns {Set<string>}
|
|
474
|
+
*/
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
function extractStatementResources(query) {
|
|
478
|
+
var names = new Set();
|
|
479
|
+
if (typeof query !== "string") return names;
|
|
480
|
+
var identifier = "(?:`(?:[^`]|``)+`|[A-Za-z_][\\w$-]*)";
|
|
481
|
+
var keyspacePattern = new RegExp("\\b(?:FROM|JOIN)\\s+(".concat(identifier, "(?:\\s*[.:]\\s*").concat(identifier, ")*)"), "gi");
|
|
482
|
+
var segmentPattern = new RegExp(identifier, "g");
|
|
483
|
+
|
|
484
|
+
for (var match of query.matchAll(keyspacePattern)) {
|
|
485
|
+
var segments = match[1].match(segmentPattern) || [];
|
|
486
|
+
var last = (segments[segments.length - 1] || "").trim();
|
|
487
|
+
var name = last.startsWith("`") ? last.slice(1, -1).replace(/``/g, "`") : last;
|
|
488
|
+
if (name) names.add(name.toLowerCase());
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
var typePattern = /(?:`type`|\btype)\s*=\s*'((?:[^']|'')*)'/gi;
|
|
492
|
+
|
|
493
|
+
for (var _match of query.matchAll(typePattern)) {
|
|
494
|
+
var _name = _match[1].replace(/''/g, "'").trim();
|
|
495
|
+
|
|
496
|
+
if (_name) names.add(_name.toLowerCase());
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return names;
|
|
500
|
+
}
|
|
438
501
|
|
|
439
502
|
module.exports = {
|
|
440
503
|
buildCoveringIndex,
|
|
441
504
|
buildCoveringIndexDefinition,
|
|
442
505
|
indexSatisfiesCoveringDefinition,
|
|
506
|
+
parseResourceList,
|
|
507
|
+
extractStatementResources,
|
|
443
508
|
extractWhereFields,
|
|
444
509
|
extractSelectFields,
|
|
445
510
|
extractGroupByFields,
|
package/build/utility.test.js
CHANGED
|
@@ -8,7 +8,9 @@ var {
|
|
|
8
8
|
extractSelectFields,
|
|
9
9
|
extractGroupByFields,
|
|
10
10
|
extractOrderByFields,
|
|
11
|
-
generateHash
|
|
11
|
+
generateHash,
|
|
12
|
+
parseResourceList,
|
|
13
|
+
extractStatementResources
|
|
12
14
|
} = require("./utility"); // Test utilities
|
|
13
15
|
|
|
14
16
|
|
|
@@ -825,6 +827,52 @@ test("field order: WHERE -> GROUP BY -> ORDER BY -> SELECT", () => {
|
|
|
825
827
|
|
|
826
828
|
assert.ok(createdAtIndex < emailIndex); // ORDER BY before SELECT
|
|
827
829
|
}); // ============================================
|
|
830
|
+
// Tests for parseResourceList / extractStatementResources
|
|
831
|
+
// ============================================
|
|
832
|
+
|
|
833
|
+
console.log("\n--- parseResourceList tests ---");
|
|
834
|
+
test("parses a comma-separated string, trimming, unquoting and lower-casing", () => {
|
|
835
|
+
var result = parseResourceList(" Users, `Audit_Log` ,, sessions ");
|
|
836
|
+
assert.deepStrictEqual([...result].sort(), ["audit_log", "sessions", "users"]);
|
|
837
|
+
});
|
|
838
|
+
test("accepts arrays and Sets and ignores non-strings", () => {
|
|
839
|
+
assert.deepStrictEqual([...parseResourceList(["Users", 42, null, " logs "])].sort(), ["logs", "users"]);
|
|
840
|
+
assert.deepStrictEqual([...parseResourceList(new Set(["A", "b"]))].sort(), ["a", "b"]);
|
|
841
|
+
});
|
|
842
|
+
test("returns an empty set for undefined, empty or unsupported input", () => {
|
|
843
|
+
assert.strictEqual(parseResourceList(undefined).size, 0);
|
|
844
|
+
assert.strictEqual(parseResourceList("").size, 0);
|
|
845
|
+
assert.strictEqual(parseResourceList(", ,").size, 0);
|
|
846
|
+
assert.strictEqual(parseResourceList({
|
|
847
|
+
users: true
|
|
848
|
+
}).size, 0);
|
|
849
|
+
});
|
|
850
|
+
console.log("\n--- extractStatementResources tests ---");
|
|
851
|
+
test("extracts a bare FROM keyspace", () => {
|
|
852
|
+
assert.deepStrictEqual([...extractStatementResources("SELECT name FROM users WHERE x = 1")], ["users"]);
|
|
853
|
+
});
|
|
854
|
+
test("extracts the last segment of a qualified or quoted keyspace", () => {
|
|
855
|
+
assert.deepStrictEqual([...extractStatementResources("SELECT name FROM `default`:`saturn`.`app`.`Users` AS u")], ["users"]);
|
|
856
|
+
assert.deepStrictEqual([...extractStatementResources("SELECT name FROM saturn.app.logs")], ["logs"]);
|
|
857
|
+
});
|
|
858
|
+
test("extracts JOIN keyspaces alongside FROM", () => {
|
|
859
|
+
var result = extractStatementResources("SELECT u.name FROM users AS u JOIN `orders` AS o ON o.user_id = META(u).id");
|
|
860
|
+
assert.deepStrictEqual([...result].sort(), ["orders", "users"]);
|
|
861
|
+
});
|
|
862
|
+
test("extracts type literals used by legacy bucket mode", () => {
|
|
863
|
+
var result = extractStatementResources("SELECT actor FROM `saturn` WHERE type = 'Audit_Log' AND deleted = false");
|
|
864
|
+
assert.deepStrictEqual([...result].sort(), ["audit_log", "saturn"]);
|
|
865
|
+
assert.ok(extractStatementResources("SELECT a FROM b WHERE `type`='x'").has("x"));
|
|
866
|
+
assert.ok(extractStatementResources("SELECT a FROM b WHERE type = 'it''s'").has("it's"));
|
|
867
|
+
});
|
|
868
|
+
test("does not treat META() arguments, subqueries or column names as keyspaces", () => {
|
|
869
|
+
var result = extractStatementResources("SELECT META(`users`).id AS id, from_date FROM (SELECT * FROM users) AS sub WHERE status = 'from'");
|
|
870
|
+
assert.deepStrictEqual([...result], ["users"]);
|
|
871
|
+
});
|
|
872
|
+
test("handles non-string input", () => {
|
|
873
|
+
assert.strictEqual(extractStatementResources(undefined).size, 0);
|
|
874
|
+
assert.strictEqual(extractStatementResources(null).size, 0);
|
|
875
|
+
}); // ============================================
|
|
828
876
|
// Summary
|
|
829
877
|
// ============================================
|
|
830
878
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spice-couchbase",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.63",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "node src/utility.test.js",
|
|
8
8
|
"test:auto-optimize": "node --test src/index.auto-optimize.test.js",
|
|
9
9
|
"test:query-consistency": "node src/index.query-consistency.test.js",
|
|
10
|
-
"test:count-query": "node src/index.count-query.test.js"
|
|
10
|
+
"test:count-query": "node src/index.count-query.test.js",
|
|
11
|
+
"test:connection-health": "node --test src/connectionHealth.test.js"
|
|
11
12
|
},
|
|
12
13
|
"author": "Chad Fraser <chad@sonover.com> (http://sonover.com)",
|
|
13
14
|
"license": "ISC",
|