awslabs.dynamodb-mcp-server 2.0.10__py3-none-any.whl
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.
- awslabs/__init__.py +17 -0
- awslabs/dynamodb_mcp_server/__init__.py +17 -0
- awslabs/dynamodb_mcp_server/cdk_generator/__init__.py +19 -0
- awslabs/dynamodb_mcp_server/cdk_generator/generator.py +276 -0
- awslabs/dynamodb_mcp_server/cdk_generator/models.py +521 -0
- awslabs/dynamodb_mcp_server/cdk_generator/templates/README.md +57 -0
- awslabs/dynamodb_mcp_server/cdk_generator/templates/stack.ts.j2 +70 -0
- awslabs/dynamodb_mcp_server/common.py +94 -0
- awslabs/dynamodb_mcp_server/db_analyzer/__init__.py +30 -0
- awslabs/dynamodb_mcp_server/db_analyzer/analyzer_utils.py +394 -0
- awslabs/dynamodb_mcp_server/db_analyzer/base_plugin.py +355 -0
- awslabs/dynamodb_mcp_server/db_analyzer/mysql.py +450 -0
- awslabs/dynamodb_mcp_server/db_analyzer/plugin_registry.py +73 -0
- awslabs/dynamodb_mcp_server/db_analyzer/postgresql.py +215 -0
- awslabs/dynamodb_mcp_server/db_analyzer/sqlserver.py +255 -0
- awslabs/dynamodb_mcp_server/markdown_formatter.py +513 -0
- awslabs/dynamodb_mcp_server/model_validation_utils.py +845 -0
- awslabs/dynamodb_mcp_server/prompts/dynamodb_architect.md +851 -0
- awslabs/dynamodb_mcp_server/prompts/json_generation_guide.md +185 -0
- awslabs/dynamodb_mcp_server/prompts/transform_model_validation_result.md +168 -0
- awslabs/dynamodb_mcp_server/server.py +524 -0
- awslabs_dynamodb_mcp_server-2.0.10.dist-info/METADATA +306 -0
- awslabs_dynamodb_mcp_server-2.0.10.dist-info/RECORD +27 -0
- awslabs_dynamodb_mcp_server-2.0.10.dist-info/WHEEL +4 -0
- awslabs_dynamodb_mcp_server-2.0.10.dist-info/entry_points.txt +2 -0
- awslabs_dynamodb_mcp_server-2.0.10.dist-info/licenses/LICENSE +175 -0
- awslabs_dynamodb_mcp_server-2.0.10.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""PostgreSQL database analyzer plugin."""
|
|
16
|
+
|
|
17
|
+
from awslabs.dynamodb_mcp_server.db_analyzer.base_plugin import DatabasePlugin
|
|
18
|
+
from typing import Any, Dict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_postgresql_analysis_queries = {
|
|
22
|
+
'pg_stat_statements_check': {
|
|
23
|
+
'name': 'pg_stat_statements Extension Check',
|
|
24
|
+
'description': 'Check if pg_stat_statements extension is installed and enabled',
|
|
25
|
+
'category': 'internal',
|
|
26
|
+
'sql': """SELECT
|
|
27
|
+
CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END as enabled
|
|
28
|
+
FROM pg_extension
|
|
29
|
+
WHERE extname = 'pg_stat_statements';""",
|
|
30
|
+
'parameters': [],
|
|
31
|
+
},
|
|
32
|
+
'comprehensive_table_analysis': {
|
|
33
|
+
'name': 'Comprehensive Table Analysis',
|
|
34
|
+
'description': 'Complete table statistics including structure, size, and I/O',
|
|
35
|
+
'category': 'information_schema',
|
|
36
|
+
'sql': """SELECT
|
|
37
|
+
pst.relname as table_name,
|
|
38
|
+
pst.n_live_tup as row_count,
|
|
39
|
+
pg_total_relation_size(c.oid) as total_size_bytes,
|
|
40
|
+
pg_relation_size(c.oid) as data_size_bytes,
|
|
41
|
+
pg_total_relation_size(c.oid) - pg_relation_size(c.oid) as index_size_bytes,
|
|
42
|
+
ROUND(pg_relation_size(c.oid)::numeric/1024/1024, 2) as data_size_mb,
|
|
43
|
+
ROUND((pg_total_relation_size(c.oid) - pg_relation_size(c.oid))::numeric/1024/1024, 2)
|
|
44
|
+
as index_size_mb,
|
|
45
|
+
ROUND(pg_total_relation_size(c.oid)::numeric/1024/1024, 2) as total_size_mb,
|
|
46
|
+
pst.seq_scan as sequential_scans,
|
|
47
|
+
pst.seq_tup_read as sequential_rows_read,
|
|
48
|
+
pst.idx_scan as index_scans,
|
|
49
|
+
pst.idx_tup_fetch as index_rows_fetched,
|
|
50
|
+
pst.n_tup_ins as inserts,
|
|
51
|
+
pst.n_tup_upd as updates,
|
|
52
|
+
pst.n_tup_del as deletes,
|
|
53
|
+
pst.n_tup_hot_upd as hot_updates,
|
|
54
|
+
pst.n_live_tup as live_tuples,
|
|
55
|
+
pst.n_dead_tup as dead_tuples
|
|
56
|
+
FROM pg_stat_user_tables pst
|
|
57
|
+
JOIN pg_class c ON c.relname = pst.relname
|
|
58
|
+
AND c.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = pst.schemaname)
|
|
59
|
+
ORDER BY pst.n_live_tup DESC;""",
|
|
60
|
+
'parameters': [],
|
|
61
|
+
},
|
|
62
|
+
'comprehensive_index_analysis': {
|
|
63
|
+
'name': 'Comprehensive Index Analysis',
|
|
64
|
+
'description': 'Complete index statistics including structure and usage',
|
|
65
|
+
'category': 'information_schema',
|
|
66
|
+
'sql': """SELECT
|
|
67
|
+
psi.relname as table_name,
|
|
68
|
+
psi.indexrelname as index_name,
|
|
69
|
+
psi.idx_scan as index_scans,
|
|
70
|
+
psi.idx_tup_read as tuples_read,
|
|
71
|
+
psi.idx_tup_fetch as tuples_fetched,
|
|
72
|
+
pg_size_pretty(pg_relation_size(psi.indexrelid)) as index_size,
|
|
73
|
+
pg_relation_size(psi.indexrelid) as index_size_bytes
|
|
74
|
+
FROM pg_stat_user_indexes psi
|
|
75
|
+
ORDER BY psi.relname, psi.indexrelname;""",
|
|
76
|
+
'parameters': [],
|
|
77
|
+
},
|
|
78
|
+
'column_analysis': {
|
|
79
|
+
'name': 'Column Information Analysis',
|
|
80
|
+
'description': 'Returns all column definitions including data types, nullability, and defaults',
|
|
81
|
+
'category': 'information_schema',
|
|
82
|
+
'sql': """SELECT
|
|
83
|
+
table_name,
|
|
84
|
+
column_name,
|
|
85
|
+
ordinal_position as position,
|
|
86
|
+
column_default as default_value,
|
|
87
|
+
is_nullable as nullable,
|
|
88
|
+
data_type,
|
|
89
|
+
character_maximum_length as char_max_length,
|
|
90
|
+
numeric_precision,
|
|
91
|
+
numeric_scale,
|
|
92
|
+
udt_name as column_type
|
|
93
|
+
FROM information_schema.columns
|
|
94
|
+
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
95
|
+
ORDER BY table_name, ordinal_position;""",
|
|
96
|
+
'parameters': [],
|
|
97
|
+
},
|
|
98
|
+
'foreign_key_analysis': {
|
|
99
|
+
'name': 'Foreign Key Relationship Analysis',
|
|
100
|
+
'description': 'Returns foreign key relationships with constraint names and table/column mappings',
|
|
101
|
+
'category': 'information_schema',
|
|
102
|
+
'sql': """SELECT
|
|
103
|
+
tc.constraint_name,
|
|
104
|
+
tc.table_name as child_table,
|
|
105
|
+
kcu.column_name as child_column,
|
|
106
|
+
ccu.table_name as parent_table,
|
|
107
|
+
ccu.column_name as parent_column,
|
|
108
|
+
rc.update_rule,
|
|
109
|
+
rc.delete_rule
|
|
110
|
+
FROM information_schema.table_constraints tc
|
|
111
|
+
JOIN information_schema.key_column_usage kcu
|
|
112
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
113
|
+
AND tc.table_schema = kcu.table_schema
|
|
114
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
115
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
116
|
+
AND ccu.table_schema = tc.table_schema
|
|
117
|
+
JOIN information_schema.referential_constraints rc
|
|
118
|
+
ON rc.constraint_name = tc.constraint_name
|
|
119
|
+
AND rc.constraint_schema = tc.table_schema
|
|
120
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
121
|
+
AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
122
|
+
ORDER BY tc.table_name, kcu.column_name;""",
|
|
123
|
+
'parameters': [],
|
|
124
|
+
},
|
|
125
|
+
'query_performance_stats': {
|
|
126
|
+
'name': 'Query Performance Statistics',
|
|
127
|
+
'description': 'Query execution statistics with performance metrics from pg_stat_statements',
|
|
128
|
+
'category': 'performance_schema',
|
|
129
|
+
'sql': """SELECT
|
|
130
|
+
LEFT(pss.query, 200) as query_pattern,
|
|
131
|
+
pss.calls as executions,
|
|
132
|
+
ROUND((pss.total_exec_time / NULLIF(pss.calls, 0))::numeric, 2) as avg_latency_ms,
|
|
133
|
+
ROUND(pss.total_exec_time::numeric, 2) as total_time_ms,
|
|
134
|
+
pss.rows as rows_affected,
|
|
135
|
+
ROUND((pss.rows::numeric / NULLIF(pss.calls, 0)), 2) as avg_rows_returned,
|
|
136
|
+
pss.shared_blks_hit as cache_hits,
|
|
137
|
+
pss.shared_blks_read as disk_reads,
|
|
138
|
+
ROUND((pss.shared_blks_hit::numeric /
|
|
139
|
+
NULLIF(pss.shared_blks_hit + pss.shared_blks_read, 0) * 100), 2) as cache_hit_ratio_pct,
|
|
140
|
+
pss.temp_blks_read as temp_blocks_read,
|
|
141
|
+
pss.temp_blks_written as temp_blocks_written,
|
|
142
|
+
ROUND(pss.blk_read_time::numeric, 2) as io_read_time_ms,
|
|
143
|
+
ROUND(pss.blk_write_time::numeric, 2) as io_write_time_ms,
|
|
144
|
+
COALESCE(psd.stats_reset, pg_postmaster_start_time()) as first_seen,
|
|
145
|
+
now() as last_seen,
|
|
146
|
+
ROUND((pss.calls::numeric / NULLIF(EXTRACT(EPOCH FROM
|
|
147
|
+
(now() - COALESCE(psd.stats_reset, pg_postmaster_start_time()))), 0)), 6) as estimated_rps
|
|
148
|
+
FROM pg_stat_statements pss
|
|
149
|
+
JOIN pg_stat_database psd ON pss.dbid = psd.datid
|
|
150
|
+
WHERE pss.dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
|
151
|
+
-- Filter out PostgreSQL system catalogs and views (explicit list to avoid filtering user tables with pg_ prefix)
|
|
152
|
+
AND pss.query NOT LIKE '%pg_catalog%'
|
|
153
|
+
AND pss.query NOT LIKE '%pg_stat_statements%'
|
|
154
|
+
AND pss.query NOT LIKE '%pg_stat_user_tables%'
|
|
155
|
+
AND pss.query NOT LIKE '%pg_stat_user_indexes%'
|
|
156
|
+
AND pss.query NOT LIKE '%pg_stat_user_functions%'
|
|
157
|
+
AND pss.query NOT LIKE '%pg_stat_database%'
|
|
158
|
+
AND pss.query NOT LIKE '%pg_stat_activity%'
|
|
159
|
+
AND pss.query NOT LIKE '%pg_class%'
|
|
160
|
+
AND pss.query NOT LIKE '%pg_namespace%'
|
|
161
|
+
AND pss.query NOT LIKE '%pg_attribute%'
|
|
162
|
+
AND pss.query NOT LIKE '%pg_index%'
|
|
163
|
+
AND pss.query NOT LIKE '%pg_constraint%'
|
|
164
|
+
AND pss.query NOT LIKE '%pg_type%'
|
|
165
|
+
AND pss.query NOT LIKE '%pg_extension%'
|
|
166
|
+
AND pss.query NOT LIKE '%pg_database%'
|
|
167
|
+
AND pss.query NOT LIKE '%pg_tables%'
|
|
168
|
+
AND pss.query NOT LIKE '%pg_indexes%'
|
|
169
|
+
AND pss.query NOT LIKE '%pg_views%'
|
|
170
|
+
AND pss.query NOT LIKE '%pg_locks%'
|
|
171
|
+
AND pss.query NOT LIKE '%pg_settings%'
|
|
172
|
+
-- Filter out PostgreSQL system functions
|
|
173
|
+
AND pss.query NOT LIKE '%pg_relation_size%'
|
|
174
|
+
AND pss.query NOT LIKE '%pg_total_relation_size%'
|
|
175
|
+
AND pss.query NOT LIKE '%pg_size_pretty%'
|
|
176
|
+
AND pss.query NOT LIKE '%pg_postmaster_start_time%'
|
|
177
|
+
AND pss.query NOT LIKE '%pg_sleep%'
|
|
178
|
+
-- Filter out information_schema
|
|
179
|
+
AND pss.query NOT LIKE '%information_schema%'
|
|
180
|
+
-- Filter out session/transaction control
|
|
181
|
+
AND pss.query !~* '^(SET|SHOW|RESET|BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE|DEALLOCATE|DISCARD)\\s'
|
|
182
|
+
AND pss.query !~* '^(LISTEN|NOTIFY|UNLISTEN)\\s'
|
|
183
|
+
-- Filter out utility/maintenance commands
|
|
184
|
+
AND pss.query !~* '^(VACUUM|ANALYZE|REINDEX|CLUSTER|CHECKPOINT|EXPLAIN)\\s'
|
|
185
|
+
-- Filter out DDL (focus on DML for access patterns)
|
|
186
|
+
AND pss.query !~* '^(CREATE|ALTER|DROP|TRUNCATE|COMMENT|GRANT|REVOKE)\\s'
|
|
187
|
+
ORDER BY pss.total_exec_time DESC;""",
|
|
188
|
+
'parameters': [],
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class PostgreSQLPlugin(DatabasePlugin):
|
|
194
|
+
"""PostgreSQL-specific database analyzer plugin."""
|
|
195
|
+
|
|
196
|
+
def get_queries(self) -> Dict[str, Any]:
|
|
197
|
+
"""Get all PostgreSQL analysis queries."""
|
|
198
|
+
return _postgresql_analysis_queries
|
|
199
|
+
|
|
200
|
+
def get_database_display_name(self) -> str:
|
|
201
|
+
"""Get the display name of the database type."""
|
|
202
|
+
return 'PostgreSQL'
|
|
203
|
+
|
|
204
|
+
# write_queries_to_file and apply_result_limit are inherited from DatabasePlugin base class
|
|
205
|
+
|
|
206
|
+
# parse_results_from_file is inherited from DatabasePlugin base class
|
|
207
|
+
|
|
208
|
+
async def execute_managed_mode(self, connection_params: Dict[str, Any]) -> Dict[str, Any]:
|
|
209
|
+
"""Execute PostgreSQL analysis in managed mode.
|
|
210
|
+
|
|
211
|
+
Note: Managed mode not yet implemented for PostgreSQL.
|
|
212
|
+
"""
|
|
213
|
+
raise NotImplementedError(
|
|
214
|
+
'Managed mode is not yet implemented for PostgreSQL. Please use self_service mode.'
|
|
215
|
+
)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""SQL Server database analyzer plugin."""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from awslabs.dynamodb_mcp_server.db_analyzer.base_plugin import DatabasePlugin
|
|
19
|
+
from typing import Any, Dict
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_sqlserver_analysis_queries = {
|
|
23
|
+
'comprehensive_table_analysis': {
|
|
24
|
+
'name': 'Comprehensive Table Analysis',
|
|
25
|
+
'description': 'Complete table statistics including structure, size, and I/O',
|
|
26
|
+
'category': 'information_schema',
|
|
27
|
+
'sql': """SELECT
|
|
28
|
+
t.name as table_name,
|
|
29
|
+
MAX(p.rows) as row_count,
|
|
30
|
+
SUM(CASE WHEN idx.index_id < 2 THEN ps.used_page_count ELSE 0 END) * 8 as data_size_kb,
|
|
31
|
+
SUM(CASE WHEN idx.index_id >= 2 THEN ps.used_page_count ELSE 0 END) * 8 as index_size_kb,
|
|
32
|
+
SUM(ps.used_page_count) * 8 as total_size_kb,
|
|
33
|
+
ROUND(SUM(CASE WHEN idx.index_id < 2 THEN ps.used_page_count ELSE 0 END) * 8.0 / 1024, 2) as data_size_mb,
|
|
34
|
+
ROUND(SUM(CASE WHEN idx.index_id >= 2 THEN ps.used_page_count ELSE 0 END) * 8.0 / 1024, 2) as index_size_mb,
|
|
35
|
+
ROUND(SUM(ps.used_page_count) * 8.0 / 1024, 2) as total_size_mb,
|
|
36
|
+
MAX(ISNULL(i.user_seeks, 0)) as index_seeks,
|
|
37
|
+
MAX(ISNULL(i.user_scans, 0)) as table_scans,
|
|
38
|
+
MAX(ISNULL(i.user_lookups, 0)) as index_lookups,
|
|
39
|
+
MAX(ISNULL(i.user_updates, 0)) as updates
|
|
40
|
+
FROM sys.tables t
|
|
41
|
+
INNER JOIN sys.indexes idx ON t.object_id = idx.object_id
|
|
42
|
+
INNER JOIN sys.partitions p ON idx.object_id = p.object_id AND idx.index_id = p.index_id
|
|
43
|
+
INNER JOIN sys.dm_db_partition_stats ps ON idx.object_id = ps.object_id AND idx.index_id = ps.index_id
|
|
44
|
+
LEFT JOIN sys.dm_db_index_usage_stats i ON idx.object_id = i.object_id AND idx.index_id = i.index_id AND i.database_id = DB_ID()
|
|
45
|
+
WHERE t.is_ms_shipped = 0
|
|
46
|
+
GROUP BY t.name
|
|
47
|
+
ORDER BY MAX(p.rows) DESC;""",
|
|
48
|
+
'parameters': ['target_database'],
|
|
49
|
+
},
|
|
50
|
+
'comprehensive_index_analysis': {
|
|
51
|
+
'name': 'Comprehensive Index Analysis',
|
|
52
|
+
'description': 'Complete index statistics including structure and usage',
|
|
53
|
+
'category': 'information_schema',
|
|
54
|
+
'sql': """SELECT
|
|
55
|
+
OBJECT_NAME(i.object_id) as table_name,
|
|
56
|
+
i.name as index_name,
|
|
57
|
+
i.type_desc as index_type,
|
|
58
|
+
i.is_unique as is_unique,
|
|
59
|
+
ISNULL(s.user_seeks, 0) as seeks,
|
|
60
|
+
ISNULL(s.user_scans, 0) as scans,
|
|
61
|
+
ISNULL(s.user_lookups, 0) as lookups,
|
|
62
|
+
ISNULL(s.user_updates, 0) as updates,
|
|
63
|
+
SUM(ps.used_page_count) * 8 as index_size_kb
|
|
64
|
+
FROM sys.indexes i
|
|
65
|
+
LEFT JOIN sys.dm_db_index_usage_stats s ON i.object_id = s.object_id AND i.index_id = s.index_id AND s.database_id = DB_ID()
|
|
66
|
+
LEFT JOIN sys.dm_db_partition_stats ps ON i.object_id = ps.object_id AND i.index_id = ps.index_id
|
|
67
|
+
WHERE OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
|
|
68
|
+
GROUP BY i.object_id, i.name, i.type_desc, i.is_unique, s.user_seeks, s.user_scans, s.user_lookups, s.user_updates
|
|
69
|
+
ORDER BY OBJECT_NAME(i.object_id), i.name;""",
|
|
70
|
+
'parameters': ['target_database'],
|
|
71
|
+
},
|
|
72
|
+
'column_analysis': {
|
|
73
|
+
'name': 'Column Information Analysis',
|
|
74
|
+
'description': 'Returns all column definitions including data types, nullability, and defaults',
|
|
75
|
+
'category': 'information_schema',
|
|
76
|
+
'sql': """SELECT
|
|
77
|
+
TABLE_NAME as table_name,
|
|
78
|
+
COLUMN_NAME as column_name,
|
|
79
|
+
ORDINAL_POSITION as position,
|
|
80
|
+
COLUMN_DEFAULT as default_value,
|
|
81
|
+
IS_NULLABLE as nullable,
|
|
82
|
+
DATA_TYPE as data_type,
|
|
83
|
+
CHARACTER_MAXIMUM_LENGTH as char_max_length,
|
|
84
|
+
NUMERIC_PRECISION as numeric_precision,
|
|
85
|
+
NUMERIC_SCALE as numeric_scale
|
|
86
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
87
|
+
WHERE TABLE_CATALOG = '{target_database}'
|
|
88
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION;""",
|
|
89
|
+
'parameters': ['target_database'],
|
|
90
|
+
},
|
|
91
|
+
'foreign_key_analysis': {
|
|
92
|
+
'name': 'Foreign Key Relationship Analysis',
|
|
93
|
+
'description': 'Returns foreign key relationships with constraint names and table/column mappings',
|
|
94
|
+
'category': 'information_schema',
|
|
95
|
+
'sql': """SELECT
|
|
96
|
+
fk.name as constraint_name,
|
|
97
|
+
OBJECT_NAME(fk.parent_object_id) as child_table,
|
|
98
|
+
COL_NAME(fkc.parent_object_id, fkc.parent_column_id) as child_column,
|
|
99
|
+
OBJECT_NAME(fk.referenced_object_id) as parent_table,
|
|
100
|
+
COL_NAME(fkc.referenced_object_id, fkc.referenced_column_id) as parent_column,
|
|
101
|
+
fk.update_referential_action_desc as update_rule,
|
|
102
|
+
fk.delete_referential_action_desc as delete_rule
|
|
103
|
+
FROM sys.foreign_keys fk
|
|
104
|
+
INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
|
|
105
|
+
ORDER BY child_table, child_column;""",
|
|
106
|
+
'parameters': ['target_database'],
|
|
107
|
+
},
|
|
108
|
+
'query_performance_stats': {
|
|
109
|
+
'name': 'Query Performance Statistics',
|
|
110
|
+
'description': 'Unified view of query execution including stored procedures with metrics',
|
|
111
|
+
'category': 'performance_schema',
|
|
112
|
+
'sql': """SELECT
|
|
113
|
+
'QUERY' as source_type,
|
|
114
|
+
SUBSTRING(
|
|
115
|
+
st.text,
|
|
116
|
+
(qs.statement_start_offset/2) + 1,
|
|
117
|
+
((CASE qs.statement_end_offset
|
|
118
|
+
WHEN -1 THEN DATALENGTH(st.text)
|
|
119
|
+
ELSE qs.statement_end_offset
|
|
120
|
+
END - qs.statement_start_offset)/2) + 1
|
|
121
|
+
) as query_pattern,
|
|
122
|
+
NULL as procedure_name,
|
|
123
|
+
qs.execution_count as total_executions,
|
|
124
|
+
ROUND(qs.total_elapsed_time / 1000.0 / qs.execution_count, 2) as avg_latency_ms,
|
|
125
|
+
ROUND(qs.min_elapsed_time / 1000.0, 2) as min_latency_ms,
|
|
126
|
+
ROUND(qs.max_elapsed_time / 1000.0, 2) as max_latency_ms,
|
|
127
|
+
ROUND(qs.total_elapsed_time / 1000.0, 2) as total_time_ms,
|
|
128
|
+
ROUND(CAST(qs.total_rows as FLOAT) / qs.execution_count, 2) as avg_rows_returned,
|
|
129
|
+
ROUND(CAST(qs.total_logical_reads as FLOAT) / qs.execution_count, 2) as avg_logical_reads,
|
|
130
|
+
ROUND(CAST(qs.total_physical_reads as FLOAT) / qs.execution_count, 2) as avg_physical_reads,
|
|
131
|
+
ROUND(qs.total_worker_time / 1000.0 / qs.execution_count, 2) as avg_cpu_time_ms,
|
|
132
|
+
qs.creation_time as first_seen,
|
|
133
|
+
qs.last_execution_time as last_seen,
|
|
134
|
+
CASE
|
|
135
|
+
WHEN DATEDIFF(SECOND, qs.creation_time, qs.last_execution_time) > 0
|
|
136
|
+
THEN ROUND(
|
|
137
|
+
CAST(qs.execution_count as FLOAT) /
|
|
138
|
+
DATEDIFF(SECOND, qs.creation_time, qs.last_execution_time),
|
|
139
|
+
2
|
|
140
|
+
)
|
|
141
|
+
ELSE NULL
|
|
142
|
+
END as calculated_rps
|
|
143
|
+
FROM sys.dm_exec_query_stats qs
|
|
144
|
+
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
|
|
145
|
+
WHERE st.dbid = DB_ID('{target_database}')
|
|
146
|
+
AND qs.execution_count > 0
|
|
147
|
+
-- Filter out system catalog views (FROM sys.xxx pattern)
|
|
148
|
+
AND st.text NOT LIKE '%FROM sys.%' AND st.text NOT LIKE '%JOIN sys.%'
|
|
149
|
+
AND st.text NOT LIKE '%INFORMATION_SCHEMA%'
|
|
150
|
+
-- Filter out DMVs (dm_exec_, dm_db_, dm_os_, dm_tran_ prefixes)
|
|
151
|
+
AND st.text NOT LIKE '%dm[_]exec[_]%' ESCAPE '['
|
|
152
|
+
AND st.text NOT LIKE '%dm[_]db[_]%' ESCAPE '['
|
|
153
|
+
AND st.text NOT LIKE '%dm[_]os[_]%' ESCAPE '['
|
|
154
|
+
AND st.text NOT LIKE '%dm[_]tran[_]%' ESCAPE '['
|
|
155
|
+
-- Filter out system metadata functions
|
|
156
|
+
AND st.text NOT LIKE '%OBJECT[_]NAME(%' ESCAPE '['
|
|
157
|
+
AND st.text NOT LIKE '%OBJECT[_]ID(%' ESCAPE '['
|
|
158
|
+
AND st.text NOT LIKE '%COL[_]NAME(%' ESCAPE '['
|
|
159
|
+
AND st.text NOT LIKE '%OBJECTPROPERTY(%'
|
|
160
|
+
AND st.text NOT LIKE '%DB[_]ID(%' ESCAPE '['
|
|
161
|
+
AND st.text NOT LIKE '%DB[_]NAME(%' ESCAPE '['
|
|
162
|
+
-- Filter out utility/maintenance commands
|
|
163
|
+
AND st.text NOT LIKE 'SET %' AND st.text NOT LIKE 'DBCC %'
|
|
164
|
+
AND st.text NOT LIKE 'BACKUP %' AND st.text NOT LIKE 'RESTORE %'
|
|
165
|
+
AND st.text NOT LIKE 'CHECKPOINT%' AND st.text NOT LIKE 'WAITFOR %'
|
|
166
|
+
AND st.text NOT LIKE 'USE %' AND st.text NOT LIKE 'PRINT %'
|
|
167
|
+
AND st.text NOT LIKE 'RAISERROR%' AND st.text NOT LIKE 'THROW%'
|
|
168
|
+
-- Filter out DDL (focus on DML for access patterns)
|
|
169
|
+
AND st.text NOT LIKE 'CREATE %' AND st.text NOT LIKE 'ALTER %'
|
|
170
|
+
AND st.text NOT LIKE 'DROP %' AND st.text NOT LIKE 'TRUNCATE %'
|
|
171
|
+
AND st.text NOT LIKE 'GRANT %' AND st.text NOT LIKE 'REVOKE %' AND st.text NOT LIKE 'DENY %'
|
|
172
|
+
-- Filter out transaction control
|
|
173
|
+
AND st.text NOT LIKE 'BEGIN TRAN%' AND st.text NOT LIKE 'COMMIT%'
|
|
174
|
+
AND st.text NOT LIKE 'ROLLBACK%' AND st.text NOT LIKE 'SAVE TRAN%'
|
|
175
|
+
-- Filter out system stored procedures (sp_xxx pattern)
|
|
176
|
+
AND st.text NOT LIKE '%sp[_]who%' ESCAPE '['
|
|
177
|
+
AND st.text NOT LIKE '%sp[_]help%' ESCAPE '['
|
|
178
|
+
AND st.text NOT LIKE '%sp[_]executesql%' ESCAPE '['
|
|
179
|
+
|
|
180
|
+
UNION ALL
|
|
181
|
+
|
|
182
|
+
SELECT
|
|
183
|
+
'PROCEDURE' as source_type,
|
|
184
|
+
'PROCEDURE: ' + OBJECT_NAME(ps.object_id, ps.database_id) as query_pattern,
|
|
185
|
+
OBJECT_NAME(ps.object_id, ps.database_id) as procedure_name,
|
|
186
|
+
ps.execution_count as total_executions,
|
|
187
|
+
ROUND(ps.total_elapsed_time / 1000.0 / ps.execution_count, 2) as avg_latency_ms,
|
|
188
|
+
ROUND(ps.min_elapsed_time / 1000.0, 2) as min_latency_ms,
|
|
189
|
+
ROUND(ps.max_elapsed_time / 1000.0, 2) as max_latency_ms,
|
|
190
|
+
ROUND(ps.total_elapsed_time / 1000.0, 2) as total_time_ms,
|
|
191
|
+
NULL as avg_rows_returned,
|
|
192
|
+
ROUND(CAST(ps.total_logical_reads as FLOAT) / ps.execution_count, 2) as avg_logical_reads,
|
|
193
|
+
ROUND(CAST(ps.total_physical_reads as FLOAT) / ps.execution_count, 2) as avg_physical_reads,
|
|
194
|
+
ROUND(ps.total_worker_time / 1000.0 / ps.execution_count, 2) as avg_cpu_time_ms,
|
|
195
|
+
ps.cached_time as first_seen,
|
|
196
|
+
ps.last_execution_time as last_seen,
|
|
197
|
+
CASE
|
|
198
|
+
WHEN DATEDIFF(SECOND, ps.cached_time, ps.last_execution_time) > 0
|
|
199
|
+
THEN ROUND(
|
|
200
|
+
CAST(ps.execution_count as FLOAT) /
|
|
201
|
+
DATEDIFF(SECOND, ps.cached_time, ps.last_execution_time),
|
|
202
|
+
2
|
|
203
|
+
)
|
|
204
|
+
ELSE NULL
|
|
205
|
+
END as calculated_rps
|
|
206
|
+
FROM sys.dm_exec_procedure_stats ps
|
|
207
|
+
WHERE ps.database_id = DB_ID('{target_database}')
|
|
208
|
+
AND ps.execution_count > 0
|
|
209
|
+
AND ps.type IN ('P', 'PC')
|
|
210
|
+
AND OBJECT_NAME(ps.object_id, ps.database_id) IS NOT NULL
|
|
211
|
+
ORDER BY total_time_ms DESC;""",
|
|
212
|
+
'parameters': ['target_database'],
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class SQLServerPlugin(DatabasePlugin):
|
|
218
|
+
"""SQL Server-specific database analyzer plugin."""
|
|
219
|
+
|
|
220
|
+
def get_queries(self) -> Dict[str, Any]:
|
|
221
|
+
"""Get all SQL Server analysis queries."""
|
|
222
|
+
return _sqlserver_analysis_queries
|
|
223
|
+
|
|
224
|
+
def get_database_display_name(self) -> str:
|
|
225
|
+
"""Get the display name of the database type."""
|
|
226
|
+
return 'SQL Server'
|
|
227
|
+
|
|
228
|
+
def apply_result_limit(self, sql: str, max_results: int) -> str:
|
|
229
|
+
"""Apply result limit using SQL Server TOP syntax.
|
|
230
|
+
|
|
231
|
+
SQL Server uses TOP instead of LIMIT.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
sql: SQL query string
|
|
235
|
+
max_results: Maximum number of results
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
SQL query with TOP applied
|
|
239
|
+
"""
|
|
240
|
+
return re.sub(
|
|
241
|
+
r'\bSELECT\b', f'SELECT TOP {max_results}', sql, count=1, flags=re.IGNORECASE
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# write_queries_to_file is inherited from DatabasePlugin base class
|
|
245
|
+
|
|
246
|
+
# parse_results_from_file is inherited from DatabasePlugin base class
|
|
247
|
+
|
|
248
|
+
async def execute_managed_mode(self, connection_params: Dict[str, Any]) -> Dict[str, Any]:
|
|
249
|
+
"""Execute SQL Server analysis in managed mode.
|
|
250
|
+
|
|
251
|
+
Note: Managed mode not yet implemented for SQL Server.
|
|
252
|
+
"""
|
|
253
|
+
raise NotImplementedError(
|
|
254
|
+
'Managed mode is not yet implemented for SQL Server. Please use self_service mode.'
|
|
255
|
+
)
|