754 lines
24 KiB
TypeScript
754 lines
24 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
|
|
import { QueryParser } from './query-parser.js';
|
|
import type { QueryConditionNumber, QueryConditionText, QueryFilter, QueryOperator } from './query-parser.schemas.js';
|
|
|
|
describe('QueryParser', () => {
|
|
const parser = new QueryParser();
|
|
|
|
describe('parse', () => {
|
|
describe('text conditions', () => {
|
|
it('should parse simple text equality', () => {
|
|
const result = parser.parse("name = 'John'");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['name'],
|
|
conditions: { equal: 'John' },
|
|
});
|
|
});
|
|
|
|
it('should parse nested field text equality', () => {
|
|
const result = parser.parse("metadata.author = 'John'");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['metadata', 'author'],
|
|
conditions: { equal: 'John' },
|
|
});
|
|
});
|
|
|
|
it('should parse deeply nested field', () => {
|
|
const result = parser.parse("metadata.nested.deep.field = 'value'");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['metadata', 'nested', 'deep', 'field'],
|
|
conditions: { equal: 'value' },
|
|
});
|
|
});
|
|
|
|
it('should parse text not equal', () => {
|
|
const result = parser.parse("type != 'draft'");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['type'],
|
|
conditions: { notEqual: 'draft' },
|
|
});
|
|
});
|
|
|
|
it('should parse LIKE pattern', () => {
|
|
const result = parser.parse("title LIKE '%cat%'");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['title'],
|
|
conditions: { like: '%cat%' },
|
|
});
|
|
});
|
|
|
|
it('should parse NOT LIKE pattern', () => {
|
|
const result = parser.parse("author NOT LIKE '%admin%'");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['author'],
|
|
conditions: { notLike: '%admin%' },
|
|
});
|
|
});
|
|
|
|
it('should parse text IN list', () => {
|
|
const result = parser.parse("status IN ('published', 'archived', 'draft')");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['status'],
|
|
conditions: { in: ['published', 'archived', 'draft'] },
|
|
});
|
|
});
|
|
|
|
it('should parse text NOT IN list', () => {
|
|
const result = parser.parse("category NOT IN ('deleted', 'hidden')");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['category'],
|
|
conditions: { notIn: ['deleted', 'hidden'] },
|
|
});
|
|
});
|
|
|
|
it('should parse IS NULL', () => {
|
|
const result = parser.parse('source IS NULL');
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['source'],
|
|
conditions: { equal: null },
|
|
});
|
|
});
|
|
|
|
it('should handle escaped quotes in strings', () => {
|
|
const result = parser.parse("name = 'O''Brien'");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['name'],
|
|
conditions: { equal: "O'Brien" },
|
|
});
|
|
});
|
|
|
|
it('should handle empty string', () => {
|
|
const result = parser.parse("name = ''");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['name'],
|
|
conditions: { equal: '' },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('numeric conditions', () => {
|
|
it('should parse numeric equality', () => {
|
|
const result = parser.parse('age = 30');
|
|
expect(result).toEqual({
|
|
type: 'number',
|
|
field: ['age'],
|
|
conditions: { equals: 30 },
|
|
});
|
|
});
|
|
|
|
it('should parse numeric not equal', () => {
|
|
const result = parser.parse('count != 0');
|
|
expect(result).toEqual({
|
|
type: 'number',
|
|
field: ['count'],
|
|
conditions: { notEquals: 0 },
|
|
});
|
|
});
|
|
|
|
it('should parse greater than', () => {
|
|
const result = parser.parse('views > 100');
|
|
expect(result).toEqual({
|
|
type: 'number',
|
|
field: ['views'],
|
|
conditions: { greaterThan: 100 },
|
|
});
|
|
});
|
|
|
|
it('should parse greater than or equal', () => {
|
|
const result = parser.parse('views >= 100');
|
|
expect(result).toEqual({
|
|
type: 'number',
|
|
field: ['views'],
|
|
conditions: { greaterThanOrEqual: 100 },
|
|
});
|
|
});
|
|
|
|
it('should parse less than', () => {
|
|
const result = parser.parse('priority < 5');
|
|
expect(result).toEqual({
|
|
type: 'number',
|
|
field: ['priority'],
|
|
conditions: { lessThan: 5 },
|
|
});
|
|
});
|
|
|
|
it('should parse less than or equal', () => {
|
|
const result = parser.parse('age <= 65');
|
|
expect(result).toEqual({
|
|
type: 'number',
|
|
field: ['age'],
|
|
conditions: { lessThanOrEqual: 65 },
|
|
});
|
|
});
|
|
|
|
it('should parse decimal numbers', () => {
|
|
const result = parser.parse('score > 0.5');
|
|
expect(result).toEqual({
|
|
type: 'number',
|
|
field: ['score'],
|
|
conditions: { greaterThan: 0.5 },
|
|
});
|
|
});
|
|
|
|
it('should parse negative numbers', () => {
|
|
const result = parser.parse('temperature > -10');
|
|
expect(result).toEqual({
|
|
type: 'number',
|
|
field: ['temperature'],
|
|
conditions: { greaterThan: -10 },
|
|
});
|
|
});
|
|
|
|
it('should parse numeric IN list', () => {
|
|
const result = parser.parse('priority IN (1, 2, 3)');
|
|
expect(result).toEqual({
|
|
type: 'number',
|
|
field: ['priority'],
|
|
conditions: { in: [1, 2, 3] },
|
|
});
|
|
});
|
|
|
|
it('should parse numeric NOT IN list', () => {
|
|
const result = parser.parse('count NOT IN (0, -1)');
|
|
expect(result).toEqual({
|
|
type: 'number',
|
|
field: ['count'],
|
|
conditions: { notIn: [0, -1] },
|
|
});
|
|
});
|
|
|
|
it('should parse nested field numeric condition', () => {
|
|
const result = parser.parse('metadata.score >= 0.8');
|
|
expect(result).toEqual({
|
|
type: 'number',
|
|
field: ['metadata', 'score'],
|
|
conditions: { greaterThanOrEqual: 0.8 },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('logical operators', () => {
|
|
it('should parse AND operator', () => {
|
|
const result = parser.parse("type = 'article' AND status = 'published'");
|
|
expect(result).toEqual({
|
|
type: 'operator',
|
|
operator: 'and',
|
|
conditions: [
|
|
{ type: 'text', field: ['type'], conditions: { equal: 'article' } },
|
|
{ type: 'text', field: ['status'], conditions: { equal: 'published' } },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should parse OR operator', () => {
|
|
const result = parser.parse("category = 'tech' OR category = 'science'");
|
|
expect(result).toEqual({
|
|
type: 'operator',
|
|
operator: 'or',
|
|
conditions: [
|
|
{ type: 'text', field: ['category'], conditions: { equal: 'tech' } },
|
|
{ type: 'text', field: ['category'], conditions: { equal: 'science' } },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should parse multiple AND conditions', () => {
|
|
const result = parser.parse("type = 'article' AND status = 'published' AND views > 100");
|
|
expect(result).toEqual({
|
|
type: 'operator',
|
|
operator: 'and',
|
|
conditions: [
|
|
{ type: 'text', field: ['type'], conditions: { equal: 'article' } },
|
|
{ type: 'text', field: ['status'], conditions: { equal: 'published' } },
|
|
{ type: 'number', field: ['views'], conditions: { greaterThan: 100 } },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should parse multiple OR conditions', () => {
|
|
const result = parser.parse("type = 'a' OR type = 'b' OR type = 'c'");
|
|
expect(result).toEqual({
|
|
type: 'operator',
|
|
operator: 'or',
|
|
conditions: [
|
|
{ type: 'text', field: ['type'], conditions: { equal: 'a' } },
|
|
{ type: 'text', field: ['type'], conditions: { equal: 'b' } },
|
|
{ type: 'text', field: ['type'], conditions: { equal: 'c' } },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should respect AND precedence over OR', () => {
|
|
// A AND B OR C should be parsed as (A AND B) OR C
|
|
const result = parser.parse("a = '1' AND b = '2' OR c = '3'");
|
|
expect(result).toEqual({
|
|
type: 'operator',
|
|
operator: 'or',
|
|
conditions: [
|
|
{
|
|
type: 'operator',
|
|
operator: 'and',
|
|
conditions: [
|
|
{ type: 'text', field: ['a'], conditions: { equal: '1' } },
|
|
{ type: 'text', field: ['b'], conditions: { equal: '2' } },
|
|
],
|
|
},
|
|
{ type: 'text', field: ['c'], conditions: { equal: '3' } },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should parse parenthesized expressions', () => {
|
|
const result = parser.parse("(type = 'post' OR type = 'page') AND views > 100");
|
|
expect(result).toEqual({
|
|
type: 'operator',
|
|
operator: 'and',
|
|
conditions: [
|
|
{
|
|
type: 'operator',
|
|
operator: 'or',
|
|
conditions: [
|
|
{ type: 'text', field: ['type'], conditions: { equal: 'post' } },
|
|
{ type: 'text', field: ['type'], conditions: { equal: 'page' } },
|
|
],
|
|
},
|
|
{ type: 'number', field: ['views'], conditions: { greaterThan: 100 } },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should parse nested parentheses', () => {
|
|
const result = parser.parse(
|
|
"((status = 'active' AND views > 100) OR (status = 'featured' AND views > 50)) AND category = 'news'",
|
|
);
|
|
expect(result).toEqual({
|
|
type: 'operator',
|
|
operator: 'and',
|
|
conditions: [
|
|
{
|
|
type: 'operator',
|
|
operator: 'or',
|
|
conditions: [
|
|
{
|
|
type: 'operator',
|
|
operator: 'and',
|
|
conditions: [
|
|
{ type: 'text', field: ['status'], conditions: { equal: 'active' } },
|
|
{ type: 'number', field: ['views'], conditions: { greaterThan: 100 } },
|
|
],
|
|
},
|
|
{
|
|
type: 'operator',
|
|
operator: 'and',
|
|
conditions: [
|
|
{ type: 'text', field: ['status'], conditions: { equal: 'featured' } },
|
|
{ type: 'number', field: ['views'], conditions: { greaterThan: 50 } },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{ type: 'text', field: ['category'], conditions: { equal: 'news' } },
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('case insensitivity', () => {
|
|
it('should parse lowercase AND', () => {
|
|
const result = parser.parse("a = '1' and b = '2'");
|
|
expect(result.type).toBe('operator');
|
|
expect((result as QueryOperator).operator).toBe('and');
|
|
});
|
|
|
|
it('should parse lowercase OR', () => {
|
|
const result = parser.parse("a = '1' or b = '2'");
|
|
expect(result.type).toBe('operator');
|
|
expect((result as QueryOperator).operator).toBe('or');
|
|
});
|
|
|
|
it('should parse mixed case LIKE', () => {
|
|
const result = parser.parse("title Like '%test%'");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['title'],
|
|
conditions: { like: '%test%' },
|
|
});
|
|
});
|
|
|
|
it('should parse mixed case IS NULL', () => {
|
|
const result = parser.parse('field Is Null');
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['field'],
|
|
conditions: { equal: null },
|
|
});
|
|
});
|
|
|
|
it('should parse mixed case IN', () => {
|
|
const result = parser.parse("status In ('a', 'b')");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['status'],
|
|
conditions: { in: ['a', 'b'] },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('whitespace handling', () => {
|
|
it('should handle extra whitespace', () => {
|
|
const result = parser.parse(" name = 'John' ");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['name'],
|
|
conditions: { equal: 'John' },
|
|
});
|
|
});
|
|
|
|
it('should handle no whitespace around operators', () => {
|
|
const result = parser.parse("name='John'");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['name'],
|
|
conditions: { equal: 'John' },
|
|
});
|
|
});
|
|
|
|
it('should handle tabs and newlines', () => {
|
|
const result = parser.parse("name\t=\n'John'");
|
|
expect(result).toEqual({
|
|
type: 'text',
|
|
field: ['name'],
|
|
conditions: { equal: 'John' },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should throw on invalid syntax', () => {
|
|
expect(() => parser.parse('invalid')).toThrow();
|
|
});
|
|
|
|
it('should throw on mismatched parentheses', () => {
|
|
expect(() => parser.parse("(type = 'a'")).toThrow();
|
|
});
|
|
|
|
it('should throw on unterminated string', () => {
|
|
expect(() => parser.parse("name = 'unterminated")).toThrow(/Unterminated string/);
|
|
});
|
|
|
|
it('should throw on unexpected token', () => {
|
|
expect(() => parser.parse("name = 'a' INVALID")).toThrow();
|
|
});
|
|
|
|
it('should throw on missing value after operator', () => {
|
|
expect(() => parser.parse('name =')).toThrow();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('stringify', () => {
|
|
describe('text conditions', () => {
|
|
it('should stringify text equality', () => {
|
|
const filter: QueryConditionText = {
|
|
type: 'text',
|
|
field: ['name'],
|
|
conditions: { equal: 'John' },
|
|
};
|
|
expect(parser.stringify(filter)).toBe("name = 'John'");
|
|
});
|
|
|
|
it('should stringify nested field', () => {
|
|
const filter: QueryConditionText = {
|
|
type: 'text',
|
|
field: ['metadata', 'author'],
|
|
conditions: { equal: 'John' },
|
|
};
|
|
expect(parser.stringify(filter)).toBe("metadata.author = 'John'");
|
|
});
|
|
|
|
it('should stringify text not equal', () => {
|
|
const filter: QueryConditionText = {
|
|
type: 'text',
|
|
field: ['type'],
|
|
conditions: { notEqual: 'draft' },
|
|
};
|
|
expect(parser.stringify(filter)).toBe("type != 'draft'");
|
|
});
|
|
|
|
it('should stringify LIKE', () => {
|
|
const filter: QueryConditionText = {
|
|
type: 'text',
|
|
field: ['title'],
|
|
conditions: { like: '%cat%' },
|
|
};
|
|
expect(parser.stringify(filter)).toBe("title LIKE '%cat%'");
|
|
});
|
|
|
|
it('should stringify NOT LIKE', () => {
|
|
const filter: QueryConditionText = {
|
|
type: 'text',
|
|
field: ['author'],
|
|
conditions: { notLike: '%admin%' },
|
|
};
|
|
expect(parser.stringify(filter)).toBe("author NOT LIKE '%admin%'");
|
|
});
|
|
|
|
it('should stringify text IN', () => {
|
|
const filter: QueryConditionText = {
|
|
type: 'text',
|
|
field: ['status'],
|
|
conditions: { in: ['published', 'archived'] },
|
|
};
|
|
expect(parser.stringify(filter)).toBe("status IN ('published', 'archived')");
|
|
});
|
|
|
|
it('should stringify text NOT IN', () => {
|
|
const filter: QueryConditionText = {
|
|
type: 'text',
|
|
field: ['category'],
|
|
conditions: { notIn: ['deleted', 'hidden'] },
|
|
};
|
|
expect(parser.stringify(filter)).toBe("category NOT IN ('deleted', 'hidden')");
|
|
});
|
|
|
|
it('should stringify IS NULL', () => {
|
|
const filter: QueryConditionText = {
|
|
type: 'text',
|
|
field: ['source'],
|
|
conditions: { equal: null },
|
|
};
|
|
expect(parser.stringify(filter)).toBe('source IS NULL');
|
|
});
|
|
|
|
it('should escape quotes in strings', () => {
|
|
const filter: QueryConditionText = {
|
|
type: 'text',
|
|
field: ['name'],
|
|
conditions: { equal: "O'Brien" },
|
|
};
|
|
expect(parser.stringify(filter)).toBe("name = 'O''Brien'");
|
|
});
|
|
});
|
|
|
|
describe('numeric conditions', () => {
|
|
it('should stringify numeric equality', () => {
|
|
const filter: QueryConditionNumber = {
|
|
type: 'number',
|
|
field: ['age'],
|
|
conditions: { equals: 30 },
|
|
};
|
|
expect(parser.stringify(filter)).toBe('age = 30');
|
|
});
|
|
|
|
it('should stringify numeric not equal', () => {
|
|
const filter: QueryConditionNumber = {
|
|
type: 'number',
|
|
field: ['count'],
|
|
conditions: { notEquals: 0 },
|
|
};
|
|
expect(parser.stringify(filter)).toBe('count != 0');
|
|
});
|
|
|
|
it('should stringify greater than', () => {
|
|
const filter: QueryConditionNumber = {
|
|
type: 'number',
|
|
field: ['views'],
|
|
conditions: { greaterThan: 100 },
|
|
};
|
|
expect(parser.stringify(filter)).toBe('views > 100');
|
|
});
|
|
|
|
it('should stringify greater than or equal', () => {
|
|
const filter: QueryConditionNumber = {
|
|
type: 'number',
|
|
field: ['views'],
|
|
conditions: { greaterThanOrEqual: 100 },
|
|
};
|
|
expect(parser.stringify(filter)).toBe('views >= 100');
|
|
});
|
|
|
|
it('should stringify less than', () => {
|
|
const filter: QueryConditionNumber = {
|
|
type: 'number',
|
|
field: ['priority'],
|
|
conditions: { lessThan: 5 },
|
|
};
|
|
expect(parser.stringify(filter)).toBe('priority < 5');
|
|
});
|
|
|
|
it('should stringify less than or equal', () => {
|
|
const filter: QueryConditionNumber = {
|
|
type: 'number',
|
|
field: ['age'],
|
|
conditions: { lessThanOrEqual: 65 },
|
|
};
|
|
expect(parser.stringify(filter)).toBe('age <= 65');
|
|
});
|
|
|
|
it('should stringify decimal numbers', () => {
|
|
const filter: QueryConditionNumber = {
|
|
type: 'number',
|
|
field: ['score'],
|
|
conditions: { greaterThan: 0.5 },
|
|
};
|
|
expect(parser.stringify(filter)).toBe('score > 0.5');
|
|
});
|
|
|
|
it('should stringify numeric IN', () => {
|
|
const filter: QueryConditionNumber = {
|
|
type: 'number',
|
|
field: ['priority'],
|
|
conditions: { in: [1, 2, 3] },
|
|
};
|
|
expect(parser.stringify(filter)).toBe('priority IN (1, 2, 3)');
|
|
});
|
|
|
|
it('should stringify numeric NOT IN', () => {
|
|
const filter: QueryConditionNumber = {
|
|
type: 'number',
|
|
field: ['count'],
|
|
conditions: { notIn: [0, -1] },
|
|
};
|
|
expect(parser.stringify(filter)).toBe('count NOT IN (0, -1)');
|
|
});
|
|
|
|
it('should stringify numeric IS NULL', () => {
|
|
const filter: QueryConditionNumber = {
|
|
type: 'number',
|
|
field: ['score'],
|
|
conditions: { equals: null },
|
|
};
|
|
expect(parser.stringify(filter)).toBe('score IS NULL');
|
|
});
|
|
|
|
it('should stringify numeric IS NOT NULL', () => {
|
|
const filter: QueryConditionNumber = {
|
|
type: 'number',
|
|
field: ['score'],
|
|
conditions: { notEquals: null },
|
|
};
|
|
expect(parser.stringify(filter)).toBe('score IS NOT NULL');
|
|
});
|
|
});
|
|
|
|
describe('logical operators', () => {
|
|
it('should stringify AND operator', () => {
|
|
const filter: QueryFilter = {
|
|
type: 'operator',
|
|
operator: 'and',
|
|
conditions: [
|
|
{ type: 'text', field: ['type'], conditions: { equal: 'article' } },
|
|
{ type: 'text', field: ['status'], conditions: { equal: 'published' } },
|
|
],
|
|
};
|
|
expect(parser.stringify(filter)).toBe("type = 'article' AND status = 'published'");
|
|
});
|
|
|
|
it('should stringify OR operator', () => {
|
|
const filter: QueryFilter = {
|
|
type: 'operator',
|
|
operator: 'or',
|
|
conditions: [
|
|
{ type: 'text', field: ['category'], conditions: { equal: 'tech' } },
|
|
{ type: 'text', field: ['category'], conditions: { equal: 'science' } },
|
|
],
|
|
};
|
|
expect(parser.stringify(filter)).toBe("category = 'tech' OR category = 'science'");
|
|
});
|
|
|
|
it('should stringify nested operators with parentheses', () => {
|
|
const filter: QueryFilter = {
|
|
type: 'operator',
|
|
operator: 'and',
|
|
conditions: [
|
|
{
|
|
type: 'operator',
|
|
operator: 'or',
|
|
conditions: [
|
|
{ type: 'text', field: ['type'], conditions: { equal: 'post' } },
|
|
{ type: 'text', field: ['type'], conditions: { equal: 'page' } },
|
|
],
|
|
},
|
|
{ type: 'number', field: ['views'], conditions: { greaterThan: 100 } },
|
|
],
|
|
};
|
|
expect(parser.stringify(filter)).toBe("(type = 'post' OR type = 'page') AND views > 100");
|
|
});
|
|
|
|
it('should stringify empty operator', () => {
|
|
const filter: QueryFilter = {
|
|
type: 'operator',
|
|
operator: 'and',
|
|
conditions: [],
|
|
};
|
|
expect(parser.stringify(filter)).toBe('');
|
|
});
|
|
|
|
it('should stringify single-condition operator', () => {
|
|
const filter: QueryFilter = {
|
|
type: 'operator',
|
|
operator: 'and',
|
|
conditions: [{ type: 'text', field: ['name'], conditions: { equal: 'test' } }],
|
|
};
|
|
expect(parser.stringify(filter)).toBe("name = 'test'");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('roundtrip', () => {
|
|
const testCases = [
|
|
"name = 'John'",
|
|
"metadata.author = 'Jane'",
|
|
'views > 100',
|
|
'score >= 0.5',
|
|
"title LIKE '%cat%'",
|
|
"author NOT LIKE '%admin%'",
|
|
"status IN ('published', 'archived')",
|
|
'priority IN (1, 2, 3)',
|
|
"type = 'article' AND status = 'published'",
|
|
"category = 'tech' OR category = 'science'",
|
|
"(type = 'post' OR type = 'page') AND views > 100",
|
|
];
|
|
|
|
testCases.forEach((query) => {
|
|
it(`should roundtrip: ${query}`, () => {
|
|
const parsed = parser.parse(query);
|
|
const stringified = parser.stringify(parsed);
|
|
const reparsed = parser.parse(stringified);
|
|
expect(reparsed).toEqual(parsed);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('complex real-world queries', () => {
|
|
it('should handle complex query with multiple field types', () => {
|
|
const query = "type = 'article' AND (metadata.author = 'John' OR metadata.author = 'Jane') AND views >= 100";
|
|
const result = parser.parse(query);
|
|
|
|
expect(result.type).toBe('operator');
|
|
const operator = result as QueryOperator;
|
|
expect(operator.operator).toBe('and');
|
|
expect(operator.conditions).toHaveLength(3);
|
|
});
|
|
|
|
it('should handle nested JSON paths with conditions', () => {
|
|
const query = "metadata.nested.deep.value = 'test' AND metadata.nested.count > 10";
|
|
const result = parser.parse(query);
|
|
|
|
expect(result.type).toBe('operator');
|
|
const operator = result as QueryOperator;
|
|
const condition1 = operator.conditions[0] as QueryConditionText;
|
|
const condition2 = operator.conditions[1] as QueryConditionNumber;
|
|
expect(condition1.field).toEqual(['metadata', 'nested', 'deep', 'value']);
|
|
expect(condition2.field).toEqual(['metadata', 'nested', 'count']);
|
|
});
|
|
|
|
it('should handle query from documentation example', () => {
|
|
// From the JSON format in docs
|
|
const expectedJson: QueryFilter = {
|
|
type: 'operator',
|
|
operator: 'and',
|
|
conditions: [
|
|
{
|
|
type: 'text',
|
|
field: ['metadata', 'foo'],
|
|
conditions: { equal: 'bar' },
|
|
},
|
|
{
|
|
type: 'text',
|
|
field: ['type'],
|
|
conditions: { equal: 'demo' },
|
|
},
|
|
],
|
|
};
|
|
|
|
const sql = "metadata.foo = 'bar' AND type = 'demo'";
|
|
const parsed = parser.parse(sql);
|
|
|
|
expect(parsed).toEqual(expectedJson);
|
|
});
|
|
});
|
|
});
|