- Replace legacy directory structure (api/, db/, functions/, logs/, pooler/) with single docker-compose.yml based self-hosted setup - Add selfhosted-supabase-mcp TypeScript MCP server for database management - Add .dockerignore for Docker build context - Update .gitignore to exclude .env files, volumes/, backups, logs
122 lines
4.5 KiB
TypeScript
122 lines
4.5 KiB
TypeScript
/**
|
|
* get_index_stats - Gets detailed statistics for a specific index.
|
|
*
|
|
* Shows usage counts, size information, and effectiveness metrics.
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import type { ToolContext, ToolPrivilegeLevel } from './types.js';
|
|
import { executeSqlWithFallback, isSqlErrorResponse } from './utils.js';
|
|
|
|
// SQL identifier validation - prevents SQL injection via identifier names
|
|
const identifierPattern = /^[a-zA-Z_][a-zA-Z0-9_$]*$/;
|
|
|
|
// Output schema for index stats
|
|
const GetIndexStatsOutputSchema = z.object({
|
|
schema_name: z.string(),
|
|
table_name: z.string(),
|
|
index_name: z.string(),
|
|
index_type: z.string(),
|
|
is_unique: z.boolean(),
|
|
is_primary: z.boolean(),
|
|
is_valid: z.boolean(),
|
|
number_of_scans: z.number(),
|
|
tuples_read: z.number(),
|
|
tuples_fetched: z.number(),
|
|
index_size: z.string(),
|
|
table_size: z.string(),
|
|
index_size_bytes: z.number(),
|
|
table_size_bytes: z.number(),
|
|
usage_ratio: z.string().nullable(), // Percentage of table accesses that used this index
|
|
});
|
|
|
|
// Input schema
|
|
const GetIndexStatsInputSchema = z.object({
|
|
schema: z.string().default('public').describe('Schema name (defaults to public).'),
|
|
index_name: z.string().describe('Name of the index.'),
|
|
});
|
|
type GetIndexStatsInput = z.infer<typeof GetIndexStatsInputSchema>;
|
|
|
|
// Static JSON Schema for MCP capabilities
|
|
const mcpInputSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
schema: {
|
|
type: 'string',
|
|
default: 'public',
|
|
description: 'Schema name (defaults to public).',
|
|
},
|
|
index_name: {
|
|
type: 'string',
|
|
description: 'Name of the index.',
|
|
},
|
|
},
|
|
required: ['index_name'],
|
|
};
|
|
|
|
export const getIndexStatsTool = {
|
|
name: 'get_index_stats',
|
|
description: 'Gets detailed statistics for a specific index including usage counts and size.',
|
|
privilegeLevel: 'regular' as ToolPrivilegeLevel,
|
|
inputSchema: GetIndexStatsInputSchema,
|
|
mcpInputSchema: mcpInputSchema,
|
|
outputSchema: GetIndexStatsOutputSchema,
|
|
|
|
execute: async (input: GetIndexStatsInput, context: ToolContext) => {
|
|
const client = context.selfhostedClient;
|
|
const { schema, index_name } = input;
|
|
|
|
// SECURITY: Validate identifiers to prevent SQL injection
|
|
if (!identifierPattern.test(schema)) {
|
|
throw new Error(`Invalid schema name: ${schema}. Must be a valid SQL identifier.`);
|
|
}
|
|
if (!identifierPattern.test(index_name)) {
|
|
throw new Error(`Invalid index name: ${index_name}. Must be a valid SQL identifier.`);
|
|
}
|
|
|
|
const sql = `
|
|
SELECT
|
|
s.schemaname AS schema_name,
|
|
s.relname AS table_name,
|
|
s.indexrelname AS index_name,
|
|
am.amname AS index_type,
|
|
i.indisunique AS is_unique,
|
|
i.indisprimary AS is_primary,
|
|
i.indisvalid AS is_valid,
|
|
COALESCE(s.idx_scan, 0)::bigint AS number_of_scans,
|
|
COALESCE(s.idx_tup_read, 0)::bigint AS tuples_read,
|
|
COALESCE(s.idx_tup_fetch, 0)::bigint AS tuples_fetched,
|
|
pg_size_pretty(pg_relation_size(s.indexrelid)) AS index_size,
|
|
pg_size_pretty(pg_relation_size(s.relid)) AS table_size,
|
|
pg_relation_size(s.indexrelid)::bigint AS index_size_bytes,
|
|
pg_relation_size(s.relid)::bigint AS table_size_bytes,
|
|
CASE
|
|
WHEN (st.seq_scan + st.idx_scan) > 0
|
|
THEN ROUND((st.idx_scan::numeric / (st.seq_scan + st.idx_scan)::numeric) * 100, 2)::text || '%'
|
|
ELSE NULL
|
|
END AS usage_ratio
|
|
FROM pg_stat_user_indexes s
|
|
JOIN pg_catalog.pg_index i ON i.indexrelid = s.indexrelid
|
|
JOIN pg_catalog.pg_class c ON c.oid = s.indexrelid
|
|
JOIN pg_catalog.pg_am am ON am.oid = c.relam
|
|
JOIN pg_stat_user_tables st ON st.relid = s.relid
|
|
WHERE s.schemaname = '${schema}'
|
|
AND s.indexrelname = '${index_name}'
|
|
LIMIT 1
|
|
`;
|
|
|
|
const result = await executeSqlWithFallback(client, sql, true);
|
|
|
|
if (isSqlErrorResponse(result)) {
|
|
throw new Error(result.error.message || 'Failed to get index stats');
|
|
}
|
|
|
|
const rows = result as unknown[];
|
|
if (rows.length === 0) {
|
|
throw new Error(`Index "${index_name}" not found in schema "${schema}".`);
|
|
}
|
|
|
|
return GetIndexStatsOutputSchema.parse(rows[0]);
|
|
},
|
|
};
|