sqlparser-devexpress 2.1.3 → 2.2.0
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/README.md +126 -13
- package/package.json +1 -1
- package/src/constants.js +13 -0
- package/src/core/converter.js +9 -17
- package/src/core/parser.js +1 -10
- package/src/core/tokenizer.js +17 -6
- package/src/debug.js +4 -6
- package/src/index.js +5 -4
- package/tests/parser.test.js +8 -0
package/README.md
CHANGED
|
@@ -1,29 +1,142 @@
|
|
|
1
1
|
# SQLParser
|
|
2
2
|
|
|
3
|
-
SQLParser is a JavaScript library that converts SQL
|
|
3
|
+
SQLParser is a JavaScript library that converts SQL `WHERE` clauses into a structured **Abstract Syntax Tree (AST)** and transforms them into DevExpress filter format. It removes inline parameters while preserving them as dynamic variables for flexible query processing.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **AST-Based Query Processing**: Parses `WHERE` clauses and generates a structured AST.
|
|
8
|
+
- **Supports Dynamic Parameters**: Identifies and extracts placeholders (`{param}`) for dynamic resolution.
|
|
9
|
+
- **Parameter Cleanup**: Removes inline parameters while maintaining their structure.
|
|
10
|
+
- **DevExpress-Compatible Output**: Converts parsed SQL conditions into the DevExpress filter format.
|
|
11
|
+
- **Short-Circuit Optimization**: By default, eliminates `value = value` expressions for DevExpress compatibility (can be disabled for performance optimization).
|
|
12
|
+
- **Separation of Concerns**: Generate AST once, then use it for multiple state updates.
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
## Example Workflow
|
|
15
|
+
|
|
16
|
+
### **Step 1: Input SQL**
|
|
17
|
+
|
|
18
|
+
```sql
|
|
19
|
+
WHERE OrderID = {CustomerOrders.OrderID} AND Status IN (1, 3)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### **Step 2: Generate AST**
|
|
10
23
|
|
|
11
24
|
```javascript
|
|
12
|
-
|
|
13
|
-
|
|
25
|
+
import { convertSQLToAst } from "sqlparser-devexpress";
|
|
26
|
+
|
|
27
|
+
const sqlQuery = "OrderID = {CustomerOrders.OrderID} AND Status IN (1, 3)";
|
|
28
|
+
const { ast, variables } = convertSQLToAst(sqlQuery, true); // Enable logs
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
#### **AST Output**
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"type": "logical",
|
|
36
|
+
"operator": "AND",
|
|
37
|
+
"left": {
|
|
38
|
+
"type": "comparison",
|
|
39
|
+
"field": "OrderID",
|
|
40
|
+
"operator": "=",
|
|
41
|
+
"value": {
|
|
42
|
+
"type": "placeholder",
|
|
43
|
+
"value": "CustomerOrders.OrderID"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"right": {
|
|
47
|
+
"type": "comparison",
|
|
48
|
+
"field": "Status",
|
|
49
|
+
"operator": "in",
|
|
50
|
+
"value": {
|
|
51
|
+
"type": "value",
|
|
52
|
+
"value": [1, 3]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
14
56
|
```
|
|
15
57
|
|
|
16
|
-
|
|
58
|
+
#### Extracted Variables
|
|
17
59
|
|
|
18
|
-
|
|
60
|
+
The parser identifies placeholders within the SQL query and extracts them for dynamic value resolution.
|
|
61
|
+
|
|
62
|
+
#### **Example Output:**
|
|
63
|
+
```json
|
|
64
|
+
[
|
|
65
|
+
"CustomerOrders.OrderID"
|
|
66
|
+
]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
These extracted variables can be used to fetch the corresponding state values in the application. You can store them in a `Record<string, any>`, where the key is the placeholder name, and the value is the resolved data from the application's state.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
### **Step 3: Convert AST to DevExpress Format**
|
|
19
74
|
|
|
20
75
|
```javascript
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
76
|
+
import { convertAstToDevextreme } from "sqlparser-devexpress";
|
|
77
|
+
|
|
78
|
+
const sampleState = {
|
|
79
|
+
"CustomerOrders.OrderID": 76548
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const devexpressFilter = convertAstToDevextreme(ast, sampleState, true); // Short-circuit enabled (default)
|
|
24
83
|
|
|
25
|
-
|
|
84
|
+
console.log("DevExpress Filter:", JSON.stringify(devexpressFilter, null, 2));
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### **DevExpress Filter Output**
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
[
|
|
91
|
+
["OrderID", "=", 76548],
|
|
92
|
+
"and",
|
|
93
|
+
[
|
|
94
|
+
["Status", "=", 1],
|
|
95
|
+
"or",
|
|
96
|
+
["Status", "=", 3]
|
|
97
|
+
]
|
|
98
|
+
]
|
|
99
|
+
```
|
|
26
100
|
|
|
27
|
-
|
|
101
|
+
## Installation
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
npm install sqlparser-devexpress
|
|
28
105
|
```
|
|
29
106
|
|
|
107
|
+
## API Reference
|
|
108
|
+
|
|
109
|
+
### `convertSQLToAst(filterString, enableConsoleLogs = false)`
|
|
110
|
+
|
|
111
|
+
- **Input:** SQL `WHERE` clause as a string.
|
|
112
|
+
- **Output:** An object `{ ast }` where:
|
|
113
|
+
- `ast`: The parsed Abstract Syntax Tree.
|
|
114
|
+
- **Example:**
|
|
115
|
+
```javascript
|
|
116
|
+
const { ast } = convertSQLToAst("OrderID = {CustomerOrders.OrderID} AND Status IN (1, 3)");
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `convertAstToDevextreme(ast, state, shortCircuit = true)`
|
|
120
|
+
|
|
121
|
+
- **Input:**
|
|
122
|
+
- `ast`: The AST generated from `convertSQLToAst()`.
|
|
123
|
+
- `state`: An object containing values for placeholders.
|
|
124
|
+
- `shortCircuit`: (Optional, default `true`) Enables short-circuiting of `value = value` expressions for DevExpress compatibility.
|
|
125
|
+
- **Output:** DevExpress filter array.
|
|
126
|
+
- **Example:**
|
|
127
|
+
```javascript
|
|
128
|
+
const devexpressFilter = convertAstToDevextreme(ast, sampleState, false); // Disables short-circuiting
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Roadmap
|
|
132
|
+
|
|
133
|
+
- Support for additional SQL operators and functions.
|
|
134
|
+
- Improved error handling and validation.
|
|
135
|
+
|
|
136
|
+
## Contributing
|
|
137
|
+
|
|
138
|
+
Contributions are welcome! Feel free to open issues or submit pull requests.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
package/package.json
CHANGED
package/src/constants.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//
|
|
2
|
+
export const LITERALS = ["number", "string", "null"];
|
|
3
|
+
|
|
4
|
+
// Define operator precedence for parsing expressions
|
|
5
|
+
export const OPERATOR_PRECEDENCE = {
|
|
6
|
+
"OR": 1, "AND": 2, "=": 3, "!=": 3, ">": 3, "<": 3, ">=": 3, "<=": 3,
|
|
7
|
+
"IN": 3, "<>": 3, "LIKE": 3, "IS": 3, "BETWEEN": 3
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Regular expression to check for unsupported SQL patterns (like SELECT-FROM or JOIN statements)
|
|
11
|
+
export const UNSUPPORTED_PATTERN = /\bSELECT\b.*\bFROM\b|\bINNER\s+JOIN\b/i;
|
|
12
|
+
|
|
13
|
+
export const LOGICAL_OPERATORS = ['and', 'or'];
|
package/src/core/converter.js
CHANGED
|
@@ -1,31 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
import { LOGICAL_OPERATORS } from "../constants.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Main conversion function that sets up the global context
|
|
5
|
-
* @param {Object} ast - The abstract syntax tree
|
|
6
|
-
* @param {Array} vars - Array of variable names
|
|
7
|
-
* @param {Object} results - Optional object for placeholder resolution
|
|
8
5
|
* @returns {Array|null} DevExpress format filter
|
|
9
6
|
*/
|
|
10
7
|
function DevExpressConverter() {
|
|
11
8
|
// Global variables accessible throughout the converter
|
|
12
9
|
let resultObject = null;
|
|
13
|
-
let primaryEntity = null;
|
|
14
|
-
let primaryKey = null;
|
|
15
|
-
let variables = [];
|
|
16
10
|
const EnableShortCircuit = true;
|
|
17
11
|
|
|
18
12
|
/**
|
|
19
13
|
* Main conversion function that sets up the global context
|
|
20
14
|
* @param {Object} ast - The abstract syntax tree
|
|
21
|
-
* @param {Array} vars - Array of variable names
|
|
22
15
|
* @param {Object} ResultObject - Optional object for placeholder resolution
|
|
16
|
+
* @param {boolean} enableShortCircuit - Optional enabling and disabling the shortcircuit ie evaluating value = value scenario
|
|
23
17
|
* @returns {Array|null} DevExpress format filter
|
|
24
18
|
*/
|
|
25
|
-
function convert(ast,
|
|
19
|
+
function convert(ast, ResultObject = null, enableShortCircuit = true) {
|
|
26
20
|
// Set up global context
|
|
27
|
-
variables = vars;
|
|
28
21
|
resultObject = ResultObject;
|
|
22
|
+
EnableShortCircuit = enableShortCircuit;
|
|
29
23
|
|
|
30
24
|
// Process the AST
|
|
31
25
|
let result = processAstNode(ast);
|
|
@@ -111,8 +105,8 @@ function DevExpressConverter() {
|
|
|
111
105
|
|
|
112
106
|
// Detect and flatten nested logical expressions
|
|
113
107
|
if (parentOperator === null) {
|
|
114
|
-
if (left.length === 3 &&
|
|
115
|
-
if (right.length === 3 &&
|
|
108
|
+
if (left.length === 3 && LOGICAL_OPERATORS.includes(left[1])) parentOperator = left[1];
|
|
109
|
+
if (right.length === 3 && LOGICAL_OPERATORS.includes(right[1])) parentOperator = right[1];
|
|
116
110
|
}
|
|
117
111
|
|
|
118
112
|
// Flatten nested logical expressions if applicable
|
|
@@ -377,12 +371,10 @@ const devExpressConverter = DevExpressConverter();
|
|
|
377
371
|
/**
|
|
378
372
|
* Converts an abstract syntax tree to DevExpress format
|
|
379
373
|
* @param {Object} ast - The abstract syntax tree
|
|
380
|
-
* @param {Array} variables - Array of variable names
|
|
381
374
|
* @param {Object} resultObject - Optional object for placeholder resolution
|
|
382
|
-
* @param {string}
|
|
383
|
-
* @param {string} primaryKey - Optional primary key value
|
|
375
|
+
* @param {string} enableShortCircuit - Optional enabling and disabling the shortcircuit ie evaluating value = value scenario
|
|
384
376
|
* @returns {Array|null} DevExpress format filter
|
|
385
377
|
*/
|
|
386
|
-
export function convertToDevExpressFormat({ ast,
|
|
387
|
-
return devExpressConverter.init(ast,
|
|
378
|
+
export function convertToDevExpressFormat({ ast, resultObject = null, enableShortCircuit = true }) {
|
|
379
|
+
return devExpressConverter.init(ast, resultObject,enableShortCircuit);
|
|
388
380
|
}
|
package/src/core/parser.js
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
|
+
import { LITERALS, OPERATOR_PRECEDENCE, UNSUPPORTED_PATTERN } from "../constants.js";
|
|
1
2
|
import { Tokenizer } from "./tokenizer.js";
|
|
2
3
|
|
|
3
|
-
// Define operator precedence for parsing expressions
|
|
4
|
-
const OPERATOR_PRECEDENCE = {
|
|
5
|
-
"OR": 1, "AND": 2, "=": 3, "!=": 3, ">": 3, "<": 3, ">=": 3, "<=": 3,
|
|
6
|
-
"IN": 3, "<>": 3, "LIKE": 3, "IS": 3, "BETWEEN": 3
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const LITERALS = ["number", "string", "null"];
|
|
10
|
-
|
|
11
|
-
// Regular expression to check for unsupported SQL patterns (like SELECT-FROM or JOIN statements)
|
|
12
|
-
const UNSUPPORTED_PATTERN = /\bSELECT\b.*\bFROM\b|\bINNER\s+JOIN\b/i;
|
|
13
4
|
|
|
14
5
|
export function parse(input, variables = []) {
|
|
15
6
|
|
package/src/core/tokenizer.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import { LITERALS } from "../constants.js";
|
|
2
|
+
|
|
1
3
|
// Define regex patterns for different token types
|
|
2
4
|
const tokenPatterns = {
|
|
3
5
|
whitespace: "\\s+", // Matches spaces, tabs, and newlines
|
|
4
6
|
function: "\\b(ISNULL)\\b", // Matches function names like ISNULL (case-insensitive)
|
|
5
|
-
null: "\\bNULL\\b", // Matches NULL as a keyword
|
|
6
|
-
number: "\\d+", // Matches
|
|
7
|
+
null: "\\bNULL\\b|\\(\\s*NULL\\s*\\)", // Matches NULL as a keyword
|
|
8
|
+
number: "\\(\\d+\\)|\\d+", // Matches numbers while stripping unnecessary parentheses
|
|
7
9
|
placeholder: "'?\\{[^}]+\\}'?", // Matches placeholders like {variable} or '{variable}'
|
|
8
|
-
string: "'(?:''|[^'])*'", // Matches strings, allowing for escaped single quotes ('')
|
|
10
|
+
string: "\\('\\w+\\'\\)|'(?:''|[^'])*'", // Matches strings, allowing for escaped single quotes ('')
|
|
9
11
|
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
|
-
identifier: "[\\w.]
|
|
11
|
-
paren: "[()]", // Matches parentheses
|
|
12
|
+
identifier: "[\\w.]+|\"[^\"]+\"|\\[[^\\]]+\\]", // Matches regular identifiers, quoted identifiers ("identifier"), and bracketed identifiers [identifier]
|
|
13
|
+
paren: "[()]", // Matches standalone parentheses
|
|
12
14
|
comma: "," // Matches commas
|
|
13
15
|
};
|
|
14
16
|
|
|
@@ -45,7 +47,7 @@ class Tokenizer {
|
|
|
45
47
|
let value = match.groups[type];
|
|
46
48
|
|
|
47
49
|
// Remove surrounding single quotes from placeholders
|
|
48
|
-
if (type === "placeholder") value = value.replace(/^['"]|['"]$/g, "").replace(" ","");
|
|
50
|
+
if (type === "placeholder") value = value.replace(/^['"]|['"]$/g, "").replace(" ", "");
|
|
49
51
|
|
|
50
52
|
if (type === "operator") {
|
|
51
53
|
const lowerValue = value.toLowerCase();
|
|
@@ -57,6 +59,15 @@ class Tokenizer {
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
if (LITERALS.includes(type)){
|
|
63
|
+
value = value.replace(/^[(]|[)]$/g, "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (type === "identifier") {
|
|
67
|
+
value = value.replace(/^["\[]|["\]]$/g, "");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
60
71
|
return { type, value };
|
|
61
72
|
}
|
|
62
73
|
|
package/src/debug.js
CHANGED
|
@@ -6,17 +6,15 @@
|
|
|
6
6
|
// import { convertToDevExpressFormat } from "./core/converter.js";
|
|
7
7
|
// import { parse } from "./core/parser.js";
|
|
8
8
|
// import { sanitizeQuery } from "./core/sanitizer.js";
|
|
9
|
-
// import { convertAstToDevextreme, convertSQLToAst } from "./index.js";
|
|
10
9
|
|
|
11
10
|
// const sampleData = {
|
|
12
11
|
// 'LeadStatementGlobalRpt.StateID': null,
|
|
13
12
|
// 'LeadStatementGlobalRpt.RegionID': null,
|
|
14
|
-
// 'ServiceOrderDocument.SourceID': 2
|
|
13
|
+
// 'ServiceOrderDocument.SourceID': 2,
|
|
14
|
+
// 'CustomerOrders.OrderID': 76548
|
|
15
15
|
// }
|
|
16
16
|
|
|
17
17
|
// export function parseFilterString(filterString, sampleData = null) {
|
|
18
|
-
// if (filterString.toUpperCase().startsWith("SELECT")) return null; // Skip full SQL queries
|
|
19
|
-
|
|
20
18
|
// let { sanitizedSQL, extractedVariables } = sanitizeQuery(filterString);
|
|
21
19
|
// console.log("Sanitized SQL:", sanitizedSQL, "\n");
|
|
22
20
|
|
|
@@ -30,9 +28,9 @@
|
|
|
30
28
|
// return convertToDevExpressFormat({ ast: astTree, variables: extractedVariables, resultObject: sampleData });
|
|
31
29
|
// }
|
|
32
30
|
|
|
33
|
-
//
|
|
34
|
-
// const devexpress = parseFilterString("{LeadStatementGlobalRpt.RegionID} =0", sampleData);
|
|
31
|
+
// const devexpress = parseFilterString("OrderID = {CustomerOrders.OrderID} AND Status IN (1, 3)", sampleData);
|
|
35
32
|
// console.log("DevExpress Filter:", JSON.stringify(devexpress, null, 2));
|
|
33
|
+
// // const devexpress = parseFilterString("(RS2ID in ({LeadStatementGlobalRpt.StateID}) Or ({LeadStatementGlobalRpt.StateID} =0)) And (RS3ID in (0,{LeadStatementGlobalRpt.RegionID}) Or {LeadStatementGlobalRpt.RegionID} =0 )", sampleData);
|
|
36
34
|
|
|
37
35
|
// // const devExpressFilter = convertSQLToAst("(RS2ID in ({LeadStatementGlobalRpt.StateID}) Or ({LeadStatementGlobalRpt.StateID} =0)) And (RS3ID in (0,{LeadStatementGlobalRpt.RegionID}) Or {LeadStatementGlobalRpt.RegionID} =0 ) ");
|
|
38
36
|
// // const devExpressFilterresult = convertAstToDevextreme(devExpressFilter.ast, devExpressFilter.variables, sampleData);
|
package/src/index.js
CHANGED
|
@@ -5,13 +5,14 @@ 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");
|
|
9
8
|
|
|
10
9
|
const parsedResult = parse(sanitizedSQL, extractedVariables);
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
if (enableConsoleLogs === true) {
|
|
12
|
+
console.log("Sanitized SQL:", sanitizedSQL, "\n");
|
|
13
|
+
console.log("Extracted Variables:", JSON.stringify(parsedResult.variables, null, 2), "\n");
|
|
14
|
+
console.log("AST Tree:", JSON.stringify(parsedResult.ast, null, 2), "\n");
|
|
15
|
+
}
|
|
15
16
|
return parsedResult;
|
|
16
17
|
}
|
|
17
18
|
|
package/tests/parser.test.js
CHANGED
|
@@ -183,6 +183,14 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
183
183
|
"and",
|
|
184
184
|
["AddressType", "!=", 4]
|
|
185
185
|
]
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
input: "AddressType IN ('2', ('4'))",
|
|
189
|
+
expected: [
|
|
190
|
+
["AddressType", "=", '2'],
|
|
191
|
+
"or",
|
|
192
|
+
["AddressType", "=", '4']
|
|
193
|
+
]
|
|
186
194
|
}
|
|
187
195
|
];
|
|
188
196
|
|