@@ -98,7 +98,6 @@ import {
9898} from "./search-results.js" ;
9999import { explainGeneratedQuery , runExplainCheck } from "./sql-policy-explain.js" ;
100100import {
101- compatibilityModeSchema ,
102101 entityDiagramOutputFormatSchema ,
103102 erDocumentFormatSchema ,
104103 erExportStrategySchema ,
@@ -145,6 +144,7 @@ import { registerQuerySessionTools } from "./tools/query-sessions.js";
145144import { registerQueryTemplateTools } from "./tools/query-templates.js" ;
146145import { registerRecentRowsTools } from "./tools/recent-rows.js" ;
147146import { registerSearchCatalogTools } from "./tools/search-catalog.js" ;
147+ import { registerSqlConversionTools } from "./tools/sql-conversion.js" ;
148148import { registerSqlUsageTools } from "./tools/sql-usages.js" ;
149149import { registerTableContextTools } from "./tools/table-context.js" ;
150150import {
@@ -222,6 +222,7 @@ export function createObMcpServer(
222222 registerQueryTemplateTools ( registerTool , runtime , context ) ;
223223 registerRecentRowsTools ( registerTool , runtime , context ) ;
224224 registerSearchCatalogTools ( registerTool , runtime , context ) ;
225+ registerSqlConversionTools ( registerTool , runtime , context ) ;
225226 registerSqlUsageTools ( registerTool , runtime , context ) ;
226227
227228 registerObjectMetadataTools ( registerTool , runtime , context ) ;
@@ -670,25 +671,6 @@ export function createObMcpServer(
670671 }
671672 ) ;
672673
673- registerTool (
674- "ob_convert_sql_dialect" ,
675- {
676- title : "Convert SQL Dialect" ,
677- description : "Convert read-only SQL between OceanBase MySQL and Oracle modes with documented limitations." ,
678- inputSchema : {
679- ...profileInput ,
680- sql : z . string ( ) . min ( 1 ) ,
681- targetMode : compatibilityModeSchema ,
682- sourceMode : compatibilityModeSchema . optional ( )
683- }
684- } ,
685- async ( { profile, sql, targetMode, sourceMode } ) => {
686- const selected = selectAuthorizedProfile ( runtime , profile , context ) ;
687- const converted = convertSqlDialect ( selected . config , sql , sourceMode ?? selected . config . compatibility , targetMode ) ;
688- return toLimitedResult ( converted , selected . config ) ;
689- }
690- ) ;
691-
692674 registerTool (
693675 "ob_rewrite_sql" ,
694676 {
@@ -3409,114 +3391,6 @@ function fallbackErrorSearchTerm(errorMessage: string): string | undefined {
34093391 return / [ ' " ` ] ( [ A - Z a - z _ ] [ \w $ ] * ) [ ' " ` ] / . exec ( errorMessage ) ?. [ 1 ] ;
34103392}
34113393
3412- function convertSqlDialect (
3413- config : ObConfig ,
3414- sql : string ,
3415- sourceMode : ObConfig [ "compatibility" ] ,
3416- targetMode : ObConfig [ "compatibility" ]
3417- ) {
3418- const checks = [
3419- runExplainCheck ( "input_limits" , ( ) => assertSqlInputLimits ( sql , [ ] , config ) ) ,
3420- runExplainCheck ( "read_only_sql" , ( ) => assertReadOnlySql ( sql ) )
3421- ] ;
3422- if ( ! checks . every ( ( check ) => check . ok ) ) {
3423- return {
3424- sql,
3425- convertedSql : null ,
3426- sourceMode,
3427- targetMode,
3428- changed : false ,
3429- checks,
3430- warnings : [ "SQL was not converted because it did not pass read-only input checks." ] ,
3431- limitations : [ ]
3432- } ;
3433- }
3434- if ( sourceMode === targetMode ) {
3435- return {
3436- sql,
3437- convertedSql : trimRewriteSql ( sql ) ,
3438- sourceMode,
3439- targetMode,
3440- changed : false ,
3441- checks,
3442- policy : explainSqlPolicy ( sql , policyContext ( config ) ) ,
3443- warnings : [ "Source and target modes are the same; SQL was normalized only." ] ,
3444- limitations : [ ]
3445- } ;
3446- }
3447-
3448- const warnings = [
3449- "Review converted SQL before use; function behavior and date arithmetic may not be fully equivalent."
3450- ] ;
3451- const limitations : string [ ] = [ ] ;
3452- let convertedSql = trimRewriteSql ( sql ) ;
3453- if ( sourceMode === "mysql" && targetMode === "oracle" ) {
3454- convertedSql = convertedSql . replace ( / ` ( [ ^ ` ] + ) ` / g, ( _match , name : string ) => `"${ name . replace ( / " / g, "\"\"" ) . toUpperCase ( ) } "` ) ;
3455- convertedSql = convertedSql . replace ( / \b I F N U L L \s * \( / gi, "NVL(" ) ;
3456- convertedSql = convertedSql . replace ( / \b N O W \s * \( \s * \) / gi, "SYSTIMESTAMP" ) ;
3457- convertedSql = convertedSql . replace ( / \b C U R D A T E \s * \( \s * \) / gi, "TRUNC(SYSDATE)" ) ;
3458- convertedSql = convertLimitToOracle ( convertedSql ) ;
3459- if ( / \b d a t e _ f o r m a t \s * \( / i. test ( sql ) ) {
3460- limitations . push ( "DATE_FORMAT was not converted; rewrite it manually with TO_CHAR." ) ;
3461- }
3462- } else {
3463- convertedSql = convertedSql . replace ( / " ( [ A - Z a - z _ ] [ \w $ ] * ) " / g, ( _match , name : string ) => `\`${ name } \`` ) ;
3464- convertedSql = convertedSql . replace ( / \b N V L \s * \( / gi, "IFNULL(" ) ;
3465- convertedSql = convertedSql . replace ( / \b S Y S T I M E S T A M P \b / gi, "NOW()" ) ;
3466- convertedSql = convertedSql . replace ( / \b S Y S D A T E \b / gi, "NOW()" ) ;
3467- convertedSql = convertFetchToMysql ( convertedSql ) ;
3468- convertedSql = convertSimpleRownumToMysql ( convertedSql , limitations ) ;
3469- if ( / \b t o _ c h a r \s * \( / i. test ( sql ) ) {
3470- limitations . push ( "TO_CHAR was not converted; rewrite it manually with DATE_FORMAT if needed." ) ;
3471- }
3472- }
3473- const outputChecks = [
3474- runExplainCheck ( "converted_read_only_sql" , ( ) => assertReadOnlySql ( convertedSql ) )
3475- ] ;
3476- const policy = explainSqlPolicy ( convertedSql , policyContext ( config ) ) ;
3477- return {
3478- sql,
3479- convertedSql,
3480- sourceMode,
3481- targetMode,
3482- changed : convertedSql !== trimRewriteSql ( sql ) ,
3483- checks : [ ...checks , ...outputChecks ] ,
3484- policy,
3485- allowed : outputChecks . every ( ( check ) => check . ok ) && policy . allowed ,
3486- warnings,
3487- limitations,
3488- executed : false
3489- } ;
3490- }
3491-
3492- function convertLimitToOracle ( sql : string ) : string {
3493- let output = sql . replace ( / \s + l i m i t \s + ( \d + ) \s + o f f s e t \s + ( \d + ) \s * $ / i, ( _match , limit : string , offset : string ) =>
3494- ` OFFSET ${ offset } ROWS FETCH NEXT ${ limit } ROWS ONLY`
3495- ) ;
3496- output = output . replace ( / \s + l i m i t \s + ( \d + ) \s * $ / i, ( _match , limit : string ) =>
3497- ` FETCH FIRST ${ limit } ROWS ONLY`
3498- ) ;
3499- return output ;
3500- }
3501-
3502- function convertFetchToMysql ( sql : string ) : string {
3503- let output = sql . replace ( / \s + o f f s e t \s + ( \d + ) \s + r o w s \s + f e t c h \s + n e x t \s + ( \d + ) \s + r o w s \s + o n l y \s * $ / i, ( _match , offset : string , limit : string ) =>
3504- ` LIMIT ${ limit } OFFSET ${ offset } `
3505- ) ;
3506- output = output . replace ( / \s + f e t c h \s + f i r s t \s + ( \d + ) \s + r o w s \s + o n l y \s * $ / i, ( _match , limit : string ) =>
3507- ` LIMIT ${ limit } `
3508- ) ;
3509- return output ;
3510- }
3511-
3512- function convertSimpleRownumToMysql ( sql : string , limitations : string [ ] ) : string {
3513- const converted = sql . replace ( / \s + w h e r e \s + r o w n u m \s * < = \s * ( \d + ) \s * $ / i, ( _match , limit : string ) => ` LIMIT ${ limit } ` ) ;
3514- if ( converted === sql && / \b r o w n u m \b / i. test ( sql ) ) {
3515- limitations . push ( "ROWNUM predicates were not fully converted; use LIMIT/OFFSET manually." ) ;
3516- }
3517- return converted ;
3518- }
3519-
35203394async function rewriteReadOnlySql (
35213395 selected : ReturnType < typeof selectRuntimeProfile > ,
35223396 sql : string ,
0 commit comments