Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sqlparser-devexpress",
"version": "2.3.10",
"version": "2.3.16",
"main": "src/index.js",
"type": "module",
"scripts": {
Expand Down
22 changes: 22 additions & 0 deletions src/@types/core/converter.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ASTNode } from "./parser.js";

export interface ResultObject {
[key: string]: any;
}

export type DevExpressFilter = any[] | null;

export interface ConvertOptions {
ast: ASTNode;
resultObject?: ResultObject;
enableShortCircuit?: boolean;
}

/**
* Converts an abstract syntax tree (AST) to DevExpress filter format.
* This function uses short-circuit evaluation for optimization.
*
* @param options - The conversion options containing AST, result object, and short-circuit flag.
* @returns DevExpressFilter - The DevExpress compatible filter array or null.
*/
export function convertToDevExpressFormat(options: ConvertOptions): DevExpressFilter;
27 changes: 27 additions & 0 deletions src/@types/core/parser.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface ASTNode {
type: string;
operator?: string;
field?: string;
value?: any;
left?: ASTNode;
right?: ASTNode;
args?: ASTNode[];
name?: string;
}

/**
* Represents the result of the parse function.
*/
export interface ParseResult {
ast: ASTNode;
variables: string[];
}

/**
* The main parse function that converts SQL-like queries into AST.
*
* @param input - The SQL-like string to be parsed.
* @param variables - The list of extracted variables during parsing.
* @returns ParseResult - The resulting AST and extracted variables.
*/
export function parse(input: string, variables?: string[]): ParseResult;
28 changes: 28 additions & 0 deletions src/@types/core/sanitizer.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Represents the result of sanitizing the SQL query.
*/
export interface SanitizeResult {
sanitizedSQL: string;
variables: string[];
}

/**
* Sanitizes the SQL query by replacing placeholders or pipe-separated variables
* with standardized `{placeholder}` format and extracts all variable names.
*
* Example Input:
* ```
* SELECT * FROM Orders WHERE CustomerID = {0} | [CustomerID]
* ```
*
* Output:
* ```
* {
* sanitizedSQL: "SELECT * FROM Orders WHERE CustomerID = {CustomerID}",
* variables: ["CustomerID"]
* }
*
* @param sql - The raw SQL query containing placeholders or pipes.
* @returns SanitizeResult - The cleaned SQL query and extracted variables.
*/
export function sanitizeQuery(sql: string): SanitizeResult;
8 changes: 8 additions & 0 deletions src/@types/core/tokenizer.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Represents a single token from the tokenizer.
*/
export interface Token {
type: string;
value: string;
dataType?: string;
}
72 changes: 39 additions & 33 deletions src/@types/default.d.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,41 @@
export type StateDataObject = Record<string, any>;

export interface SanitizedQuery {
sanitizedSQL: string;
extractedVariables: string[];
}

export interface ParsedResult {
ast: any; // Define a more specific type if possible
variables: string[];
}

export interface ConvertToDevExpressFormatParams {
ast: any; // Define a more specific type if possible
resultObject?: StateDataObject | null;
enableShortCircuit?: boolean;
}

export function sanitizeQuery(filterString: string): SanitizedQuery;

export function parse(query: string, variables: string[]): ParsedResult;

export function convertToDevExpressFormat(params: ConvertToDevExpressFormatParams): any;

export function convertSQLToAst(
filterString: string,
SampleData?: StateDataObject | null,
enableConsoleLogs?: boolean
): ParsedResult;

import { DevExpressFilter, ResultObject } from "./core/converter";
import { ASTNode, ParseResult } from "./core/parser";

/**
* Converts an SQL-like filter string into an Abstract Syntax Tree (AST).
* It also extracts variables from placeholders like `{CustomerID}` or pipe-separated sections.
* Optionally logs the conversion process if `enableConsoleLogs` is `true`.
*
* Example:
* ```
* const { ast, variables } = convertSQLToAst("ID = {CustomerID} AND Status = {OrderStatus}");
* console.log(ast);
* console.log(variables);
* ```
*
* @param filterString - The raw SQL-like filter string.
* @param enableConsoleLogs - Whether to log the parsing and sanitization process.
* @returns ParseResult - The AST and extracted variables.
*/
export function convertSQLToAst(filterString: string, enableConsoleLogs?: boolean): ParseResult;

/**
* Converts an Abstract Syntax Tree (AST) into a DevExpress-compatible filter format.
* Optionally supports a result object for dynamic value resolution and short-circuit evaluation.
*
* Example:
* ```
* const filter = convertAstToDevextreme(ast, state, true);
* console.log(filter);
* ```
*
* @param ast - The parsed AST from `convertSQLToAst`.
* @param state - An optional result object to resolve placeholders to actual values.
* @param enableShortCircuit - Whether to apply short-circuit evaluation.
* @returns DevExpressFilter - The DevExpress-compatible filter array or null.
*/
export function convertAstToDevextreme(
ast: any, // Define a more specific type if possible
state?: StateDataObject | null,
ast: ASTNode,
state?: ResultObject | null,
enableShortCircuit?: boolean,
): any;
): DevExpressFilter;
4 changes: 3 additions & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ export const OPERATOR_PRECEDENCE = {
// Regular expression to check for unsupported SQL patterns (like SELECT-FROM or JOIN statements)
export const UNSUPPORTED_PATTERN = /\bSELECT\b.*\bFROM\b|\bINNER\s+JOIN\b/i;

export const LOGICAL_OPERATORS = ['and', 'or'];
export const LOGICAL_OPERATORS = ['and', 'or'];

export const LITERAL_TYPES = ["value", "placeholder"];
64 changes: 55 additions & 9 deletions src/core/converter.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LOGICAL_OPERATORS } from "../constants.js";
import { LITERAL_TYPES, LOGICAL_OPERATORS } from "../constants.js";

/**
* Main conversion function that sets up the global context
Expand Down Expand Up @@ -48,7 +48,7 @@ function DevExpressConverter() {
return handleFunction(ast);
case "field":
case "value":
return convertValue(ast.value);
return convertValue(ast.value, parentOperator);
default:
return null;
}
Expand Down Expand Up @@ -113,6 +113,7 @@ function DevExpressConverter() {
if (shouldFlattenLogicalTree(parentOperator, operator, ast)) {
return flattenLogicalTree(left, operator, right);
}

return [left, operator, right];
}

Expand All @@ -123,10 +124,11 @@ function DevExpressConverter() {
*/
function handleComparisonOperator(ast) {
const operator = ast.operator.toUpperCase();
const originalOperator = ast.originalOperator?.toUpperCase();

// Handle "IS NULL" condition
if (operator === "IS" && ast.value === null) {
return [ast.field, "=", null];
if ((operator === "IS" || originalOperator === "IS") && ast.value === null) {
return [ast.field, "=", null, { type: originalOperator }, null];
}

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

if (operatorToken === "like") {
operatorToken = "contains";
} else if (operatorToken === "not like") {
operatorToken = "notcontains";
} else if (operatorToken === "=" && originalOperator === "IS") {
includeExtradata = true
} else if (operatorToken == "!=" && originalOperator === "IS NOT") {
operatorToken = "!=";
includeExtradata = true;
}

let comparison = [left, operatorToken, right];
if (includeExtradata)
comparison = [left, operatorToken, right, { type: originalOperator }, right];

// 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
if ((ast.left && isFunctionNullCheck(ast.left, true)) || (ast.value && isFunctionNullCheck(ast.value, false))) {
Expand Down Expand Up @@ -205,22 +214,51 @@ function DevExpressConverter() {
resolvedValue = resolvedValue.split(',').map(v => v.trim());
}

// handle short circuit evaluation for IN operator
if (EnableShortCircuit && (LITERAL_TYPES.includes(ast.field?.type) && LITERAL_TYPES.includes(ast.value?.type))) {
const fieldVal = convertValue(ast.field);
if (Array.isArray(resolvedValue)) {
// normalize numeric strings if LHS is number
const list = resolvedValue.map(x =>
(typeof x === "string" && !isNaN(x) && typeof fieldVal === "number")
? Number(x)
: x
);

if (operator === "IN")
return list.includes(fieldVal);
else if (operator === "NOT IN")
return !list.includes(fieldVal);
} else if (!Array.isArray(resolvedValue)) {
// normalize numeric strings if LHS is number
const value = (typeof resolvedValue === "string" && !isNaN(resolvedValue) && typeof fieldVal === "number")
? Number(resolvedValue)
: resolvedValue;

if (operator === "IN")
return fieldVal == value;
else if (operator === "NOT IN")
return fieldVal != value;
}
}

let operatorToken = operator === "IN" ? '=' : operator === "NOT IN" ? '!=' : operator;
let joinOperatorToken = operator === "IN" ? 'or' : operator === "NOT IN" ? 'and' : operator;

let field = convertValue(ast.field);
if (Array.isArray(resolvedValue) && resolvedValue.length) {
return resolvedValue.flatMap(i => [[ast.field, operatorToken, i], joinOperatorToken]).slice(0, -1);
return resolvedValue.flatMap(i => [[field, operatorToken, i], joinOperatorToken]).slice(0, -1);
}

return [ast.field, operatorToken, resolvedValue];
return [field, operatorToken, resolvedValue];
}

/**
* Converts a single value, resolving placeholders and handling special cases.
* @param {*} val - The value to convert.
* @param {string} parentOperator - The operator of the parent logical node (if any).
* @returns {*} Converted value.
*/
function convertValue(val) {
function convertValue(val, parentOperator = null) {
if (val === null) return null;

// Handle array values
Expand Down Expand Up @@ -251,6 +289,10 @@ function DevExpressConverter() {
}
}

if (parentOperator && parentOperator.toUpperCase() === "IN" && typeof val === "string") {
return val.split(',').map(v => v.trim());
}

return val;
}

Expand Down Expand Up @@ -368,6 +410,10 @@ function DevExpressConverter() {

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

// Handle NULL == 0 OR NULL == "" cases
if (left === null && (right == 0 || right == "")) return true;
if (right === null && (left == 0 || left == "")) return true;

if (left === null || right === null) {
if (operator === '=' || operator === '==') return left === right;
if (operator === '<>' || operator === '!=') return left !== right;
Expand Down
Loading