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.
@@ -0,0 +1,412 @@
1
+ "use strict";
2
+
3
+ const assert = require("node:assert/strict");
4
+ const { test } = require("node:test");
5
+ const couchbase = require("couchbase");
6
+ const {
7
+ createHealthMonitor,
8
+ buildConnectOptions,
9
+ isConnectivityError,
10
+ } = require("./connectionHealth");
11
+
12
+ const config = {
13
+ nodes: "couchbase://db",
14
+ bucket: "saturn",
15
+ scope: "app",
16
+ username: "u",
17
+ password: "p",
18
+ };
19
+
20
+ const timeoutError = () => new couchbase.UnambiguousTimeoutError();
21
+
22
+ // A fake cluster whose probe behaviour can be flipped between healthy and
23
+ // wedged. `hang` makes operations never settle, like a stuck SDK.
24
+ function fakeCluster(id, { healthy = true, hang = false } = {}) {
25
+ const cluster = {
26
+ id,
27
+ healthy,
28
+ hang,
29
+ closed: 0,
30
+ queries: 0,
31
+ async close() {
32
+ cluster.closed += 1;
33
+ },
34
+ query() {
35
+ cluster.queries += 1;
36
+ if (cluster.hang) return new Promise(() => {});
37
+ return cluster.healthy ?
38
+ Promise.resolve({ rows: [{ $1: 1 }] })
39
+ : Promise.reject(timeoutError());
40
+ },
41
+ bucket(name) {
42
+ const bucket = {
43
+ name,
44
+ cluster,
45
+ async waitUntilReady() {
46
+ if (!cluster.healthy) throw timeoutError();
47
+ },
48
+ scope(scopeName) {
49
+ return {
50
+ name: scopeName,
51
+ bucket,
52
+ collection(collectionName) {
53
+ return {
54
+ name: collectionName,
55
+ cluster,
56
+ exists() {
57
+ if (cluster.hang) return new Promise(() => {});
58
+ return cluster.healthy ?
59
+ Promise.resolve({ exists: false })
60
+ : Promise.reject(timeoutError());
61
+ },
62
+ };
63
+ },
64
+ };
65
+ },
66
+ };
67
+ return bucket;
68
+ },
69
+ };
70
+ return cluster;
71
+ }
72
+
73
+ function handlesFor(cluster) {
74
+ const bucket = cluster.bucket(config.bucket);
75
+ const scope = bucket.scope(config.scope);
76
+ return { cluster, bucket, scope, collection: scope.collection("_default") };
77
+ }
78
+
79
+ function setup({ settings = {}, connect, clock } = {}) {
80
+ const initial = fakeCluster("initial");
81
+ const handles = handlesFor(initial);
82
+ const globals = {
83
+ database: {
84
+ clusters: { default: handles.cluster },
85
+ buckets: { default: handles.bucket },
86
+ scopes: { default: handles.scope },
87
+ connections: { default: handles.collection },
88
+ },
89
+ checked_collections: { users: true },
90
+ db: { collections: new Set(["users"]) },
91
+ };
92
+ const opened = [];
93
+ const time = clock || { now: 1000 };
94
+ const terminated = [];
95
+ const logs = [];
96
+ const log = {
97
+ log: (m) => logs.push(m),
98
+ warn: (m) => logs.push(m),
99
+ error: (m) => logs.push(m),
100
+ };
101
+ const monitor = createHealthMonitor({
102
+ settings: {
103
+ healthIntervalMs: 0,
104
+ probeTimeoutMs: 20,
105
+ probeFailureThreshold: 2,
106
+ failureThreshold: 3,
107
+ failureWindowMs: 1000,
108
+ reconnectTimeoutMs: 50,
109
+ reconnectBackoffMinMs: 100,
110
+ reconnectBackoffMaxMs: 1000,
111
+ retireGraceMs: 5,
112
+ exitAfterMs: 10000,
113
+ ...settings,
114
+ },
115
+ connect:
116
+ connect ||
117
+ (async () => {
118
+ const cluster = fakeCluster(`fresh-${opened.length + 1}`);
119
+ opened.push(cluster);
120
+ return cluster;
121
+ }),
122
+ now: () => time.now,
123
+ random: () => 0.5,
124
+ log,
125
+ getGlobals: () => globals,
126
+ terminate: () => terminated.push(time.now),
127
+ });
128
+ monitor.register("default", config);
129
+ return { monitor, initial, globals, opened, time, terminated, logs };
130
+ }
131
+
132
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
133
+
134
+ test("only connectivity errors count", () => {
135
+ assert.equal(isConnectivityError(timeoutError()), true);
136
+ assert.equal(isConnectivityError(new couchbase.AmbiguousTimeoutError()), true);
137
+ assert.equal(isConnectivityError(new couchbase.RequestCanceledError()), true);
138
+ assert.equal(isConnectivityError(new couchbase.ServiceNotAvailableError()), true);
139
+ assert.equal(isConnectivityError(new couchbase.DocumentNotFoundError()), false);
140
+ assert.equal(isConnectivityError(new Error("syntax error")), false);
141
+ assert.equal(isConnectivityError(undefined), false);
142
+ });
143
+
144
+ test("connect options carry only the timeouts that are set", () => {
145
+ assert.deepEqual(buildConnectOptions(config), { username: "u", password: "p" });
146
+ assert.deepEqual(
147
+ buildConnectOptions({
148
+ ...config,
149
+ configProfile: "wanDevelopment",
150
+ timeouts: { queryTimeout: "30000", kvTimeout: undefined, connectTimeout: "" },
151
+ }),
152
+ {
153
+ username: "u",
154
+ password: "p",
155
+ configProfile: "wanDevelopment",
156
+ timeouts: { queryTimeout: 30000 },
157
+ },
158
+ );
159
+ });
160
+
161
+ test("a healthy probe leaves the connection alone", async () => {
162
+ const { monitor, opened, globals, initial } = setup();
163
+ assert.equal(await monitor.check("default"), true);
164
+ assert.equal(opened.length, 0);
165
+ assert.equal(globals.database.clusters.default, initial);
166
+ assert.equal(monitor.status().default.healthy, true);
167
+ });
168
+
169
+ test("non-connectivity errors never trigger a probe", async () => {
170
+ const { monitor, initial } = setup();
171
+ for (let i = 0; i < 10; i++) {
172
+ monitor.recordOutcome("default", new couchbase.DocumentNotFoundError());
173
+ }
174
+ await sleep(5);
175
+ assert.equal(initial.queries, 0);
176
+ });
177
+
178
+ test("one failed probe is not enough, two trigger a reconnect", async () => {
179
+ const { monitor, initial, opened } = setup();
180
+ initial.healthy = false;
181
+ await monitor.check("default");
182
+ assert.equal(opened.length, 0);
183
+ assert.equal(monitor.status().default.healthy, true);
184
+ await monitor.check("default");
185
+ assert.equal(opened.length, 1);
186
+ assert.equal(monitor.status().default.healthy, true);
187
+ assert.equal(monitor.status().default.generation, 2);
188
+ });
189
+
190
+ test("a burst of operation timeouts probes and reconnects at once", async () => {
191
+ const { monitor, initial, opened, globals } = setup();
192
+ initial.healthy = false;
193
+ for (let i = 0; i < 3; i++) monitor.recordOutcome("default", timeoutError());
194
+ await sleep(10);
195
+ assert.equal(opened.length, 1);
196
+ assert.equal(globals.database.clusters.default, opened[0]);
197
+ });
198
+
199
+ test("timeouts outside the window do not add up", async () => {
200
+ const { monitor, initial, opened, time } = setup();
201
+ initial.healthy = false;
202
+ monitor.recordOutcome("default", timeoutError());
203
+ monitor.recordOutcome("default", timeoutError());
204
+ time.now += 2000;
205
+ monitor.recordOutcome("default", timeoutError());
206
+ await sleep(10);
207
+ assert.equal(initial.queries, 0);
208
+ assert.equal(opened.length, 0);
209
+ });
210
+
211
+ test("reconnect swaps every global handle and resets collection caches", async () => {
212
+ const { monitor, initial, opened, globals } = setup();
213
+ initial.healthy = false;
214
+ await monitor.check("default");
215
+ await monitor.check("default");
216
+ const fresh = opened[0];
217
+ assert.equal(globals.database.clusters.default, fresh);
218
+ assert.equal(globals.database.buckets.default.cluster, fresh);
219
+ assert.equal(globals.database.scopes.default.bucket.cluster, fresh);
220
+ assert.equal(globals.database.connections.default.cluster, fresh);
221
+ assert.deepEqual(globals.checked_collections, {});
222
+ assert.equal(globals.db.collections.size, 0);
223
+ });
224
+
225
+ test("the replaced cluster is closed after the grace period", async () => {
226
+ const { monitor, initial } = setup();
227
+ initial.healthy = false;
228
+ await monitor.check("default");
229
+ await monitor.check("default");
230
+ assert.equal(initial.closed, 0);
231
+ await sleep(20);
232
+ assert.equal(initial.closed, 1);
233
+ });
234
+
235
+ test("concurrent triggers share a single reconnect", async () => {
236
+ let release;
237
+ const opened = [];
238
+ const { monitor, initial } = setup({
239
+ settings: { probeFailureThreshold: 1, reconnectTimeoutMs: 1000 },
240
+ connect: () =>
241
+ new Promise((resolve) => {
242
+ release = () => {
243
+ const cluster = fakeCluster("fresh");
244
+ opened.push(cluster);
245
+ resolve(cluster);
246
+ };
247
+ }),
248
+ });
249
+ initial.healthy = false;
250
+ const first = monitor.check("default");
251
+ await sleep(5);
252
+ const others = [monitor.check("default"), monitor.reconnect("default")];
253
+ for (let i = 0; i < 5; i++) monitor.recordOutcome("default", timeoutError());
254
+ release();
255
+ await Promise.all([first, ...others]);
256
+ assert.equal(opened.length, 1);
257
+ assert.equal(monitor.status().default.generation, 2);
258
+ });
259
+
260
+ test("a probe that hangs is cut off by the hard deadline", async () => {
261
+ const { monitor, initial, opened } = setup({
262
+ settings: { probeFailureThreshold: 1 },
263
+ });
264
+ initial.hang = true;
265
+ // The deadline timer is unref'd; keep the loop alive while the probe hangs.
266
+ const keepAlive = setInterval(() => {}, 1000);
267
+ try {
268
+ const started = Date.now();
269
+ await monitor.check("default");
270
+ assert.ok(Date.now() - started < 1500);
271
+ assert.equal(opened.length, 1);
272
+ } finally {
273
+ clearInterval(keepAlive);
274
+ }
275
+ });
276
+
277
+ test("a failed reconnect backs off and keeps the old cluster", async () => {
278
+ const opened = [];
279
+ const { monitor, initial, globals, time } = setup({
280
+ settings: { probeFailureThreshold: 1 },
281
+ connect: async () => {
282
+ const cluster = fakeCluster(`bad-${opened.length + 1}`, { healthy: false });
283
+ opened.push(cluster);
284
+ return cluster;
285
+ },
286
+ });
287
+ initial.healthy = false;
288
+ await monitor.check("default");
289
+ assert.equal(opened.length, 1);
290
+ assert.equal(opened[0].closed, 1, "unverified cluster is closed");
291
+ assert.equal(globals.database.clusters.default, initial);
292
+ assert.equal(monitor.status().default.reconnectAttempts, 1);
293
+
294
+ // Inside the backoff window: probe only, no new connect.
295
+ await monitor.check("default");
296
+ assert.equal(opened.length, 1);
297
+
298
+ time.now += 150;
299
+ await monitor.check("default");
300
+ assert.equal(opened.length, 2);
301
+ assert.equal(monitor.status().default.reconnectAttempts, 2);
302
+ });
303
+
304
+ test("a connection that recovers on its own is marked healthy again", async () => {
305
+ const { monitor, initial, time } = setup({
306
+ settings: { probeFailureThreshold: 1 },
307
+ connect: async () => fakeCluster("bad", { healthy: false }),
308
+ });
309
+ initial.healthy = false;
310
+ await monitor.check("default");
311
+ assert.equal(monitor.status().default.healthy, false);
312
+ initial.healthy = true;
313
+ time.now += 5000;
314
+ await monitor.check("default");
315
+ const status = monitor.status().default;
316
+ assert.equal(status.healthy, true);
317
+ assert.equal(status.reconnectAttempts, 0);
318
+ assert.equal(status.unhealthySince, null);
319
+ });
320
+
321
+ test("staying unhealthy past the limit terminates the process once", async () => {
322
+ const { monitor, initial, time, terminated } = setup({
323
+ settings: { probeFailureThreshold: 1, exitAfterMs: 1000 },
324
+ connect: async () => fakeCluster("bad", { healthy: false }),
325
+ });
326
+ initial.healthy = false;
327
+ await monitor.check("default");
328
+ assert.equal(terminated.length, 0);
329
+ time.now += 1500;
330
+ await monitor.check("default");
331
+ time.now += 1500;
332
+ await monitor.check("default");
333
+ assert.equal(terminated.length, 1);
334
+ });
335
+
336
+ test("exitAfterMs of 0 never terminates", async () => {
337
+ const { monitor, initial, time, terminated } = setup({
338
+ settings: { probeFailureThreshold: 1, exitAfterMs: 0 },
339
+ connect: async () => fakeCluster("bad", { healthy: false }),
340
+ });
341
+ initial.healthy = false;
342
+ for (let i = 0; i < 5; i++) {
343
+ time.now += 100000;
344
+ await monitor.check("default");
345
+ }
346
+ assert.equal(terminated.length, 0);
347
+ });
348
+
349
+ test("stop() halts reconnects and closes retired clusters", async () => {
350
+ const { monitor, initial, opened } = setup({
351
+ settings: { probeFailureThreshold: 1, retireGraceMs: 60000 },
352
+ });
353
+ initial.healthy = false;
354
+ await monitor.check("default");
355
+ assert.equal(opened.length, 1);
356
+ await monitor.stop();
357
+ assert.equal(initial.closed, 1);
358
+ opened[0].healthy = false;
359
+ await monitor.check("default");
360
+ await monitor.reconnect("default");
361
+ assert.equal(opened.length, 1);
362
+ });
363
+
364
+ test("the interval probe runs when enabled", async () => {
365
+ const { monitor, initial, opened } = setup({
366
+ settings: { healthIntervalMs: 5, probeFailureThreshold: 1 },
367
+ });
368
+ initial.healthy = false;
369
+ await sleep(40);
370
+ assert.ok(opened.length >= 1);
371
+ await monitor.stop();
372
+ });
373
+
374
+ test("a Database instance follows the swapped globals", async () => {
375
+ const path = require.resolve("./index");
376
+ delete require.cache[path];
377
+ const previousSpice = global.spice;
378
+ const { monitor, initial, opened, globals } = setup({
379
+ settings: { probeFailureThreshold: 1 },
380
+ });
381
+ global.spice = {
382
+ ...globals,
383
+ config: { database: { connections: { default: { ...config } } } },
384
+ };
385
+ // Point the shared module-level monitor at this test's monitor.
386
+ const key = Symbol.for("spice-couchbase.health-monitor");
387
+ const previousMonitor = globalThis[key];
388
+ globalThis[key] = monitor;
389
+ try {
390
+ // Keep a reference to the same globals object the monitor mutates.
391
+ global.spice.database = globals.database;
392
+ // The constructor reads the bucket name off the boot-time collection.
393
+ globals.database.connections.default._scope = { _bucket: { _name: "saturn" } };
394
+ const Database = require("./index");
395
+ const db = new Database("default");
396
+ assert.equal(db.cluster, initial);
397
+ const oldCollection = db.collection;
398
+ assert.equal(oldCollection.cluster, initial);
399
+
400
+ initial.healthy = false;
401
+ await monitor.check("default");
402
+ assert.equal(opened.length, 1);
403
+ assert.equal(db.cluster, opened[0]);
404
+ assert.equal(db.collection.cluster, opened[0]);
405
+ assert.notEqual(db.collection, oldCollection);
406
+ assert.deepEqual(Database.healthStatus().default.generation, 2);
407
+ } finally {
408
+ globalThis[key] = previousMonitor;
409
+ global.spice = previousSpice;
410
+ delete require.cache[path];
411
+ }
412
+ });
@@ -37,7 +37,8 @@ function createStatements(scope) {
37
37
  }
38
38
 
39
39
  // Isolate optimizer state and time without connecting to Couchbase or sleeping.
40
- function createDatabase(durationMs, env = {}) {
40
+ // Each load is a fresh module: its own seen-set, cooldown and policy.
41
+ function loadDriver(durationMs, env = {}) {
41
42
  let tick = BigInt(0);
42
43
  const module = { exports: {} };
43
44
  vm.runInNewContext(fs.readFileSync(path.join(__dirname, "index.js"), "utf8"), {
@@ -55,7 +56,11 @@ function createDatabase(durationMs, env = {}) {
55
56
  },
56
57
  },
57
58
  });
58
- const db = Object.create(module.exports.prototype);
59
+ return module.exports;
60
+ }
61
+
62
+ function instanceOf(Database) {
63
+ const db = Object.create(Database.prototype);
59
64
  db.bucket = "saturn";
60
65
  db.scope_name = "test";
61
66
  db.collection_name = "users";
@@ -63,6 +68,13 @@ function createDatabase(durationMs, env = {}) {
63
68
  return db;
64
69
  }
65
70
 
71
+ // Opted in wholesale: per-resource resolution has its own tests further down.
72
+ function createDatabase(durationMs, env = {}) {
73
+ const db = instanceOf(loadDriver(durationMs, env));
74
+ db.auto_optimize = true;
75
+ return db;
76
+ }
77
+
66
78
  test("auto-optimization only starts above the default 3-second threshold", async () => {
67
79
  for (const duration of [1001, 2999, 3000, 3001]) {
68
80
  const db = createDatabase(duration);
@@ -235,3 +247,147 @@ test("a default collection uses default names in its inventory lookup", async ()
235
247
  }));
236
248
  assert.match(createStatements(scope)[0].statement, /`saturn`\.`_default`\.`_default`/);
237
249
  });
250
+
251
+ // ---- Per-resource opt-in for auto cover indexing ----
252
+
253
+ // Nothing decided at instance level: resolution falls through to the call,
254
+ // the application policy and the static lists.
255
+ function createUnconfiguredDatabase(env = {}) {
256
+ return instanceOf(loadDriver(3001, env));
257
+ }
258
+
259
+ async function optimizationsFor(db, statement, ...queryArgs) {
260
+ let optimizations = 0;
261
+ db.optimize = async () => { optimizations += 1; };
262
+ await db.query(statement, ...queryArgs);
263
+ return optimizations;
264
+ }
265
+
266
+ const slowLookup = "SELECT name FROM users WHERE status = 'active'";
267
+ const EVERY_RESOURCE = { SPICE_COUCHBASE_OPTIMIZE_RESOURCES: "*" };
268
+
269
+ test("no resource is auto-indexed unless something opts it in", async () => {
270
+ assert.equal(await optimizationsFor(createUnconfiguredDatabase(), slowLookup), 0);
271
+ assert.equal(await optimizationsFor(
272
+ createUnconfiguredDatabase({ SPICE_COUCHBASE_OPTIMIZE_RESOURCES: "" }), slowLookup), 0);
273
+ });
274
+
275
+ test("SPICE_COUCHBASE_OPTIMIZE_RESOURCES enables the instance's collection, case-insensitively", async () => {
276
+ const env = { SPICE_COUCHBASE_OPTIMIZE_RESOURCES: " Users ,audit_log,, `sessions` " };
277
+ assert.equal(await optimizationsFor(createUnconfiguredDatabase(env), slowLookup), 1);
278
+
279
+ const sessions = createUnconfiguredDatabase(env);
280
+ sessions.collection_name = "sessions";
281
+ assert.equal(await optimizationsFor(sessions, "SELECT id FROM sessions"), 1);
282
+
283
+ const orders = createUnconfiguredDatabase(env);
284
+ orders.collection_name = "orders";
285
+ assert.equal(await optimizationsFor(orders, "SELECT total FROM orders WHERE status = 'paid'"), 0);
286
+ });
287
+
288
+ test("a wildcard restores auto indexing for every resource", async () => {
289
+ const db = createUnconfiguredDatabase(EVERY_RESOURCE);
290
+ db.collection_name = "anything";
291
+ assert.equal(await optimizationsFor(db, "SELECT a FROM anything WHERE b = 1"), 1);
292
+ });
293
+
294
+ test("legacy bucket mode resolves the resource from the type literal the driver adds", async () => {
295
+ const env = { SPICE_COUCHBASE_OPTIMIZE_RESOURCES: "audit_log" };
296
+ for (const [type, expected] of [["audit_log", 1], ["users", 0]]) {
297
+ const db = createUnconfiguredDatabase(env);
298
+ db.collection_name = "_default";
299
+ db.scope_name = "_default";
300
+ assert.equal(await optimizationsFor(db,
301
+ `SELECT actor FROM \`saturn\` WHERE type = '${type}' AND deleted = false`), expected, type);
302
+ }
303
+ });
304
+
305
+ test("a fully qualified or aliased FROM keyspace matches on its last segment", async () => {
306
+ const env = { SPICE_COUCHBASE_OPTIMIZE_RESOURCES: "logs" };
307
+ for (const statement of [
308
+ "SELECT level FROM `default`:`saturn`.`app`.`logs` WHERE level = 'error'",
309
+ "SELECT l.level FROM saturn.app.logs AS l WHERE l.level = 'error'",
310
+ "SELECT u.name FROM users AS u JOIN `logs` AS l ON l.user_id = META(u).id",
311
+ ]) {
312
+ const db = createUnconfiguredDatabase(env);
313
+ db.collection_name = "_default";
314
+ assert.equal(await optimizationsFor(db, statement), 1, statement);
315
+ }
316
+ });
317
+
318
+ test("connection or constructor lists work without the environment variable", async () => {
319
+ const db = createUnconfiguredDatabase();
320
+ db.enabled_resources = new Set(["users"]);
321
+ assert.equal(await optimizationsFor(db, slowLookup), 1);
322
+ });
323
+
324
+ test("an explicit instance setting overrides every resource list", async () => {
325
+ const on = createUnconfiguredDatabase();
326
+ on.auto_optimize = true;
327
+ assert.equal(await optimizationsFor(on, slowLookup), 1);
328
+
329
+ const off = createUnconfiguredDatabase(EVERY_RESOURCE);
330
+ off.auto_optimize = false;
331
+ assert.equal(await optimizationsFor(off, slowLookup), 0);
332
+ });
333
+
334
+ test("a single call can opt in or out through query options", async () => {
335
+ assert.equal(await optimizationsFor(createUnconfiguredDatabase(), slowLookup, { auto_optimize: true }), 1);
336
+ assert.equal(await optimizationsFor(createUnconfiguredDatabase(EVERY_RESOURCE), slowLookup, { auto_optimize: false }), 0);
337
+ assert.equal(await optimizationsFor(createUnconfiguredDatabase(EVERY_RESOURCE), slowLookup, "cluster", { optimize: false }), 0);
338
+ // The scan-consistency shorthand is not an opt-in or opt-out.
339
+ assert.equal(await optimizationsFor(createUnconfiguredDatabase(EVERY_RESOURCE), slowLookup, "cluster", true), 1);
340
+ assert.equal(await optimizationsFor(createUnconfiguredDatabase(), slowLookup, "cluster", true), 0);
341
+ });
342
+
343
+ test("an application policy decides per resource and sees the statement context", async () => {
344
+ const Database = loadDriver(3001);
345
+ const seen = [];
346
+ Database.setAutoOptimizePolicy((resource, context) => {
347
+ seen.push([resource, context]);
348
+ return resource === "users";
349
+ });
350
+ assert.equal(Database.getAutoOptimizePolicy().length, 2);
351
+
352
+ const users = instanceOf(Database);
353
+ users.connection_name = "default";
354
+ assert.equal(await optimizationsFor(users, slowLookup), 1);
355
+ // Objects cross the vm boundary, so compare by value rather than prototype.
356
+ assert.equal(JSON.stringify(seen[0]), JSON.stringify(["users", { query: slowLookup, connection: "default", collection: "users" }]));
357
+
358
+ const orders = instanceOf(Database);
359
+ orders.collection_name = "orders";
360
+ assert.equal(await optimizationsFor(orders, "SELECT total FROM orders WHERE status = 'paid'"), 0);
361
+ });
362
+
363
+ test("a policy that returns undefined defers to the static lists", async () => {
364
+ for (const [env, expected] of [[{ SPICE_COUCHBASE_OPTIMIZE_RESOURCES: "users" }, 1], [{}, 0]]) {
365
+ const Database = loadDriver(3001, env);
366
+ Database.setAutoOptimizePolicy(() => undefined);
367
+ assert.equal(await optimizationsFor(instanceOf(Database), slowLookup), expected);
368
+ }
369
+ });
370
+
371
+ test("an explicit policy refusal wins over the wildcard, and a throwing policy counts as a refusal", async () => {
372
+ for (const policy of [() => false, () => { throw new Error("settings unavailable"); }]) {
373
+ const Database = loadDriver(3001, EVERY_RESOURCE);
374
+ Database.setAutoOptimizePolicy(policy);
375
+ assert.equal(await optimizationsFor(instanceOf(Database), slowLookup), 0);
376
+ }
377
+ });
378
+
379
+ test("the policy can be cleared and must be a function", async () => {
380
+ const Database = loadDriver(3001, EVERY_RESOURCE);
381
+ Database.setAutoOptimizePolicy(() => false);
382
+ Database.setAutoOptimizePolicy(null);
383
+ assert.equal(Database.getAutoOptimizePolicy(), null);
384
+ assert.equal(await optimizationsFor(instanceOf(Database), slowLookup), 1);
385
+ assert.throws(() => Database.setAutoOptimizePolicy("users"), { name: "TypeError" });
386
+ });
387
+
388
+ test("a declined statement is not remembered as already optimised", async () => {
389
+ const db = createUnconfiguredDatabase();
390
+ assert.equal(await optimizationsFor(db, slowLookup), 0);
391
+ db.auto_optimize = true;
392
+ assert.equal(await optimizationsFor(db, slowLookup), 1);
393
+ });