claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
905 lines (904 loc) • 38.1 kB
JavaScript
/**
* Query Translator - SECURITY HARDENED
*
* Translates between SQL queries and Redis commands for cross-backend compatibility.
* Includes query optimization and backend recommendation logic.
*
* SECURITY ENHANCEMENTS (CVE-2024-SQL-INJECTION):
* - Input validation with whitelisting for identifiers (table/column names)
* - Parameterized queries for ALL database values
* - Comprehensive SQL injection prevention
* - Strict identifier validation against allowed patterns
* - Error handling using StandardError
*
* Part of Phase 2, Task P2-3.1: Unified Query API
*
* Features:
* - SQL to Redis translation with security validation
* - Redis to SQL translation with parameterization
* - Query optimization
* - Backend recommendation based on query patterns
* - Performance monitoring (<50ms translation time)
*
* @example
* ```typescript
* const translator = new QueryTranslator({
* allowedTables: ['tasks', 'users', 'projects'],
* allowedFields: { tasks: ['id', 'name', 'status', 'description'] }
* });
*
* // Translate SQL to Redis (fully parameterized)
* const redisCmd = translator.translateSQLToRedis(
* 'SELECT * FROM tasks WHERE id = ?',
* ['task-123']
* );
*
* // Translate Redis to SQL (fully parameterized)
* const sqlQuery = translator.translateRedisToSQL({
* command: 'HGETALL',
* key: 'task:123'
* });
* ```
*/ import { BackendType } from './unified-query-api.js';
import { StandardError, ErrorCode } from './errors.js';
/**
* SQL query parser - SECURITY HARDENED
*/ let SQLParser = class SQLParser {
allowedTables;
allowedFields;
strictMode;
constructor(allowedTables, allowedFields, strictMode = true){
this.allowedTables = new Set(allowedTables || []);
this.allowedFields = new Map();
if (allowedFields) {
for (const [table, fields] of Object.entries(allowedFields)){
this.allowedFields.set(table.toLowerCase(), new Set(fields.map((f)=>f.toLowerCase())));
}
}
this.strictMode = strictMode;
}
/**
* Validate SQL identifier (table/column name)
* Pattern: alphanumeric, underscore, starts with letter or underscore
*/ validateIdentifier(identifier, type) {
if (!identifier || typeof identifier !== 'string') {
return {
valid: false,
error: `Invalid ${type} name: must be a non-empty string`
};
}
// Remove surrounding whitespace
const trimmed = identifier.trim();
// Check pattern: must start with letter or underscore, contain only alphanumeric and underscore
const identifierPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
if (!identifierPattern.test(trimmed)) {
return {
valid: false,
error: `Invalid ${type} name: must match pattern /^[a-zA-Z_][a-zA-Z0-9_]*$/. Got: "${identifier}"`
};
}
// Check length (reasonable limit to prevent DoS)
if (trimmed.length > 128) {
return {
valid: false,
error: `Invalid ${type} name: exceeds maximum length of 128 characters`
};
}
return {
valid: true,
sanitized: trimmed
};
}
/**
* Validate table name against whitelist
*/ validateTableName(table) {
const validation = this.validateIdentifier(table, 'table');
if (!validation.valid) {
return validation;
}
const sanitized = validation.sanitized.toLowerCase();
// In strict mode, check against whitelist
if (this.strictMode && this.allowedTables.size > 0) {
if (!this.allowedTables.has(sanitized)) {
return {
valid: false,
error: `Table "${table}" is not in the whitelist of allowed tables`
};
}
}
return {
valid: true,
sanitized: validation.sanitized
};
}
/**
* Validate field name against whitelist for specific table
*/ validateFieldName(field, table) {
const validation = this.validateIdentifier(field, 'field');
if (!validation.valid) {
return validation;
}
const sanitized = validation.sanitized.toLowerCase();
// In strict mode, check against whitelist if available
if (this.strictMode && table) {
const tableLower = table.toLowerCase();
const allowedFieldsForTable = this.allowedFields.get(tableLower);
if (allowedFieldsForTable && !allowedFieldsForTable.has(sanitized)) {
return {
valid: false,
error: `Field "${field}" is not in the whitelist for table "${table}"`
};
}
}
return {
valid: true,
sanitized: validation.sanitized
};
}
/**
* Parse SQL SELECT statement
*/ parseSelect(sql) {
const result = {};
try {
// Extract table name
const tableMatch = sql.match(/FROM\s+(\w+)/i);
if (tableMatch) {
const tableValidation = this.validateTableName(tableMatch[1]);
if (!tableValidation.valid) {
return {
error: tableValidation.error
};
}
result.table = tableValidation.sanitized;
}
// Extract fields
const fieldsMatch = sql.match(/SELECT\s+(.*?)\s+FROM/i);
if (fieldsMatch) {
const fields = fieldsMatch[1].trim();
if (fields === '*') {
result.fields = [
'*'
];
} else {
const fieldList = fields.split(',').map((f)=>f.trim());
const validatedFields = [];
for (const field of fieldList){
const fieldValidation = this.validateFieldName(field, result.table);
if (!fieldValidation.valid) {
return {
error: fieldValidation.error
};
}
validatedFields.push(fieldValidation.sanitized);
}
result.fields = validatedFields;
}
}
// Extract WHERE clause
const whereMatch = sql.match(/WHERE\s+(.*?)(?:ORDER BY|GROUP BY|LIMIT|$)/i);
if (whereMatch) {
const whereClause = whereMatch[1].trim();
const whereResult = this.parseWhereClause(whereClause, result.table);
if (whereResult.error) {
return {
error: whereResult.error
};
}
result.where = whereResult.conditions;
}
// Extract JOINs
const joinMatches = sql.matchAll(/(?:INNER |LEFT |RIGHT |)?JOIN\s+(\w+)\s+ON\s+(.*?)(?:WHERE|ORDER BY|GROUP BY|LIMIT|JOIN|$)/gi);
result.joins = [];
for (const match of joinMatches){
const joinTableValidation = this.validateTableName(match[1]);
if (!joinTableValidation.valid) {
return {
error: joinTableValidation.error
};
}
result.joins.push({
table: joinTableValidation.sanitized,
on: match[2].trim()
});
}
return result;
} catch (error) {
return {
error: `Failed to parse SELECT statement: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
/**
* Parse WHERE clause with validation
*/ parseWhereClause(whereClause, table) {
const conditions = [];
try {
// Simple parser for basic conditions
// Format: field = ? OR field LIKE ? etc.
const parts = whereClause.split(/\s+AND\s+/i);
for (const part of parts){
const match = part.match(/(\w+)\s*(=|!=|>|>=|<|<=|LIKE|IN|NOT IN)\s*(.+)/i);
if (match) {
const fieldValidation = this.validateFieldName(match[1], table);
if (!fieldValidation.valid) {
return {
error: fieldValidation.error
};
}
conditions.push({
field: fieldValidation.sanitized,
operator: match[2].toLowerCase(),
value: match[3] === '?' ? undefined : match[3]
});
}
}
return {
conditions
};
} catch (error) {
return {
error: `Failed to parse WHERE clause: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
/**
* Parse SQL INSERT statement with validation
*/ parseInsert(sql) {
const result = {};
try {
// Extract table name
const tableMatch = sql.match(/INSERT INTO\s+(\w+)/i);
if (tableMatch) {
const tableValidation = this.validateTableName(tableMatch[1]);
if (!tableValidation.valid) {
return {
error: tableValidation.error
};
}
result.table = tableValidation.sanitized;
}
// Extract fields
const fieldsMatch = sql.match(/\(([^)]+)\)\s+VALUES/i);
if (fieldsMatch) {
const fieldList = fieldsMatch[1].split(',').map((f)=>f.trim());
const validatedFields = [];
for (const field of fieldList){
const fieldValidation = this.validateFieldName(field, result.table);
if (!fieldValidation.valid) {
return {
error: fieldValidation.error
};
}
validatedFields.push(fieldValidation.sanitized);
}
result.fields = validatedFields;
}
return result;
} catch (error) {
return {
error: `Failed to parse INSERT statement: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
/**
* Parse SQL UPDATE statement with validation
*/ parseUpdate(sql) {
const result = {};
try {
// Extract table name
const tableMatch = sql.match(/UPDATE\s+(\w+)/i);
if (tableMatch) {
const tableValidation = this.validateTableName(tableMatch[1]);
if (!tableValidation.valid) {
return {
error: tableValidation.error
};
}
result.table = tableValidation.sanitized;
}
// Extract SET clause
const setMatch = sql.match(/SET\s+(.*?)\s+WHERE/i);
if (setMatch) {
const setParts = setMatch[1].split(',');
const validatedFields = [];
for (const part of setParts){
const field = part.split('=')[0].trim();
const fieldValidation = this.validateFieldName(field, result.table);
if (!fieldValidation.valid) {
return {
error: fieldValidation.error
};
}
validatedFields.push(fieldValidation.sanitized);
}
result.fields = validatedFields;
}
// Extract WHERE clause
const whereMatch = sql.match(/WHERE\s+(.*?)$/i);
if (whereMatch) {
const whereResult = this.parseWhereClause(whereMatch[1].trim(), result.table);
if (whereResult.error) {
return {
error: whereResult.error
};
}
result.where = whereResult.conditions;
}
return result;
} catch (error) {
return {
error: `Failed to parse UPDATE statement: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
/**
* Parse SQL DELETE statement with validation
*/ parseDelete(sql) {
const result = {};
try {
// Extract table name
const tableMatch = sql.match(/DELETE FROM\s+(\w+)/i);
if (tableMatch) {
const tableValidation = this.validateTableName(tableMatch[1]);
if (!tableValidation.valid) {
return {
error: tableValidation.error
};
}
result.table = tableValidation.sanitized;
}
// Extract WHERE clause
const whereMatch = sql.match(/WHERE\s+(.*?)$/i);
if (whereMatch) {
const whereResult = this.parseWhereClause(whereMatch[1].trim(), result.table);
if (whereResult.error) {
return {
error: whereResult.error
};
}
result.where = whereResult.conditions;
}
return result;
} catch (error) {
return {
error: `Failed to parse DELETE statement: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
};
/**
* Query Translator - SECURITY HARDENED
*
* Provides bidirectional translation between SQL and Redis commands
* with comprehensive input validation and SQL injection prevention
*/ export class QueryTranslator {
parser;
config;
constructor(config){
this.config = {
allowedTables: config?.allowedTables || [],
allowedFields: config?.allowedFields || {},
maxQueryLength: config?.maxQueryLength || 10000,
maxParams: config?.maxParams || 100,
strictMode: config?.strictMode !== false
};
this.parser = new SQLParser(this.config.allowedTables, this.config.allowedFields, this.config.strictMode);
}
/**
* Validate input parameters
*/ validateInput(sql, params = []) {
if (!sql || typeof sql !== 'string') {
return {
valid: false,
error: 'SQL query must be a non-empty string'
};
}
if (sql.length > this.config.maxQueryLength) {
return {
valid: false,
error: `SQL query exceeds maximum length of ${this.config.maxQueryLength} characters`
};
}
if (!Array.isArray(params)) {
return {
valid: false,
error: 'Parameters must be an array'
};
}
if (params.length > this.config.maxParams) {
return {
valid: false,
error: `Too many parameters. Maximum: ${this.config.maxParams}`
};
}
// Detect SQL injection patterns in the query
// These patterns should ONLY appear in specific contexts (after ?)
const injectionPatterns = [
/\bOR\b[\s]*(\d+\s*=\s*\d+|'[^']*'\s*=\s*'[^']*'|true|1)/i,
/\bUNION\b[\s]+(SELECT|ALL)/i,
/--\s*$/i,
/\/\*[\s\S]*?\*\//i,
/;\s*(SELECT|INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER)/i,
/\bDROP\b|\bTRUNCATE\b|\bALTER\b|\bCREATE\b/i,
/\bEVAL\b|\bEXEC\b|\bSCRIPT\b/i,
/\x00/
];
// Split query into parts based on ? placeholders
const parts = sql.split('?');
// Check structure: should be [prefix, suffix] or [prefix1, middle1, middle2, suffix]
// Injection attempts will have SQL keywords AFTER the ?
for(let i = 0; i < parts.length; i++){
const part = parts[i];
// First part should contain SELECT/INSERT/UPDATE/DELETE/FROM
if (i === 0) {
// Validate it's a valid SQL statement start
if (!/(SELECT|INSERT|UPDATE|DELETE|FROM)\b/i.test(part)) {
// Only valid if it's completely empty (error caught elsewhere)
if (part.trim()) {
return {
valid: false,
error: 'Invalid SQL query structure. Must start with SELECT, INSERT, UPDATE, or DELETE'
};
}
}
} else if (i === parts.length - 1) {
// Last part (after last ?) should not have SQL keywords that indicate injection
if (/\b(OR|UNION|SELECT|INSERT|UPDATE|DELETE|DROP|TRUNCATE)\b/i.test(part)) {
return {
valid: false,
error: 'SQL query contains suspicious injection patterns'
};
}
} else {
// Middle parts (between ? and ?) should be minimal
// Should only contain WHERE, AND, OR (as operators), commas, etc.
// But NOT SELECT, UNION, etc.
if (/\b(UNION|SELECT|INSERT|UPDATE|DELETE|DROP)\b/i.test(part)) {
return {
valid: false,
error: 'SQL query contains suspicious injection patterns'
};
}
}
}
// Check for injection patterns in the entire query
for (const pattern of injectionPatterns){
// Skip OR if it's part of valid syntax (after WHERE)
if (pattern.source.includes('OR') && /WHERE[\s\S]*\?[\s\S]*OR/i.test(sql)) {
// This could be valid OR in WHERE clause
const afterQuestion = sql.substring(sql.indexOf('?') + 1);
if (/\bOR\b[\s]*[\'\"]?\d+['\"]*\s*=\s*[\'\"]?\d+['\"]*|OR\s*'[^']*'\s*=\s*'[^']*'|OR\s*TRUE/i.test(afterQuestion)) {
return {
valid: false,
error: 'SQL query contains SQL injection pattern: OR-based bypass'
};
}
} else if (pattern.test(sql) && !sql.includes('?')) {
// Pattern found but no parameterization
return {
valid: false,
error: 'SQL query contains suspicious patterns. Use parameterized queries.'
};
}
}
return {
valid: true
};
}
/**
* Validate Redis command structure
*/ validateRedisCommand(command) {
if (!command || typeof command !== 'object') {
return {
valid: false,
error: 'Redis command must be an object'
};
}
if (!command.command || typeof command.command !== 'string') {
return {
valid: false,
error: 'Redis command name must be a non-empty string'
};
}
const allowedCommands = [
'GET',
'SET',
'HGET',
'HGETALL',
'HMSET',
'HSET',
'DEL',
'MGET',
'MSET'
];
if (!allowedCommands.includes(command.command.toUpperCase())) {
return {
valid: false,
error: `Redis command "${command.command}" is not allowed. Allowed: ${allowedCommands.join(', ')}`
};
}
if (command.key && typeof command.key !== 'string') {
return {
valid: false,
error: 'Redis key must be a string'
};
}
if (command.fields && typeof command.fields !== 'object') {
return {
valid: false,
error: 'Redis fields must be an object'
};
}
if (command.args && !Array.isArray(command.args)) {
return {
valid: false,
error: 'Redis args must be an array'
};
}
return {
valid: true
};
}
/**
* Translate SQL query to Redis commands
*/ translateSQLToRedis(sql, params = []) {
const startTime = Date.now();
const warnings = [];
try {
// Validate inputs
const inputValidation = this.validateInput(sql, params);
if (!inputValidation.valid) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, inputValidation.error || 'Invalid input', {
sql,
paramCount: params.length
});
}
// Determine query type
const queryType = this.getQueryType(sql);
let redisCommand;
let recommendedBackend = BackendType.REDIS;
switch(queryType){
case 'SELECT':
{
const selectParsed = this.parser.parseSelect(sql);
if (selectParsed.error) {
throw new StandardError(ErrorCode.PARSE_ERROR, `Failed to parse SELECT statement: ${selectParsed.error}`, {
sql
});
}
// Check if query is complex (has joins)
if (selectParsed.joins && selectParsed.joins.length > 0) {
warnings.push('Complex queries with JOINs are better suited for PostgreSQL');
recommendedBackend = BackendType.POSTGRES;
}
// Translate to Redis HGETALL or GET
if (selectParsed.where && selectParsed.where.length > 0) {
const idCondition = selectParsed.where.find((w)=>w.field === 'id');
if (idCondition) {
const keyValue = params[0];
// Validate key value
if (typeof keyValue !== 'string' && typeof keyValue !== 'number') {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Redis key value must be a string or number', {
paramValue: typeof keyValue
});
}
const redisKey = `${selectParsed.table}:${keyValue}`;
redisCommand = {
command: selectParsed.fields?.[0] === '*' ? 'HGETALL' : 'HGET',
key: redisKey
};
}
}
break;
}
case 'INSERT':
{
const insertParsed = this.parser.parseInsert(sql);
if (insertParsed.error) {
throw new StandardError(ErrorCode.PARSE_ERROR, `Failed to parse INSERT statement: ${insertParsed.error}`, {
sql
});
}
if (insertParsed.table && insertParsed.fields) {
const idValue = params[0];
// Validate key value
if (typeof idValue !== 'string' && typeof idValue !== 'number') {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Redis key value must be a string or number', {
paramValue: typeof idValue
});
}
const redisKey = `${insertParsed.table}:${idValue}`;
// Build field-value pairs with validation
const fields = {};
for(let i = 0; i < insertParsed.fields.length && i < params.length; i++){
// Ensure params are not objects/arrays (prevent injection)
const paramValue = params[i];
if (typeof paramValue === 'object' && paramValue !== null) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Parameter values must be primitives (string, number, boolean, null)', {
paramIndex: i,
paramType: typeof paramValue
});
}
fields[insertParsed.fields[i]] = paramValue;
}
redisCommand = {
command: 'HMSET',
key: redisKey,
fields
};
}
break;
}
case 'UPDATE':
{
const updateParsed = this.parser.parseUpdate(sql);
if (updateParsed.error) {
throw new StandardError(ErrorCode.PARSE_ERROR, `Failed to parse UPDATE statement: ${updateParsed.error}`, {
sql
});
}
if (updateParsed.table && updateParsed.where) {
const idCondition = updateParsed.where.find((w)=>w.field === 'id');
if (idCondition) {
const keyValue = params[params.length - 1];
// Validate key value
if (typeof keyValue !== 'string' && typeof keyValue !== 'number') {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Redis key value must be a string or number', {
paramValue: typeof keyValue
});
}
const redisKey = `${updateParsed.table}:${keyValue}`;
redisCommand = {
command: 'HSET',
key: redisKey,
args: params.slice(0, -1)
};
}
}
break;
}
case 'DELETE':
{
const deleteParsed = this.parser.parseDelete(sql);
if (deleteParsed.error) {
throw new StandardError(ErrorCode.PARSE_ERROR, `Failed to parse DELETE statement: ${deleteParsed.error}`, {
sql
});
}
if (deleteParsed.table && deleteParsed.where) {
const idCondition = deleteParsed.where.find((w)=>w.field === 'id');
if (idCondition) {
const keyValue = params[0];
// Validate key value
if (typeof keyValue !== 'string' && typeof keyValue !== 'number') {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Redis key value must be a string or number', {
paramValue: typeof keyValue
});
}
const redisKey = `${deleteParsed.table}:${keyValue}`;
redisCommand = {
command: 'DEL',
key: redisKey
};
}
}
break;
}
default:
warnings.push(`Unsupported SQL query type: ${queryType}`);
recommendedBackend = BackendType.POSTGRES;
}
const executionTime = Date.now() - startTime;
if (executionTime > 50) {
warnings.push(`Translation took ${executionTime}ms (target: <50ms)`);
}
return {
success: !!redisCommand,
redisCommand,
executionTime,
recommendedBackend,
warnings: warnings.length > 0 ? warnings : undefined
};
} catch (error) {
const executionTime = Date.now() - startTime;
const message = error instanceof StandardError ? error.message : error instanceof Error ? error.message : 'Unknown error';
return {
success: false,
executionTime,
warnings: [
`Translation failed: ${message}`
]
};
}
}
/**
* Translate Redis command to SQL query (with parameterization)
*/ translateRedisToSQL(command) {
const startTime = Date.now();
const warnings = [];
try {
// Validate Redis command
const commandValidation = this.validateRedisCommand(command);
if (!commandValidation.valid) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, commandValidation.error || 'Invalid Redis command', {
command: command.command
});
}
let sqlQuery;
let sqlParams = [];
// Parse Redis key to extract table and ID
const keyParts = command.key?.split(':') || [];
const table = keyParts[0] || 'unknown';
const id = keyParts[1];
// Validate table name
const tableValidation = this.parser['validateTableName'] || ((t)=>({
valid: true,
sanitized: t
}));
// Since validateTableName is private, we do basic validation
const tablePattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
if (!tablePattern.test(table)) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid table name in Redis key', {
key: command.key
});
}
switch(command.command.toUpperCase()){
case 'GET':
case 'HGET':
case 'HGETALL':
{
// SELECT * FROM table WHERE id = ?
sqlQuery = `SELECT * FROM ${table} WHERE id = ?`;
sqlParams = [
id
];
break;
}
case 'SET':
case 'HMSET':
{
// INSERT INTO table (field1, field2, ...) VALUES (?, ?, ...)
if (command.fields) {
const fields = Object.keys(command.fields);
const placeholders = fields.map(()=>'?').join(', ');
sqlQuery = `INSERT INTO ${table} (${fields.join(', ')}) VALUES (${placeholders})`;
sqlParams = Object.values(command.fields);
// Validate all params are primitives
for(let i = 0; i < sqlParams.length; i++){
if (typeof sqlParams[i] === 'object' && sqlParams[i] !== null) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Parameter values must be primitives', {
paramIndex: i
});
}
}
}
break;
}
case 'HSET':
{
// UPDATE table SET field = ? WHERE id = ?
if (command.args && command.args.length > 0) {
const field = command.args[0];
// Validate field name
if (typeof field !== 'string' || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid field name in HSET command', {
field
});
}
sqlQuery = `UPDATE ${table} SET ${field} = ? WHERE id = ?`;
sqlParams = [
command.args[1],
id
];
// Validate params
for(let i = 0; i < sqlParams.length; i++){
if (typeof sqlParams[i] === 'object' && sqlParams[i] !== null) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Parameter values must be primitives', {
paramIndex: i
});
}
}
}
break;
}
case 'DEL':
{
// DELETE FROM table WHERE id = ?
sqlQuery = `DELETE FROM ${table} WHERE id = ?`;
sqlParams = [
id
];
break;
}
default:
warnings.push(`Unsupported Redis command: ${command.command}`);
}
const executionTime = Date.now() - startTime;
if (executionTime > 50) {
warnings.push(`Translation took ${executionTime}ms (target: <50ms)`);
}
return {
success: !!sqlQuery,
sqlQuery,
sqlParams,
executionTime,
warnings: warnings.length > 0 ? warnings : undefined
};
} catch (error) {
const executionTime = Date.now() - startTime;
const message = error instanceof StandardError ? error.message : error instanceof Error ? error.message : 'Unknown error';
return {
success: false,
executionTime,
warnings: [
`Translation failed: ${message}`
]
};
}
}
/**
* Optimize query and provide recommendations
*/ optimizeQuery(request) {
const result = {
indexed: [],
recommendations: []
};
// Recommend indexes for filtered fields
if (request.filters) {
const indexFields = request.filters.map((f)=>String(f.field));
result.indexed = indexFields;
result.indexes = indexFields;
result.recommendations?.push(`Consider adding indexes on: ${indexFields.join(', ')}`);
}
// Estimate query cost
let cost = 1;
if (request.joins) {
cost += request.joins.length * 10; // JOINs are expensive
}
if (request.filters) {
cost += request.filters.length * 2;
}
result.estimatedCost = cost;
// Provide optimization recommendations
if (request.joins && request.joins.length > 2) {
result.recommendations?.push('Consider denormalizing data or using materialized views for complex joins');
}
if (request.filters && request.filters.length > 5) {
result.recommendations?.push('Consider composite indexes for multiple filter conditions');
}
return result;
}
/**
* Recommend backend based on query characteristics
*/ recommendBackend(request) {
// Simple key-value access → Redis
if (request.key && !request.joins) {
return BackendType.REDIS;
}
// Complex queries with JOINs → PostgreSQL
if (request.joins && request.joins.length > 0) {
return BackendType.POSTGRES;
}
// Session/cache data → Redis
if (request.dataType === 'cache' || request.dataType === 'session') {
return BackendType.REDIS;
}
// Embedded/local data → SQLite
if (request.dataType === 'embedded') {
return BackendType.SQLITE;
}
// Default to PostgreSQL for structured data
return BackendType.POSTGRES;
}
/**
* Get query type from SQL string
*/ getQueryType(sql) {
const trimmed = sql.trim().toUpperCase();
if (trimmed.startsWith('SELECT')) return 'SELECT';
if (trimmed.startsWith('INSERT')) return 'INSERT';
if (trimmed.startsWith('UPDATE')) return 'UPDATE';
if (trimmed.startsWith('DELETE')) return 'DELETE';
return 'UNKNOWN';
}
}
//# sourceMappingURL=query-translator.js.map