UNPKG

sqlparser-devexpress

Version:

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 qu

552 lines (462 loc) 24 kB
import { LITERAL_TYPES, LOGICAL_OPERATORS } from "../constants.js"; /** * Main conversion function that sets up the global context * @returns {Array|null} DevExpress format filter */ function DevExpressConverter() { // Global variables accessible throughout the converter let resultObject = null; let EnableShortCircuit = true; let IsValueNullShortCircuit = false; // Flag to enable/disable null short-circuiting let TreatNumberAsNullableBit = false; // Flag to enable/disable optional boolean with number conversion /** * Main conversion function that sets up the global context * @param {Object} ast - The abstract syntax tree * @param {Object} ResultObject - Optional object for placeholder resolution * @param {boolean} enableShortCircuit - Optional enabling and disabling the shortcircuit ie evaluating value = value scenario * @param {boolean} isValueNullShortCircuit - Optional enabling and disabling the null shortcircuit ie evaluating value = null scenario * @param {boolean} treatNumberAsNullableBit - Optional enabling and disabling the optional boolean with number conversion * @returns {Array|null} DevExpress format filter */ function convert(ast, ResultObject = null, enableShortCircuit = true, isValueNullShortCircuit = false, treatNumberAsNullableBit = false) { // Set up global context resultObject = ResultObject; EnableShortCircuit = enableShortCircuit; IsValueNullShortCircuit = isValueNullShortCircuit; TreatNumberAsNullableBit = treatNumberAsNullableBit; // Process the AST let result = processAstNode(ast); // Handle special cases for short circuit if (result === true || result === false || result === null) return []; if (result.length == 1) { return result[0]; } return result; } /** * Process an AST node based on its type * @param {Object} ast - The AST node to process * @param {string} parentOperator - The operator of the parent logical node (if any) * @returns {Array|null|boolean} DevExpress format filter or boolean for short-circuit */ function processAstNode(ast, parentOperator = null) { if (!ast) return null; // Return null if the AST is empty switch (ast.type) { case "logical": return handleLogicalOperator(ast, parentOperator); case "comparison": return handleComparisonOperator(ast); case "function": return handleFunction(ast); case "field": case "value": return convertValue(ast.value, parentOperator); default: return null; } } /** * Handles logical operators (AND, OR) and applies short-circuit optimizations. * @param {Object} ast - The logical operator AST node. * @param {string} parentOperator - The operator of the parent logical node. * @returns {Array|boolean} DevExpress format filter or boolean for short-circuit. */ function handleLogicalOperator(ast, parentOperator) { const operator = ast.operator.toLowerCase(); // Special case: Handle ISNULL comparison with a value if (isNullCheck(ast.left, ast.right)) { const resolvedValue = convertValue(ast.right); // Short-circuit: If left argument is a placeholder, return boolean result directly if (EnableShortCircuit && ast.left.args[0]?.value?.type === "placeholder") { return resolvedValue == null; } return [processAstNode(ast.left), operator, null]; } if (isNullCheck(ast.right, ast.left)) { const resolvedValue = convertValue(ast.left); // Short-circuit: If right argument is a placeholder, return boolean result directly if (EnableShortCircuit && ast.right.args[0]?.value?.type === "placeholder") { return resolvedValue == null; } return [null, operator, processAstNode(ast.right)]; } // Recursively process left and right operands const left = processAstNode(ast.left, operator); const right = processAstNode(ast.right, operator); if (EnableShortCircuit) { // Short-circuit: always-true conditions if (left === true || right === true) { if (operator === 'or') return true; return left === true ? right : left; } // Short-circuit: always-false conditions if (left === false || right === false) { return left === false ? right : left; } } // Detect and flatten nested logical expressions if (parentOperator === null) { if (left && left.length === 3 && LOGICAL_OPERATORS.includes(left[1])) parentOperator = left[1]; if (right && right.length === 3 && LOGICAL_OPERATORS.includes(right[1])) parentOperator = right[1]; } // Flatten nested logical expressions if applicable if (shouldFlattenLogicalTree(parentOperator, operator, ast)) { return flattenLogicalTree(left, operator, right); } return [left, operator, right]; } /** * Handles comparison operators (=, <>, IN, IS) and applies optimizations. * @param {Object} ast - The comparison operator AST node. * @returns {Array|boolean} DevExpress format filter or boolean for short-circuit. */ function handleComparisonOperator(ast) { const operator = ast.operator.toUpperCase(); const originalOperator = ast.originalOperator?.toUpperCase(); // Handle "IS NULL" condition if ((operator === "IS" || originalOperator === "IS") && ast.value === null) { return [ast.field, "=", null, { type: originalOperator }, null]; } // Handle "IN" condition, including comma-separated values if (operator === "IN" || operator === "NOT IN") { return handleInOperator(ast, operator); } const left = ast.left !== undefined ? processAstNode(ast.left) : convertValue(ast.field); const leftDefault = ast.left?.args && ast.left?.args[1]?.value; const right = ast.right !== undefined ? processAstNode(ast.right) : convertValue(ast.value); const rightDefault = ast.right?.args && 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]; let isLeftNullCheck = isFunctionNullCheck(ast.left, true); let isRightNullCheck = isFunctionNullCheck(ast.right, false) || (ast.value && isFunctionNullCheck(ast.value, false)); let isBothNullChecks = isLeftNullCheck && isRightNullCheck; let isdestructured = false; // 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. if (!isBothNullChecks && isLeftNullCheck) { const nullCheckArg = (ast.left ?? ast.value).args[1]?.value; let valueRight = null let baseComparison = comparison; if (Array.isArray(right) && (right.includes("or") || right.includes("and"))) { isdestructured = true; valueRight = right.shift(); baseComparison = [[left, operatorToken, valueRight], ...right]; } const _val = !isdestructured ? baseComparison[2] : valueRight; if (normalizeBool(_val) == normalizeBool(nullCheckArg)) comparison = [[...baseComparison], 'or', [left, operatorToken, null, { type: "ISNULL", position: "column", defaultValue: nullCheckArg }, null]]; else comparison = [...baseComparison, { type: "ISNULL", position: "column", defaultValue: nullCheckArg }, _val]; } else if (!isBothNullChecks && isRightNullCheck) { const nullCheckArg = (ast.right ?? ast.value).args[1]?.value; let valueLeft = null let baseComparison = comparison; if (Array.isArray(left) && (left.includes("or") || left.includes("and"))) { isdestructured = true; valueLeft = left.shift(); baseComparison = [[valueLeft, operatorToken, right], ...left]; } const _val = !isdestructured ? baseComparison[2] : valueRight; comparison = [...baseComparison, { type: "ISNULL", position: "value", defaultValue: nullCheckArg }, _val]; } else if (isBothNullChecks) { const nullCheckArgleft = (ast.left ?? ast.value).args[1]?.value; const nullCheckArgright = (ast.right ?? ast.value).args[1]?.value; let valueLeft = null; let baseComparison = comparison; if (Array.isArray(left) && (left.includes("or") || left.includes("and"))) { isdestructured = true; valueLeft = left.shift(); baseComparison = [[valueLeft, operatorToken, right], ...left]; } const _val = !isdestructured ? baseComparison[2] : valueLeft; if (normalizeBool(nullCheckArgleft) == normalizeBool(nullCheckArgright)) comparison = [[...baseComparison, { type: "ISNULL", position: "both", defaultValue: nullCheckArgleft, defaultValueRight: nullCheckArgright }, _val], 'or', [left, operatorToken, null]]; else comparison = [[...baseComparison, { type: "ISNULL", position: "both", defaultValue: nullCheckArgleft, defaultValueRight: nullCheckArgright }, _val]]; } // Apply short-circuit evaluation if enabled if (EnableShortCircuit && IsValueNullShortCircuit && (left == null || right == null)) { return true; // If either value is null, return true for short-circuit evaluation } if (EnableShortCircuit) { if (isAlwaysTrue(comparison, leftDefault, rightDefault)) return true; if (isAlwaysFalse(comparison, leftDefault, rightDefault)) return false; } if (TreatNumberAsNullableBit && typeof right === "number" && (right == 0 || right == 1)) { if (Array.isArray(comparison[0])) { return [ ...comparison, 'or', [left, operatorToken, right == true] ]; } return [ [...comparison], 'or', [left, operatorToken, right == true] ]; } return comparison; } /** * Normalizes numeric boolean-like values to actual booleans. * * Converts the number `0` to `false` and `1` to `true`. * If the input is not exactly `0` or `1`, it returns the value unchanged. * * @param {*} value - The value to normalize. Can be of any type. * @returns {*} - Returns `false` if `value` is `0`, `true` if `value` is `1`, otherwise returns the original value. */ function normalizeBool(value) { return value === 0 || value === 1 ? Boolean(value) : value; } /** * Handles function calls, focusing on ISNULL. * @param {Object} ast - The function AST node. * @returns {*} Resolved function result. */ function handleFunction(ast) { if (ast.name === "ISNULL" && ast.args && ast.args.length >= 2) { const firstArg = ast.args[0]; // Resolve placeholders if (firstArg.type === "placeholder") { return resolvePlaceholderFromResultObject(firstArg.value); } return convertValue(firstArg); } // this should never happen as we are only handling ISNULL and should throw an error throw new Error(`Unsupported function: ${ast.name}`); } /** * Handles the IN operator specifically. * @param {Object} ast - The comparison operator AST node. * @returns {Array} DevExpress format filter. */ function handleInOperator(ast, operator) { let resolvedValue = convertValue(ast.value); // Handle comma-separated values in a string if (Array.isArray(resolvedValue) && resolvedValue.length === 1) { const firstValue = resolvedValue[0]; if (typeof firstValue === 'string' && firstValue.includes(',')) { resolvedValue = firstValue.split(',').map(v => v.trim()); } else { resolvedValue = firstValue; } } else if (typeof resolvedValue === 'string' && resolvedValue.includes(',')) { 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; } } if (EnableShortCircuit && IsValueNullShortCircuit && (ast.field?.type === "placeholder" || ast.value?.type === "placeholder" || ast.value === null) && resolvedValue === null) { return true; } 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 => [[field, operatorToken, i], joinOperatorToken]).slice(0, -1); } 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, parentOperator = null) { if (val === null) return null; // Handle array values if (Array.isArray(val)) { return val.map(item => convertValue(item)); } // Handle object-based values if (typeof val === "object") { if (val.type === "placeholder") { const placeholderValue = resolvePlaceholderFromResultObject(val.value); if (val?.dataType === "string") { return placeholderValue?.toString(); } return placeholderValue; } // Special handling for ISNULL function if (isFunctionNullCheck(val)) { return convertValue(val.args[0]); } // Handle nested AST nodes if (val.type) { return processAstNode(val); } } if (parentOperator && parentOperator.toUpperCase() === "IN" && typeof val === "string") { return val.split(',').map(v => v.trim()); } return val; } /** * Resolves placeholder values from the result object. * @param {string} placeholder - The placeholder to resolve. * @returns {*} Resolved placeholder value. */ function resolvePlaceholderFromResultObject(placeholder) { if (!resultObject) return `{${placeholder}}`; return resultObject.hasOwnProperty(placeholder) ? resultObject[placeholder] : `{${placeholder.value ?? placeholder}}`; } /** * Checks if a node is an ISNULL function check. * @param {Object} node - The node to check. * @param {Object} valueNode - The value node. * @returns {boolean} True if this is an ISNULL check. */ function isNullCheck(node, valueNode) { return node?.type === "function" && node.name === "ISNULL" && valueNode?.type === "value"; } /** * Checks if a node is a ISNULL function without value * @param {Object} node * @returns {boolean} True if this is an ISNULL check. */ function isFunctionNullCheck(node, isPlaceholderCheck = false) { const isValidFunction = node?.type === "function" && node?.name === "ISNULL" && node?.args?.length >= 2; return isPlaceholderCheck ? isValidFunction && node?.args[0]?.value?.type !== "placeholder" : isValidFunction; } /** * Determines whether the logical tree should be flattened. * This is based on the parent operator and the current operator. * @param {string} parentOperator - The operator of the parent logical node. * @param {string} operator - The operator of the current logical node. * @param {Object} ast - The current AST node. * @returns {boolean} True if the tree should be flattened, false otherwise. */ function shouldFlattenLogicalTree(parentOperator, operator, ast) { return parentOperator !== null && operator === parentOperator || ast.operator === ast.right?.operator; } /** * Flattens a logical tree by combining nested logical nodes. * @param {Array} left - The left side of the logical expression. * @param {string} operator - The logical operator. * @param {Array} right - The right side of the logical expression. * @returns {Array} The flattened logical tree. */ function flattenLogicalTree(left, operator, right) { const parts = []; // Flatten left side if it has the same operator if (Array.isArray(left) && left[1] === operator) { parts.push(...left); } else { parts.push(left); } // Add the operator parts.push(operator); // Flatten right side if it has the same operator if (Array.isArray(right) && right[1] === operator) { parts.push(...right); } else { parts.push(right); } return parts; } /** * Checks if a condition is always true. * @param {Array} condition - The condition to check. * @param {*} leftDefault - The default value for the left operand. * @param {*} rightDefault - The default value for the right operand. * @returns {boolean} True if the condition is always true. */ function isAlwaysTrue(condition, leftDefault, rightDefault) { return Array.isArray(condition) && condition.length >= 3 && evaluateExpression(...condition, leftDefault, rightDefault) == true; } /** * Checks if a condition is always false. * @param {Array} condition - The condition to check. * @param {*} leftDefault - The default value for the left operand. * @param {*} rightDefault - The default value for the right operand. * @returns {boolean} True if the condition is always false. */ function isAlwaysFalse(condition, leftDefault, rightDefault) { return Array.isArray(condition) && condition.length >= 3 && evaluateExpression(...condition, leftDefault, rightDefault) == false; } /** * Evaluates a simple expression. * @param {*} left - The left operand. * @param {string} operator - The operator. * @param {*} right - The right operand. * @param {*} leftDefault - The default value for the left operand. * @param {*} rightDefault - The default value for the right * @returns {boolean|null} The result of the evaluation or null if not evaluable. */ function evaluateExpression(left, operator, right, leftDefault, rightDefault) { if (left == null && leftDefault != undefined) left = leftDefault; if (right == null && rightDefault != undefined) right = rightDefault; 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; return null; // Any comparison with null should return null } switch (operator) { case '=': case '==': return left === right; case '<>': case '!=': return left !== right; case '>': return left > right; case '>=': return left >= right; case '<': return left < right; case '<=': return left <= right; default: return null; // Invalid operator } } return { init: convert }; } // Create a global instance const devExpressConverter = DevExpressConverter(); /** * Converts an abstract syntax tree to DevExpress format * @param {Object} ast - The abstract syntax tree * @param {Object} resultObject - Optional object for placeholder resolution * @param {Object} options - Optional options for conversion * @param {boolean} options.enableShortCircuit - Enable or disable short-circuit evaluation * @param {boolean} options.isValueNullShortCircuit - Enable or disable null short-circuit evaluation * @param {boolean} options.treatNumberAsNullableBit - Enable or disable optional boolean with number conversion * @returns {Array|null} DevExpress format filter */ export function convertToDevExpressFormat({ ast, resultObject = null, options = {} }) { const { enableShortCircuit = true, isValueNullShortCircuit = false, treatNumberAsNullableBit = false } = options; return devExpressConverter.init(ast, resultObject, enableShortCircuit, isValueNullShortCircuit, treatNumberAsNullableBit); }