jev-ql
Version:
PostgreSQL-compatible query language powered by TypeSafe Jev System One models
285 lines (251 loc) • 8.28 kB
JavaScript
import { isSemanticFunction } from './functions.js';
import { sha256 } from './utils.js';
/**
* Traverses an expression AST to find all semantic function calls (NOUL, CHOICE, SCORE).
*/
export function extractSemanticNodes(exprNode, results = []) {
if (!exprNode || typeof exprNode !== 'object') return results;
if (exprNode.type === 'FunctionCall') {
const fnName = exprNode.name.toUpperCase();
if (['NOUL', 'CHOICE', 'SCORE', 'IS_TRUE', 'IS_FALSE', 'JEV', 'SEMANTIC'].includes(fnName)) {
results.push(exprNode);
// Even if it's a semantic node, its arguments could conceivably contain nested expressions
}
// Also traverse arguments (e.g. CONFIDENCE(CHOICE(...)))
for (const arg of exprNode.arguments || []) {
extractSemanticNodes(arg, results);
}
return results;
}
if (exprNode.type === 'BinaryExpression') {
extractSemanticNodes(exprNode.left, results);
extractSemanticNodes(exprNode.right, results);
} else if (exprNode.type === 'UnaryExpression') {
extractSemanticNodes(exprNode.argument, results);
} else if (exprNode.type === 'CaseExpression') {
for (const cond of exprNode.conditions || []) {
extractSemanticNodes(cond.when, results);
extractSemanticNodes(cond.then, results);
}
if (exprNode.elseExpr) extractSemanticNodes(exprNode.elseExpr, results);
} else if (exprNode.type === 'InExpression') {
extractSemanticNodes(exprNode.expr, results);
for (const v of exprNode.values || []) extractSemanticNodes(v, results);
} else if (exprNode.type === 'BetweenExpression') {
extractSemanticNodes(exprNode.expr, results);
extractSemanticNodes(exprNode.lower, results);
extractSemanticNodes(exprNode.upper, results);
} else if (exprNode.type === 'IsNullExpression' || exprNode.type === 'LikeExpression') {
extractSemanticNodes(exprNode.expr, results);
if (exprNode.pattern) extractSemanticNodes(exprNode.pattern, results);
}
return results;
}
/**
* Check if an expression AST node has any semantic function calls.
*/
export function containsSemanticCalls(exprNode) {
const list = extractSemanticNodes(exprNode);
return list.length > 0;
}
/**
* Splits a WHERE clause conjuncts (top-level ANDs) into deterministic vs semantic.
*/
export function splitWhereClause(whereNode) {
if (!whereNode) {
return { deterministic: null, semantic: null };
}
const conjuncts = [];
function collectConjuncts(node) {
if (node.type === 'BinaryExpression' && node.operator === 'AND') {
collectConjuncts(node.left);
collectConjuncts(node.right);
} else {
conjuncts.push(node);
}
}
collectConjuncts(whereNode);
const deterministicList = [];
const semanticList = [];
for (const c of conjuncts) {
if (containsSemanticCalls(c)) {
semanticList.push(c);
} else {
deterministicList.push(c);
}
}
function combineWithAnd(list) {
if (list.length === 0) return null;
let curr = list[0];
for (let i = 1; i < list.length; i++) {
curr = { type: 'BinaryExpression', operator: 'AND', left: curr, right: list[i] };
}
return curr;
}
return {
deterministic: combineWithAnd(deterministicList),
semantic: combineWithAnd(semanticList)
};
}
export function defaultEvalLiteral(node) {
if (!node) return null;
if (node.type === 'Literal') return node.value;
if (node.type === 'ArrayLiteral') {
return (node.elements || []).map(defaultEvalLiteral);
}
if (node.type === 'ObjectLiteral') {
const obj = {};
for (const [k, v] of Object.entries(node.properties || {})) {
obj[k] = defaultEvalLiteral(v);
}
return obj;
}
if (node.type === 'Identifier') return node.name;
return node.value ?? null;
}
/**
* Converts a Semantic AST FunctionCall into a standardized TypeSafe Question descriptor.
*/
export function buildQuestionDescriptor(fnNode, evalLiteralFn = defaultEvalLiteral) {
const fnName = fnNode.name.toUpperCase();
const args = fnNode.arguments;
const stateArg = args[0];
const instructionArg = args[1];
const instructions = evalLiteralFn(instructionArg) || 'Evaluate';
let type = 'noul';
let criteria = undefined;
if (fnName === 'NOUL' || fnName === 'IS_TRUE' || fnName === 'IS_FALSE' || fnName === 'SEMANTIC') {
type = 'noul';
if (args[2]) {
const criteriaTrue = evalLiteralFn(args[2]);
const criteriaFalse = args[3] ? evalLiteralFn(args[3]) : undefined;
criteria = { true: criteriaTrue, false: criteriaFalse };
}
} else if (fnName === 'CHOICE') {
type = 'choice';
const criteriaArg = evalLiteralFn(args[2]);
if (Array.isArray(criteriaArg)) {
criteria = {};
for (const opt of criteriaArg) {
criteria[String(opt)] = null;
}
} else if (typeof criteriaArg === 'object' && criteriaArg !== null) {
criteria = criteriaArg;
} else {
criteria = { yes: null, no: null };
}
} else if (fnName === 'SCORE') {
type = 'score';
const levelsArg = evalLiteralFn(args[2]);
if (Array.isArray(levelsArg)) {
criteria = levelsArg.map(lvl => typeof lvl === 'object' ? lvl : String(lvl));
} else {
criteria = ['Low', 'Medium', 'High'];
}
}
const questionKey = 'q_' + sha256({ type, instructions, criteria }).slice(0, 12);
return {
key: questionKey,
stateExpr: stateArg,
question: {
type,
instructions,
criteria
},
fnName
};
}
/**
* Builds an execution plan for a SELECT statement AST.
*/
export function createQueryPlan(ast, evalLiteralFn = defaultEvalLiteral) {
// Build alias map from SELECT columns
const aliasMap = new Map();
for (const col of ast.columns) {
if (col.alias) {
aliasMap.set(col.alias.toLowerCase(), col.expr);
}
}
// Resolve any alias references in groupBy
if (ast.groupBy) {
ast.groupBy = ast.groupBy.map(g => {
if (g.type === 'Identifier' && !g.table && aliasMap.has(g.name.toLowerCase())) {
return aliasMap.get(g.name.toLowerCase());
}
return g;
});
}
const { deterministic, semantic } = splitWhereClause(ast.where);
// Collect all semantic nodes across the query
const allSemanticNodes = [];
// In columns
for (const col of ast.columns) {
extractSemanticNodes(col.expr, allSemanticNodes);
}
// In WHERE
if (ast.where) {
extractSemanticNodes(ast.where, allSemanticNodes);
}
// In GROUP BY
if (ast.groupBy) {
for (const g of ast.groupBy) {
extractSemanticNodes(g, allSemanticNodes);
}
}
// In HAVING
if (ast.having) {
extractSemanticNodes(ast.having, allSemanticNodes);
}
// In ORDER BY
if (ast.orderBy) {
for (const o of ast.orderBy) {
extractSemanticNodes(o.expr, allSemanticNodes);
}
}
// Map to distinct question descriptors
const questionMap = new Map();
for (const node of allSemanticNodes) {
const desc = buildQuestionDescriptor(node, evalLiteralFn);
if (!questionMap.has(desc.key)) {
questionMap.set(desc.key, desc);
}
// Tag the AST node with its resolved question key
node._questionKey = desc.key;
}
// Detect aggregations
const aggregateCalls = [];
function findAggregates(node) {
if (!node || typeof node !== 'object') return;
if (node.type === 'FunctionCall') {
const name = node.name.toUpperCase();
if (['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'ARRAY_AGG', 'STRING_AGG'].includes(name)) {
aggregateCalls.push(node);
}
}
for (const key of Object.keys(node)) {
if (key !== '_questionKey' && typeof node[key] === 'object') {
findAggregates(node[key]);
}
}
}
for (const col of ast.columns) {
findAggregates(col.expr);
}
const isAggregateQuery = Boolean(ast.groupBy?.length || aggregateCalls.length);
return {
source: ast.from?.source,
alias: ast.from?.alias,
joins: ast.from?.joins || [],
pushdownFilter: deterministic,
semanticFilter: semantic,
questions: Array.from(questionMap.values()),
isAggregateQuery,
aggregates: aggregateCalls,
distinct: ast.distinct,
groupBy: ast.groupBy,
having: ast.having,
orderBy: ast.orderBy,
limit: ast.limit,
offset: ast.offset
};
}