From fc67ad0b0ff0ef7f037696740041ff9cb40720c3 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 12 Mar 2020 14:09:13 +0100 Subject: [PATCH] filter parser --- packages/filterparser/src/parseFilter.ts | 180 +++++++++++++++++------ packages/sqltree/src/dumpSqlCondition.ts | 17 +++ packages/sqltree/src/types.ts | 10 +- 3 files changed, 159 insertions(+), 48 deletions(-) diff --git a/packages/filterparser/src/parseFilter.ts b/packages/filterparser/src/parseFilter.ts index a5004330..3fed8453 100644 --- a/packages/filterparser/src/parseFilter.ts +++ b/packages/filterparser/src/parseFilter.ts @@ -44,6 +44,17 @@ const binaryCondition = operator => value => ({ }, }); +const likeCondition = (conditionType, likeString) => value => ({ + conditionType, + left: { + exprType: 'placeholder', + }, + right: { + exprType: 'value', + value: likeString.replace('#VALUE#', value), + }, +}); + const compoudCondition = conditionType => conditions => { if (conditions.length == 1) return conditions[0]; return { @@ -75,58 +86,135 @@ const binaryFixedValueCondition = value => () => { }; }; -const parser = P.createLanguage({ - string1: () => - token(P.regexp(/"((?:\\.|.)*?)"/, 1)) - .map(interpretEscapes) - .map(binaryCondition('=')) - .desc('string quoted'), +const negateCondition = condition => { + return { + conditionType: 'not', + condition, + }; +}; - string2: () => - token(P.regexp(/'((?:\\.|.)*?)'/, 1)) - .map(interpretEscapes) - .map(binaryCondition('=')) - .desc('string quoted'), +const createParser = (filterType: FilterType) => { + const langDef = { + string1: () => + token(P.regexp(/"((?:\\.|.)*?)"/, 1)) + .map(interpretEscapes) + .desc('string quoted'), - number: () => - token(P.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)) - .map(Number) - .map(binaryCondition('=')) - .desc('number'), + string2: () => + token(P.regexp(/'((?:\\.|.)*?)'/, 1)) + .map(interpretEscapes) + .desc('string quoted'), - noQuotedString: () => - P.regexp(/[^\s^,^'^"]+/) - .desc('string unquoted') - .map(binaryCondition('=')), + number: () => + token(P.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)) + .map(Number) + .desc('number'), - comma: () => word(','), - not: () => word('NOT'), - notNull: r => r.not.then(r.null).map(unaryCondition('isNotNull')), - null: () => word('NULL').map(unaryCondition('isNull')), - empty: () => word('EMPTY').map(unaryCondition('isEmpty')), - notEmpty: r => r.not.then(r.empty).map(unaryCondition('isNotEmpty')), - true: () => word('TRUE').map(binaryFixedValueCondition(1)), - false: () => word('FALSE').map(binaryFixedValueCondition(0)), + noQuotedString: () => P.regexp(/[^\s^,^'^"]+/).desc('string unquoted'), - element: r => - P.alt( - r.string1, - r.string2, - r.null, - r.notNull, - r.number, - r.empty, - r.notEmpty, - r.true, - r.false, - // must be last - r.noQuotedString - ).trim(whitespace), - factor: r => r.element.sepBy(whitespace).map(compoudCondition('and')), - list: r => r.factor.sepBy(r.comma).map(compoudCondition('or')), -}); + value: r => P.alt(...allowedValues.map(x => r[x])), + valueTestEq: r => r.value.map(binaryCondition('=')), + valueTestStr: r => r.value.map(likeCondition('like', '%#VALUE#%')), + + comma: () => word(','), + not: () => word('NOT'), + notNull: r => r.not.then(r.null).map(unaryCondition('isNotNull')), + null: () => word('NULL').map(unaryCondition('isNull')), + empty: () => word('EMPTY').map(unaryCondition('isEmpty')), + notEmpty: r => r.not.then(r.empty).map(unaryCondition('isNotEmpty')), + true: () => word('TRUE').map(binaryFixedValueCondition(1)), + false: () => word('FALSE').map(binaryFixedValueCondition(0)), + eq: r => + word('=') + .then(r.value) + .map(binaryCondition('=')), + ne: r => + word('!=') + .then(r.value) + .map(binaryCondition('<>')), + lt: r => + word('<') + .then(r.value) + .map(binaryCondition('<')), + gt: r => + word('>') + .then(r.value) + .map(binaryCondition('>')), + le: r => + word('<=') + .then(r.value) + .map(binaryCondition('<=')), + ge: r => + word('>=') + .then(r.value) + .map(binaryCondition('>=')), + startsWith: r => + word('^') + .then(r.value) + .map(likeCondition('like', '#VALUE#%')), + endsWith: r => + word('$') + .then(r.value) + .map(likeCondition('like', '%#VALUE#')), + contains: r => + word('+') + .then(r.value) + .map(likeCondition('like', '%#VALUE#%')), + startsWithNot: r => + word('!^') + .then(r.value) + .map(likeCondition('like', '#VALUE#%')) + .map(negateCondition), + endsWithNot: r => + word('!$') + .then(r.value) + .map(likeCondition('like', '%#VALUE#')) + .map(negateCondition), + containsNot: r => + word('~') + .then(r.value) + .map(likeCondition('like', '%#VALUE#%')) + .map(negateCondition), + + element: r => P.alt(...allowedElements.map(x => r[x])).trim(whitespace), + factor: r => r.element.sepBy(whitespace).map(compoudCondition('and')), + list: r => r.factor.sepBy(r.comma).map(compoudCondition('or')), + }; + + const allowedValues = []; // 'string1', 'string2', 'number', 'noQuotedString']; + if (filterType == 'string') allowedValues.push('string1', 'string2', 'noQuotedString'); + if (filterType == 'number') allowedValues.push('number'); + + const allowedElements = ['null', 'notNull', 'eq', 'ne']; + if (filterType == 'number' || filterType == 'datetime') allowedElements.push('lt', 'gt', 'le', 'ge'); + if (filterType == 'string') + allowedElements.push( + 'empty', + 'notEmpty', + 'startsWith', + 'endsWith', + 'contains', + 'startsWithNot', + 'endsWithNot', + 'containsNot' + ); + if (filterType == 'logical') allowedElements.push('true', 'false'); + + // must be last + if (filterType == 'string') allowedElements.push('valueTestStr'); + else allowedElements.push('valueTestEq'); + + return P.createLanguage(langDef); +}; + +const parsers = { + number: createParser('number'), + string: createParser('string'), + datetime: createParser('datetime'), + logical: createParser('logical'), +}; export function parseFilter(value: string, filterType: FilterType) { - const ast = parser.list.tryParse(value); + const ast = parsers[filterType].list.tryParse(value); return ast; } diff --git a/packages/sqltree/src/dumpSqlCondition.ts b/packages/sqltree/src/dumpSqlCondition.ts index f3b00987..f20b1ab3 100644 --- a/packages/sqltree/src/dumpSqlCondition.ts +++ b/packages/sqltree/src/dumpSqlCondition.ts @@ -1,6 +1,7 @@ import { SqlDumper } from '@dbgate/types'; import { Condition, BinaryCondition } from './types'; import { dumpSqlExpression } from './dumpSqlExpression'; +import { link } from 'fs'; export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) { switch (condition.conditionType) { @@ -34,5 +35,21 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) { dumpSqlCondition(dmp, cond); dmp.putRaw(')'); }); + break; + case 'like': + dumpSqlExpression(dmp, condition.left); + dmp.put(' ^like '); + dumpSqlExpression(dmp, condition.right); + break; + case 'notLike': + dumpSqlExpression(dmp, condition.left); + dmp.put(' ^not ^like '); + dumpSqlExpression(dmp, condition.right); + break; + case 'not': + dmp.put('^not ('); + dumpSqlCondition(dmp, condition.condition); + dmp.put(')'); + break; } } diff --git a/packages/sqltree/src/types.ts b/packages/sqltree/src/types.ts index 218df173..c2f026a3 100644 --- a/packages/sqltree/src/types.ts +++ b/packages/sqltree/src/types.ts @@ -28,8 +28,14 @@ export interface UnaryCondition { } export interface BinaryCondition { - operator: '=' | '!=' | '<' | '>' | '>=' | '<='; conditionType: 'binary'; + operator: '=' | '!=' | '<' | '>' | '>=' | '<='; + left: Expression; + right: Expression; +} + +export interface LikeCondition { + conditionType: 'like' | 'notLike'; left: Expression; right: Expression; } @@ -48,7 +54,7 @@ export interface CompoudCondition { conditions: Condition[]; } -export type Condition = BinaryCondition | NotCondition | TestCondition | CompoudCondition; +export type Condition = BinaryCondition | NotCondition | TestCondition | CompoudCondition | LikeCondition; export interface Source { name?: NamedObjectInfo;