From 9a0e1b9c1c4e0516766647fcb6f3857cd544342f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 05:02:59 +0000 Subject: [PATCH 1/3] Initial plan From f3fe9fee531b0b6497c6be6c59dabe211bd25807 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 05:09:36 +0000 Subject: [PATCH 2/3] Fix query test failures - convert array syntax to object syntax Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/spec/src/data/query.test.ts | 168 +- packages/spec/src/data/query.test.ts.backup | 1932 +++++++++++++++++++ 2 files changed, 2004 insertions(+), 96 deletions(-) create mode 100644 packages/spec/src/data/query.test.ts.backup diff --git a/packages/spec/src/data/query.test.ts b/packages/spec/src/data/query.test.ts index fe38c95880..e67cc0bd30 100644 --- a/packages/spec/src/data/query.test.ts +++ b/packages/spec/src/data/query.test.ts @@ -1,8 +1,6 @@ import { describe, it, expect } from 'vitest'; import { QuerySchema, - FilterOperator, - LogicOperator, AggregationFunction, JoinType, WindowFunction, @@ -12,35 +10,6 @@ import { type WindowFunctionNode, } from './query.zod'; -describe('FilterOperator', () => { - it('should accept valid filter operators', () => { - const validOperators = [ - '=', '!=', '<>', - '>', '>=', '<', '<=', - 'startswith', 'contains', 'notcontains', - 'between', 'in', 'notin', - 'is_null', 'is_not_null' - ]; - - validOperators.forEach(op => { - expect(() => FilterOperator.parse(op)).not.toThrow(); - }); - }); - - it('should reject invalid operators', () => { - expect(() => FilterOperator.parse('LIKE')).toThrow(); - expect(() => FilterOperator.parse('equals')).toThrow(); - }); -}); - -describe('LogicOperator', () => { - it('should accept valid logic operators', () => { - expect(() => LogicOperator.parse('and')).not.toThrow(); - expect(() => LogicOperator.parse('or')).not.toThrow(); - expect(() => LogicOperator.parse('not')).not.toThrow(); - }); -}); - describe('AggregationFunction', () => { it('should accept valid aggregation functions', () => { const validFunctions = [ @@ -111,7 +80,7 @@ describe('QuerySchema - Basic', () => { const query: QueryAST = { object: 'account', fields: ['name', 'email'], - sort: [ + orderBy: [ { field: 'name', order: 'asc' }, { field: 'created_at', order: 'desc' }, ], @@ -303,7 +272,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'count', alias: 'order_count' }, ], groupBy: ['customer_id'], - having: ['order_count', '>', 5], + having: { order_count: { $gt: 5 } }, }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -317,7 +286,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'sum', field: 'amount', alias: 'total_amount' }, ], groupBy: ['customer_id'], - having: ['total_amount', '>', 1000], + having: { total_amount: { $gt: 1000 } }, }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -331,7 +300,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'avg', field: 'amount', alias: 'avg_amount' }, ], groupBy: ['customer_id'], - having: ['avg_amount', '>=', 500], + having: { avg_amount: { $gte: 500 } }, }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -346,7 +315,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'sum', field: 'amount', alias: 'total_amount' }, ], groupBy: ['customer_id'], - having: [['order_count', '>', 3], 'and', ['total_amount', '>', 1000]], + having: { $and: [{ order_count: { $gt: 3 } }, { total_amount: { $gt: 1000 } }] }, }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -361,7 +330,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'sum', field: 'amount', alias: 'total_amount' }, ], groupBy: ['customer_id'], - having: ['total_amount', '>', 5000], + having: { total_amount: { $gt: 5000 } }, }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -379,7 +348,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'sum', field: 'amount', alias: 'total_amount' }, ], groupBy: ['customer_id'], - sort: [{ field: 'total_amount', order: 'desc' }], + orderBy: [{ field: 'total_amount', order: 'desc' }], }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -393,7 +362,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'count', alias: 'order_count' }, ], groupBy: ['customer_id'], - sort: [{ field: 'order_count', order: 'desc' }], + orderBy: [{ field: 'order_count', order: 'desc' }], top: 10, skip: 0, }; @@ -455,8 +424,8 @@ describe('QuerySchema - Aggregations', () => { { function: 'max', field: 'created_at', alias: 'last_order_date' }, ], groupBy: ['customer_id'], - having: ['num_orders', '>', 1], - sort: [{ field: 'lifetime_value', order: 'desc' }], + having: { num_orders: { $gt: 1 } }, + orderBy: [{ field: 'lifetime_value', order: 'desc' }], }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -472,7 +441,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'sum', field: 'line_total', alias: 'total_revenue' }, ], groupBy: ['product_id'], - sort: [{ field: 'total_revenue', order: 'desc' }], + orderBy: [{ field: 'total_revenue', order: 'desc' }], top: 20, }; @@ -494,7 +463,7 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, ], }; @@ -510,7 +479,7 @@ describe('QuerySchema - Joins', () => { { type: 'inner', object: 'customer', - on: ['order.customer_id', '=', 'customer.id'], + on: { 'order.customer_id': { $eq: { $field: 'customer.id' } } }, }, ], }; @@ -527,7 +496,12 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: [['order.customer_id', '=', 'c.id'], 'and', ['order.status', '=', 'active']], + on: { + $and: [ + { 'order.customer_id': { $eq: { $field: 'c.id' } } }, + { 'order.status': 'active' }, + ], + }, }, ], }; @@ -547,7 +521,7 @@ describe('QuerySchema - Joins', () => { { type: 'left', object: 'order', - on: ['customer.id', '=', 'order.customer_id'], + on: { 'customer.id': { $eq: { $field: 'order.customer_id' } } }, }, ], }; @@ -564,7 +538,7 @@ describe('QuerySchema - Joins', () => { type: 'left', object: 'order', alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], + on: { 'customer.id': { $eq: { $field: 'o.customer_id' } } }, }, ], }; @@ -581,7 +555,7 @@ describe('QuerySchema - Joins', () => { type: 'left', object: 'order', alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], + on: { 'customer.id': { $eq: { $field: 'o.customer_id' } } }, }, ], filters: ['o.id', 'is_null', null], @@ -603,7 +577,7 @@ describe('QuerySchema - Joins', () => { type: 'right', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, ], }; @@ -619,7 +593,7 @@ describe('QuerySchema - Joins', () => { { type: 'right', object: 'customer', - on: ['order.customer_id', '=', 'customer.id'], + on: { 'order.customer_id': { $eq: { $field: 'customer.id' } } }, }, ], }; @@ -640,7 +614,7 @@ describe('QuerySchema - Joins', () => { type: 'full', object: 'order', alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], + on: { 'customer.id': { $eq: { $field: 'o.customer_id' } } }, }, ], }; @@ -657,7 +631,7 @@ describe('QuerySchema - Joins', () => { type: 'full', object: 'order', alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], + on: { 'customer.id': { $eq: { $field: 'o.customer_id' } } }, }, ], filters: [['customer.id', 'is_null', null], 'or', ['o.id', 'is_null', null]], @@ -679,13 +653,13 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, { type: 'inner', object: 'product', alias: 'p', - on: ['order.product_id', '=', 'p.id'], + on: { 'order.product_id': { $eq: { $field: 'p.id' } } }, }, ], }; @@ -702,19 +676,19 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, { type: 'left', object: 'product', alias: 'p', - on: ['order.product_id', '=', 'p.id'], + on: { 'order.product_id': { $eq: { $field: 'p.id' } } }, }, { type: 'left', object: 'shipment', alias: 's', - on: ['order.id', '=', 's.order_id'], + on: { 'order.id': { $eq: { $field: 's.order_id' } } }, }, ], }; @@ -731,25 +705,25 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, { type: 'inner', object: 'order_item', alias: 'oi', - on: ['order.id', '=', 'oi.order_id'], + on: { 'order.id': { $eq: { $field: 'oi.order_id' } } }, }, { type: 'inner', object: 'product', alias: 'p', - on: ['oi.product_id', '=', 'p.id'], + on: { 'oi.product_id': { $eq: { $field: 'p.id' } } }, }, { type: 'left', object: 'category', alias: 'cat', - on: ['p.category_id', '=', 'cat.id'], + on: { 'p.category_id': { $eq: { $field: 'cat.id' } } }, }, ], }; @@ -770,7 +744,7 @@ describe('QuerySchema - Joins', () => { type: 'left', object: 'employee', alias: 'manager', - on: ['employee.manager_id', '=', 'manager.id'], + on: { 'employee.manager_id': { $eq: { $field: 'manager.id' } } }, }, ], }; @@ -787,7 +761,7 @@ describe('QuerySchema - Joins', () => { type: 'left', object: 'category', alias: 'parent', - on: ['category.parent_id', '=', 'parent.id'], + on: { 'category.parent_id': { $eq: { $field: 'parent.id' } } }, }, ], }; @@ -809,7 +783,7 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, ], }; @@ -826,11 +800,12 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: [ - ['order.customer_id', '=', 'c.id'], - 'and', - ['c.status', '=', 'active'], - ], + on: { + $and: [ + { 'order.customer_id': { $eq: { $field: 'c.id' } } }, + { 'c.status': 'active' }, + ], + }, }, ], }; @@ -851,7 +826,7 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, ], aggregations: [ @@ -877,7 +852,7 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'high_value_customers', - on: ['order.customer_id', '=', 'high_value_customers.id'], + on: { 'order.customer_id': { $eq: { $field: 'high_value_customers.id' } } }, subquery: { object: 'customer', fields: ['id'], @@ -899,7 +874,7 @@ describe('QuerySchema - Joins', () => { type: 'left', object: 'order', alias: 'order_summary', - on: ['customer.id', '=', 'order_summary.customer_id'], + on: { 'customer.id': { $eq: { $field: 'order_summary.customer_id' } } }, subquery: { object: 'order', fields: ['customer_id'], @@ -928,7 +903,7 @@ describe('QuerySchema - Joins', () => { { type: 'left', object: 'contact', - on: ['account.id', '=', 'contact.account_id'], + on: { 'account.id': { $eq: { $field: 'contact.account_id' } } }, }, ], }; @@ -945,19 +920,19 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, { type: 'inner', object: 'order_item', alias: 'oi', - on: ['order.id', '=', 'oi.order_id'], + on: { 'order.id': { $eq: { $field: 'oi.order_id' } } }, }, { type: 'inner', object: 'product', alias: 'p', - on: ['oi.product_id', '=', 'p.id'], + on: { 'oi.product_id': { $eq: { $field: 'p.id' } } }, }, ], aggregations: [ @@ -979,7 +954,7 @@ describe('QuerySchema - Joins', () => { type: 'left', object: 'order', alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], + on: { 'customer.id': { $eq: { $field: 'o.customer_id' } } }, }, ], aggregations: [ @@ -988,7 +963,7 @@ describe('QuerySchema - Joins', () => { { function: 'max', field: 'o.created_at', alias: 'last_order_date' }, ], groupBy: ['customer.id', 'customer.name', 'customer.email'], - sort: [{ field: 'lifetime_value', order: 'desc' }], + orderBy: [{ field: 'lifetime_value', order: 'desc' }], }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -1565,7 +1540,7 @@ describe('QuerySchema - Complex Queries', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, ], aggregations: [ @@ -1573,8 +1548,8 @@ describe('QuerySchema - Complex Queries', () => { { function: 'count', alias: 'order_count' }, ], groupBy: ['customer_id'], - having: ['order_count', '>', 5], - sort: [{ field: 'total_amount', order: 'desc' }], + having: { order_count: { $gt: 5 } }, + orderBy: [{ field: 'total_amount', order: 'desc' }], top: 100, }; @@ -1591,7 +1566,7 @@ describe('QuerySchema - Complex Queries', () => { { type: 'inner', object: 'customer', - on: ['order.customer_id', '=', 'customer.id'], + on: { 'order.customer_id': { $eq: { $field: 'customer.id' } } }, }, ], aggregations: [ @@ -1608,8 +1583,8 @@ describe('QuerySchema - Complex Queries', () => { }, ], groupBy: ['customer_id'], - having: ['avg_amount', '>', 500], - sort: [{ field: 'avg_amount', order: 'desc' }], + having: { avg_amount: { $gt: 500 } }, + orderBy: [{ field: 'avg_amount', order: 'desc' }], top: 50, skip: 0, }; @@ -1653,7 +1628,7 @@ describe('QuerySchema - Edge Cases and Null Handling', () => { joins: [], windowFunctions: [], groupBy: [], - sort: [], + orderBy: [], }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -1780,13 +1755,13 @@ describe('QuerySchema - Edge Cases and Null Handling', () => { { type: 'left', object: 'order', - on: ['customer.id', '=', 'order.customer_id'], + on: { 'customer.id': { $eq: { $field: 'order.customer_id' } } }, }, { type: 'inner', object: 'filtered_orders', alias: 'fo', - on: ['customer.id', '=', 'fo.customer_id'], + on: { 'customer.id': { $eq: { $field: 'fo.customer_id' } } }, subquery: { object: 'order', fields: ['customer_id', 'amount'], @@ -1829,7 +1804,7 @@ describe('QuerySchema - Edge Cases and Null Handling', () => { { type: 'invalid_join', object: 'customer', - on: ['order.customer_id', '=', 'customer.id'], + on: { 'order.customer_id': { $eq: { $field: 'customer.id' } } }, }, ], })).toThrow(); @@ -1851,7 +1826,7 @@ describe('QuerySchema - Edge Cases and Null Handling', () => { it('should reject invalid sort order', () => { expect(() => QuerySchema.parse({ object: 'account', - sort: [{ field: 'name', order: 'invalid' }], + orderBy: [{ field: 'name', order: 'invalid' }], })).toThrow(); }); }); @@ -1892,11 +1867,11 @@ describe('QuerySchema - Type Coercion Edge Cases', () => { it('should handle default sort order', () => { const query: QueryAST = { object: 'account', - sort: [{ field: 'name' }], // order defaults to 'asc' + orderBy: [{ field: 'name' }], // order defaults to 'asc' }; const result = QuerySchema.parse(query); - expect(result.sort?.[0].order).toBe('asc'); + expect(result.orderBy?.[0].order).toBe('asc'); }); it('should handle mixed field types', () => { @@ -1945,11 +1920,12 @@ describe('QuerySchema - Type Coercion Edge Cases', () => { { function: 'sum', field: 'amount', alias: 'total' }, ], groupBy: ['customer_id'], - having: [ - ['order_count', '>', 5], - 'and', - ['total', '>', 1000], - ], + having: { + $and: [ + { order_count: { $gt: 5 } }, + { total: { $gt: 1000 } }, + ], + }, }; expect(() => QuerySchema.parse(query)).not.toThrow(); diff --git a/packages/spec/src/data/query.test.ts.backup b/packages/spec/src/data/query.test.ts.backup new file mode 100644 index 0000000000..55d130f112 --- /dev/null +++ b/packages/spec/src/data/query.test.ts.backup @@ -0,0 +1,1932 @@ +import { describe, it, expect } from 'vitest'; +import { + QuerySchema, + AggregationFunction, + JoinType, + WindowFunction, + type QueryAST, + type AggregationNode, + type JoinNode, + type WindowFunctionNode, +} from './query.zod'; + +describe('AggregationFunction', () => { + it('should accept valid aggregation functions', () => { + const validFunctions = [ + 'count', 'sum', 'avg', 'min', 'max', + 'count_distinct', 'array_agg', 'string_agg' + ]; + + validFunctions.forEach(fn => { + expect(() => AggregationFunction.parse(fn)).not.toThrow(); + }); + }); + + it('should reject invalid aggregation functions', () => { + expect(() => AggregationFunction.parse('COUNT')).toThrow(); + expect(() => AggregationFunction.parse('median')).toThrow(); + }); +}); + +describe('JoinType', () => { + it('should accept valid join types', () => { + expect(() => JoinType.parse('inner')).not.toThrow(); + expect(() => JoinType.parse('left')).not.toThrow(); + expect(() => JoinType.parse('right')).not.toThrow(); + expect(() => JoinType.parse('full')).not.toThrow(); + }); + + it('should reject invalid join types', () => { + expect(() => JoinType.parse('INNER')).toThrow(); + expect(() => JoinType.parse('cross')).toThrow(); + }); +}); + +describe('WindowFunction', () => { + it('should accept valid window functions', () => { + const validFunctions = [ + 'row_number', 'rank', 'dense_rank', 'percent_rank', + 'lag', 'lead', 'first_value', 'last_value', + 'sum', 'avg', 'count', 'min', 'max' + ]; + + validFunctions.forEach(fn => { + expect(() => WindowFunction.parse(fn)).not.toThrow(); + }); + }); +}); + +describe('QuerySchema - Basic', () => { + it('should accept simple query', () => { + const query: QueryAST = { + object: 'account', + fields: ['name', 'email'], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with filters', () => { + const query: QueryAST = { + object: 'account', + fields: ['name', 'email'], + filters: ['status', '=', 'active'], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with sort', () => { + const query: QueryAST = { + object: 'account', + fields: ['name', 'email'], + sort: [ + { field: 'name', order: 'asc' }, + { field: 'created_at', order: 'desc' }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with pagination', () => { + const query: QueryAST = { + object: 'account', + fields: ['name'], + top: 10, + skip: 20, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with distinct', () => { + const query: QueryAST = { + object: 'account', + fields: ['status'], + distinct: true, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); +}); + +describe('QuerySchema - Aggregations', () => { + // ============================================================================ + // Basic Aggregation Tests + // ============================================================================ + + it('should accept query with simple COUNT aggregation', () => { + const query: QueryAST = { + object: 'order', + aggregations: [ + { function: 'count', alias: 'total_orders' }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with SUM aggregation', () => { + const query: QueryAST = { + object: 'order', + aggregations: [ + { function: 'sum', field: 'amount', alias: 'total_amount' }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with AVG aggregation', () => { + const query: QueryAST = { + object: 'order', + aggregations: [ + { function: 'avg', field: 'amount', alias: 'avg_amount' }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with MIN aggregation', () => { + const query: QueryAST = { + object: 'product', + aggregations: [ + { function: 'min', field: 'price', alias: 'min_price' }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with MAX aggregation', () => { + const query: QueryAST = { + object: 'product', + aggregations: [ + { function: 'max', field: 'price', alias: 'max_price' }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with multiple aggregations', () => { + const query: QueryAST = { + object: 'order', + aggregations: [ + { function: 'count', alias: 'total_orders' }, + { function: 'sum', field: 'amount', alias: 'total_amount' }, + { function: 'avg', field: 'amount', alias: 'avg_amount' }, + { function: 'min', field: 'amount', alias: 'min_amount' }, + { function: 'max', field: 'amount', alias: 'max_amount' }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // COUNT DISTINCT Tests + // ============================================================================ + + it('should accept COUNT DISTINCT aggregation', () => { + const query: QueryAST = { + object: 'order', + aggregations: [ + { function: 'count_distinct', field: 'customer_id', alias: 'unique_customers' }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept aggregation with distinct flag', () => { + const query: QueryAST = { + object: 'order', + aggregations: [ + { function: 'count', field: 'customer_id', distinct: true, alias: 'unique_customers' }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // GROUP BY Tests + // ============================================================================ + + it('should accept query with single GROUP BY field', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + aggregations: [ + { function: 'count', alias: 'order_count' }, + ], + groupBy: ['customer_id'], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with multiple GROUP BY fields', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id', 'status'], + aggregations: [ + { function: 'count', alias: 'order_count' }, + { function: 'sum', field: 'amount', alias: 'total_amount' }, + ], + groupBy: ['customer_id', 'status'], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept GROUP BY with multiple aggregations', () => { + const query: QueryAST = { + object: 'sales', + fields: ['region', 'product_category'], + aggregations: [ + { function: 'sum', field: 'revenue', alias: 'total_revenue' }, + { function: 'avg', field: 'revenue', alias: 'avg_revenue' }, + { function: 'count', alias: 'num_sales' }, + { function: 'min', field: 'sale_date', alias: 'first_sale' }, + { function: 'max', field: 'sale_date', alias: 'last_sale' }, + ], + groupBy: ['region', 'product_category'], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // HAVING Clause Tests + // ============================================================================ + + it('should accept query with HAVING clause on COUNT', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + aggregations: [ + { function: 'count', alias: 'order_count' }, + ], + groupBy: ['customer_id'], + having: { order_count: { $gt: 5 } }, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with HAVING clause on SUM', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + aggregations: [ + { function: 'sum', field: 'amount', alias: 'total_amount' }, + ], + groupBy: ['customer_id'], + having: { total_amount: { $gt: 1000 } }, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with HAVING clause on AVG', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + aggregations: [ + { function: 'avg', field: 'amount', alias: 'avg_amount' }, + ], + groupBy: ['customer_id'], + having: { avg_amount: { $gte: 500 } }, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with complex HAVING clause', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + aggregations: [ + { function: 'count', alias: 'order_count' }, + { function: 'sum', field: 'amount', alias: 'total_amount' }, + ], + groupBy: ['customer_id'], + having: { $and: [{ order_count: { $gt: 3 } }, { total_amount: { $gt: 1000 } }] }, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with HAVING and WHERE clauses', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + filters: ['status', '=', 'completed'], + aggregations: [ + { function: 'sum', field: 'amount', alias: 'total_amount' }, + ], + groupBy: ['customer_id'], + having: { total_amount: { $gt: 5000 } }, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // Complex Aggregation Scenarios + // ============================================================================ + + it('should accept query with aggregation and sorting', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + aggregations: [ + { function: 'sum', field: 'amount', alias: 'total_amount' }, + ], + groupBy: ['customer_id'], + sort: [{ field: 'total_amount', order: 'desc' }], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with aggregation and pagination', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + aggregations: [ + { function: 'count', alias: 'order_count' }, + ], + groupBy: ['customer_id'], + sort: [{ field: 'order_count', order: 'desc' }], + top: 10, + skip: 0, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with ARRAY_AGG aggregation', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + aggregations: [ + { function: 'array_agg', field: 'product_id', alias: 'products' }, + ], + groupBy: ['customer_id'], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with STRING_AGG aggregation', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + aggregations: [ + { function: 'string_agg', field: 'product_name', alias: 'product_names' }, + ], + groupBy: ['customer_id'], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // Real-World Aggregation Examples (SQL Comparisons) + // ============================================================================ + + it('should accept sales report aggregation (SQL: SELECT region, SUM(amount) FROM sales GROUP BY region)', () => { + const query: QueryAST = { + object: 'sales', + fields: ['region'], + aggregations: [ + { function: 'sum', field: 'amount', alias: 'total_sales' }, + ], + groupBy: ['region'], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept customer summary aggregation (SQL: Multi-metric GROUP BY)', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + aggregations: [ + { function: 'count', alias: 'num_orders' }, + { function: 'sum', field: 'amount', alias: 'lifetime_value' }, + { function: 'avg', field: 'amount', alias: 'avg_order_value' }, + { function: 'max', field: 'created_at', alias: 'last_order_date' }, + ], + groupBy: ['customer_id'], + having: { num_orders: { $gt: 1 } }, + sort: [{ field: 'lifetime_value', order: 'desc' }], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept product analytics aggregation', () => { + const query: QueryAST = { + object: 'order_item', + fields: ['product_id'], + aggregations: [ + { function: 'count', alias: 'times_purchased' }, + { function: 'sum', field: 'quantity', alias: 'total_quantity' }, + { function: 'sum', field: 'line_total', alias: 'total_revenue' }, + ], + groupBy: ['product_id'], + sort: [{ field: 'total_revenue', order: 'desc' }], + top: 20, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); +}); + +describe('QuerySchema - Joins', () => { + // ============================================================================ + // INNER JOIN Tests + // ============================================================================ + + it('should accept query with INNER JOIN', () => { + const query: QueryAST = { + object: 'order', + fields: ['id', 'amount'], + joins: [ + { + type: 'inner', + object: 'customer', + alias: 'c', + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept INNER JOIN without alias', () => { + const query: QueryAST = { + object: 'order', + fields: ['id'], + joins: [ + { + type: 'inner', + object: 'customer', + on: { 'order.customer_id': { $eq: { $field: 'customer.id' } } }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept INNER JOIN with complex ON condition', () => { + const query: QueryAST = { + object: 'order', + fields: ['id'], + joins: [ + { + type: 'inner', + object: 'customer', + alias: 'c', + on: { + $and: [ + { 'order.customer_id': { $eq: { $field: 'c.id' } } }, + { 'order.status': 'active' }, + ], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // LEFT JOIN Tests + // ============================================================================ + + it('should accept query with LEFT JOIN', () => { + const query: QueryAST = { + object: 'customer', + fields: ['name'], + joins: [ + { + type: 'left', + object: 'order', + on: ['customer.id', '=', 'order.customer_id'], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept LEFT JOIN with alias', () => { + const query: QueryAST = { + object: 'customer', + fields: ['id', 'name'], + joins: [ + { + type: 'left', + object: 'order', + alias: 'o', + on: ['customer.id', '=', 'o.customer_id'], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept LEFT JOIN to find unmatched records', () => { + const query: QueryAST = { + object: 'customer', + fields: ['id', 'name'], + joins: [ + { + type: 'left', + object: 'order', + alias: 'o', + on: ['customer.id', '=', 'o.customer_id'], + }, + ], + filters: ['o.id', 'is_null', null], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // RIGHT JOIN Tests + // ============================================================================ + + it('should accept query with RIGHT JOIN', () => { + const query: QueryAST = { + object: 'order', + fields: ['id'], + joins: [ + { + type: 'right', + object: 'customer', + alias: 'c', + on: ['order.customer_id', '=', 'c.id'], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept RIGHT JOIN without alias', () => { + const query: QueryAST = { + object: 'order', + fields: ['id', 'amount'], + joins: [ + { + type: 'right', + object: 'customer', + on: ['order.customer_id', '=', 'customer.id'], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // FULL OUTER JOIN Tests + // ============================================================================ + + it('should accept query with FULL OUTER JOIN', () => { + const query: QueryAST = { + object: 'customer', + fields: ['id', 'name'], + joins: [ + { + type: 'full', + object: 'order', + alias: 'o', + on: ['customer.id', '=', 'o.customer_id'], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept FULL JOIN to find all unmatched records', () => { + const query: QueryAST = { + object: 'customer', + fields: ['id'], + joins: [ + { + type: 'full', + object: 'order', + alias: 'o', + on: ['customer.id', '=', 'o.customer_id'], + }, + ], + filters: [['customer.id', 'is_null', null], 'or', ['o.id', 'is_null', null]], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // Multiple Joins Tests + // ============================================================================ + + it('should accept query with multiple INNER JOINs', () => { + const query: QueryAST = { + object: 'order', + fields: ['id'], + joins: [ + { + type: 'inner', + object: 'customer', + alias: 'c', + on: ['order.customer_id', '=', 'c.id'], + }, + { + type: 'inner', + object: 'product', + alias: 'p', + on: ['order.product_id', '=', 'p.id'], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with mixed join types', () => { + const query: QueryAST = { + object: 'order', + fields: ['id'], + joins: [ + { + type: 'inner', + object: 'customer', + alias: 'c', + on: ['order.customer_id', '=', 'c.id'], + }, + { + type: 'left', + object: 'product', + alias: 'p', + on: ['order.product_id', '=', 'p.id'], + }, + { + type: 'left', + object: 'shipment', + alias: 's', + on: ['order.id', '=', 's.order_id'], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with 4+ table joins', () => { + const query: QueryAST = { + object: 'order', + fields: ['id', 'total'], + joins: [ + { + type: 'inner', + object: 'customer', + alias: 'c', + on: ['order.customer_id', '=', 'c.id'], + }, + { + type: 'inner', + object: 'order_item', + alias: 'oi', + on: ['order.id', '=', 'oi.order_id'], + }, + { + type: 'inner', + object: 'product', + alias: 'p', + on: ['oi.product_id', '=', 'p.id'], + }, + { + type: 'left', + object: 'category', + alias: 'cat', + on: ['p.category_id', '=', 'cat.id'], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // Self-Join Tests + // ============================================================================ + + it('should accept self-join query', () => { + const query: QueryAST = { + object: 'employee', + fields: ['id', 'name'], + joins: [ + { + type: 'left', + object: 'employee', + alias: 'manager', + on: ['employee.manager_id', '=', 'manager.id'], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept hierarchical self-join', () => { + const query: QueryAST = { + object: 'category', + fields: ['id', 'name'], + joins: [ + { + type: 'left', + object: 'category', + alias: 'parent', + on: ['category.parent_id', '=', 'parent.id'], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // Join with Filters Tests + // ============================================================================ + + it('should accept join with WHERE clause on main table', () => { + const query: QueryAST = { + object: 'order', + fields: ['id'], + filters: ['order.status', '=', 'completed'], + joins: [ + { + type: 'inner', + object: 'customer', + alias: 'c', + on: ['order.customer_id', '=', 'c.id'], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept join with ON clause containing multiple conditions', () => { + const query: QueryAST = { + object: 'order', + fields: ['id'], + joins: [ + { + type: 'inner', + object: 'customer', + alias: 'c', + on: [ + ['order.customer_id', '=', 'c.id'], + 'and', + ['c.status', '=', 'active'], + ], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // Join with Aggregations Tests + // ============================================================================ + + it('should accept join with GROUP BY and aggregations', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + joins: [ + { + type: 'inner', + object: 'customer', + alias: 'c', + on: ['order.customer_id', '=', 'c.id'], + }, + ], + aggregations: [ + { function: 'count', alias: 'order_count' }, + { function: 'sum', field: 'amount', alias: 'total_amount' }, + ], + groupBy: ['customer_id'], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // Subquery Join Tests + // ============================================================================ + + it('should accept query with subquery join', () => { + const query: QueryAST = { + object: 'order', + fields: ['id', 'amount'], + joins: [ + { + type: 'inner', + object: 'customer', + alias: 'high_value_customers', + on: ['order.customer_id', '=', 'high_value_customers.id'], + subquery: { + object: 'customer', + fields: ['id'], + filters: ['total_spent', '>', 10000], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept LEFT JOIN with aggregated subquery', () => { + const query: QueryAST = { + object: 'customer', + fields: ['id', 'name'], + joins: [ + { + type: 'left', + object: 'order', + alias: 'order_summary', + on: ['customer.id', '=', 'order_summary.customer_id'], + subquery: { + object: 'order', + fields: ['customer_id'], + aggregations: [ + { function: 'count', alias: 'order_count' }, + { function: 'sum', field: 'amount', alias: 'total_spent' }, + ], + groupBy: ['customer_id'], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // Real-World Join Examples (SOQL Comparisons) + // ============================================================================ + + it('should accept Salesforce-style relationship query (SOQL: SELECT Name, (SELECT Name FROM Contacts) FROM Account)', () => { + const query: QueryAST = { + object: 'account', + fields: ['id', 'name'], + joins: [ + { + type: 'left', + object: 'contact', + on: ['account.id', '=', 'contact.account_id'], + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept complex multi-table join for reporting', () => { + const query: QueryAST = { + object: 'order', + fields: ['id', 'order_date'], + joins: [ + { + type: 'inner', + object: 'customer', + alias: 'c', + on: ['order.customer_id', '=', 'c.id'], + }, + { + type: 'inner', + object: 'order_item', + alias: 'oi', + on: ['order.id', '=', 'oi.order_id'], + }, + { + type: 'inner', + object: 'product', + alias: 'p', + on: ['oi.product_id', '=', 'p.id'], + }, + ], + aggregations: [ + { function: 'sum', field: 'oi.quantity', alias: 'total_quantity' }, + { function: 'sum', field: 'oi.line_total', alias: 'order_total' }, + ], + groupBy: ['order.id', 'order.order_date'], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept customer order history join', () => { + const query: QueryAST = { + object: 'customer', + fields: ['id', 'name', 'email'], + joins: [ + { + type: 'left', + object: 'order', + alias: 'o', + on: ['customer.id', '=', 'o.customer_id'], + }, + ], + aggregations: [ + { function: 'count', field: 'o.id', alias: 'total_orders' }, + { function: 'sum', field: 'o.amount', alias: 'lifetime_value' }, + { function: 'max', field: 'o.created_at', alias: 'last_order_date' }, + ], + groupBy: ['customer.id', 'customer.name', 'customer.email'], + sort: [{ field: 'lifetime_value', order: 'desc' }], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); +}); + +describe('QuerySchema - Window Functions', () => { + // ============================================================================ + // ROW_NUMBER Tests + // ============================================================================ + + it('should accept query with ROW_NUMBER window function', () => { + const query: QueryAST = { + object: 'order', + fields: ['id', 'customer_id', 'amount'], + windowFunctions: [ + { + function: 'row_number', + alias: 'row_num', + over: { + partitionBy: ['customer_id'], + orderBy: [{ field: 'amount', order: 'desc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept ROW_NUMBER without partition', () => { + const query: QueryAST = { + object: 'student', + fields: ['name', 'score'], + windowFunctions: [ + { + function: 'row_number', + alias: 'rank', + over: { + orderBy: [{ field: 'score', order: 'desc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept ROW_NUMBER with multiple partition fields', () => { + const query: QueryAST = { + object: 'sales', + fields: ['region', 'product', 'revenue'], + windowFunctions: [ + { + function: 'row_number', + alias: 'row_num', + over: { + partitionBy: ['region', 'product'], + orderBy: [{ field: 'revenue', order: 'desc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // RANK and DENSE_RANK Tests + // ============================================================================ + + it('should accept query with RANK window function', () => { + const query: QueryAST = { + object: 'student', + fields: ['name', 'score'], + windowFunctions: [ + { + function: 'rank', + alias: 'rank', + over: { + orderBy: [{ field: 'score', order: 'desc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with DENSE_RANK window function', () => { + const query: QueryAST = { + object: 'employee', + fields: ['name', 'salary'], + windowFunctions: [ + { + function: 'dense_rank', + alias: 'salary_rank', + over: { + partitionBy: ['department'], + orderBy: [{ field: 'salary', order: 'desc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with PERCENT_RANK window function', () => { + const query: QueryAST = { + object: 'student', + fields: ['name', 'score'], + windowFunctions: [ + { + function: 'percent_rank', + alias: 'percentile', + over: { + orderBy: [{ field: 'score', order: 'desc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // LAG and LEAD Tests + // ============================================================================ + + it('should accept query with LAG window function', () => { + const query: QueryAST = { + object: 'sales', + fields: ['month', 'revenue'], + windowFunctions: [ + { + function: 'lag', + field: 'revenue', + alias: 'prev_month_revenue', + over: { + orderBy: [{ field: 'month', order: 'asc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with LEAD window function', () => { + const query: QueryAST = { + object: 'sales', + fields: ['month', 'revenue'], + windowFunctions: [ + { + function: 'lead', + field: 'revenue', + alias: 'next_month_revenue', + over: { + orderBy: [{ field: 'month', order: 'asc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept LAG and LEAD together', () => { + const query: QueryAST = { + object: 'stock_price', + fields: ['date', 'price'], + windowFunctions: [ + { + function: 'lag', + field: 'price', + alias: 'prev_day_price', + over: { + orderBy: [{ field: 'date', order: 'asc' }], + }, + }, + { + function: 'lead', + field: 'price', + alias: 'next_day_price', + over: { + orderBy: [{ field: 'date', order: 'asc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // FIRST_VALUE and LAST_VALUE Tests + // ============================================================================ + + it('should accept query with FIRST_VALUE window function', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id', 'order_date', 'amount'], + windowFunctions: [ + { + function: 'first_value', + field: 'amount', + alias: 'first_order_amount', + over: { + partitionBy: ['customer_id'], + orderBy: [{ field: 'order_date', order: 'asc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with LAST_VALUE window function', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id', 'order_date', 'amount'], + windowFunctions: [ + { + function: 'last_value', + field: 'amount', + alias: 'last_order_amount', + over: { + partitionBy: ['customer_id'], + orderBy: [{ field: 'order_date', order: 'asc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // Aggregate Window Function Tests + // ============================================================================ + + it('should accept query with SUM aggregate window function', () => { + const query: QueryAST = { + object: 'order', + fields: ['id', 'amount'], + windowFunctions: [ + { + function: 'sum', + field: 'amount', + alias: 'running_total', + over: { + orderBy: [{ field: 'created_at', order: 'asc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with AVG aggregate window function', () => { + const query: QueryAST = { + object: 'sales', + fields: ['month', 'revenue'], + windowFunctions: [ + { + function: 'avg', + field: 'revenue', + alias: 'moving_avg', + over: { + orderBy: [{ field: 'month', order: 'asc' }], + frame: { + type: 'rows', + start: '2 PRECEDING', + end: 'CURRENT ROW', + }, + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with COUNT aggregate window function', () => { + const query: QueryAST = { + object: 'event', + fields: ['timestamp', 'user_id'], + windowFunctions: [ + { + function: 'count', + alias: 'running_count', + over: { + partitionBy: ['user_id'], + orderBy: [{ field: 'timestamp', order: 'asc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with MIN/MAX aggregate window functions', () => { + const query: QueryAST = { + object: 'temperature', + fields: ['date', 'value'], + windowFunctions: [ + { + function: 'min', + field: 'value', + alias: 'min_so_far', + over: { + orderBy: [{ field: 'date', order: 'asc' }], + }, + }, + { + function: 'max', + field: 'value', + alias: 'max_so_far', + over: { + orderBy: [{ field: 'date', order: 'asc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // Window Frame Specification Tests + // ============================================================================ + + it('should accept query with ROWS frame specification', () => { + const query: QueryAST = { + object: 'order', + fields: ['id', 'amount'], + windowFunctions: [ + { + function: 'sum', + field: 'amount', + alias: 'running_total', + over: { + orderBy: [{ field: 'created_at', order: 'asc' }], + frame: { + type: 'rows', + start: 'UNBOUNDED PRECEDING', + end: 'CURRENT ROW', + }, + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with RANGE frame specification', () => { + const query: QueryAST = { + object: 'sales', + fields: ['date', 'amount'], + windowFunctions: [ + { + function: 'sum', + field: 'amount', + alias: 'total_in_range', + over: { + orderBy: [{ field: 'date', order: 'asc' }], + frame: { + type: 'range', + start: '7 PRECEDING', + end: 'CURRENT ROW', + }, + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with window frame FOLLOWING', () => { + const query: QueryAST = { + object: 'sales', + fields: ['month', 'revenue'], + windowFunctions: [ + { + function: 'avg', + field: 'revenue', + alias: 'centered_avg', + over: { + orderBy: [{ field: 'month', order: 'asc' }], + frame: { + type: 'rows', + start: '1 PRECEDING', + end: '1 FOLLOWING', + }, + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // Multiple Window Functions Tests + // ============================================================================ + + it('should accept query with multiple window functions', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id', 'amount', 'created_at'], + windowFunctions: [ + { + function: 'row_number', + alias: 'row_num', + over: { + partitionBy: ['customer_id'], + orderBy: [{ field: 'created_at', order: 'desc' }], + }, + }, + { + function: 'rank', + alias: 'amount_rank', + over: { + partitionBy: ['customer_id'], + orderBy: [{ field: 'amount', order: 'desc' }], + }, + }, + { + function: 'sum', + field: 'amount', + alias: 'running_total', + over: { + partitionBy: ['customer_id'], + orderBy: [{ field: 'created_at', order: 'asc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + // ============================================================================ + // Real-World Window Function Examples + // ============================================================================ + + it('should accept query for top N per group (SQL: ROW_NUMBER() OVER (PARTITION BY ...))', () => { + const query: QueryAST = { + object: 'product', + fields: ['category_id', 'name', 'price'], + windowFunctions: [ + { + function: 'row_number', + alias: 'rank_in_category', + over: { + partitionBy: ['category_id'], + orderBy: [{ field: 'price', order: 'desc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept running total query', () => { + const query: QueryAST = { + object: 'transaction', + fields: ['date', 'amount'], + windowFunctions: [ + { + function: 'sum', + field: 'amount', + alias: 'running_balance', + over: { + orderBy: [{ field: 'date', order: 'asc' }], + frame: { + type: 'rows', + start: 'UNBOUNDED PRECEDING', + end: 'CURRENT ROW', + }, + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept moving average query', () => { + const query: QueryAST = { + object: 'stock_price', + fields: ['date', 'close_price'], + windowFunctions: [ + { + function: 'avg', + field: 'close_price', + alias: 'ma_7_day', + over: { + orderBy: [{ field: 'date', order: 'asc' }], + frame: { + type: 'rows', + start: '6 PRECEDING', + end: 'CURRENT ROW', + }, + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept year-over-year comparison query', () => { + const query: QueryAST = { + object: 'monthly_sales', + fields: ['month', 'revenue'], + windowFunctions: [ + { + function: 'lag', + field: 'revenue', + alias: 'prev_year_revenue', + over: { + orderBy: [{ field: 'month', order: 'asc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept employee ranking within department', () => { + const query: QueryAST = { + object: 'employee', + fields: ['department', 'name', 'salary'], + windowFunctions: [ + { + function: 'rank', + alias: 'salary_rank', + over: { + partitionBy: ['department'], + orderBy: [{ field: 'salary', order: 'desc' }], + }, + }, + { + function: 'percent_rank', + alias: 'salary_percentile', + over: { + partitionBy: ['department'], + orderBy: [{ field: 'salary', order: 'desc' }], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); +}); + +describe('QuerySchema - Complex Queries', () => { + it('should accept complex query with joins, aggregations, and window functions', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + joins: [ + { + type: 'inner', + object: 'customer', + alias: 'c', + on: ['order.customer_id', '=', 'c.id'], + }, + ], + aggregations: [ + { function: 'sum', field: 'amount', alias: 'total_amount' }, + { function: 'count', alias: 'order_count' }, + ], + groupBy: ['customer_id'], + having: { order_count: { $gt: 5 } }, + sort: [{ field: 'total_amount', order: 'desc' }], + top: 100, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should accept query with all features', () => { + const query: QueryAST = { + object: 'order', + fields: ['id', 'customer_id', 'amount'], + distinct: true, + filters: ['status', '=', 'completed'], + joins: [ + { + type: 'inner', + object: 'customer', + on: ['order.customer_id', '=', 'customer.id'], + }, + ], + aggregations: [ + { function: 'avg', field: 'amount', alias: 'avg_amount' }, + ], + windowFunctions: [ + { + function: 'rank', + alias: 'customer_rank', + over: { + partitionBy: ['customer_id'], + orderBy: [{ field: 'amount', order: 'desc' }], + }, + }, + ], + groupBy: ['customer_id'], + having: { avg_amount: { $gt: 500 } }, + sort: [{ field: 'avg_amount', order: 'desc' }], + top: 50, + skip: 0, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); +}); + +describe('QuerySchema - Edge Cases and Null Handling', () => { + it('should handle null values in filter expressions', () => { + const query: QueryAST = { + object: 'account', + fields: ['name'], + filters: ['deleted_at', 'is_null', null], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should handle undefined for optional fields', () => { + const query: QueryAST = { + object: 'account', + fields: undefined, + filters: undefined, + sort: undefined, + aggregations: undefined, + joins: undefined, + groupBy: undefined, + having: undefined, + windowFunctions: undefined, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should handle empty arrays', () => { + const query: QueryAST = { + object: 'account', + fields: [], + aggregations: [], + joins: [], + windowFunctions: [], + groupBy: [], + sort: [], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should handle zero and negative values in pagination', () => { + const queries = [ + { object: 'account', top: 0, skip: 0 }, + { object: 'account', top: 1, skip: 0 }, + { object: 'account', top: 100, skip: 1000 }, + ]; + + queries.forEach(query => { + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + }); + + it('should handle complex nested null filters', () => { + const query: QueryAST = { + object: 'order', + fields: ['id'], + filters: [ + ['approved_at', 'is_null', null], + 'and', + ['rejected_at', 'is_null', null], + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should handle optional alias in field nodes', () => { + const query: QueryAST = { + object: 'account', + fields: [ + 'name', + { field: 'owner', fields: ['name', 'email'] }, + { field: 'manager', fields: ['name'], alias: 'mgr' }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should handle aggregation without field for COUNT', () => { + const query: QueryAST = { + object: 'order', + aggregations: [ + { function: 'count', alias: 'total_count' }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should handle optional distinct flag in aggregation', () => { + const query: QueryAST = { + object: 'order', + aggregations: [ + { function: 'count', field: 'customer_id', alias: 'unique_customers', distinct: true }, + { function: 'sum', field: 'amount', alias: 'total_amount' }, // distinct undefined + ], + groupBy: ['region'], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should handle optional properties in window functions', () => { + const query: QueryAST = { + object: 'sales', + fields: ['amount'], + windowFunctions: [ + { + function: 'row_number', + alias: 'row_num', + over: { + // partitionBy and orderBy are optional + }, + }, + { + function: 'sum', + field: 'amount', + alias: 'total', + over: { + partitionBy: ['region'], + // orderBy is optional + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should handle optional frame in window specification', () => { + const query: QueryAST = { + object: 'transactions', + fields: ['amount'], + windowFunctions: [ + { + function: 'sum', + field: 'amount', + alias: 'running_total', + over: { + orderBy: [{ field: 'date', order: 'asc' }], + frame: { + type: 'rows', + start: 'UNBOUNDED PRECEDING', + end: 'CURRENT ROW', + }, + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should handle optional subquery in joins', () => { + const query: QueryAST = { + object: 'customer', + joins: [ + { + type: 'left', + object: 'order', + on: ['customer.id', '=', 'order.customer_id'], + }, + { + type: 'inner', + object: 'filtered_orders', + alias: 'fo', + on: ['customer.id', '=', 'fo.customer_id'], + subquery: { + object: 'order', + fields: ['customer_id', 'amount'], + filters: ['amount', '>', 1000], + }, + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should reject invalid object type', () => { + expect(() => QuerySchema.parse({ + object: 123, // Should be string + fields: ['name'], + })).toThrow(); + }); + + it('should reject invalid field types in array', () => { + expect(() => QuerySchema.parse({ + object: 'account', + fields: [123, 456], // Should be strings or objects + })).toThrow(); + }); + + it('should reject invalid aggregation function', () => { + expect(() => QuerySchema.parse({ + object: 'order', + aggregations: [ + { function: 'invalid_func', alias: 'test' }, + ], + })).toThrow(); + }); + + it('should reject invalid join type', () => { + expect(() => QuerySchema.parse({ + object: 'order', + joins: [ + { + type: 'invalid_join', + object: 'customer', + on: ['order.customer_id', '=', 'customer.id'], + }, + ], + })).toThrow(); + }); + + it('should reject invalid window function', () => { + expect(() => QuerySchema.parse({ + object: 'sales', + windowFunctions: [ + { + function: 'invalid_window_func', + alias: 'test', + over: {}, + }, + ], + })).toThrow(); + }); + + it('should reject invalid sort order', () => { + expect(() => QuerySchema.parse({ + object: 'account', + sort: [{ field: 'name', order: 'invalid' }], + })).toThrow(); + }); +}); + +describe('QuerySchema - Type Coercion Edge Cases', () => { + it('should handle various data types in filter values', () => { + const queries = [ + { object: 'account', filters: ['age', '>', 18] }, // number + { object: 'account', filters: ['active', '=', true] }, // boolean + { object: 'account', filters: ['name', '=', 'John'] }, // string + { object: 'account', filters: ['tags', 'in', ['a', 'b', 'c']] }, // array + { object: 'account', filters: ['value', 'between', [0, 100]] }, // array + ]; + + queries.forEach(query => { + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + }); + + it('should handle boolean flags', () => { + const query: QueryAST = { + object: 'account', + fields: ['name'], + distinct: true, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + + const query2: QueryAST = { + object: 'account', + fields: ['name'], + distinct: false, + }; + + expect(() => QuerySchema.parse(query2)).not.toThrow(); + }); + + it('should handle default sort order', () => { + const query: QueryAST = { + object: 'account', + sort: [{ field: 'name' }], // order defaults to 'asc' + }; + + const result = QuerySchema.parse(query); + expect(result.sort?.[0].order).toBe('asc'); + }); + + it('should handle mixed field types', () => { + const query: QueryAST = { + object: 'account', + fields: [ + 'simple_field', + { + field: 'related_field', + fields: ['nested_field'], + alias: 'rel', + }, + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should handle deeply nested filters', () => { + const query: QueryAST = { + object: 'order', + filters: [ + [ + ['status', '=', 'active'], + 'and', + ['amount', '>', 100], + ], + 'or', + [ + ['priority', '=', 'high'], + 'and', + ['urgent', '=', true], + ], + ], + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); + + it('should handle complex having clauses', () => { + const query: QueryAST = { + object: 'order', + fields: ['customer_id'], + aggregations: [ + { function: 'count', alias: 'order_count' }, + { function: 'sum', field: 'amount', alias: 'total' }, + ], + groupBy: ['customer_id'], + having: { + $and: [ + { order_count: { $gt: 5 } }, + { total: { $gt: 1000 } }, + ], + }, + }; + + expect(() => QuerySchema.parse(query)).not.toThrow(); + }); +}); From 88c545ad1486210f568b066bbcd89ef560ac34e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 05:11:57 +0000 Subject: [PATCH 3/3] Remove temporary backup file Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/spec/src/data/query.test.ts.backup | 1932 ------------------- 1 file changed, 1932 deletions(-) delete mode 100644 packages/spec/src/data/query.test.ts.backup diff --git a/packages/spec/src/data/query.test.ts.backup b/packages/spec/src/data/query.test.ts.backup deleted file mode 100644 index 55d130f112..0000000000 --- a/packages/spec/src/data/query.test.ts.backup +++ /dev/null @@ -1,1932 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - QuerySchema, - AggregationFunction, - JoinType, - WindowFunction, - type QueryAST, - type AggregationNode, - type JoinNode, - type WindowFunctionNode, -} from './query.zod'; - -describe('AggregationFunction', () => { - it('should accept valid aggregation functions', () => { - const validFunctions = [ - 'count', 'sum', 'avg', 'min', 'max', - 'count_distinct', 'array_agg', 'string_agg' - ]; - - validFunctions.forEach(fn => { - expect(() => AggregationFunction.parse(fn)).not.toThrow(); - }); - }); - - it('should reject invalid aggregation functions', () => { - expect(() => AggregationFunction.parse('COUNT')).toThrow(); - expect(() => AggregationFunction.parse('median')).toThrow(); - }); -}); - -describe('JoinType', () => { - it('should accept valid join types', () => { - expect(() => JoinType.parse('inner')).not.toThrow(); - expect(() => JoinType.parse('left')).not.toThrow(); - expect(() => JoinType.parse('right')).not.toThrow(); - expect(() => JoinType.parse('full')).not.toThrow(); - }); - - it('should reject invalid join types', () => { - expect(() => JoinType.parse('INNER')).toThrow(); - expect(() => JoinType.parse('cross')).toThrow(); - }); -}); - -describe('WindowFunction', () => { - it('should accept valid window functions', () => { - const validFunctions = [ - 'row_number', 'rank', 'dense_rank', 'percent_rank', - 'lag', 'lead', 'first_value', 'last_value', - 'sum', 'avg', 'count', 'min', 'max' - ]; - - validFunctions.forEach(fn => { - expect(() => WindowFunction.parse(fn)).not.toThrow(); - }); - }); -}); - -describe('QuerySchema - Basic', () => { - it('should accept simple query', () => { - const query: QueryAST = { - object: 'account', - fields: ['name', 'email'], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with filters', () => { - const query: QueryAST = { - object: 'account', - fields: ['name', 'email'], - filters: ['status', '=', 'active'], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with sort', () => { - const query: QueryAST = { - object: 'account', - fields: ['name', 'email'], - sort: [ - { field: 'name', order: 'asc' }, - { field: 'created_at', order: 'desc' }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with pagination', () => { - const query: QueryAST = { - object: 'account', - fields: ['name'], - top: 10, - skip: 20, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with distinct', () => { - const query: QueryAST = { - object: 'account', - fields: ['status'], - distinct: true, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); -}); - -describe('QuerySchema - Aggregations', () => { - // ============================================================================ - // Basic Aggregation Tests - // ============================================================================ - - it('should accept query with simple COUNT aggregation', () => { - const query: QueryAST = { - object: 'order', - aggregations: [ - { function: 'count', alias: 'total_orders' }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with SUM aggregation', () => { - const query: QueryAST = { - object: 'order', - aggregations: [ - { function: 'sum', field: 'amount', alias: 'total_amount' }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with AVG aggregation', () => { - const query: QueryAST = { - object: 'order', - aggregations: [ - { function: 'avg', field: 'amount', alias: 'avg_amount' }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with MIN aggregation', () => { - const query: QueryAST = { - object: 'product', - aggregations: [ - { function: 'min', field: 'price', alias: 'min_price' }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with MAX aggregation', () => { - const query: QueryAST = { - object: 'product', - aggregations: [ - { function: 'max', field: 'price', alias: 'max_price' }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with multiple aggregations', () => { - const query: QueryAST = { - object: 'order', - aggregations: [ - { function: 'count', alias: 'total_orders' }, - { function: 'sum', field: 'amount', alias: 'total_amount' }, - { function: 'avg', field: 'amount', alias: 'avg_amount' }, - { function: 'min', field: 'amount', alias: 'min_amount' }, - { function: 'max', field: 'amount', alias: 'max_amount' }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // COUNT DISTINCT Tests - // ============================================================================ - - it('should accept COUNT DISTINCT aggregation', () => { - const query: QueryAST = { - object: 'order', - aggregations: [ - { function: 'count_distinct', field: 'customer_id', alias: 'unique_customers' }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept aggregation with distinct flag', () => { - const query: QueryAST = { - object: 'order', - aggregations: [ - { function: 'count', field: 'customer_id', distinct: true, alias: 'unique_customers' }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // GROUP BY Tests - // ============================================================================ - - it('should accept query with single GROUP BY field', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - aggregations: [ - { function: 'count', alias: 'order_count' }, - ], - groupBy: ['customer_id'], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with multiple GROUP BY fields', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id', 'status'], - aggregations: [ - { function: 'count', alias: 'order_count' }, - { function: 'sum', field: 'amount', alias: 'total_amount' }, - ], - groupBy: ['customer_id', 'status'], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept GROUP BY with multiple aggregations', () => { - const query: QueryAST = { - object: 'sales', - fields: ['region', 'product_category'], - aggregations: [ - { function: 'sum', field: 'revenue', alias: 'total_revenue' }, - { function: 'avg', field: 'revenue', alias: 'avg_revenue' }, - { function: 'count', alias: 'num_sales' }, - { function: 'min', field: 'sale_date', alias: 'first_sale' }, - { function: 'max', field: 'sale_date', alias: 'last_sale' }, - ], - groupBy: ['region', 'product_category'], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // HAVING Clause Tests - // ============================================================================ - - it('should accept query with HAVING clause on COUNT', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - aggregations: [ - { function: 'count', alias: 'order_count' }, - ], - groupBy: ['customer_id'], - having: { order_count: { $gt: 5 } }, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with HAVING clause on SUM', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - aggregations: [ - { function: 'sum', field: 'amount', alias: 'total_amount' }, - ], - groupBy: ['customer_id'], - having: { total_amount: { $gt: 1000 } }, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with HAVING clause on AVG', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - aggregations: [ - { function: 'avg', field: 'amount', alias: 'avg_amount' }, - ], - groupBy: ['customer_id'], - having: { avg_amount: { $gte: 500 } }, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with complex HAVING clause', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - aggregations: [ - { function: 'count', alias: 'order_count' }, - { function: 'sum', field: 'amount', alias: 'total_amount' }, - ], - groupBy: ['customer_id'], - having: { $and: [{ order_count: { $gt: 3 } }, { total_amount: { $gt: 1000 } }] }, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with HAVING and WHERE clauses', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - filters: ['status', '=', 'completed'], - aggregations: [ - { function: 'sum', field: 'amount', alias: 'total_amount' }, - ], - groupBy: ['customer_id'], - having: { total_amount: { $gt: 5000 } }, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // Complex Aggregation Scenarios - // ============================================================================ - - it('should accept query with aggregation and sorting', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - aggregations: [ - { function: 'sum', field: 'amount', alias: 'total_amount' }, - ], - groupBy: ['customer_id'], - sort: [{ field: 'total_amount', order: 'desc' }], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with aggregation and pagination', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - aggregations: [ - { function: 'count', alias: 'order_count' }, - ], - groupBy: ['customer_id'], - sort: [{ field: 'order_count', order: 'desc' }], - top: 10, - skip: 0, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with ARRAY_AGG aggregation', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - aggregations: [ - { function: 'array_agg', field: 'product_id', alias: 'products' }, - ], - groupBy: ['customer_id'], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with STRING_AGG aggregation', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - aggregations: [ - { function: 'string_agg', field: 'product_name', alias: 'product_names' }, - ], - groupBy: ['customer_id'], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // Real-World Aggregation Examples (SQL Comparisons) - // ============================================================================ - - it('should accept sales report aggregation (SQL: SELECT region, SUM(amount) FROM sales GROUP BY region)', () => { - const query: QueryAST = { - object: 'sales', - fields: ['region'], - aggregations: [ - { function: 'sum', field: 'amount', alias: 'total_sales' }, - ], - groupBy: ['region'], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept customer summary aggregation (SQL: Multi-metric GROUP BY)', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - aggregations: [ - { function: 'count', alias: 'num_orders' }, - { function: 'sum', field: 'amount', alias: 'lifetime_value' }, - { function: 'avg', field: 'amount', alias: 'avg_order_value' }, - { function: 'max', field: 'created_at', alias: 'last_order_date' }, - ], - groupBy: ['customer_id'], - having: { num_orders: { $gt: 1 } }, - sort: [{ field: 'lifetime_value', order: 'desc' }], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept product analytics aggregation', () => { - const query: QueryAST = { - object: 'order_item', - fields: ['product_id'], - aggregations: [ - { function: 'count', alias: 'times_purchased' }, - { function: 'sum', field: 'quantity', alias: 'total_quantity' }, - { function: 'sum', field: 'line_total', alias: 'total_revenue' }, - ], - groupBy: ['product_id'], - sort: [{ field: 'total_revenue', order: 'desc' }], - top: 20, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); -}); - -describe('QuerySchema - Joins', () => { - // ============================================================================ - // INNER JOIN Tests - // ============================================================================ - - it('should accept query with INNER JOIN', () => { - const query: QueryAST = { - object: 'order', - fields: ['id', 'amount'], - joins: [ - { - type: 'inner', - object: 'customer', - alias: 'c', - on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept INNER JOIN without alias', () => { - const query: QueryAST = { - object: 'order', - fields: ['id'], - joins: [ - { - type: 'inner', - object: 'customer', - on: { 'order.customer_id': { $eq: { $field: 'customer.id' } } }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept INNER JOIN with complex ON condition', () => { - const query: QueryAST = { - object: 'order', - fields: ['id'], - joins: [ - { - type: 'inner', - object: 'customer', - alias: 'c', - on: { - $and: [ - { 'order.customer_id': { $eq: { $field: 'c.id' } } }, - { 'order.status': 'active' }, - ], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // LEFT JOIN Tests - // ============================================================================ - - it('should accept query with LEFT JOIN', () => { - const query: QueryAST = { - object: 'customer', - fields: ['name'], - joins: [ - { - type: 'left', - object: 'order', - on: ['customer.id', '=', 'order.customer_id'], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept LEFT JOIN with alias', () => { - const query: QueryAST = { - object: 'customer', - fields: ['id', 'name'], - joins: [ - { - type: 'left', - object: 'order', - alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept LEFT JOIN to find unmatched records', () => { - const query: QueryAST = { - object: 'customer', - fields: ['id', 'name'], - joins: [ - { - type: 'left', - object: 'order', - alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], - }, - ], - filters: ['o.id', 'is_null', null], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // RIGHT JOIN Tests - // ============================================================================ - - it('should accept query with RIGHT JOIN', () => { - const query: QueryAST = { - object: 'order', - fields: ['id'], - joins: [ - { - type: 'right', - object: 'customer', - alias: 'c', - on: ['order.customer_id', '=', 'c.id'], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept RIGHT JOIN without alias', () => { - const query: QueryAST = { - object: 'order', - fields: ['id', 'amount'], - joins: [ - { - type: 'right', - object: 'customer', - on: ['order.customer_id', '=', 'customer.id'], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // FULL OUTER JOIN Tests - // ============================================================================ - - it('should accept query with FULL OUTER JOIN', () => { - const query: QueryAST = { - object: 'customer', - fields: ['id', 'name'], - joins: [ - { - type: 'full', - object: 'order', - alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept FULL JOIN to find all unmatched records', () => { - const query: QueryAST = { - object: 'customer', - fields: ['id'], - joins: [ - { - type: 'full', - object: 'order', - alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], - }, - ], - filters: [['customer.id', 'is_null', null], 'or', ['o.id', 'is_null', null]], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // Multiple Joins Tests - // ============================================================================ - - it('should accept query with multiple INNER JOINs', () => { - const query: QueryAST = { - object: 'order', - fields: ['id'], - joins: [ - { - type: 'inner', - object: 'customer', - alias: 'c', - on: ['order.customer_id', '=', 'c.id'], - }, - { - type: 'inner', - object: 'product', - alias: 'p', - on: ['order.product_id', '=', 'p.id'], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with mixed join types', () => { - const query: QueryAST = { - object: 'order', - fields: ['id'], - joins: [ - { - type: 'inner', - object: 'customer', - alias: 'c', - on: ['order.customer_id', '=', 'c.id'], - }, - { - type: 'left', - object: 'product', - alias: 'p', - on: ['order.product_id', '=', 'p.id'], - }, - { - type: 'left', - object: 'shipment', - alias: 's', - on: ['order.id', '=', 's.order_id'], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with 4+ table joins', () => { - const query: QueryAST = { - object: 'order', - fields: ['id', 'total'], - joins: [ - { - type: 'inner', - object: 'customer', - alias: 'c', - on: ['order.customer_id', '=', 'c.id'], - }, - { - type: 'inner', - object: 'order_item', - alias: 'oi', - on: ['order.id', '=', 'oi.order_id'], - }, - { - type: 'inner', - object: 'product', - alias: 'p', - on: ['oi.product_id', '=', 'p.id'], - }, - { - type: 'left', - object: 'category', - alias: 'cat', - on: ['p.category_id', '=', 'cat.id'], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // Self-Join Tests - // ============================================================================ - - it('should accept self-join query', () => { - const query: QueryAST = { - object: 'employee', - fields: ['id', 'name'], - joins: [ - { - type: 'left', - object: 'employee', - alias: 'manager', - on: ['employee.manager_id', '=', 'manager.id'], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept hierarchical self-join', () => { - const query: QueryAST = { - object: 'category', - fields: ['id', 'name'], - joins: [ - { - type: 'left', - object: 'category', - alias: 'parent', - on: ['category.parent_id', '=', 'parent.id'], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // Join with Filters Tests - // ============================================================================ - - it('should accept join with WHERE clause on main table', () => { - const query: QueryAST = { - object: 'order', - fields: ['id'], - filters: ['order.status', '=', 'completed'], - joins: [ - { - type: 'inner', - object: 'customer', - alias: 'c', - on: ['order.customer_id', '=', 'c.id'], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept join with ON clause containing multiple conditions', () => { - const query: QueryAST = { - object: 'order', - fields: ['id'], - joins: [ - { - type: 'inner', - object: 'customer', - alias: 'c', - on: [ - ['order.customer_id', '=', 'c.id'], - 'and', - ['c.status', '=', 'active'], - ], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // Join with Aggregations Tests - // ============================================================================ - - it('should accept join with GROUP BY and aggregations', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - joins: [ - { - type: 'inner', - object: 'customer', - alias: 'c', - on: ['order.customer_id', '=', 'c.id'], - }, - ], - aggregations: [ - { function: 'count', alias: 'order_count' }, - { function: 'sum', field: 'amount', alias: 'total_amount' }, - ], - groupBy: ['customer_id'], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // Subquery Join Tests - // ============================================================================ - - it('should accept query with subquery join', () => { - const query: QueryAST = { - object: 'order', - fields: ['id', 'amount'], - joins: [ - { - type: 'inner', - object: 'customer', - alias: 'high_value_customers', - on: ['order.customer_id', '=', 'high_value_customers.id'], - subquery: { - object: 'customer', - fields: ['id'], - filters: ['total_spent', '>', 10000], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept LEFT JOIN with aggregated subquery', () => { - const query: QueryAST = { - object: 'customer', - fields: ['id', 'name'], - joins: [ - { - type: 'left', - object: 'order', - alias: 'order_summary', - on: ['customer.id', '=', 'order_summary.customer_id'], - subquery: { - object: 'order', - fields: ['customer_id'], - aggregations: [ - { function: 'count', alias: 'order_count' }, - { function: 'sum', field: 'amount', alias: 'total_spent' }, - ], - groupBy: ['customer_id'], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // Real-World Join Examples (SOQL Comparisons) - // ============================================================================ - - it('should accept Salesforce-style relationship query (SOQL: SELECT Name, (SELECT Name FROM Contacts) FROM Account)', () => { - const query: QueryAST = { - object: 'account', - fields: ['id', 'name'], - joins: [ - { - type: 'left', - object: 'contact', - on: ['account.id', '=', 'contact.account_id'], - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept complex multi-table join for reporting', () => { - const query: QueryAST = { - object: 'order', - fields: ['id', 'order_date'], - joins: [ - { - type: 'inner', - object: 'customer', - alias: 'c', - on: ['order.customer_id', '=', 'c.id'], - }, - { - type: 'inner', - object: 'order_item', - alias: 'oi', - on: ['order.id', '=', 'oi.order_id'], - }, - { - type: 'inner', - object: 'product', - alias: 'p', - on: ['oi.product_id', '=', 'p.id'], - }, - ], - aggregations: [ - { function: 'sum', field: 'oi.quantity', alias: 'total_quantity' }, - { function: 'sum', field: 'oi.line_total', alias: 'order_total' }, - ], - groupBy: ['order.id', 'order.order_date'], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept customer order history join', () => { - const query: QueryAST = { - object: 'customer', - fields: ['id', 'name', 'email'], - joins: [ - { - type: 'left', - object: 'order', - alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], - }, - ], - aggregations: [ - { function: 'count', field: 'o.id', alias: 'total_orders' }, - { function: 'sum', field: 'o.amount', alias: 'lifetime_value' }, - { function: 'max', field: 'o.created_at', alias: 'last_order_date' }, - ], - groupBy: ['customer.id', 'customer.name', 'customer.email'], - sort: [{ field: 'lifetime_value', order: 'desc' }], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); -}); - -describe('QuerySchema - Window Functions', () => { - // ============================================================================ - // ROW_NUMBER Tests - // ============================================================================ - - it('should accept query with ROW_NUMBER window function', () => { - const query: QueryAST = { - object: 'order', - fields: ['id', 'customer_id', 'amount'], - windowFunctions: [ - { - function: 'row_number', - alias: 'row_num', - over: { - partitionBy: ['customer_id'], - orderBy: [{ field: 'amount', order: 'desc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept ROW_NUMBER without partition', () => { - const query: QueryAST = { - object: 'student', - fields: ['name', 'score'], - windowFunctions: [ - { - function: 'row_number', - alias: 'rank', - over: { - orderBy: [{ field: 'score', order: 'desc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept ROW_NUMBER with multiple partition fields', () => { - const query: QueryAST = { - object: 'sales', - fields: ['region', 'product', 'revenue'], - windowFunctions: [ - { - function: 'row_number', - alias: 'row_num', - over: { - partitionBy: ['region', 'product'], - orderBy: [{ field: 'revenue', order: 'desc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // RANK and DENSE_RANK Tests - // ============================================================================ - - it('should accept query with RANK window function', () => { - const query: QueryAST = { - object: 'student', - fields: ['name', 'score'], - windowFunctions: [ - { - function: 'rank', - alias: 'rank', - over: { - orderBy: [{ field: 'score', order: 'desc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with DENSE_RANK window function', () => { - const query: QueryAST = { - object: 'employee', - fields: ['name', 'salary'], - windowFunctions: [ - { - function: 'dense_rank', - alias: 'salary_rank', - over: { - partitionBy: ['department'], - orderBy: [{ field: 'salary', order: 'desc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with PERCENT_RANK window function', () => { - const query: QueryAST = { - object: 'student', - fields: ['name', 'score'], - windowFunctions: [ - { - function: 'percent_rank', - alias: 'percentile', - over: { - orderBy: [{ field: 'score', order: 'desc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // LAG and LEAD Tests - // ============================================================================ - - it('should accept query with LAG window function', () => { - const query: QueryAST = { - object: 'sales', - fields: ['month', 'revenue'], - windowFunctions: [ - { - function: 'lag', - field: 'revenue', - alias: 'prev_month_revenue', - over: { - orderBy: [{ field: 'month', order: 'asc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with LEAD window function', () => { - const query: QueryAST = { - object: 'sales', - fields: ['month', 'revenue'], - windowFunctions: [ - { - function: 'lead', - field: 'revenue', - alias: 'next_month_revenue', - over: { - orderBy: [{ field: 'month', order: 'asc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept LAG and LEAD together', () => { - const query: QueryAST = { - object: 'stock_price', - fields: ['date', 'price'], - windowFunctions: [ - { - function: 'lag', - field: 'price', - alias: 'prev_day_price', - over: { - orderBy: [{ field: 'date', order: 'asc' }], - }, - }, - { - function: 'lead', - field: 'price', - alias: 'next_day_price', - over: { - orderBy: [{ field: 'date', order: 'asc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // FIRST_VALUE and LAST_VALUE Tests - // ============================================================================ - - it('should accept query with FIRST_VALUE window function', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id', 'order_date', 'amount'], - windowFunctions: [ - { - function: 'first_value', - field: 'amount', - alias: 'first_order_amount', - over: { - partitionBy: ['customer_id'], - orderBy: [{ field: 'order_date', order: 'asc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with LAST_VALUE window function', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id', 'order_date', 'amount'], - windowFunctions: [ - { - function: 'last_value', - field: 'amount', - alias: 'last_order_amount', - over: { - partitionBy: ['customer_id'], - orderBy: [{ field: 'order_date', order: 'asc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // Aggregate Window Function Tests - // ============================================================================ - - it('should accept query with SUM aggregate window function', () => { - const query: QueryAST = { - object: 'order', - fields: ['id', 'amount'], - windowFunctions: [ - { - function: 'sum', - field: 'amount', - alias: 'running_total', - over: { - orderBy: [{ field: 'created_at', order: 'asc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with AVG aggregate window function', () => { - const query: QueryAST = { - object: 'sales', - fields: ['month', 'revenue'], - windowFunctions: [ - { - function: 'avg', - field: 'revenue', - alias: 'moving_avg', - over: { - orderBy: [{ field: 'month', order: 'asc' }], - frame: { - type: 'rows', - start: '2 PRECEDING', - end: 'CURRENT ROW', - }, - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with COUNT aggregate window function', () => { - const query: QueryAST = { - object: 'event', - fields: ['timestamp', 'user_id'], - windowFunctions: [ - { - function: 'count', - alias: 'running_count', - over: { - partitionBy: ['user_id'], - orderBy: [{ field: 'timestamp', order: 'asc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with MIN/MAX aggregate window functions', () => { - const query: QueryAST = { - object: 'temperature', - fields: ['date', 'value'], - windowFunctions: [ - { - function: 'min', - field: 'value', - alias: 'min_so_far', - over: { - orderBy: [{ field: 'date', order: 'asc' }], - }, - }, - { - function: 'max', - field: 'value', - alias: 'max_so_far', - over: { - orderBy: [{ field: 'date', order: 'asc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // Window Frame Specification Tests - // ============================================================================ - - it('should accept query with ROWS frame specification', () => { - const query: QueryAST = { - object: 'order', - fields: ['id', 'amount'], - windowFunctions: [ - { - function: 'sum', - field: 'amount', - alias: 'running_total', - over: { - orderBy: [{ field: 'created_at', order: 'asc' }], - frame: { - type: 'rows', - start: 'UNBOUNDED PRECEDING', - end: 'CURRENT ROW', - }, - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with RANGE frame specification', () => { - const query: QueryAST = { - object: 'sales', - fields: ['date', 'amount'], - windowFunctions: [ - { - function: 'sum', - field: 'amount', - alias: 'total_in_range', - over: { - orderBy: [{ field: 'date', order: 'asc' }], - frame: { - type: 'range', - start: '7 PRECEDING', - end: 'CURRENT ROW', - }, - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with window frame FOLLOWING', () => { - const query: QueryAST = { - object: 'sales', - fields: ['month', 'revenue'], - windowFunctions: [ - { - function: 'avg', - field: 'revenue', - alias: 'centered_avg', - over: { - orderBy: [{ field: 'month', order: 'asc' }], - frame: { - type: 'rows', - start: '1 PRECEDING', - end: '1 FOLLOWING', - }, - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // Multiple Window Functions Tests - // ============================================================================ - - it('should accept query with multiple window functions', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id', 'amount', 'created_at'], - windowFunctions: [ - { - function: 'row_number', - alias: 'row_num', - over: { - partitionBy: ['customer_id'], - orderBy: [{ field: 'created_at', order: 'desc' }], - }, - }, - { - function: 'rank', - alias: 'amount_rank', - over: { - partitionBy: ['customer_id'], - orderBy: [{ field: 'amount', order: 'desc' }], - }, - }, - { - function: 'sum', - field: 'amount', - alias: 'running_total', - over: { - partitionBy: ['customer_id'], - orderBy: [{ field: 'created_at', order: 'asc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - // ============================================================================ - // Real-World Window Function Examples - // ============================================================================ - - it('should accept query for top N per group (SQL: ROW_NUMBER() OVER (PARTITION BY ...))', () => { - const query: QueryAST = { - object: 'product', - fields: ['category_id', 'name', 'price'], - windowFunctions: [ - { - function: 'row_number', - alias: 'rank_in_category', - over: { - partitionBy: ['category_id'], - orderBy: [{ field: 'price', order: 'desc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept running total query', () => { - const query: QueryAST = { - object: 'transaction', - fields: ['date', 'amount'], - windowFunctions: [ - { - function: 'sum', - field: 'amount', - alias: 'running_balance', - over: { - orderBy: [{ field: 'date', order: 'asc' }], - frame: { - type: 'rows', - start: 'UNBOUNDED PRECEDING', - end: 'CURRENT ROW', - }, - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept moving average query', () => { - const query: QueryAST = { - object: 'stock_price', - fields: ['date', 'close_price'], - windowFunctions: [ - { - function: 'avg', - field: 'close_price', - alias: 'ma_7_day', - over: { - orderBy: [{ field: 'date', order: 'asc' }], - frame: { - type: 'rows', - start: '6 PRECEDING', - end: 'CURRENT ROW', - }, - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept year-over-year comparison query', () => { - const query: QueryAST = { - object: 'monthly_sales', - fields: ['month', 'revenue'], - windowFunctions: [ - { - function: 'lag', - field: 'revenue', - alias: 'prev_year_revenue', - over: { - orderBy: [{ field: 'month', order: 'asc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept employee ranking within department', () => { - const query: QueryAST = { - object: 'employee', - fields: ['department', 'name', 'salary'], - windowFunctions: [ - { - function: 'rank', - alias: 'salary_rank', - over: { - partitionBy: ['department'], - orderBy: [{ field: 'salary', order: 'desc' }], - }, - }, - { - function: 'percent_rank', - alias: 'salary_percentile', - over: { - partitionBy: ['department'], - orderBy: [{ field: 'salary', order: 'desc' }], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); -}); - -describe('QuerySchema - Complex Queries', () => { - it('should accept complex query with joins, aggregations, and window functions', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - joins: [ - { - type: 'inner', - object: 'customer', - alias: 'c', - on: ['order.customer_id', '=', 'c.id'], - }, - ], - aggregations: [ - { function: 'sum', field: 'amount', alias: 'total_amount' }, - { function: 'count', alias: 'order_count' }, - ], - groupBy: ['customer_id'], - having: { order_count: { $gt: 5 } }, - sort: [{ field: 'total_amount', order: 'desc' }], - top: 100, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should accept query with all features', () => { - const query: QueryAST = { - object: 'order', - fields: ['id', 'customer_id', 'amount'], - distinct: true, - filters: ['status', '=', 'completed'], - joins: [ - { - type: 'inner', - object: 'customer', - on: ['order.customer_id', '=', 'customer.id'], - }, - ], - aggregations: [ - { function: 'avg', field: 'amount', alias: 'avg_amount' }, - ], - windowFunctions: [ - { - function: 'rank', - alias: 'customer_rank', - over: { - partitionBy: ['customer_id'], - orderBy: [{ field: 'amount', order: 'desc' }], - }, - }, - ], - groupBy: ['customer_id'], - having: { avg_amount: { $gt: 500 } }, - sort: [{ field: 'avg_amount', order: 'desc' }], - top: 50, - skip: 0, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); -}); - -describe('QuerySchema - Edge Cases and Null Handling', () => { - it('should handle null values in filter expressions', () => { - const query: QueryAST = { - object: 'account', - fields: ['name'], - filters: ['deleted_at', 'is_null', null], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should handle undefined for optional fields', () => { - const query: QueryAST = { - object: 'account', - fields: undefined, - filters: undefined, - sort: undefined, - aggregations: undefined, - joins: undefined, - groupBy: undefined, - having: undefined, - windowFunctions: undefined, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should handle empty arrays', () => { - const query: QueryAST = { - object: 'account', - fields: [], - aggregations: [], - joins: [], - windowFunctions: [], - groupBy: [], - sort: [], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should handle zero and negative values in pagination', () => { - const queries = [ - { object: 'account', top: 0, skip: 0 }, - { object: 'account', top: 1, skip: 0 }, - { object: 'account', top: 100, skip: 1000 }, - ]; - - queries.forEach(query => { - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - }); - - it('should handle complex nested null filters', () => { - const query: QueryAST = { - object: 'order', - fields: ['id'], - filters: [ - ['approved_at', 'is_null', null], - 'and', - ['rejected_at', 'is_null', null], - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should handle optional alias in field nodes', () => { - const query: QueryAST = { - object: 'account', - fields: [ - 'name', - { field: 'owner', fields: ['name', 'email'] }, - { field: 'manager', fields: ['name'], alias: 'mgr' }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should handle aggregation without field for COUNT', () => { - const query: QueryAST = { - object: 'order', - aggregations: [ - { function: 'count', alias: 'total_count' }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should handle optional distinct flag in aggregation', () => { - const query: QueryAST = { - object: 'order', - aggregations: [ - { function: 'count', field: 'customer_id', alias: 'unique_customers', distinct: true }, - { function: 'sum', field: 'amount', alias: 'total_amount' }, // distinct undefined - ], - groupBy: ['region'], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should handle optional properties in window functions', () => { - const query: QueryAST = { - object: 'sales', - fields: ['amount'], - windowFunctions: [ - { - function: 'row_number', - alias: 'row_num', - over: { - // partitionBy and orderBy are optional - }, - }, - { - function: 'sum', - field: 'amount', - alias: 'total', - over: { - partitionBy: ['region'], - // orderBy is optional - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should handle optional frame in window specification', () => { - const query: QueryAST = { - object: 'transactions', - fields: ['amount'], - windowFunctions: [ - { - function: 'sum', - field: 'amount', - alias: 'running_total', - over: { - orderBy: [{ field: 'date', order: 'asc' }], - frame: { - type: 'rows', - start: 'UNBOUNDED PRECEDING', - end: 'CURRENT ROW', - }, - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should handle optional subquery in joins', () => { - const query: QueryAST = { - object: 'customer', - joins: [ - { - type: 'left', - object: 'order', - on: ['customer.id', '=', 'order.customer_id'], - }, - { - type: 'inner', - object: 'filtered_orders', - alias: 'fo', - on: ['customer.id', '=', 'fo.customer_id'], - subquery: { - object: 'order', - fields: ['customer_id', 'amount'], - filters: ['amount', '>', 1000], - }, - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should reject invalid object type', () => { - expect(() => QuerySchema.parse({ - object: 123, // Should be string - fields: ['name'], - })).toThrow(); - }); - - it('should reject invalid field types in array', () => { - expect(() => QuerySchema.parse({ - object: 'account', - fields: [123, 456], // Should be strings or objects - })).toThrow(); - }); - - it('should reject invalid aggregation function', () => { - expect(() => QuerySchema.parse({ - object: 'order', - aggregations: [ - { function: 'invalid_func', alias: 'test' }, - ], - })).toThrow(); - }); - - it('should reject invalid join type', () => { - expect(() => QuerySchema.parse({ - object: 'order', - joins: [ - { - type: 'invalid_join', - object: 'customer', - on: ['order.customer_id', '=', 'customer.id'], - }, - ], - })).toThrow(); - }); - - it('should reject invalid window function', () => { - expect(() => QuerySchema.parse({ - object: 'sales', - windowFunctions: [ - { - function: 'invalid_window_func', - alias: 'test', - over: {}, - }, - ], - })).toThrow(); - }); - - it('should reject invalid sort order', () => { - expect(() => QuerySchema.parse({ - object: 'account', - sort: [{ field: 'name', order: 'invalid' }], - })).toThrow(); - }); -}); - -describe('QuerySchema - Type Coercion Edge Cases', () => { - it('should handle various data types in filter values', () => { - const queries = [ - { object: 'account', filters: ['age', '>', 18] }, // number - { object: 'account', filters: ['active', '=', true] }, // boolean - { object: 'account', filters: ['name', '=', 'John'] }, // string - { object: 'account', filters: ['tags', 'in', ['a', 'b', 'c']] }, // array - { object: 'account', filters: ['value', 'between', [0, 100]] }, // array - ]; - - queries.forEach(query => { - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - }); - - it('should handle boolean flags', () => { - const query: QueryAST = { - object: 'account', - fields: ['name'], - distinct: true, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - - const query2: QueryAST = { - object: 'account', - fields: ['name'], - distinct: false, - }; - - expect(() => QuerySchema.parse(query2)).not.toThrow(); - }); - - it('should handle default sort order', () => { - const query: QueryAST = { - object: 'account', - sort: [{ field: 'name' }], // order defaults to 'asc' - }; - - const result = QuerySchema.parse(query); - expect(result.sort?.[0].order).toBe('asc'); - }); - - it('should handle mixed field types', () => { - const query: QueryAST = { - object: 'account', - fields: [ - 'simple_field', - { - field: 'related_field', - fields: ['nested_field'], - alias: 'rel', - }, - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should handle deeply nested filters', () => { - const query: QueryAST = { - object: 'order', - filters: [ - [ - ['status', '=', 'active'], - 'and', - ['amount', '>', 100], - ], - 'or', - [ - ['priority', '=', 'high'], - 'and', - ['urgent', '=', true], - ], - ], - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); - - it('should handle complex having clauses', () => { - const query: QueryAST = { - object: 'order', - fields: ['customer_id'], - aggregations: [ - { function: 'count', alias: 'order_count' }, - { function: 'sum', field: 'amount', alias: 'total' }, - ], - groupBy: ['customer_id'], - having: { - $and: [ - { order_count: { $gt: 5 } }, - { total: { $gt: 1000 } }, - ], - }, - }; - - expect(() => QuerySchema.parse(query)).not.toThrow(); - }); -});