@@ -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" ;
128127import { registerSqlConversionTools } from "./tools/sql-conversion.js" ;
129128import { 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" ;
134130import { registerSqlMapperContextTools } from "./tools/sql-mapper-context.js" ;
135131import { registerSqlProjectQueryTools } from "./tools/sql-project-query.js" ;
136132import { registerSqlQueryDocTools } from "./tools/sql-query-doc.js" ;
137133import { 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" ;
145135import {
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 - z 0 - 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-
19721758function columnNames ( rows : unknown [ ] ) : string [ ] {
19731759 const names : string [ ] = [ ] ;
19741760 for ( const row of rows ) {
0 commit comments