spice-couchbase 2.0.45 → 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 CHANGED
@@ -757,40 +757,96 @@ module.exports = class Database {
757
757
  return response.rows;
758
758
  })();
759
759
  }
760
+ /**
761
+ * Recursively checks if the query plan contains a Fetch operator
762
+ * @param {Object} plan - The EXPLAIN query plan
763
+ * @returns {boolean} - True if Fetch operator is found
764
+ */
765
+
766
+
767
+ hasFetchOperator(plan) {
768
+ if (!plan || typeof plan !== "object") {
769
+ return false;
770
+ } // Check if this node is a Fetch operator
771
+
772
+
773
+ if (plan["#operator"] === "Fetch") {
774
+ return true;
775
+ } // Recursively check all child properties
776
+
777
+
778
+ for (var key of Object.keys(plan)) {
779
+ var value = plan[key];
780
+
781
+ if (Array.isArray(value)) {
782
+ for (var item of value) {
783
+ if (this.hasFetchOperator(item)) {
784
+ return true;
785
+ }
786
+ }
787
+ } else if (typeof value === "object" && value !== null) {
788
+ if (this.hasFetchOperator(value)) {
789
+ return true;
790
+ }
791
+ }
792
+ }
793
+
794
+ return false;
795
+ }
760
796
 
761
797
  optimize(query, exec_scope) {
798
+ var _this18 = this;
799
+
762
800
  return _asyncToGenerator(function* () {
763
801
  try {
764
- var _$last, _$first, _$first$advice, _$first$advice$advise, _$first$advice$advise2;
802
+ var _explainResponse$rows;
765
803
 
766
- var options = {
767
- adhoc: true
768
- };
769
- var advise_query = "ADVISE ".concat(query);
770
- var response = yield exec_scope.query(advise_query, options);
771
- var best_index = (_$last = _.last(((_$first = _.first((response === null || response === void 0 ? void 0 : response.rows) || [])) === null || _$first === void 0 ? void 0 : (_$first$advice = _$first.advice) === null || _$first$advice === void 0 ? void 0 : (_$first$advice$advise = _$first$advice.adviseinfo) === null || _$first$advice$advise === void 0 ? void 0 : (_$first$advice$advise2 = _$first$advice$advise.recommended_indexes) === null || _$first$advice$advise2 === void 0 ? void 0 : _$first$advice$advise2.indexes) || [])) === null || _$last === void 0 ? void 0 : _$last.index_statement;
804
+ var {
805
+ buildCoveringIndex
806
+ } = require("./utility"); // 1. Run EXPLAIN to get the query plan
807
+
808
+
809
+ var explainQuery = "EXPLAIN ".concat(query);
810
+ var explainResponse = yield exec_scope.query(explainQuery); // 2. Check if query plan contains Fetch operator (not covered)
811
+
812
+ var plan = explainResponse === null || explainResponse === void 0 ? void 0 : (_explainResponse$rows = explainResponse.rows) === null || _explainResponse$rows === void 0 ? void 0 : _explainResponse$rows[0];
813
+
814
+ if (!_this18.hasFetchOperator(plan)) {
815
+ // Query is already covered, no need to create index
816
+ return;
817
+ } // 3. Build covering index from the query
772
818
 
773
- if (best_index) {
774
- yield exec_scope.query(best_index);
819
+
820
+ var indexStatement = buildCoveringIndex(query, {
821
+ bucket: _this18.bucket,
822
+ scope: _this18.scope_name,
823
+ collection: _this18.collection_name
824
+ }); // 4. Execute the index creation if we got a valid statement
825
+
826
+ if (indexStatement) {
827
+ console.log("Executing index creation:", indexStatement);
828
+ var response = yield exec_scope.query(indexStatement);
829
+ console.log("Index creation response:", response);
775
830
  }
776
- } catch (e) {}
831
+ } catch (e) {// Silently handle errors (index may already exist, etc.)
832
+ }
777
833
  })();
778
834
  }
779
835
 
780
836
  query(query) {
781
- var _this18 = this;
837
+ var _this19 = this;
782
838
 
783
839
  var query_scope = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "cluster";
784
840
  return _asyncToGenerator(function* () {
785
- var scope = query_scope || _this18.query_scope;
841
+ var scope = query_scope || _this19.query_scope;
786
842
  var sanitizedQuery = query; //sanitization step to be implemented
787
843
 
788
844
  var exec_scope = null;
789
845
 
790
846
  if (scope === "cluster") {
791
- exec_scope = _this18.cluster;
847
+ exec_scope = _this19.cluster;
792
848
  } else {
793
- exec_scope = _this18.scope;
849
+ exec_scope = _this19.scope;
794
850
  } // Start time
795
851
 
796
852
 
@@ -803,10 +859,10 @@ module.exports = class Database {
803
859
  var endTime = process.hrtime.bigint(); // Calculate duration in microseconds
804
860
 
805
861
  var durationInMilliseconds = (endTime - startTime) / BigInt(1000000); // optimize if response time is greater than 1 second
862
+ //if (durationInMilliseconds > 2000) {
863
+
864
+ _this19.optimize(query, exec_scope); //}
806
865
 
807
- if (durationInMilliseconds > 2000) {
808
- _this18.optimize(query, exec_scope);
809
- }
810
866
 
811
867
  return response;
812
868
  })();
@@ -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
+ };