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.
Files changed (27) hide show
  1. awslabs/__init__.py +17 -0
  2. awslabs/dynamodb_mcp_server/__init__.py +17 -0
  3. awslabs/dynamodb_mcp_server/cdk_generator/__init__.py +19 -0
  4. awslabs/dynamodb_mcp_server/cdk_generator/generator.py +276 -0
  5. awslabs/dynamodb_mcp_server/cdk_generator/models.py +521 -0
  6. awslabs/dynamodb_mcp_server/cdk_generator/templates/README.md +57 -0
  7. awslabs/dynamodb_mcp_server/cdk_generator/templates/stack.ts.j2 +70 -0
  8. awslabs/dynamodb_mcp_server/common.py +94 -0
  9. awslabs/dynamodb_mcp_server/db_analyzer/__init__.py +30 -0
  10. awslabs/dynamodb_mcp_server/db_analyzer/analyzer_utils.py +394 -0
  11. awslabs/dynamodb_mcp_server/db_analyzer/base_plugin.py +355 -0
  12. awslabs/dynamodb_mcp_server/db_analyzer/mysql.py +450 -0
  13. awslabs/dynamodb_mcp_server/db_analyzer/plugin_registry.py +73 -0
  14. awslabs/dynamodb_mcp_server/db_analyzer/postgresql.py +215 -0
  15. awslabs/dynamodb_mcp_server/db_analyzer/sqlserver.py +255 -0
  16. awslabs/dynamodb_mcp_server/markdown_formatter.py +513 -0
  17. awslabs/dynamodb_mcp_server/model_validation_utils.py +845 -0
  18. awslabs/dynamodb_mcp_server/prompts/dynamodb_architect.md +851 -0
  19. awslabs/dynamodb_mcp_server/prompts/json_generation_guide.md +185 -0
  20. awslabs/dynamodb_mcp_server/prompts/transform_model_validation_result.md +168 -0
  21. awslabs/dynamodb_mcp_server/server.py +524 -0
  22. awslabs_dynamodb_mcp_server-2.0.10.dist-info/METADATA +306 -0
  23. awslabs_dynamodb_mcp_server-2.0.10.dist-info/RECORD +27 -0
  24. awslabs_dynamodb_mcp_server-2.0.10.dist-info/WHEEL +4 -0
  25. awslabs_dynamodb_mcp_server-2.0.10.dist-info/entry_points.txt +2 -0
  26. awslabs_dynamodb_mcp_server-2.0.10.dist-info/licenses/LICENSE +175 -0
  27. 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
+ )