spice-couchbase 2.0.45 → 2.0.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +69 -15
- package/build/utility.js +347 -0
- package/build/utility.test.js +807 -0
- package/package.json +2 -2
- package/src/index.js +79 -30
- package/src/utility.js +370 -0
- package/src/utility.test.js +1017 -0
|
@@ -0,0 +1,1017 @@
|
|
|
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("same index name for different queries with same fields", () => {
|
|
710
|
+
// Two different queries that would be covered by the same index
|
|
711
|
+
const query1 = "SELECT name, email FROM users WHERE status = 'active'";
|
|
712
|
+
const query2 = "SELECT email, name FROM users WHERE status = 'pending'";
|
|
713
|
+
|
|
714
|
+
const result1 = buildCoveringIndex(query1, defaultOptions);
|
|
715
|
+
const result2 = buildCoveringIndex(query2, defaultOptions);
|
|
716
|
+
|
|
717
|
+
// Extract index names
|
|
718
|
+
const name1 = result1.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
719
|
+
const name2 = result2.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
720
|
+
|
|
721
|
+
// Should have the same index name since they have the same fields
|
|
722
|
+
assert.strictEqual(name1, name2);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test("different index name for queries with different fields", () => {
|
|
726
|
+
const query1 = "SELECT name FROM users WHERE status = 'active'";
|
|
727
|
+
const query2 = "SELECT email FROM users WHERE type = 'admin'";
|
|
728
|
+
|
|
729
|
+
const result1 = buildCoveringIndex(query1, defaultOptions);
|
|
730
|
+
const result2 = buildCoveringIndex(query2, defaultOptions);
|
|
731
|
+
|
|
732
|
+
// Extract index names
|
|
733
|
+
const name1 = result1.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
734
|
+
const name2 = result2.match(/CREATE INDEX `(adv_covered_\w+)`/)[1];
|
|
735
|
+
|
|
736
|
+
// Should have different index names since they have different fields
|
|
737
|
+
assert.notStrictEqual(name1, name2);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("index includes all fields needed for covering", () => {
|
|
741
|
+
const query = `SELECT
|
|
742
|
+
first_name,
|
|
743
|
+
middle_name,
|
|
744
|
+
last_name,
|
|
745
|
+
status,
|
|
746
|
+
constituency,
|
|
747
|
+
division,
|
|
748
|
+
description
|
|
749
|
+
FROM subject
|
|
750
|
+
WHERE (deleted = false OR deleted IS MISSING)
|
|
751
|
+
AND created_at > '2024-01-01'`;
|
|
752
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
753
|
+
|
|
754
|
+
// Verify structure matches the working example pattern
|
|
755
|
+
assert.ok(result.startsWith("CREATE INDEX `adv_covered_"));
|
|
756
|
+
assert.ok(result.includes("`deleted` INCLUDE MISSING"));
|
|
757
|
+
assert.ok(result.includes("`created_at`"));
|
|
758
|
+
assert.ok(result.includes("`first_name`"));
|
|
759
|
+
assert.ok(result.includes("`middle_name`"));
|
|
760
|
+
assert.ok(result.includes("`last_name`"));
|
|
761
|
+
assert.ok(result.includes("`status`"));
|
|
762
|
+
assert.ok(result.includes("`constituency`"));
|
|
763
|
+
assert.ok(result.includes("`division`"));
|
|
764
|
+
assert.ok(result.includes("`description`"));
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test("handles query with ANY array operator", () => {
|
|
768
|
+
const query = `SELECT name, email FROM users
|
|
769
|
+
WHERE (deleted = false OR deleted IS MISSING)
|
|
770
|
+
AND ANY tag IN tags SATISFIES tag = 'premium' END`;
|
|
771
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
772
|
+
|
|
773
|
+
assert.ok(result !== null);
|
|
774
|
+
assert.ok(result.includes("`deleted` INCLUDE MISSING"));
|
|
775
|
+
assert.ok(result.includes("`tags`"));
|
|
776
|
+
assert.ok(result.includes("`name`"));
|
|
777
|
+
assert.ok(result.includes("`email`"));
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("handles query with EVERY array operator", () => {
|
|
781
|
+
const query = `SELECT name FROM users
|
|
782
|
+
WHERE status = 'active'
|
|
783
|
+
AND EVERY score IN scores SATISFIES score > 0 END`;
|
|
784
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
785
|
+
|
|
786
|
+
assert.ok(result !== null);
|
|
787
|
+
assert.ok(result.includes("`status`"));
|
|
788
|
+
assert.ok(result.includes("`scores`"));
|
|
789
|
+
assert.ok(result.includes("`name`"));
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test("handles complex query with multiple ANY/EVERY", () => {
|
|
793
|
+
const query = `SELECT name, email, status FROM users
|
|
794
|
+
WHERE (deleted = false OR deleted IS MISSING)
|
|
795
|
+
AND type = 'user'
|
|
796
|
+
AND ANY tag IN tags SATISFIES tag = 'active' END
|
|
797
|
+
AND EVERY perm IN permissions SATISFIES perm.level >= 1 END`;
|
|
798
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
799
|
+
|
|
800
|
+
assert.ok(result !== null);
|
|
801
|
+
assert.ok(result.includes("`deleted` INCLUDE MISSING"));
|
|
802
|
+
assert.ok(result.includes("`type`"));
|
|
803
|
+
assert.ok(result.includes("`tags`"));
|
|
804
|
+
assert.ok(result.includes("`permissions`"));
|
|
805
|
+
assert.ok(result.includes("`name`"));
|
|
806
|
+
assert.ok(result.includes("`email`"));
|
|
807
|
+
assert.ok(result.includes("`status`"));
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test("includes ORDER BY fields in index", () => {
|
|
811
|
+
const query = "SELECT name, email FROM users WHERE status = 'active' ORDER BY created_at DESC";
|
|
812
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
813
|
+
|
|
814
|
+
assert.ok(result !== null);
|
|
815
|
+
assert.ok(result.includes("`deleted` INCLUDE MISSING"));
|
|
816
|
+
assert.ok(result.includes("`status`"));
|
|
817
|
+
assert.ok(result.includes("`created_at`"));
|
|
818
|
+
assert.ok(result.includes("`name`"));
|
|
819
|
+
assert.ok(result.includes("`email`"));
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
test("ORDER BY fields come after WHERE fields", () => {
|
|
823
|
+
const query = "SELECT email FROM users WHERE status = 'active' ORDER BY created_at";
|
|
824
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
825
|
+
|
|
826
|
+
const statusIndex = result.indexOf("`status`");
|
|
827
|
+
const createdAtIndex = result.indexOf("`created_at`");
|
|
828
|
+
const emailIndex = result.indexOf("`email`");
|
|
829
|
+
|
|
830
|
+
// WHERE field (status) should come before ORDER BY field (created_at)
|
|
831
|
+
assert.ok(statusIndex < createdAtIndex);
|
|
832
|
+
// ORDER BY field (created_at) should come before SELECT field (email)
|
|
833
|
+
assert.ok(createdAtIndex < emailIndex);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
test("deduplicates ORDER BY field if already in WHERE", () => {
|
|
837
|
+
const query = "SELECT name FROM users WHERE status = 'active' AND created_at > '2024-01-01' ORDER BY created_at DESC";
|
|
838
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
839
|
+
|
|
840
|
+
// created_at should only appear once
|
|
841
|
+
const firstCreatedAt = result.indexOf("`created_at`");
|
|
842
|
+
const lastCreatedAt = result.lastIndexOf("`created_at`");
|
|
843
|
+
assert.strictEqual(firstCreatedAt, lastCreatedAt);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
test("deduplicates ORDER BY field if already in SELECT", () => {
|
|
847
|
+
const query = "SELECT name, created_at FROM users WHERE status = 'active' ORDER BY created_at DESC";
|
|
848
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
849
|
+
|
|
850
|
+
// created_at should only appear once (in ORDER BY position, before SELECT fields)
|
|
851
|
+
const matches = result.match(/`created_at`/g);
|
|
852
|
+
assert.strictEqual(matches.length, 1);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test("handles multiple ORDER BY fields", () => {
|
|
856
|
+
const query = "SELECT email FROM users WHERE status = 'active' ORDER BY last_name ASC, first_name ASC";
|
|
857
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
858
|
+
|
|
859
|
+
assert.ok(result.includes("`last_name`"));
|
|
860
|
+
assert.ok(result.includes("`first_name`"));
|
|
861
|
+
|
|
862
|
+
// ORDER BY fields should come before SELECT fields
|
|
863
|
+
const lastNameIndex = result.indexOf("`last_name`");
|
|
864
|
+
const firstNameIndex = result.indexOf("`first_name`");
|
|
865
|
+
const emailIndex = result.indexOf("`email`");
|
|
866
|
+
|
|
867
|
+
assert.ok(lastNameIndex < emailIndex);
|
|
868
|
+
assert.ok(firstNameIndex < emailIndex);
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
test("handles realistic query with ORDER BY", () => {
|
|
872
|
+
const query = `SELECT first_name, last_name, email, status
|
|
873
|
+
FROM users
|
|
874
|
+
WHERE (deleted = false OR deleted IS MISSING) AND status = 'active'
|
|
875
|
+
ORDER BY created_at DESC
|
|
876
|
+
LIMIT 50 OFFSET 0`;
|
|
877
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
878
|
+
|
|
879
|
+
assert.ok(result !== null);
|
|
880
|
+
assert.ok(result.includes("`deleted` INCLUDE MISSING"));
|
|
881
|
+
assert.ok(result.includes("`status`"));
|
|
882
|
+
assert.ok(result.includes("`created_at`"));
|
|
883
|
+
assert.ok(result.includes("`first_name`"));
|
|
884
|
+
assert.ok(result.includes("`last_name`"));
|
|
885
|
+
assert.ok(result.includes("`email`"));
|
|
886
|
+
|
|
887
|
+
// Verify order: status (WHERE) -> created_at (ORDER BY) -> SELECT fields
|
|
888
|
+
const statusIndex = result.indexOf("`status`");
|
|
889
|
+
const createdAtIndex = result.indexOf("`created_at`");
|
|
890
|
+
const firstNameIndex = result.indexOf("`first_name`");
|
|
891
|
+
|
|
892
|
+
assert.ok(statusIndex < createdAtIndex);
|
|
893
|
+
assert.ok(createdAtIndex < firstNameIndex);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
test("includes GROUP BY fields in index", () => {
|
|
897
|
+
const query = "SELECT status, COUNT(name) as cnt FROM users WHERE type = 'user' GROUP BY status";
|
|
898
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
899
|
+
|
|
900
|
+
assert.ok(result !== null);
|
|
901
|
+
assert.ok(result.includes("`deleted` INCLUDE MISSING"));
|
|
902
|
+
assert.ok(result.includes("`type`"));
|
|
903
|
+
assert.ok(result.includes("`status`"));
|
|
904
|
+
assert.ok(result.includes("`name`"));
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
test("GROUP BY fields come after WHERE fields", () => {
|
|
908
|
+
const query = "SELECT COUNT(name) FROM users WHERE type = 'user' GROUP BY status";
|
|
909
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
910
|
+
|
|
911
|
+
const typeIndex = result.indexOf("`type`");
|
|
912
|
+
const statusIndex = result.indexOf("`status`");
|
|
913
|
+
|
|
914
|
+
// WHERE field (type) should come before GROUP BY field (status)
|
|
915
|
+
assert.ok(typeIndex < statusIndex);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
test("GROUP BY fields come before ORDER BY fields", () => {
|
|
919
|
+
const query = "SELECT COUNT(name) FROM users WHERE type = 'user' GROUP BY status ORDER BY status";
|
|
920
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
921
|
+
|
|
922
|
+
const typeIndex = result.indexOf("`type`");
|
|
923
|
+
const statusIndex = result.indexOf("`status`");
|
|
924
|
+
|
|
925
|
+
// status is in both GROUP BY and ORDER BY, should only appear once
|
|
926
|
+
const matches = result.match(/`status`/g);
|
|
927
|
+
assert.strictEqual(matches.length, 1);
|
|
928
|
+
|
|
929
|
+
// WHERE field (type) should come before GROUP BY field (status)
|
|
930
|
+
assert.ok(typeIndex < statusIndex);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
test("deduplicates GROUP BY field if already in WHERE", () => {
|
|
934
|
+
const query = "SELECT COUNT(name) FROM users WHERE status = 'active' GROUP BY status";
|
|
935
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
936
|
+
|
|
937
|
+
// status should only appear once
|
|
938
|
+
const matches = result.match(/`status`/g);
|
|
939
|
+
assert.strictEqual(matches.length, 1);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test("handles multiple GROUP BY fields", () => {
|
|
943
|
+
const query = "SELECT status, type, COUNT(name) FROM users WHERE category = 'A' GROUP BY status, type";
|
|
944
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
945
|
+
|
|
946
|
+
assert.ok(result.includes("`category`"));
|
|
947
|
+
assert.ok(result.includes("`status`"));
|
|
948
|
+
assert.ok(result.includes("`type`"));
|
|
949
|
+
assert.ok(result.includes("`name`"));
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
test("handles GROUP BY with HAVING and ORDER BY", () => {
|
|
953
|
+
const query = `SELECT status, COUNT(name) as cnt
|
|
954
|
+
FROM users
|
|
955
|
+
WHERE type = 'user'
|
|
956
|
+
GROUP BY status
|
|
957
|
+
HAVING COUNT(name) > 5
|
|
958
|
+
ORDER BY cnt DESC`;
|
|
959
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
960
|
+
|
|
961
|
+
assert.ok(result !== null);
|
|
962
|
+
assert.ok(result.includes("`type`"));
|
|
963
|
+
assert.ok(result.includes("`status`"));
|
|
964
|
+
assert.ok(result.includes("`name`"));
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
test("handles realistic GROUP BY query", () => {
|
|
968
|
+
const query = `SELECT status, COUNT(first_name) as user_count
|
|
969
|
+
FROM users
|
|
970
|
+
WHERE (deleted = false OR deleted IS MISSING) AND type = 'user'
|
|
971
|
+
GROUP BY status
|
|
972
|
+
ORDER BY user_count DESC
|
|
973
|
+
LIMIT 10`;
|
|
974
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
975
|
+
|
|
976
|
+
assert.ok(result !== null);
|
|
977
|
+
assert.ok(result.includes("`deleted` INCLUDE MISSING"));
|
|
978
|
+
assert.ok(result.includes("`type`"));
|
|
979
|
+
assert.ok(result.includes("`status`"));
|
|
980
|
+
assert.ok(result.includes("`first_name`"));
|
|
981
|
+
|
|
982
|
+
// Verify order: type (WHERE) -> status (GROUP BY) -> first_name (SELECT)
|
|
983
|
+
const typeIndex = result.indexOf("`type`");
|
|
984
|
+
const statusIndex = result.indexOf("`status`");
|
|
985
|
+
const firstNameIndex = result.indexOf("`first_name`");
|
|
986
|
+
|
|
987
|
+
assert.ok(typeIndex < statusIndex);
|
|
988
|
+
assert.ok(statusIndex < firstNameIndex);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
test("field order: WHERE -> GROUP BY -> ORDER BY -> SELECT", () => {
|
|
992
|
+
const query = `SELECT email
|
|
993
|
+
FROM users
|
|
994
|
+
WHERE type = 'user'
|
|
995
|
+
GROUP BY status
|
|
996
|
+
ORDER BY created_at`;
|
|
997
|
+
const result = buildCoveringIndex(query, defaultOptions);
|
|
998
|
+
|
|
999
|
+
const typeIndex = result.indexOf("`type`");
|
|
1000
|
+
const statusIndex = result.indexOf("`status`");
|
|
1001
|
+
const createdAtIndex = result.indexOf("`created_at`");
|
|
1002
|
+
const emailIndex = result.indexOf("`email`");
|
|
1003
|
+
|
|
1004
|
+
// Verify correct order
|
|
1005
|
+
assert.ok(typeIndex < statusIndex); // WHERE before GROUP BY
|
|
1006
|
+
assert.ok(statusIndex < createdAtIndex); // GROUP BY before ORDER BY
|
|
1007
|
+
assert.ok(createdAtIndex < emailIndex); // ORDER BY before SELECT
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
// ============================================
|
|
1011
|
+
// Summary
|
|
1012
|
+
// ============================================
|
|
1013
|
+
console.log("\n========================================");
|
|
1014
|
+
console.log(`Tests: ${passed + failed} | Passed: ${passed} | Failed: ${failed}`);
|
|
1015
|
+
console.log("========================================\n");
|
|
1016
|
+
|
|
1017
|
+
process.exit(failed > 0 ? 1 : 0);
|