spice-couchbase 2.0.44 → 2.0.46

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,986 @@
1
+ "use strict";
2
+
3
+ const assert = require("assert");
4
+ const {
5
+ buildCoveringIndex,
6
+ extractWhereFields,
7
+ extractSelectFields,
8
+ extractGroupByFields,
9
+ extractOrderByFields,
10
+ generateHash,
11
+ } = require("./utility");
12
+
13
+ // Test utilities
14
+ let passed = 0;
15
+ let failed = 0;
16
+
17
+ function test(name, fn) {
18
+ try {
19
+ fn();
20
+ console.log(`✓ ${name}`);
21
+ passed++;
22
+ } catch (error) {
23
+ console.log(`✗ ${name}`);
24
+ console.log(` Error: ${error.message}`);
25
+ failed++;
26
+ }
27
+ }
28
+
29
+ function arraysEqual(a, b) {
30
+ if (a.length !== b.length) return false;
31
+ const sortedA = [...a].sort();
32
+ const sortedB = [...b].sort();
33
+ return sortedA.every((val, i) => val === sortedB[i]);
34
+ }
35
+
36
+ // ============================================
37
+ // Tests for generateHash
38
+ // ============================================
39
+ console.log("\n--- generateHash tests ---");
40
+
41
+ test("generates 8 character hash", () => {
42
+ const hash = generateHash("test query");
43
+ assert.strictEqual(hash.length, 8);
44
+ });
45
+
46
+ test("generates consistent hash for same input", () => {
47
+ const hash1 = generateHash("SELECT * FROM users");
48
+ const hash2 = generateHash("SELECT * FROM users");
49
+ assert.strictEqual(hash1, hash2);
50
+ });
51
+
52
+ test("generates different hash for different input", () => {
53
+ const hash1 = generateHash("SELECT * FROM users");
54
+ const hash2 = generateHash("SELECT * FROM orders");
55
+ assert.notStrictEqual(hash1, hash2);
56
+ });
57
+
58
+ // ============================================
59
+ // Tests for extractWhereFields
60
+ // ============================================
61
+ console.log("\n--- extractWhereFields tests ---");
62
+
63
+ test("extracts simple equality field", () => {
64
+ const query = "SELECT name FROM users WHERE status = 'active'";
65
+ const fields = extractWhereFields(query);
66
+ assert.ok(arraysEqual(fields, ["status"]));
67
+ });
68
+
69
+ test("extracts multiple fields with AND", () => {
70
+ const query = "SELECT name FROM users WHERE status = 'active' AND type = 'admin'";
71
+ const fields = extractWhereFields(query);
72
+ assert.ok(arraysEqual(fields, ["status", "type"]));
73
+ });
74
+
75
+ test("extracts fields with comparison operators", () => {
76
+ const query = "SELECT name FROM users WHERE age > 18 AND score <= 100";
77
+ const fields = extractWhereFields(query);
78
+ assert.ok(arraysEqual(fields, ["age", "score"]));
79
+ });
80
+
81
+ test("extracts backtick-quoted fields", () => {
82
+ const query = "SELECT name FROM users WHERE `first_name` = 'John'";
83
+ const fields = extractWhereFields(query);
84
+ assert.ok(arraysEqual(fields, ["first_name"]));
85
+ });
86
+
87
+ test("extracts fields with IN clause", () => {
88
+ const query = "SELECT name FROM users WHERE status IN ['active', 'pending']";
89
+ const fields = extractWhereFields(query);
90
+ assert.ok(arraysEqual(fields, ["status"]));
91
+ });
92
+
93
+ test("extracts fields with LIKE clause", () => {
94
+ const query = "SELECT name FROM users WHERE name LIKE '%john%'";
95
+ const fields = extractWhereFields(query);
96
+ assert.ok(arraysEqual(fields, ["name"]));
97
+ });
98
+
99
+ test("extracts fields with IS MISSING", () => {
100
+ const query = "SELECT name FROM users WHERE deleted IS MISSING";
101
+ const fields = extractWhereFields(query);
102
+ assert.ok(arraysEqual(fields, ["deleted"]));
103
+ });
104
+
105
+ test("extracts fields with IS NULL", () => {
106
+ const query = "SELECT name FROM users WHERE email IS NULL";
107
+ const fields = extractWhereFields(query);
108
+ assert.ok(arraysEqual(fields, ["email"]));
109
+ });
110
+
111
+ test("extracts fields from LOWER function", () => {
112
+ const query = "SELECT name FROM users WHERE LOWER(email) = 'test@test.com'";
113
+ const fields = extractWhereFields(query);
114
+ assert.ok(fields.includes("email"));
115
+ });
116
+
117
+ test("returns empty array when no WHERE clause", () => {
118
+ const query = "SELECT name FROM users";
119
+ const fields = extractWhereFields(query);
120
+ assert.ok(arraysEqual(fields, []));
121
+ });
122
+
123
+ test("handles deleted check pattern", () => {
124
+ const query = "SELECT name FROM users WHERE (deleted = false OR deleted IS MISSING) AND status = 'active'";
125
+ const fields = extractWhereFields(query);
126
+ assert.ok(fields.includes("deleted"));
127
+ assert.ok(fields.includes("status"));
128
+ });
129
+
130
+ test("stops at ORDER BY", () => {
131
+ const query = "SELECT name FROM users WHERE status = 'active' ORDER BY created_at";
132
+ const fields = extractWhereFields(query);
133
+ assert.ok(arraysEqual(fields, ["status"]));
134
+ assert.ok(!fields.includes("created_at"));
135
+ });
136
+
137
+ test("stops at LIMIT", () => {
138
+ const query = "SELECT name FROM users WHERE status = 'active' LIMIT 10";
139
+ const fields = extractWhereFields(query);
140
+ assert.ok(arraysEqual(fields, ["status"]));
141
+ });
142
+
143
+ test("stops at OFFSET", () => {
144
+ const query = "SELECT name FROM users WHERE status = 'active' OFFSET 20";
145
+ const fields = extractWhereFields(query);
146
+ assert.ok(arraysEqual(fields, ["status"]));
147
+ });
148
+
149
+ test("handles type field (legacy pattern)", () => {
150
+ const query = "SELECT name FROM bucket WHERE type = 'user' AND status = 'active'";
151
+ const fields = extractWhereFields(query);
152
+ assert.ok(fields.includes("type"));
153
+ assert.ok(fields.includes("status"));
154
+ });
155
+
156
+ test("handles not equals operator !=", () => {
157
+ const query = "SELECT name FROM users WHERE status != 'deleted'";
158
+ const fields = extractWhereFields(query);
159
+ assert.ok(arraysEqual(fields, ["status"]));
160
+ });
161
+
162
+ test("handles not equals operator <>", () => {
163
+ const query = "SELECT name FROM users WHERE status <> 'deleted'";
164
+ const fields = extractWhereFields(query);
165
+ assert.ok(arraysEqual(fields, ["status"]));
166
+ });
167
+
168
+ test("handles BETWEEN clause", () => {
169
+ const query = "SELECT name FROM users WHERE age BETWEEN 18 AND 65";
170
+ const fields = extractWhereFields(query);
171
+ assert.ok(fields.includes("age"));
172
+ });
173
+
174
+ test("handles OR conditions", () => {
175
+ const query = "SELECT name FROM users WHERE status = 'active' OR status = 'pending'";
176
+ const fields = extractWhereFields(query);
177
+ assert.ok(fields.includes("status"));
178
+ });
179
+
180
+ test("handles nested parentheses", () => {
181
+ const query = "SELECT name FROM users WHERE ((status = 'active') AND (type = 'admin'))";
182
+ const fields = extractWhereFields(query);
183
+ assert.ok(fields.includes("status"));
184
+ assert.ok(fields.includes("type"));
185
+ });
186
+
187
+ test("handles complex deleted pattern with additional conditions", () => {
188
+ const query = `SELECT first_name, last_name FROM users
189
+ WHERE (deleted = false OR deleted IS MISSING)
190
+ AND status = 'active'
191
+ AND type = 'user'
192
+ ORDER BY created_at`;
193
+ const fields = extractWhereFields(query);
194
+ assert.ok(fields.includes("deleted"));
195
+ assert.ok(fields.includes("status"));
196
+ assert.ok(fields.includes("type"));
197
+ assert.ok(!fields.includes("created_at")); // ORDER BY field should not be included
198
+ });
199
+
200
+ test("stops at GROUP BY", () => {
201
+ const query = "SELECT COUNT(status) FROM users WHERE type = 'admin' GROUP BY status";
202
+ const fields = extractWhereFields(query);
203
+ assert.ok(arraysEqual(fields, ["type"]));
204
+ });
205
+
206
+ test("stops at HAVING", () => {
207
+ const query = "SELECT status FROM users WHERE type = 'admin' GROUP BY status HAVING COUNT(*) > 5";
208
+ const fields = extractWhereFields(query);
209
+ assert.ok(arraysEqual(fields, ["type"]));
210
+ });
211
+
212
+ test("handles ANY array operator", () => {
213
+ const query = "SELECT name FROM users WHERE ANY tag IN tags SATISFIES tag = 'vip' END";
214
+ const fields = extractWhereFields(query);
215
+ assert.ok(fields.includes("tags"));
216
+ });
217
+
218
+ test("handles EVERY array operator", () => {
219
+ const query = "SELECT name FROM users WHERE EVERY score IN scores SATISFIES score > 50 END";
220
+ const fields = extractWhereFields(query);
221
+ assert.ok(fields.includes("scores"));
222
+ });
223
+
224
+ test("handles ANY with additional conditions", () => {
225
+ const query = "SELECT name FROM users WHERE status = 'active' AND ANY tag IN tags SATISFIES tag = 'premium' END";
226
+ const fields = extractWhereFields(query);
227
+ assert.ok(fields.includes("status"));
228
+ assert.ok(fields.includes("tags"));
229
+ });
230
+
231
+ test("handles EVERY with additional conditions", () => {
232
+ const query = "SELECT name FROM users WHERE type = 'user' AND EVERY item IN items SATISFIES item.quantity > 0 END";
233
+ const fields = extractWhereFields(query);
234
+ assert.ok(fields.includes("type"));
235
+ assert.ok(fields.includes("items"));
236
+ });
237
+
238
+ test("handles ANY with backtick-quoted array field", () => {
239
+ const query = "SELECT name FROM users WHERE ANY t IN `user_tags` SATISFIES t = 'admin' END";
240
+ const fields = extractWhereFields(query);
241
+ assert.ok(fields.includes("user_tags"));
242
+ });
243
+
244
+ test("handles multiple ANY clauses", () => {
245
+ const query = "SELECT name FROM users WHERE ANY t IN tags SATISFIES t = 'vip' END AND ANY r IN roles SATISFIES r = 'admin' END";
246
+ const fields = extractWhereFields(query);
247
+ assert.ok(fields.includes("tags"));
248
+ assert.ok(fields.includes("roles"));
249
+ });
250
+
251
+ test("handles ANY and EVERY together", () => {
252
+ const query = "SELECT name FROM users WHERE ANY t IN tags SATISFIES t = 'active' END AND EVERY s IN scores SATISFIES s >= 0 END";
253
+ const fields = extractWhereFields(query);
254
+ assert.ok(fields.includes("tags"));
255
+ assert.ok(fields.includes("scores"));
256
+ });
257
+
258
+ test("handles complex ANY with deleted pattern", () => {
259
+ const query = `SELECT name, email FROM users
260
+ WHERE (deleted = false OR deleted IS MISSING)
261
+ AND status = 'active'
262
+ AND ANY tag IN tags SATISFIES tag = 'premium' END`;
263
+ const fields = extractWhereFields(query);
264
+ assert.ok(fields.includes("deleted"));
265
+ assert.ok(fields.includes("status"));
266
+ assert.ok(fields.includes("tags"));
267
+ });
268
+
269
+ // ============================================
270
+ // Tests for extractSelectFields
271
+ // ============================================
272
+ console.log("\n--- extractSelectFields tests ---");
273
+
274
+ test("extracts simple field", () => {
275
+ const query = "SELECT name FROM users";
276
+ const fields = extractSelectFields(query);
277
+ assert.ok(arraysEqual(fields, ["name"]));
278
+ });
279
+
280
+ test("extracts multiple fields", () => {
281
+ const query = "SELECT name, email, status FROM users";
282
+ const fields = extractSelectFields(query);
283
+ assert.ok(arraysEqual(fields, ["name", "email", "status"]));
284
+ });
285
+
286
+ test("returns null for SELECT *", () => {
287
+ const query = "SELECT * FROM users";
288
+ const fields = extractSelectFields(query);
289
+ assert.strictEqual(fields, null);
290
+ });
291
+
292
+ test("returns null for SELECT collection.*", () => {
293
+ const query = "SELECT users.* FROM users";
294
+ const fields = extractSelectFields(query);
295
+ assert.strictEqual(fields, null);
296
+ });
297
+
298
+ test("returns null for SELECT (bucket).* pattern", () => {
299
+ const query = "SELECT (`users`).*, META(`users`).id AS id FROM users";
300
+ const fields = extractSelectFields(query);
301
+ assert.strictEqual(fields, null);
302
+ });
303
+
304
+ test("extracts backtick-quoted fields", () => {
305
+ const query = "SELECT `first_name`, `last_name` FROM users";
306
+ const fields = extractSelectFields(query);
307
+ assert.ok(arraysEqual(fields, ["first_name", "last_name"]));
308
+ });
309
+
310
+ test("handles AS alias", () => {
311
+ const query = "SELECT first_name AS fname, last_name AS lname FROM users";
312
+ const fields = extractSelectFields(query);
313
+ assert.ok(arraysEqual(fields, ["first_name", "last_name"]));
314
+ });
315
+
316
+ test("skips META().id", () => {
317
+ const query = "SELECT name, META(users).id AS id FROM users";
318
+ const fields = extractSelectFields(query);
319
+ assert.ok(arraysEqual(fields, ["name"]));
320
+ });
321
+
322
+ test("extracts field from aggregate function", () => {
323
+ const query = "SELECT COUNT(status) FROM users";
324
+ const fields = extractSelectFields(query);
325
+ assert.ok(arraysEqual(fields, ["status"]));
326
+ });
327
+
328
+ test("handles collection.field syntax", () => {
329
+ const query = "SELECT users.name, users.email FROM users";
330
+ const fields = extractSelectFields(query);
331
+ assert.ok(arraysEqual(fields, ["name", "email"]));
332
+ });
333
+
334
+ test("returns null when no SELECT clause found", () => {
335
+ const query = "INVALID QUERY";
336
+ const fields = extractSelectFields(query);
337
+ assert.strictEqual(fields, null);
338
+ });
339
+
340
+ test("handles multiple aggregate functions", () => {
341
+ const query = "SELECT COUNT(status), SUM(amount), AVG(score) FROM users";
342
+ const fields = extractSelectFields(query);
343
+ assert.ok(fields.includes("status"));
344
+ assert.ok(fields.includes("amount"));
345
+ assert.ok(fields.includes("score"));
346
+ });
347
+
348
+ test("handles nested function calls", () => {
349
+ const query = "SELECT LOWER(TRIM(name)) AS clean_name FROM users";
350
+ const fields = extractSelectFields(query);
351
+ // Should extract 'name' from nested functions - currently extracts last match
352
+ assert.ok(fields.length >= 0); // At minimum should not crash
353
+ });
354
+
355
+ test("handles query with JOIN", () => {
356
+ const query = "SELECT users.name, orders.total FROM users JOIN orders ON users.id = orders.user_id";
357
+ const fields = extractSelectFields(query);
358
+ assert.ok(fields.includes("name"));
359
+ assert.ok(fields.includes("total"));
360
+ });
361
+
362
+ test("handles DISTINCT keyword", () => {
363
+ const query = "SELECT DISTINCT status, type FROM users";
364
+ const fields = extractSelectFields(query);
365
+ assert.ok(fields.includes("status"));
366
+ assert.ok(fields.includes("type"));
367
+ });
368
+
369
+ test("handles query generated by search function pattern", () => {
370
+ // Pattern: SELECT columns FROM `collection` WHERE conditions ORDER BY ... LIMIT ... OFFSET ...
371
+ const query = `SELECT first_name, last_name, email, META(\`users\`).id AS id FROM \`users\` WHERE type = 'user' AND status = 'active' ORDER BY created_at LIMIT 100 OFFSET 0`;
372
+ const fields = extractSelectFields(query);
373
+ assert.ok(fields.includes("first_name"));
374
+ assert.ok(fields.includes("last_name"));
375
+ assert.ok(fields.includes("email"));
376
+ assert.ok(!fields.includes("id")); // META().id should be skipped
377
+ });
378
+
379
+ test("handles backtick collection prefix", () => {
380
+ const query = "SELECT `users`.`first_name`, `users`.`last_name` FROM `users`";
381
+ const fields = extractSelectFields(query);
382
+ assert.ok(fields.includes("first_name"));
383
+ assert.ok(fields.includes("last_name"));
384
+ });
385
+
386
+ // ============================================
387
+ // Tests for extractGroupByFields
388
+ // ============================================
389
+ console.log("\n--- extractGroupByFields tests ---");
390
+
391
+ test("extracts simple GROUP BY field", () => {
392
+ const query = "SELECT status, COUNT(*) FROM users GROUP BY status";
393
+ const fields = extractGroupByFields(query);
394
+ assert.ok(arraysEqual(fields, ["status"]));
395
+ });
396
+
397
+ test("extracts multiple GROUP BY fields", () => {
398
+ const query = "SELECT status, type, COUNT(*) FROM users GROUP BY status, type";
399
+ const fields = extractGroupByFields(query);
400
+ assert.ok(arraysEqual(fields, ["status", "type"]));
401
+ });
402
+
403
+ test("extracts GROUP BY with backtick-quoted fields", () => {
404
+ const query = "SELECT `status`, COUNT(*) FROM users GROUP BY `status`";
405
+ const fields = extractGroupByFields(query);
406
+ assert.ok(arraysEqual(fields, ["status"]));
407
+ });
408
+
409
+ test("extracts GROUP BY with collection prefix", () => {
410
+ const query = "SELECT status, COUNT(*) FROM users GROUP BY users.status";
411
+ const fields = extractGroupByFields(query);
412
+ assert.ok(arraysEqual(fields, ["status"]));
413
+ });
414
+
415
+ test("extracts GROUP BY with backtick collection prefix", () => {
416
+ const query = "SELECT status, COUNT(*) FROM users GROUP BY `users`.`status`";
417
+ const fields = extractGroupByFields(query);
418
+ assert.ok(arraysEqual(fields, ["status"]));
419
+ });
420
+
421
+ test("returns empty array when no GROUP BY clause", () => {
422
+ const query = "SELECT name FROM users WHERE status = 'active'";
423
+ const fields = extractGroupByFields(query);
424
+ assert.ok(arraysEqual(fields, []));
425
+ });
426
+
427
+ test("stops at HAVING", () => {
428
+ const query = "SELECT status, COUNT(*) as cnt FROM users GROUP BY status HAVING COUNT(*) > 5";
429
+ const fields = extractGroupByFields(query);
430
+ assert.ok(arraysEqual(fields, ["status"]));
431
+ });
432
+
433
+ test("stops at ORDER BY", () => {
434
+ const query = "SELECT status, COUNT(*) FROM users GROUP BY status ORDER BY status";
435
+ const fields = extractGroupByFields(query);
436
+ assert.ok(arraysEqual(fields, ["status"]));
437
+ });
438
+
439
+ test("stops at LIMIT", () => {
440
+ const query = "SELECT status, COUNT(*) FROM users GROUP BY status LIMIT 10";
441
+ const fields = extractGroupByFields(query);
442
+ assert.ok(arraysEqual(fields, ["status"]));
443
+ });
444
+
445
+ test("handles GROUP BY with HAVING and ORDER BY", () => {
446
+ const query = "SELECT status, COUNT(*) FROM users GROUP BY status HAVING COUNT(*) > 5 ORDER BY status";
447
+ const fields = extractGroupByFields(query);
448
+ assert.ok(arraysEqual(fields, ["status"]));
449
+ });
450
+
451
+ // ============================================
452
+ // Tests for extractOrderByFields
453
+ // ============================================
454
+ console.log("\n--- extractOrderByFields tests ---");
455
+
456
+ test("extracts simple ORDER BY field", () => {
457
+ const query = "SELECT name FROM users ORDER BY created_at";
458
+ const fields = extractOrderByFields(query);
459
+ assert.ok(arraysEqual(fields, ["created_at"]));
460
+ });
461
+
462
+ test("extracts ORDER BY field with ASC", () => {
463
+ const query = "SELECT name FROM users ORDER BY created_at ASC";
464
+ const fields = extractOrderByFields(query);
465
+ assert.ok(arraysEqual(fields, ["created_at"]));
466
+ });
467
+
468
+ test("extracts ORDER BY field with DESC", () => {
469
+ const query = "SELECT name FROM users ORDER BY created_at DESC";
470
+ const fields = extractOrderByFields(query);
471
+ assert.ok(arraysEqual(fields, ["created_at"]));
472
+ });
473
+
474
+ test("extracts multiple ORDER BY fields", () => {
475
+ const query = "SELECT name FROM users ORDER BY last_name ASC, first_name ASC";
476
+ const fields = extractOrderByFields(query);
477
+ assert.ok(arraysEqual(fields, ["last_name", "first_name"]));
478
+ });
479
+
480
+ test("extracts ORDER BY with backtick-quoted fields", () => {
481
+ const query = "SELECT name FROM users ORDER BY `created_at` DESC";
482
+ const fields = extractOrderByFields(query);
483
+ assert.ok(arraysEqual(fields, ["created_at"]));
484
+ });
485
+
486
+ test("extracts ORDER BY with collection prefix", () => {
487
+ const query = "SELECT name FROM users ORDER BY users.created_at DESC";
488
+ const fields = extractOrderByFields(query);
489
+ assert.ok(arraysEqual(fields, ["created_at"]));
490
+ });
491
+
492
+ test("extracts ORDER BY with backtick collection prefix", () => {
493
+ const query = "SELECT name FROM users ORDER BY `users`.`created_at` DESC";
494
+ const fields = extractOrderByFields(query);
495
+ assert.ok(arraysEqual(fields, ["created_at"]));
496
+ });
497
+
498
+ test("returns empty array when no ORDER BY clause", () => {
499
+ const query = "SELECT name FROM users WHERE status = 'active'";
500
+ const fields = extractOrderByFields(query);
501
+ assert.ok(arraysEqual(fields, []));
502
+ });
503
+
504
+ test("stops at LIMIT", () => {
505
+ const query = "SELECT name FROM users ORDER BY created_at DESC LIMIT 10";
506
+ const fields = extractOrderByFields(query);
507
+ assert.ok(arraysEqual(fields, ["created_at"]));
508
+ });
509
+
510
+ test("stops at OFFSET", () => {
511
+ const query = "SELECT name FROM users ORDER BY created_at DESC OFFSET 20";
512
+ const fields = extractOrderByFields(query);
513
+ assert.ok(arraysEqual(fields, ["created_at"]));
514
+ });
515
+
516
+ test("handles ORDER BY with LIMIT and OFFSET", () => {
517
+ const query = "SELECT name FROM users ORDER BY created_at DESC LIMIT 10 OFFSET 20";
518
+ const fields = extractOrderByFields(query);
519
+ assert.ok(arraysEqual(fields, ["created_at"]));
520
+ });
521
+
522
+ test("handles mixed ASC and DESC", () => {
523
+ const query = "SELECT name FROM users ORDER BY last_name ASC, created_at DESC";
524
+ const fields = extractOrderByFields(query);
525
+ assert.ok(arraysEqual(fields, ["last_name", "created_at"]));
526
+ });
527
+
528
+ // ============================================
529
+ // Tests for buildCoveringIndex
530
+ // ============================================
531
+ console.log("\n--- buildCoveringIndex tests ---");
532
+
533
+ const defaultOptions = {
534
+ bucket: "saturn",
535
+ scope: "test-scope",
536
+ collection: "users",
537
+ };
538
+
539
+ test("builds basic covering index", () => {
540
+ const query = "SELECT name, email FROM users WHERE status = 'active'";
541
+ const result = buildCoveringIndex(query, defaultOptions);
542
+
543
+ assert.ok(result !== null);
544
+ assert.ok(result.startsWith("CREATE INDEX"));
545
+ assert.ok(result.includes("`deleted` INCLUDE MISSING"));
546
+ assert.ok(result.includes("`status`"));
547
+ assert.ok(result.includes("`name`"));
548
+ assert.ok(result.includes("`email`"));
549
+ assert.ok(result.includes("`saturn`.`test-scope`.`users`"));
550
+ });
551
+
552
+ test("deleted field comes first", () => {
553
+ const query = "SELECT name FROM users WHERE status = 'active'";
554
+ const result = buildCoveringIndex(query, defaultOptions);
555
+
556
+ const deletedIndex = result.indexOf("`deleted` INCLUDE MISSING");
557
+ const statusIndex = result.indexOf("`status`");
558
+ assert.ok(deletedIndex < statusIndex);
559
+ });
560
+
561
+ test("WHERE fields come before SELECT fields", () => {
562
+ const query = "SELECT email FROM users WHERE status = 'active'";
563
+ const result = buildCoveringIndex(query, defaultOptions);
564
+
565
+ const statusIndex = result.indexOf("`status`");
566
+ const emailIndex = result.indexOf("`email`");
567
+ assert.ok(statusIndex < emailIndex);
568
+ });
569
+
570
+ test("deduplicates fields in WHERE and SELECT", () => {
571
+ const query = "SELECT status, name FROM users WHERE status = 'active'";
572
+ const result = buildCoveringIndex(query, defaultOptions);
573
+
574
+ // status should only appear once
575
+ const firstStatus = result.indexOf("`status`");
576
+ const lastStatus = result.lastIndexOf("`status`");
577
+ assert.strictEqual(firstStatus, lastStatus);
578
+ });
579
+
580
+ test("excludes deleted from WHERE fields (added separately)", () => {
581
+ const query = "SELECT name FROM users WHERE (deleted = false OR deleted IS MISSING) AND status = 'active'";
582
+ const result = buildCoveringIndex(query, defaultOptions);
583
+
584
+ // deleted should only appear once (as INCLUDE MISSING)
585
+ const matches = result.match(/`deleted`/g);
586
+ assert.strictEqual(matches.length, 1);
587
+ assert.ok(result.includes("`deleted` INCLUDE MISSING"));
588
+ });
589
+
590
+ test("returns null for SELECT * query", () => {
591
+ const query = "SELECT * FROM users WHERE status = 'active'";
592
+ const result = buildCoveringIndex(query, defaultOptions);
593
+ assert.strictEqual(result, null);
594
+ });
595
+
596
+ test("returns null when missing bucket option", () => {
597
+ const query = "SELECT name FROM users WHERE status = 'active'";
598
+ const result = buildCoveringIndex(query, { scope: "test", collection: "users" });
599
+ assert.strictEqual(result, null);
600
+ });
601
+
602
+ test("returns null when missing scope option", () => {
603
+ const query = "SELECT name FROM users WHERE status = 'active'";
604
+ const result = buildCoveringIndex(query, { bucket: "test", collection: "users" });
605
+ assert.strictEqual(result, null);
606
+ });
607
+
608
+ test("returns null when missing collection option", () => {
609
+ const query = "SELECT name FROM users WHERE status = 'active'";
610
+ const result = buildCoveringIndex(query, { bucket: "test", scope: "scope" });
611
+ assert.strictEqual(result, null);
612
+ });
613
+
614
+ test("generates unique index name from query hash", () => {
615
+ const query1 = "SELECT name FROM users WHERE status = 'active'";
616
+ const query2 = "SELECT email FROM users WHERE type = 'admin'";
617
+
618
+ const result1 = buildCoveringIndex(query1, defaultOptions);
619
+ const result2 = buildCoveringIndex(query2, defaultOptions);
620
+
621
+ // Extract index names
622
+ const name1 = result1.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
623
+ const name2 = result2.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
624
+
625
+ assert.notStrictEqual(name1, name2);
626
+ });
627
+
628
+ test("handles complex query with multiple conditions", () => {
629
+ const query = `
630
+ SELECT first_name, last_name, email, status
631
+ FROM users
632
+ WHERE (deleted = false OR deleted IS MISSING)
633
+ AND status = 'active'
634
+ AND created_at > '2024-01-01'
635
+ ORDER BY created_at
636
+ LIMIT 100
637
+ `;
638
+ const result = buildCoveringIndex(query, defaultOptions);
639
+
640
+ assert.ok(result !== null);
641
+ assert.ok(result.includes("`deleted` INCLUDE MISSING"));
642
+ assert.ok(result.includes("`status`"));
643
+ assert.ok(result.includes("`created_at`"));
644
+ assert.ok(result.includes("`first_name`"));
645
+ assert.ok(result.includes("`last_name`"));
646
+ assert.ok(result.includes("`email`"));
647
+ });
648
+
649
+ test("returns null when only deleted field in query", () => {
650
+ const query = "SELECT deleted FROM users WHERE deleted = false";
651
+ const result = buildCoveringIndex(query, defaultOptions);
652
+ // Should return null because after filtering out 'deleted', there are no fields left
653
+ assert.strictEqual(result, null);
654
+ });
655
+
656
+ test("handles query with type field (legacy pattern)", () => {
657
+ const query = "SELECT name, email FROM bucket WHERE type = 'user' AND status = 'active'";
658
+ const result = buildCoveringIndex(query, defaultOptions);
659
+
660
+ assert.ok(result !== null);
661
+ assert.ok(result.includes("`type`"));
662
+ assert.ok(result.includes("`status`"));
663
+ assert.ok(result.includes("`name`"));
664
+ assert.ok(result.includes("`email`"));
665
+ });
666
+
667
+ test("handles realistic search query pattern", () => {
668
+ const query = `SELECT first_name, last_name, email, status, META(\`users\`).id AS id
669
+ FROM \`users\`
670
+ WHERE (deleted = false OR deleted IS MISSING) AND status = 'active'
671
+ ORDER BY created_at DESC
672
+ LIMIT 50 OFFSET 0`;
673
+ const result = buildCoveringIndex(query, defaultOptions);
674
+
675
+ assert.ok(result !== null);
676
+ assert.ok(result.includes("`deleted` INCLUDE MISSING"));
677
+ assert.ok(result.includes("`status`"));
678
+ assert.ok(result.includes("`first_name`"));
679
+ assert.ok(result.includes("`last_name`"));
680
+ assert.ok(result.includes("`email`"));
681
+ });
682
+
683
+ test("handles query with BETWEEN in WHERE", () => {
684
+ const query = "SELECT name FROM users WHERE age BETWEEN 18 AND 65";
685
+ const result = buildCoveringIndex(query, defaultOptions);
686
+
687
+ assert.ok(result !== null);
688
+ assert.ok(result.includes("`age`"));
689
+ assert.ok(result.includes("`name`"));
690
+ });
691
+
692
+ test("handles query with multiple OR conditions", () => {
693
+ const query = "SELECT name FROM users WHERE status = 'active' OR status = 'pending' OR status = 'review'";
694
+ const result = buildCoveringIndex(query, defaultOptions);
695
+
696
+ assert.ok(result !== null);
697
+ assert.ok(result.includes("`status`"));
698
+ assert.ok(result.includes("`name`"));
699
+ });
700
+
701
+ test("consistent index name for same query", () => {
702
+ const query = "SELECT name FROM users WHERE status = 'active'";
703
+ const result1 = buildCoveringIndex(query, defaultOptions);
704
+ const result2 = buildCoveringIndex(query, defaultOptions);
705
+
706
+ assert.strictEqual(result1, result2);
707
+ });
708
+
709
+ test("index includes all fields needed for covering", () => {
710
+ const query = `SELECT
711
+ first_name,
712
+ middle_name,
713
+ last_name,
714
+ status,
715
+ constituency,
716
+ division,
717
+ description
718
+ FROM subject
719
+ WHERE (deleted = false OR deleted IS MISSING)
720
+ AND created_at > '2024-01-01'`;
721
+ const result = buildCoveringIndex(query, defaultOptions);
722
+
723
+ // Verify structure matches the working example pattern
724
+ assert.ok(result.startsWith("CREATE INDEX `adv_covered_"));
725
+ assert.ok(result.includes("`deleted` INCLUDE MISSING"));
726
+ assert.ok(result.includes("`created_at`"));
727
+ assert.ok(result.includes("`first_name`"));
728
+ assert.ok(result.includes("`middle_name`"));
729
+ assert.ok(result.includes("`last_name`"));
730
+ assert.ok(result.includes("`status`"));
731
+ assert.ok(result.includes("`constituency`"));
732
+ assert.ok(result.includes("`division`"));
733
+ assert.ok(result.includes("`description`"));
734
+ });
735
+
736
+ test("handles query with ANY array operator", () => {
737
+ const query = `SELECT name, email FROM users
738
+ WHERE (deleted = false OR deleted IS MISSING)
739
+ AND ANY tag IN tags SATISFIES tag = 'premium' END`;
740
+ const result = buildCoveringIndex(query, defaultOptions);
741
+
742
+ assert.ok(result !== null);
743
+ assert.ok(result.includes("`deleted` INCLUDE MISSING"));
744
+ assert.ok(result.includes("`tags`"));
745
+ assert.ok(result.includes("`name`"));
746
+ assert.ok(result.includes("`email`"));
747
+ });
748
+
749
+ test("handles query with EVERY array operator", () => {
750
+ const query = `SELECT name FROM users
751
+ WHERE status = 'active'
752
+ AND EVERY score IN scores SATISFIES score > 0 END`;
753
+ const result = buildCoveringIndex(query, defaultOptions);
754
+
755
+ assert.ok(result !== null);
756
+ assert.ok(result.includes("`status`"));
757
+ assert.ok(result.includes("`scores`"));
758
+ assert.ok(result.includes("`name`"));
759
+ });
760
+
761
+ test("handles complex query with multiple ANY/EVERY", () => {
762
+ const query = `SELECT name, email, status FROM users
763
+ WHERE (deleted = false OR deleted IS MISSING)
764
+ AND type = 'user'
765
+ AND ANY tag IN tags SATISFIES tag = 'active' END
766
+ AND EVERY perm IN permissions SATISFIES perm.level >= 1 END`;
767
+ const result = buildCoveringIndex(query, defaultOptions);
768
+
769
+ assert.ok(result !== null);
770
+ assert.ok(result.includes("`deleted` INCLUDE MISSING"));
771
+ assert.ok(result.includes("`type`"));
772
+ assert.ok(result.includes("`tags`"));
773
+ assert.ok(result.includes("`permissions`"));
774
+ assert.ok(result.includes("`name`"));
775
+ assert.ok(result.includes("`email`"));
776
+ assert.ok(result.includes("`status`"));
777
+ });
778
+
779
+ test("includes ORDER BY fields in index", () => {
780
+ const query = "SELECT name, email FROM users WHERE status = 'active' ORDER BY created_at DESC";
781
+ const result = buildCoveringIndex(query, defaultOptions);
782
+
783
+ assert.ok(result !== null);
784
+ assert.ok(result.includes("`deleted` INCLUDE MISSING"));
785
+ assert.ok(result.includes("`status`"));
786
+ assert.ok(result.includes("`created_at`"));
787
+ assert.ok(result.includes("`name`"));
788
+ assert.ok(result.includes("`email`"));
789
+ });
790
+
791
+ test("ORDER BY fields come after WHERE fields", () => {
792
+ const query = "SELECT email FROM users WHERE status = 'active' ORDER BY created_at";
793
+ const result = buildCoveringIndex(query, defaultOptions);
794
+
795
+ const statusIndex = result.indexOf("`status`");
796
+ const createdAtIndex = result.indexOf("`created_at`");
797
+ const emailIndex = result.indexOf("`email`");
798
+
799
+ // WHERE field (status) should come before ORDER BY field (created_at)
800
+ assert.ok(statusIndex < createdAtIndex);
801
+ // ORDER BY field (created_at) should come before SELECT field (email)
802
+ assert.ok(createdAtIndex < emailIndex);
803
+ });
804
+
805
+ test("deduplicates ORDER BY field if already in WHERE", () => {
806
+ const query = "SELECT name FROM users WHERE status = 'active' AND created_at > '2024-01-01' ORDER BY created_at DESC";
807
+ const result = buildCoveringIndex(query, defaultOptions);
808
+
809
+ // created_at should only appear once
810
+ const firstCreatedAt = result.indexOf("`created_at`");
811
+ const lastCreatedAt = result.lastIndexOf("`created_at`");
812
+ assert.strictEqual(firstCreatedAt, lastCreatedAt);
813
+ });
814
+
815
+ test("deduplicates ORDER BY field if already in SELECT", () => {
816
+ const query = "SELECT name, created_at FROM users WHERE status = 'active' ORDER BY created_at DESC";
817
+ const result = buildCoveringIndex(query, defaultOptions);
818
+
819
+ // created_at should only appear once (in ORDER BY position, before SELECT fields)
820
+ const matches = result.match(/`created_at`/g);
821
+ assert.strictEqual(matches.length, 1);
822
+ });
823
+
824
+ test("handles multiple ORDER BY fields", () => {
825
+ const query = "SELECT email FROM users WHERE status = 'active' ORDER BY last_name ASC, first_name ASC";
826
+ const result = buildCoveringIndex(query, defaultOptions);
827
+
828
+ assert.ok(result.includes("`last_name`"));
829
+ assert.ok(result.includes("`first_name`"));
830
+
831
+ // ORDER BY fields should come before SELECT fields
832
+ const lastNameIndex = result.indexOf("`last_name`");
833
+ const firstNameIndex = result.indexOf("`first_name`");
834
+ const emailIndex = result.indexOf("`email`");
835
+
836
+ assert.ok(lastNameIndex < emailIndex);
837
+ assert.ok(firstNameIndex < emailIndex);
838
+ });
839
+
840
+ test("handles realistic query with ORDER BY", () => {
841
+ const query = `SELECT first_name, last_name, email, status
842
+ FROM users
843
+ WHERE (deleted = false OR deleted IS MISSING) AND status = 'active'
844
+ ORDER BY created_at DESC
845
+ LIMIT 50 OFFSET 0`;
846
+ const result = buildCoveringIndex(query, defaultOptions);
847
+
848
+ assert.ok(result !== null);
849
+ assert.ok(result.includes("`deleted` INCLUDE MISSING"));
850
+ assert.ok(result.includes("`status`"));
851
+ assert.ok(result.includes("`created_at`"));
852
+ assert.ok(result.includes("`first_name`"));
853
+ assert.ok(result.includes("`last_name`"));
854
+ assert.ok(result.includes("`email`"));
855
+
856
+ // Verify order: status (WHERE) -> created_at (ORDER BY) -> SELECT fields
857
+ const statusIndex = result.indexOf("`status`");
858
+ const createdAtIndex = result.indexOf("`created_at`");
859
+ const firstNameIndex = result.indexOf("`first_name`");
860
+
861
+ assert.ok(statusIndex < createdAtIndex);
862
+ assert.ok(createdAtIndex < firstNameIndex);
863
+ });
864
+
865
+ test("includes GROUP BY fields in index", () => {
866
+ const query = "SELECT status, COUNT(name) as cnt FROM users WHERE type = 'user' GROUP BY status";
867
+ const result = buildCoveringIndex(query, defaultOptions);
868
+
869
+ assert.ok(result !== null);
870
+ assert.ok(result.includes("`deleted` INCLUDE MISSING"));
871
+ assert.ok(result.includes("`type`"));
872
+ assert.ok(result.includes("`status`"));
873
+ assert.ok(result.includes("`name`"));
874
+ });
875
+
876
+ test("GROUP BY fields come after WHERE fields", () => {
877
+ const query = "SELECT COUNT(name) FROM users WHERE type = 'user' GROUP BY status";
878
+ const result = buildCoveringIndex(query, defaultOptions);
879
+
880
+ const typeIndex = result.indexOf("`type`");
881
+ const statusIndex = result.indexOf("`status`");
882
+
883
+ // WHERE field (type) should come before GROUP BY field (status)
884
+ assert.ok(typeIndex < statusIndex);
885
+ });
886
+
887
+ test("GROUP BY fields come before ORDER BY fields", () => {
888
+ const query = "SELECT COUNT(name) FROM users WHERE type = 'user' GROUP BY status ORDER BY status";
889
+ const result = buildCoveringIndex(query, defaultOptions);
890
+
891
+ const typeIndex = result.indexOf("`type`");
892
+ const statusIndex = result.indexOf("`status`");
893
+
894
+ // status is in both GROUP BY and ORDER BY, should only appear once
895
+ const matches = result.match(/`status`/g);
896
+ assert.strictEqual(matches.length, 1);
897
+
898
+ // WHERE field (type) should come before GROUP BY field (status)
899
+ assert.ok(typeIndex < statusIndex);
900
+ });
901
+
902
+ test("deduplicates GROUP BY field if already in WHERE", () => {
903
+ const query = "SELECT COUNT(name) FROM users WHERE status = 'active' GROUP BY status";
904
+ const result = buildCoveringIndex(query, defaultOptions);
905
+
906
+ // status should only appear once
907
+ const matches = result.match(/`status`/g);
908
+ assert.strictEqual(matches.length, 1);
909
+ });
910
+
911
+ test("handles multiple GROUP BY fields", () => {
912
+ const query = "SELECT status, type, COUNT(name) FROM users WHERE category = 'A' GROUP BY status, type";
913
+ const result = buildCoveringIndex(query, defaultOptions);
914
+
915
+ assert.ok(result.includes("`category`"));
916
+ assert.ok(result.includes("`status`"));
917
+ assert.ok(result.includes("`type`"));
918
+ assert.ok(result.includes("`name`"));
919
+ });
920
+
921
+ test("handles GROUP BY with HAVING and ORDER BY", () => {
922
+ const query = `SELECT status, COUNT(name) as cnt
923
+ FROM users
924
+ WHERE type = 'user'
925
+ GROUP BY status
926
+ HAVING COUNT(name) > 5
927
+ ORDER BY cnt DESC`;
928
+ const result = buildCoveringIndex(query, defaultOptions);
929
+
930
+ assert.ok(result !== null);
931
+ assert.ok(result.includes("`type`"));
932
+ assert.ok(result.includes("`status`"));
933
+ assert.ok(result.includes("`name`"));
934
+ });
935
+
936
+ test("handles realistic GROUP BY query", () => {
937
+ const query = `SELECT status, COUNT(first_name) as user_count
938
+ FROM users
939
+ WHERE (deleted = false OR deleted IS MISSING) AND type = 'user'
940
+ GROUP BY status
941
+ ORDER BY user_count DESC
942
+ LIMIT 10`;
943
+ const result = buildCoveringIndex(query, defaultOptions);
944
+
945
+ assert.ok(result !== null);
946
+ assert.ok(result.includes("`deleted` INCLUDE MISSING"));
947
+ assert.ok(result.includes("`type`"));
948
+ assert.ok(result.includes("`status`"));
949
+ assert.ok(result.includes("`first_name`"));
950
+
951
+ // Verify order: type (WHERE) -> status (GROUP BY) -> first_name (SELECT)
952
+ const typeIndex = result.indexOf("`type`");
953
+ const statusIndex = result.indexOf("`status`");
954
+ const firstNameIndex = result.indexOf("`first_name`");
955
+
956
+ assert.ok(typeIndex < statusIndex);
957
+ assert.ok(statusIndex < firstNameIndex);
958
+ });
959
+
960
+ test("field order: WHERE -> GROUP BY -> ORDER BY -> SELECT", () => {
961
+ const query = `SELECT email
962
+ FROM users
963
+ WHERE type = 'user'
964
+ GROUP BY status
965
+ ORDER BY created_at`;
966
+ const result = buildCoveringIndex(query, defaultOptions);
967
+
968
+ const typeIndex = result.indexOf("`type`");
969
+ const statusIndex = result.indexOf("`status`");
970
+ const createdAtIndex = result.indexOf("`created_at`");
971
+ const emailIndex = result.indexOf("`email`");
972
+
973
+ // Verify correct order
974
+ assert.ok(typeIndex < statusIndex); // WHERE before GROUP BY
975
+ assert.ok(statusIndex < createdAtIndex); // GROUP BY before ORDER BY
976
+ assert.ok(createdAtIndex < emailIndex); // ORDER BY before SELECT
977
+ });
978
+
979
+ // ============================================
980
+ // Summary
981
+ // ============================================
982
+ console.log("\n========================================");
983
+ console.log(`Tests: ${passed + failed} | Passed: ${passed} | Failed: ${failed}`);
984
+ console.log("========================================\n");
985
+
986
+ process.exit(failed > 0 ? 1 : 0);