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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlparser-devexpress",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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
  /**
@@ -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 funcName = currentToken.value.toUpperCase();
81
+ const functionName = currentToken.value.toUpperCase();
82
82
  next();
83
83
 
84
- expectedToken(currentToken, "(", `Expected ( after ${funcName}`);
84
+ expectedToken(currentToken, "(", `Expected ( after ${functionName}`);
85
85
 
86
86
  next();
87
87
 
88
- const args = [];
88
+ const functionArgs = [];
89
89
  while (currentToken && currentToken.value !== ")") {
90
- args.push(parseExpression());
90
+ functionArgs.push(parseExpression());
91
91
  if (currentToken && currentToken.value === ",") next();
92
92
  }
93
93
 
94
- expectedToken(currentToken, ")", `Expected ) after ${funcName}`);
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 value = parseValue(); // Parse the value after the operator
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: funcName, args },
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: funcName, args };
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
- if (token.type === "number") return Number(token.value);
191
- if (token.type === "string") return token.value.slice(1, -1).replace(/''/g, "");
192
- if (token.type === "identifier") return token.value;
193
- if (token.type === "null") return null;
215
+ switch (token.type) {
216
+ case "number":
217
+ return Number(token.value);
194
218
 
195
- // Handle placeholders like `{VariableName}`
196
- if (token.type === "placeholder") {
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
- operatorToken = operatorToken.toUpperCase();
222
+ case "identifier":
223
+ return token.value;
203
224
 
204
- // Handle IN operator which requires a list of values
205
- if (operatorToken && (operatorToken === "IN" || operatorToken === "NOT IN")) return parseInList(token);
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("AddressType IN ('2', ('4')) OR AddressType =({ServiceOrderDocument .SourceID})", sampleData);
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
+ };
@@ -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
- try {
208
- astwithVariables = convertSQLToAst(input);
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
  };