Skip to content

Commit 6c3df2d

Browse files
committed
refactor: move sql rewrite tool
1 parent 8aa10ce commit 6c3df2d

2 files changed

Lines changed: 212 additions & 163 deletions

File tree

src/mcp/server.ts

Lines changed: 5 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -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";
5353
import { ensureQuerySessionStore } from "../query/sessions.js";
5454
import { generateQueryTemplates, type QueryTemplateScenario } from "../query-generation.js";
5555
import { selectRuntimeProfile, withQuerySlot, type GatewayRuntime } from "../runtime.js";
56-
import { assertReadOnlySql, assertSqlInputLimits, buildPagedReadOnlySql } from "../sql.js";
5756
import { extractSqlSnippetsFromContent, type SqlScanSnippet } from "../sql-scan.js";
5857
import {
5958
gatewayLimitConfig,
@@ -76,9 +75,7 @@ import { policyContext } from "./policy-context.js";
7675
import {
7776
buildSelectColumnInfos,
7877
buildStructuredSelect,
79-
getBuildColumn,
80-
selectBuildColumns,
81-
sqlQuoteIdentifier
78+
getBuildColumn
8279
} from "./query-builder.js";
8380
import { registerMcpResources } from "./resources.js";
8481
import {
@@ -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";
9491
import {
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";
149147
import { registerSqlUsageTools } from "./tools/sql-usages.js";
150148
import { registerTableContextTools } from "./tools/table-context.js";
151149
import {
@@ -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(/\bselect\s+((?:(?:all|distinct|distinctrow|unique)\s+)*)\*\s+\bfrom\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 /\bselect\s+(?:(?:all|distinct|distinctrow|unique)\s+)*\*\s+\bfrom\b/i.test(sql);
3356-
}
3357-
3358-
function trimRewriteSql(sql: string): string {
3359-
return sql.trim().replace(/;+$/, "").trimEnd();
3360-
}
3361-
33623204
interface SnippetMetadataValidation {
33633205
tables: Array<{
33643206
schema?: string;

0 commit comments

Comments
 (0)