Skip to content

Commit 2d3762a

Browse files
Copilothuangyiirene
andcommitted
Add comprehensive edge case tests for query.zod.ts and validation.zod.ts
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
1 parent 413ad4b commit 2d3762a

2 files changed

Lines changed: 793 additions & 0 deletions

File tree

packages/spec/src/data/query.test.ts

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,3 +1617,341 @@ describe('QuerySchema - Complex Queries', () => {
16171617
expect(() => QuerySchema.parse(query)).not.toThrow();
16181618
});
16191619
});
1620+
1621+
describe('QuerySchema - Edge Cases and Null Handling', () => {
1622+
it('should handle null values in filter expressions', () => {
1623+
const query: QueryAST = {
1624+
object: 'account',
1625+
fields: ['name'],
1626+
filters: ['deleted_at', 'is_null', null],
1627+
};
1628+
1629+
expect(() => QuerySchema.parse(query)).not.toThrow();
1630+
});
1631+
1632+
it('should handle undefined for optional fields', () => {
1633+
const query: QueryAST = {
1634+
object: 'account',
1635+
fields: undefined,
1636+
filters: undefined,
1637+
sort: undefined,
1638+
aggregations: undefined,
1639+
joins: undefined,
1640+
groupBy: undefined,
1641+
having: undefined,
1642+
windowFunctions: undefined,
1643+
};
1644+
1645+
expect(() => QuerySchema.parse(query)).not.toThrow();
1646+
});
1647+
1648+
it('should handle empty arrays', () => {
1649+
const query: QueryAST = {
1650+
object: 'account',
1651+
fields: [],
1652+
aggregations: [],
1653+
joins: [],
1654+
windowFunctions: [],
1655+
groupBy: [],
1656+
sort: [],
1657+
};
1658+
1659+
expect(() => QuerySchema.parse(query)).not.toThrow();
1660+
});
1661+
1662+
it('should handle zero and negative values in pagination', () => {
1663+
const queries = [
1664+
{ object: 'account', top: 0, skip: 0 },
1665+
{ object: 'account', top: 1, skip: 0 },
1666+
{ object: 'account', top: 100, skip: 1000 },
1667+
];
1668+
1669+
queries.forEach(query => {
1670+
expect(() => QuerySchema.parse(query)).not.toThrow();
1671+
});
1672+
});
1673+
1674+
it('should handle complex nested null filters', () => {
1675+
const query: QueryAST = {
1676+
object: 'order',
1677+
fields: ['id'],
1678+
filters: [
1679+
['approved_at', 'is_null', null],
1680+
'and',
1681+
['rejected_at', 'is_null', null],
1682+
],
1683+
};
1684+
1685+
expect(() => QuerySchema.parse(query)).not.toThrow();
1686+
});
1687+
1688+
it('should handle optional alias in field nodes', () => {
1689+
const query: QueryAST = {
1690+
object: 'account',
1691+
fields: [
1692+
'name',
1693+
{ field: 'owner', fields: ['name', 'email'] },
1694+
{ field: 'manager', fields: ['name'], alias: 'mgr' },
1695+
],
1696+
};
1697+
1698+
expect(() => QuerySchema.parse(query)).not.toThrow();
1699+
});
1700+
1701+
it('should handle aggregation without field for COUNT', () => {
1702+
const query: QueryAST = {
1703+
object: 'order',
1704+
aggregations: [
1705+
{ function: 'count', alias: 'total_count' },
1706+
],
1707+
};
1708+
1709+
expect(() => QuerySchema.parse(query)).not.toThrow();
1710+
});
1711+
1712+
it('should handle optional distinct flag in aggregation', () => {
1713+
const query: QueryAST = {
1714+
object: 'order',
1715+
aggregations: [
1716+
{ function: 'count', field: 'customer_id', alias: 'unique_customers', distinct: true },
1717+
{ function: 'sum', field: 'amount', alias: 'total_amount' }, // distinct undefined
1718+
],
1719+
groupBy: ['region'],
1720+
};
1721+
1722+
expect(() => QuerySchema.parse(query)).not.toThrow();
1723+
});
1724+
1725+
it('should handle optional properties in window functions', () => {
1726+
const query: QueryAST = {
1727+
object: 'sales',
1728+
fields: ['amount'],
1729+
windowFunctions: [
1730+
{
1731+
function: 'row_number',
1732+
alias: 'row_num',
1733+
over: {
1734+
// partitionBy and orderBy are optional
1735+
},
1736+
},
1737+
{
1738+
function: 'sum',
1739+
field: 'amount',
1740+
alias: 'total',
1741+
over: {
1742+
partitionBy: ['region'],
1743+
// orderBy is optional
1744+
},
1745+
},
1746+
],
1747+
};
1748+
1749+
expect(() => QuerySchema.parse(query)).not.toThrow();
1750+
});
1751+
1752+
it('should handle optional frame in window specification', () => {
1753+
const query: QueryAST = {
1754+
object: 'transactions',
1755+
fields: ['amount'],
1756+
windowFunctions: [
1757+
{
1758+
function: 'sum',
1759+
field: 'amount',
1760+
alias: 'running_total',
1761+
over: {
1762+
orderBy: [{ field: 'date', order: 'asc' }],
1763+
frame: {
1764+
type: 'rows',
1765+
start: 'UNBOUNDED PRECEDING',
1766+
end: 'CURRENT ROW',
1767+
},
1768+
},
1769+
},
1770+
],
1771+
};
1772+
1773+
expect(() => QuerySchema.parse(query)).not.toThrow();
1774+
});
1775+
1776+
it('should handle optional subquery in joins', () => {
1777+
const query: QueryAST = {
1778+
object: 'customer',
1779+
joins: [
1780+
{
1781+
type: 'left',
1782+
object: 'order',
1783+
on: ['customer.id', '=', 'order.customer_id'],
1784+
},
1785+
{
1786+
type: 'inner',
1787+
object: 'filtered_orders',
1788+
alias: 'fo',
1789+
on: ['customer.id', '=', 'fo.customer_id'],
1790+
subquery: {
1791+
object: 'order',
1792+
fields: ['customer_id', 'amount'],
1793+
filters: ['amount', '>', 1000],
1794+
},
1795+
},
1796+
],
1797+
};
1798+
1799+
expect(() => QuerySchema.parse(query)).not.toThrow();
1800+
});
1801+
1802+
it('should reject invalid object type', () => {
1803+
expect(() => QuerySchema.parse({
1804+
object: 123, // Should be string
1805+
fields: ['name'],
1806+
})).toThrow();
1807+
});
1808+
1809+
it('should reject invalid field types in array', () => {
1810+
expect(() => QuerySchema.parse({
1811+
object: 'account',
1812+
fields: [123, 456], // Should be strings or objects
1813+
})).toThrow();
1814+
});
1815+
1816+
it('should reject invalid aggregation function', () => {
1817+
expect(() => QuerySchema.parse({
1818+
object: 'order',
1819+
aggregations: [
1820+
{ function: 'invalid_func', alias: 'test' },
1821+
],
1822+
})).toThrow();
1823+
});
1824+
1825+
it('should reject invalid join type', () => {
1826+
expect(() => QuerySchema.parse({
1827+
object: 'order',
1828+
joins: [
1829+
{
1830+
type: 'invalid_join',
1831+
object: 'customer',
1832+
on: ['order.customer_id', '=', 'customer.id'],
1833+
},
1834+
],
1835+
})).toThrow();
1836+
});
1837+
1838+
it('should reject invalid window function', () => {
1839+
expect(() => QuerySchema.parse({
1840+
object: 'sales',
1841+
windowFunctions: [
1842+
{
1843+
function: 'invalid_window_func',
1844+
alias: 'test',
1845+
over: {},
1846+
},
1847+
],
1848+
})).toThrow();
1849+
});
1850+
1851+
it('should reject invalid sort order', () => {
1852+
expect(() => QuerySchema.parse({
1853+
object: 'account',
1854+
sort: [{ field: 'name', order: 'invalid' }],
1855+
})).toThrow();
1856+
});
1857+
});
1858+
1859+
describe('QuerySchema - Type Coercion Edge Cases', () => {
1860+
it('should handle various data types in filter values', () => {
1861+
const queries = [
1862+
{ object: 'account', filters: ['age', '>', 18] }, // number
1863+
{ object: 'account', filters: ['active', '=', true] }, // boolean
1864+
{ object: 'account', filters: ['name', '=', 'John'] }, // string
1865+
{ object: 'account', filters: ['tags', 'in', ['a', 'b', 'c']] }, // array
1866+
{ object: 'account', filters: ['value', 'between', [0, 100]] }, // array
1867+
];
1868+
1869+
queries.forEach(query => {
1870+
expect(() => QuerySchema.parse(query)).not.toThrow();
1871+
});
1872+
});
1873+
1874+
it('should handle boolean flags', () => {
1875+
const query: QueryAST = {
1876+
object: 'account',
1877+
fields: ['name'],
1878+
distinct: true,
1879+
};
1880+
1881+
expect(() => QuerySchema.parse(query)).not.toThrow();
1882+
1883+
const query2: QueryAST = {
1884+
object: 'account',
1885+
fields: ['name'],
1886+
distinct: false,
1887+
};
1888+
1889+
expect(() => QuerySchema.parse(query2)).not.toThrow();
1890+
});
1891+
1892+
it('should handle default sort order', () => {
1893+
const query: QueryAST = {
1894+
object: 'account',
1895+
sort: [{ field: 'name' }], // order defaults to 'asc'
1896+
};
1897+
1898+
const result = QuerySchema.parse(query);
1899+
expect(result.sort?.[0].order).toBe('asc');
1900+
});
1901+
1902+
it('should handle mixed field types', () => {
1903+
const query: QueryAST = {
1904+
object: 'account',
1905+
fields: [
1906+
'simple_field',
1907+
{
1908+
field: 'related_field',
1909+
fields: ['nested_field'],
1910+
alias: 'rel',
1911+
},
1912+
],
1913+
};
1914+
1915+
expect(() => QuerySchema.parse(query)).not.toThrow();
1916+
});
1917+
1918+
it('should handle deeply nested filters', () => {
1919+
const query: QueryAST = {
1920+
object: 'order',
1921+
filters: [
1922+
[
1923+
['status', '=', 'active'],
1924+
'and',
1925+
['amount', '>', 100],
1926+
],
1927+
'or',
1928+
[
1929+
['priority', '=', 'high'],
1930+
'and',
1931+
['urgent', '=', true],
1932+
],
1933+
],
1934+
};
1935+
1936+
expect(() => QuerySchema.parse(query)).not.toThrow();
1937+
});
1938+
1939+
it('should handle complex having clauses', () => {
1940+
const query: QueryAST = {
1941+
object: 'order',
1942+
fields: ['customer_id'],
1943+
aggregations: [
1944+
{ function: 'count', alias: 'order_count' },
1945+
{ function: 'sum', field: 'amount', alias: 'total' },
1946+
],
1947+
groupBy: ['customer_id'],
1948+
having: [
1949+
['order_count', '>', 5],
1950+
'and',
1951+
['total', '>', 1000],
1952+
],
1953+
};
1954+
1955+
expect(() => QuerySchema.parse(query)).not.toThrow();
1956+
});
1957+
});

0 commit comments

Comments
 (0)