Skip to content

Commit 380cf7a

Browse files
Copilothotlong
andcommitted
Add comprehensive tests for window functions and object validation engine
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent ecc82db commit 380cf7a

2 files changed

Lines changed: 842 additions & 0 deletions

File tree

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* @object-ui/core - Window Function Tests
11+
*
12+
* Tests for ObjectStack Spec v0.7.1 window function support
13+
*/
14+
15+
import { describe, it, expect } from 'vitest';
16+
import { QueryASTBuilder } from '../query-ast';
17+
import type { WindowNode, WindowFunction } from '@object-ui/types';
18+
19+
describe('QueryASTBuilder - Window Functions', () => {
20+
const builder = new QueryASTBuilder();
21+
22+
describe('buildWindow', () => {
23+
it('should build a simple row_number window function', () => {
24+
const config = {
25+
function: 'row_number' as WindowFunction,
26+
alias: 'row_num',
27+
partitionBy: ['department'],
28+
orderBy: [{ field: 'salary', direction: 'desc' as const }],
29+
};
30+
31+
// @ts-ignore - testing private method
32+
const result: WindowNode = builder.buildWindow(config);
33+
34+
expect(result).toMatchObject({
35+
type: 'window',
36+
function: 'row_number',
37+
alias: 'row_num',
38+
});
39+
40+
expect(result.partitionBy).toHaveLength(1);
41+
expect(result.partitionBy![0]).toMatchObject({
42+
type: 'field',
43+
name: 'department',
44+
});
45+
46+
expect(result.orderBy).toHaveLength(1);
47+
expect(result.orderBy![0]).toMatchObject({
48+
field: { type: 'field', name: 'salary' },
49+
direction: 'desc',
50+
});
51+
});
52+
53+
it('should build a rank window function with multiple partition fields', () => {
54+
const config = {
55+
function: 'rank' as WindowFunction,
56+
alias: 'rank_val',
57+
partitionBy: ['department', 'location'],
58+
orderBy: [
59+
{ field: 'performance_score', direction: 'desc' as const },
60+
{ field: 'tenure_years', direction: 'desc' as const },
61+
],
62+
};
63+
64+
// @ts-ignore
65+
const result: WindowNode = builder.buildWindow(config);
66+
67+
expect(result).toMatchObject({
68+
type: 'window',
69+
function: 'rank',
70+
alias: 'rank_val',
71+
});
72+
73+
expect(result.partitionBy).toHaveLength(2);
74+
expect(result.orderBy).toHaveLength(2);
75+
});
76+
77+
it('should build a lag window function with offset and default value', () => {
78+
const config = {
79+
function: 'lag' as WindowFunction,
80+
field: 'revenue',
81+
alias: 'prev_month_revenue',
82+
partitionBy: ['product_id'],
83+
orderBy: [{ field: 'month', direction: 'asc' as const }],
84+
offset: 1,
85+
defaultValue: 0,
86+
};
87+
88+
// @ts-ignore
89+
const result: WindowNode = builder.buildWindow(config);
90+
91+
expect(result).toMatchObject({
92+
type: 'window',
93+
function: 'lag',
94+
alias: 'prev_month_revenue',
95+
offset: 1,
96+
});
97+
98+
expect(result.field).toMatchObject({
99+
type: 'field',
100+
name: 'revenue',
101+
});
102+
103+
expect(result.defaultValue).toMatchObject({
104+
type: 'literal',
105+
value: 0,
106+
data_type: 'number',
107+
});
108+
});
109+
110+
it('should build a lead window function', () => {
111+
const config = {
112+
function: 'lead' as WindowFunction,
113+
field: 'sales',
114+
alias: 'next_day_sales',
115+
orderBy: [{ field: 'date', direction: 'asc' as const }],
116+
offset: 1,
117+
};
118+
119+
// @ts-ignore
120+
const result: WindowNode = builder.buildWindow(config);
121+
122+
expect(result).toMatchObject({
123+
type: 'window',
124+
function: 'lead',
125+
alias: 'next_day_sales',
126+
offset: 1,
127+
});
128+
});
129+
130+
it('should build aggregate window functions (sum, avg, count)', () => {
131+
const sumConfig = {
132+
function: 'sum' as WindowFunction,
133+
field: 'amount',
134+
alias: 'running_total',
135+
orderBy: [{ field: 'date', direction: 'asc' as const }],
136+
frame: {
137+
unit: 'rows' as const,
138+
start: 'unbounded_preceding' as const,
139+
end: 'current_row' as const,
140+
},
141+
};
142+
143+
// @ts-ignore
144+
const result: WindowNode = builder.buildWindow(sumConfig);
145+
146+
expect(result).toMatchObject({
147+
type: 'window',
148+
function: 'sum',
149+
alias: 'running_total',
150+
});
151+
152+
expect(result.field).toMatchObject({
153+
type: 'field',
154+
name: 'amount',
155+
});
156+
157+
expect(result.frame).toEqual({
158+
unit: 'rows',
159+
start: 'unbounded_preceding',
160+
end: 'current_row',
161+
});
162+
});
163+
164+
it('should build first_value window function', () => {
165+
const config = {
166+
function: 'first_value' as WindowFunction,
167+
field: 'price',
168+
alias: 'first_price',
169+
partitionBy: ['product_category'],
170+
orderBy: [{ field: 'created_at', direction: 'asc' as const }],
171+
};
172+
173+
// @ts-ignore
174+
const result: WindowNode = builder.buildWindow(config);
175+
176+
expect(result).toMatchObject({
177+
type: 'window',
178+
function: 'first_value',
179+
alias: 'first_price',
180+
});
181+
});
182+
183+
it('should build last_value window function', () => {
184+
const config = {
185+
function: 'last_value' as WindowFunction,
186+
field: 'status',
187+
alias: 'latest_status',
188+
partitionBy: ['customer_id'],
189+
orderBy: [{ field: 'updated_at', direction: 'desc' as const }],
190+
};
191+
192+
// @ts-ignore
193+
const result: WindowNode = builder.buildWindow(config);
194+
195+
expect(result).toMatchObject({
196+
type: 'window',
197+
function: 'last_value',
198+
alias: 'latest_status',
199+
});
200+
});
201+
202+
it('should handle window function without partition by', () => {
203+
const config = {
204+
function: 'row_number' as WindowFunction,
205+
alias: 'global_row_num',
206+
orderBy: [{ field: 'created_at', direction: 'asc' as const }],
207+
};
208+
209+
// @ts-ignore
210+
const result: WindowNode = builder.buildWindow(config);
211+
212+
expect(result.partitionBy).toBeUndefined();
213+
expect(result.orderBy).toBeDefined();
214+
});
215+
216+
it('should handle window function with frame specification', () => {
217+
const config = {
218+
function: 'avg' as WindowFunction,
219+
field: 'temperature',
220+
alias: 'moving_avg_3days',
221+
orderBy: [{ field: 'date', direction: 'asc' as const }],
222+
frame: {
223+
unit: 'rows' as const,
224+
start: { type: 'preceding' as const, offset: 2 },
225+
end: 'current_row' as const,
226+
},
227+
};
228+
229+
// @ts-ignore
230+
const result: WindowNode = builder.buildWindow(config);
231+
232+
expect(result.frame).toEqual({
233+
unit: 'rows',
234+
start: { type: 'preceding', offset: 2 },
235+
end: 'current_row',
236+
});
237+
});
238+
239+
it('should build dense_rank window function', () => {
240+
const config = {
241+
function: 'dense_rank' as WindowFunction,
242+
alias: 'dense_rank_val',
243+
partitionBy: ['team'],
244+
orderBy: [{ field: 'score', direction: 'desc' as const }],
245+
};
246+
247+
// @ts-ignore
248+
const result: WindowNode = builder.buildWindow(config);
249+
250+
expect(result).toMatchObject({
251+
type: 'window',
252+
function: 'dense_rank',
253+
alias: 'dense_rank_val',
254+
});
255+
});
256+
257+
it('should build percent_rank window function', () => {
258+
const config = {
259+
function: 'percent_rank' as WindowFunction,
260+
alias: 'percentile',
261+
partitionBy: ['class'],
262+
orderBy: [{ field: 'exam_score', direction: 'desc' as const }],
263+
};
264+
265+
// @ts-ignore
266+
const result: WindowNode = builder.buildWindow(config);
267+
268+
expect(result).toMatchObject({
269+
type: 'window',
270+
function: 'percent_rank',
271+
alias: 'percentile',
272+
});
273+
});
274+
});
275+
});

0 commit comments

Comments
 (0)