filter parser

This commit is contained in:
Jan Prochazka
2020-03-12 14:09:13 +01:00
parent 2de6033dc9
commit fc67ad0b0f
3 changed files with 159 additions and 48 deletions

View File

@@ -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 => { const compoudCondition = conditionType => conditions => {
if (conditions.length == 1) return conditions[0]; if (conditions.length == 1) return conditions[0];
return { return {
@@ -75,58 +86,135 @@ const binaryFixedValueCondition = value => () => {
}; };
}; };
const parser = P.createLanguage({ const negateCondition = condition => {
string1: () => return {
token(P.regexp(/"((?:\\.|.)*?)"/, 1)) conditionType: 'not',
.map(interpretEscapes) condition,
.map(binaryCondition('=')) };
.desc('string quoted'), };
string2: () => const createParser = (filterType: FilterType) => {
token(P.regexp(/'((?:\\.|.)*?)'/, 1)) const langDef = {
.map(interpretEscapes) string1: () =>
.map(binaryCondition('=')) token(P.regexp(/"((?:\\.|.)*?)"/, 1))
.desc('string quoted'), .map(interpretEscapes)
.desc('string quoted'),
number: () => string2: () =>
token(P.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)) token(P.regexp(/'((?:\\.|.)*?)'/, 1))
.map(Number) .map(interpretEscapes)
.map(binaryCondition('=')) .desc('string quoted'),
.desc('number'),
noQuotedString: () => number: () =>
P.regexp(/[^\s^,^'^"]+/) token(P.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/))
.desc('string unquoted') .map(Number)
.map(binaryCondition('=')), .desc('number'),
comma: () => word(','), noQuotedString: () => P.regexp(/[^\s^,^'^"]+/).desc('string unquoted'),
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)),
element: r => value: r => P.alt(...allowedValues.map(x => r[x])),
P.alt( valueTestEq: r => r.value.map(binaryCondition('=')),
r.string1, valueTestStr: r => r.value.map(likeCondition('like', '%#VALUE#%')),
r.string2,
r.null, comma: () => word(','),
r.notNull, not: () => word('NOT'),
r.number, notNull: r => r.not.then(r.null).map(unaryCondition('isNotNull')),
r.empty, null: () => word('NULL').map(unaryCondition('isNull')),
r.notEmpty, empty: () => word('EMPTY').map(unaryCondition('isEmpty')),
r.true, notEmpty: r => r.not.then(r.empty).map(unaryCondition('isNotEmpty')),
r.false, true: () => word('TRUE').map(binaryFixedValueCondition(1)),
// must be last false: () => word('FALSE').map(binaryFixedValueCondition(0)),
r.noQuotedString eq: r =>
).trim(whitespace), word('=')
factor: r => r.element.sepBy(whitespace).map(compoudCondition('and')), .then(r.value)
list: r => r.factor.sepBy(r.comma).map(compoudCondition('or')), .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) { export function parseFilter(value: string, filterType: FilterType) {
const ast = parser.list.tryParse(value); const ast = parsers[filterType].list.tryParse(value);
return ast; return ast;
} }

View File

@@ -1,6 +1,7 @@
import { SqlDumper } from '@dbgate/types'; import { SqlDumper } from '@dbgate/types';
import { Condition, BinaryCondition } from './types'; import { Condition, BinaryCondition } from './types';
import { dumpSqlExpression } from './dumpSqlExpression'; import { dumpSqlExpression } from './dumpSqlExpression';
import { link } from 'fs';
export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) { export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
switch (condition.conditionType) { switch (condition.conditionType) {
@@ -34,5 +35,21 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
dumpSqlCondition(dmp, cond); dumpSqlCondition(dmp, cond);
dmp.putRaw(')'); 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;
} }
} }

View File

@@ -28,8 +28,14 @@ export interface UnaryCondition {
} }
export interface BinaryCondition { export interface BinaryCondition {
operator: '=' | '!=' | '<' | '>' | '>=' | '<=';
conditionType: 'binary'; conditionType: 'binary';
operator: '=' | '!=' | '<' | '>' | '>=' | '<=';
left: Expression;
right: Expression;
}
export interface LikeCondition {
conditionType: 'like' | 'notLike';
left: Expression; left: Expression;
right: Expression; right: Expression;
} }
@@ -48,7 +54,7 @@ export interface CompoudCondition {
conditions: Condition[]; conditions: Condition[];
} }
export type Condition = BinaryCondition | NotCondition | TestCondition | CompoudCondition; export type Condition = BinaryCondition | NotCondition | TestCondition | CompoudCondition | LikeCondition;
export interface Source { export interface Source {
name?: NamedObjectInfo; name?: NamedObjectInfo;