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