spice-couchbase 2.0.64 → 2.0.65
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.auto-optimize.test.js +122 -204
- package/build/index.count-query.test.js +27 -63
- package/build/index.index-management.test.js +283 -0
- package/build/index.js +486 -418
- package/build/index.multi_get.test.js +9 -56
- package/build/index.query-consistency.test.js +28 -72
- package/build/utility.js +182 -152
- package/build/utility.test.js +79 -59
- package/package.json +1 -1
- package/src/index.index-management.test.js +232 -0
- package/src/index.js +270 -8
- package/src/utility.js +78 -0
package/build/utility.test.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
var assert = require("assert");
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
4
|
+
var _require = require("./utility"),
|
|
5
|
+
buildCoveringIndex = _require.buildCoveringIndex,
|
|
6
|
+
extractWhereFields = _require.extractWhereFields,
|
|
7
|
+
extractSelectFields = _require.extractSelectFields,
|
|
8
|
+
extractGroupByFields = _require.extractGroupByFields,
|
|
9
|
+
extractOrderByFields = _require.extractOrderByFields,
|
|
10
|
+
generateHash = _require.generateHash,
|
|
11
|
+
parseResourceList = _require.parseResourceList,
|
|
12
|
+
extractStatementResources = _require.extractStatementResources;
|
|
13
|
+
|
|
14
|
+
// Test utilities
|
|
17
15
|
var passed = 0;
|
|
18
16
|
var failed = 0;
|
|
19
|
-
|
|
20
17
|
function test(name, fn) {
|
|
21
18
|
try {
|
|
22
19
|
fn();
|
|
@@ -28,17 +25,16 @@ function test(name, fn) {
|
|
|
28
25
|
failed++;
|
|
29
26
|
}
|
|
30
27
|
}
|
|
31
|
-
|
|
32
28
|
function arraysEqual(a, b) {
|
|
33
29
|
if (a.length !== b.length) return false;
|
|
34
30
|
var sortedA = [...a].sort();
|
|
35
31
|
var sortedB = [...b].sort();
|
|
36
32
|
return sortedA.every((val, i) => val === sortedB[i]);
|
|
37
|
-
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================
|
|
38
36
|
// Tests for generateHash
|
|
39
37
|
// ============================================
|
|
40
|
-
|
|
41
|
-
|
|
42
38
|
console.log("\n--- generateHash tests ---");
|
|
43
39
|
test("generates 8 character hash", () => {
|
|
44
40
|
var hash = generateHash("test query");
|
|
@@ -53,10 +49,11 @@ test("generates different hash for different input", () => {
|
|
|
53
49
|
var hash1 = generateHash("SELECT * FROM users");
|
|
54
50
|
var hash2 = generateHash("SELECT * FROM orders");
|
|
55
51
|
assert.notStrictEqual(hash1, hash2);
|
|
56
|
-
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ============================================
|
|
57
55
|
// Tests for extractWhereFields
|
|
58
56
|
// ============================================
|
|
59
|
-
|
|
60
57
|
console.log("\n--- extractWhereFields tests ---");
|
|
61
58
|
test("extracts simple equality field", () => {
|
|
62
59
|
var query = "SELECT name FROM users WHERE status = 'active'";
|
|
@@ -225,10 +222,11 @@ test("handles complex ANY with deleted pattern", () => {
|
|
|
225
222
|
assert.ok(fields.includes("deleted"));
|
|
226
223
|
assert.ok(fields.includes("status"));
|
|
227
224
|
assert.ok(fields.includes("tags"));
|
|
228
|
-
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ============================================
|
|
229
228
|
// Tests for extractSelectFields
|
|
230
229
|
// ============================================
|
|
231
|
-
|
|
232
230
|
console.log("\n--- extractSelectFields tests ---");
|
|
233
231
|
test("extracts simple field", () => {
|
|
234
232
|
var query = "SELECT name FROM users";
|
|
@@ -294,8 +292,8 @@ test("handles multiple aggregate functions", () => {
|
|
|
294
292
|
});
|
|
295
293
|
test("handles nested function calls", () => {
|
|
296
294
|
var query = "SELECT LOWER(TRIM(name)) AS clean_name FROM users";
|
|
297
|
-
var fields = extractSelectFields(query);
|
|
298
|
-
|
|
295
|
+
var fields = extractSelectFields(query);
|
|
296
|
+
// Should extract 'name' from nested functions - currently extracts last match
|
|
299
297
|
assert.ok(fields.length >= 0); // At minimum should not crash
|
|
300
298
|
});
|
|
301
299
|
test("handles query with JOIN", () => {
|
|
@@ -324,10 +322,11 @@ test("handles backtick collection prefix", () => {
|
|
|
324
322
|
var fields = extractSelectFields(query);
|
|
325
323
|
assert.ok(fields.includes("first_name"));
|
|
326
324
|
assert.ok(fields.includes("last_name"));
|
|
327
|
-
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// ============================================
|
|
328
328
|
// Tests for extractGroupByFields
|
|
329
329
|
// ============================================
|
|
330
|
-
|
|
331
330
|
console.log("\n--- extractGroupByFields tests ---");
|
|
332
331
|
test("extracts simple GROUP BY field", () => {
|
|
333
332
|
var query = "SELECT status, COUNT(*) FROM users GROUP BY status";
|
|
@@ -378,10 +377,11 @@ test("handles GROUP BY with HAVING and ORDER BY", () => {
|
|
|
378
377
|
var query = "SELECT status, COUNT(*) FROM users GROUP BY status HAVING COUNT(*) > 5 ORDER BY status";
|
|
379
378
|
var fields = extractGroupByFields(query);
|
|
380
379
|
assert.ok(arraysEqual(fields, ["status"]));
|
|
381
|
-
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// ============================================
|
|
382
383
|
// Tests for extractOrderByFields
|
|
383
384
|
// ============================================
|
|
384
|
-
|
|
385
385
|
console.log("\n--- extractOrderByFields tests ---");
|
|
386
386
|
test("extracts simple ORDER BY field", () => {
|
|
387
387
|
var query = "SELECT name FROM users ORDER BY created_at";
|
|
@@ -442,10 +442,11 @@ test("handles mixed ASC and DESC", () => {
|
|
|
442
442
|
var query = "SELECT name FROM users ORDER BY last_name ASC, created_at DESC";
|
|
443
443
|
var fields = extractOrderByFields(query);
|
|
444
444
|
assert.ok(arraysEqual(fields, ["last_name", "created_at"]));
|
|
445
|
-
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// ============================================
|
|
446
448
|
// Tests for buildCoveringIndex
|
|
447
449
|
// ============================================
|
|
448
|
-
|
|
449
450
|
console.log("\n--- buildCoveringIndex tests ---");
|
|
450
451
|
var defaultOptions = {
|
|
451
452
|
bucket: "saturn",
|
|
@@ -479,16 +480,18 @@ test("WHERE fields come before SELECT fields", () => {
|
|
|
479
480
|
});
|
|
480
481
|
test("deduplicates fields in WHERE and SELECT", () => {
|
|
481
482
|
var query = "SELECT status, name FROM users WHERE status = 'active'";
|
|
482
|
-
var result = buildCoveringIndex(query, defaultOptions);
|
|
483
|
+
var result = buildCoveringIndex(query, defaultOptions);
|
|
483
484
|
|
|
485
|
+
// status should only appear once
|
|
484
486
|
var firstStatus = result.indexOf("`status`");
|
|
485
487
|
var lastStatus = result.lastIndexOf("`status`");
|
|
486
488
|
assert.strictEqual(firstStatus, lastStatus);
|
|
487
489
|
});
|
|
488
490
|
test("excludes deleted from WHERE fields (added separately)", () => {
|
|
489
491
|
var query = "SELECT name FROM users WHERE (deleted = false OR deleted IS MISSING) AND status = 'active'";
|
|
490
|
-
var result = buildCoveringIndex(query, defaultOptions);
|
|
492
|
+
var result = buildCoveringIndex(query, defaultOptions);
|
|
491
493
|
|
|
494
|
+
// deleted should only appear once (as INCLUDE MISSING)
|
|
492
495
|
var matches = result.match(/`deleted`/g);
|
|
493
496
|
assert.strictEqual(matches.length, 1);
|
|
494
497
|
assert.ok(result.includes("`deleted` INCLUDE MISSING"));
|
|
@@ -526,8 +529,9 @@ test("generates unique index name from query hash", () => {
|
|
|
526
529
|
var query1 = "SELECT name FROM users WHERE status = 'active'";
|
|
527
530
|
var query2 = "SELECT email FROM users WHERE type = 'admin'";
|
|
528
531
|
var result1 = buildCoveringIndex(query1, defaultOptions);
|
|
529
|
-
var result2 = buildCoveringIndex(query2, defaultOptions);
|
|
532
|
+
var result2 = buildCoveringIndex(query2, defaultOptions);
|
|
530
533
|
|
|
534
|
+
// Extract index names
|
|
531
535
|
var name1 = result1.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
532
536
|
var name2 = result2.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
533
537
|
assert.notStrictEqual(name1, name2);
|
|
@@ -545,8 +549,8 @@ test("handles complex query with multiple conditions", () => {
|
|
|
545
549
|
});
|
|
546
550
|
test("returns null when only deleted field in query", () => {
|
|
547
551
|
var query = "SELECT deleted FROM users WHERE deleted = false";
|
|
548
|
-
var result = buildCoveringIndex(query, defaultOptions);
|
|
549
|
-
|
|
552
|
+
var result = buildCoveringIndex(query, defaultOptions);
|
|
553
|
+
// Should return null because after filtering out 'deleted', there are no fields left
|
|
550
554
|
assert.strictEqual(result, null);
|
|
551
555
|
});
|
|
552
556
|
test("handles query with type field (legacy pattern)", () => {
|
|
@@ -593,11 +597,13 @@ test("same index name for different queries with same fields", () => {
|
|
|
593
597
|
var query1 = "SELECT name, email FROM users WHERE status = 'active'";
|
|
594
598
|
var query2 = "SELECT email, name FROM users WHERE status = 'pending'";
|
|
595
599
|
var result1 = buildCoveringIndex(query1, defaultOptions);
|
|
596
|
-
var result2 = buildCoveringIndex(query2, defaultOptions);
|
|
600
|
+
var result2 = buildCoveringIndex(query2, defaultOptions);
|
|
597
601
|
|
|
602
|
+
// Extract index names
|
|
598
603
|
var name1 = result1.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
599
|
-
var name2 = result2.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
604
|
+
var name2 = result2.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
600
605
|
|
|
606
|
+
// Should have the same index name since they have the same fields
|
|
601
607
|
assert.strictEqual(name1, name2);
|
|
602
608
|
});
|
|
603
609
|
test("different index name for different ORDER BY direction", () => {
|
|
@@ -622,17 +628,20 @@ test("different index name for queries with different fields", () => {
|
|
|
622
628
|
var query1 = "SELECT name FROM users WHERE status = 'active'";
|
|
623
629
|
var query2 = "SELECT email FROM users WHERE type = 'admin'";
|
|
624
630
|
var result1 = buildCoveringIndex(query1, defaultOptions);
|
|
625
|
-
var result2 = buildCoveringIndex(query2, defaultOptions);
|
|
631
|
+
var result2 = buildCoveringIndex(query2, defaultOptions);
|
|
626
632
|
|
|
633
|
+
// Extract index names
|
|
627
634
|
var name1 = result1.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
628
|
-
var name2 = result2.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
635
|
+
var name2 = result2.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
629
636
|
|
|
637
|
+
// Should have different index names since they have different fields
|
|
630
638
|
assert.notStrictEqual(name1, name2);
|
|
631
639
|
});
|
|
632
640
|
test("index includes all fields needed for covering", () => {
|
|
633
641
|
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'";
|
|
634
|
-
var result = buildCoveringIndex(query, defaultOptions);
|
|
642
|
+
var result = buildCoveringIndex(query, defaultOptions);
|
|
635
643
|
|
|
644
|
+
// Verify structure matches the working example pattern
|
|
636
645
|
assert.ok(result.startsWith("CREATE INDEX `adv_covered_"));
|
|
637
646
|
assert.ok(result.includes("`deleted` INCLUDE MISSING"));
|
|
638
647
|
assert.ok(result.includes("`created_at`"));
|
|
@@ -688,16 +697,18 @@ test("ORDER BY fields come after WHERE fields", () => {
|
|
|
688
697
|
var result = buildCoveringIndex(query, defaultOptions);
|
|
689
698
|
var statusIndex = result.indexOf("`status`");
|
|
690
699
|
var createdAtIndex = result.indexOf("`created_at`");
|
|
691
|
-
var emailIndex = result.indexOf("`email`");
|
|
692
|
-
|
|
693
|
-
assert.ok(statusIndex < createdAtIndex); // ORDER BY field (created_at) should come before SELECT field (email)
|
|
700
|
+
var emailIndex = result.indexOf("`email`");
|
|
694
701
|
|
|
702
|
+
// WHERE field (status) should come before ORDER BY field (created_at)
|
|
703
|
+
assert.ok(statusIndex < createdAtIndex);
|
|
704
|
+
// ORDER BY field (created_at) should come before SELECT field (email)
|
|
695
705
|
assert.ok(createdAtIndex < emailIndex);
|
|
696
706
|
});
|
|
697
707
|
test("deduplicates ORDER BY field if already in WHERE", () => {
|
|
698
708
|
var query = "SELECT name FROM users WHERE status = 'active' AND created_at > '2024-01-01' ORDER BY created_at DESC";
|
|
699
|
-
var result = buildCoveringIndex(query, defaultOptions);
|
|
709
|
+
var result = buildCoveringIndex(query, defaultOptions);
|
|
700
710
|
|
|
711
|
+
// created_at should only appear once
|
|
701
712
|
var firstCreatedAt = result.indexOf("`created_at`");
|
|
702
713
|
var lastCreatedAt = result.lastIndexOf("`created_at`");
|
|
703
714
|
assert.strictEqual(firstCreatedAt, lastCreatedAt);
|
|
@@ -705,8 +716,9 @@ test("deduplicates ORDER BY field if already in WHERE", () => {
|
|
|
705
716
|
});
|
|
706
717
|
test("deduplicates ORDER BY field if already in SELECT", () => {
|
|
707
718
|
var query = "SELECT name, created_at FROM users WHERE status = 'active' ORDER BY created_at DESC";
|
|
708
|
-
var result = buildCoveringIndex(query, defaultOptions);
|
|
719
|
+
var result = buildCoveringIndex(query, defaultOptions);
|
|
709
720
|
|
|
721
|
+
// created_at should only appear once (in ORDER BY position, before SELECT fields)
|
|
710
722
|
var matches = result.match(/`created_at`/g);
|
|
711
723
|
assert.strictEqual(matches.length, 1);
|
|
712
724
|
assert.ok(result.includes("`created_at` DESC"));
|
|
@@ -715,8 +727,9 @@ test("handles multiple ORDER BY fields", () => {
|
|
|
715
727
|
var query = "SELECT email FROM users WHERE status = 'active' ORDER BY last_name ASC, first_name ASC";
|
|
716
728
|
var result = buildCoveringIndex(query, defaultOptions);
|
|
717
729
|
assert.ok(result.includes("`last_name`"));
|
|
718
|
-
assert.ok(result.includes("`first_name`"));
|
|
730
|
+
assert.ok(result.includes("`first_name`"));
|
|
719
731
|
|
|
732
|
+
// ORDER BY fields should come before SELECT fields
|
|
720
733
|
var lastNameIndex = result.indexOf("`last_name`");
|
|
721
734
|
var firstNameIndex = result.indexOf("`first_name`");
|
|
722
735
|
var emailIndex = result.indexOf("`email`");
|
|
@@ -739,8 +752,9 @@ test("handles realistic query with ORDER BY", () => {
|
|
|
739
752
|
assert.ok(result.includes("`created_at` DESC"));
|
|
740
753
|
assert.ok(result.includes("`first_name`"));
|
|
741
754
|
assert.ok(result.includes("`last_name`"));
|
|
742
|
-
assert.ok(result.includes("`email`"));
|
|
755
|
+
assert.ok(result.includes("`email`"));
|
|
743
756
|
|
|
757
|
+
// Verify order: status (WHERE) -> created_at (ORDER BY) -> SELECT fields
|
|
744
758
|
var statusIndex = result.indexOf("`status`");
|
|
745
759
|
var createdAtIndex = result.indexOf("`created_at`");
|
|
746
760
|
var firstNameIndex = result.indexOf("`first_name`");
|
|
@@ -760,25 +774,29 @@ test("GROUP BY fields come after WHERE fields", () => {
|
|
|
760
774
|
var query = "SELECT COUNT(name) FROM users WHERE type = 'user' GROUP BY status";
|
|
761
775
|
var result = buildCoveringIndex(query, defaultOptions);
|
|
762
776
|
var typeIndex = result.indexOf("`type`");
|
|
763
|
-
var statusIndex = result.indexOf("`status`");
|
|
777
|
+
var statusIndex = result.indexOf("`status`");
|
|
764
778
|
|
|
779
|
+
// WHERE field (type) should come before GROUP BY field (status)
|
|
765
780
|
assert.ok(typeIndex < statusIndex);
|
|
766
781
|
});
|
|
767
782
|
test("GROUP BY fields come before ORDER BY fields", () => {
|
|
768
783
|
var query = "SELECT COUNT(name) FROM users WHERE type = 'user' GROUP BY status ORDER BY status";
|
|
769
784
|
var result = buildCoveringIndex(query, defaultOptions);
|
|
770
785
|
var typeIndex = result.indexOf("`type`");
|
|
771
|
-
var statusIndex = result.indexOf("`status`");
|
|
786
|
+
var statusIndex = result.indexOf("`status`");
|
|
772
787
|
|
|
788
|
+
// status is in both GROUP BY and ORDER BY, should only appear once
|
|
773
789
|
var matches = result.match(/`status`/g);
|
|
774
|
-
assert.strictEqual(matches.length, 1);
|
|
790
|
+
assert.strictEqual(matches.length, 1);
|
|
775
791
|
|
|
792
|
+
// WHERE field (type) should come before GROUP BY field (status)
|
|
776
793
|
assert.ok(typeIndex < statusIndex);
|
|
777
794
|
});
|
|
778
795
|
test("deduplicates GROUP BY field if already in WHERE", () => {
|
|
779
796
|
var query = "SELECT COUNT(name) FROM users WHERE status = 'active' GROUP BY status";
|
|
780
|
-
var result = buildCoveringIndex(query, defaultOptions);
|
|
797
|
+
var result = buildCoveringIndex(query, defaultOptions);
|
|
781
798
|
|
|
799
|
+
// status should only appear once
|
|
782
800
|
var matches = result.match(/`status`/g);
|
|
783
801
|
assert.strictEqual(matches.length, 1);
|
|
784
802
|
});
|
|
@@ -805,8 +823,9 @@ test("handles realistic GROUP BY query", () => {
|
|
|
805
823
|
assert.ok(result.includes("`deleted` INCLUDE MISSING"));
|
|
806
824
|
assert.ok(result.includes("`type`"));
|
|
807
825
|
assert.ok(result.includes("`status`"));
|
|
808
|
-
assert.ok(result.includes("`first_name`"));
|
|
826
|
+
assert.ok(result.includes("`first_name`"));
|
|
809
827
|
|
|
828
|
+
// Verify order: type (WHERE) -> status (GROUP BY) -> first_name (SELECT)
|
|
810
829
|
var typeIndex = result.indexOf("`type`");
|
|
811
830
|
var statusIndex = result.indexOf("`status`");
|
|
812
831
|
var firstNameIndex = result.indexOf("`first_name`");
|
|
@@ -819,17 +838,17 @@ test("field order: WHERE -> GROUP BY -> ORDER BY -> SELECT", () => {
|
|
|
819
838
|
var typeIndex = result.indexOf("`type`");
|
|
820
839
|
var statusIndex = result.indexOf("`status`");
|
|
821
840
|
var createdAtIndex = result.indexOf("`created_at`");
|
|
822
|
-
var emailIndex = result.indexOf("`email`");
|
|
841
|
+
var emailIndex = result.indexOf("`email`");
|
|
823
842
|
|
|
843
|
+
// Verify correct order
|
|
824
844
|
assert.ok(typeIndex < statusIndex); // WHERE before GROUP BY
|
|
825
|
-
|
|
826
845
|
assert.ok(statusIndex < createdAtIndex); // GROUP BY before ORDER BY
|
|
827
|
-
|
|
828
846
|
assert.ok(createdAtIndex < emailIndex); // ORDER BY before SELECT
|
|
829
|
-
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// ============================================
|
|
830
850
|
// Tests for parseResourceList / extractStatementResources
|
|
831
851
|
// ============================================
|
|
832
|
-
|
|
833
852
|
console.log("\n--- parseResourceList tests ---");
|
|
834
853
|
test("parses a comma-separated string, trimming, unquoting and lower-casing", () => {
|
|
835
854
|
var result = parseResourceList(" Users, `Audit_Log` ,, sessions ");
|
|
@@ -872,10 +891,11 @@ test("does not treat META() arguments, subqueries or column names as keyspaces",
|
|
|
872
891
|
test("handles non-string input", () => {
|
|
873
892
|
assert.strictEqual(extractStatementResources(undefined).size, 0);
|
|
874
893
|
assert.strictEqual(extractStatementResources(null).size, 0);
|
|
875
|
-
});
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// ============================================
|
|
876
897
|
// Summary
|
|
877
898
|
// ============================================
|
|
878
|
-
|
|
879
899
|
console.log("\n========================================");
|
|
880
900
|
console.log("Tests: ".concat(passed + failed, " | Passed: ").concat(passed, " | Failed: ").concat(failed));
|
|
881
901
|
console.log("========================================\n");
|
package/package.json
CHANGED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("node:assert/strict");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const vm = require("node:vm");
|
|
7
|
+
const { test } = require("node:test");
|
|
8
|
+
const {
|
|
9
|
+
statementShape,
|
|
10
|
+
planIndexNames,
|
|
11
|
+
usesManagedIndex,
|
|
12
|
+
renderIndexKey,
|
|
13
|
+
BASE_INDEX,
|
|
14
|
+
} = require("./utility");
|
|
15
|
+
|
|
16
|
+
// Same isolation as index.auto-optimize.test.js: a fresh module per load, so
|
|
17
|
+
// the seen-set, cooldown and handler never leak between tests.
|
|
18
|
+
function loadDriver(durationMs, env = {}) {
|
|
19
|
+
let tick = BigInt(0);
|
|
20
|
+
const module = { exports: {} };
|
|
21
|
+
vm.runInNewContext(fs.readFileSync(path.join(__dirname, "index.js"), "utf8"), {
|
|
22
|
+
require,
|
|
23
|
+
module,
|
|
24
|
+
console,
|
|
25
|
+
process: {
|
|
26
|
+
env,
|
|
27
|
+
hrtime: {
|
|
28
|
+
bigint: () => {
|
|
29
|
+
const current = tick;
|
|
30
|
+
tick += BigInt(durationMs) * BigInt(1000000);
|
|
31
|
+
return current;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return module.exports;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function recordingCluster(rowsFor = () => []) {
|
|
40
|
+
const calls = [];
|
|
41
|
+
return {
|
|
42
|
+
calls,
|
|
43
|
+
async query(statement, options) {
|
|
44
|
+
calls.push({ statement, options });
|
|
45
|
+
return { rows: rowsFor(statement), meta: {} };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createDatabase(Database, cluster) {
|
|
51
|
+
const db = Object.create(Database.prototype);
|
|
52
|
+
db.bucket = "saturn";
|
|
53
|
+
db.scope_name = "test";
|
|
54
|
+
db.collection_name = "users";
|
|
55
|
+
db.db_config = { bucket: "saturn" };
|
|
56
|
+
db.auto_optimize = true;
|
|
57
|
+
db.cluster = cluster;
|
|
58
|
+
db.scope = cluster;
|
|
59
|
+
return db;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const KEYSPACE = "default:`saturn`.`test`.`users`";
|
|
63
|
+
|
|
64
|
+
test("statement shapes ignore literal values and IN list lengths", () => {
|
|
65
|
+
assert.equal(
|
|
66
|
+
statementShape("SELECT * FROM `users` WHERE email = 'a@b.c' AND age > 30 AND id IN ['x', 'y']"),
|
|
67
|
+
statementShape("SELECT * FROM `users` WHERE email = \"d@e.f\" AND age > 7 AND id IN ['z']"),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("managed indexes are recognised in a plan, the base fallback is not", () => {
|
|
72
|
+
const plan = (index) => ({ "#operator": "Sequence", "~children": [{ "#operator": "IndexScan3", index }] });
|
|
73
|
+
assert.deepEqual(planIndexNames(plan("ix_sort_1a2b3c_9f0e")), ["ix_sort_1a2b3c_9f0e"]);
|
|
74
|
+
assert.equal(usesManagedIndex(plan("ix_sort_1a2b3c_9f0e")), true);
|
|
75
|
+
assert.equal(usesManagedIndex(plan("ix_base")), false);
|
|
76
|
+
assert.equal(usesManagedIndex(plan("adv_covered_1a3952f8")), false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("index keys render direction and INCLUDE MISSING", () => {
|
|
80
|
+
assert.deepEqual(BASE_INDEX.keys.map(renderIndexKey), ["`created_at` DESC INCLUDE MISSING", "`deleted`"]);
|
|
81
|
+
assert.equal(renderIndexKey("`name`"), "`name`");
|
|
82
|
+
assert.throws(() => renderIndexKey({}), TypeError);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("createIndex is idempotent, deferred by default and fully qualified", async () => {
|
|
86
|
+
const cluster = recordingCluster();
|
|
87
|
+
const db = createDatabase(loadDriver(0), cluster);
|
|
88
|
+
await db.createIndex({
|
|
89
|
+
name: "ix_sort_1a2b3c_9f0e",
|
|
90
|
+
keys: [{ expr: "`priority`", desc: true, include_missing: true }, { expr: "IFMISSINGORNULL(`due_at`, {})" }],
|
|
91
|
+
where: "IFMISSING(`deleted`, false) = false",
|
|
92
|
+
num_replica: 1,
|
|
93
|
+
});
|
|
94
|
+
assert.equal(
|
|
95
|
+
cluster.calls[0].statement,
|
|
96
|
+
`CREATE INDEX IF NOT EXISTS \`ix_sort_1a2b3c_9f0e\` ON ${KEYSPACE}` +
|
|
97
|
+
"(`priority` DESC INCLUDE MISSING, IFMISSINGORNULL(`due_at`, {})) " +
|
|
98
|
+
'WHERE IFMISSING(`deleted`, false) = false WITH {"defer_build":true,"num_replica":1}',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("build, drop and primary drop target the collection", async () => {
|
|
103
|
+
const cluster = recordingCluster();
|
|
104
|
+
const db = createDatabase(loadDriver(0), cluster);
|
|
105
|
+
await db.buildIndexes(["ix_a", "ix_b", "ix_a"]);
|
|
106
|
+
await db.dropIndex("ix_a");
|
|
107
|
+
await db.dropPrimaryIndex();
|
|
108
|
+
assert.deepEqual(cluster.calls.map(({ statement }) => statement), [
|
|
109
|
+
`BUILD INDEX ON ${KEYSPACE}(\`ix_a\`, \`ix_b\`)`,
|
|
110
|
+
`DROP INDEX IF EXISTS \`ix_a\` ON ${KEYSPACE}`,
|
|
111
|
+
`DROP PRIMARY INDEX IF EXISTS ON ${KEYSPACE}`,
|
|
112
|
+
]);
|
|
113
|
+
assert.equal(await db.buildIndexes([]), null);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("a partial index inventory is an error, not an empty list", async () => {
|
|
117
|
+
const Database = loadDriver(0);
|
|
118
|
+
const db = createDatabase(Database, {
|
|
119
|
+
async query() {
|
|
120
|
+
return { rows: [], meta: { warnings: [{ msg: "partial" }] } };
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
await assert.rejects(() => db.listIndexes(), /index inventory/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("the client context id reaches the SDK options", async () => {
|
|
127
|
+
const cluster = recordingCluster();
|
|
128
|
+
const db = createDatabase(loadDriver(0), cluster);
|
|
129
|
+
await db.query("SELECT 1", "cluster", { client_context_id: "scheduler.due" });
|
|
130
|
+
assert.equal(cluster.calls[0].options.clientContextId, "scheduler.due");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
function scanTimeout() {
|
|
134
|
+
const error = new Error("Index scan timed out");
|
|
135
|
+
error.cause = { first_error_code: 12015, first_error_message: "Index scan timed out" };
|
|
136
|
+
return error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
test("an expired scan_wait reruns without waiting only when the caller allows it", async () => {
|
|
140
|
+
const Database = loadDriver(0);
|
|
141
|
+
const attempts = [];
|
|
142
|
+
const cluster = {
|
|
143
|
+
async query(statement, options) {
|
|
144
|
+
attempts.push(options);
|
|
145
|
+
if (attempts.length === 1) throw scanTimeout();
|
|
146
|
+
return { rows: [{ ok: true }] };
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
const db = createDatabase(Database, cluster);
|
|
150
|
+
const response = await db.query("SELECT 1", "cluster", {
|
|
151
|
+
scan_consistency: "request_plus",
|
|
152
|
+
scan_wait: 2000,
|
|
153
|
+
scan_wait_fallback: true,
|
|
154
|
+
});
|
|
155
|
+
assert.deepEqual(response.rows, [{ ok: true }]);
|
|
156
|
+
assert.equal(attempts.length, 2);
|
|
157
|
+
assert.equal(attempts[1].scanWait, undefined);
|
|
158
|
+
assert.notEqual(attempts[1].scanConsistency, attempts[0].scanConsistency);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("without the fallback an expired scan_wait is an error", async () => {
|
|
162
|
+
const Database = loadDriver(0);
|
|
163
|
+
const db = createDatabase(Database, {
|
|
164
|
+
async query() {
|
|
165
|
+
throw scanTimeout();
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
await assert.rejects(
|
|
169
|
+
() => db.query("SELECT 1", "cluster", { scan_consistency: "request_plus", scan_wait: 2000 }),
|
|
170
|
+
/Index scan timed out/,
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("with a handler installed, slow statements are handed off once per shape and never indexed here", async () => {
|
|
175
|
+
const Database = loadDriver(5000);
|
|
176
|
+
const cluster = recordingCluster((statement) =>
|
|
177
|
+
statement.startsWith("EXPLAIN ") ? [{ "#operator": "IndexScan3", index: "ix_base" }] : [],
|
|
178
|
+
);
|
|
179
|
+
const db = createDatabase(Database, cluster);
|
|
180
|
+
const reports = [];
|
|
181
|
+
Database.setSlowStatementHandler(async (report) => reports.push(report));
|
|
182
|
+
let legacy = 0;
|
|
183
|
+
db.optimize = async () => { legacy += 1; };
|
|
184
|
+
|
|
185
|
+
await db.query("SELECT name FROM users WHERE email = 'a@b.c'");
|
|
186
|
+
await db.query("SELECT name FROM users WHERE email = 'd@e.f'");
|
|
187
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
188
|
+
|
|
189
|
+
assert.equal(reports.length, 1);
|
|
190
|
+
assert.equal(reports[0].shape, "SELECT name FROM users WHERE email = ?");
|
|
191
|
+
assert.equal(reports[0].collection, "users");
|
|
192
|
+
assert.equal(legacy, 0);
|
|
193
|
+
assert.equal(cluster.calls.some(({ statement }) => statement.startsWith("CREATE ")), false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("a slow statement already served by a managed index is not reported", async () => {
|
|
197
|
+
const Database = loadDriver(5000);
|
|
198
|
+
const cluster = recordingCluster((statement) =>
|
|
199
|
+
statement.startsWith("EXPLAIN ") ? [{ "#operator": "IndexScan3", index: "ix_sort_1a2b3c_9f0e" }] : [],
|
|
200
|
+
);
|
|
201
|
+
const db = createDatabase(Database, cluster);
|
|
202
|
+
const reports = [];
|
|
203
|
+
Database.setSlowStatementHandler(async (report) => reports.push(report));
|
|
204
|
+
await db.query("SELECT name FROM users ORDER BY priority DESC");
|
|
205
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
206
|
+
assert.equal(reports.length, 0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("without a handler the legacy covering-index path still runs", async () => {
|
|
210
|
+
const Database = loadDriver(5000);
|
|
211
|
+
const db = createDatabase(Database, recordingCluster());
|
|
212
|
+
let legacy = 0;
|
|
213
|
+
db.optimize = async () => { legacy += 1; };
|
|
214
|
+
await db.query("SELECT name FROM users WHERE status = 'active'");
|
|
215
|
+
assert.equal(legacy, 1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a new collection gets the base index instead of a primary index when asked", async () => {
|
|
219
|
+
const Database = loadDriver(0);
|
|
220
|
+
const cluster = recordingCluster();
|
|
221
|
+
const db = createDatabase(Database, cluster);
|
|
222
|
+
db.db_config.primary_index = false;
|
|
223
|
+
db.checkGlobelConnectionObject = () => false;
|
|
224
|
+
db.collectionExists = async () => false;
|
|
225
|
+
db.createCollection = async () => {};
|
|
226
|
+
db.wait = async () => {};
|
|
227
|
+
let primary = 0;
|
|
228
|
+
db.createPrimaryIndex = async () => { primary += 1; };
|
|
229
|
+
await db.upsertCollection();
|
|
230
|
+
assert.equal(primary, 0);
|
|
231
|
+
assert.match(cluster.calls[0].statement, /^CREATE INDEX IF NOT EXISTS `ix_base` ON .*\(`created_at` DESC INCLUDE MISSING, `deleted`\) WITH \{"defer_build":false\}$/);
|
|
232
|
+
});
|