sqlparser-devexpress 2.1.1 → 2.1.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 +32 -9
- package/src/core/parser.js +3 -1
- package/src/core/tokenizer.js +6 -6
- package/src/index.js +3 -3
- package/tests/parser.test.js +38 -5
package/package.json
CHANGED
package/src/core/converter.js
CHANGED
|
@@ -136,13 +136,21 @@ function DevExpressConverter() {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// Handle "IN" condition, including comma-separated values
|
|
139
|
-
if (operator === "IN") {
|
|
140
|
-
return handleInOperator(ast);
|
|
139
|
+
if (operator === "IN" || operator === "NOT IN") {
|
|
140
|
+
return handleInOperator(ast, operator);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
const left = ast.left !== undefined ? processAstNode(ast.left) : convertValue(ast.field);
|
|
144
144
|
const right = ast.right !== undefined ? processAstNode(ast.right) : convertValue(ast.value);
|
|
145
|
-
const
|
|
145
|
+
const operatorToken = ast.operator.toLowerCase();
|
|
146
|
+
|
|
147
|
+
let comparison = [left, operatorToken, right];
|
|
148
|
+
|
|
149
|
+
if (isFunctionNullCheck(ast.left, true)) {
|
|
150
|
+
comparison = [[left, operatorToken, right], 'or', [left, operatorToken, null]];
|
|
151
|
+
} else if (isFunctionNullCheck(ast.right, true)) {
|
|
152
|
+
comparison = [[left, operatorToken, right], 'or', [right, operatorToken, null]];
|
|
153
|
+
}
|
|
146
154
|
|
|
147
155
|
// Apply short-circuit evaluation if enabled
|
|
148
156
|
if (EnableShortCircuit) {
|
|
@@ -179,7 +187,7 @@ function DevExpressConverter() {
|
|
|
179
187
|
* @param {Object} ast - The comparison operator AST node.
|
|
180
188
|
* @returns {Array} DevExpress format filter.
|
|
181
189
|
*/
|
|
182
|
-
function handleInOperator(ast) {
|
|
190
|
+
function handleInOperator(ast, operator) {
|
|
183
191
|
let resolvedValue = convertValue(ast.value);
|
|
184
192
|
|
|
185
193
|
// Handle comma-separated values in a string
|
|
@@ -190,15 +198,18 @@ function DevExpressConverter() {
|
|
|
190
198
|
} else {
|
|
191
199
|
resolvedValue = firstValue;
|
|
192
200
|
}
|
|
201
|
+
} else if (typeof resolvedValue === 'string' && resolvedValue.includes(',')) {
|
|
202
|
+
resolvedValue = resolvedValue.split(',').map(v => v.trim());
|
|
193
203
|
}
|
|
194
204
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return resolvedValue.flatMap(i => [[ast.field, '=', i], 'or']).slice(0, -1);
|
|
205
|
+
let operatorToken = operator === "IN" ? '=' : operator === "NOT IN" ? '!=' : operator;
|
|
206
|
+
let joinOperatorToken = operator === "IN" ? 'or' : operator === "NOT IN" ? 'and' : operator;
|
|
198
207
|
|
|
208
|
+
if (Array.isArray(resolvedValue) && resolvedValue.length) {
|
|
209
|
+
return resolvedValue.flatMap(i => [[ast.field, operatorToken, i], joinOperatorToken]).slice(0, -1);
|
|
199
210
|
}
|
|
200
211
|
|
|
201
|
-
return [ast.field,
|
|
212
|
+
return [ast.field, operatorToken, resolvedValue];
|
|
202
213
|
}
|
|
203
214
|
|
|
204
215
|
/**
|
|
@@ -221,7 +232,7 @@ function DevExpressConverter() {
|
|
|
221
232
|
}
|
|
222
233
|
|
|
223
234
|
// Special handling for ISNULL function
|
|
224
|
-
if (val
|
|
235
|
+
if (isFunctionNullCheck(val)) {
|
|
225
236
|
return convertValue(val.args[0]);
|
|
226
237
|
}
|
|
227
238
|
|
|
@@ -256,6 +267,18 @@ function DevExpressConverter() {
|
|
|
256
267
|
return node?.type === "function" && node.name === "ISNULL" && valueNode?.type === "value";
|
|
257
268
|
}
|
|
258
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Checks if a node is a ISNULL function without value
|
|
272
|
+
* @param {Object} node
|
|
273
|
+
* @returns {boolean} True if this is an ISNULL check.
|
|
274
|
+
*/
|
|
275
|
+
function isFunctionNullCheck(node, isPlaceholderCheck = false) {
|
|
276
|
+
const isValidFunction = node?.type === "function" && node?.name === "ISNULL" && node?.args?.length >= 2;
|
|
277
|
+
|
|
278
|
+
return isPlaceholderCheck ? isValidFunction && node?.args[0]?.value?.type !== "placeholder" : isValidFunction;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
259
282
|
/**
|
|
260
283
|
* Determines whether the logical tree should be flattened.
|
|
261
284
|
* This is based on the parent operator and the current operator.
|
package/src/core/parser.js
CHANGED
|
@@ -208,8 +208,10 @@ export function parse(input, variables = []) {
|
|
|
208
208
|
return { type: "placeholder", value: val };
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
operatorToken = operatorToken.toUpperCase();
|
|
212
|
+
|
|
211
213
|
// Handle IN operator which requires a list of values
|
|
212
|
-
if (operatorToken && operatorToken
|
|
214
|
+
if (operatorToken && (operatorToken === "IN" || operatorToken === "NOT IN")) return parseInList(token);
|
|
213
215
|
|
|
214
216
|
throw new Error(`Unexpected value: ${token.value}`);
|
|
215
217
|
}
|
package/src/core/tokenizer.js
CHANGED
|
@@ -6,7 +6,7 @@ const tokenPatterns = {
|
|
|
6
6
|
number: "\\d+", // Matches numerical values
|
|
7
7
|
placeholder: "'?\\{[^}]+\\}'?", // Matches placeholders like {variable} or '{variable}'
|
|
8
8
|
string: "'(?:''|[^'])*'", // Matches strings, allowing for escaped single quotes ('')
|
|
9
|
-
operator: "=>|<=|!=|>=|=|<>|>|<|\\bAND\\b|\\bOR\\b|\\bBETWEEN\\b|\\bIN\\b|\\bLIKE\\b|\\bIS NOT\\b|\\bNOT LIKE\\b|\\bIS\\b", // Matches SQL operators and logical keywords
|
|
9
|
+
operator: "=>|<=|!=|>=|=|<>|>|<|\\bAND\\b|\\bOR\\b|\\bBETWEEN\\b|\\bIN\\b|\\bNOT IN\\b|\\bLIKE\\b|\\bIS NOT\\b|\\bNOT LIKE\\b|\\bIS\\b", // Matches SQL operators and logical keywords
|
|
10
10
|
identifier: "[\\w.]+", // Matches identifiers, including table.column format
|
|
11
11
|
paren: "[()]", // Matches parentheses
|
|
12
12
|
comma: "," // Matches commas
|
|
@@ -45,18 +45,18 @@ class Tokenizer {
|
|
|
45
45
|
let value = match.groups[type];
|
|
46
46
|
|
|
47
47
|
// Remove surrounding single quotes from placeholders
|
|
48
|
-
if (type === "placeholder") value = value.replace(/^['"]|['"]$/g, "");
|
|
48
|
+
if (type === "placeholder") value = value.replace(/^['"]|['"]$/g, "").replace(" ","");
|
|
49
49
|
|
|
50
50
|
if (type === "operator") {
|
|
51
51
|
const lowerValue = value.toLowerCase();
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
if (lowerValue === "is") {
|
|
54
54
|
value = "=";
|
|
55
55
|
} else if (lowerValue === "is not") {
|
|
56
56
|
value = "!=";
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
return { type, value };
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -66,7 +66,7 @@ class Tokenizer {
|
|
|
66
66
|
|
|
67
67
|
peekNextToken() {
|
|
68
68
|
if (this.index >= this.input.length) return null;
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
const savedIndex = this.index; // Save current index
|
|
71
71
|
try {
|
|
72
72
|
return this.nextToken(); // Get next token
|
|
@@ -78,7 +78,7 @@ class Tokenizer {
|
|
|
78
78
|
reset() {
|
|
79
79
|
this.index = 0; // Reset index to the beginning of the input
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export { Tokenizer };
|
package/src/index.js
CHANGED
|
@@ -5,12 +5,12 @@ import { sanitizeQuery } from "./core/sanitizer.js";
|
|
|
5
5
|
|
|
6
6
|
export function convertSQLToAst(filterString, enableConsoleLogs = false) {
|
|
7
7
|
let { sanitizedSQL, extractedVariables } = sanitizeQuery(filterString);
|
|
8
|
-
enableConsoleLogs && console.log("Sanitized SQL:", sanitizedSQL, "\n");
|
|
8
|
+
!!enableConsoleLogs && console.log("Sanitized SQL:", sanitizedSQL, "\n");
|
|
9
9
|
|
|
10
10
|
const parsedResult = parse(sanitizedSQL, extractedVariables);
|
|
11
11
|
|
|
12
|
-
enableConsoleLogs && console.log("Extracted Variables:", JSON.stringify(parsedResult.variables, null, 2), "\n");
|
|
13
|
-
enableConsoleLogs && console.log("AST Tree:", JSON.stringify(parsedResult.ast, null, 2), "\n");
|
|
12
|
+
!!enableConsoleLogs && console.log("Extracted Variables:", JSON.stringify(parsedResult.variables, null, 2), "\n");
|
|
13
|
+
!!enableConsoleLogs && console.log("AST Tree:", JSON.stringify(parsedResult.ast, null, 2), "\n");
|
|
14
14
|
|
|
15
15
|
return parsedResult;
|
|
16
16
|
}
|
package/tests/parser.test.js
CHANGED
|
@@ -37,7 +37,7 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
37
37
|
{
|
|
38
38
|
input: "ID IN ({WorkOrderLine.ApplicableUoms}) AND (CompanyID = {WorkOrderDocument.CompanyID} OR {WorkOrderDocument.CompanyID} = 0)",
|
|
39
39
|
expected: [
|
|
40
|
-
[["ID", "=", "UOM1"]
|
|
40
|
+
[["ID", "=", "UOM1"], 'or', ["ID", "=", "UOM2"], 'or', ["ID", "=", "UOM3"]],
|
|
41
41
|
"and",
|
|
42
42
|
// [
|
|
43
43
|
["CompanyID", "=", 42],
|
|
@@ -145,12 +145,44 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
145
145
|
expected: [
|
|
146
146
|
["SourceID", "=", 2],
|
|
147
147
|
"or",
|
|
148
|
-
["SourceID", "=",
|
|
148
|
+
["SourceID", "=", null],
|
|
149
|
+
"or",
|
|
150
|
+
["SourceID", "=", 0],
|
|
151
|
+
"or",
|
|
152
|
+
["SourceID", "=", null]
|
|
149
153
|
]
|
|
150
154
|
},
|
|
151
155
|
{
|
|
152
156
|
input: "CompanyID = CompanyID2 = {AccountingRule.CompanyID}",
|
|
153
157
|
expected: "Error: Invalid comparison: CompanyID = CompanyID2",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
input: "(CompanyID = {LeadDocument.CompanyID} OR ISNULL(CompanyID,0) = 0) AND (ISNULL(IsSubdealer,0) = {LeadDocument.AllowSubDealer})",
|
|
161
|
+
expected: [
|
|
162
|
+
[
|
|
163
|
+
["CompanyID", "=", 7],
|
|
164
|
+
"or",
|
|
165
|
+
[
|
|
166
|
+
["CompanyID", "=", 0],
|
|
167
|
+
"or",
|
|
168
|
+
["CompanyID", "=", null]
|
|
169
|
+
]
|
|
170
|
+
],
|
|
171
|
+
"and",
|
|
172
|
+
[
|
|
173
|
+
["IsSubdealer", "=", true],
|
|
174
|
+
"or",
|
|
175
|
+
["IsSubdealer", "=", null]
|
|
176
|
+
]
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
input: 'AddressType NOT IN (2, 4)',
|
|
181
|
+
expected: [
|
|
182
|
+
["AddressType", "!=", 2],
|
|
183
|
+
"and",
|
|
184
|
+
["AddressType", "!=", 4]
|
|
185
|
+
]
|
|
154
186
|
}
|
|
155
187
|
];
|
|
156
188
|
|
|
@@ -165,7 +197,7 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
165
197
|
try {
|
|
166
198
|
astwithVariables = convertSQLToAst(input);
|
|
167
199
|
} catch (error) {
|
|
168
|
-
expect(error.message).toEqual(expected.replace("Error: ",""));
|
|
200
|
+
expect(error.message).toEqual(expected.replace("Error: ", ""));
|
|
169
201
|
return;
|
|
170
202
|
}
|
|
171
203
|
|
|
@@ -186,7 +218,7 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
186
218
|
|
|
187
219
|
expect(result).toEqual(expected);
|
|
188
220
|
});
|
|
189
|
-
|
|
221
|
+
});
|
|
190
222
|
});
|
|
191
223
|
|
|
192
224
|
|
|
@@ -213,5 +245,6 @@ const sampleData = {
|
|
|
213
245
|
"TransferOutwardDocument.CompanyID": 7,
|
|
214
246
|
"LeadDocument.BranchID": 42,
|
|
215
247
|
"LeadDocument.CompanyID": 7,
|
|
216
|
-
"ServiceOrderDocument.SourceID": 2
|
|
248
|
+
"ServiceOrderDocument.SourceID": 2,
|
|
249
|
+
"LeadDocument.AllowSubDealer": true
|
|
217
250
|
};
|