diff --git a/packages/opencode/src/altimate/native/altimate-core.ts b/packages/opencode/src/altimate/native/altimate-core.ts index ef861543f3..d44de33d26 100644 --- a/packages/opencode/src/altimate/native/altimate-core.ts +++ b/packages/opencode/src/altimate/native/altimate-core.ts @@ -131,11 +131,15 @@ register("altimate_core.transpile", async (params) => { // Post-process QUALIFY for targets that lack native support const targetLower = params.to_dialect.toLowerCase() if (QUALIFY_TARGETS.has(targetLower)) { - const translated = - (data.sql as string) || (data.translated_sql as string) || "" - if (translated && translated.toUpperCase().includes("QUALIFY")) { - const fixed = postprocessQualify(translated) - if ("sql" in data) { + // Rust returns transpiled_sql as string[] — use first element + const transpiled = Array.isArray(data.transpiled_sql) + ? (data.transpiled_sql as string[])[0] + : (data.transpiled_sql as string) || (data.sql as string) || (data.translated_sql as string) || "" + if (transpiled && transpiled.toUpperCase().includes("QUALIFY")) { + const fixed = postprocessQualify(transpiled) + if (Array.isArray(data.transpiled_sql)) { + ;(data.transpiled_sql as string[])[0] = fixed + } else if ("sql" in data) { data.sql = fixed } else { data.translated_sql = fixed @@ -326,12 +330,14 @@ register("altimate_core.query_pii", async (params) => { } }) -// 19. altimate_core.resolve_term +// 19. altimate_core.resolve_term — returns array, must wrap register("altimate_core.resolve_term", async (params) => { try { const schema = schemaOrEmpty(params.schema_path, params.schema_context) const raw = core.resolveTerm(params.term, schema) - return ok(true, toData(raw)) + // Rust returns an array of matches — wrap for consistent object shape + const matches = Array.isArray(raw) ? JSON.parse(JSON.stringify(raw)) : [] + return ok(matches.length > 0, { matches }) } catch (e) { return fail(e) } diff --git a/packages/opencode/src/altimate/native/schema-resolver.ts b/packages/opencode/src/altimate/native/schema-resolver.ts index fadc8df0f9..9dc042e129 100644 --- a/packages/opencode/src/altimate/native/schema-resolver.ts +++ b/packages/opencode/src/altimate/native/schema-resolver.ts @@ -3,10 +3,94 @@ * * Translates the bridge protocol's `schema_path` / `schema_context` parameters * into altimate-core `Schema` objects. + * + * Tools pass `schema_context` in two possible formats: + * + * 1. **Flat format** (used by most tools): + * `{ "table_name": { "col_name": "TYPE", ... } }` + * + * 2. **SchemaDefinition format** (matches Rust struct): + * `{ "tables": { "table_name": { "columns": [{ "name": "col", "type": "TYPE" }] } } }` + * + * This module normalizes both formats into the SchemaDefinition format + * expected by `Schema.fromJson()`. */ import { Schema } from "@altimateai/altimate-core" +/** + * Detect whether a schema_context object is in flat format or SchemaDefinition format. + * + * Flat format: `{ "table_name": { "col_name": "TYPE" } }` + * SchemaDefinition format: has a `tables` key with nested structure. + */ +function isSchemaDefinitionFormat(ctx: Record): boolean { + if (!("tables" in ctx) || typeof ctx.tables !== "object" || ctx.tables === null) { + return false + } + // Verify at least one value under `tables` looks like a table definition + // (has a `columns` array), not a flat column map like { "col": "TYPE" }. + // This prevents false positives when a flat schema has a table named "tables". + const values = Object.values(ctx.tables) + if (values.length === 0) return true // empty tables is valid SchemaDefinition + return values.some((v: any) => Array.isArray(v?.columns)) +} + +/** + * Convert flat schema format to SchemaDefinition format. + * + * Handles three input variants: + * + * 1. Flat map: `{ "customers": { "id": "INTEGER", "name": "VARCHAR" } }` + * 2. Array form: `{ "customers": [{ "name": "id", "data_type": "INTEGER" }] }` + * 3. Partial SD: `{ "customers": { "columns": [{ "name": "id", "type": "INTEGER" }] } }` + * + * Output: `{ "tables": { "customers": { "columns": [{ "name": "id", "type": "INTEGER" }, ...] } } }` + */ +function flatToSchemaDefinition(flat: Record): Record { + const tables: Record = {} + for (const [tableName, colsOrDef] of Object.entries(flat)) { + if (colsOrDef === null || colsOrDef === undefined) continue + + // Variant 2: array of column definitions + if (Array.isArray(colsOrDef)) { + if (colsOrDef.length === 0) continue // skip empty tables + const columns = colsOrDef.map((c: any) => ({ + name: c.name, + type: c.type ?? c.data_type ?? "VARCHAR", + })) + tables[tableName] = { columns } + } else if (typeof colsOrDef === "object") { + // Variant 3: already has a `columns` array + if (Array.isArray(colsOrDef.columns)) { + if (colsOrDef.columns.length === 0) continue // skip empty tables + tables[tableName] = colsOrDef + } else { + // Variant 1: flat map { "col_name": "TYPE", ... } + const entries = Object.entries(colsOrDef) + if (entries.length === 0) continue // skip empty tables + const columns = entries.map(([colName, colType]) => ({ + name: colName, + type: String(colType), + })) + tables[tableName] = { columns } + } + } + } + return { tables } +} + +/** + * Normalize a schema_context into SchemaDefinition JSON format. + * Accepts both flat and SchemaDefinition formats. + */ +function normalizeSchemaContext(ctx: Record): string { + if (isSchemaDefinitionFormat(ctx)) { + return JSON.stringify(ctx) + } + return JSON.stringify(flatToSchemaDefinition(ctx)) +} + /** * Resolve a Schema from a file path or inline JSON context. * Returns null when neither source is provided. @@ -19,7 +103,7 @@ export function resolveSchema( return Schema.fromFile(schemaPath) } if (schemaContext && Object.keys(schemaContext).length > 0) { - return Schema.fromJson(JSON.stringify(schemaContext)) + return Schema.fromJson(normalizeSchemaContext(schemaContext)) } return null } diff --git a/packages/opencode/src/altimate/native/sql/register.ts b/packages/opencode/src/altimate/native/sql/register.ts index 23f22b1098..78b6ac5a2a 100644 --- a/packages/opencode/src/altimate/native/sql/register.ts +++ b/packages/opencode/src/altimate/native/sql/register.ts @@ -20,6 +20,10 @@ import type { SchemaDiffResult, } from "../types" +/** Register all composite SQL handlers with the Dispatcher. + * Exported so tests can re-register after Dispatcher.reset(). */ +export function registerAllSql(): void { + // --------------------------------------------------------------------------- // sql.analyze — lint + semantics + safety // --------------------------------------------------------------------------- @@ -362,8 +366,10 @@ register("sql.rewrite", async (params) => { rewritten_sql: result.suggestions?.[0]?.rewritten_sql ?? null, rewrites_applied: result.suggestions?.map((s: any) => ({ rule: s.rule, - description: s.explanation, - rewritten_sql: s.rewritten_sql, + original_fragment: params.sql, + rewritten_fragment: s.rewritten_sql ?? params.sql, + explanation: s.explanation ?? s.improvement ?? "", + can_auto_apply: (s.confidence ?? 0) >= 0.7, })) ?? [], } } catch (e) { @@ -430,3 +436,8 @@ register("lineage.check", async (params) => { } satisfies LineageCheckResult } }) + +} // end registerAllSql + +// Auto-register on module load +registerAllSql() diff --git a/packages/opencode/src/altimate/tools/altimate-core-classify-pii.ts b/packages/opencode/src/altimate/tools/altimate-core-classify-pii.ts index 9c9a04140d..8a5bbb099e 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-classify-pii.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-classify-pii.ts @@ -16,7 +16,8 @@ export const AltimateCoreClassifyPiiTool = Tool.define("altimate_core_classify_p schema_context: args.schema_context, }) const data = result.data as Record - const findingCount = data.findings?.length ?? 0 + const piiColumns = data.columns ?? data.findings ?? [] + const findingCount = piiColumns.length return { title: `PII Classification: ${findingCount} finding(s)`, metadata: { success: result.success, finding_count: findingCount }, @@ -31,10 +32,20 @@ export const AltimateCoreClassifyPiiTool = Tool.define("altimate_core_classify_p function formatClassifyPii(data: Record): string { if (data.error) return `Error: ${data.error}` - if (!data.findings?.length) return "No PII columns detected." - const lines = ["PII columns found:\n"] - for (const f of data.findings) { - lines.push(` ${f.table}.${f.column}: ${f.category} (${f.confidence} confidence)`) + const piiColumns = data.columns ?? data.findings ?? [] + if (!piiColumns.length) return "No PII columns detected." + const lines: string[] = [] + if (data.risk_level) lines.push(`Risk level: ${data.risk_level}`) + if (data.pii_count != null) lines.push(`PII columns: ${data.pii_count} of ${data.total_columns}`) + lines.push("") + lines.push("PII columns found:") + for (const f of piiColumns) { + const classification = f.classification ?? f.category ?? "PII" + const confidence = f.confidence ?? "high" + const table = f.table ?? "unknown" + const column = f.column ?? "unknown" + lines.push(` ${table}.${column}: ${classification} (${confidence} confidence)`) + if (f.suggested_masking) lines.push(` Masking: ${f.suggested_masking}`) } return lines.join("\n") } diff --git a/packages/opencode/src/altimate/tools/altimate-core-column-lineage.ts b/packages/opencode/src/altimate/tools/altimate-core-column-lineage.ts index ccec57dd4a..d19e412d6c 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-column-lineage.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-column-lineage.ts @@ -35,10 +35,26 @@ export const AltimateCoreColumnLineageTool = Tool.define("altimate_core_column_l function formatColumnLineage(data: Record): string { if (data.error) return `Error: ${data.error}` - if (!data.column_lineage?.length) return "No column lineage edges found." - const lines = ["Column lineage:\n"] - for (const edge of data.column_lineage) { - lines.push(` ${edge.source} -> ${edge.target}${edge.transform ? ` (${edge.transform})` : ""}`) + if (!data.column_lineage?.length && !data.column_dict) return "No column lineage edges found." + const lines: string[] = [] + + // column_dict: output columns -> source columns mapping + if (data.column_dict && Object.keys(data.column_dict).length > 0) { + lines.push("Column Mappings:") + for (const [target, sources] of Object.entries(data.column_dict)) { + const srcList = Array.isArray(sources) ? (sources as string[]).join(", ") : JSON.stringify(sources) + lines.push(` ${target} ← ${srcList}`) + } + lines.push("") + } + + if (data.column_lineage?.length) { + lines.push("Lineage Edges:") + for (const edge of data.column_lineage) { + const transform = edge.lens_type ?? edge.transform_type ?? edge.transform ?? "" + lines.push(` ${edge.source} → ${edge.target}${transform ? ` (${transform})` : ""}`) + } } - return lines.join("\n") + + return lines.length ? lines.join("\n") : "No column lineage edges found." } diff --git a/packages/opencode/src/altimate/tools/altimate-core-complete.ts b/packages/opencode/src/altimate/tools/altimate-core-complete.ts index b2a833e8ee..7584146174 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-complete.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-complete.ts @@ -20,7 +20,7 @@ export const AltimateCoreCompleteTool = Tool.define("altimate_core_complete", { schema_context: args.schema_context, }) const data = result.data as Record - const count = data.suggestions?.length ?? 0 + const count = data.items?.length ?? data.suggestions?.length ?? 0 return { title: `Complete: ${count} suggestion(s)`, metadata: { success: result.success, suggestion_count: count }, @@ -35,9 +35,10 @@ export const AltimateCoreCompleteTool = Tool.define("altimate_core_complete", { function formatComplete(data: Record): string { if (data.error) return `Error: ${data.error}` - if (!data.suggestions?.length) return "No completions available." + const items = data.items ?? data.suggestions ?? [] + if (!items.length) return "No completions available." const lines = ["Suggestions:\n"] - for (const s of data.suggestions) { + for (const s of items) { lines.push(` ${s.label ?? s.text} (${s.kind ?? s.type ?? "unknown"})`) } return lines.join("\n") diff --git a/packages/opencode/src/altimate/tools/altimate-core-fix.ts b/packages/opencode/src/altimate/tools/altimate-core-fix.ts index 052fe3ec7d..4e9dddb20c 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-fix.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-fix.ts @@ -35,21 +35,23 @@ export const AltimateCoreFixTool = Tool.define("altimate_core_fix", { function formatFix(data: Record): string { if (data.error) return `Error: ${data.error}` const lines: string[] = [] - if (data.fixed_sql) { + if (data.fixed_sql && data.fixed !== false) { lines.push("Fixed SQL:") lines.push(data.fixed_sql) - if (data.changes?.length) { + const fixes = data.fixes_applied ?? data.changes ?? [] + if (fixes.length) { lines.push("\nChanges applied:") - for (const c of data.changes) { - lines.push(` - ${c.description ?? c}`) + for (const c of fixes) { + lines.push(` - ${c.description ?? c.message ?? c}`) } } } else { lines.push("Could not auto-fix the SQL.") - if (data.errors?.length) { + const unfixable = data.unfixable_errors ?? data.errors ?? [] + if (unfixable.length) { lines.push("\nErrors found:") - for (const e of data.errors) { - lines.push(` - ${e.message ?? e}`) + for (const e of unfixable) { + lines.push(` - ${e.message ?? e.reason ?? e}`) } } } diff --git a/packages/opencode/src/altimate/tools/altimate-core-grade.ts b/packages/opencode/src/altimate/tools/altimate-core-grade.ts index 122b286c6c..1a1adf1555 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-grade.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-grade.ts @@ -18,9 +18,11 @@ export const AltimateCoreGradeTool = Tool.define("altimate_core_grade", { schema_context: args.schema_context, }) const data = result.data as Record + const grade = data.overall_grade ?? data.grade + const score = data.scores?.overall != null ? Math.round(data.scores.overall * 100) : data.score return { - title: `Grade: ${data.grade ?? "?"}`, - metadata: { success: result.success, grade: data.grade, score: data.score }, + title: `Grade: ${grade ?? "?"}`, + metadata: { success: result.success, grade, score }, output: formatGrade(data), } } catch (e) { @@ -33,8 +35,20 @@ export const AltimateCoreGradeTool = Tool.define("altimate_core_grade", { function formatGrade(data: Record): string { if (data.error) return `Error: ${data.error}` const lines: string[] = [] - lines.push(`Grade: ${data.grade}`) - if (data.score != null) lines.push(`Score: ${data.score}/100`) + const grade = data.overall_grade ?? data.grade + lines.push(`Grade: ${grade}`) + const scores = data.scores + if (scores) { + const overall = scores.overall != null ? Math.round(scores.overall * 100) : null + if (overall != null) lines.push(`Score: ${overall}/100`) + lines.push("\nCategory scores:") + if (scores.syntax != null) lines.push(` syntax: ${Math.round(scores.syntax * 100)}/100`) + if (scores.style != null) lines.push(` style: ${Math.round(scores.style * 100)}/100`) + if (scores.safety != null) lines.push(` safety: ${Math.round(scores.safety * 100)}/100`) + if (scores.complexity != null) lines.push(` complexity: ${Math.round(scores.complexity * 100)}/100`) + } else if (data.score != null) { + lines.push(`Score: ${data.score}/100`) + } if (data.categories) { lines.push("\nCategory scores:") for (const [cat, score] of Object.entries(data.categories)) { diff --git a/packages/opencode/src/altimate/tools/altimate-core-prune-schema.ts b/packages/opencode/src/altimate/tools/altimate-core-prune-schema.ts index 7cc78218b5..f6d7ffa913 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-prune-schema.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-prune-schema.ts @@ -32,6 +32,19 @@ export const AltimateCorePruneSchemaTool = Tool.define("altimate_core_prune_sche function formatPruneSchema(data: Record): string { if (data.error) return `Error: ${data.error}` - if (data.pruned) return JSON.stringify(data.pruned, null, 2) - return JSON.stringify(data, null, 2) + const lines: string[] = [] + if (data.tables_pruned != null) { + lines.push(`Pruned ${data.tables_pruned} of ${data.total_tables} tables to ${data.relevant_tables?.length ?? "?"} relevant.`) + } + if (data.relevant_tables?.length) { + lines.push(`Relevant tables: ${data.relevant_tables.join(", ")}`) + } + if (data.pruned_schema_yaml) { + lines.push("") + lines.push(data.pruned_schema_yaml) + } else if (data.pruned) { + lines.push("") + lines.push(JSON.stringify(data.pruned, null, 2)) + } + return lines.length > 0 ? lines.join("\n") : JSON.stringify(data, null, 2) } diff --git a/packages/opencode/src/altimate/tools/altimate-core-query-pii.ts b/packages/opencode/src/altimate/tools/altimate-core-query-pii.ts index 0cbf70739f..30a839b9b9 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-query-pii.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-query-pii.ts @@ -18,7 +18,8 @@ export const AltimateCoreQueryPiiTool = Tool.define("altimate_core_query_pii", { schema_context: args.schema_context, }) const data = result.data as Record - const exposureCount = data.exposures?.length ?? 0 + const piiCols = data.pii_columns ?? data.exposures ?? [] + const exposureCount = piiCols.length return { title: `Query PII: ${exposureCount === 0 ? "CLEAN" : `${exposureCount} exposure(s)`}`, metadata: { success: result.success, exposure_count: exposureCount }, @@ -33,10 +34,23 @@ export const AltimateCoreQueryPiiTool = Tool.define("altimate_core_query_pii", { function formatQueryPii(data: Record): string { if (data.error) return `Error: ${data.error}` - if (!data.exposures?.length) return "Query does not access PII columns." - const lines = ["PII exposure detected:\n"] - for (const e of data.exposures) { - lines.push(` ${e.column}: ${e.category} (${e.risk ?? "medium"} risk)`) + const piiCols = data.pii_columns ?? data.exposures ?? [] + if (!piiCols.length) return "Query does not access PII columns." + const lines: string[] = [] + if (data.risk_level) lines.push(`Risk level: ${data.risk_level}`) + lines.push("PII exposure detected:\n") + for (const e of piiCols) { + const classification = e.classification ?? e.category ?? "PII" + const table = e.table ?? "unknown" + const column = e.column ?? "unknown" + lines.push(` ${table}.${column}: ${classification}`) + if (e.suggested_masking) lines.push(` Masking: ${e.suggested_masking}`) + } + if (data.suggested_alternatives?.length) { + lines.push("\nSuggested alternatives:") + for (const alt of data.suggested_alternatives) { + lines.push(` - ${alt}`) + } } return lines.join("\n") } diff --git a/packages/opencode/src/altimate/tools/altimate-core-resolve-term.ts b/packages/opencode/src/altimate/tools/altimate-core-resolve-term.ts index fe9e384b93..1f215f88f2 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-resolve-term.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-resolve-term.ts @@ -36,7 +36,11 @@ function formatResolveTerm(data: Record, term: string): string { if (!data.matches?.length) return `No schema elements match "${term}".` const lines = [`Matches for "${term}":\n`] for (const m of data.matches) { - lines.push(` ${m.fqn ?? `${m.table}.${m.column}`} (${m.score ?? m.confidence} match)`) + const col = m.matched_column + const loc = col ? `${col.table}.${col.column}` : (m.fqn ?? `${m.table}.${m.column}`) + const score = m.confidence ?? m.score ?? "?" + const source = m.source ? ` [${m.source}]` : "" + lines.push(` ${loc} (${score} confidence)${source}`) } return lines.join("\n") } diff --git a/packages/opencode/src/altimate/tools/altimate-core-rewrite.ts b/packages/opencode/src/altimate/tools/altimate-core-rewrite.ts index 828e2e2e61..91626cb4fd 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-rewrite.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-rewrite.ts @@ -18,7 +18,8 @@ export const AltimateCoreRewriteTool = Tool.define("altimate_core_rewrite", { schema_context: args.schema_context, }) const data = result.data as Record - const rewriteCount = data.rewrites?.length ?? (data.rewritten_sql && data.rewritten_sql !== args.sql ? 1 : 0) + const suggestions = data.suggestions ?? data.rewrites ?? [] + const rewriteCount = suggestions.length || (data.rewritten_sql && data.rewritten_sql !== args.sql ? 1 : 0) return { title: `Rewrite: ${rewriteCount} suggestion(s)`, metadata: { success: result.success, rewrite_count: rewriteCount }, @@ -33,19 +34,22 @@ export const AltimateCoreRewriteTool = Tool.define("altimate_core_rewrite", { function formatRewrite(data: Record): string { if (data.error) return `Error: ${data.error}` - if (!data.rewrites?.length) { + const suggestions = data.suggestions ?? data.rewrites ?? [] + if (!suggestions.length) { if (data.rewritten_sql) return `Optimized SQL:\n${data.rewritten_sql}` return "No rewrites suggested." } const lines: string[] = [] - if (data.rewritten_sql) { + // Use first suggestion's rewritten_sql if top-level rewritten_sql not present + const bestSql = data.rewritten_sql ?? suggestions[0]?.rewritten_sql + if (bestSql) { lines.push("Optimized SQL:") - lines.push(data.rewritten_sql) + lines.push(bestSql) lines.push("") } lines.push("Rewrites applied:") - for (const r of data.rewrites) { - lines.push(` - ${r.rule ?? r.type}: ${r.explanation ?? r.description}`) + for (const r of suggestions) { + lines.push(` - ${r.rule ?? r.type}: ${r.explanation ?? r.description ?? r.improvement}`) } return lines.join("\n") } diff --git a/packages/opencode/src/altimate/tools/altimate-core-schema-diff.ts b/packages/opencode/src/altimate/tools/altimate-core-schema-diff.ts index 71aa975fd7..bda0a07374 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-schema-diff.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-schema-diff.ts @@ -21,9 +21,10 @@ export const AltimateCoreSchemaDiffTool = Tool.define("altimate_core_schema_diff }) const data = result.data as Record const changeCount = data.changes?.length ?? 0 + const hasBreaking = data.has_breaking_changes ?? data.has_breaking ?? false return { - title: `Schema Diff: ${changeCount} change(s)${data.has_breaking ? " (BREAKING)" : ""}`, - metadata: { success: result.success, change_count: changeCount, has_breaking: data.has_breaking }, + title: `Schema Diff: ${changeCount} change(s)${hasBreaking ? " (BREAKING)" : ""}`, + metadata: { success: result.success, change_count: changeCount, has_breaking: hasBreaking }, output: formatSchemaDiff(data), } } catch (e) { @@ -37,10 +38,31 @@ function formatSchemaDiff(data: Record): string { if (data.error) return `Error: ${data.error}` if (!data.changes?.length) return "Schemas are identical." const lines: string[] = [] - if (data.has_breaking) lines.push("WARNING: Breaking changes detected!\n") + const hasBreaking = data.has_breaking_changes ?? data.has_breaking ?? false + if (hasBreaking) lines.push("WARNING: Breaking changes detected!\n") + + // Rust SchemaChange uses tagged enum: { type: "column_added", table: "...", ... } + const breakingTypes = new Set(["table_removed", "column_removed", "column_type_changed"]) for (const c of data.changes) { - const marker = c.breaking ? "BREAKING" : c.severity ?? "info" - lines.push(` [${marker}] ${c.type}: ${c.description ?? c.message}`) + const isBreaking = breakingTypes.has(c.type) || + (c.type === "nullability_changed" && c.old_nullable && !c.new_nullable) + const marker = isBreaking ? "BREAKING" : "info" + const desc = formatChange(c) + lines.push(` [${marker}] ${desc}`) } + + if (data.summary) lines.push(`\nSummary: ${data.summary}`) return lines.join("\n") } + +function formatChange(c: Record): string { + switch (c.type) { + case "table_added": return `Table '${c.table}' added` + case "table_removed": return `Table '${c.table}' removed` + case "column_added": return `Column '${c.table}.${c.column}' added (${c.data_type})` + case "column_removed": return `Column '${c.table}.${c.column}' removed` + case "column_type_changed": return `Column '${c.table}.${c.column}' type changed: ${c.old_type} → ${c.new_type}` + case "nullability_changed": return `Column '${c.table}.${c.column}' nullability: ${c.old_nullable ? "nullable" : "not null"} → ${c.new_nullable ? "nullable" : "not null"}` + default: return `${c.type}: ${c.description ?? c.message ?? JSON.stringify(c)}` + } +} diff --git a/packages/opencode/src/altimate/tools/altimate-core-testgen.ts b/packages/opencode/src/altimate/tools/altimate-core-testgen.ts index 9bece66be6..462edab9a8 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-testgen.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-testgen.ts @@ -39,7 +39,14 @@ function formatTestgen(data: Record): string { const lines: string[] = [`Generated ${tests.length} test case(s):\n`] for (const test of tests) { lines.push(`--- ${test.name ?? test.description ?? "Test"} ---`) + if (test.description) lines.push(` ${test.description}`) if (test.sql) lines.push(test.sql) + if (test.inputs?.length) { + for (const input of test.inputs) { + lines.push(` Input: ${input.column} (${input.data_type}) = ${input.value}`) + } + } + if (test.expected) lines.push(` Expected: ${test.expected}`) if (test.assertion) lines.push(` Assert: ${test.assertion}`) if (test.category) lines.push(` Category: ${test.category}`) lines.push("") diff --git a/packages/opencode/src/altimate/tools/sql-analyze.ts b/packages/opencode/src/altimate/tools/sql-analyze.ts index 7cc6acd6e6..d48c4e805e 100644 --- a/packages/opencode/src/altimate/tools/sql-analyze.ts +++ b/packages/opencode/src/altimate/tools/sql-analyze.ts @@ -42,8 +42,8 @@ export const SqlAnalyzeTool = Tool.define("sql_analyze", { }) function formatAnalysis(result: SqlAnalyzeResult): string { - if (!result.success) { - return `Analysis failed: ${result.error ?? "Unknown error"}` + if (result.error) { + return `Analysis failed: ${result.error}` } if (result.issues.length === 0) { diff --git a/packages/opencode/test/altimate/altimate-core-e2e.test.ts b/packages/opencode/test/altimate/altimate-core-e2e.test.ts new file mode 100644 index 0000000000..4db6f27fb3 --- /dev/null +++ b/packages/opencode/test/altimate/altimate-core-e2e.test.ts @@ -0,0 +1,935 @@ +/** + * End-to-end tests for all altimate-core tools via the Dispatcher. + * + * Exercises every registered dispatcher method with realistic inputs, + * validates output field names match Rust napi return types, and checks + * error recovery paths. + * + * Requires @altimateai/altimate-core napi binary to be installed. + * Skips gracefully if the binary is not available. + */ + +import { describe, expect, test, beforeAll, afterAll } from "bun:test" + +// --------------------------------------------------------------------------- +// Availability check +// --------------------------------------------------------------------------- + +let coreAvailable = false +try { + require.resolve("@altimateai/altimate-core") + coreAvailable = true +} catch {} + +const describeIf = coreAvailable ? describe : describe.skip + +// --------------------------------------------------------------------------- +// Schema fixtures +// --------------------------------------------------------------------------- + +/** Flat format — what agents typically pass */ +const ECOMMERCE_FLAT = { + customers: { + customer_id: "INTEGER", + first_name: "VARCHAR", + last_name: "VARCHAR", + email: "VARCHAR", + created_at: "TIMESTAMP", + }, + orders: { + order_id: "INTEGER", + customer_id: "INTEGER", + order_date: "DATE", + status: "VARCHAR", + amount: "DECIMAL", + }, + payments: { + payment_id: "INTEGER", + order_id: "INTEGER", + payment_method: "VARCHAR", + amount: "DECIMAL", + }, + products: { + product_id: "INTEGER", + name: "VARCHAR", + category: "VARCHAR", + price: "DECIMAL", + }, + order_items: { + item_id: "INTEGER", + order_id: "INTEGER", + product_id: "INTEGER", + quantity: "INTEGER", + unit_price: "DECIMAL", + }, +} + +/** SchemaDefinition format — what the Rust engine expects natively */ +const ECOMMERCE_SD = { + tables: { + customers: { + columns: [ + { name: "customer_id", type: "INTEGER" }, + { name: "first_name", type: "VARCHAR" }, + { name: "last_name", type: "VARCHAR" }, + { name: "email", type: "VARCHAR" }, + { name: "created_at", type: "TIMESTAMP" }, + ], + }, + orders: { + columns: [ + { name: "order_id", type: "INTEGER" }, + { name: "customer_id", type: "INTEGER" }, + { name: "order_date", type: "DATE" }, + { name: "status", type: "VARCHAR" }, + { name: "amount", type: "DECIMAL" }, + ], + }, + }, +} + +/** Array-of-columns format — what lineage_check uses */ +const ARRAY_SCHEMA = { + customers: [ + { name: "customer_id", data_type: "INTEGER" }, + { name: "first_name", data_type: "VARCHAR" }, + { name: "email", data_type: "VARCHAR" }, + ], + orders: [ + { name: "order_id", data_type: "INTEGER" }, + { name: "customer_id", data_type: "INTEGER" }, + { name: "amount", data_type: "DECIMAL" }, + ], +} + +// --------------------------------------------------------------------------- +// SQL fixtures +// --------------------------------------------------------------------------- + +const SQL = { + simple: "SELECT customer_id, first_name FROM customers WHERE customer_id = 1", + selectStar: "SELECT * FROM orders", + join: `SELECT c.customer_id, c.first_name, o.order_id, o.amount +FROM customers c +INNER JOIN orders o ON c.customer_id = o.customer_id +WHERE o.status = 'completed'`, + multiJoin: `SELECT c.customer_id, c.first_name, c.last_name, + COUNT(o.order_id) AS order_count, + SUM(p.amount) AS total_paid, + MAX(o.order_date) AS last_order +FROM customers c +LEFT JOIN orders o ON c.customer_id = o.customer_id +LEFT JOIN payments p ON o.order_id = p.order_id +GROUP BY c.customer_id, c.first_name, c.last_name`, + subquery: `SELECT customer_id, first_name FROM customers +WHERE customer_id IN (SELECT customer_id FROM orders WHERE amount > 100)`, + cte: `WITH high_value AS ( + SELECT customer_id, SUM(amount) AS total FROM orders + GROUP BY customer_id HAVING SUM(amount) > 1000 +) +SELECT c.first_name, c.last_name, h.total +FROM customers c JOIN high_value h ON c.customer_id = h.customer_id`, + cartesian: "SELECT * FROM customers, orders", + syntaxError: "SELCT * FORM customers", + missingColumn: "SELECT nonexistent FROM customers", + windowFunc: `SELECT customer_id, order_date, amount, + ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) AS rn, + SUM(amount) OVER (PARTITION BY customer_id) AS customer_total +FROM orders`, + caseWhen: `SELECT order_id, + CASE WHEN amount > 100 THEN 'high' WHEN amount > 50 THEN 'medium' ELSE 'low' END AS tier +FROM orders`, + groupBy: `SELECT customer_id, COUNT(order_id) AS order_count, SUM(amount) AS total +FROM orders GROUP BY customer_id`, +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describeIf("altimate-core E2E", () => { + let D: any + + beforeAll(async () => { + process.env.ALTIMATE_TELEMETRY_DISABLED = "true" + D = await import("../../src/altimate/native/dispatcher") + const core = await import("../../src/altimate/native/altimate-core") + const sql = await import("../../src/altimate/native/sql/register") + // Re-register handlers in case another test file called Dispatcher.reset() + core.registerAll() + sql.registerAllSql() + }) + + afterAll(() => { delete process.env.ALTIMATE_TELEMETRY_DISABLED }) + + // ========================================================================= + // altimate_core.validate + // ========================================================================= + + describe("altimate_core.validate", () => { + test("valid simple query", async () => { + const r = await D.call("altimate_core.validate", { sql: SQL.simple, schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).valid).toBe(true) + expect((r.data as any).errors).toHaveLength(0) + }) + + test("valid multi-join query", async () => { + const r = await D.call("altimate_core.validate", { sql: SQL.multiJoin, schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).valid).toBe(true) + }) + + test("invalid — missing column returns errors with messages", async () => { + const r = await D.call("altimate_core.validate", { sql: SQL.missingColumn, schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + expect(d.valid).toBe(false) + expect(d.errors.length).toBeGreaterThan(0) + expect(d.errors[0].message).toBeDefined() + expect(d.errors[0].message.length).toBeGreaterThan(0) + }) + + test("syntax error detected", async () => { + const r = await D.call("altimate_core.validate", { sql: SQL.syntaxError, schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).valid).toBe(false) + expect((r.data as any).errors.length).toBeGreaterThan(0) + }) + + test("works with SchemaDefinition format", async () => { + const r = await D.call("altimate_core.validate", { sql: "SELECT customer_id FROM customers", schema_context: ECOMMERCE_SD }) + expect((r.data as any).valid).toBe(true) + }) + + test("works with empty schema_context", async () => { + const r = await D.call("altimate_core.validate", { sql: SQL.simple }) + expect(r.data).toBeDefined() + }) + + test("empty SQL handled gracefully", async () => { + const r = await D.call("altimate_core.validate", { sql: "", schema_context: ECOMMERCE_FLAT }) + expect(r).toBeDefined() + }) + + test("CTE validates", async () => { + const r = await D.call("altimate_core.validate", { sql: SQL.cte, schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).valid).toBe(true) + }) + + test("window function validates", async () => { + const r = await D.call("altimate_core.validate", { sql: SQL.windowFunc, schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).valid).toBe(true) + }) + + test("correlated subquery validates", async () => { + const sql = `SELECT c.first_name, (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.customer_id) AS cnt FROM customers c` + const r = await D.call("altimate_core.validate", { sql, schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).valid).toBe(true) + }) + + test("EXISTS subquery validates", async () => { + const sql = `SELECT first_name FROM customers c WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id)` + const r = await D.call("altimate_core.validate", { sql, schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).valid).toBe(true) + }) + + test("self-join validates", async () => { + const sql = `SELECT a.order_id, b.order_id AS related FROM orders a JOIN orders b ON a.customer_id = b.customer_id AND a.order_id <> b.order_id` + const r = await D.call("altimate_core.validate", { sql, schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).valid).toBe(true) + }) + + test("deeply nested subqueries", async () => { + const sql = `SELECT * FROM (SELECT * FROM (SELECT * FROM (SELECT customer_id, first_name FROM customers WHERE customer_id > 0) t1) t2) t3` + const r = await D.call("altimate_core.validate", { sql, schema_context: ECOMMERCE_FLAT }) + expect(r).toBeDefined() + }) + + test("multiple CTEs", async () => { + const sql = `WITH a AS (SELECT customer_id FROM customers), b AS (SELECT customer_id, COUNT(*) AS cnt FROM orders GROUP BY customer_id), c AS (SELECT customer_id, SUM(amount) AS total FROM orders GROUP BY customer_id HAVING SUM(amount) > 1000) SELECT a.customer_id, b.cnt, c.total FROM a LEFT JOIN b ON a.customer_id = b.customer_id LEFT JOIN c ON a.customer_id = c.customer_id` + const r = await D.call("altimate_core.validate", { sql, schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).valid).toBe(true) + }) + }) + + // ========================================================================= + // altimate_core.lint + // ========================================================================= + + describe("altimate_core.lint", () => { + test("SELECT * triggers finding", async () => { + const r = await D.call("altimate_core.lint", { sql: SQL.selectStar, schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).findings?.some((f: any) => f.rule === "select_star")).toBe(true) + }) + + test("clean query with LIMIT has fewer findings", async () => { + const r = await D.call("altimate_core.lint", { sql: "SELECT customer_id FROM customers WHERE customer_id = 1 LIMIT 10", schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + const selectStarFindings = d.findings?.filter((f: any) => f.rule === "select_star") ?? [] + expect(selectStarFindings.length).toBe(0) + }) + + test("cartesian product detected", async () => { + const r = await D.call("altimate_core.lint", { sql: SQL.cartesian, schema_context: ECOMMERCE_FLAT }) + expect(r).toBeDefined() + }) + }) + + // ========================================================================= + // altimate_core.safety + is_safe + // ========================================================================= + + describe("altimate_core.safety", () => { + test("clean SQL is safe", async () => { + const r = await D.call("altimate_core.safety", { sql: SQL.simple }) + expect((r.data as any).safe).toBe(true) + }) + + test("multi-statement detected", async () => { + const r = await D.call("altimate_core.safety", { sql: "SELECT 1; DROP TABLE users;" }) + expect((r.data as any).statement_count).toBeGreaterThan(1) + }) + + test("is_safe returns boolean", async () => { + const r = await D.call("altimate_core.is_safe", { sql: SQL.simple }) + expect(r.data.safe).toBe(true) + }) + }) + + // ========================================================================= + // altimate_core.explain + // ========================================================================= + + describe("altimate_core.explain", () => { + test("explains a query", async () => { + const r = await D.call("altimate_core.explain", { sql: SQL.simple, schema_context: ECOMMERCE_FLAT }) + expect(r.success).toBe(true) + }) + + test("explains a complex join", async () => { + const r = await D.call("altimate_core.explain", { sql: SQL.multiJoin, schema_context: ECOMMERCE_FLAT }) + expect(r.success).toBe(true) + }) + }) + + // ========================================================================= + // altimate_core.check (composite) + // ========================================================================= + + describe("altimate_core.check", () => { + test("returns validation, lint, and safety", async () => { + const r = await D.call("altimate_core.check", { sql: SQL.selectStar, schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + expect(d.validation).toBeDefined() + expect(d.lint).toBeDefined() + expect(d.safety).toBeDefined() + }) + }) + + // ========================================================================= + // altimate_core.fix + // ========================================================================= + + describe("altimate_core.fix", () => { + test("fixes typo in column name", async () => { + const r = await D.call("altimate_core.fix", { sql: "SELECT custmer_id FROM customers", schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + if (d.fixed) expect(d.fixed_sql.toLowerCase()).toContain("customer_id") + }) + + test("fixes typo in table name", async () => { + const r = await D.call("altimate_core.fix", { sql: "SELECT order_id FROM ordrs", schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + if (d.fixed) expect(d.fixed_sql.toLowerCase()).toContain("orders") + }) + + test("already valid SQL returns without error", async () => { + const r = await D.call("altimate_core.fix", { sql: SQL.simple, schema_context: ECOMMERCE_FLAT }) + expect(r.data).toBeDefined() + }) + + test("completely broken SQL handled gracefully", async () => { + const r = await D.call("altimate_core.fix", { sql: "GIBBERISH NONSENSE BLAH", schema_context: ECOMMERCE_FLAT }) + expect(r).toBeDefined() + }) + + test("reports iteration count", async () => { + const r = await D.call("altimate_core.fix", { sql: "SELECT nme FROM ordrs", schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).iterations).toBeDefined() + }) + }) + + // ========================================================================= + // altimate_core.grade + // ========================================================================= + + describe("altimate_core.grade", () => { + test("returns A-F grade with score breakdown", async () => { + const r = await D.call("altimate_core.grade", { sql: SQL.simple, schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + expect(d.overall_grade).toBeDefined() + expect(["A", "B", "C", "D", "F"]).toContain(d.overall_grade) + expect(d.scores).toBeDefined() + expect(typeof d.scores.overall).toBe("number") + expect(d.scores.overall).toBeGreaterThanOrEqual(0) + expect(d.scores.overall).toBeLessThanOrEqual(1) + for (const key of ["syntax", "style", "safety", "complexity"]) { + expect(d.scores[key]).toBeDefined() + } + }) + + test("explicit columns grade >= SELECT *", async () => { + const r1 = await D.call("altimate_core.grade", { sql: SQL.simple, schema_context: ECOMMERCE_FLAT }) + const r2 = await D.call("altimate_core.grade", { sql: SQL.selectStar, schema_context: ECOMMERCE_FLAT }) + expect((r1.data as any).scores.overall).toBeGreaterThanOrEqual((r2.data as any).scores.overall) + }) + + test("syntax error gets low grade", async () => { + const r = await D.call("altimate_core.grade", { sql: SQL.syntaxError, schema_context: ECOMMERCE_FLAT }) + expect(["C", "D", "F"]).toContain((r.data as any).overall_grade) + }) + }) + + // ========================================================================= + // altimate_core.rewrite + // ========================================================================= + + describe("altimate_core.rewrite", () => { + test("suggestions have rule and rewritten_sql", async () => { + const r = await D.call("altimate_core.rewrite", { sql: SQL.selectStar, schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + if (d.suggestions?.length) { + for (const s of d.suggestions) { + expect(s.rule).toBeDefined() + expect(s.rewritten_sql).toBeDefined() + } + } + }) + + test("does not crash on DML", async () => { + const r = await D.call("altimate_core.rewrite", { sql: "INSERT INTO orders (order_id) VALUES (1)", schema_context: ECOMMERCE_FLAT }) + expect(r).toBeDefined() + }) + }) + + // ========================================================================= + // altimate_core.testgen + // ========================================================================= + + describe("altimate_core.testgen", () => { + test("generates test cases with inputs and categories", async () => { + const r = await D.call("altimate_core.testgen", { sql: SQL.groupBy, schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + const tests = d.test_cases ?? d.tests ?? [] + expect(tests.length).toBeGreaterThan(0) + for (const tc of tests) { + expect(tc.name || tc.description).toBeTruthy() + expect(tc.category).toBeDefined() + expect(tc.sql || tc.inputs).toBeDefined() + } + }) + + test("CASE WHEN query gets tests", async () => { + const r = await D.call("altimate_core.testgen", { sql: SQL.caseWhen, schema_context: ECOMMERCE_FLAT }) + expect(((r.data as any).test_cases ?? []).length).toBeGreaterThan(0) + }) + }) + + // ========================================================================= + // altimate_core.complete + // ========================================================================= + + describe("altimate_core.complete", () => { + test("suggests columns after SELECT", async () => { + const r = await D.call("altimate_core.complete", { sql: "SELECT ", cursor_pos: 7, schema_context: ECOMMERCE_FLAT }) + const items = (r.data as any).items ?? [] + expect(items.length).toBeGreaterThan(0) + }) + + test("suggests tables after FROM", async () => { + const r = await D.call("altimate_core.complete", { sql: "SELECT * FROM ", cursor_pos: 14, schema_context: ECOMMERCE_FLAT }) + const items = (r.data as any).items ?? [] + expect(items.length).toBeGreaterThan(0) + const labels = items.map((i: any) => i.label) + expect(labels).toContain("customers") + expect(labels).toContain("orders") + }) + + test("cursor at 0 doesn't crash", async () => { + const r = await D.call("altimate_core.complete", { sql: "", cursor_pos: 0, schema_context: ECOMMERCE_FLAT }) + expect(r).toBeDefined() + }) + + test("cursor beyond length doesn't crash", async () => { + const r = await D.call("altimate_core.complete", { sql: "SELECT", cursor_pos: 999, schema_context: ECOMMERCE_FLAT }) + expect(r).toBeDefined() + }) + }) + + // ========================================================================= + // altimate_core.column_lineage + // ========================================================================= + + describe("altimate_core.column_lineage", () => { + test("traces direct column references", async () => { + const r = await D.call("altimate_core.column_lineage", { sql: "SELECT customer_id, first_name FROM customers", schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + expect(d.column_dict).toBeDefined() + expect(Object.keys(d.column_dict).length).toBeGreaterThan(0) + }) + + test("traces through JOIN", async () => { + const r = await D.call("altimate_core.column_lineage", { sql: SQL.join, schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).column_lineage?.length).toBeGreaterThan(0) + }) + + test("traces through CTE", async () => { + const r = await D.call("altimate_core.column_lineage", { sql: SQL.cte, schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + expect(d.column_dict && Object.keys(d.column_dict).length > 0 || d.column_lineage?.length > 0).toBe(true) + }) + + test("traces through aggregation", async () => { + const r = await D.call("altimate_core.column_lineage", { sql: SQL.groupBy, schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + expect(d.column_dict).toBeDefined() + expect(d.column_dict.customer_id).toBeDefined() + }) + + test("traces through CONCAT", async () => { + const r = await D.call("altimate_core.column_lineage", { + sql: "SELECT customer_id, first_name || ' ' || last_name AS full_name FROM customers", + schema_context: ECOMMERCE_FLAT, + }) + expect((r.data as any).column_dict?.full_name).toBeDefined() + }) + + test("traces through arithmetic", async () => { + const r = await D.call("altimate_core.column_lineage", { + sql: "SELECT order_id, amount * 1.1 AS with_tax FROM orders", + schema_context: ECOMMERCE_FLAT, + }) + expect((r.data as any).column_dict).toBeDefined() + }) + + test("no schema still returns partial lineage", async () => { + const r = await D.call("altimate_core.column_lineage", { sql: "SELECT a, b FROM t" }) + expect(r.success).toBe(true) + }) + }) + + // ========================================================================= + // altimate_core.schema_diff + // ========================================================================= + + describe("altimate_core.schema_diff", () => { + test("identical schemas — empty changes", async () => { + const r = await D.call("altimate_core.schema_diff", { schema1_context: ECOMMERCE_FLAT, schema2_context: ECOMMERCE_FLAT }) + const d = r.data as any + expect(d.changes).toHaveLength(0) + expect(d.has_breaking_changes).toBe(false) + }) + + test("detects added table", async () => { + const s2 = { ...ECOMMERCE_FLAT, reviews: { review_id: "INTEGER", content: "TEXT" } } + const r = await D.call("altimate_core.schema_diff", { schema1_context: ECOMMERCE_FLAT, schema2_context: s2 }) + expect((r.data as any).changes.some((c: any) => c.type === "table_added" && c.table === "reviews")).toBe(true) + }) + + test("detects removed table (breaking)", async () => { + const { products, ...without } = ECOMMERCE_FLAT + const r = await D.call("altimate_core.schema_diff", { schema1_context: ECOMMERCE_FLAT, schema2_context: without }) + const d = r.data as any + expect(d.changes.some((c: any) => c.type === "table_removed" && c.table === "products")).toBe(true) + expect(d.has_breaking_changes).toBe(true) + }) + + test("detects column type change (breaking)", async () => { + const s2 = { ...ECOMMERCE_FLAT, orders: { ...ECOMMERCE_FLAT.orders, amount: "BIGINT" } } + const r = await D.call("altimate_core.schema_diff", { schema1_context: ECOMMERCE_FLAT, schema2_context: s2 }) + expect((r.data as any).changes.some((c: any) => c.type === "column_type_changed" && c.column === "amount")).toBe(true) + }) + + test("case-insensitive type comparison", async () => { + const s1 = { t: { a: "varchar" } } + const s2 = { t: { a: "VARCHAR" } } + const r = await D.call("altimate_core.schema_diff", { schema1_context: s1, schema2_context: s2 }) + expect((r.data as any).changes.length).toBe(0) + }) + + test("only additions is non-breaking", async () => { + const s1 = { t: { a: "INT" } } + const s2 = { t: { a: "INT", b: "VARCHAR", c: "DATE" } } + const r = await D.call("altimate_core.schema_diff", { schema1_context: s1, schema2_context: s2 }) + const d = r.data as any + expect(d.has_breaking_changes).toBe(false) + expect(d.changes.length).toBe(2) + }) + + test("summary string is well-formed", async () => { + const s1 = { t: { a: "INT" } } + const s2 = { t: { a: "INT", b: "VARCHAR" } } + const r = await D.call("altimate_core.schema_diff", { schema1_context: s1, schema2_context: s2 }) + expect((r.data as any).summary).toContain("1 change") + }) + + test("100-column table diff", async () => { + const cols1: Record = {} + const cols2: Record = {} + for (let i = 0; i < 100; i++) { + cols1[`col_${i}`] = "VARCHAR" + cols2[`col_${i}`] = i < 50 ? "VARCHAR" : "INTEGER" + } + const r = await D.call("altimate_core.schema_diff", { schema1_context: { big: cols1 }, schema2_context: { big: cols2 } }) + expect((r.data as any).changes.length).toBe(50) + }) + + test("empty to full schema", async () => { + const r = await D.call("altimate_core.schema_diff", { schema1_context: {}, schema2_context: ECOMMERCE_FLAT }) + expect((r.data as any).changes.filter((c: any) => c.type === "table_added").length).toBe(Object.keys(ECOMMERCE_FLAT).length) + }) + }) + + // ========================================================================= + // altimate_core.equivalence + // ========================================================================= + + describe("altimate_core.equivalence", () => { + test("identical queries are equivalent", async () => { + const r = await D.call("altimate_core.equivalence", { sql1: SQL.simple, sql2: SQL.simple, schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).equivalent).toBe(true) + }) + + test("different queries are not equivalent", async () => { + const r = await D.call("altimate_core.equivalence", { + sql1: "SELECT customer_id FROM customers", + sql2: "SELECT order_id FROM orders", + schema_context: ECOMMERCE_FLAT, + }) + expect((r.data as any).equivalent).toBe(false) + }) + + test("semantically different WHERE are not equivalent", async () => { + const r = await D.call("altimate_core.equivalence", { + sql1: "SELECT customer_id FROM customers WHERE customer_id > 10", + sql2: "SELECT customer_id FROM customers WHERE customer_id < 10", + schema_context: ECOMMERCE_FLAT, + }) + expect((r.data as any).equivalent).toBe(false) + }) + }) + + // ========================================================================= + // altimate_core.semantics + // ========================================================================= + + describe("altimate_core.semantics", () => { + test("clean join has no issues", async () => { + const r = await D.call("altimate_core.semantics", { sql: SQL.join, schema_context: ECOMMERCE_FLAT }) + expect(r.success).toBe(true) + }) + }) + + // ========================================================================= + // altimate_core.correct + // ========================================================================= + + describe("altimate_core.correct", () => { + test("corrects broken SQL", async () => { + const r = await D.call("altimate_core.correct", { sql: "SELECT custmer_id FROM ordrs", schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + expect(d.status || d.fixed || d.corrected_sql).toBeDefined() + }) + }) + + // ========================================================================= + // altimate_core.transpile + // ========================================================================= + + describe("altimate_core.transpile", () => { + test("snowflake to postgres", async () => { + const r = await D.call("altimate_core.transpile", { sql: "SELECT NVL(first_name, 'Unknown') FROM customers", from_dialect: "snowflake", to_dialect: "postgres" }) + expect(r).toBeDefined() + }) + + test("same dialect is identity", async () => { + const r = await D.call("altimate_core.transpile", { sql: "SELECT 1", from_dialect: "postgres", to_dialect: "postgres" }) + expect(r).toBeDefined() + }) + }) + + // ========================================================================= + // altimate_core.format + // ========================================================================= + + describe("altimate_core.format", () => { + test("formats messy SQL", async () => { + const r = await D.call("altimate_core.format", { sql: "select a,b,c from t where x=1" }) + const d = r.data as any + expect(d.formatted_sql).toBeDefined() + expect(d.formatted_sql.length).toBeGreaterThan(0) + }) + }) + + // ========================================================================= + // altimate_core.metadata + // ========================================================================= + + describe("altimate_core.metadata", () => { + test("extracts tables, columns, and flags", async () => { + const r = await D.call("altimate_core.metadata", { sql: SQL.multiJoin }) + const d = r.data as any + expect(d.tables).toBeDefined() + expect(d.tables.length).toBe(3) + expect(d.has_aggregation).toBe(true) + }) + + test("detects subqueries", async () => { + // Use derived table (FROM subquery) which the engine reliably flags + const r = await D.call("altimate_core.metadata", { sql: "SELECT * FROM (SELECT customer_id FROM customers) t" }) + expect((r.data as any).has_subqueries).toBe(true) + }) + + test("detects window functions", async () => { + const r = await D.call("altimate_core.metadata", { sql: SQL.windowFunc }) + expect((r.data as any).has_window_functions).toBe(true) + }) + }) + + // ========================================================================= + // altimate_core.compare + // ========================================================================= + + describe("altimate_core.compare", () => { + test("identical queries — no diffs", async () => { + const r = await D.call("altimate_core.compare", { left_sql: SQL.simple, right_sql: SQL.simple }) + const d = r.data as any + expect(d.identical).toBe(true) + expect(d.diff_count).toBe(0) + }) + + test("different queries — has diffs", async () => { + const r = await D.call("altimate_core.compare", { left_sql: "SELECT a FROM t", right_sql: "SELECT a, b FROM t WHERE x > 0" }) + const d = r.data as any + expect(d.identical).toBe(false) + expect(d.diff_count).toBeGreaterThan(0) + }) + }) + + // ========================================================================= + // altimate_core.migration + // ========================================================================= + + describe("altimate_core.migration", () => { + test("adding nullable column is safe", async () => { + const r = await D.call("altimate_core.migration", { old_ddl: "CREATE TABLE t (id INT);", new_ddl: "CREATE TABLE t (id INT, name VARCHAR);" }) + expect(r).toBeDefined() + }) + + test("dropping column detected", async () => { + const r = await D.call("altimate_core.migration", { old_ddl: "CREATE TABLE t (id INT, name VARCHAR);", new_ddl: "CREATE TABLE t (id INT);" }) + expect(r).toBeDefined() + }) + }) + + // ========================================================================= + // altimate_core.import_ddl / export_ddl / fingerprint + // ========================================================================= + + describe("altimate_core DDL and fingerprint", () => { + test("import DDL", async () => { + const r = await D.call("altimate_core.import_ddl", { ddl: "CREATE TABLE users (id INT NOT NULL, name VARCHAR);" }) + const d = r.data as any + expect(d.success).toBe(true) + expect(d.schema).toBeDefined() + }) + + test("export DDL", async () => { + const r = await D.call("altimate_core.export_ddl", { schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).ddl).toContain("CREATE TABLE") + }) + + test("DDL roundtrip preserves tables", async () => { + const exp = await D.call("altimate_core.export_ddl", { schema_context: ECOMMERCE_FLAT }) + const imp = await D.call("altimate_core.import_ddl", { ddl: (exp.data as any).ddl }) + expect((imp.data as any).schema.tables).toBeDefined() + }) + + test("fingerprint is stable SHA-256", async () => { + const r1 = await D.call("altimate_core.fingerprint", { schema_context: ECOMMERCE_FLAT }) + const r2 = await D.call("altimate_core.fingerprint", { schema_context: ECOMMERCE_FLAT }) + expect((r1.data as any).fingerprint).toBe((r2.data as any).fingerprint) + expect((r1.data as any).fingerprint.length).toBe(64) + }) + + test("different schemas — different fingerprints", async () => { + const r1 = await D.call("altimate_core.fingerprint", { schema_context: { a: { x: "INT" } } }) + const r2 = await D.call("altimate_core.fingerprint", { schema_context: { b: { y: "INT" } } }) + expect((r1.data as any).fingerprint).not.toBe((r2.data as any).fingerprint) + }) + }) + + // ========================================================================= + // PII tools + // ========================================================================= + + describe("PII detection", () => { + test("classify_pii finds PII columns", async () => { + const r = await D.call("altimate_core.classify_pii", { schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + const cols = d.columns ?? d.findings ?? [] + expect(cols.length).toBeGreaterThan(0) + }) + + test("query_pii flags PII access", async () => { + const r = await D.call("altimate_core.query_pii", { sql: "SELECT first_name, email FROM customers", schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + expect(d.accesses_pii).toBe(true) + expect((d.pii_columns ?? []).length).toBeGreaterThan(0) + }) + + test("query_pii clean for non-PII columns", async () => { + const r = await D.call("altimate_core.query_pii", { sql: "SELECT customer_id FROM customers", schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).accesses_pii).toBe(false) + }) + }) + + // ========================================================================= + // altimate_core.resolve_term + // ========================================================================= + + describe("altimate_core.resolve_term", () => { + test("resolves exact column name", async () => { + const r = await D.call("altimate_core.resolve_term", { term: "email", schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + expect(d.matches?.length).toBeGreaterThan(0) + expect(d.matches[0].matched_column.column).toBe("email") + }) + + test("resolves fuzzy match", async () => { + const r = await D.call("altimate_core.resolve_term", { term: "price", schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).matches?.length).toBeGreaterThan(0) + }) + }) + + // ========================================================================= + // altimate_core.track_lineage + // ========================================================================= + + describe("altimate_core.track_lineage", () => { + test("tracks multi-query pipeline", async () => { + const r = await D.call("altimate_core.track_lineage", { + queries: [ + "CREATE TABLE staging AS SELECT customer_id, first_name FROM customers", + "CREATE TABLE summary AS SELECT customer_id, COUNT(*) AS cnt FROM staging GROUP BY customer_id", + ], + schema_context: ECOMMERCE_FLAT, + }) + expect(r.success).toBe(true) + }) + }) + + // ========================================================================= + // altimate_core.prune_schema / optimize_context + // ========================================================================= + + describe("Schema optimization", () => { + test("prune_schema to relevant tables", async () => { + const r = await D.call("altimate_core.prune_schema", { sql: "SELECT customer_id FROM customers", schema_context: ECOMMERCE_FLAT }) + const d = r.data as any + expect(d.relevant_tables).toBeDefined() + }) + + test("optimize_context returns compression info", async () => { + const r = await D.call("altimate_core.optimize_context", { schema_context: ECOMMERCE_FLAT }) + expect(r.success).toBe(true) + }) + }) + + // ========================================================================= + // altimate_core.introspection_sql + // ========================================================================= + + describe("altimate_core.introspection_sql", () => { + for (const dbType of ["snowflake", "postgres", "bigquery", "mysql", "redshift"]) { + test(`generates SQL for ${dbType}`, async () => { + const r = await D.call("altimate_core.introspection_sql", { db_type: dbType, database: "my_db", schema_name: "public" }) + expect(r).toBeDefined() + }) + } + }) + + // ========================================================================= + // altimate_core.policy + // ========================================================================= + + describe("altimate_core.policy", () => { + test("policy check with forbidden ops", async () => { + const r = await D.call("altimate_core.policy", { + sql: "SELECT * FROM customers", + schema_context: ECOMMERCE_FLAT, + policy_json: JSON.stringify({ forbidden_operations: ["DROP", "DELETE"] }), + }) + expect(r).toBeDefined() + }) + }) + + // ========================================================================= + // Composite SQL methods + // ========================================================================= + + describe("sql.analyze", () => { + test("returns issues not 'Unknown error'", async () => { + const r = await D.call("sql.analyze", { sql: SQL.selectStar, schema_context: ECOMMERCE_FLAT }) + expect(r.error).toBeUndefined() + expect(Array.isArray(r.issues)).toBe(true) + }) + }) + + describe("sql.rewrite", () => { + test("works with flat schema", async () => { + const r = await D.call("sql.rewrite", { sql: SQL.selectStar, schema_context: ECOMMERCE_FLAT }) + expect(r.success).toBe(true) + expect(r.error).toBeUndefined() + }) + }) + + describe("lineage.check", () => { + test("works with flat schema", async () => { + const r = await D.call("lineage.check", { sql: SQL.groupBy, dialect: "duckdb", schema_context: ECOMMERCE_FLAT }) + expect(r.success).toBe(true) + expect(r.error).toBeUndefined() + }) + + test("works with array schema", async () => { + const r = await D.call("lineage.check", { sql: "SELECT customer_id FROM customers", dialect: "duckdb", schema_context: ARRAY_SCHEMA }) + expect(r.success).toBe(true) + }) + }) + + // ========================================================================= + // Schema format variants + // ========================================================================= + + describe("Schema format variants", () => { + test("flat format loads for validate", async () => { + const r = await D.call("altimate_core.validate", { sql: "SELECT customer_id, first_name FROM customers", schema_context: ECOMMERCE_FLAT }) + expect((r.data as any).valid).toBe(true) + }) + + test("SchemaDefinition format still works", async () => { + const r = await D.call("altimate_core.validate", { sql: "SELECT customer_id FROM customers", schema_context: ECOMMERCE_SD }) + expect((r.data as any).valid).toBe(true) + }) + + test("array format loads for lineage", async () => { + const r = await D.call("lineage.check", { sql: "SELECT customer_id FROM customers", dialect: "duckdb", schema_context: ARRAY_SCHEMA }) + expect(r.success).toBe(true) + }) + + test("empty table in schema is skipped gracefully", async () => { + const r = await D.call("altimate_core.validate", { + sql: "SELECT customer_id FROM customers", + schema_context: { customers: { customer_id: "INTEGER" }, empty_table: {} }, + }) + expect(r).toBeDefined() + }) + }) +}) diff --git a/packages/opencode/test/altimate/altimate-core-native.test.ts b/packages/opencode/test/altimate/altimate-core-native.test.ts index 72f573e9d2..7deff69c86 100644 --- a/packages/opencode/test/altimate/altimate-core-native.test.ts +++ b/packages/opencode/test/altimate/altimate-core-native.test.ts @@ -65,6 +65,46 @@ describe("Schema Resolution", () => { }) expect(schema.tableNames()).toContain("orders") }) + + test("resolveSchema from flat format (tool-style schema_context)", () => { + // This is the format most tools pass: { "table_name": { "col": "TYPE" } } + const ctx = { + customers: { customer_id: "INTEGER", name: "VARCHAR", email: "VARCHAR" }, + orders: { order_id: "INTEGER", customer_id: "INTEGER", amount: "DECIMAL" }, + } + const schema = resolveSchema(undefined, ctx) + expect(schema).not.toBeNull() + const tables = schema!.tableNames().sort() + expect(tables).toContain("customers") + expect(tables).toContain("orders") + expect(schema!.columnNames("customers")).toContain("customer_id") + expect(schema!.columnNames("customers")).toContain("email") + expect(schema!.columnNames("orders")).toContain("amount") + }) + + test("resolveSchema from array-of-columns format (lineage_check style)", () => { + // This is the format lineage_check uses: { "table": [{ name, data_type }] } + const ctx = { + users: [ + { name: "id", data_type: "INT" }, + { name: "email", data_type: "VARCHAR" }, + ], + } + const schema = resolveSchema(undefined, ctx) + expect(schema).not.toBeNull() + expect(schema!.tableNames()).toContain("users") + expect(schema!.columnNames("users")).toContain("id") + expect(schema!.columnNames("users")).toContain("email") + }) + + test("schemaOrEmpty handles flat format without falling back to empty", () => { + const schema = schemaOrEmpty(undefined, { + products: { id: "INT", name: "VARCHAR", price: "DECIMAL" }, + }) + const tables = schema.tableNames() + expect(tables).toContain("products") + expect(tables).not.toContain("_empty_") + }) }) // --------------------------------------------------------------------------- diff --git a/packages/opencode/test/altimate/altimate-core-stress-e2e.test.ts b/packages/opencode/test/altimate/altimate-core-stress-e2e.test.ts new file mode 100644 index 0000000000..3291f04142 --- /dev/null +++ b/packages/opencode/test/altimate/altimate-core-stress-e2e.test.ts @@ -0,0 +1,354 @@ +/** + * Stress E2E tests for altimate-core tools. + * + * Covers: dialect matrix transpilation, fuzzy fix matching, large schema diffs, + * SQL pattern variations, cursor-position completions, PII across tables, + * term resolution, testgen feature coverage, and grading consistency. + * + * Requires @altimateai/altimate-core napi binary. + */ + +import { describe, expect, test, beforeAll, afterAll } from "bun:test" + +let coreAvailable = false +try { + require.resolve("@altimateai/altimate-core") + coreAvailable = true +} catch {} +const describeIf = coreAvailable ? describe : describe.skip + +const S = { + employees: { emp_id: "INTEGER", first_name: "VARCHAR", last_name: "VARCHAR", dept_id: "INTEGER", salary: "DECIMAL", hire_date: "DATE", manager_id: "INTEGER", email: "VARCHAR" }, + departments: { dept_id: "INTEGER", dept_name: "VARCHAR", location: "VARCHAR", budget: "DECIMAL" }, + projects: { proj_id: "INTEGER", proj_name: "VARCHAR", dept_id: "INTEGER", start_date: "DATE", end_date: "DATE", budget: "DECIMAL" }, + assignments: { assign_id: "INTEGER", emp_id: "INTEGER", proj_id: "INTEGER", role: "VARCHAR", hours: "DECIMAL" }, + salaries: { id: "INTEGER", emp_id: "INTEGER", amount: "DECIMAL", effective_date: "DATE", end_date: "DATE" }, + audit_log: { log_id: "INTEGER", table_name: "VARCHAR", action: "VARCHAR", old_value: "TEXT", new_value: "TEXT", changed_by: "INTEGER", changed_at: "TIMESTAMP" }, + customers: { cust_id: "INTEGER", company_name: "VARCHAR", contact_email: "VARCHAR", phone: "VARCHAR", address: "TEXT", country: "VARCHAR", credit_limit: "DECIMAL" }, + invoices: { inv_id: "INTEGER", cust_id: "INTEGER", amount: "DECIMAL", status: "VARCHAR", due_date: "DATE", paid_date: "DATE" }, +} + +describeIf("altimate-core Stress E2E", () => { + let D: any + beforeAll(async () => { + process.env.ALTIMATE_TELEMETRY_DISABLED = "true" + D = await import("../../src/altimate/native/dispatcher") + const core = await import("../../src/altimate/native/altimate-core") + const sql = await import("../../src/altimate/native/sql/register") + // Re-register handlers in case another test file called Dispatcher.reset() + core.registerAll() + sql.registerAllSql() + }) + afterAll(() => { delete process.env.ALTIMATE_TELEMETRY_DISABLED }) + + // ========================================================================= + // Validate: advanced SQL patterns + // ========================================================================= + + describe("Validate: advanced SQL patterns", () => { + const patterns = [ + { name: "recursive CTE", sql: `WITH RECURSIVE org AS (SELECT emp_id, manager_id, 1 AS lvl FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.emp_id, e.manager_id, o.lvl+1 FROM employees e JOIN org o ON e.manager_id = o.emp_id) SELECT * FROM org` }, + { name: "INTERSECT and EXCEPT", sql: `SELECT emp_id FROM assignments WHERE proj_id = 1 INTERSECT SELECT emp_id FROM assignments WHERE proj_id = 2 EXCEPT SELECT emp_id FROM assignments WHERE role = 'observer'` }, + { name: "multi-level subquery", sql: `SELECT * FROM employees WHERE emp_id IN (SELECT emp_id FROM assignments WHERE proj_id IN (SELECT proj_id FROM projects WHERE budget > (SELECT AVG(budget) FROM projects)))` }, + { name: "multiple window funcs", sql: `SELECT emp_id, salary, RANK() OVER w, DENSE_RANK() OVER w, NTILE(4) OVER w FROM employees WINDOW w AS (ORDER BY salary DESC)` }, + { name: "CASE in ORDER BY", sql: `SELECT emp_id, first_name, salary FROM employees ORDER BY CASE WHEN salary > 100000 THEN 1 WHEN salary > 50000 THEN 2 ELSE 3 END` }, + { name: "UPDATE with subquery", sql: `UPDATE employees SET salary = salary * 1.1 WHERE dept_id IN (SELECT dept_id FROM departments WHERE location = 'NYC')` }, + { name: "UNION ALL", sql: `SELECT emp_id, first_name, 'emp' AS src FROM employees UNION ALL SELECT cust_id, company_name, 'cust' FROM customers` }, + { name: "multi-table join", sql: `SELECT e.first_name, d.dept_name, p.proj_name, a.hours FROM employees e JOIN departments d ON e.dept_id = d.dept_id JOIN assignments a ON e.emp_id = a.emp_id JOIN projects p ON a.proj_id = p.proj_id WHERE d.location = 'NYC' ORDER BY a.hours DESC LIMIT 20` }, + { name: "HAVING with subquery", sql: `SELECT dept_id, COUNT(*) AS cnt FROM employees GROUP BY dept_id HAVING COUNT(*) > (SELECT AVG(c) FROM (SELECT COUNT(*) AS c FROM employees GROUP BY dept_id) t)` }, + { name: "CASE WHEN with NULL", sql: `SELECT emp_id, CASE WHEN salary IS NULL THEN 'unknown' WHEN salary > 100000 THEN 'high' ELSE 'normal' END AS tier, COALESCE(manager_id, 0) AS mgr FROM employees` }, + ] + for (const { name, sql } of patterns) { + test(name, async () => { + const r = await D.call("altimate_core.validate", { sql, schema_context: S }) + expect(r).toBeDefined() + expect(r.data).toBeDefined() + }) + } + }) + + // ========================================================================= + // Transpile: dialect matrix + // ========================================================================= + + describe("Transpile: dialect matrix", () => { + const pairs = [ + ["snowflake", "postgres"], ["snowflake", "bigquery"], ["postgres", "mysql"], + ["mysql", "postgres"], ["bigquery", "snowflake"], ["duckdb", "postgres"], + ["redshift", "snowflake"], ["postgres", "duckdb"], ["snowflake", "databricks"], + ["sqlite", "postgres"], + ] + for (const [from, to] of pairs) { + test(`${from} → ${to}`, async () => { + const r = await D.call("altimate_core.transpile", { + sql: "SELECT COALESCE(a, b), COUNT(*) FROM t WHERE x > 0 GROUP BY 1", + from_dialect: from, to_dialect: to, + }) + expect(r).toBeDefined() + const d = r.data as any + const transpiled = Array.isArray(d.transpiled_sql) ? d.transpiled_sql[0] : d.transpiled_sql + if (d.success !== false && transpiled) { + expect(transpiled.length).toBeGreaterThan(0) + } + }) + } + }) + + // ========================================================================= + // Fix: fuzzy matching + // ========================================================================= + + describe("Fix: fuzzy matching", () => { + const cases = [ + { name: "typo in table", sql: "SELECT emp_id FROM employes", match: "employees" }, + { name: "typo in column", sql: "SELECT fist_name FROM employees", match: "first_name" }, + { name: "missing underscore", sql: "SELECT empid FROM employees", match: "emp_id" }, + { name: "close match column", sql: "SELECT salry FROM employees", match: "salary" }, + { name: "preserves valid parts", sql: "SELECT emp_id, fist_name FROM employees WHERE dept_id = 1", match: "emp_id" }, + { name: "multiple errors", sql: "SELECT fist_name, lst_name FROM employes", match: "employees" }, + { name: "completely invalid", sql: "THIS IS NOT SQL AT ALL", match: null }, + { name: "valid SQL fast path", sql: "SELECT emp_id FROM employees", match: null }, + ] + for (const { name, sql, match } of cases) { + test(name, async () => { + const r = await D.call("altimate_core.fix", { sql, schema_context: S }) + const d = r.data as any + expect(d).toBeDefined() + if (match && d.fixed && d.fixed_sql) { + expect(d.fixed_sql.toLowerCase()).toContain(match) + } + }) + } + }) + + // ========================================================================= + // Grade: scoring consistency + // ========================================================================= + + describe("Grade: scoring consistency", () => { + const queries = [ + { name: "perfect", sql: "SELECT emp_id, first_name FROM employees WHERE dept_id = 1 ORDER BY first_name LIMIT 10" }, + { name: "select star", sql: "SELECT * FROM employees" }, + { name: "cartesian", sql: "SELECT * FROM employees, departments" }, + { name: "complex clean", sql: `SELECT e.first_name, d.dept_name, COUNT(a.assign_id) AS cnt FROM employees e JOIN departments d ON e.dept_id = d.dept_id LEFT JOIN assignments a ON e.emp_id = a.emp_id WHERE e.salary > 50000 GROUP BY e.first_name, d.dept_name HAVING COUNT(a.assign_id) > 0 ORDER BY cnt DESC LIMIT 20` }, + { name: "deeply nested", sql: `SELECT * FROM (SELECT * FROM (SELECT * FROM (SELECT * FROM employees) t1) t2) t3` }, + ] + for (const { name, sql } of queries) { + test(`grade: ${name}`, async () => { + const r = await D.call("altimate_core.grade", { sql, schema_context: S }) + const d = r.data as any + expect(["A", "B", "C", "D", "F"]).toContain(d.overall_grade) + expect(d.scores.overall).toBeGreaterThanOrEqual(0) + expect(d.scores.overall).toBeLessThanOrEqual(1) + }) + } + + test("clean > select_star", async () => { + const r1 = await D.call("altimate_core.grade", { sql: "SELECT emp_id FROM employees WHERE dept_id = 1 LIMIT 10", schema_context: S }) + const r2 = await D.call("altimate_core.grade", { sql: "SELECT * FROM employees", schema_context: S }) + expect((r1.data as any).scores.overall).toBeGreaterThanOrEqual((r2.data as any).scores.overall) + }) + }) + + // ========================================================================= + // Testgen: feature coverage + // ========================================================================= + + describe("Testgen: feature coverage", () => { + const cases = [ + "SELECT emp_id, salary FROM employees", + "SELECT emp_id FROM employees WHERE salary > 50000", + "SELECT dept_id, AVG(salary) AS avg_sal FROM employees GROUP BY dept_id", + "SELECT dept_id, COUNT(*) AS cnt FROM employees GROUP BY dept_id HAVING COUNT(*) > 5", + "SELECT e.first_name, d.dept_name FROM employees e JOIN departments d ON e.dept_id = d.dept_id", + "SELECT emp_id, RANK() OVER (ORDER BY salary DESC) AS rnk FROM employees", + "SELECT emp_id, CASE WHEN salary > 100000 THEN 'high' ELSE 'low' END AS tier FROM employees", + "SELECT DISTINCT dept_id FROM employees", + ] + for (const sql of cases) { + test(sql.substring(0, 50), async () => { + const r = await D.call("altimate_core.testgen", { sql, schema_context: S }) + const tests = (r.data as any).test_cases ?? [] + expect(tests.length).toBeGreaterThan(0) + for (const tc of tests) { + expect(tc.name || tc.description).toBeTruthy() + expect(tc.category).toBeDefined() + } + }) + } + }) + + // ========================================================================= + // Complete: cursor positions + // ========================================================================= + + describe("Complete: cursor positions", () => { + test("after SELECT", async () => { + const items = ((await D.call("altimate_core.complete", { sql: "SELECT ", cursor_pos: 7, schema_context: S })).data as any).items ?? [] + expect(items.length).toBeGreaterThan(0) + }) + + test("after FROM — all tables", async () => { + const items = ((await D.call("altimate_core.complete", { sql: "SELECT * FROM ", cursor_pos: 14, schema_context: S })).data as any).items ?? [] + const labels = new Set(items.map((i: any) => i.label)) + for (const t of Object.keys(S)) expect(labels.has(t)).toBe(true) + }) + + test("after WHERE", async () => { + const r = await D.call("altimate_core.complete", { sql: "SELECT * FROM employees WHERE ", cursor_pos: 30, schema_context: S }) + expect(((r.data as any).items ?? []).length).toBeGreaterThan(0) + }) + + test("after JOIN", async () => { + const r = await D.call("altimate_core.complete", { sql: "SELECT * FROM employees JOIN ", cursor_pos: 29, schema_context: S }) + expect(((r.data as any).items ?? []).length).toBeGreaterThan(0) + }) + + test("after table alias dot", async () => { + const items = ((await D.call("altimate_core.complete", { sql: "SELECT e. FROM employees e", cursor_pos: 9, schema_context: S })).data as any).items ?? [] + if (items.length > 0) { + expect(items.some((i: any) => ["emp_id", "first_name", "salary"].includes(i.label))).toBe(true) + } + }) + + test("partial table name", async () => { + const items = ((await D.call("altimate_core.complete", { sql: "SELECT * FROM emp", cursor_pos: 17, schema_context: S })).data as any).items ?? [] + if (items.length > 0) expect(items.some((i: any) => i.label === "employees")).toBe(true) + }) + }) + + // ========================================================================= + // Column lineage: transformation tracing + // ========================================================================= + + describe("Column lineage: transformations", () => { + test("through CONCAT", async () => { + const d = (await D.call("altimate_core.column_lineage", { sql: "SELECT emp_id, first_name || ' ' || last_name AS full_name FROM employees", schema_context: S })).data as any + expect(d.column_dict?.full_name).toBeDefined() + }) + + test("through arithmetic", async () => { + const d = (await D.call("altimate_core.column_lineage", { sql: "SELECT emp_id, salary * 12 AS annual, salary * 0.3 AS tax FROM employees", schema_context: S })).data as any + expect(Object.keys(d.column_dict).length).toBeGreaterThanOrEqual(3) + }) + + test("through multi-table join", async () => { + const d = (await D.call("altimate_core.column_lineage", { sql: `SELECT e.first_name, d.dept_name, p.proj_name, a.hours FROM employees e JOIN departments d ON e.dept_id = d.dept_id JOIN assignments a ON e.emp_id = a.emp_id JOIN projects p ON a.proj_id = p.proj_id`, schema_context: S })).data as any + expect(d.column_lineage?.length).toBeGreaterThanOrEqual(4) + }) + + test("through GROUP BY with aggregations", async () => { + const d = (await D.call("altimate_core.column_lineage", { sql: `SELECT dept_id, COUNT(*) AS headcount, AVG(salary) AS avg_sal, MIN(hire_date) AS earliest FROM employees GROUP BY dept_id`, schema_context: S })).data as any + expect(Object.keys(d.column_dict).length).toBeGreaterThanOrEqual(3) + }) + + test("through CTE chain", async () => { + const d = (await D.call("altimate_core.column_lineage", { sql: `WITH step1 AS (SELECT dept_id, AVG(salary) AS avg_sal FROM employees GROUP BY dept_id), step2 AS (SELECT dept_id, avg_sal, RANK() OVER (ORDER BY avg_sal DESC) AS dept_rank FROM step1) SELECT s.dept_id, d.dept_name, s.avg_sal FROM step2 s JOIN departments d ON s.dept_id = d.dept_id`, schema_context: S })).data as any + expect(d.column_dict).toBeDefined() + }) + + test("through window function", async () => { + const d = (await D.call("altimate_core.column_lineage", { sql: `SELECT emp_id, salary, SUM(salary) OVER (PARTITION BY dept_id ORDER BY hire_date) AS running FROM employees`, schema_context: S })).data as any + expect(d.column_dict).toBeDefined() + }) + + test("through UNION ALL", async () => { + const r = await D.call("altimate_core.column_lineage", { sql: `SELECT emp_id AS id, first_name AS name FROM employees UNION ALL SELECT cust_id AS id, company_name AS name FROM customers`, schema_context: S }) + expect(r.success).toBe(true) + }) + + test("star expansion", async () => { + const d = (await D.call("altimate_core.column_lineage", { sql: "SELECT * FROM departments", schema_context: S })).data as any + expect(d.column_dict).toBeDefined() + }) + }) + + // ========================================================================= + // PII across tables + // ========================================================================= + + describe("PII across tables", () => { + test("detects PII in employee columns", async () => { + const cols = ((await D.call("altimate_core.classify_pii", { schema_context: S })).data as any).columns ?? [] + expect(cols.length).toBeGreaterThanOrEqual(3) // email, first_name, last_name, contact_email, phone, address + }) + + test("query accessing PII across JOIN", async () => { + const d = (await D.call("altimate_core.query_pii", { sql: "SELECT e.email, c.contact_email, c.phone FROM employees e JOIN customers c ON e.emp_id = c.cust_id", schema_context: S })).data as any + expect(d.accesses_pii).toBe(true) + expect((d.pii_columns ?? []).length).toBeGreaterThanOrEqual(2) + }) + }) + + // ========================================================================= + // Resolve term + // ========================================================================= + + describe("Resolve term", () => { + test("resolves 'salary'", async () => { + expect(((await D.call("altimate_core.resolve_term", { term: "salary", schema_context: S })).data as any).matches?.length).toBeGreaterThan(0) + }) + + test("resolves 'budget' across tables", async () => { + expect(((await D.call("altimate_core.resolve_term", { term: "budget", schema_context: S })).data as any).matches?.length).toBeGreaterThanOrEqual(2) + }) + + test("resolves 'email' across tables", async () => { + expect(((await D.call("altimate_core.resolve_term", { term: "email", schema_context: S })).data as any).matches?.length).toBeGreaterThanOrEqual(2) + }) + }) + + // ========================================================================= + // Schema diff: mutation matrix + // ========================================================================= + + describe("Schema diff: mutations", () => { + test("rename column (add+remove)", async () => { + const d = (await D.call("altimate_core.schema_diff", { schema1_context: { t: { old_name: "VARCHAR" } }, schema2_context: { t: { new_name: "VARCHAR" } } })).data as any + expect(d.changes.length).toBe(2) + }) + + test("100-column type changes", async () => { + const c1: Record = {}, c2: Record = {} + for (let i = 0; i < 100; i++) { c1[`c${i}`] = "VARCHAR"; c2[`c${i}`] = i < 50 ? "VARCHAR" : "INTEGER" } + expect(((await D.call("altimate_core.schema_diff", { schema1_context: { big: c1 }, schema2_context: { big: c2 } })).data as any).changes.length).toBe(50) + }) + + test("full to empty", async () => { + const d = (await D.call("altimate_core.schema_diff", { schema1_context: S, schema2_context: {} })).data as any + expect(d.changes.filter((c: any) => c.type === "table_removed").length).toBe(Object.keys(S).length) + expect(d.has_breaking_changes).toBe(true) + }) + + test("special chars in names", async () => { + const d = (await D.call("altimate_core.schema_diff", { schema1_context: { "my-table": { "col-1": "INT" } }, schema2_context: { "my-table": { "col-1": "INT", "col-2": "VARCHAR" } } })).data as any + expect(d.changes.length).toBe(1) + }) + }) + + // ========================================================================= + // Lint: anti-pattern matrix + // ========================================================================= + + describe("Lint: anti-pattern matrix", () => { + test("SELECT * from join", async () => { + const d = (await D.call("altimate_core.lint", { sql: "SELECT * FROM employees e JOIN departments d ON e.dept_id = d.dept_id", schema_context: S })).data as any + expect(d.findings?.length).toBeGreaterThan(0) + }) + + test("function in WHERE", async () => { + const r = await D.call("altimate_core.lint", { sql: "SELECT * FROM employees WHERE UPPER(first_name) = 'JOHN'", schema_context: S }) + expect(r).toBeDefined() + }) + + test("DISTINCT on large result", async () => { + const r = await D.call("altimate_core.lint", { sql: "SELECT DISTINCT * FROM employees", schema_context: S }) + expect(r).toBeDefined() + }) + + test("implicit cross join", async () => { + const r = await D.call("altimate_core.lint", { sql: "SELECT e.first_name, d.dept_name FROM employees e, departments d", schema_context: S }) + expect(r).toBeDefined() + }) + }) +}) diff --git a/packages/opencode/test/altimate/issue-261-e2e.test.ts b/packages/opencode/test/altimate/issue-261-e2e.test.ts new file mode 100644 index 0000000000..940238a517 --- /dev/null +++ b/packages/opencode/test/altimate/issue-261-e2e.test.ts @@ -0,0 +1,329 @@ +/** + * End-to-end regression tests for GitHub issue #261. + * + * Tests all 11 broken/degraded tools against realistic inputs + * using the Dispatcher directly (bypasses the CLI/agent layer). + * + * Requires @altimateai/altimate-core napi binary to be installed. + */ + +import { describe, expect, test, beforeAll, afterAll } from "bun:test" + +// Check if napi binary is available +let coreAvailable = false +try { + require.resolve("@altimateai/altimate-core") + coreAvailable = true +} catch { + // napi binary not installed — skip tests +} + +const describeIf = coreAvailable ? describe : describe.skip + +// --------------------------------------------------------------------------- +// Test Data +// --------------------------------------------------------------------------- + +/** Flat schema format — what agents typically pass */ +const FLAT_SCHEMA = { + customers: { + customer_id: "INTEGER", + first_name: "VARCHAR", + last_name: "VARCHAR", + email: "VARCHAR", + }, + orders: { + order_id: "INTEGER", + customer_id: "INTEGER", + order_date: "DATE", + amount: "DECIMAL", + status: "VARCHAR", + }, + payments: { + payment_id: "INTEGER", + order_id: "INTEGER", + amount: "DECIMAL", + payment_method: "VARCHAR", + }, +} + +/** Array-of-columns format — what lineage_check uses */ +const ARRAY_SCHEMA = { + customers: [ + { name: "customer_id", data_type: "INTEGER" }, + { name: "first_name", data_type: "VARCHAR" }, + { name: "last_name", data_type: "VARCHAR" }, + { name: "email", data_type: "VARCHAR" }, + ], + orders: [ + { name: "order_id", data_type: "INTEGER" }, + { name: "customer_id", data_type: "INTEGER" }, + { name: "order_date", data_type: "DATE" }, + { name: "amount", data_type: "DECIMAL" }, + ], +} + +/** Two different schemas for diff testing */ +const SCHEMA1 = { + customers: { + customer_id: "INTEGER", + first_name: "VARCHAR", + last_name: "VARCHAR", + email: "VARCHAR", + }, +} + +const SCHEMA2 = { + customers: { + customer_id: "INTEGER", + full_name: "VARCHAR", + email: "VARCHAR", + phone: "VARCHAR", + }, +} + +/** Realistic compiled SQL from jaffle_shop */ +const CUSTOMERS_SQL = ` +SELECT + customers.customer_id, + customers.first_name, + customers.last_name, + customer_orders.first_order, + customer_orders.most_recent_order, + customer_orders.number_of_orders, + customer_payments.total_amount AS customer_lifetime_value +FROM customers +LEFT JOIN ( + SELECT customer_id, MIN(order_date) AS first_order, + MAX(order_date) AS most_recent_order, COUNT(order_id) AS number_of_orders + FROM orders GROUP BY customer_id +) AS customer_orders ON customers.customer_id = customer_orders.customer_id +LEFT JOIN ( + SELECT orders.customer_id, SUM(payments.amount) AS total_amount + FROM payments LEFT JOIN orders ON payments.order_id = orders.order_id + GROUP BY orders.customer_id +) AS customer_payments ON customers.customer_id = customer_payments.customer_id +` + +const SELECT_STAR_SQL = "SELECT * FROM customers" + +const BROKEN_SQL = "SELECT order_id as order_id asdas FROM orders" + +const GROUP_BY_SQL = ` +SELECT customer_id, COUNT(order_id) AS order_count, SUM(amount) AS total +FROM orders +GROUP BY customer_id +` + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describeIf("Issue #261 E2E: Tool Regression Tests", () => { + let Dispatcher: any + + beforeAll(async () => { + process.env.ALTIMATE_TELEMETRY_DISABLED = "true" + Dispatcher = await import("../../src/altimate/native/dispatcher") + const core = await import("../../src/altimate/native/altimate-core") + const sql = await import("../../src/altimate/native/sql/register") + // Re-register handlers in case another test file called Dispatcher.reset() + core.registerAll() + sql.registerAllSql() + }) + + afterAll(() => { + delete process.env.ALTIMATE_TELEMETRY_DISABLED + }) + + // ---- BROKEN TOOLS (should now work) ---- + + test("1. sql_rewrite — should NOT fail with schema parse error", async () => { + const result = await Dispatcher.call("sql.rewrite", { + sql: SELECT_STAR_SQL, + dialect: "duckdb", + schema_context: FLAT_SCHEMA, + }) + expect(result.success).toBe(true) + // Should not contain "missing field 'tables'" error + expect(result.error).toBeUndefined() + }) + + test("2. lineage_check — should NOT fail with schema parse error", async () => { + const result = await Dispatcher.call("lineage.check", { + sql: CUSTOMERS_SQL, + dialect: "duckdb", + schema_context: ARRAY_SCHEMA, + }) + expect(result.success).toBe(true) + expect(result.error).toBeUndefined() + // Should have lineage data + expect(result.data).toBeDefined() + }) + + test("3. altimate_core_fix — should attempt to fix broken SQL", async () => { + const result = await Dispatcher.call("altimate_core.fix", { + sql: BROKEN_SQL, + schema_context: FLAT_SCHEMA, + }) + // Should not silently fail — either fixes it or reports unfixable errors + expect(result.data).toBeDefined() + const data = result.data as Record + const hasResult = data.fixed_sql || data.unfixable_errors?.length || data.fixed !== undefined + expect(hasResult).toBeTruthy() + }) + + test("4. altimate_core_rewrite — should suggest rewrites for SELECT *", async () => { + const result = await Dispatcher.call("altimate_core.rewrite", { + sql: SELECT_STAR_SQL, + schema_context: FLAT_SCHEMA, + }) + expect(result.success).toBe(true) + const data = result.data as Record + // With a proper schema, SELECT * should trigger expand_select_star suggestion + const suggestions = data.suggestions ?? [] + // Even if no rewrite suggestions, it should not error + expect(result.error).toBeUndefined() + }) + + test("5. altimate_core_schema_diff — should detect differences between schemas", async () => { + const result = await Dispatcher.call("altimate_core.schema_diff", { + schema1_context: SCHEMA1, + schema2_context: SCHEMA2, + }) + expect(result.success).toBe(true) + const data = result.data as Record + // CRITICAL: Should NOT say "Schemas are identical" + expect(data.changes).toBeDefined() + expect(data.changes.length).toBeGreaterThan(0) + // Should detect: first_name removed, last_name removed, full_name added, phone added + expect(data.changes.length).toBeGreaterThanOrEqual(4) + expect(data.has_breaking_changes).toBe(true) + }) + + // ---- DEGRADED TOOLS (should now return useful data) ---- + + test("6. sql_analyze — should NOT show 'Unknown error'", async () => { + const result = await Dispatcher.call("sql.analyze", { + sql: SELECT_STAR_SQL, + dialect: "duckdb", + schema_context: FLAT_SCHEMA, + }) + // Should not have an error field (issues found is not an error) + expect(result.error).toBeUndefined() + // Should have issues array (SELECT * is a lint finding) + expect(result.issues).toBeDefined() + expect(Array.isArray(result.issues)).toBe(true) + }) + + test("7. altimate_core_validate — should return error details, not empty message", async () => { + const result = await Dispatcher.call("altimate_core.validate", { + sql: "SELECT nonexistent_column FROM customers", + schema_context: FLAT_SCHEMA, + }) + const data = result.data as Record + // If invalid, should have errors with messages + if (!data.valid) { + expect(data.errors).toBeDefined() + expect(data.errors.length).toBeGreaterThan(0) + expect(data.errors[0].message).toBeDefined() + expect(data.errors[0].message.length).toBeGreaterThan(0) + } + }) + + test("8. altimate_core_grade — should return a grade, not 'undefined'", async () => { + const result = await Dispatcher.call("altimate_core.grade", { + sql: CUSTOMERS_SQL, + schema_context: FLAT_SCHEMA, + }) + expect(result.success).toBe(true) + const data = result.data as Record + // Should have overall_grade field with A-F value + const grade = data.overall_grade ?? data.grade + expect(grade).toBeDefined() + expect(["A", "B", "C", "D", "F"]).toContain(grade) + // Should have scores + const scores = data.scores + expect(scores).toBeDefined() + expect(scores.overall).toBeDefined() + expect(typeof scores.overall).toBe("number") + }) + + test("9. altimate_core_column_lineage — should find lineage edges", async () => { + const result = await Dispatcher.call("altimate_core.column_lineage", { + sql: "SELECT customer_id, SUM(amount) AS total FROM orders GROUP BY customer_id", + dialect: "duckdb", + schema_context: FLAT_SCHEMA, + }) + expect(result.success).toBe(true) + const data = result.data as Record + // Should have column_dict or column_lineage + const hasLineage = (data.column_lineage?.length > 0) || (data.column_dict && Object.keys(data.column_dict).length > 0) + expect(hasLineage).toBe(true) + }) + + test("10. altimate_core_testgen — should generate test cases", async () => { + const result = await Dispatcher.call("altimate_core.testgen", { + sql: GROUP_BY_SQL, + schema_context: FLAT_SCHEMA, + }) + expect(result.success).toBe(true) + const data = result.data as Record + const tests = data.test_cases ?? data.tests ?? data.generated_tests ?? [] + // Should generate at least some test cases for a GROUP BY query + expect(tests.length).toBeGreaterThan(0) + }) + + test("11. altimate_core_complete — should return completions", async () => { + const result = await Dispatcher.call("altimate_core.complete", { + sql: "SELECT ", + cursor_pos: 7, + schema_context: FLAT_SCHEMA, + }) + expect(result.success).toBe(true) + const data = result.data as Record + const items = data.items ?? data.suggestions ?? [] + // Should suggest table names or columns from the schema + expect(items.length).toBeGreaterThan(0) + }) + + // ---- SCHEMA FORMAT VARIANTS ---- + + test("Flat format schema loads correctly for validate", async () => { + const result = await Dispatcher.call("altimate_core.validate", { + sql: "SELECT customer_id, first_name FROM customers", + schema_context: FLAT_SCHEMA, + }) + const data = result.data as Record + // Should validate successfully since columns exist in schema + expect(data.valid).toBe(true) + }) + + test("Array format schema loads correctly for lineage", async () => { + const result = await Dispatcher.call("lineage.check", { + sql: "SELECT customer_id FROM customers", + dialect: "duckdb", + schema_context: ARRAY_SCHEMA, + }) + expect(result.success).toBe(true) + expect(result.error).toBeUndefined() + }) + + test("SchemaDefinition format still works", async () => { + const result = await Dispatcher.call("altimate_core.validate", { + sql: "SELECT customer_id FROM customers", + schema_context: { + tables: { + customers: { + columns: [ + { name: "customer_id", type: "INTEGER" }, + { name: "email", type: "VARCHAR" }, + ], + }, + }, + }, + }) + const data = result.data as Record + expect(data.valid).toBe(true) + }) +}) diff --git a/packages/opencode/test/altimate/schema-resolver.test.ts b/packages/opencode/test/altimate/schema-resolver.test.ts new file mode 100644 index 0000000000..c4c0e8d294 --- /dev/null +++ b/packages/opencode/test/altimate/schema-resolver.test.ts @@ -0,0 +1,266 @@ +/** + * Unit tests for schema-resolver format normalization. + * + * These tests verify the flat-to-SchemaDefinition conversion logic + * WITHOUT requiring the @altimateai/altimate-core napi binary. + * The actual Schema.fromJson() call is tested in altimate-core-native.test.ts. + */ + +import { describe, expect, test } from "bun:test" + +// We can't import the functions directly because the module imports +// @altimateai/altimate-core at the top level. Instead, we test the +// pure conversion logic by extracting it into testable form. + +/** + * Detect whether a schema_context object is in SchemaDefinition format. + */ +function isSchemaDefinitionFormat(ctx: Record): boolean { + if (!("tables" in ctx) || typeof ctx.tables !== "object" || ctx.tables === null) { + return false + } + const values = Object.values(ctx.tables) + if (values.length === 0) return true + return values.some((v: any) => Array.isArray(v?.columns)) +} + +/** + * Convert flat schema format to SchemaDefinition format. + */ +function flatToSchemaDefinition(flat: Record): Record { + const tables: Record = {} + for (const [tableName, colsOrDef] of Object.entries(flat)) { + if (colsOrDef === null || colsOrDef === undefined) continue + if (Array.isArray(colsOrDef)) { + const columns = colsOrDef.map((c: any) => ({ + name: c.name, + type: c.type ?? c.data_type ?? "VARCHAR", + })) + tables[tableName] = { columns } + } else if (typeof colsOrDef === "object") { + if (Array.isArray(colsOrDef.columns)) { + tables[tableName] = colsOrDef + } else { + const columns = Object.entries(colsOrDef).map(([colName, colType]) => ({ + name: colName, + type: String(colType), + })) + tables[tableName] = { columns } + } + } + } + return { tables } +} + +function normalizeSchemaContext(ctx: Record): string { + if (isSchemaDefinitionFormat(ctx)) { + return JSON.stringify(ctx) + } + return JSON.stringify(flatToSchemaDefinition(ctx)) +} + +// --------------------------------------------------------------------------- +// Format Detection +// --------------------------------------------------------------------------- + +describe("isSchemaDefinitionFormat", () => { + test("detects SchemaDefinition format", () => { + expect(isSchemaDefinitionFormat({ + tables: { users: { columns: [{ name: "id", type: "INT" }] } }, + })).toBe(true) + }) + + test("detects SchemaDefinition with version/dialect", () => { + expect(isSchemaDefinitionFormat({ + version: "1", + dialect: "generic", + tables: { users: { columns: [{ name: "id", type: "INT" }] } }, + })).toBe(true) + }) + + test("detects SchemaDefinition with empty tables map", () => { + expect(isSchemaDefinitionFormat({ + tables: {}, + })).toBe(true) + }) + + test("rejects flat format", () => { + expect(isSchemaDefinitionFormat({ + users: { id: "INT", name: "VARCHAR" }, + })).toBe(false) + }) + + test("rejects array format", () => { + expect(isSchemaDefinitionFormat({ + users: [{ name: "id", data_type: "INT" }], + })).toBe(false) + }) + + test("rejects flat schema with table named 'tables'", () => { + // A flat schema that happens to have a table called "tables" + // should NOT be mistaken for SchemaDefinition format + expect(isSchemaDefinitionFormat({ + tables: { id: "INT", name: "VARCHAR" }, + users: { id: "INT", email: "VARCHAR" }, + })).toBe(false) + }) + + test("rejects empty object", () => { + expect(isSchemaDefinitionFormat({})).toBe(false) + }) +}) + +// --------------------------------------------------------------------------- +// Flat Format Conversion +// --------------------------------------------------------------------------- + +describe("flatToSchemaDefinition", () => { + test("converts flat map format", () => { + const result = flatToSchemaDefinition({ + customers: { customer_id: "INTEGER", name: "VARCHAR", email: "VARCHAR" }, + orders: { order_id: "INTEGER", amount: "DECIMAL" }, + }) + + expect(result.tables).toBeDefined() + expect(result.tables.customers.columns).toHaveLength(3) + expect(result.tables.customers.columns).toContainEqual({ name: "customer_id", type: "INTEGER" }) + expect(result.tables.customers.columns).toContainEqual({ name: "name", type: "VARCHAR" }) + expect(result.tables.orders.columns).toHaveLength(2) + }) + + test("converts array-of-columns format (lineage_check style)", () => { + const result = flatToSchemaDefinition({ + users: [ + { name: "id", data_type: "INT" }, + { name: "email", data_type: "VARCHAR" }, + ], + }) + + expect(result.tables.users.columns).toHaveLength(2) + expect(result.tables.users.columns).toContainEqual({ name: "id", type: "INT" }) + expect(result.tables.users.columns).toContainEqual({ name: "email", type: "VARCHAR" }) + }) + + test("passes through partial SchemaDefinition format", () => { + const result = flatToSchemaDefinition({ + users: { columns: [{ name: "id", type: "INT" }] }, + }) + + expect(result.tables.users.columns).toEqual([{ name: "id", type: "INT" }]) + }) + + test("handles mixed formats", () => { + const result = flatToSchemaDefinition({ + flat_table: { id: "INT", name: "VARCHAR" }, + array_table: [{ name: "id", data_type: "BIGINT" }], + sd_table: { columns: [{ name: "id", type: "INT" }] }, + }) + + expect(result.tables.flat_table.columns).toHaveLength(2) + expect(result.tables.array_table.columns).toHaveLength(1) + expect(result.tables.sd_table.columns).toHaveLength(1) + }) + + test("skips null/undefined values", () => { + const result = flatToSchemaDefinition({ + valid: { id: "INT" }, + invalid: null, + }) + + expect(Object.keys(result.tables)).toEqual(["valid"]) + }) + + test("handles array-of-columns with type field", () => { + const result = flatToSchemaDefinition({ + users: [ + { name: "id", type: "INT" }, + { name: "name", type: "VARCHAR" }, + ], + }) + + expect(result.tables.users.columns).toContainEqual({ name: "id", type: "INT" }) + }) +}) + +// --------------------------------------------------------------------------- +// End-to-End Normalization +// --------------------------------------------------------------------------- + +describe("normalizeSchemaContext", () => { + test("passes through SchemaDefinition format unchanged", () => { + const ctx = { + version: "1", + tables: { + users: { columns: [{ name: "id", type: "INT" }] }, + }, + } + const result = JSON.parse(normalizeSchemaContext(ctx)) + expect(result.version).toBe("1") + expect(result.tables.users.columns[0].name).toBe("id") + }) + + test("converts flat format to SchemaDefinition", () => { + const ctx = { + customers: { customer_id: "INTEGER", name: "VARCHAR" }, + } + const result = JSON.parse(normalizeSchemaContext(ctx)) + expect(result.tables).toBeDefined() + expect(result.tables.customers.columns).toContainEqual({ name: "customer_id", type: "INTEGER" }) + }) + + test("produces valid JSON for Rust SchemaDefinition deserialization", () => { + const ctx = { + customers: { + customer_id: "INTEGER", + first_name: "VARCHAR", + last_name: "VARCHAR", + email: "VARCHAR", + }, + } + const json = normalizeSchemaContext(ctx) + const parsed = JSON.parse(json) + + // Must have `tables` key + expect(parsed.tables).toBeDefined() + // Each table must have `columns` array + expect(Array.isArray(parsed.tables.customers.columns)).toBe(true) + // Each column must have `name` and `type` + for (const col of parsed.tables.customers.columns) { + expect(typeof col.name).toBe("string") + expect(typeof col.type).toBe("string") + } + }) + + test("schema_diff scenario: two different schemas normalize correctly", () => { + const schema1 = { + customers: { + customer_id: "INTEGER", + first_name: "VARCHAR", + last_name: "VARCHAR", + email: "VARCHAR", + }, + } + const schema2 = { + customers: { + customer_id: "INTEGER", + full_name: "VARCHAR", + email: "VARCHAR", + phone: "VARCHAR", + }, + } + + const s1 = JSON.parse(normalizeSchemaContext(schema1)) + const s2 = JSON.parse(normalizeSchemaContext(schema2)) + + // Schema 1 columns + const s1Cols = s1.tables.customers.columns.map((c: any) => c.name).sort() + expect(s1Cols).toEqual(["customer_id", "email", "first_name", "last_name"]) + + // Schema 2 columns + const s2Cols = s2.tables.customers.columns.map((c: any) => c.name).sort() + expect(s2Cols).toEqual(["customer_id", "email", "full_name", "phone"]) + + // These are clearly different — the diff engine should find changes + expect(s1Cols).not.toEqual(s2Cols) + }) +}) diff --git a/packages/opencode/test/altimate/tool-formatters.test.ts b/packages/opencode/test/altimate/tool-formatters.test.ts new file mode 100644 index 0000000000..4dc9b53409 --- /dev/null +++ b/packages/opencode/test/altimate/tool-formatters.test.ts @@ -0,0 +1,216 @@ +/** + * Tests for tool output formatting functions. + * + * These tests verify that tool formatters correctly handle the data + * shapes returned by the Rust altimate-core napi bindings. + * Tests don't require the napi binary — they test formatter logic only. + */ + +import { describe, expect, test } from "bun:test" + +// We can't import formatters directly (they're not exported), so we +// test the logic patterns they use against known Rust output shapes. + +describe("sql_analyze result interpretation", () => { + test("issues found should not show 'Unknown error'", () => { + // Simulates: sql.analyze returns issues but success=false + const result = { + success: false, + issues: [ + { type: "lint", severity: "warning", message: "SELECT * detected" }, + ], + issue_count: 1, + confidence: "high", + confidence_factors: ["lint"], + } + // The bug: !result.success + no result.error = "Unknown error" + // The fix: check result.error instead of !result.success + const hasError = !!(result as any).error + expect(hasError).toBe(false) + expect(result.issues.length).toBeGreaterThan(0) + }) +}) + +describe("altimate_core_grade result mapping", () => { + test("maps Rust EvalResult fields correctly", () => { + // Simulates Rust EvalResult output + const rustOutput = { + sql: "SELECT * FROM users", + scores: { + syntax: 1.0, + style: 0.6, + safety: 1.0, + complexity: 0.8, + overall: 0.84, + }, + overall_grade: "B", + total_time_ms: 12, + } + + // Tool accesses: + const grade = rustOutput.overall_grade // NOT .grade + const score = rustOutput.scores?.overall != null + ? Math.round(rustOutput.scores.overall * 100) + : null + + expect(grade).toBe("B") + expect(score).toBe(84) + }) + + test("handles legacy format with .grade field", () => { + const legacyOutput = { grade: "A", score: 95 } + const grade = (legacyOutput as any).overall_grade ?? legacyOutput.grade + expect(grade).toBe("A") + }) +}) + +describe("altimate_core_complete result mapping", () => { + test("maps Rust CompletionResult fields correctly", () => { + // Simulates Rust CompletionResult + const rustOutput = { + cursor_offset: 15, + context: "after_from", + items: [ + { label: "users", kind: "table", detail: "users table" }, + { label: "orders", kind: "table", detail: "orders table" }, + ], + } + + // Tool should use .items, not .suggestions + const count = rustOutput.items?.length ?? 0 + expect(count).toBe(2) + }) +}) + +describe("altimate_core_schema_diff result mapping", () => { + test("maps Rust SchemaDiff fields correctly", () => { + // Simulates Rust SchemaDiff output + const rustOutput = { + changes: [ + { type: "column_removed", table: "customers", column: "first_name" }, + { type: "column_removed", table: "customers", column: "last_name" }, + { type: "column_added", table: "customers", column: "full_name", data_type: "VARCHAR" }, + { type: "column_added", table: "customers", column: "phone", data_type: "VARCHAR" }, + ], + has_breaking_changes: true, + summary: "4 changes (2 breaking)", + } + + // Tool should use has_breaking_changes, not has_breaking + const hasBreaking = rustOutput.has_breaking_changes + expect(hasBreaking).toBe(true) + expect(rustOutput.changes.length).toBe(4) + + // Breaking type detection + const breakingTypes = new Set(["table_removed", "column_removed", "column_type_changed"]) + const breakingChanges = rustOutput.changes.filter(c => + breakingTypes.has(c.type) || + (c.type === "nullability_changed" && (c as any).old_nullable && !(c as any).new_nullable) + ) + expect(breakingChanges.length).toBe(2) + }) + + test("empty changes should report 'Schemas are identical'", () => { + const rustOutput = { + changes: [], + has_breaking_changes: false, + summary: "0 changes (0 breaking)", + } + expect(rustOutput.changes.length).toBe(0) + }) +}) + +describe("altimate_core_rewrite result mapping", () => { + test("maps Rust RewriteResult fields correctly", () => { + // Simulates Rust RewriteResult + const rustOutput = { + original_sql: "SELECT * FROM users", + suggestions: [ + { + rule: "expand_select_star", + explanation: "Replace SELECT * with explicit columns", + rewritten_sql: "SELECT id, name, email FROM users", + improvement: "Reduce data transfer", + confidence: 0.9, + }, + ], + index_suggestions: [], + } + + // Tool should use .suggestions, not .rewrites + const suggestions = rustOutput.suggestions ?? [] + expect(suggestions.length).toBe(1) + expect(suggestions[0].rewritten_sql).toContain("id, name, email") + }) +}) + +describe("altimate_core_column_lineage result mapping", () => { + test("maps Rust CompleteLineageResult fields correctly", () => { + // Simulates Rust CompleteLineageResult + const rustOutput = { + tier: "full", + depth: "full", + column_dict: { + customer_id: ['"orders"."customer_id"'], + total: ['"orders"."amount"'], + }, + column_lineage: [ + { + source: '"orders"."customer_id"', + target: "customer_id", + lineage_type: "direct", + lens_type: "passthrough", + lens_code: [], + }, + { + source: '"orders"."amount"', + target: "total", + lineage_type: "direct", + lens_type: "aggregate", + lens_code: [{ expression: "SUM(amount)", step_type: "transform" }], + }, + ], + source_tables: ["orders"], + output_columns: ["customer_id", "total"], + } + + expect(rustOutput.column_lineage.length).toBe(2) + expect(rustOutput.column_dict).toBeDefined() + // Transform info should use lens_type, not transform + expect(rustOutput.column_lineage[1].lens_type).toBe("aggregate") + }) +}) + +describe("altimate_core_fix result mapping", () => { + test("maps Rust FixResult fields correctly", () => { + // Simulates Rust FixResult + const rustOutput = { + original_sql: "SELECT id FROM uesrs", + fixed: true, + fixed_sql: "SELECT id FROM users", + fixes_applied: [ + { description: "Fixed table name: uesrs -> users" }, + ], + unfixable_errors: [], + } + + expect(rustOutput.fixed).toBe(true) + expect(rustOutput.fixed_sql).toBe("SELECT id FROM users") + expect(rustOutput.fixes_applied.length).toBe(1) + }) + + test("handles unfixable errors", () => { + const rustOutput = { + original_sql: "SELECT asdas FROM nonexistent", + fixed: false, + fixed_sql: "SELECT asdas FROM nonexistent", + fixes_applied: [], + unfixable_errors: [ + { message: "Table 'nonexistent' not found", reason: "no fuzzy match" }, + ], + } + + expect(rustOutput.fixed).toBe(false) + expect(rustOutput.unfixable_errors.length).toBe(1) + }) +})