Skip to content

Commit 6972b58

Browse files
committed
refactor: move unknown column helper tool
1 parent 010104c commit 6972b58

2 files changed

Lines changed: 267 additions & 217 deletions

File tree

src/mcp/server.ts

Lines changed: 3 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ import {
115115
columnTypeLabel,
116116
normalizeTableName,
117117
numberRecordValue,
118-
profileColumnMetadata,
119118
selectTableProfileColumns,
120119
stringRecordValue,
121120
tableProfileIndexSummary,
@@ -127,21 +126,12 @@ import {
127126
} from "./tools/profile-utils.js";
128127
import { registerSqlConversionTools } from "./tools/sql-conversion.js";
129128
import { registerSqlErrorTools } from "./tools/sql-errors.js";
130-
import {
131-
lintReadOnlySql,
132-
registerSqlLintTools
133-
} from "./tools/sql-lint.js";
129+
import { registerSqlLintTools } from "./tools/sql-lint.js";
134130
import { registerSqlMapperContextTools } from "./tools/sql-mapper-context.js";
135131
import { registerSqlProjectQueryTools } from "./tools/sql-project-query.js";
136132
import { registerSqlQueryDocTools } from "./tools/sql-query-doc.js";
137133
import { registerSqlValidationTools } from "./tools/sql-validation.js";
138-
import {
139-
normalizedSnippetTableRef,
140-
validateSnippetMetadata
141-
} from "./tools/sql-snippet-metadata.js";
142-
import {
143-
resolveProjectQuerySource
144-
} from "./tools/sql-snippet-source.js";
134+
import { registerSqlUnknownColumnTools } from "./tools/sql-unknown-columns.js";
145135
import {
146136
findPrepareQueryCandidates,
147137
prepareQuerySearchTerms,
@@ -327,71 +317,10 @@ export function createObMcpServer(
327317
registerSqlProjectQueryTools(registerTool, runtime, context);
328318
registerSqlQueryDocTools(registerTool, runtime, context);
329319
registerSqlMapperContextTools(registerTool, runtime, context);
320+
registerSqlUnknownColumnTools(registerTool, runtime, context);
330321

331322
registerSqlPrepareTools(registerTool, runtime, context);
332323

333-
registerTool(
334-
"ob_find_unknown_columns_in_sql",
335-
{
336-
title: "Find Unknown Columns In SQL",
337-
description: "Find unknown tables or columns in SQL and return visible metadata-based candidates without executing the SQL.",
338-
inputSchema: {
339-
...profileInput,
340-
sql: z.string().min(1).optional(),
341-
filePath: z.string().min(1).optional(),
342-
line: z.number().int().positive().optional(),
343-
schema: z.string().optional(),
344-
maxCandidates: z.number().int().positive().max(20).optional()
345-
}
346-
},
347-
async ({ profile, sql, filePath, line, schema, maxCandidates }) => {
348-
const selected = selectAuthorizedProfile(runtime, profile, context);
349-
const source = resolveProjectQuerySource(sql, filePath, line);
350-
const candidateLimit = Math.min(maxCandidates ?? 5, 20);
351-
const lint = await lintReadOnlySql(selected, source.sql, [], effectiveRowLimit(undefined, selected.config), false);
352-
const policy = explainSqlPolicy(source.sql, policyContext(selected.config));
353-
const metadata = await validateSnippetMetadata(selected, source.sql, policy.tableRefs, metadataSchema(selected.config, schema));
354-
const tableIssues = [];
355-
for (const table of metadata.tables.filter((item) => !item.visible)) {
356-
tableIssues.push({
357-
schema: table.schema,
358-
table: table.table,
359-
error: table.error,
360-
candidates: await findUnknownTableCandidates(selected, table.schema, table.table, candidateLimit)
361-
});
362-
}
363-
const columnIssues = [];
364-
for (const table of metadata.tables.filter((item) => item.visible && item.unknownColumns.length > 0)) {
365-
for (const column of table.unknownColumns.slice(0, 20)) {
366-
columnIssues.push({
367-
schema: table.schema,
368-
table: table.table,
369-
column,
370-
candidates: await findUnknownColumnCandidates(selected, table.schema, table.table, column, candidateLimit)
371-
});
372-
}
373-
}
374-
return toLimitedResult(
375-
{
376-
source: source.source,
377-
sql: source.sql,
378-
executed: false,
379-
lint,
380-
metadata,
381-
unknownTableCount: tableIssues.length,
382-
unknownColumnCount: columnIssues.length,
383-
tableIssues,
384-
columnIssues,
385-
warnings: uniqueStrings([
386-
...(source.source.warnings ?? []),
387-
"Candidates are based on visible metadata only."
388-
])
389-
},
390-
selected.config
391-
);
392-
}
393-
);
394-
395324
registerDocumentationTools(registerTool, runtime, context);
396325

397326
registerTool(
@@ -1826,149 +1755,6 @@ function uniqueTableRefs(refs: Array<{ schema?: string; table: string }>): Array
18261755
return output;
18271756
}
18281757

1829-
async function findUnknownTableCandidates(
1830-
selected: ReturnType<typeof selectRuntimeProfile>,
1831-
schema: string | undefined,
1832-
table: string,
1833-
limit: number
1834-
): Promise<Array<{ schema?: string; table: string; score: number; evidence: string[] }>> {
1835-
const effectiveSchema = metadataSchema(selected.config, schema);
1836-
if (effectiveSchema && !isSchemaAllowed(effectiveSchema, selected.config.policy)) {
1837-
return [];
1838-
}
1839-
const rows = filterTables(
1840-
await withQuerySlot(selected, () =>
1841-
listTables(
1842-
selected.pool,
1843-
selected.config.compatibility,
1844-
effectiveSchema,
1845-
500,
1846-
selected.config.queryTimeoutMs
1847-
)
1848-
),
1849-
selected.config.policy
1850-
);
1851-
return rows
1852-
.map((row) => tableRefFromMetadata(row, effectiveSchema))
1853-
.filter((ref): ref is { schema?: string; table: string } => Boolean(ref))
1854-
.map((ref) => ({
1855-
schema: ref.schema,
1856-
table: ref.table,
1857-
score: similarityScore(table, ref.table),
1858-
evidence: similarityEvidence(table, ref.table)
1859-
}))
1860-
.filter((candidate) => candidate.score >= 0.25)
1861-
.sort((left, right) => right.score - left.score || left.table.localeCompare(right.table))
1862-
.slice(0, limit);
1863-
}
1864-
1865-
async function findUnknownColumnCandidates(
1866-
selected: ReturnType<typeof selectRuntimeProfile>,
1867-
schema: string | undefined,
1868-
table: string,
1869-
column: string,
1870-
limit: number
1871-
): Promise<Array<{ schema?: string; table: string; column: string; type?: string; score: number; evidence: string[] }>> {
1872-
try {
1873-
const metadataRef = normalizedSnippetTableRef(selected.config, schema, table);
1874-
assertTableAccess(metadataRef.schema, metadataRef.table, policyContext(selected.config));
1875-
const rows = filterColumnMetadata(
1876-
await withQuerySlot(selected, () =>
1877-
describeTable(
1878-
selected.pool,
1879-
selected.config.compatibility,
1880-
metadataRef.table,
1881-
metadataRef.schema,
1882-
selected.config.queryTimeoutMs
1883-
)
1884-
),
1885-
selected.config.policy
1886-
);
1887-
return rows
1888-
.map((row) => profileColumnMetadata(row, selected.config.compatibility))
1889-
.filter((item): item is { name: string; dataType?: string; nullable?: boolean; comment?: string } => Boolean(item))
1890-
.map((item) => ({
1891-
schema: metadataRef.schema,
1892-
table: metadataRef.table,
1893-
column: item.name,
1894-
type: item.dataType,
1895-
score: similarityScore(column, item.name),
1896-
evidence: similarityEvidence(column, item.name)
1897-
}))
1898-
.filter((candidate) => candidate.score >= 0.25)
1899-
.sort((left, right) => right.score - left.score || left.column.localeCompare(right.column))
1900-
.slice(0, limit);
1901-
} catch {
1902-
return [];
1903-
}
1904-
}
1905-
1906-
function similarityScore(left: string, right: string): number {
1907-
const a = normalizeSimilarityText(left);
1908-
const b = normalizeSimilarityText(right);
1909-
if (!a || !b) {
1910-
return 0;
1911-
}
1912-
if (a === b) {
1913-
return 1;
1914-
}
1915-
if (a.includes(b) || b.includes(a)) {
1916-
return 0.86;
1917-
}
1918-
const distance = levenshteinDistance(a, b);
1919-
const ratio = 1 - distance / Math.max(a.length, b.length);
1920-
const prefix = commonPrefixLength(a, b) / Math.max(a.length, b.length);
1921-
return Number(Math.max(ratio, prefix * 0.9).toFixed(3));
1922-
}
1923-
1924-
function similarityEvidence(left: string, right: string): string[] {
1925-
const a = normalizeSimilarityText(left);
1926-
const b = normalizeSimilarityText(right);
1927-
const evidence = [];
1928-
if (a === b) {
1929-
evidence.push("normalized_exact_match");
1930-
}
1931-
if (a.includes(b) || b.includes(a)) {
1932-
evidence.push("substring_match");
1933-
}
1934-
if (commonPrefixLength(a, b) >= 3) {
1935-
evidence.push("common_prefix");
1936-
}
1937-
if (levenshteinDistance(a, b) <= 2) {
1938-
evidence.push("edit_distance_le_2");
1939-
}
1940-
return evidence.length > 0 ? evidence : ["name_similarity"];
1941-
}
1942-
1943-
function normalizeSimilarityText(value: string): string {
1944-
return value.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
1945-
}
1946-
1947-
function commonPrefixLength(left: string, right: string): number {
1948-
let index = 0;
1949-
while (index < left.length && index < right.length && left[index] === right[index]) {
1950-
index += 1;
1951-
}
1952-
return index;
1953-
}
1954-
1955-
function levenshteinDistance(left: string, right: string): number {
1956-
const previous = Array.from({ length: right.length + 1 }, (_value, index) => index);
1957-
for (let i = 0; i < left.length; i += 1) {
1958-
const current = [i + 1];
1959-
for (let j = 0; j < right.length; j += 1) {
1960-
const cost = left[i] === right[j] ? 0 : 1;
1961-
current[j + 1] = Math.min(
1962-
current[j] + 1,
1963-
previous[j + 1] + 1,
1964-
previous[j] + cost
1965-
);
1966-
}
1967-
previous.splice(0, previous.length, ...current);
1968-
}
1969-
return previous[right.length] ?? 0;
1970-
}
1971-
19721758
function columnNames(rows: unknown[]): string[] {
19731759
const names: string[] = [];
19741760
for (const row of rows) {

0 commit comments

Comments
 (0)