Skip to content

Commit f57b296

Browse files
committed
refactor: move recent rows tool
1 parent 1acbf05 commit f57b296

2 files changed

Lines changed: 345 additions & 220 deletions

File tree

src/mcp/server.ts

Lines changed: 2 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ import { registerQueryExecutionTools } from "./tools/query-execution.js";
143143
import { registerQueryPlanTools } from "./tools/query-plan.js";
144144
import { registerQuerySessionTools } from "./tools/query-sessions.js";
145145
import { registerQueryTemplateTools } from "./tools/query-templates.js";
146+
import { registerRecentRowsTools } from "./tools/recent-rows.js";
146147
import { registerSearchCatalogTools } from "./tools/search-catalog.js";
147148
import { registerSqlUsageTools } from "./tools/sql-usages.js";
148149
import { registerTableContextTools } from "./tools/table-context.js";
@@ -219,6 +220,7 @@ export function createObMcpServer(
219220
registerQueryPlanTools(registerTool, runtime, context);
220221
registerQueryBuildTools(registerTool, runtime, context);
221222
registerQueryTemplateTools(registerTool, runtime, context);
223+
registerRecentRowsTools(registerTool, runtime, context);
222224
registerSearchCatalogTools(registerTool, runtime, context);
223225
registerSqlUsageTools(registerTool, runtime, context);
224226

@@ -491,86 +493,6 @@ export function createObMcpServer(
491493
}
492494
);
493495

494-
registerTool(
495-
"ob_find_recent_rows",
496-
{
497-
title: "Find Recent Rows",
498-
description: "Read recent rows from an allowed table using an explicit or metadata-detected time column with safe pagination.",
499-
inputSchema: {
500-
...profileInput,
501-
table: z.string().min(1),
502-
schema: z.string().optional(),
503-
timeColumn: z.string().min(1).optional(),
504-
columns: z.array(z.string().min(1)).max(100).optional(),
505-
page: z.number().int().positive().optional().default(1),
506-
pageSize: z.number().int().positive().max(1_000).optional()
507-
}
508-
},
509-
async ({ profile, table, schema, timeColumn, columns, page = 1, pageSize }) => {
510-
const selected = selectAuthorizedProfile(runtime, profile, context);
511-
const effectiveSchema = metadataSchema(selected.config, schema);
512-
assertTableAccess(effectiveSchema, table, policyContext(selected.config));
513-
const metadataRows = await withQuerySlot(selected, () =>
514-
describeTable(selected.pool, selected.config.compatibility, table, effectiveSchema, selected.config.queryTimeoutMs)
515-
);
516-
const visibleColumns = filterColumnMetadata(metadataRows, selected.config.policy);
517-
const resolvedTime = resolveRecentRowsTimeColumn(visibleColumns, selected.config, effectiveSchema, table, timeColumn);
518-
const requestedColumns = recentRowsProjection(columns, resolvedTime.column);
519-
const built = buildStructuredSelect(
520-
selected.config,
521-
effectiveSchema,
522-
table,
523-
visibleColumns,
524-
{
525-
columns: requestedColumns,
526-
filters: [],
527-
orderBy: [{ column: resolvedTime.column, direction: "desc" }],
528-
pageSize,
529-
page
530-
}
531-
);
532-
const sqlParams = built.params as SqlParam[];
533-
assertSqlInputLimits(built.sql, sqlParams, selected.config);
534-
assertSqlPolicy(built.sql, policyContext(selected.config));
535-
const output = await withQuerySlot(selected, () =>
536-
runReadOnlyQuery(
537-
selected.pool,
538-
selected.config.compatibility,
539-
built.sql,
540-
sqlParams,
541-
built.pagination.pageSize,
542-
selected.config.queryTimeoutMs,
543-
selected.config.enforceReadOnlyTransaction,
544-
selected.config.maxResultBytes
545-
)
546-
);
547-
assertOutputAllowed(output, selected.config.policy);
548-
const visibleOutput = enrichQueryOutput(
549-
output,
550-
selected.config,
551-
recentRowsWarnings(built.warnings, resolvedTime, output.truncated)
552-
);
553-
const result = {
554-
mode: selected.config.compatibility,
555-
schema: effectiveSchema,
556-
table,
557-
timeColumn: resolvedTime,
558-
sql: built.sql,
559-
paramCount: sqlParams.length,
560-
page: built.pagination.page,
561-
pageSize: built.pagination.pageSize,
562-
columns: visibleOutput.columns,
563-
rows: visibleOutput.rows,
564-
markdownPreview: visibleOutput.markdownPreview,
565-
truncated: visibleOutput.truncated,
566-
warnings: visibleOutput.warnings,
567-
executed: true
568-
};
569-
assertJsonByteLimit(result, selected.config.maxResultBytes);
570-
return toResult(result);
571-
}
572-
);
573-
574496
registerTool(
575497
"ob_check_data_quality",
576498
{
@@ -2784,146 +2706,6 @@ function valueDistributionWarnings(
27842706
return [...warnings];
27852707
}
27862708

2787-
function detectTimeColumnCandidates(
2788-
rows: unknown[],
2789-
config: ObConfig,
2790-
schema: string | undefined,
2791-
table: string,
2792-
maxColumns: number
2793-
) {
2794-
return rows
2795-
.map((row, index) => {
2796-
const metadata = profileColumnMetadata(row, config.compatibility);
2797-
if (!metadata) {
2798-
return undefined;
2799-
}
2800-
const signal = timeColumnSignal(metadata.name, metadata.dataType, metadata.comment);
2801-
if (signal.confidence < 0.35) {
2802-
return undefined;
2803-
}
2804-
return {
2805-
column: metadata.name,
2806-
kind: signal.kind,
2807-
confidence: signal.confidence,
2808-
evidence: signal.evidence,
2809-
dataType: metadata.dataType,
2810-
nullable: metadata.nullable,
2811-
comment: metadata.comment,
2812-
masked: shouldMaskColumnValue(metadata.name, config, schema, table),
2813-
ordinalPosition: numberRecordValue(row as Record<string, unknown>, "ordinal_position", "ORDINAL_POSITION", "column_id", "COLUMN_ID") ?? index + 1
2814-
};
2815-
})
2816-
.filter((item): item is NonNullable<typeof item> => Boolean(item))
2817-
.sort((left, right) => right.confidence - left.confidence || left.ordinalPosition - right.ordinalPosition)
2818-
.slice(0, Math.max(1, Math.min(maxColumns, 50)));
2819-
}
2820-
2821-
function timeColumnSignal(
2822-
columnName: string,
2823-
dataType: string | undefined,
2824-
comment: string | undefined
2825-
): { kind: string; confidence: number; evidence: string[] } {
2826-
const normalized = columnName.trim().toLowerCase();
2827-
const commentText = (comment ?? "").trim().toLowerCase();
2828-
const typeCategory = profileColumnTypeCategory(dataType);
2829-
const evidence: string[] = [];
2830-
let confidence = 0;
2831-
let kind = "generic_time";
2832-
2833-
if (typeCategory === "temporal") {
2834-
confidence += 0.35;
2835-
evidence.push(`Data type "${dataType}" is temporal.`);
2836-
}
2837-
if (/^(created_at|created_time|create_time|gmt_create|insert_time|created)$/.test(normalized)) {
2838-
confidence += 0.5;
2839-
kind = "created_time";
2840-
evidence.push("Column name matches common create-time naming.");
2841-
} else if (/^(updated_at|updated_time|update_time|modified_at|modified_time|gmt_modified|last_update|last_modified)$/.test(normalized)) {
2842-
confidence += 0.5;
2843-
kind = "updated_time";
2844-
evidence.push("Column name matches common update-time naming.");
2845-
} else if (/(event|occur|trade|order|pay|paid|biz|business|effective|start|end).*(time|date|day)$/.test(normalized) || /^(biz_date|business_date)$/.test(normalized)) {
2846-
confidence += 0.45;
2847-
kind = "business_time";
2848-
evidence.push("Column name suggests a business event time.");
2849-
} else if (/(^|_)(time|date|day|timestamp)$/.test(normalized) || /(time|date|timestamp)/.test(normalized)) {
2850-
confidence += 0.2;
2851-
evidence.push("Column name contains a time-related token.");
2852-
}
2853-
if (/(created|create time|updated|update time|modified|event time|business time|occurred|trade time|pay time)/.test(commentText)) {
2854-
confidence += 0.15;
2855-
evidence.push("Column comment contains time-related wording.");
2856-
}
2857-
return {
2858-
kind,
2859-
confidence: Math.min(0.98, Number(confidence.toFixed(2))),
2860-
evidence
2861-
};
2862-
}
2863-
2864-
function resolveRecentRowsTimeColumn(
2865-
rows: unknown[],
2866-
config: ObConfig,
2867-
schema: string | undefined,
2868-
table: string,
2869-
requestedTimeColumn: string | undefined
2870-
) {
2871-
if (requestedTimeColumn) {
2872-
const column = getBuildColumn(config, buildSelectColumnInfos(config, schema, table, rows), requestedTimeColumn);
2873-
const signal = timeColumnSignal(column.name, column.type, column.comment);
2874-
return {
2875-
column: column.name,
2876-
source: "explicit",
2877-
confidence: Math.max(signal.confidence, 0.5),
2878-
kind: signal.kind,
2879-
evidence: signal.evidence.length > 0 ? signal.evidence : ["Time column was supplied explicitly."],
2880-
dataType: column.type,
2881-
masked: column.masked
2882-
};
2883-
}
2884-
const candidates = detectTimeColumnCandidates(rows, config, schema, table, 5);
2885-
const best = candidates[0];
2886-
if (!best || best.confidence < 0.5) {
2887-
throw new Error("No reliable time column was detected. Pass timeColumn explicitly.");
2888-
}
2889-
return {
2890-
column: best.column,
2891-
source: "detected",
2892-
confidence: best.confidence,
2893-
kind: best.kind,
2894-
evidence: best.evidence,
2895-
dataType: best.dataType,
2896-
masked: best.masked,
2897-
alternatives: candidates.slice(1, 5).map((candidate) => ({
2898-
column: candidate.column,
2899-
kind: candidate.kind,
2900-
confidence: candidate.confidence,
2901-
evidence: candidate.evidence
2902-
}))
2903-
};
2904-
}
2905-
2906-
function recentRowsProjection(columns: string[] | undefined, timeColumn: string): string[] | undefined {
2907-
if (!columns || columns.length === 0) {
2908-
return undefined;
2909-
}
2910-
return uniqueStrings([timeColumn, ...columns]);
2911-
}
2912-
2913-
function recentRowsWarnings(baseWarnings: string[], timeColumn: { source: string; alternatives?: unknown[] }, truncated: boolean): string[] {
2914-
const warnings = new Set(baseWarnings);
2915-
if (timeColumn.source === "detected") {
2916-
warnings.add("Time column was selected from metadata detection; pass timeColumn explicitly if this is not the business time field.");
2917-
}
2918-
if (Array.isArray(timeColumn.alternatives) && timeColumn.alternatives.length > 0) {
2919-
warnings.add("Other time-like columns were found; review alternatives before using this query for business decisions.");
2920-
}
2921-
if (truncated) {
2922-
warnings.add("Rows were truncated by the configured row or output limit.");
2923-
}
2924-
return [...warnings];
2925-
}
2926-
29272709
function selectDataQualityColumns(
29282710
config: ObConfig,
29292711
columns: Array<{ name: string; type?: string; nullable?: boolean; roles: string[]; masked: boolean }>,

0 commit comments

Comments
 (0)