Skip to content

Commit 8e14faa

Browse files
authored
Merge pull request #17 from RohitM-IN/development
Development
2 parents 4e2ba84 + 5528fad commit 8e14faa

11 files changed

Lines changed: 267 additions & 65 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sqlparser-devexpress",
3-
"version": "2.3.10",
3+
"version": "2.3.16",
44
"main": "src/index.js",
55
"type": "module",
66
"scripts": {

src/@types/core/converter.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ASTNode } from "./parser.js";
2+
3+
export interface ResultObject {
4+
[key: string]: any;
5+
}
6+
7+
export type DevExpressFilter = any[] | null;
8+
9+
export interface ConvertOptions {
10+
ast: ASTNode;
11+
resultObject?: ResultObject;
12+
enableShortCircuit?: boolean;
13+
}
14+
15+
/**
16+
* Converts an abstract syntax tree (AST) to DevExpress filter format.
17+
* This function uses short-circuit evaluation for optimization.
18+
*
19+
* @param options - The conversion options containing AST, result object, and short-circuit flag.
20+
* @returns DevExpressFilter - The DevExpress compatible filter array or null.
21+
*/
22+
export function convertToDevExpressFormat(options: ConvertOptions): DevExpressFilter;

src/@types/core/parser.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export interface ASTNode {
2+
type: string;
3+
operator?: string;
4+
field?: string;
5+
value?: any;
6+
left?: ASTNode;
7+
right?: ASTNode;
8+
args?: ASTNode[];
9+
name?: string;
10+
}
11+
12+
/**
13+
* Represents the result of the parse function.
14+
*/
15+
export interface ParseResult {
16+
ast: ASTNode;
17+
variables: string[];
18+
}
19+
20+
/**
21+
* The main parse function that converts SQL-like queries into AST.
22+
*
23+
* @param input - The SQL-like string to be parsed.
24+
* @param variables - The list of extracted variables during parsing.
25+
* @returns ParseResult - The resulting AST and extracted variables.
26+
*/
27+
export function parse(input: string, variables?: string[]): ParseResult;

src/@types/core/sanitizer.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Represents the result of sanitizing the SQL query.
3+
*/
4+
export interface SanitizeResult {
5+
sanitizedSQL: string;
6+
variables: string[];
7+
}
8+
9+
/**
10+
* Sanitizes the SQL query by replacing placeholders or pipe-separated variables
11+
* with standardized `{placeholder}` format and extracts all variable names.
12+
*
13+
* Example Input:
14+
* ```
15+
* SELECT * FROM Orders WHERE CustomerID = {0} | [CustomerID]
16+
* ```
17+
*
18+
* Output:
19+
* ```
20+
* {
21+
* sanitizedSQL: "SELECT * FROM Orders WHERE CustomerID = {CustomerID}",
22+
* variables: ["CustomerID"]
23+
* }
24+
*
25+
* @param sql - The raw SQL query containing placeholders or pipes.
26+
* @returns SanitizeResult - The cleaned SQL query and extracted variables.
27+
*/
28+
export function sanitizeQuery(sql: string): SanitizeResult;

src/@types/core/tokenizer.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Represents a single token from the tokenizer.
3+
*/
4+
export interface Token {
5+
type: string;
6+
value: string;
7+
dataType?: string;
8+
}

src/@types/default.d.ts

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,41 @@
1-
export type StateDataObject = Record<string, any>;
2-
3-
export interface SanitizedQuery {
4-
sanitizedSQL: string;
5-
extractedVariables: string[];
6-
}
7-
8-
export interface ParsedResult {
9-
ast: any; // Define a more specific type if possible
10-
variables: string[];
11-
}
12-
13-
export interface ConvertToDevExpressFormatParams {
14-
ast: any; // Define a more specific type if possible
15-
resultObject?: StateDataObject | null;
16-
enableShortCircuit?: boolean;
17-
}
18-
19-
export function sanitizeQuery(filterString: string): SanitizedQuery;
20-
21-
export function parse(query: string, variables: string[]): ParsedResult;
22-
23-
export function convertToDevExpressFormat(params: ConvertToDevExpressFormatParams): any;
24-
25-
export function convertSQLToAst(
26-
filterString: string,
27-
SampleData?: StateDataObject | null,
28-
enableConsoleLogs?: boolean
29-
): ParsedResult;
30-
1+
import { DevExpressFilter, ResultObject } from "./core/converter";
2+
import { ASTNode, ParseResult } from "./core/parser";
3+
4+
/**
5+
* Converts an SQL-like filter string into an Abstract Syntax Tree (AST).
6+
* It also extracts variables from placeholders like `{CustomerID}` or pipe-separated sections.
7+
* Optionally logs the conversion process if `enableConsoleLogs` is `true`.
8+
*
9+
* Example:
10+
* ```
11+
* const { ast, variables } = convertSQLToAst("ID = {CustomerID} AND Status = {OrderStatus}");
12+
* console.log(ast);
13+
* console.log(variables);
14+
* ```
15+
*
16+
* @param filterString - The raw SQL-like filter string.
17+
* @param enableConsoleLogs - Whether to log the parsing and sanitization process.
18+
* @returns ParseResult - The AST and extracted variables.
19+
*/
20+
export function convertSQLToAst(filterString: string, enableConsoleLogs?: boolean): ParseResult;
21+
22+
/**
23+
* Converts an Abstract Syntax Tree (AST) into a DevExpress-compatible filter format.
24+
* Optionally supports a result object for dynamic value resolution and short-circuit evaluation.
25+
*
26+
* Example:
27+
* ```
28+
* const filter = convertAstToDevextreme(ast, state, true);
29+
* console.log(filter);
30+
* ```
31+
*
32+
* @param ast - The parsed AST from `convertSQLToAst`.
33+
* @param state - An optional result object to resolve placeholders to actual values.
34+
* @param enableShortCircuit - Whether to apply short-circuit evaluation.
35+
* @returns DevExpressFilter - The DevExpress-compatible filter array or null.
36+
*/
3137
export function convertAstToDevextreme(
32-
ast: any, // Define a more specific type if possible
33-
state?: StateDataObject | null,
38+
ast: ASTNode,
39+
state?: ResultObject | null,
3440
enableShortCircuit?: boolean,
35-
): any;
41+
): DevExpressFilter;

src/constants.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ export const OPERATOR_PRECEDENCE = {
1010
// Regular expression to check for unsupported SQL patterns (like SELECT-FROM or JOIN statements)
1111
export const UNSUPPORTED_PATTERN = /\bSELECT\b.*\bFROM\b|\bINNER\s+JOIN\b/i;
1212

13-
export const LOGICAL_OPERATORS = ['and', 'or'];
13+
export const LOGICAL_OPERATORS = ['and', 'or'];
14+
15+
export const LITERAL_TYPES = ["value", "placeholder"];

src/core/converter.js

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LOGICAL_OPERATORS } from "../constants.js";
1+
import { LITERAL_TYPES, LOGICAL_OPERATORS } from "../constants.js";
22

33
/**
44
* Main conversion function that sets up the global context
@@ -48,7 +48,7 @@ function DevExpressConverter() {
4848
return handleFunction(ast);
4949
case "field":
5050
case "value":
51-
return convertValue(ast.value);
51+
return convertValue(ast.value, parentOperator);
5252
default:
5353
return null;
5454
}
@@ -113,6 +113,7 @@ function DevExpressConverter() {
113113
if (shouldFlattenLogicalTree(parentOperator, operator, ast)) {
114114
return flattenLogicalTree(left, operator, right);
115115
}
116+
116117
return [left, operator, right];
117118
}
118119

@@ -123,10 +124,11 @@ function DevExpressConverter() {
123124
*/
124125
function handleComparisonOperator(ast) {
125126
const operator = ast.operator.toUpperCase();
127+
const originalOperator = ast.originalOperator?.toUpperCase();
126128

127129
// Handle "IS NULL" condition
128-
if (operator === "IS" && ast.value === null) {
129-
return [ast.field, "=", null];
130+
if ((operator === "IS" || originalOperator === "IS") && ast.value === null) {
131+
return [ast.field, "=", null, { type: originalOperator }, null];
130132
}
131133

132134
// Handle "IN" condition, including comma-separated values
@@ -139,14 +141,21 @@ function DevExpressConverter() {
139141
const right = ast.right !== undefined ? processAstNode(ast.right) : convertValue(ast.value);
140142
const rightDefault = ast.right?.args[1]?.value;
141143
let operatorToken = ast.operator.toLowerCase();
144+
let includeExtradata = false;
142145

143146
if (operatorToken === "like") {
144147
operatorToken = "contains";
145148
} else if (operatorToken === "not like") {
146149
operatorToken = "notcontains";
150+
} else if (operatorToken === "=" && originalOperator === "IS") {
151+
includeExtradata = true
152+
} else if (operatorToken == "!=" && originalOperator === "IS NOT") {
153+
operatorToken = "!=";
154+
includeExtradata = true;
147155
}
148-
149156
let comparison = [left, operatorToken, right];
157+
if (includeExtradata)
158+
comparison = [left, operatorToken, right, { type: originalOperator }, right];
150159

151160
// Last null because of special case when using dropdown it https://github.com/DevExpress/DevExtreme/blob/25_1/packages/devextreme/js/__internal/data/m_utils.ts#L18 it takes last value as null
152161
if ((ast.left && isFunctionNullCheck(ast.left, true)) || (ast.value && isFunctionNullCheck(ast.value, false))) {
@@ -205,22 +214,51 @@ function DevExpressConverter() {
205214
resolvedValue = resolvedValue.split(',').map(v => v.trim());
206215
}
207216

217+
// handle short circuit evaluation for IN operator
218+
if (EnableShortCircuit && (LITERAL_TYPES.includes(ast.field?.type) && LITERAL_TYPES.includes(ast.value?.type))) {
219+
const fieldVal = convertValue(ast.field);
220+
if (Array.isArray(resolvedValue)) {
221+
// normalize numeric strings if LHS is number
222+
const list = resolvedValue.map(x =>
223+
(typeof x === "string" && !isNaN(x) && typeof fieldVal === "number")
224+
? Number(x)
225+
: x
226+
);
227+
228+
if (operator === "IN")
229+
return list.includes(fieldVal);
230+
else if (operator === "NOT IN")
231+
return !list.includes(fieldVal);
232+
} else if (!Array.isArray(resolvedValue)) {
233+
// normalize numeric strings if LHS is number
234+
const value = (typeof resolvedValue === "string" && !isNaN(resolvedValue) && typeof fieldVal === "number")
235+
? Number(resolvedValue)
236+
: resolvedValue;
237+
238+
if (operator === "IN")
239+
return fieldVal == value;
240+
else if (operator === "NOT IN")
241+
return fieldVal != value;
242+
}
243+
}
244+
208245
let operatorToken = operator === "IN" ? '=' : operator === "NOT IN" ? '!=' : operator;
209246
let joinOperatorToken = operator === "IN" ? 'or' : operator === "NOT IN" ? 'and' : operator;
210-
247+
let field = convertValue(ast.field);
211248
if (Array.isArray(resolvedValue) && resolvedValue.length) {
212-
return resolvedValue.flatMap(i => [[ast.field, operatorToken, i], joinOperatorToken]).slice(0, -1);
249+
return resolvedValue.flatMap(i => [[field, operatorToken, i], joinOperatorToken]).slice(0, -1);
213250
}
214251

215-
return [ast.field, operatorToken, resolvedValue];
252+
return [field, operatorToken, resolvedValue];
216253
}
217254

218255
/**
219256
* Converts a single value, resolving placeholders and handling special cases.
220257
* @param {*} val - The value to convert.
258+
* @param {string} parentOperator - The operator of the parent logical node (if any).
221259
* @returns {*} Converted value.
222260
*/
223-
function convertValue(val) {
261+
function convertValue(val, parentOperator = null) {
224262
if (val === null) return null;
225263

226264
// Handle array values
@@ -251,6 +289,10 @@ function DevExpressConverter() {
251289
}
252290
}
253291

292+
if (parentOperator && parentOperator.toUpperCase() === "IN" && typeof val === "string") {
293+
return val.split(',').map(v => v.trim());
294+
}
295+
254296
return val;
255297
}
256298

@@ -368,6 +410,10 @@ function DevExpressConverter() {
368410

369411
if ((left !== null && isNaN(left)) || (right !== null && isNaN(right))) return null;
370412

413+
// Handle NULL == 0 OR NULL == "" cases
414+
if (left === null && (right == 0 || right == "")) return true;
415+
if (right === null && (left == 0 || left == "")) return true;
416+
371417
if (left === null || right === null) {
372418
if (operator === '=' || operator === '==') return left === right;
373419
if (operator === '<>' || operator === '!=') return left !== right;

0 commit comments

Comments
 (0)