@@ -70,8 +70,7 @@ import { metadataSchema, metadataTableRef } from "./metadata-scope.js";
7070import { policyContext } from "./policy-context.js" ;
7171import {
7272 buildSelectColumnInfos ,
73- buildStructuredSelect ,
74- getBuildColumn
73+ buildStructuredSelect
7574} from "./query-builder.js" ;
7675import { registerMcpResources } from "./resources.js" ;
7776import {
@@ -121,6 +120,7 @@ import { registerQuerySessionTools } from "./tools/query-sessions.js";
121120import { registerQueryTemplateTools } from "./tools/query-templates.js" ;
122121import { registerRecentRowsTools } from "./tools/recent-rows.js" ;
123122import { registerSearchCatalogTools } from "./tools/search-catalog.js" ;
123+ import { registerDataQualityTools } from "./tools/data-quality.js" ;
124124import { registerProfileColumnTools } from "./tools/profile-columns.js" ;
125125import { registerValueDistributionTools } from "./tools/profile-value-distribution.js" ;
126126import {
@@ -324,87 +324,7 @@ export function createObMcpServer(
324324
325325 registerValueDistributionTools ( registerTool , runtime , context ) ;
326326
327- registerTool (
328- "ob_check_data_quality" ,
329- {
330- title : "Check Data Quality" ,
331- description : "Run bounded read-only data quality checks for nulls, duplicate key risk, dimensions, and time columns." ,
332- inputSchema : {
333- ...profileInput ,
334- table : z . string ( ) . min ( 1 ) ,
335- schema : z . string ( ) . optional ( ) ,
336- columns : z . array ( z . string ( ) . min ( 1 ) ) . max ( 50 ) . optional ( ) ,
337- rowLimit : z . number ( ) . int ( ) . positive ( ) . max ( 10_000 ) . optional ( ) ,
338- maxColumns : z . number ( ) . int ( ) . positive ( ) . max ( 50 ) . optional ( ) ,
339- nullRatioThreshold : z . number ( ) . min ( 0 ) . max ( 1 ) . optional ( )
340- }
341- } ,
342- async ( { profile, table, schema, columns, rowLimit, maxColumns, nullRatioThreshold } ) => {
343- const selected = selectAuthorizedProfile ( runtime , profile , context ) ;
344- const effectiveSchema = metadataSchema ( selected . config , schema ) ;
345- assertTableAccess ( effectiveSchema , table , policyContext ( selected . config ) ) ;
346- const metadata = await withQuerySlot ( selected , ( ) =>
347- getTableDdlMetadata ( selected . pool , selected . config . compatibility , table , effectiveSchema , selected . config . queryTimeoutMs )
348- ) ;
349- const visibleColumns = filterColumnMetadata ( metadata . columns , selected . config . policy ) ;
350- const visibleConstraints = filterColumnMetadata ( metadata . constraints , selected . config . policy ) ;
351- const compactColumns = compactSchemaColumns ( visibleColumns , visibleConstraints , selected . config , metadata . schema , metadata . table ) ;
352- const primaryKeys = schemaMapPrimaryKeyColumns ( visibleConstraints ) ;
353- const selectedColumns = selectDataQualityColumns ( selected . config , compactColumns , primaryKeys , columns , maxColumns ?? 12 ) ;
354- const profileRowLimit = Math . min ( effectiveRowLimit ( rowLimit , selected . config ) , 10_000 ) ;
355- const result = selectedColumns . length > 0
356- ? await withQuerySlot ( selected , ( ) =>
357- profileColumns (
358- selected . pool ,
359- selected . config . compatibility ,
360- metadata . schema ,
361- metadata . table ,
362- selectedColumns . map ( ( column ) => ( { name : column . name , dataType : column . type } ) ) ,
363- profileRowLimit ,
364- 5 ,
365- 1 ,
366- selected . config . queryTimeoutMs
367- )
368- )
369- : undefined ;
370- const columnProfiles = result
371- ? tableProfileMaskedColumns ( result , compactColumns , selected . config , metadata . schema , metadata . table )
372- : [ ] ;
373- const checks = buildDataQualityChecks (
374- columnProfiles ,
375- selectedColumns ,
376- primaryKeys ,
377- nullRatioThreshold ?? 0
378- ) ;
379-
380- return toLimitedResult (
381- {
382- mode : selected . config . compatibility ,
383- schema : metadata . schema ,
384- table : metadata . table ,
385- primaryKeys,
386- profileScope : {
387- rowLimit : profileRowLimit ,
388- selectedColumns : selectedColumns . map ( ( column ) => column . name ) ,
389- visibleColumnCount : compactColumns . length ,
390- selectedColumnCount : selectedColumns . length
391- } ,
392- checks,
393- summary : summarizeDataQualityChecks ( checks ) ,
394- columnProfiles,
395- warnings : uniqueStrings ( [
396- ...metadata . warnings ,
397- ...( result ?. warnings ?? [ ] ) ,
398- "Data quality checks use bounded samples; counts and ratios are not full-table exact values." ,
399- ...( compactColumns . length > selectedColumns . length
400- ? [ `Only ${ selectedColumns . length } of ${ compactColumns . length } visible columns were checked. Pass columns or maxColumns to change scope.` ]
401- : [ ] )
402- ] )
403- } ,
404- selected . config
405- ) ;
406- }
407- ) ;
327+ registerDataQualityTools ( registerTool , runtime , context ) ;
408328
409329 registerSqlPrepareTools ( registerTool , runtime , context ) ;
410330
@@ -2103,104 +2023,6 @@ function tableRefKey(mode: ObConfig["compatibility"], schema: string | undefined
21032023 return `${ schema ? normalizeTableName ( mode , schema ) : "" } .${ normalizeTableName ( mode , table ) } ` ;
21042024}
21052025
2106- function selectDataQualityColumns (
2107- config : ObConfig ,
2108- columns : Array < { name : string ; type ?: string ; nullable ?: boolean ; roles : string [ ] ; masked : boolean } > ,
2109- primaryKeys : string [ ] ,
2110- requestedColumns : string [ ] | undefined ,
2111- maxColumns : number
2112- ) {
2113- if ( requestedColumns && requestedColumns . length > 0 ) {
2114- const available = columns . map ( ( column ) => ( {
2115- name : column . name ,
2116- type : column . type ,
2117- nullable : column . nullable ,
2118- masked : column . masked
2119- } ) ) ;
2120- return requestedColumns . map ( ( column ) => {
2121- const match = getBuildColumn ( config , available , column ) ;
2122- const full = columns . find ( ( item ) => normalizeTableName ( config . compatibility , item . name ) === normalizeTableName ( config . compatibility , match . name ) ) ;
2123- return full ?? { ...match , roles : [ ] } ;
2124- } ) ;
2125- }
2126- return selectTableProfileColumns ( columns , primaryKeys , maxColumns ) ;
2127- }
2128-
2129- function buildDataQualityChecks (
2130- profiles : Array < Record < string , unknown > > ,
2131- metadataColumns : Array < { name : string ; type ?: string ; nullable ?: boolean ; roles : string [ ] ; masked : boolean } > ,
2132- primaryKeys : string [ ] ,
2133- nullRatioThreshold : number
2134- ) {
2135- const checks : Array < Record < string , unknown > > = [ ] ;
2136- const primaryKeySet = new Set ( primaryKeys . map ( ( column ) => column . toLowerCase ( ) ) ) ;
2137- for ( const profile of profiles ) {
2138- const column = stringRecordValue ( profile , "column" , "COLUMN" ) ?? "" ;
2139- if ( ! column ) {
2140- continue ;
2141- }
2142- const metadata = metadataColumns . find ( ( item ) => item . name . toLowerCase ( ) === column . toLowerCase ( ) ) ;
2143- const sampledRows = numberRecordValue ( profile , "sampledRows" , "sampled_rows" ) ?? 0 ;
2144- const nulls = profile . nulls && typeof profile . nulls === "object" && ! Array . isArray ( profile . nulls )
2145- ? profile . nulls as Record < string , unknown >
2146- : { } ;
2147- const nullCount = numberRecordValue ( nulls , "count" ) ?? 0 ;
2148- const nullRatio = numberRecordValue ( nulls , "ratio" ) ?? 0 ;
2149- if ( nullCount > 0 && nullRatio >= nullRatioThreshold ) {
2150- checks . push ( {
2151- rule : "null_values" ,
2152- severity : nullRatio >= 0.5 ? "high" : "warn" ,
2153- column,
2154- evidence : [ `${ nullCount } of ${ sampledRows } sampled rows are null.` ] ,
2155- ratio : nullRatio ,
2156- recommendation : "Confirm whether nulls are valid for this field before using it as a required filter or join key."
2157- } ) ;
2158- }
2159- const distinct = profile . distinct && typeof profile . distinct === "object" && ! Array . isArray ( profile . distinct )
2160- ? numberRecordValue ( profile . distinct as Record < string , unknown > , "value" )
2161- : undefined ;
2162- if ( primaryKeySet . has ( column . toLowerCase ( ) ) && distinct != null && sampledRows > 0 && distinct < sampledRows - nullCount ) {
2163- checks . push ( {
2164- rule : "duplicate_key_risk" ,
2165- severity : "high" ,
2166- column,
2167- evidence : [ `Primary-key-like column has ${ distinct } distinct values in ${ sampledRows } sampled rows.` ] ,
2168- recommendation : "Review key uniqueness with a scoped query before relying on this column as a unique identifier."
2169- } ) ;
2170- }
2171- if ( metadata ?. roles . includes ( "status" ) && distinct != null && sampledRows > 0 && distinct / sampledRows > 0.8 ) {
2172- checks . push ( {
2173- rule : "high_cardinality_dimension" ,
2174- severity : "warn" ,
2175- column,
2176- evidence : [ `Dimension-like column has ${ distinct } distinct values in ${ sampledRows } sampled rows.` ] ,
2177- recommendation : "Check whether this field is suitable for grouping, or use a different business dimension."
2178- } ) ;
2179- }
2180- if ( metadata ?. roles . includes ( "time" ) && nullRatio > 0.1 ) {
2181- checks . push ( {
2182- rule : "time_column_nulls" ,
2183- severity : "warn" ,
2184- column,
2185- evidence : [ `Time-like column null ratio is ${ nullRatio } .` ] ,
2186- recommendation : "Pass an explicit timeColumn when querying recent rows if this field is incomplete."
2187- } ) ;
2188- }
2189- }
2190- return checks ;
2191- }
2192-
2193- function summarizeDataQualityChecks ( checks : Array < Record < string , unknown > > ) {
2194- const high = checks . filter ( ( check ) => check . severity === "high" ) . length ;
2195- const warn = checks . filter ( ( check ) => check . severity === "warn" ) . length ;
2196- return {
2197- high,
2198- warn,
2199- total : checks . length ,
2200- status : high > 0 ? "attention_required" : warn > 0 ? "review" : "no_sampled_issues"
2201- } ;
2202- }
2203-
22042026function uniqueTableRefs ( refs : Array < { schema ?: string ; table : string } > ) : Array < { schema ?: string ; table : string } > {
22052027 const output : Array < { schema ?: string ; table : string } > = [ ] ;
22062028 const seen = new Set < string > ( ) ;
0 commit comments