Skip to content

Commit

Permalink
Fix #10497. Fixed logic operators precedence in CQL filter parse (#10498
Browse files Browse the repository at this point in the history
)
  • Loading branch information
offtherailz authored Jul 31, 2024
1 parent ebbd065 commit 1e593af
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 11 deletions.
13 changes: 13 additions & 0 deletions web/client/utils/__tests__/FilterUtils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2227,6 +2227,19 @@ describe('FilterUtils', () => {
xmlnsToAdd: ['xmlns:ogc="http://www.opengis.net/ogc"', 'xmlns:gml="http://www.opengis.net/gml"']
}, undefined, {...filterObj, ogcVersion});
expect(filter).toEqual(expectedFilter);
const args = [{
"ogcVersion": "1.1.0"
}, "P1 = 'V1' AND P2 = 'V2' OR P1 = 'V3' AND P2 = 'V4'", undefined, {
"featureTypeName": "cgd:GEO_FEATURE",
"filterType": "OGC",
"ogcVersion": "1.1.0",
"pagination": {
"startIndex": 0,
"maxFeatures": 20
}
}];
expect(mergeFiltersToOGC(...args)).toEqual(`<ogc:Filter><ogc:And><ogc:Or><ogc:And><ogc:PropertyIsEqualTo><ogc:PropertyName>P1</ogc:PropertyName><ogc:Literal>V1</ogc:Literal></ogc:PropertyIsEqualTo><ogc:PropertyIsEqualTo><ogc:PropertyName>P2</ogc:PropertyName><ogc:Literal>V2</ogc:Literal></ogc:PropertyIsEqualTo></ogc:And><ogc:And><ogc:PropertyIsEqualTo><ogc:PropertyName>P1</ogc:PropertyName><ogc:Literal>V3</ogc:Literal></ogc:PropertyIsEqualTo><ogc:PropertyIsEqualTo><ogc:PropertyName>P2</ogc:PropertyName><ogc:Literal>V4</ogc:Literal></ogc:PropertyIsEqualTo></ogc:And></ogc:Or></ogc:And></ogc:Filter>`);

});
// sub function to convert filters from other formats
describe('sub function to convert filters from other formats', () => {
Expand Down
48 changes: 46 additions & 2 deletions web/client/utils/ogc/Filter/CQL/__tests__/parser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,49 @@ const LOGICAL = [
"filters[1].filters[1].type": "not",
"filters[1].filters[1].filters[0].args[0].name": "PROP2"
}
},
// operator precedence
{
cql: "P1 = 'P_VALUE_1' AND P2 = 'P2_VALUE_1' OR P1 = 'P_VALUE_2' AND P2 = 'P2_VALUE_2'",
expected: {
"type": "or",
"filters[0].type": "and",
"filters[0].filters[0].type": "=",
"filters[0].filters[0].args[0].name": "P1",
"filters[0].filters[0].args[1].value": "P_VALUE_1",
"filters[0].filters[1].type": "=",
"filters[0].filters[1].args[0].name": "P2",
"filters[0].filters[1].args[1].value": "P2_VALUE_1",
"filters[1].type": "and",
"filters[1].filters[0].type": "=",
"filters[1].filters[0].args[0].name": "P1",
"filters[1].filters[0].args[1].value": "P_VALUE_2",
"filters[1].filters[1].type": "=",
"filters[1].filters[1].args[0].name": "P2",
"filters[1].filters[1].args[1].value": "P2_VALUE_2"

}
},
// operator precedence with parenthesis
{
cql: "P1 = 'P_VALUE_1' OR P2 = 'P2_VALUE_1' AND P1 = 'P_VALUE_2' OR P2 = 'P2_VALUE_2'",
expected: { // transformed in ((P1 = P_VALUE_1) OR ((P2 = P2_VALUE_1) AND (P1 = P_VALUE_2))) OR (P2 = P2_VALUE_2)
"type": "or",
"filters[0].type": "or",
"filters[0].filters[0].type": "=",
"filters[0].filters[0].args[0].name": "P1",
"filters[0].filters[0].args[1].value": "P_VALUE_1",
"filters[0].filters[1].type": "and",
"filters[0].filters[1].filters[0].type": "=",
"filters[0].filters[1].filters[0].args[0].name": "P2",
"filters[0].filters[1].filters[0].args[1].value": "P2_VALUE_1",
"filters[0].filters[1].filters[1].type": "=",
"filters[0].filters[1].filters[1].args[0].name": "P1",
"filters[0].filters[1].filters[1].args[1].value": "P_VALUE_2",
"filters[1].type": "=",
"filters[1].args[0].name": "P2",
"filters[1].args[1].value": "P2_VALUE_2"
}
}
];

Expand Down Expand Up @@ -631,10 +674,11 @@ const REAL_WORLD = [
];
const testRules = rules => rules.map(({ cql, expected }) => {
it(`testing ${cql}`, () => {
let res;
try {
const res = read(cql);
res = read(cql);
Object.keys(expected).map(k => {
expect(get(res, k)).toEqual(expected[k]);
expect(get(res, k)).toEqual(expected[k], ([a, b]) => `from "${cql}" filter: \n\texpected:\n\t\t${JSON.stringify(b)} \n\tat "${k}"\n\tbut got: \n\t\t${JSON.stringify(a)}. \n full object output:\n\t\t${JSON.stringify(res)}`);
});
} catch (e) {
throw e;
Expand Down
23 changes: 14 additions & 9 deletions web/client/utils/ogc/Filter/CQL/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export const patterns = {
COMPARISON: /^(=|<>|<=|<|>=|>|LIKE)/i,
IS_NULL: /^IS NULL/i,
COMMA: /^,/,
LOGICAL: /^(AND|OR)/i,
AND: /^(AND)/i,
OR: /^(OR)/i,
VALUE: /^('([^']|'')*'|-?\d+(\.\d*)?|\.\d+|true|false)/i,
LPAREN: /^\(/,
RPAREN: /^\)/,
Expand Down Expand Up @@ -60,15 +61,16 @@ export const patterns = {
const follows = {
INCLUDE: ['END'],
LPAREN: ['GEOMETRY', 'SPATIAL', 'FUNCTION', 'PROPERTY', 'VALUE', 'LPAREN', 'RPAREN', 'NOT'],
RPAREN: ['NOT', 'LOGICAL', 'END', 'RPAREN', 'COMMA', 'COMPARISON', 'BETWEEN', 'IS_NULL'],
RPAREN: ['NOT', 'AND', 'OR', 'END', 'RPAREN', 'COMMA', 'COMPARISON', 'BETWEEN', 'IS_NULL'],
PROPERTY: ['COMPARISON', 'BETWEEN', 'COMMA', 'IS_NULL', 'RPAREN'],
BETWEEN: ['VALUE'],
IS_NULL: ['END'],
COMPARISON: ['VALUE', 'FUNCTION'],
COMMA: ['GEOMETRY', 'FUNCTION', 'VALUE', 'PROPERTY'],
VALUE: ['LOGICAL', 'COMMA', 'RPAREN', 'END'],
VALUE: ['AND', 'OR', 'COMMA', 'RPAREN', 'END'],
SPATIAL: ['LPAREN'],
LOGICAL: ['NOT', 'VALUE', 'SPATIAL', 'FUNCTION', 'PROPERTY', 'LPAREN'],
AND: ['NOT', 'VALUE', 'SPATIAL', 'FUNCTION', 'PROPERTY', 'LPAREN'],
OR: ['NOT', 'VALUE', 'SPATIAL', 'FUNCTION', 'PROPERTY', 'LPAREN'],
NOT: ['PROPERTY', 'LPAREN'],
GEOMETRY: ['COMMA', 'RPAREN'],
FUNCTION: ['LPAREN', 'FUNCTION', 'VALUE', 'PROPERTY']
Expand Down Expand Up @@ -98,10 +100,12 @@ const cql = {
};

const precedence = {
'RPAREN': 3,
'LOGICAL': 2,
'RPAREN': 4,
'OR': 3,
'AND': 2,
'COMPARISON': 1
};

const tryToken = (text, pattern) => {
if (pattern instanceof RegExp) {
return pattern.exec(text);
Expand Down Expand Up @@ -180,9 +184,9 @@ const buildAst = (tokens) => {
case "BETWEEN":
case "IS_NULL":
case "INCLUDE":
case "LOGICAL":
case "AND":
case "OR":
let p = precedence[tok.type];

while (operatorStack.length > 0 &&
(precedence[operatorStack[operatorStack.length - 1].type] <= p)
) {
Expand Down Expand Up @@ -244,7 +248,8 @@ const buildAst = (tokens) => {
function buildTree() {
let tok = postfix.pop();
switch (tok.type) {
case "LOGICAL":
case "AND":
case "OR":
let rhs = buildTree();
let lhs = buildTree();
return ({
Expand Down
27 changes: 27 additions & 0 deletions web/client/utils/ogc/Filter/__tests__/fromObject-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,33 @@ const FUNCTIONS = [

const REAL_WORLD = [
// real world example
{
cql: "P1 = 'V1' AND P2 = 'V3' OR P1 = 'V1' AND P2 = 'V3'",
expected:
'<ogc:Or>'
+ '<ogc:And>'
+ '<ogc:PropertyIsEqualTo>'
+ '<ogc:PropertyName>P1</ogc:PropertyName>'
+ '<ogc:Literal>V1</ogc:Literal>'
+ '</ogc:PropertyIsEqualTo>'
+ '<ogc:PropertyIsEqualTo>'
+ '<ogc:PropertyName>P2</ogc:PropertyName>'
+ '<ogc:Literal>V3</ogc:Literal>'
+ '</ogc:PropertyIsEqualTo>'
+ '</ogc:And>'
+ '<ogc:And>'
+ '<ogc:PropertyIsEqualTo>'
+ '<ogc:PropertyName>P1</ogc:PropertyName>'
+ '<ogc:Literal>V1</ogc:Literal>'
+ '</ogc:PropertyIsEqualTo>'
+ '<ogc:PropertyIsEqualTo>'
+ '<ogc:PropertyName>P2</ogc:PropertyName>'
+ '<ogc:Literal>V3</ogc:Literal>'
+ '</ogc:PropertyIsEqualTo>'
+ '</ogc:And>'
+ '</ogc:Or>'

},
{
cql: "( DTINCID <= '1789-07-13' AND DTINCID >= '1492-10-11' ) AND (DOW='1') AND (TPINCID='1')",
expected:
Expand Down

0 comments on commit 1e593af

Please sign in to comment.