spice-couchbase 2.0.44 → 2.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,352 @@
1
+ "use strict";
2
+
3
+ var crypto = require("crypto");
4
+ /**
5
+ * Generates a short hash from a string for unique index naming
6
+ * @param {string} str - The string to hash
7
+ * @returns {string} - A short hash string
8
+ */
9
+
10
+
11
+ function generateHash(str) {
12
+ return crypto.createHash("md5").update(str).digest("hex").substring(0, 8);
13
+ }
14
+ /**
15
+ * Extracts field names from WHERE clause of a query
16
+ * @param {string} query - The N1QL query
17
+ * @returns {string[]} - Array of field names found in WHERE clause
18
+ */
19
+
20
+
21
+ function extractWhereFields(query) {
22
+ var fields = new Set(); // Find the WHERE clause
23
+
24
+ var whereMatch = query.match(/\bWHERE\b([\s\S]+?)(?:\bORDER\b|\bLIMIT\b|\bOFFSET\b|\bGROUP\b|\bHAVING\b|$)/i);
25
+
26
+ if (!whereMatch) {
27
+ return [];
28
+ }
29
+
30
+ var whereClause = whereMatch[1]; // Pattern to match field names in comparisons
31
+ // Matches: field = value, field > value, field < value, field >= value, field <= value
32
+ // Also matches: field IN (...), field LIKE ..., field IS NULL, field IS MISSING, field IS NOT NULL
33
+ // Handles backtick-quoted fields like `field_name`
34
+
35
+ var fieldPattern = /`?(\w+)`?\s*(?:=|!=|<>|>=|<=|>|<|\bIN\b|\bLIKE\b|\bIS\b|\bBETWEEN\b)/gi;
36
+ var match;
37
+
38
+ while ((match = fieldPattern.exec(whereClause)) !== null) {
39
+ var fieldName = match[1].toLowerCase(); // Skip common SQL keywords that might be matched
40
+
41
+ if (!["and", "or", "not", "null", "missing", "true", "false"].includes(fieldName)) {
42
+ fields.add(match[1]); // Keep original case
43
+ }
44
+ } // Also look for fields in function calls like LOWER(field), UPPER(field)
45
+
46
+
47
+ var functionPattern = /(?:LOWER|UPPER|TRIM|LENGTH|SUBSTR)\s*\(\s*`?(\w+)`?/gi;
48
+
49
+ while ((match = functionPattern.exec(whereClause)) !== null) {
50
+ fields.add(match[1]);
51
+ } // Extract array fields from ANY/EVERY ... IN array_field SATISFIES ... END
52
+ // Pattern: ANY/EVERY variable IN array_field SATISFIES ... END
53
+
54
+
55
+ var anyEveryPattern = /\b(?:ANY|EVERY)\s+\w+\s+IN\s+`?(\w+)`?\s+SATISFIES/gi;
56
+
57
+ while ((match = anyEveryPattern.exec(whereClause)) !== null) {
58
+ fields.add(match[1]);
59
+ } // Also extract nested property access within SATISFIES: v.property = value
60
+ // This captures fields accessed through array iteration variables
61
+
62
+
63
+ var satisfiesPattern = /SATISFIES\s+(.+?)\s+END/gi;
64
+
65
+ while ((match = satisfiesPattern.exec(whereClause)) !== null) {
66
+ var satisfiesClause = match[1]; // Look for comparisons like: v.property = value, v.field > 10
67
+
68
+ var nestedFieldPattern = /\w+\.`?(\w+)`?\s*(?:=|!=|<>|>=|<=|>|<|\bIN\b|\bLIKE\b|\bIS\b)/gi;
69
+ var nestedMatch = void 0;
70
+
71
+ while ((nestedMatch = nestedFieldPattern.exec(satisfiesClause)) !== null) {// These are nested properties, we might want to track them separately
72
+ // For now, we'll note them but the array field is what matters for indexing
73
+ }
74
+ }
75
+
76
+ return Array.from(fields);
77
+ }
78
+ /**
79
+ * Extracts field names from SELECT clause of a query
80
+ * @param {string} query - The N1QL query
81
+ * @returns {string[]|null} - Array of field names, or null if SELECT *
82
+ */
83
+
84
+
85
+ function extractSelectFields(query) {
86
+ // Find the SELECT clause (between SELECT and FROM)
87
+ var selectMatch = query.match(/\bSELECT\b([\s\S]+?)\bFROM\b/i);
88
+
89
+ if (!selectMatch) {
90
+ return null;
91
+ }
92
+
93
+ var selectClause = selectMatch[1].trim(); // Check for SELECT * patterns
94
+
95
+ if (/^\s*\*\s*$/.test(selectClause) || /\(\s*\w+\s*\)\s*\.\s*\*/.test(selectClause)) {
96
+ return null; // Cannot determine fields for SELECT *
97
+ }
98
+
99
+ var fields = new Set(); // Split by comma, handling nested parentheses
100
+
101
+ var parts = splitSelectClause(selectClause);
102
+
103
+ for (var part of parts) {
104
+ var trimmedPart = part.trim(); // Skip if it's a * pattern
105
+
106
+ if (trimmedPart === "*" || trimmedPart.endsWith(".*")) {
107
+ return null;
108
+ } // Handle AS alias - get the source field
109
+
110
+
111
+ var asMatch = trimmedPart.match(/^(.+?)\s+AS\s+\w+$/i);
112
+ var fieldExpr = asMatch ? asMatch[1].trim() : trimmedPart; // Extract field from META().id pattern - skip these
113
+
114
+ if (/META\s*\(/i.test(fieldExpr)) {
115
+ continue;
116
+ } // Extract field from function calls like COUNT(field), SUM(field)
117
+
118
+
119
+ var funcMatch = fieldExpr.match(/^\w+\s*\(\s*`?(\w+)`?\s*\)$/);
120
+
121
+ if (funcMatch && funcMatch[1].toLowerCase() !== "id") {
122
+ fields.add(funcMatch[1]);
123
+ continue;
124
+ } // Handle simple field reference: `field` or collection.field or `collection`.`field`
125
+
126
+
127
+ var simpleFieldMatch = fieldExpr.match(/(?:`?\w+`?\s*\.\s*)?`?(\w+)`?$/);
128
+
129
+ if (simpleFieldMatch) {
130
+ var fieldName = simpleFieldMatch[1];
131
+
132
+ if (fieldName.toLowerCase() !== "id") {
133
+ fields.add(fieldName);
134
+ }
135
+ }
136
+ }
137
+
138
+ return Array.from(fields);
139
+ }
140
+ /**
141
+ * Extracts field names from GROUP BY clause of a query
142
+ * @param {string} query - The N1QL query
143
+ * @returns {string[]} - Array of field names found in GROUP BY clause
144
+ */
145
+
146
+
147
+ function extractGroupByFields(query) {
148
+ var fields = []; // Find the GROUP BY clause
149
+
150
+ var groupByMatch = query.match(/\bGROUP[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+BY\b([\s\S]+?)(?:\bHAVING\b|\bORDER\b|\bLIMIT\b|\bOFFSET\b|$)/i);
151
+
152
+ if (!groupByMatch) {
153
+ return [];
154
+ }
155
+
156
+ var groupByClause = groupByMatch[1]; // Split by comma and extract field names
157
+
158
+ var parts = groupByClause.split(",");
159
+
160
+ for (var part of parts) {
161
+ var trimmedPart = part.trim(); // Handle collection.field or `collection`.`field` syntax
162
+
163
+ var fieldMatch = trimmedPart.match(/(?:`?\w+`?\s*\.\s*)?`?(\w+)`?$/);
164
+
165
+ if (fieldMatch) {
166
+ var fieldName = fieldMatch[1]; // Skip if it's a keyword or id
167
+
168
+ if (!["id"].includes(fieldName.toLowerCase())) {
169
+ fields.push(fieldName);
170
+ }
171
+ }
172
+ }
173
+
174
+ return fields;
175
+ }
176
+ /**
177
+ * Extracts field names from ORDER BY clause of a query
178
+ * @param {string} query - The N1QL query
179
+ * @returns {string[]} - Array of field names found in ORDER BY clause
180
+ */
181
+
182
+
183
+ function extractOrderByFields(query) {
184
+ var fields = []; // Find the ORDER BY clause
185
+
186
+ var orderByMatch = query.match(/\bORDER[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+BY\b([\s\S]+?)(?:\bLIMIT\b|\bOFFSET\b|$)/i);
187
+
188
+ if (!orderByMatch) {
189
+ return [];
190
+ }
191
+
192
+ var orderByClause = orderByMatch[1]; // Split by comma and extract field names
193
+
194
+ var parts = orderByClause.split(",");
195
+
196
+ for (var part of parts) {
197
+ var trimmedPart = part.trim(); // Remove ASC/DESC suffix
198
+
199
+ var withoutDirection = trimmedPart.replace(/\s+(ASC|DESC)\s*$/i, "").trim(); // Handle collection.field or `collection`.`field` syntax
200
+
201
+ var fieldMatch = withoutDirection.match(/(?:`?\w+`?\s*\.\s*)?`?(\w+)`?$/);
202
+
203
+ if (fieldMatch) {
204
+ var fieldName = fieldMatch[1]; // Skip if it's a keyword or id
205
+
206
+ if (!["id", "asc", "desc"].includes(fieldName.toLowerCase())) {
207
+ fields.push(fieldName);
208
+ }
209
+ }
210
+ }
211
+
212
+ return fields;
213
+ }
214
+ /**
215
+ * Splits SELECT clause by commas, respecting parentheses
216
+ * @param {string} clause - The SELECT clause content
217
+ * @returns {string[]} - Array of individual select expressions
218
+ */
219
+
220
+
221
+ function splitSelectClause(clause) {
222
+ var parts = [];
223
+ var current = "";
224
+ var depth = 0;
225
+
226
+ for (var char of clause) {
227
+ if (char === "(") {
228
+ depth++;
229
+ current += char;
230
+ } else if (char === ")") {
231
+ depth--;
232
+ current += char;
233
+ } else if (char === "," && depth === 0) {
234
+ parts.push(current);
235
+ current = "";
236
+ } else {
237
+ current += char;
238
+ }
239
+ }
240
+
241
+ if (current.trim()) {
242
+ parts.push(current);
243
+ }
244
+
245
+ return parts;
246
+ }
247
+ /**
248
+ * Builds a covering index CREATE statement from a query
249
+ * @param {string} query - The N1QL query to analyze
250
+ * @param {Object} options - Configuration options
251
+ * @param {string} options.bucket - The bucket name
252
+ * @param {string} options.scope - The scope name
253
+ * @param {string} options.collection - The collection name
254
+ * @returns {string|null} - The CREATE INDEX statement, or null if cannot build
255
+ */
256
+
257
+
258
+ function buildCoveringIndex(query, options) {
259
+ var {
260
+ bucket,
261
+ scope,
262
+ collection
263
+ } = options;
264
+
265
+ if (!bucket || !scope || !collection) {
266
+ return null;
267
+ } // Extract fields from SELECT clause
268
+
269
+
270
+ var selectFields = extractSelectFields(query);
271
+
272
+ if (selectFields === null) {
273
+ // SELECT * query - cannot create covering index
274
+ return null;
275
+ } // Extract fields from WHERE clause
276
+
277
+
278
+ var whereFields = extractWhereFields(query); // Extract fields from GROUP BY clause
279
+
280
+ var groupByFields = extractGroupByFields(query); // Extract fields from ORDER BY clause
281
+
282
+ var orderByFields = extractOrderByFields(query); // Remove 'deleted' from whereFields as it will be added first with INCLUDE MISSING
283
+
284
+ var filteredWhereFields = whereFields.filter(f => f.toLowerCase() !== "deleted"); // Track fields already added to avoid duplicates
285
+
286
+ var addedFields = new Set(filteredWhereFields.map(f => f.toLowerCase())); // Filter GROUP BY fields (remove duplicates from WHERE, remove deleted)
287
+
288
+ var filteredGroupByFields = groupByFields.filter(f => {
289
+ var lower = f.toLowerCase();
290
+
291
+ if (addedFields.has(lower) || lower === "deleted") {
292
+ return false;
293
+ }
294
+
295
+ addedFields.add(lower);
296
+ return true;
297
+ }); // Filter ORDER BY fields (remove duplicates from WHERE and GROUP BY, remove deleted)
298
+
299
+ var filteredOrderByFields = orderByFields.filter(f => {
300
+ var lower = f.toLowerCase();
301
+
302
+ if (addedFields.has(lower) || lower === "deleted") {
303
+ return false;
304
+ }
305
+
306
+ addedFields.add(lower);
307
+ return true;
308
+ }); // Filter SELECT fields (remove duplicates from WHERE, GROUP BY, and ORDER BY, remove deleted)
309
+
310
+ var selectFieldsFiltered = selectFields.filter(f => {
311
+ var lower = f.toLowerCase();
312
+
313
+ if (addedFields.has(lower) || lower === "deleted") {
314
+ return false;
315
+ }
316
+
317
+ addedFields.add(lower);
318
+ return true;
319
+ }); // All fields for the index: WHERE -> GROUP BY -> ORDER BY -> SELECT
320
+
321
+ var allFields = [...filteredWhereFields, ...filteredGroupByFields, ...filteredOrderByFields, ...selectFieldsFiltered];
322
+
323
+ if (allFields.length === 0) {
324
+ // No fields to index besides deleted
325
+ return null;
326
+ } // Generate unique index name based on query hash
327
+
328
+
329
+ var hash = generateHash(query);
330
+ var indexName = "adv_covered_".concat(hash); // Build the field list with deleted INCLUDE MISSING first
331
+
332
+ var fieldList = ["`deleted` INCLUDE MISSING", ...allFields.map(f => "`".concat(f, "`"))].join(", "); // Build the CREATE INDEX statement
333
+
334
+ var indexStatement = "CREATE INDEX `".concat(indexName, "` ON `default`:`").concat(bucket, "`.`").concat(scope, "`.`").concat(collection, "`(").concat(fieldList, ")"); // Debug logs
335
+
336
+ console.log("[DEBUG buildCoveringIndex] Original query:", query);
337
+ console.log("[DEBUG buildCoveringIndex] WHERE fields:", filteredWhereFields);
338
+ console.log("[DEBUG buildCoveringIndex] GROUP BY fields:", filteredGroupByFields);
339
+ console.log("[DEBUG buildCoveringIndex] ORDER BY fields:", filteredOrderByFields);
340
+ console.log("[DEBUG buildCoveringIndex] SELECT fields:", selectFieldsFiltered);
341
+ console.log("[DEBUG buildCoveringIndex] Index statement:", indexStatement);
342
+ return indexStatement;
343
+ }
344
+
345
+ module.exports = {
346
+ buildCoveringIndex,
347
+ extractWhereFields,
348
+ extractSelectFields,
349
+ extractGroupByFields,
350
+ extractOrderByFields,
351
+ generateHash
352
+ };