UNPKG

jev-ql

Version:

PostgreSQL-compatible query language powered by TypeSafe Jev System One models

649 lines (585 loc) • 22.2 kB
/** * JevQL Haskell-Style Pattern Matching & Natural Query Language (NQL / PQL) * * Supported syntaxes: * * 1. Haskell Pattern Matching with Guards: * tickets { status: open } * | "immediate outage?" > 0.7 * | dept -> [billing, security, tech] * take 10 * * 2. Haskell Branching Case Pattern: * tickets { status: open } * | "outage?" -> tech * | "security?" -> security * | otherwise -> billing * take 5 * * 3. Haskell List Comprehensions: * [ id, dept | tickets { status: open }, "outage?" > 0.7, dept -> [billing, tech] ] * * 4. Natural Language Queries: * from tickets * where status is open * ask "immediate outage?" as is_outage > 0.7 * tag as billing, security, tech * top 10 by is_outage * * 5. Traditional Pipeline Dataflow: * from tickets | filter status == 'open' | classify ... */ import { parse } from './parser.js'; export function isPipelineQuery(text) { if (typeof text !== 'string') return false; const trimmed = text.trim(); const upper = trimmed.toUpperCase(); if (upper.startsWith('SELECT') || upper.startsWith('EXPLAIN') || upper.startsWith('WITH ')) { return false; } // Haskell List Comprehension: [ id, dept | ... ] if (trimmed.startsWith('[') && trimmed.endsWith(']') && trimmed.includes('|')) { return true; } // Haskell Record Pattern: tickets { status: open } if (/^([a-zA-Z0-9_\.'"\-/\\]+|\$\{.*?\})\s*\{/i.test(trimmed)) { return true; } // Cognitive Syntax: ? "question" if (trimmed.includes('? "') || trimmed.includes("? '") || trimmed.includes('?“') || /^\s*\?\s*["'“]/m.test(trimmed)) { return true; } // Cognitive Syntax: source: filter OR var = opt1 | opt2 OR var = lvl1 .. lvl2 if (/^[a-zA-Z0-9_\.'"\-/\\]+\s*:\s*[a-zA-Z0-9_]/m.test(trimmed)) { return true; } if (/^\w+\s*=\s*[^|\n]+\|/m.test(trimmed) || /^\w+\s*=\s*[^.\n]+\.\./m.test(trimmed)) { return true; } const lower = trimmed.toLowerCase(); if (lower.startsWith('from ') || lower.startsWith('in ') || lower.startsWith('use ')) { return true; } if (trimmed.includes('|')) { return true; } const lines = trimmed.split('\n').map(l => l.trim().toLowerCase()); const nlKeywords = ['where ', 'filter ', 'ask ', 'tag ', 'label ', 'classify ', 'score ', 'rate ', 'top ', 'take ', 'sort ', 'order ', 'show ']; return lines.some(line => nlKeywords.some(kw => line.startsWith(kw))); } function splitClausesRespectingBrackets(str, delimiter = ',') { const clauses = []; let current = ''; let depth = 0; let inQuotes = false; let quoteChar = ''; for (let i = 0; i < str.length; i++) { const char = str[i]; if ((char === '"' || char === "'") && (i === 0 || str[i - 1] !== '\\')) { if (!inQuotes) { inQuotes = true; quoteChar = char; } else if (quoteChar === char) { inQuotes = false; } } if (!inQuotes) { if (char === '[' || char === '{' || char === '(') depth++; else if (char === ']' || char === '}' || char === ')') depth--; else if (char === delimiter && depth === 0) { if (current.trim()) clauses.push(current.trim()); current = ''; continue; } } current += char; } if (current.trim()) { clauses.push(current.trim()); } return clauses; } function normalizeCondition(rawCond) { let c = rawCond.trim(); c = c.replace(/^(where|filter|and)\s+/i, '').trim(); // Replace 'is not' with '!=' c = c.replace(/(\w+)\s+is\s+not\s+([^\s]+)/gi, (match, col, val) => { val = val.trim(); if (/^['"].*['"]$/.test(val) || /^[0-9\.]+$/.test(val) || /^(true|false|null)$/i.test(val)) { return `${col} != ${val}`; } return `${col} != '${val}'`; }); // Replace 'is' with '=' c = c.replace(/(\w+)\s+is\s+([^\s]+)/gi, (match, col, val) => { val = val.trim(); if (/^['"].*['"]$/.test(val) || /^[0-9\.]+$/.test(val) || /^(true|false|null)$/i.test(val)) { return `${col} = ${val}`; } return `${col} = '${val}'`; }); // Replace '==' with '=' c = c.replace(/==/g, '='); return c; } /** * Transpiles JevQL Pattern Matching / Natural / Pipeline syntax into standard JevQL SQL. */ export function pipelineToSQL(queryStr) { let text = queryStr.trim(); const comprehensionProjections = []; // Check Haskell List Comprehension: [ proj1, proj2 | source { pattern }, guards... ] if (text.startsWith('[') && text.endsWith(']') && text.includes('|')) { const inner = text.slice(1, -1).trim(); const pipeIdx = inner.indexOf('|'); const projPart = inner.slice(0, pipeIdx).trim(); const bodyPart = inner.slice(pipeIdx + 1).trim(); comprehensionProjections.push(...projPart.split(',').map(s => s.trim()).filter(Boolean)); text = bodyPart; } // Split into stages let rawStages = []; if (text.includes('\n')) { rawStages = text.split('\n') .map(p => p.trim().replace(/^([#]|--).*$/, '').trim()) .filter(Boolean); } else if (text.includes('|')) { rawStages = splitClausesRespectingBrackets(text, '|'); } else { rawStages = splitClausesRespectingBrackets(text, ','); } let source = null; const filters = []; const semanticEnrichments = []; const projections = [...comprehensionProjections]; let groupBy = []; const aggregates = []; let having = []; const orderBy = []; let limit = null; let offset = null; const caseBranches = []; // For Haskell branching case pattern for (let idx = 0; idx < rawStages.length; idx++) { const raw = rawStages[idx].trim(); const stage = raw.replace(/^\|\s*/, '').trim(); // 0. Haskell Branching Case Guard: "prompt?" -> result OR otherwise -> result const caseMatch = stage.match(/^(?:["'“]([^"'”]+)["'”]|(otherwise|_))\s*(?:->|=>)\s*(\w+)$/i); if (caseMatch) { const prompt = caseMatch[1]; const isElse = Boolean(caseMatch[2]); const result = caseMatch[3]; caseBranches.push({ prompt, isElse, result }); continue; } // Cognitive 1: Question with `?` (e.g. ? "immediate outage?" > 0.7) const cognitiveQuestionMatch = stage.match(/^(?:(\w+)\s+)?\?\s*["'“]([^"'”]+)["'”](?:\s*(>|<|>=|<=)\s*([0-9\.]+))?(?:\s+as\s+(\w+))?/i); if (cognitiveQuestionMatch) { const col = cognitiveQuestionMatch[1] || 'auto'; const prompt = cognitiveQuestionMatch[2].replace(/'/g, "\\'"); const op = cognitiveQuestionMatch[3] || '>'; const thresh = cognitiveQuestionMatch[4] || '0.5'; const words = cognitiveQuestionMatch[2].replace(/[^\w\s]/g, '').trim().split(/\s+/); const defaultAlias = `is_${words.slice(0, 3).join('_').toLowerCase()}`; const alias = cognitiveQuestionMatch[5] || defaultAlias; filters.push(`NOUL(${col}, '${prompt}') ${op} ${thresh}`); semanticEnrichments.push({ type: 'NOUL', col, prompt, alias }); if (orderBy.length === 0) { orderBy.push(`${alias} DESC`); } continue; } // Cognitive 2: Categorical Choice with `|` (e.g. dept = billing | security | tech) const cognitiveChoiceMatch = stage.match(/^(\w+)\s*(?:=|:)\s*([^|\n]+(?:\|[^|\n]+)+)$/i); if (cognitiveChoiceMatch) { const alias = cognitiveChoiceMatch[1]; const opts = cognitiveChoiceMatch[2].split('|').map(s => s.trim().replace(/^['"`]|['"`]$/g, '')).filter(Boolean); const criteria = `[${opts.map(o => `'${o.replace(/'/g, "\\'")}'`).join(', ')}]`; semanticEnrichments.push({ type: 'CHOICE', col: 'auto', prompt: `Classify ${alias}`, criteria, alias }); continue; } // Cognitive 3: Continuous Score with `..` (e.g. urgency = low .. medium .. high) const cognitiveScoreMatch = stage.match(/^(\w+)\s*(?:=|:)\s*([^.\n]+(?:\.\.[^.\n]+)+)$/i); if (cognitiveScoreMatch) { const alias = cognitiveScoreMatch[1]; const lvls = cognitiveScoreMatch[2].split('..').map(s => s.trim().replace(/^['"`]|['"`]$/g, '')).filter(Boolean); const criteria = `[${lvls.map(l => `'${l.replace(/'/g, "\\'")}'`).join(', ')}]`; semanticEnrichments.push({ type: 'SCORE', col: 'auto', prompt: `Rate ${alias}`, criteria, alias }); continue; } // Cognitive 4: Source with Constraints via `:` (e.g. tickets: status = open, priority = P1) const cognitiveSourceMatch = stage.match(/^([a-zA-Z0-9_\.'"\-/\\]+|\$\{.*?\})\s*:\s*([^|\n.]+)$/i); if (cognitiveSourceMatch && !stage.includes('|') && !stage.includes('..') && !stage.toLowerCase().startsWith('from ') && !stage.toLowerCase().startsWith('select ')) { source = cognitiveSourceMatch[1].trim(); const filterBody = cognitiveSourceMatch[2].trim(); if (filterBody) { const props = splitClausesRespectingBrackets(filterBody); for (const prop of props) { const eqMatch = prop.match(/^(\w+)\s*(:|!=|==|=|>|<|>=|<=)\s*(.+)$/); if (eqMatch) { const col = eqMatch[1].trim(); let op = eqMatch[2].trim(); if (op === ':' || op === '==') op = '='; let val = eqMatch[3].trim(); if (!/^['"].*['"]$/.test(val) && !/^[0-9\.]+$/.test(val) && !/^(true|false|null)$/i.test(val)) { val = `'${val}'`; } filters.push(`${col} ${op} ${val}`); } else if (prop) { filters.push(`status = '${prop.replace(/['"]/g, '')}'`); } } } continue; } // 1. Haskell Record Pattern: source { status: open, priority: P1 } const recordMatch = stage.match(/^([a-zA-Z0-9_\.'"\-/\\]+|\$\{.*?\})\s*\{([^}]*)\}/i); if (recordMatch) { source = recordMatch[1].trim(); const patternBody = recordMatch[2].trim(); if (patternBody) { const props = splitClausesRespectingBrackets(patternBody); for (const prop of props) { const eqMatch = prop.match(/^(\w+)\s*(:|!=|==|=|>|<|>=|<=)\s*(.+)$/); if (eqMatch) { const col = eqMatch[1].trim(); let op = eqMatch[2].trim(); if (op === ':' || op === '==') op = '='; let val = eqMatch[3].trim(); if (!/^['"].*['"]$/.test(val) && !/^[0-9\.]+$/.test(val) && !/^(true|false|null)$/i.test(val)) { val = `'${val}'`; } filters.push(`${col} ${op} ${val}`); } else if (prop) { filters.push(`${prop} = true`); } } } continue; } // 2. Haskell Classification Guard: dept -> [billing, security, tech] or dept <- [a, b] const choiceGuardMatch = stage.match(/^(\w+)\s*(?:->|<-|in)\s*\[([^\]]+)\]/i); if (choiceGuardMatch) { const alias = choiceGuardMatch[1]; const opts = choiceGuardMatch[2].split(',').map(s => s.trim().replace(/^['"`]|['"`]$/g, '')).filter(Boolean); const criteria = `[${opts.map(o => `'${o.replace(/'/g, "\\'")}'`).join(', ')}]`; semanticEnrichments.push({ type: 'CHOICE', col: 'auto', prompt: `Classify ${alias}`, criteria, alias }); continue; } // 3. Haskell Scoring Guard: urgency ~> [low, medium, high] const scoreGuardMatch = stage.match(/^(\w+)\s*~>\s*\[([^\]]+)\]/i); if (scoreGuardMatch) { const alias = scoreGuardMatch[1]; const lvls = scoreGuardMatch[2].split(',').map(s => s.trim().replace(/^['"`]|['"`]$/g, '')).filter(Boolean); const criteria = `[${lvls.map(l => `'${l.replace(/'/g, "\\'")}'`).join(', ')}]`; semanticEnrichments.push({ type: 'SCORE', col: 'auto', prompt: `Rate ${alias}`, criteria, alias }); continue; } // 4. Haskell Predicate Guard: "immediate outage?" > 0.7 or "immediate outage?" const predGuardMatch = stage.match(/^["'“]([^"'”]+)["'”](?:\s*(>|<|>=|<=)\s*([0-9\.]+))?$/i); if (predGuardMatch) { const prompt = predGuardMatch[1]; const op = predGuardMatch[2] || '>'; const thresh = predGuardMatch[3] || '0.5'; const cleanPrompt = prompt.replace(/'/g, "\\'"); const words = prompt.replace(/[^\w\s]/g, '').trim().split(/\s+/); const alias = `is_${words.slice(0, 3).join('_').toLowerCase()}`; filters.push(`NOUL(auto, '${cleanPrompt}') ${op} ${thresh}`); semanticEnrichments.push({ type: 'NOUL', col: 'auto', prompt: cleanPrompt, alias }); if (orderBy.length === 0) { orderBy.push(`${alias} DESC`); } continue; } // 5. FROM / IN / USE stage if (/^(from|in|use)\s+/i.test(stage)) { source = stage.replace(/^(from|in|use)\s+/i, '').trim(); continue; } // 6. ASK / JUDGE / CHECK stage (Natural language) if (/^(ask|check|judge)\s+/i.test(stage) || /^judge\s+\w+\s*\?/i.test(stage)) { let m = stage.match(/^judge\s+(\w+)\s*\?\s*["'“]([^"'”]+)["'”](?:\s+as\s+(\w+))?(?:\s*(>|<|>=|<=)\s*([0-9\.]+))?/i); let col, prompt, alias, op, thresh; if (m) { [, col, prompt, alias, op, thresh] = m; } else { m = stage.match(/^(?:ask|check)\s+(?:(\w+)\s+)?["'“]([^"'”]+)["'”](?:\s+as\s+(\w+))?(?:\s*(>|<|>=|<=)\s*([0-9\.]+))?/i); if (m) [, col, prompt, alias, op, thresh] = m; } if (m) { const targetCol = col || 'auto'; const cleanPrompt = prompt.replace(/'/g, "\\'"); const words = prompt.replace(/[^\w\s]/g, '').trim().split(/\s+/); const defaultAlias = `is_${words.slice(0, 3).join('_').toLowerCase()}`; const outAlias = alias || defaultAlias; semanticEnrichments.push({ type: 'NOUL', col: targetCol, prompt: cleanPrompt, alias: outAlias }); if (op && thresh) { filters.push(`NOUL(${targetCol}, '${cleanPrompt}') ${op} ${thresh}`); } continue; } } // 7. TAG / CLASSIFY / LABEL / CATEGORIZE stage if (/^(tag|label|classify|categorize|pick)\s+/i.test(stage)) { let col, optionsRaw, alias; let m = stage.match(/^(?:classify|tag)\s+(\w+)\s*->\s*(\[.*?\]|\{.*?\})\s+as\s+(\w+)/i); if (m) { [, col, optionsRaw, alias] = m; } else { m = stage.match(/^(?:tag|label|classify|categorize|pick)\s+(?:(\w+)\s+)?(?:as|from|:\s*)\s*(?:\[([^\]]+)\]|([^\n\r]+?))(?:\s+as\s+(\w+))?$/i); if (m) { const explicitCol = m[1]; const optsString = m[2] || m[3]; alias = m[4]; col = explicitCol || 'auto'; let opts = []; if (optsString) { opts = optsString.split(',').map(s => s.trim().replace(/^['"`]|['"`]$/g, '')).filter(Boolean); } optionsRaw = `[${opts.map(o => `'${o.replace(/'/g, "\\'")}'`).join(', ')}]`; } } if (m) { const targetCol = col || 'auto'; const targetAlias = alias || (stage.startsWith('tag') ? 'tag' : 'category'); semanticEnrichments.push({ type: 'CHOICE', col: targetCol, prompt: `Classify ${targetAlias}`, criteria: optionsRaw, alias: targetAlias }); continue; } } // 8. SCORE / RATE stage if (/^(score|rate)\s+/i.test(stage)) { let col, levelsRaw, alias; let m = stage.match(/^score\s+(\w+)\s*~>\s*(\[.*?\])\s+as\s+(\w+)/i); if (m) { [, col, levelsRaw, alias] = m; } else { m = stage.match(/^(?:score|rate)\s+(?:(\w+)\s+)?(?:as|:\s*)\s*(?:\[([^\]]+)\]|([^\n\r]+?))(?:\s+as\s+(\w+))?$/i); if (m) { const explicitCol = m[1]; const lvlsString = m[2] || m[3]; alias = m[4]; col = explicitCol || 'auto'; let lvls = []; if (lvlsString) { lvls = lvlsString.split(',').map(s => s.trim().replace(/^['"`]|['"`]$/g, '')).filter(Boolean); } levelsRaw = `[${lvls.map(l => `'${l.replace(/'/g, "\\'")}'`).join(', ')}]`; } } if (m) { const targetCol = col || 'auto'; const targetAlias = alias || 'score'; semanticEnrichments.push({ type: 'SCORE', col: targetCol, prompt: `Rate ${targetAlias}`, criteria: levelsRaw, alias: targetAlias }); continue; } } // 9. TOP / TAKE / LIMIT / FIRST stage if (/^(top|take|limit|first)\s+(\d+)(?:\s+by\s+(.+))?/i.test(stage)) { const m = stage.match(/^(top|take|limit|first)\s+(\d+)(?:\s+by\s+(.+))?/i); limit = m[2]; if (m[3]) { const sortField = m[3].trim(); if (/(\bdesc|\basc)$/i.test(sortField)) { orderBy.push(sortField.toUpperCase()); } else { orderBy.push(`${sortField} DESC`); } } continue; } // 10. SORT / ORDER stage if (/^(sort|order)\s+(by\s+)?(.+)/i.test(stage)) { const items = stage.replace(/^(sort|order)\s+(by\s+)?/i, '').split(',').map(s => s.trim()); for (const item of items) { if (item.startsWith('-')) orderBy.push(`${item.slice(1)} DESC`); else if (item.startsWith('+')) orderBy.push(`${item.slice(1)} ASC`); else if (/\s+(desc|asc)$/i.test(item)) orderBy.push(item); else orderBy.push(`${item} ASC`); } continue; } if (/^highest\s+(\w+)/i.test(stage)) { orderBy.push(`${stage.replace(/^highest\s+/i, '').trim()} DESC`); continue; } if (/^lowest\s+(\w+)/i.test(stage)) { orderBy.push(`${stage.replace(/^lowest\s+/i, '').trim()} ASC`); continue; } // 11. FILTER / WHERE / AND stage if (/^(where|filter|and)\s+/i.test(stage) || /^\w+\s+is\s+/i.test(stage)) { const cond = normalizeCondition(stage); if (groupBy.length > 0) having.push(cond); else filters.push(cond); continue; } // 12. SHOW / SELECT / KEEP stage if (/^(show|select|keep)\s+/i.test(stage)) { const cols = stage.replace(/^(show|select|keep)\s+/i, '').split(',').map(s => s.trim()); projections.push(...cols); continue; } // 13. GROUP stage if (/^group\s+/i.test(stage)) { const cols = stage.replace(/^group\s+(by\s+)?/i, '').split(',').map(s => s.trim()); groupBy = cols; continue; } // 14. AGGREGATE / COUNT stage if (/^(aggregate|agg)\s+/i.test(stage)) { const exprs = stage.replace(/^(aggregate|agg)\s+/i, '').split(',').map(s => s.trim()); for (const e of exprs) { if (/^count\(\s*\)$/i.test(e)) aggregates.push('COUNT(*) AS count'); else aggregates.push(e); } continue; } if (/^count$/i.test(stage)) { aggregates.push('COUNT(*) AS count'); continue; } // 15. Fallback for first line as source name if (idx === 0 && !source && !stage.includes(' ')) { source = stage; continue; } } // Assemble branching CASE statement if case branches exist let caseExpressionStr = null; if (caseBranches.length > 0) { const whenClauses = []; let elseClause = "'other'"; for (const b of caseBranches) { if (b.isElse) { elseClause = `'${b.result}'`; } else { whenClauses.push(`WHEN NOUL(auto, '${b.prompt.replace(/'/g, "\\'")}') > 0.5 THEN '${b.result}'`); } } caseExpressionStr = `CASE ${whenClauses.join(' ')} ELSE ${elseClause} END AS category`; } function resolveEnrichment(exprStr) { let res = exprStr; for (const enr of semanticEnrichments) { let callStr = ''; if (enr.type === 'NOUL') callStr = `NOUL(${enr.col}, '${enr.prompt.replace(/'/g, "\\'")}')`; else if (enr.type === 'CHOICE') callStr = `CHOICE(${enr.col}, '${enr.prompt}', ${enr.criteria})`; else if (enr.type === 'SCORE') callStr = `SCORE(${enr.col}, '${enr.prompt}', ${enr.criteria})`; if (res === enr.alias) { return `${callStr} AS ${enr.alias}`; } const aliasRegex = new RegExp(`\\b${enr.alias}\\b`, 'g'); if (aliasRegex.test(res)) { res = res.replace(aliasRegex, callStr); } } return res; } // Construct standard SQL let selectCols = []; if (groupBy.length > 0 || aggregates.length > 0) { selectCols = [...groupBy, ...aggregates].map(col => resolveEnrichment(col)); } else if (projections.length > 0) { selectCols = projections.map(col => resolveEnrichment(col)); if (caseExpressionStr) { selectCols.push(caseExpressionStr); } } else { // Default projection: all columns plus enrichments selectCols = ['*']; for (const enr of semanticEnrichments) { if (enr.type === 'NOUL') { selectCols.push(`NOUL(${enr.col}, '${enr.prompt.replace(/'/g, "\\'")}') AS ${enr.alias}`); } else if (enr.type === 'CHOICE') { selectCols.push(`CHOICE(${enr.col}, '${enr.prompt}', ${enr.criteria}) AS ${enr.alias}`); } else if (enr.type === 'SCORE') { selectCols.push(`SCORE(${enr.col}, '${enr.prompt}', ${enr.criteria}) AS ${enr.alias}`); } } if (caseExpressionStr) { selectCols.push(caseExpressionStr); } } const resolvedGroupBy = groupBy.map(g => { const r = resolveEnrichment(g); return r.replace(/\s+AS\s+\w+$/i, ''); }); const sqlParts = [ `SELECT ${selectCols.join(', ')}`, `FROM ${source || 'data'}` ]; if (filters.length > 0) { sqlParts.push(`WHERE ${filters.join(' AND ')}`); } if (groupBy.length > 0) { sqlParts.push(`GROUP BY ${resolvedGroupBy.join(', ')}`); } if (having.length > 0) { sqlParts.push(`HAVING ${having.join(' AND ')}`); } if (orderBy.length > 0) { const resolvedOrderBy = orderBy.map(o => o.replace(/\bcount\b/i, 'COUNT(*)')); sqlParts.push(`ORDER BY ${resolvedOrderBy.join(', ')}`); } if (limit) { sqlParts.push(`LIMIT ${limit}`); } if (offset) { sqlParts.push(`OFFSET ${offset}`); } return sqlParts.join('\n'); } export function compilePipeline(pipelineCode) { const sql = pipelineToSQL(pipelineCode); return parse(sql); }