@react-awesome-query-builder/sql
Version:
User-friendly query builder for React. SQL support
297 lines (277 loc) • 9.31 kB
text/typescript
/* eslint-disable @typescript-eslint/no-redundant-type-constituents, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
import type { Meta, OutLogic, OutSelect } from "./types";
import { SqlPrimitiveTypes } from "./conv";
import {
// ext
ConjExpr, BinaryExpr, UnaryExpr, AnyExpr, Logic, BaseExpr,
// orig
AST, Select, Function as SqlFunction, ExpressionValue, ExprList, LocationRange,
ColumnRef, ValueExpr, Value, Case, Interval, Column,
} from "node-sql-parser";
export const processAst = (sqlAst: AST, meta: Meta): OutSelect => {
if (sqlAst.type !== "select") {
meta.errors.push(`Expected SELECT, but got ${sqlAst.type}`);
}
const select = sqlAst as Select;
const pWhere = processLogic(select.where, meta);
const selectExpr = ((select.columns ?? []) as Column[]).find(c => c.type === "expr");
const pSelectExpr = processLogic(selectExpr?.expr as Logic, meta);
const hasLogic = select.where || selectExpr?.expr;
if (!hasLogic) {
meta.errors.push("No logic found in WHERE/SELECT");
}
return {
where: pWhere,
select: pSelectExpr,
};
};
const processLogic = (logic: Logic | null | undefined, meta: Meta, not = false): OutLogic | undefined => {
if (!logic) {
return undefined;
}
let ret: OutLogic | undefined;
if (logic.type === "function") {
ret = processFunc(logic as SqlFunction, meta, not);
} else if (logic.type === "binary_expr") {
if (["AND", "OR"].includes((logic as BinaryExpr).operator)) {
ret = processConj(logic as ConjExpr, meta, not);
} else {
ret = processBinaryOp(logic as BinaryExpr, meta, not);
}
} else if (logic.type === "unary_expr") {
if ((logic as UnaryExpr).operator === "NOT") {
const subFilter = (logic as UnaryExpr).expr;
ret = processLogic(subFilter, meta, !not);
if (ret) {
ret.parentheses = true;
}
} else {
meta.errors.push(`Unexpected unary operator ${(logic as UnaryExpr).operator}`);
}
} else if (logic.type === "expr_list") {
ret = processExprList(logic as ExprList, meta, not);
} else if (logic.type === "case") {
ret = processCase(logic as Case, meta, not);
} else if (logic.type === "column_ref") {
ret = processField(logic as ColumnRef, meta, not);
} else if (logic.type === "interval") {
ret = processInterval(logic as Interval, meta, not);
} else if ((logic as Value).value !== undefined) {
ret = processValue(logic as Value, meta, not);
} else {
// todo: aggr_func (like COUNT)
meta.errors.push(`Unexpected logic type ${logic.type}`);
}
return ret;
};
const processConj = (expr: ConjExpr, meta: Meta, not = false): OutLogic | undefined => {
const parentheses = expr.parentheses;
const conj = expr.operator;
// flatize OR/AND
const childrenChain = [
processLogic(expr.left, meta),
processLogic(expr.right, meta),
].filter(c => !!c) as OutLogic[];
const children = childrenChain.reduce((acc, child) => {
const canFlatize = child?.conj === conj && !child.parentheses && !child.not;
const flat = canFlatize ? (child.children ?? []) : [child];
return [...acc, ...flat];
}, [] as OutLogic[]);
return {
parentheses,
not,
conj,
children,
};
};
const processCase = (expr: Case, meta: Meta, not = false): OutLogic | undefined => {
const parentheses = (expr as BaseExpr).parentheses;
const children = expr.args
.map(arg => {
if (!["when", "else"].includes(arg.type)) {
meta.errors.push(`Unexpected type ${arg.type} inside CASE`);
}
return {
cond: arg.type === "when" ? processLogic(arg.cond, meta) : undefined,
result: processLogic(arg.result, meta),
};
})
.filter(a => !!a) as OutLogic[];
return {
parentheses,
children,
_type: "case",
not,
};
};
const processBinaryOp = (expr: BinaryExpr, meta: Meta, not = false): OutLogic | undefined => {
const parentheses = expr.parentheses;
const operator = expr.operator;
let children = [
processLogic(expr.left, meta),
processLogic(expr.right, meta),
].filter(c => !!c) as OutLogic[];
if (operator === "BETWEEN" || operator === "NOT BETWEEN") {
// flatize BETWEEN
children = [
children[0],
...(children[1].children ?? []),
].filter(c => !!c);
}
return {
parentheses,
not,
children,
operator,
};
};
const getExprValue = (expr: ValueExpr, meta: Meta, not = false): string | number | boolean => {
let value = expr.value;
if (expr.type === "boolean" && not) {
value = !value;
}
// todo: date literals?
return value;
};
const getExprStringValue = (expr: ValueExpr, meta: Meta, not = false): string => {
const v = getExprValue(expr, meta, not);
return String(v);
};
const processExprList = (expr: ExprList, meta: Meta, not = false): OutLogic | undefined => {
const children = expr.value
.map(ev => processLogic(ev, meta, false))
.filter(ev => !!ev) as OutLogic[];
const valueTypes = children.map(ch => ch.valueType).filter(ev => !!ev);
const uniqValueTypes = Array.from(new Set(valueTypes));
const oneValueType = valueTypes.length === children.length && uniqValueTypes.length === 1 ? uniqValueTypes[0] : undefined;
let values;
if (oneValueType && SqlPrimitiveTypes[oneValueType]) {
// it's list of primitive values
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
values = children.map(ch => ch.value);
}
return {
children,
not,
_type: "expr_list",
oneValueType,
values,
};
};
const processInterval = (expr: Interval, meta: Meta, not = false): OutLogic | undefined => {
return {
_type: "interval",
...(processValue(expr.expr, meta) ?? {}),
unit: expr.unit,
};
};
const processValue = (expr: Value, meta: Meta, not = false): OutLogic | undefined => {
const {type: valueType} = expr;
const value = getExprValue(expr as ValueExpr, meta, not);
return {
valueType,
value,
};
};
const processField = (expr: ColumnRef, meta: Meta, not = false): OutLogic | undefined => {
if (expr.type === "expr") {
return undefined;
}
const parentheses = (expr as BaseExpr).parentheses;
const field = typeof expr.column === "string" ? expr.column : getExprStringValue(expr.column.expr, meta, not);
const table = expr.table ?? undefined;
if (field === "") {
// fix for empty string
return {
valueType: "single_quote_string",
value: "",
};
} else {
return {
parentheses,
field,
table,
};
}
};
const flatizeTernary = (children: OutLogic[], meta: Meta) => {
const flat: [OutLogic | undefined, OutLogic][] = [];
function _processTernaryChildren(tern: OutLogic[]) {
const [cond, if_val, else_val] = tern;
if (if_val.func === "IF") {
meta.errors.push("Unexpected IF inside IF");
}
flat.push([cond, if_val]);
if (else_val?.func === "IF") {
_processTernaryChildren(else_val?.children!);
} else {
flat.push([undefined, else_val]);
}
}
_processTernaryChildren(children);
return flat;
};
const processFunc = (expr: SqlFunction, meta: Meta, not = false): OutLogic | undefined => {
const nameType = expr.name.name[0].type as string;
const firstName = expr.name.name[0].value;
const getArgs = (useNot: boolean): OutLogic[] => {
if (expr.args) {
if (expr.args.type === "expr_list") {
return expr.args.value
.map(arg => processLogic(arg, meta, useNot ? not : undefined))
.filter(a => !!a) as OutLogic[];
} else {
meta.errors.push(`Unexpected args type for func (${JSON.stringify(expr.name.name)}): ${JSON.stringify(expr.args.type)}`);
}
}
return [];
};
let ret: OutLogic | undefined;
if (nameType === "default" && firstName === "NOT") {
if (expr.args?.value.length === 1) {
const args = getArgs(false);
ret = args[0];
// ret.parentheses = true;
ret.not = !ret.not;
if (not) {
ret.not = !ret.not;
}
} else {
meta.errors.push(`Unexpected args for func NOT: ${JSON.stringify(expr.args?.value)}`);
}
} else if (nameType === "default" && firstName === "IFNULL") {
const args = getArgs(true);
// const defaultValue = args[1].value;
ret = args[0];
} else if (nameType === "default") {
const flatizeArgs = firstName === "IF";
const args = getArgs(false);
ret = {
func: firstName,
children: args,
not: not,
};
if (flatizeArgs) {
ret.ternaryChildren = flatizeTernary(args, meta);
}
return ret;
} else {
meta.errors.push(`Unexpected function name ${JSON.stringify(expr.name.name)}`);
}
return ret;
};
export const getLogicDescr = (logic?: OutLogic) => {
if (logic?._type === "case") {
// const cases = logic.children as {cond: OutLogic, result: OutLogic}[];
return "CASE";
} else if (logic?._type === "interval") {
return `INTERVAL ${JSON.stringify(logic.value)} ${logic.unit}`;
} else if (logic?.func) {
return `${logic.func}()`;
} else if (logic?.operator) {
return `operator ${logic.operator}`;
} else if (logic?._type === "expr_list") {
return JSON.stringify(logic.values);
}
return JSON.stringify(logic);
};