@@ -49,11 +49,10 @@ import {
4949 pagedQueryWarnings ,
5050 shouldMaskColumnValue
5151} from "../query/output.js" ;
52- import { hasSqlRowLimit , queryPreviewWarnings , queryShape } from "../query/preview.js" ;
52+ import { queryPreviewWarnings , queryShape } from "../query/preview.js" ;
5353import { ensureQuerySessionStore } from "../query/sessions.js" ;
5454import { generateQueryTemplates , type QueryTemplateScenario } from "../query-generation.js" ;
5555import { selectRuntimeProfile , withQuerySlot , type GatewayRuntime } from "../runtime.js" ;
56- import { assertReadOnlySql , assertSqlInputLimits , buildPagedReadOnlySql } from "../sql.js" ;
5756import { extractSqlSnippetsFromContent , type SqlScanSnippet } from "../sql-scan.js" ;
5857import {
5958 gatewayLimitConfig ,
@@ -76,9 +75,7 @@ import { policyContext } from "./policy-context.js";
7675import {
7776 buildSelectColumnInfos ,
7877 buildStructuredSelect ,
79- getBuildColumn ,
80- selectBuildColumns ,
81- sqlQuoteIdentifier
78+ getBuildColumn
8279} from "./query-builder.js" ;
8380import { registerMcpResources } from "./resources.js" ;
8481import {
@@ -90,7 +87,7 @@ import {
9087 type BusinessTableCandidate ,
9188 type SearchEverythingResult
9289} from "./search-results.js" ;
93- import { explainGeneratedQuery , runExplainCheck } from "./sql-policy-explain.js" ;
90+ import { explainGeneratedQuery } from "./sql-policy-explain.js" ;
9491import {
9592 entityDiagramOutputFormatSchema ,
9693 erDocumentFormatSchema ,
@@ -146,6 +143,7 @@ import {
146143 registerSqlLintTools ,
147144 type LintFinding
148145} from "./tools/sql-lint.js" ;
146+ import { registerSqlRewriteTools } from "./tools/sql-rewrite.js" ;
149147import { registerSqlUsageTools } from "./tools/sql-usages.js" ;
150148import { registerTableContextTools } from "./tools/table-context.js" ;
151149import {
@@ -226,6 +224,7 @@ export function createObMcpServer(
226224 registerSqlConversionTools ( registerTool , runtime , context ) ;
227225 registerSqlErrorTools ( registerTool , runtime , context ) ;
228226 registerSqlLintTools ( registerTool , runtime , context ) ;
227+ registerSqlRewriteTools ( registerTool , runtime , context ) ;
229228 registerSqlUsageTools ( registerTool , runtime , context ) ;
230229
231230 registerObjectMetadataTools ( registerTool , runtime , context ) ;
@@ -644,34 +643,6 @@ export function createObMcpServer(
644643 }
645644 ) ;
646645
647- registerTool (
648- "ob_rewrite_sql" ,
649- {
650- title : "Rewrite SQL" ,
651- description : "Rewrite SELECT/WITH SQL into a safer read-only form without executing it." ,
652- inputSchema : {
653- ...profileInput ,
654- sql : z . string ( ) . min ( 1 ) ,
655- params : z . array ( sqlParamSchema ) . optional ( ) . default ( [ ] ) ,
656- maxRows : z . number ( ) . int ( ) . positive ( ) . max ( 10_000 ) . optional ( ) ,
657- orderBy : z . string ( ) . optional ( ) ,
658- maxColumns : z . number ( ) . int ( ) . positive ( ) . max ( 50 ) . optional ( )
659- }
660- } ,
661- async ( { profile, sql, params = [ ] , maxRows, orderBy, maxColumns } ) => {
662- const selected = selectAuthorizedProfile ( runtime , profile , context ) ;
663- const rewritten = await rewriteReadOnlySql (
664- selected ,
665- sql ,
666- params as SqlParam [ ] ,
667- effectiveRowLimit ( maxRows , selected . config ) ,
668- orderBy ,
669- maxColumns ?? 12
670- ) ;
671- return toLimitedResult ( rewritten , selected . config ) ;
672- }
673- ) ;
674-
675646 registerTool (
676647 "ob_validate_sql_snippet" ,
677648 {
@@ -3230,135 +3201,6 @@ function uniqueTableRefs(refs: Array<{ schema?: string; table: string }>): Array
32303201 return output ;
32313202}
32323203
3233- async function rewriteReadOnlySql (
3234- selected : ReturnType < typeof selectRuntimeProfile > ,
3235- sql : string ,
3236- params : SqlParam [ ] ,
3237- maxRows : number ,
3238- orderBy : string | undefined ,
3239- maxColumns : number
3240- ) {
3241- const checks = [
3242- runExplainCheck ( "input_limits" , ( ) => assertSqlInputLimits ( sql , params , selected . config ) ) ,
3243- runExplainCheck ( "read_only_sql" , ( ) => assertReadOnlySql ( sql ) )
3244- ] ;
3245- const originalPolicy = explainSqlPolicy ( sql , policyContext ( selected . config ) ) ;
3246- if ( ! checks . every ( ( check ) => check . ok ) ) {
3247- return {
3248- sql,
3249- rewrittenSql : null ,
3250- changed : false ,
3251- allowed : false ,
3252- executed : false ,
3253- checks,
3254- originalPolicy,
3255- rewrittenPolicy : originalPolicy ,
3256- changes : [ ] ,
3257- warnings : [ "SQL was not rewritten because it did not pass read-only input checks." ]
3258- } ;
3259- }
3260-
3261- const warnings : string [ ] = [ ] ;
3262- const changes = [ ] ;
3263- let rewrittenSql = trimRewriteSql ( sql ) ;
3264- if ( hasSelectStar ( rewrittenSql ) ) {
3265- const replacement = await rewriteSelectStar ( selected , rewrittenSql , originalPolicy . tableRefs , maxColumns , warnings ) ;
3266- if ( replacement . changed ) {
3267- rewrittenSql = replacement . sql ;
3268- changes . push ( {
3269- rule : "replace_select_star" ,
3270- reason : "Replaced SELECT * with visible columns from metadata." ,
3271- evidence : replacement . columns
3272- } ) ;
3273- }
3274- }
3275- if ( ! hasSqlRowLimit ( rewrittenSql , selected . config . compatibility ) ) {
3276- rewrittenSql = buildPagedReadOnlySql ( rewrittenSql , selected . config . compatibility , {
3277- page : 1 ,
3278- pageSize : maxRows ,
3279- fetchRows : maxRows ,
3280- orderBy
3281- } ) ;
3282- changes . push ( {
3283- rule : "add_pagination" ,
3284- reason : "Added a dialect-correct row cap for safer inspection." ,
3285- evidence : [ `maxRows:${ maxRows } ` ]
3286- } ) ;
3287- }
3288-
3289- const rewrittenPolicy = explainSqlPolicy ( rewrittenSql , policyContext ( selected . config ) ) ;
3290- const outputChecks = [
3291- runExplainCheck ( "rewritten_input_limits" , ( ) => assertSqlInputLimits ( rewrittenSql , params , selected . config ) ) ,
3292- runExplainCheck ( "rewritten_read_only_sql" , ( ) => assertReadOnlySql ( rewrittenSql ) )
3293- ] ;
3294- return {
3295- sql,
3296- rewrittenSql,
3297- changed : rewrittenSql !== trimRewriteSql ( sql ) ,
3298- allowed : outputChecks . every ( ( check ) => check . ok ) && rewrittenPolicy . allowed ,
3299- executed : false ,
3300- checks : [ ...checks , ...outputChecks ] ,
3301- originalPolicy,
3302- rewrittenPolicy,
3303- changes,
3304- warnings
3305- } ;
3306- }
3307-
3308- async function rewriteSelectStar (
3309- selected : ReturnType < typeof selectRuntimeProfile > ,
3310- sql : string ,
3311- tableRefs : Array < { schema ?: string ; table : string ; raw : string } > ,
3312- maxColumns : number ,
3313- warnings : string [ ]
3314- ) : Promise < { changed : boolean ; sql : string ; columns : string [ ] } > {
3315- const refs = uniqueTableRefs ( tableRefs ) ;
3316- if ( refs . length !== 1 ) {
3317- warnings . push ( "SELECT * was not replaced because the query does not have exactly one visible table reference." ) ;
3318- return { changed : false , sql, columns : [ ] } ;
3319- }
3320- if ( ! canRewriteDirectSelectStar ( sql ) ) {
3321- warnings . push ( "SELECT * was not replaced because the projection is not a simple single-table SELECT * form." ) ;
3322- return { changed : false , sql, columns : [ ] } ;
3323- }
3324- const ref = refs [ 0 ] ;
3325- const schema = metadataSchema ( selected . config , ref . schema ) ;
3326- const table = selected . config . compatibility === "oracle" ? ref . table . trim ( ) . toUpperCase ( ) : ref . table ;
3327- assertTableAccess ( schema , table , policyContext ( selected . config ) ) ;
3328- const rows = filterColumnMetadata (
3329- await withQuerySlot ( selected , ( ) =>
3330- describeTable ( selected . pool , selected . config . compatibility , table , schema , selected . config . queryTimeoutMs )
3331- ) ,
3332- selected . config . policy
3333- ) ;
3334- const columns = selectBuildColumns (
3335- selected . config ,
3336- buildSelectColumnInfos ( selected . config , schema , table , rows ) ,
3337- undefined ,
3338- warnings
3339- ) . slice ( 0 , Math . max ( 1 , Math . min ( maxColumns , 50 ) ) ) ;
3340- if ( columns . length === 0 ) {
3341- warnings . push ( "SELECT * was not replaced because no visible columns were available." ) ;
3342- return { changed : false , sql, columns : [ ] } ;
3343- }
3344- const selectList = columns . map ( ( column ) => sqlQuoteIdentifier ( selected . config . compatibility , column . name ) ) . join ( ", " ) ;
3345- return {
3346- changed : true ,
3347- sql : sql . replace ( / \b s e l e c t \s + ( (?: (?: a l l | d i s t i n c t | d i s t i n c t r o w | u n i q u e ) \s + ) * ) \* \s + \b f r o m \b / i, ( _match , modifier : string ) =>
3348- `SELECT ${ modifier ?? "" } ${ selectList } FROM`
3349- ) ,
3350- columns : columns . map ( ( column ) => column . name )
3351- } ;
3352- }
3353-
3354- function canRewriteDirectSelectStar ( sql : string ) : boolean {
3355- return / \b s e l e c t \s + (?: (?: a l l | d i s t i n c t | d i s t i n c t r o w | u n i q u e ) \s + ) * \* \s + \b f r o m \b / i. test ( sql ) ;
3356- }
3357-
3358- function trimRewriteSql ( sql : string ) : string {
3359- return sql . trim ( ) . replace ( / ; + $ / , "" ) . trimEnd ( ) ;
3360- }
3361-
33623204interface SnippetMetadataValidation {
33633205 tables : Array < {
33643206 schema ?: string ;
0 commit comments