postgres-scout-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ import { z } from 'zod';
2
+ import { executeQuery } from '../utils/database.js';
3
+ import { formatQueryResult } from '../utils/result-formatter.js';
4
+ import { assertNoSensitiveCatalogAccess } from '../utils/sanitize.js';
5
+ const ExecuteQuerySchema = z.object({
6
+ query: z.string(),
7
+ params: z.array(z.any()).optional().default([]),
8
+ timeout: z.number().optional(),
9
+ maxRows: z.number().optional()
10
+ });
11
+ const ExplainQuerySchema = z.object({
12
+ query: z.string(),
13
+ params: z.array(z.any()).optional().default([]),
14
+ analyze: z.boolean().optional().default(true),
15
+ verbose: z.boolean().optional().default(false),
16
+ buffers: z.boolean().optional().default(true)
17
+ });
18
+ export async function executeQueryTool(connection, logger, args) {
19
+ const { query, params, timeout, maxRows } = args;
20
+ logger.info('executeQuery', 'Executing user query', {
21
+ queryLength: query.length,
22
+ paramCount: params.length
23
+ });
24
+ assertNoSensitiveCatalogAccess(query);
25
+ const startTime = Date.now();
26
+ const result = await executeQuery(connection, logger, {
27
+ query,
28
+ params,
29
+ options: { timeout, maxRows }
30
+ });
31
+ const executionTimeMs = Date.now() - startTime;
32
+ return formatQueryResult(result, executionTimeMs);
33
+ }
34
+ export async function explainQueryTool(connection, logger, args) {
35
+ const { query, params, verbose, buffers } = args;
36
+ const isReadOnly = connection.config.mode === 'read-only';
37
+ const requestedAnalyze = args.analyze;
38
+ const analyze = isReadOnly ? false : requestedAnalyze;
39
+ if (isReadOnly && requestedAnalyze) {
40
+ logger.warn('explainQuery', 'Read-only mode forces analyze=false for safety', {
41
+ queryLength: query.length
42
+ });
43
+ }
44
+ logger.info('explainQuery', 'Analyzing query performance', {
45
+ queryLength: query.length,
46
+ analyze,
47
+ verbose,
48
+ buffers
49
+ });
50
+ assertNoSensitiveCatalogAccess(query);
51
+ const explainOptions = ['FORMAT JSON'];
52
+ if (analyze)
53
+ explainOptions.push('ANALYZE');
54
+ if (verbose)
55
+ explainOptions.push('VERBOSE');
56
+ if (analyze && buffers)
57
+ explainOptions.push('BUFFERS');
58
+ const explainQuery = `EXPLAIN (${explainOptions.join(', ')}) ${query}`;
59
+ const startTime = Date.now();
60
+ const result = await executeQuery(connection, logger, {
61
+ query: explainQuery,
62
+ params
63
+ });
64
+ const executionTimeMs = Date.now() - startTime;
65
+ const planData = result.rows[0]?.['QUERY PLAN'] || result.rows[0];
66
+ let plan;
67
+ let planningTime = 0;
68
+ let executionTime = 0;
69
+ if (Array.isArray(planData)) {
70
+ plan = planData[0]?.Plan;
71
+ planningTime = planData[0]?.['Planning Time'] || 0;
72
+ executionTime = planData[0]?.['Execution Time'] || 0;
73
+ }
74
+ else {
75
+ plan = planData;
76
+ }
77
+ const recommendations = generateRecommendations(plan);
78
+ return {
79
+ query,
80
+ plan,
81
+ executionTimeMs: executionTime || executionTimeMs,
82
+ planningTimeMs: planningTime,
83
+ recommendations
84
+ };
85
+ }
86
+ function generateRecommendations(plan) {
87
+ const recommendations = [];
88
+ if (!plan)
89
+ return recommendations;
90
+ const nodeType = plan['Node Type'];
91
+ const relationName = plan['Relation Name'];
92
+ if (nodeType === 'Seq Scan') {
93
+ recommendations.push(`⚠ Sequential scan on table "${relationName}" - consider adding an index`);
94
+ }
95
+ if (nodeType === 'Index Scan' || nodeType === 'Index Only Scan') {
96
+ recommendations.push(`✓ Using index: ${plan['Index Name']}`);
97
+ }
98
+ if (plan['Actual Rows'] && plan['Plan Rows']) {
99
+ const ratio = plan['Actual Rows'] / plan['Plan Rows'];
100
+ if (ratio > 10 || ratio < 0.1) {
101
+ recommendations.push(`⚠ Poor row estimate (planned: ${plan['Plan Rows']}, actual: ${plan['Actual Rows']}) - run ANALYZE`);
102
+ }
103
+ }
104
+ if (plan['Buffers']) {
105
+ const shared = plan['Buffers']['Shared'];
106
+ if (shared) {
107
+ const hit = shared['Hit'] || 0;
108
+ const read = shared['Read'] || 0;
109
+ const total = hit + read;
110
+ if (total > 0) {
111
+ const hitRatio = hit / total;
112
+ if (hitRatio < 0.9) {
113
+ recommendations.push(`⚠ Low cache hit ratio (${(hitRatio * 100).toFixed(1)}%) - data mostly from disk`);
114
+ }
115
+ else {
116
+ recommendations.push(`✓ Excellent cache hit ratio (${(hitRatio * 100).toFixed(1)}%)`);
117
+ }
118
+ }
119
+ }
120
+ }
121
+ if (plan.Plans && Array.isArray(plan.Plans)) {
122
+ for (const subPlan of plan.Plans) {
123
+ recommendations.push(...generateRecommendations(subPlan));
124
+ }
125
+ }
126
+ return recommendations;
127
+ }
128
+ export const queryTools = {
129
+ executeQuery: {
130
+ schema: ExecuteQuerySchema,
131
+ handler: executeQueryTool
132
+ },
133
+ explainQuery: {
134
+ schema: ExplainQuerySchema,
135
+ handler: explainQueryTool
136
+ }
137
+ };
138
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1,261 @@
1
+ import { z } from 'zod';
2
+ import { executeInternalQuery } from '../utils/database.js';
3
+ import { escapeIdentifier, sanitizeIdentifier } from '../utils/sanitize.js';
4
+ const ExploreRelationshipsSchema = z.object({
5
+ table: z.string(),
6
+ recordId: z.union([z.string(), z.number()]),
7
+ schema: z.string().optional().default('public'),
8
+ depth: z.number().optional().default(1),
9
+ includeReverse: z.boolean().optional().default(true)
10
+ });
11
+ const AnalyzeForeignKeysSchema = z.object({
12
+ schema: z.string().optional().default('public'),
13
+ checkOrphans: z.boolean().optional().default(false),
14
+ checkIndexes: z.boolean().optional().default(true)
15
+ });
16
+ export async function exploreRelationships(connection, logger, args) {
17
+ const { table, recordId, schema, depth, includeReverse } = args;
18
+ logger.info('exploreRelationships', 'Exploring relationships', { table, recordId, depth });
19
+ const sanitizedSchema = sanitizeIdentifier(schema);
20
+ const sanitizedTable = sanitizeIdentifier(table);
21
+ const pkQuery = `
22
+ SELECT a.attname as column_name
23
+ FROM pg_index i
24
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
25
+ WHERE i.indrelid = $1::regclass
26
+ AND i.indisprimary
27
+ LIMIT 1
28
+ `;
29
+ const pkResult = await executeInternalQuery(connection, logger, {
30
+ query: pkQuery,
31
+ params: [`${sanitizedSchema}.${sanitizedTable}`]
32
+ });
33
+ if (pkResult.rows.length === 0) {
34
+ throw new Error(`No primary key found for table ${schema}.${table}`);
35
+ }
36
+ const pkColumn = pkResult.rows[0].column_name;
37
+ const recordQuery = `
38
+ SELECT *
39
+ FROM ${escapeIdentifier(sanitizedSchema)}.${escapeIdentifier(sanitizedTable)}
40
+ WHERE ${escapeIdentifier(sanitizeIdentifier(pkColumn))} = $1
41
+ `;
42
+ const recordResult = await executeInternalQuery(connection, logger, {
43
+ query: recordQuery,
44
+ params: [recordId]
45
+ });
46
+ if (recordResult.rows.length === 0) {
47
+ throw new Error(`Record not found: ${schema}.${table} where ${pkColumn} = ${recordId}`);
48
+ }
49
+ const record = recordResult.rows[0];
50
+ const fkQuery = `
51
+ SELECT
52
+ c.conname as constraint_name,
53
+ a.attname as column_name,
54
+ ref_ns.nspname as ref_schema,
55
+ ref_tbl.relname as ref_table,
56
+ ref_attr.attname as ref_column
57
+ FROM pg_constraint c
58
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
59
+ JOIN pg_class ref_tbl ON ref_tbl.oid = c.confrelid
60
+ JOIN pg_namespace ref_ns ON ref_ns.oid = ref_tbl.relnamespace
61
+ JOIN pg_attribute ref_attr ON ref_attr.attrelid = c.confrelid AND ref_attr.attnum = ANY(c.confkey)
62
+ WHERE c.conrelid = $1::regclass
63
+ AND c.contype = 'f'
64
+ `;
65
+ const fkResult = await executeInternalQuery(connection, logger, {
66
+ query: fkQuery,
67
+ params: [`${sanitizedSchema}.${sanitizedTable}`]
68
+ });
69
+ const related = {};
70
+ for (const fk of fkResult.rows) {
71
+ const fkValue = record[fk.column_name];
72
+ if (fkValue) {
73
+ const relatedQuery = `
74
+ SELECT *
75
+ FROM ${escapeIdentifier(fk.ref_schema)}.${escapeIdentifier(fk.ref_table)}
76
+ WHERE ${escapeIdentifier(fk.ref_column)} = $1
77
+ `;
78
+ const relatedResult = await executeInternalQuery(connection, logger, {
79
+ query: relatedQuery,
80
+ params: [fkValue]
81
+ });
82
+ if (relatedResult.rows.length > 0) {
83
+ related[fk.ref_table] = {
84
+ via: `${fk.column_name} -> ${fk.ref_table}.${fk.ref_column}`,
85
+ record: relatedResult.rows[0]
86
+ };
87
+ }
88
+ }
89
+ }
90
+ const reverseReferences = {};
91
+ if (includeReverse) {
92
+ const reverseFkQuery = `
93
+ SELECT
94
+ ns.nspname as schema,
95
+ tbl.relname as table,
96
+ a.attname as column_name,
97
+ c.conname as constraint_name
98
+ FROM pg_constraint c
99
+ JOIN pg_class tbl ON tbl.oid = c.conrelid
100
+ JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
101
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
102
+ WHERE c.confrelid = $1::regclass
103
+ AND c.contype = 'f'
104
+ `;
105
+ const reverseFkResult = await executeInternalQuery(connection, logger, {
106
+ query: reverseFkQuery,
107
+ params: [`${sanitizedSchema}.${sanitizedTable}`]
108
+ });
109
+ for (const revFk of reverseFkResult.rows) {
110
+ const reverseQuery = `
111
+ SELECT *
112
+ FROM ${escapeIdentifier(revFk.schema)}.${escapeIdentifier(revFk.table)}
113
+ WHERE ${escapeIdentifier(revFk.column_name)} = $1
114
+ LIMIT 10
115
+ `;
116
+ const reverseResult = await executeInternalQuery(connection, logger, {
117
+ query: reverseQuery,
118
+ params: [recordId]
119
+ });
120
+ if (reverseResult.rows.length > 0) {
121
+ reverseReferences[revFk.table] = reverseResult.rows;
122
+ }
123
+ }
124
+ }
125
+ return {
126
+ table,
127
+ schema,
128
+ primaryKey: pkColumn,
129
+ recordId,
130
+ record,
131
+ related,
132
+ ...(includeReverse && { reverseReferences })
133
+ };
134
+ }
135
+ export async function analyzeForeignKeys(connection, logger, args) {
136
+ const { schema, checkOrphans, checkIndexes } = args;
137
+ logger.info('analyzeForeignKeys', 'Analyzing foreign keys', { schema });
138
+ const sanitizedSchema = sanitizeIdentifier(schema);
139
+ const fkQuery = `
140
+ SELECT
141
+ c.conname as constraint_name,
142
+ ns.nspname as schema,
143
+ tbl.relname as table,
144
+ a.attname as column,
145
+ ref_ns.nspname as ref_schema,
146
+ ref_tbl.relname as ref_table,
147
+ ref_attr.attname as ref_column,
148
+ c.confupdtype as on_update,
149
+ c.confdeltype as on_delete
150
+ FROM pg_constraint c
151
+ JOIN pg_class tbl ON tbl.oid = c.conrelid
152
+ JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
153
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
154
+ JOIN pg_class ref_tbl ON ref_tbl.oid = c.confrelid
155
+ JOIN pg_namespace ref_ns ON ref_ns.oid = ref_tbl.relnamespace
156
+ JOIN pg_attribute ref_attr ON ref_attr.attrelid = c.confrelid AND ref_attr.attnum = ANY(c.confkey)
157
+ WHERE ns.nspname = $1
158
+ AND c.contype = 'f'
159
+ ORDER BY tbl.relname, c.conname
160
+ `;
161
+ const fkResult = await executeInternalQuery(connection, logger, {
162
+ query: fkQuery,
163
+ params: [sanitizedSchema]
164
+ });
165
+ const issues = [];
166
+ for (const fk of fkResult.rows) {
167
+ const fkIssues = [];
168
+ if (checkIndexes) {
169
+ const indexQuery = `
170
+ SELECT COUNT(*) as index_count
171
+ FROM pg_index i
172
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
173
+ WHERE i.indrelid = $1::regclass
174
+ AND a.attname = $2
175
+ `;
176
+ const indexResult = await executeInternalQuery(connection, logger, {
177
+ query: indexQuery,
178
+ params: [`${fk.schema}.${fk.table}`, fk.column]
179
+ });
180
+ const indexCount = parseInt(indexResult.rows[0]?.index_count || '0', 10);
181
+ if (indexCount === 0) {
182
+ fkIssues.push({
183
+ type: 'missing_index',
184
+ severity: 'warning',
185
+ message: 'Foreign key column not indexed',
186
+ impact: `Slow DELETE/UPDATE on ${fk.ref_table} table`,
187
+ recommendation: `CREATE INDEX CONCURRENTLY idx_${fk.table}_${fk.column} ON ${fk.schema}.${fk.table}(${fk.column});`,
188
+ estimatedImpact: 'Will speed up CASCADE operations and JOIN queries'
189
+ });
190
+ }
191
+ }
192
+ if (checkOrphans) {
193
+ const orphanQuery = `
194
+ SELECT COUNT(*) as orphan_count
195
+ FROM ${escapeIdentifier(fk.schema)}.${escapeIdentifier(fk.table)} t
196
+ LEFT JOIN ${escapeIdentifier(fk.ref_schema)}.${escapeIdentifier(fk.ref_table)} r
197
+ ON t.${escapeIdentifier(fk.column)} = r.${escapeIdentifier(fk.ref_column)}
198
+ WHERE t.${escapeIdentifier(fk.column)} IS NOT NULL
199
+ AND r.${escapeIdentifier(fk.ref_column)} IS NULL
200
+ `;
201
+ const orphanResult = await executeInternalQuery(connection, logger, {
202
+ query: orphanQuery
203
+ });
204
+ const orphanCount = parseInt(orphanResult.rows[0]?.orphan_count || '0', 10);
205
+ if (orphanCount > 0) {
206
+ fkIssues.push({
207
+ type: 'orphans',
208
+ severity: 'error',
209
+ message: `${orphanCount} orphaned records found`,
210
+ orphanCount,
211
+ recommendation: 'Clean up orphans before enforcing constraint',
212
+ cleanupQuery: `DELETE FROM ${fk.schema}.${fk.table} WHERE ${fk.column} NOT IN (SELECT ${fk.ref_column} FROM ${fk.ref_schema}.${fk.ref_table});`
213
+ });
214
+ }
215
+ }
216
+ if (fkIssues.length > 0) {
217
+ issues.push({
218
+ constraint: fk.constraint_name,
219
+ table: fk.table,
220
+ schema: fk.schema,
221
+ column: fk.column,
222
+ referencesTable: fk.ref_table,
223
+ referencesSchema: fk.ref_schema,
224
+ referencesColumn: fk.ref_column,
225
+ issues: fkIssues
226
+ });
227
+ }
228
+ }
229
+ const recommendations = [];
230
+ const missingIndexes = issues.filter(i => i.issues.some((issue) => issue.type === 'missing_index')).length;
231
+ const orphanedRecords = issues.filter(i => i.issues.some((issue) => issue.type === 'orphans')).length;
232
+ if (missingIndexes > 0) {
233
+ recommendations.push(`⚠ ${missingIndexes} foreign key columns missing indexes - significant performance impact`);
234
+ recommendations.push('Add missing indexes during low-traffic period (use CONCURRENTLY)');
235
+ }
236
+ if (orphanedRecords > 0) {
237
+ recommendations.push(`⚠ ${orphanedRecords} foreign keys have orphaned records - data integrity issue`);
238
+ recommendations.push('Clean up orphaned records before enforcing constraints');
239
+ }
240
+ if (issues.length === 0) {
241
+ recommendations.push('✓ All foreign keys are properly indexed and have no orphaned records');
242
+ }
243
+ return {
244
+ schema,
245
+ totalForeignKeys: fkResult.rows.length,
246
+ issuesFound: issues.length,
247
+ issues,
248
+ recommendations
249
+ };
250
+ }
251
+ export const relationshipTools = {
252
+ exploreRelationships: {
253
+ schema: ExploreRelationshipsSchema,
254
+ handler: exploreRelationships
255
+ },
256
+ analyzeForeignKeys: {
257
+ schema: AnalyzeForeignKeysSchema,
258
+ handler: analyzeForeignKeys
259
+ }
260
+ };
261
+ //# sourceMappingURL=relationships.js.map
@@ -0,0 +1,253 @@
1
+ import { z } from 'zod';
2
+ import { executeInternalQuery } from '../utils/database.js';
3
+ const ListTablesSchema = z.object({
4
+ schema: z.string().optional().default('public'),
5
+ includeSystemTables: z.boolean().optional().default(false)
6
+ });
7
+ const DescribeTableSchema = z.object({
8
+ table: z.string(),
9
+ schema: z.string().optional().default('public')
10
+ });
11
+ const ListSchemasSchema = z.object({});
12
+ export async function listSchemas(connection, logger, args) {
13
+ logger.info('listSchemas', 'Listing database schemas');
14
+ const query = `
15
+ SELECT
16
+ n.nspname as name,
17
+ pg_catalog.pg_get_userbyid(n.nspowner) as owner,
18
+ (SELECT COUNT(*) FROM pg_catalog.pg_class c WHERE c.relnamespace = n.oid AND c.relkind = 'r') as table_count
19
+ FROM pg_catalog.pg_namespace n
20
+ WHERE n.nspname !~ '^pg_' AND n.nspname <> 'information_schema'
21
+ ORDER BY n.nspname;
22
+ `;
23
+ const result = await executeInternalQuery(connection, logger, { query });
24
+ return {
25
+ schemas: result.rows.map(row => ({
26
+ name: row.name,
27
+ owner: row.owner,
28
+ tableCount: parseInt(row.table_count, 10)
29
+ }))
30
+ };
31
+ }
32
+ export async function listTables(connection, logger, args) {
33
+ const { schema, includeSystemTables } = args;
34
+ logger.info('listTables', 'Listing tables', { schema, includeSystemTables });
35
+ const systemTableFilter = includeSystemTables ? '' : "AND c.relname NOT LIKE 'pg_%'";
36
+ const query = `
37
+ SELECT
38
+ c.relname as name,
39
+ n.nspname as schema,
40
+ CASE c.relkind
41
+ WHEN 'r' THEN 'BASE TABLE'
42
+ WHEN 'v' THEN 'VIEW'
43
+ WHEN 'm' THEN 'MATERIALIZED VIEW'
44
+ WHEN 'p' THEN 'PARTITIONED TABLE'
45
+ END as type,
46
+ CASE
47
+ WHEN c.reltuples < 0 THEN NULL
48
+ ELSE c.reltuples::bigint
49
+ END as row_estimate,
50
+ pg_stat_get_live_tuples(c.oid) as live_tuples,
51
+ c.reltuples < 0 as needs_analyze,
52
+ pg_total_relation_size(c.oid) as total_size,
53
+ pg_table_size(c.oid) as table_size,
54
+ pg_indexes_size(c.oid) as index_size,
55
+ pg_stat_get_last_vacuum_time(c.oid) as last_vacuum,
56
+ pg_stat_get_last_analyze_time(c.oid) as last_analyze,
57
+ c.relkind = 'p' as is_partitioned
58
+ FROM pg_catalog.pg_class c
59
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
60
+ WHERE c.relkind IN ('r', 'v', 'm', 'p')
61
+ AND n.nspname = $1
62
+ ${systemTableFilter}
63
+ ORDER BY c.relname;
64
+ `;
65
+ const result = await executeInternalQuery(connection, logger, {
66
+ query,
67
+ params: [schema]
68
+ });
69
+ const tables = result.rows.map(row => {
70
+ const rowEstimate = row.row_estimate === null
71
+ ? parseInt(row.live_tuples || '0', 10)
72
+ : parseInt(row.row_estimate, 10);
73
+ return {
74
+ name: row.name,
75
+ schema: row.schema,
76
+ type: row.type,
77
+ rowEstimate,
78
+ ...(row.needs_analyze && { needsAnalyze: true }),
79
+ sizeBytes: parseInt(row.table_size, 10),
80
+ indexSize: parseInt(row.index_size, 10),
81
+ totalSize: parseInt(row.total_size, 10),
82
+ lastVacuum: row.last_vacuum,
83
+ lastAnalyze: row.last_analyze,
84
+ isPartitioned: row.is_partitioned
85
+ };
86
+ });
87
+ return {
88
+ schema,
89
+ tableCount: tables.length,
90
+ tables: tables.map(t => ({
91
+ ...t,
92
+ sizeMB: (t.sizeBytes / 1024 / 1024).toFixed(2),
93
+ indexSizeMB: (t.indexSize / 1024 / 1024).toFixed(2),
94
+ totalSizeMB: (t.totalSize / 1024 / 1024).toFixed(2)
95
+ }))
96
+ };
97
+ }
98
+ export async function describeTable(connection, logger, args) {
99
+ const { table, schema } = args;
100
+ logger.info('describeTable', 'Describing table', { table, schema });
101
+ const [columns, constraints, indexes, stats] = await Promise.all([
102
+ getColumns(connection, logger, schema, table),
103
+ getConstraints(connection, logger, schema, table),
104
+ getIndexes(connection, logger, schema, table),
105
+ getTableStats(connection, logger, schema, table)
106
+ ]);
107
+ return {
108
+ table,
109
+ schema,
110
+ columns,
111
+ constraints,
112
+ indexes,
113
+ ...stats
114
+ };
115
+ }
116
+ async function getColumns(connection, logger, schema, table) {
117
+ const query = `
118
+ SELECT
119
+ a.attname as name,
120
+ pg_catalog.format_type(a.atttypid, a.atttypmod) as type,
121
+ NOT a.attnotnull as nullable,
122
+ pg_catalog.pg_get_expr(d.adbin, d.adrelid) as default_value,
123
+ EXISTS(
124
+ SELECT 1 FROM pg_catalog.pg_constraint c
125
+ WHERE c.conrelid = a.attrelid
126
+ AND a.attnum = ANY(c.conkey)
127
+ AND c.contype = 'p'
128
+ ) as is_primary_key,
129
+ EXISTS(
130
+ SELECT 1 FROM pg_catalog.pg_constraint c
131
+ WHERE c.conrelid = a.attrelid
132
+ AND a.attnum = ANY(c.conkey)
133
+ AND c.contype = 'f'
134
+ ) as is_foreign_key
135
+ FROM pg_catalog.pg_attribute a
136
+ LEFT JOIN pg_catalog.pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
137
+ WHERE a.attrelid = $1::regclass
138
+ AND a.attnum > 0
139
+ AND NOT a.attisdropped
140
+ ORDER BY a.attnum;
141
+ `;
142
+ const result = await executeInternalQuery(connection, logger, {
143
+ query,
144
+ params: [`${schema}.${table}`]
145
+ });
146
+ return result.rows.map(row => ({
147
+ name: row.name,
148
+ type: row.type,
149
+ nullable: row.nullable,
150
+ default: row.default_value,
151
+ isPrimaryKey: row.is_primary_key,
152
+ isForeignKey: row.is_foreign_key
153
+ }));
154
+ }
155
+ async function getConstraints(connection, logger, schema, table) {
156
+ const query = `
157
+ SELECT
158
+ c.conname as name,
159
+ c.contype as type,
160
+ pg_catalog.pg_get_constraintdef(c.oid, true) as definition,
161
+ ARRAY(
162
+ SELECT a.attname
163
+ FROM unnest(c.conkey) k(n)
164
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.n
165
+ ) as columns
166
+ FROM pg_catalog.pg_constraint c
167
+ WHERE c.conrelid = $1::regclass
168
+ ORDER BY c.conname;
169
+ `;
170
+ const result = await executeInternalQuery(connection, logger, {
171
+ query,
172
+ params: [`${schema}.${table}`]
173
+ });
174
+ return result.rows.map(row => {
175
+ const typeMap = {
176
+ 'p': 'PRIMARY KEY',
177
+ 'f': 'FOREIGN KEY',
178
+ 'u': 'UNIQUE',
179
+ 'c': 'CHECK'
180
+ };
181
+ return {
182
+ name: row.name,
183
+ type: typeMap[row.type] || 'CHECK',
184
+ columns: row.columns,
185
+ definition: row.definition
186
+ };
187
+ });
188
+ }
189
+ async function getIndexes(connection, logger, schema, table) {
190
+ const query = `
191
+ SELECT
192
+ i.relname as name,
193
+ ARRAY(
194
+ SELECT a.attname
195
+ FROM pg_catalog.pg_attribute a
196
+ WHERE a.attrelid = i.oid
197
+ AND a.attnum > 0
198
+ ORDER BY a.attnum
199
+ ) as columns,
200
+ am.amname as type,
201
+ ix.indisunique as unique,
202
+ ix.indisprimary as primary,
203
+ pg_size_pretty(pg_relation_size(i.oid)) as size
204
+ FROM pg_catalog.pg_index ix
205
+ JOIN pg_catalog.pg_class i ON i.oid = ix.indexrelid
206
+ JOIN pg_catalog.pg_am am ON am.oid = i.relam
207
+ WHERE ix.indrelid = $1::regclass
208
+ ORDER BY i.relname;
209
+ `;
210
+ const result = await executeInternalQuery(connection, logger, {
211
+ query,
212
+ params: [`${schema}.${table}`]
213
+ });
214
+ return result.rows.map(row => ({
215
+ name: row.name,
216
+ columns: row.columns,
217
+ type: row.type,
218
+ unique: row.unique,
219
+ primary: row.primary,
220
+ size: row.size
221
+ }));
222
+ }
223
+ async function getTableStats(connection, logger, schema, table) {
224
+ const query = `
225
+ SELECT
226
+ c.reltuples::bigint as estimated_row_count,
227
+ pg_size_pretty(pg_table_size(c.oid)) as disk_size,
228
+ pg_size_pretty(pg_indexes_size(c.oid)) as index_size,
229
+ pg_size_pretty(pg_total_relation_size(c.oid)) as total_size
230
+ FROM pg_catalog.pg_class c
231
+ WHERE c.oid = $1::regclass;
232
+ `;
233
+ const result = await executeInternalQuery(connection, logger, {
234
+ query,
235
+ params: [`${schema}.${table}`]
236
+ });
237
+ return result.rows[0] || {};
238
+ }
239
+ export const schemaTools = {
240
+ listSchemas: {
241
+ schema: ListSchemasSchema,
242
+ handler: listSchemas
243
+ },
244
+ listTables: {
245
+ schema: ListTablesSchema,
246
+ handler: listTables
247
+ },
248
+ describeTable: {
249
+ schema: DescribeTableSchema,
250
+ handler: describeTable
251
+ }
252
+ };
253
+ //# sourceMappingURL=schema.js.map