Skip to content

Commit fea0d1a

Browse files
committed
Wrap expressions with parenthesis based on their precedence
Change-type: patch See: https://balena.zulipchat.com/#narrow/stream/346007-balena-io.2FbalenaCloud/topic/pinejs.2015
1 parent b39c90a commit fea0d1a

File tree

3 files changed

+105
-22
lines changed

3 files changed

+105
-22
lines changed

src/AbstractSQLRules2SQL.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -387,14 +387,41 @@ const mathOps = {
387387
};
388388
export type MathOps = keyof typeof mathOps;
389389

390-
const mathOperatorNodeTypes = new Set([
391-
...Object.keys(mathOps),
392-
'AddDateDuration',
393-
'AddDateNumber',
394-
'SubtractDateDate',
395-
'SubtractDateDuration',
396-
'SubtractDateNumber',
397-
]);
390+
const opsPrecedence = (() => {
391+
const operatorsByPrecedence = [
392+
['Multiply', 'Divide'],
393+
[
394+
'Add',
395+
'AddDateDuration',
396+
'AddDateNumber',
397+
'Subtract',
398+
'SubtractDateDate',
399+
'SubtractDateDuration',
400+
'SubtractDateNumber',
401+
],
402+
['Between', 'Like'],
403+
[
404+
'Equals',
405+
'NotEquals',
406+
'GreaterThan',
407+
'GreaterThanOrEqual',
408+
'LessThan',
409+
'LessThanOrEqual',
410+
],
411+
// In, Exists, NotExists, 'IsDistinctFrom', 'IsNotDistinctFrom', Not,
412+
// And, Or are already adding parenthesis.
413+
] as const;
414+
415+
const operatorPrecedence = {} as Record<string, number>;
416+
let precedence = 0;
417+
for (const samePrecedenceOps of operatorsByPrecedence) {
418+
for (const op of samePrecedenceOps) {
419+
operatorPrecedence[op] = precedence;
420+
}
421+
precedence++;
422+
}
423+
return operatorPrecedence;
424+
})();
398425

399426
const precedenceSafeOpValue = (
400427
parentNodeType: string,
@@ -406,11 +433,12 @@ const precedenceSafeOpValue = (
406433
const operandAbstractSql = getAbstractSqlQuery(args, index);
407434
const valueExpr = valueMatchFn(operandAbstractSql, indent);
408435
const [childNodeType] = operandAbstractSql;
436+
const parentOperatorPrecedence = opsPrecedence[parentNodeType];
437+
const childOperatorPrecedence = opsPrecedence[childNodeType];
409438
if (
410-
(mathOperatorNodeTypes.has(parentNodeType) &&
411-
mathOperatorNodeTypes.has(childNodeType)) ||
412-
// We need parenthesis for chained boolean comparisons, otherwise PostgreSQL complains.
413-
(parentNodeType in comparisons && childNodeType in comparisons)
439+
childOperatorPrecedence != null &&
440+
parentOperatorPrecedence != null &&
441+
parentOperatorPrecedence <= childOperatorPrecedence
414442
) {
415443
return `(${valueExpr})`;
416444
}
@@ -1016,9 +1044,9 @@ const typeRules: Dictionary<MatchFn> = {
10161044
},
10171045
Between: (args, indent) => {
10181046
checkArgs('Between', args, 3);
1019-
const v = AnyValue(getAbstractSqlQuery(args, 0), indent);
1020-
const a = AnyValue(getAbstractSqlQuery(args, 1), indent);
1021-
const b = AnyValue(getAbstractSqlQuery(args, 2), indent);
1047+
const v = precedenceSafeOpValue('Between', AnyValue, args, 0, indent);
1048+
const a = precedenceSafeOpValue('Between', AnyValue, args, 1, indent);
1049+
const b = precedenceSafeOpValue('Between', AnyValue, args, 2, indent);
10221050
return `${v} BETWEEN ${a} AND (${b})`;
10231051
},
10241052
Add: MathOp('Add'),

test/abstract-sql/comparisons.ts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,9 @@ describe('Comparison Operator Precedence', () => {
9696
it('should produce a valid NotEquals statement when the operands are math expressions with nested parenthesis', () => {
9797
sqlEquals(
9898
result.query,
99+
// ideally we shouldn't have any parenthesis here
99100
stripIndent`
100-
SELECT 1 + (2 + (3 * 4)) != 1 + 0
101+
SELECT 1 + (2 + 3 * 4) != 1 + 0
101102
`,
102103
);
103104
});
@@ -159,7 +160,7 @@ describe('Comparison Operator Precedence', () => {
159160
sqlEquals(
160161
result.query,
161162
stripIndent`
162-
SELECT 1 + (2 * 5) >= 12 + 2
163+
SELECT 1 + 2 * 5 >= 12 + 2
163164
`,
164165
);
165166
});
@@ -330,6 +331,33 @@ describe('Comparison Operator Precedence', () => {
330331
},
331332
);
332333

334+
test(
335+
[
336+
'SelectQuery',
337+
[
338+
'Select',
339+
[
340+
[
341+
'Between',
342+
['Equals', ['Integer', 1], ['Integer', 0]],
343+
['LessThan', ['Integer', 1], ['Integer', 0]],
344+
['GreaterThan', ['Integer', 1], ['Integer', 0]],
345+
],
346+
],
347+
],
348+
],
349+
(result, sqlEquals) => {
350+
it('should produce a valid Between statement when the operands are comparison expressions', () => {
351+
sqlEquals(
352+
result.query,
353+
stripIndent`
354+
SELECT (1 = 0) BETWEEN (1 < 0) AND ((1 > 0))
355+
`,
356+
);
357+
});
358+
},
359+
);
360+
333361
test(
334362
[
335363
'SelectQuery',
@@ -356,4 +384,31 @@ describe('Comparison Operator Precedence', () => {
356384
});
357385
},
358386
);
387+
388+
test(
389+
[
390+
'SelectQuery',
391+
[
392+
'Select',
393+
[
394+
[
395+
'Between',
396+
['Equals', ['Integer', 1], ['Integer', 0]],
397+
['LessThan', ['Integer', 1], ['Integer', 0]],
398+
['GreaterThan', ['Integer', 1], ['Integer', 0]],
399+
],
400+
],
401+
],
402+
],
403+
(result, sqlEquals) => {
404+
it('should produce a valid Between statement when the operands are comparison expressions', () => {
405+
sqlEquals(
406+
result.query,
407+
stripIndent`
408+
SELECT (1 = 0) BETWEEN (1 < 0) AND ((1 > 0))
409+
`,
410+
);
411+
});
412+
},
413+
);
359414
});

test/abstract-sql/math.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ describe('Math Operator Precedence', () => {
128128
],
129129
(result, sqlEquals) => {
130130
it('should produce a valid Add statement when the first operand is a Multiply', () => {
131-
sqlEquals(result.query, 'SELECT (2 * 3) + 4');
131+
sqlEquals(result.query, 'SELECT 2 * 3 + 4');
132132
});
133133
},
134134
);
@@ -143,7 +143,7 @@ describe('Math Operator Precedence', () => {
143143
],
144144
(result, sqlEquals) => {
145145
it('should produce a valid Add statement when the second operand is a Multiply', () => {
146-
sqlEquals(result.query, 'SELECT 2 + (3 * 4)');
146+
sqlEquals(result.query, 'SELECT 2 + 3 * 4');
147147
});
148148
},
149149
);
@@ -164,7 +164,7 @@ describe('Math Operator Precedence', () => {
164164
],
165165
(result, sqlEquals) => {
166166
it('should produce a valid Add statement of two Multiplications', () => {
167-
sqlEquals(result.query, 'SELECT (2 * 3) + (4 * 5)');
167+
sqlEquals(result.query, 'SELECT 2 * 3 + 4 * 5');
168168
});
169169
},
170170
);
@@ -236,7 +236,7 @@ describe('Math Operator Precedence', () => {
236236
],
237237
(result, sqlEquals) => {
238238
it('should produce a valid Subtract statement of two Multiplications', () => {
239-
sqlEquals(result.query, 'SELECT (2 * 3) - (4 * 5)');
239+
sqlEquals(result.query, 'SELECT 2 * 3 - 4 * 5');
240240
});
241241
},
242242
);
@@ -257,7 +257,7 @@ describe('Math Operator Precedence', () => {
257257
],
258258
(result, sqlEquals) => {
259259
it('should produce a valid Subtract statement of two Divisions', () => {
260-
sqlEquals(result.query, 'SELECT (2 / 3) - (4 / 5)');
260+
sqlEquals(result.query, 'SELECT 2 / 3 - 4 / 5');
261261
});
262262
},
263263
);

0 commit comments

Comments
 (0)