import type { RuleProcessor } from '@react-querybuilder/core';
import {
betweenOperators,
defaultRuleProcessorDrizzle,
lc,
parseNumber,
shouldRenderAsNumber,
} from '@react-querybuilder/core';
import { defaultDrizzleSerializers } from '../functions/drizzle';
import { defaultFunctionMeta } from '../functions/meta';
import { getRuleExpressions } from '../registry';
import type { DrizzleSerializerRegistry, DrizzleSqlTag } from '../types';
import { serializeDrizzle } from '../utils/serializeDrizzle';
import { validateExpression } from '../utils/validateExpression';
const COMPARE: Record<string, string> = {
'=': '=',
'!=': '<>',
'<': '<',
'<=': '<=',
'>': '>',
'>=': '>=',
};
const BETWEEN_OPERATORS = new Set<string>([...betweenOperators].map(lc));
const STRING_MATCH: Record<
string,
{ kind: 'contains' | 'beginswith' | 'endswith'; negate: boolean }
> = {
contains: { kind: 'contains', negate: false },
doesnotcontain: { kind: 'contains', negate: true },
beginswith: { kind: 'beginswith', negate: false },
doesnotbeginwith: { kind: 'beginswith', negate: true },
endswith: { kind: 'endswith', negate: false },
doesnotendwith: { kind: 'endswith', negate: true },
};
* Generates a rule processor with expression support for use by
* {@link @react-querybuilder/core!formatQuery formatQuery} with the "drizzle" format.
* Expression rules compose Drizzle `SQL` fragments from `context.columns` (arithmetic,
* `abs`, `least`/`greatest`, `upper`/`lower`, etc.). Requires `context.columns` and
* `context.drizzleOperators` (whose `sql` tag is used) — the same context the stock
* processor needs. Pass custom `serializers` to add functions or override built-ins; they
* are merged over {@link defaultDrizzleSerializers}. Rules without expressions, or with an
* unsupported operator, fall back to the stock processor.
*/
export const getExpressionRuleProcessorDrizzle =
(serializers?: DrizzleSerializerRegistry): RuleProcessor =>
(rule, options) => {
const opts = options ?? {};
const serial = serializers
? { ...defaultDrizzleSerializers, ...serializers }
: defaultDrizzleSerializers;
const expr = getRuleExpressions(rule);
if (!expr || (!expr.lhs && !expr.rhs)) return defaultRuleProcessorDrizzle(rule, opts);
const { columns, drizzleOperators } = (opts.context ?? {}) as {
columns?: Record<string, unknown>;
drizzleOperators?: { sql?: DrizzleSqlTag };
};
const sql = drizzleOperators?.sql;
if (!columns || !sql) return defaultRuleProcessorDrizzle(rule, opts);
const operator = lc(rule.operator);
const unary = operator === 'null' || operator === 'notnull';
const cmp = COMPARE[operator];
const betweenExpr = BETWEEN_OPERATORS.has(operator) && !!(expr.rhs || expr.rhs2);
const stringMatch = STRING_MATCH[operator];
if (!unary && !cmp && !betweenExpr && !stringMatch)
return defaultRuleProcessorDrizzle(rule, opts);
const validate = { functions: serial, meta: defaultFunctionMeta };
if (
(expr.lhs && !validateExpression(expr.lhs, validate).valid) ||
(expr.rhs && !validateExpression(expr.rhs, validate).valid) ||
(expr.rhs2 && !validateExpression(expr.rhs2, validate).valid)
) {
return undefined;
}
const ctx = { sql, columns };
const lhs = expr.lhs ? serializeDrizzle(expr.lhs, serial, ctx, opts) : columns[rule.field];
if (!lhs) return defaultRuleProcessorDrizzle(rule, opts);
if (unary) {
return operator === 'notnull' ? sql`${lhs} is not null` : sql`${lhs} is null`;
}
if (betweenExpr) {
if (!expr.rhs || !expr.rhs2) return undefined;
const from = serializeDrizzle(expr.rhs, serial, ctx, opts);
const to = serializeDrizzle(expr.rhs2, serial, ctx, opts);
return operator === 'notbetween'
? sql`${lhs} not between ${from} and ${to}`
: sql`${lhs} between ${from} and ${to}`;
}
const rhs = expr.rhs
? serializeDrizzle(expr.rhs, serial, ctx, opts)
: rule.valueSource === 'field'
? columns[rule.value]
: shouldRenderAsNumber(rule.value, opts.parseNumbers)
? parseNumber(rule.value, { parseNumbers: opts.parseNumbers })
: rule.value;
if (stringMatch) {
const { kind, negate } = stringMatch;
const pattern =
kind === 'contains'
? sql`'%' || ${rhs} || '%'`
: kind === 'beginswith'
? sql`${rhs} || '%'`
: sql`'%' || ${rhs}`;
return negate ? sql`${lhs} not like ${pattern}` : sql`${lhs} like ${pattern}`;
}
return sql`${lhs} ${sql.raw(cmp)} ${rhs}`;
};