@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.
Files changed (96) hide show
  1. package/CHANGELOG.md +17 -3
  2. package/README.md +31 -9
  3. package/dist/ai-agent.d.ts +4 -3
  4. package/dist/ai-agent.js +10 -3
  5. package/dist/ai-agent.js.map +1 -1
  6. package/dist/app.d.ts +29 -6
  7. package/dist/app.js +117 -58
  8. package/dist/app.js.map +1 -1
  9. package/dist/formula-engine.d.ts +7 -0
  10. package/dist/formula-engine.js +9 -2
  11. package/dist/formula-engine.js.map +1 -1
  12. package/dist/formula-plugin.d.ts +52 -0
  13. package/dist/formula-plugin.js +107 -0
  14. package/dist/formula-plugin.js.map +1 -0
  15. package/dist/index.d.ts +16 -3
  16. package/dist/index.js +14 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/plugin.d.ts +89 -0
  19. package/dist/plugin.js +136 -0
  20. package/dist/plugin.js.map +1 -0
  21. package/dist/query/filter-translator.d.ts +39 -0
  22. package/dist/query/filter-translator.js +135 -0
  23. package/dist/query/filter-translator.js.map +1 -0
  24. package/dist/query/index.d.ts +22 -0
  25. package/dist/query/index.js +39 -0
  26. package/dist/query/index.js.map +1 -0
  27. package/dist/query/query-analyzer.d.ts +188 -0
  28. package/dist/query/query-analyzer.js +349 -0
  29. package/dist/query/query-analyzer.js.map +1 -0
  30. package/dist/query/query-builder.d.ts +29 -0
  31. package/dist/query/query-builder.js +71 -0
  32. package/dist/query/query-builder.js.map +1 -0
  33. package/dist/query/query-service.d.ts +152 -0
  34. package/dist/query/query-service.js +268 -0
  35. package/dist/query/query-service.js.map +1 -0
  36. package/dist/repository.d.ts +23 -2
  37. package/dist/repository.js +81 -14
  38. package/dist/repository.js.map +1 -1
  39. package/dist/util.d.ts +7 -0
  40. package/dist/util.js +18 -3
  41. package/dist/util.js.map +1 -1
  42. package/dist/validator-plugin.d.ts +56 -0
  43. package/dist/validator-plugin.js +106 -0
  44. package/dist/validator-plugin.js.map +1 -0
  45. package/dist/validator.d.ts +7 -0
  46. package/dist/validator.js +10 -8
  47. package/dist/validator.js.map +1 -1
  48. package/jest.config.js +16 -0
  49. package/package.json +7 -5
  50. package/src/ai-agent.ts +8 -0
  51. package/src/app.ts +136 -72
  52. package/src/formula-engine.ts +8 -0
  53. package/src/formula-plugin.ts +141 -0
  54. package/src/index.ts +28 -3
  55. package/src/plugin.ts +224 -0
  56. package/src/query/filter-translator.ts +148 -0
  57. package/src/query/index.ts +24 -0
  58. package/src/query/query-analyzer.ts +537 -0
  59. package/src/query/query-builder.ts +81 -0
  60. package/src/query/query-service.ts +393 -0
  61. package/src/repository.ts +101 -18
  62. package/src/util.ts +19 -3
  63. package/src/validator-plugin.ts +140 -0
  64. package/src/validator.ts +12 -5
  65. package/test/__mocks__/@objectstack/runtime.ts +255 -0
  66. package/test/app.test.ts +23 -35
  67. package/test/filter-syntax.test.ts +233 -0
  68. package/test/formula-engine.test.ts +8 -0
  69. package/test/formula-integration.test.ts +8 -0
  70. package/test/formula-plugin.test.ts +197 -0
  71. package/test/introspection.test.ts +8 -0
  72. package/test/mock-driver.ts +8 -0
  73. package/test/plugin-integration.test.ts +213 -0
  74. package/test/repository-validation.test.ts +8 -0
  75. package/test/repository.test.ts +8 -0
  76. package/test/util.test.ts +9 -1
  77. package/test/utils.ts +8 -0
  78. package/test/validator-plugin.test.ts +126 -0
  79. package/test/validator.test.ts +8 -0
  80. package/tsconfig.json +8 -0
  81. package/tsconfig.tsbuildinfo +1 -1
  82. package/dist/action.d.ts +0 -7
  83. package/dist/action.js +0 -23
  84. package/dist/action.js.map +0 -1
  85. package/dist/hook.d.ts +0 -8
  86. package/dist/hook.js +0 -25
  87. package/dist/hook.js.map +0 -1
  88. package/dist/object.d.ts +0 -3
  89. package/dist/object.js +0 -28
  90. package/dist/object.js.map +0 -1
  91. package/src/action.ts +0 -40
  92. package/src/hook.ts +0 -42
  93. package/src/object.ts +0 -26
  94. package/test/action.test.ts +0 -276
  95. package/test/hook.test.ts +0 -343
  96. 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
+ }