@objectql/core 3.0.1 → 4.0.0

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.
Files changed (97) hide show
  1. package/IMPLEMENTATION_STATUS.md +364 -0
  2. package/README.md +31 -9
  3. package/RUNTIME_INTEGRATION.md +391 -0
  4. package/dist/ai-agent.d.ts +4 -3
  5. package/dist/ai-agent.js +10 -3
  6. package/dist/ai-agent.js.map +1 -1
  7. package/dist/app.d.ts +29 -6
  8. package/dist/app.js +117 -58
  9. package/dist/app.js.map +1 -1
  10. package/dist/formula-engine.d.ts +7 -0
  11. package/dist/formula-engine.js +9 -2
  12. package/dist/formula-engine.js.map +1 -1
  13. package/dist/formula-plugin.d.ts +52 -0
  14. package/dist/formula-plugin.js +107 -0
  15. package/dist/formula-plugin.js.map +1 -0
  16. package/dist/index.d.ts +13 -3
  17. package/dist/index.js +14 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/plugin.d.ts +89 -0
  20. package/dist/plugin.js +99 -0
  21. package/dist/plugin.js.map +1 -0
  22. package/dist/query/filter-translator.d.ts +37 -0
  23. package/dist/query/filter-translator.js +135 -0
  24. package/dist/query/filter-translator.js.map +1 -0
  25. package/dist/query/index.d.ts +22 -0
  26. package/dist/query/index.js +39 -0
  27. package/dist/query/index.js.map +1 -0
  28. package/dist/query/query-analyzer.d.ts +186 -0
  29. package/dist/query/query-analyzer.js +349 -0
  30. package/dist/query/query-analyzer.js.map +1 -0
  31. package/dist/query/query-builder.d.ts +27 -0
  32. package/dist/query/query-builder.js +71 -0
  33. package/dist/query/query-builder.js.map +1 -0
  34. package/dist/query/query-service.d.ts +150 -0
  35. package/dist/query/query-service.js +268 -0
  36. package/dist/query/query-service.js.map +1 -0
  37. package/dist/repository.d.ts +23 -2
  38. package/dist/repository.js +62 -13
  39. package/dist/repository.js.map +1 -1
  40. package/dist/util.d.ts +7 -0
  41. package/dist/util.js +18 -3
  42. package/dist/util.js.map +1 -1
  43. package/dist/validator-plugin.d.ts +56 -0
  44. package/dist/validator-plugin.js +106 -0
  45. package/dist/validator-plugin.js.map +1 -0
  46. package/dist/validator.d.ts +7 -0
  47. package/dist/validator.js +10 -8
  48. package/dist/validator.js.map +1 -1
  49. package/jest.config.js +16 -0
  50. package/package.json +8 -5
  51. package/src/ai-agent.ts +8 -0
  52. package/src/app.ts +136 -72
  53. package/src/formula-engine.ts +8 -0
  54. package/src/formula-plugin.ts +141 -0
  55. package/src/index.ts +25 -3
  56. package/src/plugin.ts +179 -0
  57. package/src/query/filter-translator.ts +147 -0
  58. package/src/query/index.ts +24 -0
  59. package/src/query/query-analyzer.ts +535 -0
  60. package/src/query/query-builder.ts +80 -0
  61. package/src/query/query-service.ts +392 -0
  62. package/src/repository.ts +81 -17
  63. package/src/util.ts +19 -3
  64. package/src/validator-plugin.ts +140 -0
  65. package/src/validator.ts +12 -5
  66. package/test/__mocks__/@objectstack/runtime.ts +255 -0
  67. package/test/app.test.ts +23 -35
  68. package/test/filter-syntax.test.ts +233 -0
  69. package/test/formula-engine.test.ts +8 -0
  70. package/test/formula-integration.test.ts +8 -0
  71. package/test/formula-plugin.test.ts +197 -0
  72. package/test/introspection.test.ts +8 -0
  73. package/test/mock-driver.ts +8 -0
  74. package/test/plugin-integration.test.ts +213 -0
  75. package/test/repository-validation.test.ts +8 -0
  76. package/test/repository.test.ts +8 -0
  77. package/test/util.test.ts +9 -1
  78. package/test/utils.ts +8 -0
  79. package/test/validator-plugin.test.ts +126 -0
  80. package/test/validator.test.ts +8 -0
  81. package/tsconfig.json +9 -0
  82. package/tsconfig.tsbuildinfo +1 -1
  83. package/dist/action.d.ts +0 -7
  84. package/dist/action.js +0 -23
  85. package/dist/action.js.map +0 -1
  86. package/dist/hook.d.ts +0 -8
  87. package/dist/hook.js +0 -25
  88. package/dist/hook.js.map +0 -1
  89. package/dist/object.d.ts +0 -3
  90. package/dist/object.js +0 -28
  91. package/dist/object.js.map +0 -1
  92. package/src/action.ts +0 -40
  93. package/src/hook.ts +0 -42
  94. package/src/object.ts +0 -26
  95. package/test/action.test.ts +0 -276
  96. package/test/hook.test.ts +0 -343
  97. package/test/object.test.ts +0 -183
@@ -0,0 +1,147 @@
1
+ /**
2
+ * ObjectQL
3
+ * Copyright (c) 2026-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import type { Filter } from '@objectql/types';
10
+ import type { FilterNode } from '@objectstack/spec';
11
+ import { ObjectQLError } from '@objectql/types';
12
+
13
+ /**
14
+ * Filter Translator
15
+ *
16
+ * Translates ObjectQL Filter (FilterCondition) to ObjectStack FilterNode format.
17
+ * Converts modern object-based syntax to legacy array-based syntax for backward compatibility.
18
+ *
19
+ * @example
20
+ * Input: { age: { $gte: 18 }, $or: [{ status: "active" }, { role: "admin" }] }
21
+ * Output: [["age", ">=", 18], "or", [["status", "=", "active"], "or", ["role", "=", "admin"]]]
22
+ */
23
+ export class FilterTranslator {
24
+ /**
25
+ * Translate filters from ObjectQL format to ObjectStack FilterNode format
26
+ */
27
+ translate(filters?: Filter): FilterNode | undefined {
28
+ if (!filters) {
29
+ return undefined;
30
+ }
31
+
32
+ // Backward compatibility: if it's already an array (old format), pass through
33
+ if (Array.isArray(filters)) {
34
+ return filters as unknown as FilterNode;
35
+ }
36
+
37
+ // If it's an empty object, return undefined
38
+ if (typeof filters === 'object' && Object.keys(filters).length === 0) {
39
+ return undefined;
40
+ }
41
+
42
+ return this.convertToNode(filters);
43
+ }
44
+
45
+ /**
46
+ * Recursively converts FilterCondition to FilterNode array format
47
+ */
48
+ private convertToNode(filter: Filter): FilterNode {
49
+ const nodes: any[] = [];
50
+
51
+ // Process logical operators first
52
+ if (filter.$and) {
53
+ const andNodes = filter.$and.map((f: Filter) => this.convertToNode(f));
54
+ nodes.push(...this.interleaveWithOperator(andNodes, 'and'));
55
+ }
56
+
57
+ if (filter.$or) {
58
+ const orNodes = filter.$or.map((f: Filter) => this.convertToNode(f));
59
+ if (nodes.length > 0) {
60
+ nodes.push('and');
61
+ }
62
+ nodes.push(...this.interleaveWithOperator(orNodes, 'or'));
63
+ }
64
+
65
+ // Note: $not operator is not currently supported in the legacy FilterNode format
66
+ if (filter.$not) {
67
+ throw new ObjectQLError({
68
+ code: 'UNSUPPORTED_OPERATOR',
69
+ message: '$not operator is not supported. Use $ne for field negation instead.'
70
+ });
71
+ }
72
+
73
+ // Process field conditions
74
+ for (const [field, value] of Object.entries(filter)) {
75
+ if (field.startsWith('$')) {
76
+ continue; // Skip logical operators (already processed)
77
+ }
78
+
79
+ if (nodes.length > 0) {
80
+ nodes.push('and');
81
+ }
82
+
83
+ // Handle field value
84
+ if (value === null || value === undefined) {
85
+ nodes.push([field, '=', value]);
86
+ } else if (typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
87
+ // Explicit operators - multiple operators on same field are AND-ed together
88
+ const entries = Object.entries(value);
89
+ for (let i = 0; i < entries.length; i++) {
90
+ const [op, opValue] = entries[i];
91
+
92
+ // Add 'and' before each operator (except the very first node)
93
+ if (nodes.length > 0 || i > 0) {
94
+ nodes.push('and');
95
+ }
96
+
97
+ const legacyOp = this.mapOperatorToLegacy(op);
98
+ nodes.push([field, legacyOp, opValue]);
99
+ }
100
+ } else {
101
+ // Implicit equality
102
+ nodes.push([field, '=', value]);
103
+ }
104
+ }
105
+
106
+ // Return as FilterNode (type assertion for backward compatibility)
107
+ return (nodes.length === 1 ? nodes[0] : nodes) as unknown as FilterNode;
108
+ }
109
+
110
+ /**
111
+ * Interleaves filter nodes with a logical operator
112
+ */
113
+ private interleaveWithOperator(nodes: FilterNode[], operator: string): any[] {
114
+ if (nodes.length === 0) return [];
115
+ if (nodes.length === 1) return [nodes[0]];
116
+
117
+ const result: any[] = [nodes[0]];
118
+ for (let i = 1; i < nodes.length; i++) {
119
+ result.push(operator, nodes[i]);
120
+ }
121
+ return result;
122
+ }
123
+
124
+ /**
125
+ * Maps modern $-prefixed operators to legacy format
126
+ */
127
+ private mapOperatorToLegacy(operator: string): string {
128
+ const mapping: Record<string, string> = {
129
+ '$eq': '=',
130
+ '$ne': '!=',
131
+ '$gt': '>',
132
+ '$gte': '>=',
133
+ '$lt': '<',
134
+ '$lte': '<=',
135
+ '$in': 'in',
136
+ '$nin': 'nin',
137
+ '$contains': 'contains',
138
+ '$startsWith': 'startswith',
139
+ '$endsWith': 'endswith',
140
+ '$null': 'is_null',
141
+ '$exist': 'is_not_null',
142
+ '$between': 'between',
143
+ };
144
+
145
+ return mapping[operator] || operator.replace('$', '');
146
+ }
147
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * ObjectQL
3
+ * Copyright (c) 2026-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * Query Module
11
+ *
12
+ * This module contains ObjectQL's query-specific functionality:
13
+ * - FilterTranslator: Converts ObjectQL filters to ObjectStack FilterNode
14
+ * - QueryBuilder: Builds ObjectStack QueryAST from ObjectQL UnifiedQuery
15
+ * - QueryService: Executes queries via drivers with profiling support
16
+ * - QueryAnalyzer: Provides query performance analysis and optimization suggestions
17
+ *
18
+ * These are the core components that differentiate ObjectQL from generic runtime systems.
19
+ */
20
+
21
+ export * from './filter-translator';
22
+ export * from './query-builder';
23
+ export * from './query-service';
24
+ export * from './query-analyzer';
@@ -0,0 +1,535 @@
1
+ /**
2
+ * ObjectQL Query Analyzer
3
+ * Copyright (c) 2026-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import type { UnifiedQuery, ObjectConfig, MetadataRegistry } from '@objectql/types';
10
+ import type { QueryAST, FilterNode } from '@objectstack/spec';
11
+ import { QueryService, QueryOptions } from './query-service';
12
+
13
+ /**
14
+ * Query execution plan
15
+ */
16
+ export interface QueryPlan {
17
+ /**
18
+ * The compiled QueryAST
19
+ */
20
+ ast: QueryAST;
21
+
22
+ /**
23
+ * Estimated number of rows to be scanned
24
+ */
25
+ estimatedRows?: number;
26
+
27
+ /**
28
+ * Indexes that could be used for this query
29
+ */
30
+ indexes: string[];
31
+
32
+ /**
33
+ * Warnings about potential performance issues
34
+ */
35
+ warnings: string[];
36
+
37
+ /**
38
+ * Suggestions for optimization
39
+ */
40
+ suggestions: string[];
41
+
42
+ /**
43
+ * Complexity score (0-100, higher is more complex)
44
+ */
45
+ complexity: number;
46
+ }
47
+
48
+ /**
49
+ * Query profile result with execution statistics
50
+ */
51
+ export interface ProfileResult {
52
+ /**
53
+ * Execution time in milliseconds
54
+ */
55
+ executionTime: number;
56
+
57
+ /**
58
+ * Number of rows scanned by the database
59
+ */
60
+ rowsScanned: number;
61
+
62
+ /**
63
+ * Number of rows returned
64
+ */
65
+ rowsReturned: number;
66
+
67
+ /**
68
+ * Whether an index was used
69
+ */
70
+ indexUsed: boolean;
71
+
72
+ /**
73
+ * The query plan
74
+ */
75
+ plan: QueryPlan;
76
+
77
+ /**
78
+ * Efficiency ratio (rowsReturned / rowsScanned)
79
+ * Higher is better (1.0 is perfect, 0.0 is worst)
80
+ */
81
+ efficiency: number;
82
+ }
83
+
84
+ /**
85
+ * Aggregated query statistics
86
+ */
87
+ export interface QueryStats {
88
+ /**
89
+ * Total number of queries executed
90
+ */
91
+ totalQueries: number;
92
+
93
+ /**
94
+ * Average execution time in milliseconds
95
+ */
96
+ avgExecutionTime: number;
97
+
98
+ /**
99
+ * Slowest query execution time
100
+ */
101
+ slowestQuery: number;
102
+
103
+ /**
104
+ * Fastest query execution time
105
+ */
106
+ fastestQuery: number;
107
+
108
+ /**
109
+ * Queries by object
110
+ */
111
+ byObject: Record<string, {
112
+ count: number;
113
+ avgTime: number;
114
+ }>;
115
+
116
+ /**
117
+ * Top slow queries
118
+ */
119
+ slowQueries: Array<{
120
+ objectName: string;
121
+ executionTime: number;
122
+ query: UnifiedQuery;
123
+ timestamp: Date;
124
+ }>;
125
+ }
126
+
127
+ /**
128
+ * Query Analyzer
129
+ *
130
+ * Provides query performance analysis and profiling capabilities.
131
+ * This class helps developers optimize queries by:
132
+ * - Analyzing query plans
133
+ * - Profiling execution performance
134
+ * - Tracking statistics
135
+ * - Providing optimization suggestions
136
+ */
137
+ export class QueryAnalyzer {
138
+ private stats: QueryStats = {
139
+ totalQueries: 0,
140
+ avgExecutionTime: 0,
141
+ slowestQuery: 0,
142
+ fastestQuery: Number.MAX_VALUE,
143
+ byObject: {},
144
+ slowQueries: []
145
+ };
146
+
147
+ private executionTimes: number[] = [];
148
+ private readonly MAX_SLOW_QUERIES = 10;
149
+
150
+ constructor(
151
+ private queryService: QueryService,
152
+ private metadata: MetadataRegistry
153
+ ) {}
154
+
155
+ /**
156
+ * Analyze a query and generate an execution plan
157
+ *
158
+ * @param objectName - The object to query
159
+ * @param query - The unified query
160
+ * @returns Query plan with optimization suggestions
161
+ */
162
+ async explain(objectName: string, query: UnifiedQuery): Promise<QueryPlan> {
163
+ const schema = this.getSchema(objectName);
164
+
165
+ // Build the QueryAST (without executing)
166
+ const ast: QueryAST = {
167
+ object: objectName,
168
+ filters: query.filters as any, // FilterCondition is compatible with FilterNode
169
+ sort: query.sort as any, // Will be converted to SortNode[] format
170
+ top: query.limit, // Changed from limit to top (QueryAST uses 'top')
171
+ skip: query.skip,
172
+ fields: query.fields
173
+ };
174
+
175
+ // Analyze filters for index usage
176
+ const indexes = this.findApplicableIndexes(schema, query);
177
+
178
+ // Detect potential issues
179
+ const warnings = this.analyzeWarnings(schema, query);
180
+
181
+ // Generate suggestions
182
+ const suggestions = this.generateSuggestions(schema, query, indexes);
183
+
184
+ // Calculate complexity
185
+ const complexity = this.calculateComplexity(query);
186
+
187
+ // Try to estimate rows (basic heuristic)
188
+ const estimatedRows = this.estimateRows(schema, query);
189
+
190
+ return {
191
+ ast,
192
+ estimatedRows,
193
+ indexes,
194
+ warnings,
195
+ suggestions,
196
+ complexity
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Profile a query execution
202
+ *
203
+ * @param objectName - The object to query
204
+ * @param query - The unified query
205
+ * @param options - Query execution options
206
+ * @returns Profile result with execution statistics
207
+ */
208
+ async profile(
209
+ objectName: string,
210
+ query: UnifiedQuery,
211
+ options: QueryOptions = {}
212
+ ): Promise<ProfileResult> {
213
+ // Get the query plan first
214
+ const plan = await this.explain(objectName, query);
215
+
216
+ // Execute with profiling enabled
217
+ const result = await this.queryService.find(objectName, query, {
218
+ ...options,
219
+ profile: true
220
+ });
221
+
222
+ const executionTime = result.profile?.executionTime || 0;
223
+ const rowsReturned = result.value.length;
224
+ const rowsScanned = result.profile?.rowsScanned || rowsReturned;
225
+
226
+ // Calculate efficiency
227
+ const efficiency = rowsScanned > 0 ? rowsReturned / rowsScanned : 0;
228
+
229
+ // Determine if index was used (heuristic)
230
+ const indexUsed = plan.indexes.length > 0 && efficiency > 0.5;
231
+
232
+ // Update statistics
233
+ this.recordExecution(objectName, executionTime, query);
234
+
235
+ return {
236
+ executionTime,
237
+ rowsScanned,
238
+ rowsReturned,
239
+ indexUsed,
240
+ plan,
241
+ efficiency
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Get aggregated query statistics
247
+ *
248
+ * @returns Current query statistics
249
+ */
250
+ getStatistics(): QueryStats {
251
+ return { ...this.stats };
252
+ }
253
+
254
+ /**
255
+ * Reset statistics
256
+ */
257
+ resetStatistics(): void {
258
+ this.stats = {
259
+ totalQueries: 0,
260
+ avgExecutionTime: 0,
261
+ slowestQuery: 0,
262
+ fastestQuery: Number.MAX_VALUE,
263
+ byObject: {},
264
+ slowQueries: []
265
+ };
266
+ this.executionTimes = [];
267
+ }
268
+
269
+ /**
270
+ * Get schema for an object
271
+ * @private
272
+ */
273
+ private getSchema(objectName: string): ObjectConfig {
274
+ const obj = this.metadata.get<ObjectConfig>('object', objectName);
275
+ if (!obj) {
276
+ throw new Error(`Object '${objectName}' not found`);
277
+ }
278
+ return obj;
279
+ }
280
+
281
+ /**
282
+ * Find indexes that could be used for the query
283
+ * @private
284
+ */
285
+ private findApplicableIndexes(schema: ObjectConfig, query: UnifiedQuery): string[] {
286
+ const indexes: string[] = [];
287
+
288
+ if (!schema.indexes || !query.filters) {
289
+ return indexes;
290
+ }
291
+
292
+ // Extract fields used in filters
293
+ const filterFields = new Set<string>();
294
+
295
+ // FilterCondition is an object-based filter (e.g., { field: value } or { field: { $eq: value } })
296
+ // We need to extract field names from the filter object
297
+ const extractFieldsFromFilter = (filter: any): void => {
298
+ if (!filter || typeof filter !== 'object') return;
299
+
300
+ for (const key of Object.keys(filter)) {
301
+ // Skip logical operators
302
+ if (key.startsWith('$')) {
303
+ // Logical operators contain nested filters
304
+ if (key === '$and' || key === '$or') {
305
+ const nested = filter[key];
306
+ if (Array.isArray(nested)) {
307
+ nested.forEach(extractFieldsFromFilter);
308
+ }
309
+ }
310
+ continue;
311
+ }
312
+ // This is a field name
313
+ filterFields.add(key);
314
+ }
315
+ };
316
+
317
+ extractFieldsFromFilter(query.filters);
318
+
319
+ // Check which indexes could be used
320
+ const indexesArray = Array.isArray(schema.indexes) ? schema.indexes : Object.values(schema.indexes || {});
321
+ for (const index of indexesArray) {
322
+ const indexFields = Array.isArray(index.fields)
323
+ ? index.fields
324
+ : [index.fields];
325
+
326
+ // Simple heuristic: index is applicable if first field is in filter
327
+ if (indexFields.length > 0 && filterFields.has(indexFields[0])) {
328
+ const indexName = index.name || indexFields.join('_');
329
+ indexes.push(indexName);
330
+ }
331
+ }
332
+
333
+ return indexes;
334
+ }
335
+
336
+ /**
337
+ * Analyze query for potential issues
338
+ * @private
339
+ */
340
+ private analyzeWarnings(schema: ObjectConfig, query: UnifiedQuery): string[] {
341
+ const warnings: string[] = [];
342
+
343
+ // Warning: No filters (full table scan)
344
+ if (!query.filters || query.filters.length === 0) {
345
+ warnings.push('No filters specified - this will scan all records');
346
+ }
347
+
348
+ // Warning: No limit on potentially large dataset
349
+ if (!query.limit) {
350
+ warnings.push('No limit specified - consider adding pagination');
351
+ }
352
+
353
+ // Warning: Selecting all fields
354
+ if (!query.fields || query.fields.length === 0) {
355
+ const fieldCount = Object.keys(schema.fields || {}).length;
356
+ if (fieldCount > 10) {
357
+ warnings.push(`Selecting all ${fieldCount} fields - consider selecting only needed fields`);
358
+ }
359
+ }
360
+
361
+ // Warning: Complex filters without indexes
362
+ if (query.filters && query.filters.length > 5) {
363
+ const indexes = this.findApplicableIndexes(schema, query);
364
+ if (indexes.length === 0) {
365
+ warnings.push('Complex filters without matching indexes - consider adding indexes');
366
+ }
367
+ }
368
+
369
+ return warnings;
370
+ }
371
+
372
+ /**
373
+ * Generate optimization suggestions
374
+ * @private
375
+ */
376
+ private generateSuggestions(
377
+ schema: ObjectConfig,
378
+ query: UnifiedQuery,
379
+ indexes: string[]
380
+ ): string[] {
381
+ const suggestions: string[] = [];
382
+
383
+ // Suggest adding limit
384
+ if (!query.limit) {
385
+ suggestions.push('Add a limit clause to restrict result set size');
386
+ }
387
+
388
+ // Suggest adding indexes
389
+ if (query.filters && query.filters.length > 0 && indexes.length === 0) {
390
+ const filterFields = query.filters
391
+ .filter((f: any) => Array.isArray(f) && f.length >= 1)
392
+ .map((f: any) => String(f[0]));
393
+
394
+ if (filterFields.length > 0) {
395
+ suggestions.push(`Consider adding an index on: ${filterFields.join(', ')}`);
396
+ }
397
+ }
398
+
399
+ // Suggest field selection
400
+ if (!query.fields || query.fields.length === 0) {
401
+ suggestions.push('Select only required fields to reduce data transfer');
402
+ }
403
+
404
+ // Suggest composite index for multiple filters
405
+ if (query.filters && query.filters.length > 1 && indexes.length < 2) {
406
+ const filterFields = query.filters
407
+ .filter((f: any) => Array.isArray(f) && f.length >= 1)
408
+ .map((f: any) => String(f[0]))
409
+ .slice(0, 3); // Top 3 fields
410
+
411
+ if (filterFields.length > 1) {
412
+ suggestions.push(`Consider a composite index on: (${filterFields.join(', ')})`);
413
+ }
414
+ }
415
+
416
+ return suggestions;
417
+ }
418
+
419
+ /**
420
+ * Calculate query complexity score (0-100)
421
+ * @private
422
+ */
423
+ private calculateComplexity(query: UnifiedQuery): number {
424
+ let complexity = 0;
425
+
426
+ // Base complexity
427
+ complexity += 10;
428
+
429
+ // Filters add complexity
430
+ if (query.filters) {
431
+ complexity += query.filters.length * 5;
432
+
433
+ // Nested filters (OR conditions) add more
434
+ const hasNestedFilters = query.filters.some((f: any) =>
435
+ Array.isArray(f) && Array.isArray(f[0])
436
+ );
437
+ if (hasNestedFilters) {
438
+ complexity += 15;
439
+ }
440
+ }
441
+
442
+ // Sorting adds complexity
443
+ if (query.sort && query.sort.length > 0) {
444
+ complexity += query.sort.length * 3;
445
+ }
446
+
447
+ // Field selection reduces complexity slightly
448
+ if (query.fields && query.fields.length > 0 && query.fields.length < 10) {
449
+ complexity -= 5;
450
+ }
451
+
452
+ // Pagination reduces complexity
453
+ if (query.limit) {
454
+ complexity -= 5;
455
+ }
456
+
457
+ // Cap at 100
458
+ return Math.min(Math.max(complexity, 0), 100);
459
+ }
460
+
461
+ /**
462
+ * Estimate number of rows (very rough heuristic)
463
+ * @private
464
+ */
465
+ private estimateRows(schema: ObjectConfig, query: UnifiedQuery): number {
466
+ // This is a placeholder - real implementation would need statistics
467
+ // from the database (row count, index selectivity, etc.)
468
+
469
+ // Default to unknown
470
+ if (!query.filters || query.filters.length === 0) {
471
+ return -1; // Unknown, full scan
472
+ }
473
+
474
+ // Very rough estimate based on filter count
475
+ const baseEstimate = 1000;
476
+ const filterReduction = Math.pow(0.5, query.filters.length);
477
+ const estimated = Math.floor(baseEstimate * filterReduction);
478
+
479
+ // Apply limit if present
480
+ if (query.limit) {
481
+ return Math.min(estimated, query.limit);
482
+ }
483
+
484
+ return estimated;
485
+ }
486
+
487
+ /**
488
+ * Record a query execution for statistics
489
+ * @private
490
+ */
491
+ private recordExecution(
492
+ objectName: string,
493
+ executionTime: number,
494
+ query: UnifiedQuery
495
+ ): void {
496
+ // Update totals
497
+ this.stats.totalQueries++;
498
+ this.executionTimes.push(executionTime);
499
+
500
+ // Update average
501
+ this.stats.avgExecutionTime =
502
+ this.executionTimes.reduce((sum, t) => sum + t, 0) / this.executionTimes.length;
503
+
504
+ // Update min/max
505
+ this.stats.slowestQuery = Math.max(this.stats.slowestQuery, executionTime);
506
+ this.stats.fastestQuery = Math.min(this.stats.fastestQuery, executionTime);
507
+
508
+ // Update per-object stats
509
+ if (!this.stats.byObject[objectName]) {
510
+ this.stats.byObject[objectName] = { count: 0, avgTime: 0 };
511
+ }
512
+ const objStats = this.stats.byObject[objectName];
513
+ objStats.count++;
514
+ objStats.avgTime = ((objStats.avgTime * (objStats.count - 1)) + executionTime) / objStats.count;
515
+
516
+ // Track slow queries
517
+ if (this.stats.slowQueries.length < this.MAX_SLOW_QUERIES) {
518
+ this.stats.slowQueries.push({
519
+ objectName,
520
+ executionTime,
521
+ query,
522
+ timestamp: new Date()
523
+ });
524
+ this.stats.slowQueries.sort((a, b) => b.executionTime - a.executionTime);
525
+ } else if (executionTime > this.stats.slowQueries[this.MAX_SLOW_QUERIES - 1].executionTime) {
526
+ this.stats.slowQueries[this.MAX_SLOW_QUERIES - 1] = {
527
+ objectName,
528
+ executionTime,
529
+ query,
530
+ timestamp: new Date()
531
+ };
532
+ this.stats.slowQueries.sort((a, b) => b.executionTime - a.executionTime);
533
+ }
534
+ }
535
+ }