sqlparser-devexpress 2.3.1 → 2.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/converter.js +3 -3
- package/src/core/parser.js +72 -23
- package/src/debug.js +1 -1
- package/tests/error.test.js +66 -0
- package/tests/parser.test.js +25 -19
package/package.json
CHANGED
package/src/core/converter.js
CHANGED
|
@@ -140,9 +140,9 @@ function DevExpressConverter() {
|
|
|
140
140
|
|
|
141
141
|
let comparison = [left, operatorToken, right];
|
|
142
142
|
|
|
143
|
-
if (isFunctionNullCheck(ast.left, true)) {
|
|
143
|
+
if ((ast.left && isFunctionNullCheck(ast.left, true)) || (ast.value && isFunctionNullCheck(ast.value, false))) {
|
|
144
144
|
comparison = [[left, operatorToken, right], 'or', [left, operatorToken, null]];
|
|
145
|
-
} else if (isFunctionNullCheck(ast.right, true)) {
|
|
145
|
+
} else if (ast.right && isFunctionNullCheck(ast.right, true)) {
|
|
146
146
|
comparison = [[left, operatorToken, right], 'or', [right, operatorToken, null]];
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -248,7 +248,7 @@ function DevExpressConverter() {
|
|
|
248
248
|
if (!resultObject) return `{${placeholder}}`;
|
|
249
249
|
|
|
250
250
|
|
|
251
|
-
return resultObject.hasOwnProperty(placeholder) ? resultObject[placeholder] : `{${placeholder}}`;
|
|
251
|
+
return resultObject.hasOwnProperty(placeholder) ? resultObject[placeholder] : `{${placeholder.value ?? placeholder}}`;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/**
|
package/src/core/parser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LITERALS, OPERATOR_PRECEDENCE, UNSUPPORTED_PATTERN } from "../constants.js";
|
|
1
|
+
import { LITERALS, LOGICAL_OPERATORS, OPERATOR_PRECEDENCE, UNSUPPORTED_PATTERN } from "../constants.js";
|
|
2
2
|
import { Tokenizer } from "./tokenizer.js";
|
|
3
3
|
|
|
4
4
|
|
|
@@ -78,20 +78,20 @@ export function parse(input, variables = []) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
function parseFunction() {
|
|
81
|
-
const
|
|
81
|
+
const functionName = currentToken.value.toUpperCase();
|
|
82
82
|
next();
|
|
83
83
|
|
|
84
|
-
expectedToken(currentToken, "(", `Expected ( after ${
|
|
84
|
+
expectedToken(currentToken, "(", `Expected ( after ${functionName}`);
|
|
85
85
|
|
|
86
86
|
next();
|
|
87
87
|
|
|
88
|
-
const
|
|
88
|
+
const functionArgs = [];
|
|
89
89
|
while (currentToken && currentToken.value !== ")") {
|
|
90
|
-
|
|
90
|
+
functionArgs.push(parseExpression());
|
|
91
91
|
if (currentToken && currentToken.value === ",") next();
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
expectedToken(currentToken, ")", `Expected ) after ${
|
|
94
|
+
expectedToken(currentToken, ")", `Expected ) after ${functionName}`);
|
|
95
95
|
|
|
96
96
|
next(); // Consume the closing parenthesis
|
|
97
97
|
|
|
@@ -99,17 +99,22 @@ export function parse(input, variables = []) {
|
|
|
99
99
|
if (currentToken && currentToken.type === "operator") {
|
|
100
100
|
const operator = currentToken.value;
|
|
101
101
|
next(); // Move to the next token after the operator
|
|
102
|
-
const
|
|
102
|
+
const rightOperand = parseValue(); // Parse the value after the operator
|
|
103
|
+
const nodeType = LOGICAL_OPERATORS.includes(operator.toLowerCase()) ? "logical" : "comparison";
|
|
104
|
+
|
|
105
|
+
if(nodeType === "logical") {
|
|
106
|
+
return { type: "logical", operator, left: { type: "function", name: functionName, args: functionArgs }, right: rightOperand };
|
|
107
|
+
}
|
|
103
108
|
|
|
104
109
|
return {
|
|
105
110
|
type: "comparison",
|
|
106
|
-
left: { type: "function", name:
|
|
111
|
+
left: { type: "function", name: functionName, args: functionArgs },
|
|
107
112
|
operator,
|
|
108
|
-
value
|
|
113
|
+
value: rightOperand
|
|
109
114
|
};
|
|
110
115
|
}
|
|
111
116
|
|
|
112
|
-
return { type: "function", name:
|
|
117
|
+
return { type: "function", name: functionName, args: functionArgs };
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
// Parses logical expressions using operator precedence
|
|
@@ -165,6 +170,21 @@ export function parse(input, variables = []) {
|
|
|
165
170
|
|
|
166
171
|
if (operator === "between") return parseBetweenComparison(field, operator);
|
|
167
172
|
|
|
173
|
+
if (currentToken.type === "function") {
|
|
174
|
+
const functionNode = parseFunction();
|
|
175
|
+
|
|
176
|
+
// Wrap the function inside a comparison if it's directly after an operator
|
|
177
|
+
const leftComparison = {
|
|
178
|
+
type: "comparison",
|
|
179
|
+
field,
|
|
180
|
+
operator,
|
|
181
|
+
value: functionNode.left
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
functionNode.left = leftComparison;
|
|
185
|
+
return functionNode;
|
|
186
|
+
}
|
|
187
|
+
|
|
168
188
|
// For other comparison operators, parse a single right-hand value
|
|
169
189
|
const valueType = currentToken.type;
|
|
170
190
|
const value = parseValue(operator);
|
|
@@ -184,30 +204,59 @@ export function parse(input, variables = []) {
|
|
|
184
204
|
function parseValue(operatorToken) {
|
|
185
205
|
if (!currentToken) throw new Error("Unexpected end of input");
|
|
186
206
|
|
|
207
|
+
// Handle function without consuming the token
|
|
208
|
+
if (currentToken.type === "function") {
|
|
209
|
+
return parseFunction();
|
|
210
|
+
}
|
|
211
|
+
|
|
187
212
|
const token = currentToken;
|
|
188
213
|
next(); // Move to the next token
|
|
189
214
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (token.type === "null") return null;
|
|
215
|
+
switch (token.type) {
|
|
216
|
+
case "number":
|
|
217
|
+
return Number(token.value);
|
|
194
218
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const val = token.value.slice(1, -1);
|
|
198
|
-
if (!variables.includes(val)) variables.push(val);
|
|
199
|
-
return { type: "placeholder", value: val };
|
|
200
|
-
}
|
|
219
|
+
case "string":
|
|
220
|
+
return token.value.slice(1, -1).replace(/''/g, "");
|
|
201
221
|
|
|
202
|
-
|
|
222
|
+
case "identifier":
|
|
223
|
+
return token.value;
|
|
203
224
|
|
|
204
|
-
|
|
205
|
-
|
|
225
|
+
case "null":
|
|
226
|
+
return null;
|
|
227
|
+
|
|
228
|
+
case "placeholder": {
|
|
229
|
+
const val = token.value.slice(1, -1);
|
|
230
|
+
if (!variables.includes(val)) variables.push(val);
|
|
231
|
+
return { type: "placeholder", value: val };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case "paren": {
|
|
235
|
+
if (currentToken.type === "function") {
|
|
236
|
+
return parseFunction();
|
|
237
|
+
}
|
|
238
|
+
// Handle ({Placeholder}) syntax for placeholders inside parentheses
|
|
239
|
+
const nextToken = tokenizer.peekNextToken();
|
|
240
|
+
if (currentToken && currentToken.type === "placeholder" &&
|
|
241
|
+
nextToken && nextToken.type === "paren") {
|
|
242
|
+
const val = parseValue();
|
|
243
|
+
return { type: "placeholder", value: val };
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Handle IN or NOT IN operator (outside switch as intended)
|
|
250
|
+
operatorToken = operatorToken?.toUpperCase();
|
|
251
|
+
if (operatorToken === "IN" || operatorToken === "NOT IN") {
|
|
252
|
+
return parseInList(token);
|
|
253
|
+
}
|
|
206
254
|
|
|
207
255
|
throw new Error(`Unexpected value: ${token.value}`);
|
|
208
256
|
}
|
|
209
257
|
|
|
210
258
|
|
|
259
|
+
|
|
211
260
|
// Start parsing and return the AST with extracted variables
|
|
212
261
|
return { ast: parseExpression(), variables };
|
|
213
262
|
}
|
package/src/debug.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// return convertToDevExpressFormat({ ast: astTree, resultObject: sampleData });
|
|
29
29
|
// }
|
|
30
30
|
|
|
31
|
-
// const devexpress = parseFilterString("
|
|
31
|
+
// const devexpress = parseFilterString("(ISNULL(TicketID, 0) = ISNULL({CustomerOrders.OrderID}, 0))", sampleData);
|
|
32
32
|
// console.log("DevExpress Filter:", JSON.stringify(devexpress, null, 2));
|
|
33
33
|
// // const devexpress = parseFilterString("(RS2ID in ({LeadStatementGlobalRpt.StateID}) Or ({LeadStatementGlobalRpt.StateID} =0)) And (RS3ID in (0,{LeadStatementGlobalRpt.RegionID}) Or {LeadStatementGlobalRpt.RegionID} =0 )", sampleData);
|
|
34
34
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { convertToDevExpressFormat } from "../src/core/converter";
|
|
3
|
+
import { parse } from "../src/core/parser";
|
|
4
|
+
import { sanitizeQuery } from "../src/core/sanitizer";
|
|
5
|
+
import { convertAstToDevextreme, convertSQLToAst } from "../src";
|
|
6
|
+
|
|
7
|
+
describe("Parser SQL to dx Filter Builder", () => {
|
|
8
|
+
const testCases = [
|
|
9
|
+
{
|
|
10
|
+
input: "NULL",
|
|
11
|
+
expected: []
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
input: "SELECT DISTINCT O.OrderID AS ID, O.CustomerName, O.OrderType, O.CustomerName AS [Online Order], O.OrderDate AS OrderDate, D.DeliveryStatus, O.OrderDate AS [Online Order Date], O.CompanyID, CAST(CAST(O.OrderDate AS DATE) AS VARCHAR(10)) AS DocumentDate FROM Orders O INNER JOIN Payment P ON P.OrderID = O.OrderID INNER JOIN Shipment S ON S.PaymentID = P.PaymentID INNER JOIN Delivery D ON D.ShipmentID = S.ShipmentID ",
|
|
15
|
+
expect: null
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
input: "CompanyID = CompanyID2 = {AccountingRule.CompanyID}",
|
|
19
|
+
expected: "Error: Invalid comparison: CompanyID = CompanyID2",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
input: "( CompanyID = {AccountingRule.CompanyID}",
|
|
23
|
+
expected: "Error: Missing closing parenthesis"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
testCases.forEach(({ input, expected }, index) => {
|
|
29
|
+
it(`Test Case ${index + 1}: ${input}`, () => {
|
|
30
|
+
|
|
31
|
+
if (expected == undefined) {
|
|
32
|
+
expected = null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let astwithVariables;
|
|
36
|
+
try {
|
|
37
|
+
astwithVariables = convertSQLToAst(input);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
expect(error.message).toEqual(expected.replace("Error: ", ""));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (astwithVariables == null) {
|
|
44
|
+
expect(null).toEqual(expected);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const variables = astwithVariables.variables;
|
|
49
|
+
const ast = astwithVariables.ast;
|
|
50
|
+
|
|
51
|
+
const result = convertAstToDevextreme(ast, sampleData);
|
|
52
|
+
|
|
53
|
+
if (result == null || result == true || result == false) {
|
|
54
|
+
expect([]).toEqual(expected);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
expect(result).toEqual(expected);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
const sampleData = {
|
|
65
|
+
"AccountingRule.CompanyID": 42,
|
|
66
|
+
};
|
package/tests/parser.test.js
CHANGED
|
@@ -102,10 +102,6 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
102
102
|
["ItemGroupType", "=", ""]
|
|
103
103
|
]
|
|
104
104
|
},
|
|
105
|
-
{
|
|
106
|
-
input: "NULL",
|
|
107
|
-
expected: []
|
|
108
|
-
},
|
|
109
105
|
{
|
|
110
106
|
input: "((ISNULL({0}, 0) = 0 AND CompanyID = {1}) OR CompanyID IS NULL) OR BranchID = {0} | [LeadDocument.BranchID] | [LeadDocument.CompanyID]",
|
|
111
107
|
expected: [
|
|
@@ -122,10 +118,6 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
122
118
|
["BranchID", "=", 42]
|
|
123
119
|
]
|
|
124
120
|
},
|
|
125
|
-
{
|
|
126
|
-
input: "SELECT DISTINCT OP.DocID ID,OP.DocName,OP.DocType,OP.DocName [Work Purchase Order],OP.DocDate DocDate,SP.WoStatus,OP.DocDate [Work Purchase Order Date], OP.CompanyID, cast(cast(OP.DocDate as date) as varchar(10)) DocumentDate FROM OpenDocuments OP inner join PurchaseHeader PH on PH.Id=op.DocID inner JOIN PurchasePosting PP ON PP.DocID = PH.ID inner JOIN SalePosting SP ON SP.PurchasePostingLineID = PP.ID",
|
|
127
|
-
expect: null
|
|
128
|
-
},
|
|
129
121
|
{
|
|
130
122
|
input: "FromDate Between '10-10-2021' AND '10-10-2022'",
|
|
131
123
|
expected: [
|
|
@@ -152,10 +144,6 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
152
144
|
["SourceID", "=", null]
|
|
153
145
|
]
|
|
154
146
|
},
|
|
155
|
-
{
|
|
156
|
-
input: "CompanyID = CompanyID2 = {AccountingRule.CompanyID}",
|
|
157
|
-
expected: "Error: Invalid comparison: CompanyID = CompanyID2",
|
|
158
|
-
},
|
|
159
147
|
{
|
|
160
148
|
input: "(CompanyID = {LeadDocument.CompanyID} OR ISNULL(CompanyID,0) = 0) AND (ISNULL(IsSubdealer,0) = {LeadDocument.AllowSubDealer})",
|
|
161
149
|
expected: [
|
|
@@ -193,6 +181,27 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
193
181
|
"or",
|
|
194
182
|
["AddressType", "=", 2]
|
|
195
183
|
]
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
input: "(ISNULL(TicketID, 0) = ISNULL({SupportResolution.TicketID}, 0))",
|
|
187
|
+
expected: [
|
|
188
|
+
["TicketID", "=", 123],
|
|
189
|
+
"or",
|
|
190
|
+
["TicketID", "=", null]
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
input: "CompanyID = ISNULL({LeadDocument.CompanyID},0) OR (ISNULL(CompanyID,0) = 0))",
|
|
195
|
+
expected: [
|
|
196
|
+
["CompanyID", "=", 7],
|
|
197
|
+
"or",
|
|
198
|
+
["CompanyID", "=", null],
|
|
199
|
+
"or",
|
|
200
|
+
["CompanyID", "=", 0],
|
|
201
|
+
"or",
|
|
202
|
+
["CompanyID", "=", null]
|
|
203
|
+
|
|
204
|
+
]
|
|
196
205
|
}
|
|
197
206
|
];
|
|
198
207
|
|
|
@@ -204,12 +213,8 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
204
213
|
}
|
|
205
214
|
|
|
206
215
|
let astwithVariables;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
} catch (error) {
|
|
210
|
-
expect(error.message).toEqual(expected.replace("Error: ", ""));
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
216
|
+
astwithVariables = convertSQLToAst(input);
|
|
217
|
+
|
|
213
218
|
|
|
214
219
|
if (astwithVariables == null) {
|
|
215
220
|
expect(null).toEqual(expected);
|
|
@@ -256,5 +261,6 @@ const sampleData = {
|
|
|
256
261
|
"LeadDocument.BranchID": 42,
|
|
257
262
|
"LeadDocument.CompanyID": 7,
|
|
258
263
|
"ServiceOrderDocument.SourceID": 2,
|
|
259
|
-
"LeadDocument.AllowSubDealer": true
|
|
264
|
+
"LeadDocument.AllowSubDealer": true,
|
|
265
|
+
"SupportResolution.TicketID": 123
|
|
260
266
|
};
|