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