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 CHANGED
@@ -1,29 +1,142 @@
1
1
  # SQLParser
2
2
 
3
- SQLParser is a JavaScript library that converts SQL-like filter strings into DevExpress format filters. It provides utilities for parsing, sanitizing, and converting SQL-like expressions into a format that can be used with DevExpress components.
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
- ## Usage
5
+ ## Features
6
6
 
7
- ### Convert SQL to AST
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
- To convert a SQL-like filter string to an Abstract Syntax Tree (AST):
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
- const filterString= "(ID <> {Item.ID}) AND (ItemGroupType IN ({Item.AllowedItemGroupType}))";
13
- const parsedResult = convertSQLToAst(filterString);
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
- ### Convert AST to DevExpress Format
58
+ #### Extracted Variables
17
59
 
18
- To convert an AST to DevExpress format:
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
- const ast = { /* your AST here */ };
22
- const variables = [/* your variables here */];
23
- const state = { /* your state here */ };
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
- const devExpressFilter = convertAstToDevextreme(ast, variables, state);
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
- console.log(devExpressFilter);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlparser-devexpress",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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'];
@@ -1,31 +1,25 @@
1
- const logicalOperators = ['and', 'or'];
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, vars, ResultObject = null) {
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 && logicalOperators.includes(left[1])) parentOperator = left[1];
115
- if (right.length === 3 && logicalOperators.includes(right[1])) parentOperator = right[1];
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} primaryEntity - Optional primary entity name
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, variables, resultObject = null }) {
387
- return devExpressConverter.init(ast, variables, resultObject);
378
+ export function convertToDevExpressFormat({ ast, resultObject = null, enableShortCircuit = true }) {
379
+ return devExpressConverter.init(ast, resultObject,enableShortCircuit);
388
380
  }
@@ -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
 
@@ -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 numerical values
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.]+", // Matches identifiers, including table.column format
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
- // // const devexpress = parseFilterString("(RS2ID in ({LeadStatementGlobalRpt.StateID}) Or ({LeadStatementGlobalRpt.StateID} =0)) And (RS3ID in (0,{LeadStatementGlobalRpt.RegionID}) Or {LeadStatementGlobalRpt.RegionID} =0 )", sampleData);
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
- !!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
-
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
 
@@ -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