import { DatabaseManager } from '../core/database'; export interface Suggestion { text: string; type: 'table' | 'column' | 'keyword' | 'function' | 'event-type' | 'category'; detail?: string; } const SQL_KEYWORDS = [ 'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'LIKE', 'BETWEEN', 'ORDER BY', 'GROUP BY', 'HAVING', 'LIMIT', 'OFFSET', 'AS', 'ON', 'JOIN', 'LEFT JOIN', 'INNER JOIN', 'DISTINCT', 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'IS', 'NULL', 'ASC', 'DESC', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'WITH', 'UNION', 'EXCEPT', 'INTERSECT', ]; const SQL_FUNCTIONS = [ 'COUNT(', 'SUM(', 'AVG(', 'MIN(', 'MAX(', 'COALESCE(', 'IFNULL(', 'NULLIF(', 'LENGTH(', 'LOWER(', 'UPPER(', 'TRIM(', 'SUBSTR(', 'DATE(', 'TIME(', 'DATETIME(', 'STRFTIME(', 'JSON_EXTRACT(', 'JSON_ARRAY_LENGTH(', 'TYPEOF(', 'CAST(', 'ABS(', 'ROUND(', 'REPLACE(', 'INSTR(', 'PRINTF(', 'GROUP_CONCAT(', 'TOTAL(', ]; const EVENT_TYPES = [ 'file:create', 'file:delete', 'file:rename', 'file:move', 'file:modify', 'content:words-changed', 'content:link-added', 'content:link-removed', 'content:tag-added', 'content:tag-removed', 'content:frontmatter-changed', 'content:heading-changed', 'content:embed-added', 'content:embed-removed', 'nav:file-open', 'nav:file-close', 'nav:active-leaf-change', 'editor:change', 'editor:selection', 'editor:paste', 'vault:folder-create', 'vault:folder-delete', 'vault:folder-rename', 'system:session-start', 'system:session-end', 'system:baseline-scan', 'system:rescan', 'system:maintenance', 'plugin:command-executed', ]; const CATEGORIES = [ 'file', 'content', 'navigation', 'editor', 'vault', 'plugin', 'system', ]; const TABLE_NAMES = [ 'events', 'sessions', 'baseline', 'daily_stats', 'monthly_stats', 'retention_log', 'schema_version', '_files', '_links', '_tags', '_headings', ]; export class SqlAutocomplete { private columnCache = new Map(); constructor(private db: DatabaseManager) { this.preloadColumns(); } private preloadColumns(): void { for (const table of TABLE_NAMES) { try { const rows = this.db.queryReadOnly( `PRAGMA table_info('${table}')`, ) as { name: string }[]; this.columnCache.set(table, rows.map(r => r.name)); } catch { // Virtual tables oder nicht existente Tabellen ueberspringen } } } getSuggestions(sql: string, cursorPos: number): Suggestion[] { const textBefore = sql.substring(0, cursorPos); const lastWord = getLastWord(textBefore); if (!lastWord) return []; const context = detectContext(textBefore); const suggestions: Suggestion[] = []; const lw = lastWord.toLowerCase(); switch (context) { case 'from': case 'join': for (const t of TABLE_NAMES) { if (t.toLowerCase().startsWith(lw)) { suggestions.push({ text: t, type: 'table' }); } } break; case 'select': case 'where': case 'group-by': case 'order-by': { // Spalten aus aktiven Tabellen const tables = extractTables(textBefore); for (const table of tables) { const cols = this.columnCache.get(table) ?? []; for (const col of cols) { if (col.toLowerCase().startsWith(lw)) { suggestions.push({ text: col, type: 'column', detail: table }); } } } // Funktionen for (const fn of SQL_FUNCTIONS) { if (fn.toLowerCase().startsWith(lw)) { suggestions.push({ text: fn, type: 'function' }); } } // Keywords for (const kw of SQL_KEYWORDS) { if (kw.toLowerCase().startsWith(lw)) { suggestions.push({ text: kw, type: 'keyword' }); } } break; } case 'string': // Event-Typen und Kategorien for (const et of EVENT_TYPES) { if (et.toLowerCase().startsWith(lw)) { suggestions.push({ text: et, type: 'event-type' }); } } for (const cat of CATEGORIES) { if (cat.toLowerCase().startsWith(lw)) { suggestions.push({ text: cat, type: 'category' }); } } break; default: // Generisch: Alle Typen for (const t of TABLE_NAMES) { if (t.toLowerCase().startsWith(lw)) { suggestions.push({ text: t, type: 'table' }); } } for (const kw of SQL_KEYWORDS) { if (kw.toLowerCase().startsWith(lw)) { suggestions.push({ text: kw, type: 'keyword' }); } } } // Sortieren: exakte Prefix-Matches zuerst, dann alphabetisch suggestions.sort((a, b) => { const aExact = a.text.toLowerCase().startsWith(lw) ? 0 : 1; const bExact = b.text.toLowerCase().startsWith(lw) ? 0 : 1; if (aExact !== bExact) return aExact - bExact; return a.text.localeCompare(b.text); }); return suggestions.slice(0, 20); } getColumns(table: string): string[] { return this.columnCache.get(table) ?? []; } } // --------------------------------------------------------------------------- // Context detection // --------------------------------------------------------------------------- type SqlContext = 'select' | 'from' | 'join' | 'where' | 'group-by' | 'order-by' | 'string' | 'unknown'; function detectContext(textBefore: string): SqlContext { // In String-Kontext? (ungerade Anzahl an einfachen Anführungszeichen) const quoteCount = (textBefore.match(/'/g) || []).length; if (quoteCount % 2 === 1) return 'string'; const upper = textBefore.toUpperCase(); const lastKeyword = findLastKeyword(upper); switch (lastKeyword) { case 'SELECT': return 'select'; case 'FROM': return 'from'; case 'JOIN': return 'join'; case 'WHERE': case 'AND': case 'OR': case 'ON': case 'HAVING': return 'where'; case 'GROUP BY': return 'group-by'; case 'ORDER BY': return 'order-by'; default: return 'unknown'; } } function findLastKeyword(upper: string): string { const keywords = [ 'ORDER BY', 'GROUP BY', 'LEFT JOIN', 'INNER JOIN', 'JOIN', 'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'ON', 'HAVING', ]; let lastPos = -1; let lastKw = ''; for (const kw of keywords) { const pos = upper.lastIndexOf(kw); if (pos > lastPos) { lastPos = pos; lastKw = kw; } } return lastKw; } function getLastWord(text: string): string { const match = text.match(/[\w._:-]+$/); return match ? match[0] : ''; } function extractTables(text: string): string[] { const upper = text.toUpperCase(); const tables: string[] = []; const fromMatch = upper.match(/FROM\s+([\w_]+)/g); if (fromMatch) { for (const m of fromMatch) { const name = m.replace(/FROM\s+/i, '').trim().toLowerCase(); if (TABLE_NAMES.includes(name)) tables.push(name); } } const joinMatch = upper.match(/JOIN\s+([\w_]+)/g); if (joinMatch) { for (const m of joinMatch) { const name = m.replace(/.*JOIN\s+/i, '').trim().toLowerCase(); if (TABLE_NAMES.includes(name)) tables.push(name); } } return tables.length > 0 ? tables : ['events']; }