sqlserver-mcp-tools 0.1.0__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.
mcp_sqlserver/tools.py ADDED
@@ -0,0 +1,971 @@
1
+ import json
2
+ import re
3
+ from .connection import conectar_pyodbc, get_connection, get_connection_string
4
+
5
+ MAX_ROWS = 2000
6
+
7
+ _BANCO_PADRAO = "master"
8
+
9
+
10
+ def _descobrir_banco_padrao() -> str:
11
+ """Obtem o banco padrao a partir da string de conexao, sem falhar no
12
+ import caso a conexao ainda nao esteja configurada.
13
+
14
+ Permite que o servidor suba mesmo sem conexao definida (fail-lazy),
15
+ reportando o erro apenas quando uma ferramenta for de fato chamada.
16
+ """
17
+ try:
18
+ conn_str = get_connection_string()
19
+ except Exception:
20
+ return "master"
21
+ for parte in conn_str.split(";"):
22
+ if parte.strip().upper().startswith("DATABASE="):
23
+ return parte.split("=", 1)[1].strip()
24
+ return "master"
25
+
26
+
27
+ _BANCO_PADRAO = _descobrir_banco_padrao()
28
+
29
+
30
+ def _validar_identificador_sql(nome: str) -> bool:
31
+ return bool(re.match(
32
+ r'^\[?[a-zA-Z_][a-zA-Z0-9_$\s]*\]?'
33
+ r'(\.[a-zA-Z_][a-zA-Z0-9_$\s]*\]?)*$',
34
+ nome
35
+ ))
36
+
37
+
38
+ _PALAVRAS_PROIBIDAS = [
39
+ "INSERT", "UPDATE", "DELETE", "DROP", "TRUNCATE", "ALTER",
40
+ "CREATE", "EXEC", "EXECUTE", "MERGE", "GRANT", "REVOKE",
41
+ "BACKUP", "RESTORE", "DBCC",
42
+ ]
43
+
44
+
45
+ def _validar_sem_injecao(texto: str) -> bool:
46
+ if not texto:
47
+ return True
48
+ for char in [";", "--", "/*", "*/"]:
49
+ if char in texto:
50
+ return False
51
+ texto_upper = texto.upper()
52
+ for palavra in _PALAVRAS_PROIBIDAS:
53
+ if re.search(r'\b' + palavra + r'\b', texto_upper):
54
+ return False
55
+ return True
56
+
57
+
58
+ def _definir_banco(cursor, banco=None):
59
+ banco_alvo = banco if banco else _BANCO_PADRAO
60
+ cursor.execute(f"USE [{banco_alvo}]")
61
+
62
+
63
+ def _format_resultado(cursor) -> str:
64
+ colunas = [col[0] for col in cursor.description] if cursor.description else []
65
+ linhas = [dict(zip(colunas, row)) for row in cursor.fetchmany(MAX_ROWS)]
66
+ return json.dumps(linhas, default=str, ensure_ascii=False, indent=2)
67
+
68
+
69
+ def listar_bancos() -> str:
70
+ """Lista todos os bancos de dados acessiveis no servidor."""
71
+ sql = """
72
+ SELECT name, state_desc, create_date, compatibility_level
73
+ FROM sys.databases
74
+ ORDER BY name
75
+ """
76
+ conn = get_connection()
77
+ cursor = conn.cursor()
78
+ try:
79
+ cursor.execute(sql)
80
+ return _format_resultado(cursor)
81
+ except Exception as e:
82
+ return f"ERRO: {str(e)}"
83
+ finally:
84
+ cursor.close()
85
+
86
+
87
+ def consulta(sql: str, timeout_segundos: int = 120) -> str:
88
+ """Executa uma consulta SELECT (somente leitura).
89
+
90
+ Use notacao de tres niveis para acessar outros bancos:
91
+ banco.esquema.tabela
92
+
93
+ Args:
94
+ sql: Query SQL do tipo SELECT.
95
+ timeout_segundos: Tempo maximo de execucao em segundos (default: 120).
96
+ """
97
+ sql_upper = sql.strip().upper()
98
+ for palavra in _PALAVRAS_PROIBIDAS:
99
+ if sql_upper.startswith(palavra) or f"\n{palavra}" in sql_upper:
100
+ return f"OPERACAO BLOQUEADA: '{palavra}' nao permitido. Apenas SELECT."
101
+
102
+ if ";" in sql:
103
+ return "OPERACAO BLOQUEADA: Comandos encadeados (;) nao sao permitidos."
104
+
105
+ conn = get_connection()
106
+ conn.timeout = timeout_segundos
107
+ cursor = conn.cursor()
108
+ try:
109
+ cursor.execute(sql)
110
+ return _format_resultado(cursor)
111
+ except Exception as e:
112
+ return f"ERRO: {str(e)}"
113
+ finally:
114
+ cursor.close()
115
+
116
+
117
+ def listar_tabelas(banco: str = "") -> str:
118
+ """Lista todas as tabelas e views do banco de dados.
119
+
120
+ Args:
121
+ banco: Nome do banco de dados (opcional). Se nao informado,
122
+ usa o banco padrao da conexao.
123
+ """
124
+ sql = """
125
+ SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE
126
+ FROM INFORMATION_SCHEMA.TABLES
127
+ ORDER BY TABLE_SCHEMA, TABLE_NAME
128
+ """
129
+ conn = get_connection()
130
+ cursor = conn.cursor()
131
+ try:
132
+ _definir_banco(cursor, banco)
133
+ cursor.execute(sql)
134
+ return _format_resultado(cursor)
135
+ except Exception as e:
136
+ return f"ERRO: {str(e)}"
137
+ finally:
138
+ cursor.close()
139
+
140
+
141
+ def descrever_tabela(tabela: str, banco: str = "") -> str:
142
+ """Descreve a estrutura de uma tabela: colunas, tipos, nulabilidade e chaves.
143
+
144
+ Args:
145
+ tabela: Nome da tabela. Use 'schema.nome' ou apenas 'nome' (default: dbo).
146
+ banco: Nome do banco de dados (opcional).
147
+ """
148
+ schema = "dbo"
149
+ nome_tabela = tabela
150
+ if "." in tabela:
151
+ schema, nome_tabela = tabela.split(".", 1)
152
+
153
+ sql = """
154
+ SELECT
155
+ c.COLUMN_NAME,
156
+ c.DATA_TYPE,
157
+ c.CHARACTER_MAXIMUM_LENGTH,
158
+ c.IS_NULLABLE,
159
+ c.COLUMN_DEFAULT,
160
+ CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 'PK' ELSE '' END AS CHAVE_PRIMARIA
161
+ FROM INFORMATION_SCHEMA.COLUMNS c
162
+ LEFT JOIN (
163
+ SELECT ku.TABLE_SCHEMA, ku.TABLE_NAME, ku.COLUMN_NAME
164
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
165
+ JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku
166
+ ON tc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME
167
+ AND tc.TABLE_SCHEMA = ku.TABLE_SCHEMA
168
+ AND tc.TABLE_NAME = ku.TABLE_NAME
169
+ WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
170
+ ) pk ON c.TABLE_SCHEMA = pk.TABLE_SCHEMA
171
+ AND c.TABLE_NAME = pk.TABLE_NAME
172
+ AND c.COLUMN_NAME = pk.COLUMN_NAME
173
+ WHERE c.TABLE_SCHEMA = ? AND c.TABLE_NAME = ?
174
+ ORDER BY c.ORDINAL_POSITION
175
+ """
176
+ conn = get_connection()
177
+ cursor = conn.cursor()
178
+ try:
179
+ _definir_banco(cursor, banco)
180
+ cursor.execute(sql, (schema, nome_tabela))
181
+ resultado = _format_resultado(cursor)
182
+ if resultado == "[]":
183
+ return f"Tabela '{tabela}' nao encontrada."
184
+ return resultado
185
+ except Exception as e:
186
+ return f"ERRO: {str(e)}"
187
+ finally:
188
+ cursor.close()
189
+
190
+
191
+ def listar_indices(tabela: str, banco: str = "") -> str:
192
+ """Lista todos os indices de uma tabela.
193
+
194
+ Args:
195
+ tabela: Nome da tabela. Use 'schema.nome' ou apenas 'nome'.
196
+ banco: Nome do banco de dados (opcional).
197
+ """
198
+ schema = "dbo"
199
+ nome_tabela = tabela
200
+ if "." in tabela:
201
+ schema, nome_tabela = tabela.split(".", 1)
202
+
203
+ sql = """
204
+ SELECT
205
+ i.name AS NOME_INDICE,
206
+ i.type_desc AS TIPO,
207
+ i.is_unique AS UNICO,
208
+ i.is_primary_key AS PK,
209
+ STUFF((
210
+ SELECT ', ' + c2.name
211
+ FROM sys.index_columns ic2
212
+ JOIN sys.columns c2
213
+ ON ic2.object_id = c2.object_id
214
+ AND ic2.column_id = c2.column_id
215
+ WHERE ic2.object_id = i.object_id
216
+ AND ic2.index_id = i.index_id
217
+ ORDER BY ic2.key_ordinal
218
+ FOR XML PATH('')
219
+ ), 1, 2, '') AS COLUNAS,
220
+ i.filter_definition AS FILTRO
221
+ FROM sys.indexes i
222
+ JOIN sys.tables t ON i.object_id = t.object_id
223
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
224
+ WHERE s.name = ? AND t.name = ?
225
+ ORDER BY i.is_primary_key DESC, i.name
226
+ """
227
+ conn = get_connection()
228
+ cursor = conn.cursor()
229
+ try:
230
+ _definir_banco(cursor, banco)
231
+ cursor.execute(sql, (schema, nome_tabela))
232
+ return _format_resultado(cursor)
233
+ except Exception as e:
234
+ return f"ERRO: {str(e)}"
235
+ finally:
236
+ cursor.close()
237
+
238
+
239
+ def listar_procedures(banco: str = "") -> str:
240
+ """Lista todas as stored procedures do banco.
241
+
242
+ Args:
243
+ banco: Nome do banco de dados (opcional).
244
+ """
245
+ sql = """
246
+ SELECT
247
+ s.name AS SCHEMA_NAME,
248
+ p.name AS PROCEDURE_NAME,
249
+ p.create_date AS CRIADA_EM,
250
+ p.modify_date AS MODIFICADA_EM
251
+ FROM sys.procedures p
252
+ JOIN sys.schemas s ON p.schema_id = s.schema_id
253
+ ORDER BY s.name, p.name
254
+ """
255
+ conn = get_connection()
256
+ cursor = conn.cursor()
257
+ try:
258
+ _definir_banco(cursor, banco)
259
+ cursor.execute(sql)
260
+ return _format_resultado(cursor)
261
+ except Exception as e:
262
+ return f"ERRO: {str(e)}"
263
+ finally:
264
+ cursor.close()
265
+
266
+
267
+ def ler_procedure(nome: str, banco: str = "") -> str:
268
+ """Retorna o codigo-fonte de uma stored procedure.
269
+
270
+ Args:
271
+ nome: Nome da procedure. Use 'schema.nome' ou apenas 'nome'.
272
+ banco: Nome do banco de dados (opcional).
273
+ """
274
+ schema = "dbo"
275
+ nome_proc = nome
276
+ if "." in nome:
277
+ schema, nome_proc = nome.split(".", 1)
278
+
279
+ sql = """
280
+ SELECT OBJECT_DEFINITION(OBJECT_ID(? + '.' + ?)) AS CODIGO_FONTE
281
+ """
282
+ conn = get_connection()
283
+ cursor = conn.cursor()
284
+ try:
285
+ _definir_banco(cursor, banco)
286
+ cursor.execute(sql, (schema, nome_proc))
287
+ row = cursor.fetchone()
288
+ if row is None or row[0] is None:
289
+ return f"Procedure '{nome}' nao encontrada."
290
+ return row[0]
291
+ except Exception as e:
292
+ return f"ERRO: {str(e)}"
293
+ finally:
294
+ cursor.close()
295
+
296
+
297
+ def _extrair_erros_messages(cursor) -> list[str]:
298
+ """Extrai mensagens de erro do SQL Server a partir de cursor.messages.
299
+
300
+ O pyodbc armazena em cursor.messages todas as mensagens retornadas pelo
301
+ servidor, incluindo erros de runtime dentro de procedures que nao acionam
302
+ XACT_ABORT (ex: erros de collation, conversao implicita, etc.).
303
+ """
304
+ erros = []
305
+ for msg in cursor.messages:
306
+ if isinstance(msg, (list, tuple)) and len(msg) >= 2:
307
+ texto = str(msg[1])
308
+ else:
309
+ texto = str(msg)
310
+ if "[SQL Server]" not in texto:
311
+ continue
312
+ texto_limpo = texto.strip()
313
+ if "row" in texto_limpo.lower():
314
+ continue
315
+ erros.append(texto_limpo)
316
+ return erros
317
+
318
+
319
+ def executar_procedure(nome: str, banco: str = "", parametros: str = "", timeout_segundos: int = 120) -> str:
320
+ """Executa uma stored procedure. Requer confirmacao explicita do usuario.
321
+
322
+ IMPORTANTE: O usuario deve ser questionado antes de executar qualquer procedure.
323
+ Apenas procedures de leitura (que nao modificam dados) devem ser executadas
324
+ sem alerta adicional.
325
+
326
+ Args:
327
+ nome: Nome da procedure. Use 'schema.nome' ou apenas 'nome'.
328
+ banco: Nome do banco de dados (opcional).
329
+ parametros: Parametros no formato SQL: 'valor1, valor2, @param=valor'.
330
+ timeout_segundos: Tempo maximo de execucao em segundos (default: 120).
331
+ """
332
+ if not _validar_identificador_sql(nome):
333
+ return "OPERACAO BLOQUEADA: Nome de procedure invalido. Use apenas identificadores SQL validos (schema.procedure)."
334
+
335
+ if not _validar_sem_injecao(parametros):
336
+ return "OPERACAO BLOQUEADA: Parametros contem comandos SQL nao permitidos."
337
+
338
+ if parametros:
339
+ sql = f"EXEC {nome} {parametros}"
340
+ else:
341
+ sql = f"EXEC {nome}"
342
+
343
+ conn = get_connection()
344
+ conn.timeout = timeout_segundos
345
+ cursor = conn.cursor()
346
+ try:
347
+ if banco:
348
+ _definir_banco(cursor, banco)
349
+ cursor.execute(sql)
350
+ erros_sql = _extrair_erros_messages(cursor)
351
+ if erros_sql:
352
+ try:
353
+ conn.rollback()
354
+ except Exception:
355
+ pass
356
+ return "ERRO: " + "; ".join(erros_sql)
357
+
358
+ resultados = []
359
+ tem_resultados = False
360
+ while True:
361
+ if cursor.description:
362
+ tem_resultados = True
363
+ resultados.append(_format_resultado(cursor))
364
+ if not cursor.nextset():
365
+ break
366
+
367
+ conn.commit()
368
+
369
+ if tem_resultados:
370
+ if len(resultados) == 1:
371
+ return "RESULTADO:\n" + resultados[0]
372
+ else:
373
+ return "RESULTADOS:\n\n" + "\n---\n".join(resultados)
374
+ else:
375
+ return f"Procedure '{nome}' executada com sucesso (sem resultados)."
376
+ except Exception as e:
377
+ try:
378
+ conn.rollback()
379
+ except Exception:
380
+ pass
381
+ return f"ERRO: {str(e)}"
382
+ finally:
383
+ cursor.close()
384
+
385
+
386
+ def plano_execucao(sql: str) -> str:
387
+ """Exibe o plano de execucao estimado para uma query, sem executa-la.
388
+
389
+ Args:
390
+ sql: Query SQL para analisar o plano de execucao.
391
+ """
392
+ conn = get_connection()
393
+ cursor = conn.cursor()
394
+ try:
395
+ cursor.execute("SET SHOWPLAN_XML ON")
396
+ cursor.execute(sql)
397
+ row = cursor.fetchone()
398
+ cursor.execute("SET SHOWPLAN_XML OFF")
399
+ if row:
400
+ return row[0]
401
+ return "Nenhum plano de execucao retornado."
402
+ except Exception as e:
403
+ try:
404
+ cursor.execute("SET SHOWPLAN_XML OFF")
405
+ except Exception:
406
+ pass
407
+ return f"ERRO: {str(e)}"
408
+ finally:
409
+ cursor.close()
410
+
411
+
412
+ def listar_constraints(tabela: str, banco: str = "") -> str:
413
+ """Lista todas as constraints de uma tabela (PK, FK, UNIQUE, CHECK, DEFAULT).
414
+
415
+ Args:
416
+ tabela: Nome da tabela. Use 'schema.nome' ou apenas 'nome'.
417
+ banco: Nome do banco de dados (opcional).
418
+ """
419
+ schema = "dbo"
420
+ nome_tabela = tabela
421
+ if "." in tabela:
422
+ schema, nome_tabela = tabela.split(".", 1)
423
+
424
+ sql = """
425
+ SELECT
426
+ tc.CONSTRAINT_NAME,
427
+ tc.CONSTRAINT_TYPE,
428
+ STRING_AGG(kcu.COLUMN_NAME, ', ') AS COLUNAS,
429
+ rc.UNIQUE_CONSTRAINT_NAME AS REFERENCIA_FK,
430
+ cc.CHECK_CLAUSE AS EXPRESSAO_CHECK
431
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
432
+ LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
433
+ ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
434
+ AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA
435
+ AND tc.TABLE_NAME = kcu.TABLE_NAME
436
+ LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
437
+ ON tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
438
+ LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc
439
+ ON tc.CONSTRAINT_NAME = cc.CONSTRAINT_NAME
440
+ WHERE tc.TABLE_SCHEMA = ? AND tc.TABLE_NAME = ?
441
+ GROUP BY tc.CONSTRAINT_NAME, tc.CONSTRAINT_TYPE,
442
+ rc.UNIQUE_CONSTRAINT_NAME, cc.CHECK_CLAUSE
443
+ ORDER BY tc.CONSTRAINT_TYPE, tc.CONSTRAINT_NAME
444
+ """
445
+ conn = get_connection()
446
+ cursor = conn.cursor()
447
+ try:
448
+ _definir_banco(cursor, banco)
449
+ cursor.execute(sql, (schema, nome_tabela))
450
+ return _format_resultado(cursor)
451
+ except Exception as e:
452
+ return f"ERRO: {str(e)}"
453
+ finally:
454
+ cursor.close()
455
+
456
+
457
+ def estatisticas_tabela(tabela: str, banco: str = "") -> str:
458
+ """Exibe estatisticas de uma tabela: numero estimado de linhas,
459
+ tamanho em disco e uso de dados.
460
+
461
+ Args:
462
+ tabela: Nome da tabela. Use 'schema.nome' ou apenas 'nome'.
463
+ banco: Nome do banco de dados (opcional).
464
+ """
465
+ schema = "dbo"
466
+ nome_tabela = tabela
467
+ if "." in tabela:
468
+ schema, nome_tabela = tabela.split(".", 1)
469
+
470
+ sql = """
471
+ SELECT
472
+ t.name AS TABELA,
473
+ p.rows AS LINHAS_ESTIMADAS,
474
+ SUM(a.total_pages) * 8 AS TAMANHO_KB,
475
+ SUM(a.used_pages) * 8 AS USADO_KB,
476
+ SUM(a.data_pages) * 8 AS DADOS_KB
477
+ FROM sys.tables t
478
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
479
+ JOIN sys.indexes i ON t.object_id = i.object_id
480
+ JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
481
+ JOIN sys.allocation_units a ON p.partition_id = a.container_id
482
+ WHERE s.name = ? AND t.name = ?
483
+ GROUP BY t.name, p.rows
484
+ """
485
+ conn = get_connection()
486
+ cursor = conn.cursor()
487
+ try:
488
+ _definir_banco(cursor, banco)
489
+ cursor.execute(sql, (schema, nome_tabela))
490
+ return _format_resultado(cursor)
491
+ except Exception as e:
492
+ return f"ERRO: {str(e)}"
493
+ finally:
494
+ cursor.close()
495
+
496
+
497
+ def listar_funcoes(banco: str = "") -> str:
498
+ """Lista todas as funcoes (scalar e table-valued) do banco.
499
+
500
+ Args:
501
+ banco: Nome do banco de dados (opcional).
502
+ """
503
+ sql = """
504
+ SELECT
505
+ s.name AS SCHEMA_NAME,
506
+ o.name AS FUNCTION_NAME,
507
+ o.type_desc AS TIPO,
508
+ o.create_date AS CRIADA_EM,
509
+ o.modify_date AS MODIFICADA_EM
510
+ FROM sys.objects o
511
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
512
+ WHERE o.type IN ('FN', 'IF', 'TF', 'FS', 'FT')
513
+ ORDER BY s.name, o.name
514
+ """
515
+ conn = get_connection()
516
+ cursor = conn.cursor()
517
+ try:
518
+ _definir_banco(cursor, banco)
519
+ cursor.execute(sql)
520
+ return _format_resultado(cursor)
521
+ except Exception as e:
522
+ return f"ERRO: {str(e)}"
523
+ finally:
524
+ cursor.close()
525
+
526
+
527
+ def status_jobs(nomes: str) -> str:
528
+ """Retorna o status da ultima execucao de jobs do SQL Server Agent.
529
+
530
+ Args:
531
+ nomes: Lista de nomes de jobs separados por virgula.
532
+ Exemplo: 'Job A, Job B, Job C'
533
+ """
534
+ import json as _json
535
+
536
+ lista_nomes = [n.strip() for n in nomes.split(",") if n.strip()]
537
+ if not lista_nomes:
538
+ return "[]"
539
+
540
+ placeholders = ", ".join(["?" for _ in lista_nomes])
541
+
542
+ sql = f"""
543
+ WITH ultima_atividade AS (
544
+ SELECT
545
+ job_id,
546
+ start_execution_date,
547
+ stop_execution_date,
548
+ ROW_NUMBER() OVER (
549
+ PARTITION BY job_id
550
+ ORDER BY
551
+ CASE WHEN stop_execution_date IS NULL THEN 0 ELSE 1 END,
552
+ start_execution_date DESC
553
+ ) AS rn
554
+ FROM msdb.dbo.sysjobactivity
555
+ WHERE start_execution_date IS NOT NULL
556
+ AND session_id = (SELECT MAX(session_id) FROM msdb.dbo.sysjobactivity)
557
+ ),
558
+ ultimo_historico_resumo AS (
559
+ SELECT
560
+ job_id,
561
+ instance_id,
562
+ run_status,
563
+ message,
564
+ ROW_NUMBER() OVER (
565
+ PARTITION BY job_id
566
+ ORDER BY instance_id DESC
567
+ ) AS rn
568
+ FROM msdb.dbo.sysjobhistory
569
+ WHERE step_id = 0
570
+ ),
571
+ ultimo_historico_step AS (
572
+ SELECT
573
+ job_id,
574
+ step_id,
575
+ step_name,
576
+ message,
577
+ ROW_NUMBER() OVER (
578
+ PARTITION BY job_id
579
+ ORDER BY instance_id DESC, step_id DESC
580
+ ) AS rn
581
+ FROM msdb.dbo.sysjobhistory
582
+ WHERE step_id > 0
583
+ )
584
+ SELECT
585
+ j.name AS nome,
586
+ FORMAT(ja.start_execution_date, 'yyyy-MM-ddTHH:mm:ss') AS inicio,
587
+ FORMAT(ja.stop_execution_date, 'yyyy-MM-ddTHH:mm:ss') AS fim,
588
+ DATEDIFF(MINUTE, ja.start_execution_date, ja.stop_execution_date) AS duracao_minutos,
589
+ CASE
590
+ WHEN ja.stop_execution_date IS NULL AND ja.start_execution_date IS NOT NULL THEN 'EM EXECUCAO'
591
+ ELSE
592
+ CASE jh.run_status
593
+ WHEN 0 THEN 'FALHA'
594
+ WHEN 1 THEN 'SUCESSO'
595
+ WHEN 2 THEN 'TENTATIVA'
596
+ WHEN 3 THEN 'CANCELADO'
597
+ ELSE 'DESCONHECIDO'
598
+ END
599
+ END AS resultado,
600
+ CASE
601
+ WHEN ja.stop_execution_date IS NULL AND ja.start_execution_date IS NOT NULL THEN 1
602
+ ELSE 0
603
+ END AS em_execucao,
604
+ CASE
605
+ WHEN ja.stop_execution_date IS NULL AND ja.start_execution_date IS NOT NULL THEN
606
+ 'Em execucao. Ultimo step registrado: ' +
607
+ CAST(COALESCE(jhs.step_id, 0) AS VARCHAR) +
608
+ ' (' + COALESCE(jhs.step_name, 'desconhecido') + ')'
609
+ ELSE
610
+ COALESCE(jh.message, 'Sem historico recente')
611
+ END AS mensagem
612
+ FROM msdb.dbo.sysjobs j
613
+ LEFT JOIN ultima_atividade ja
614
+ ON j.job_id = ja.job_id
615
+ AND ja.rn = 1
616
+ LEFT JOIN ultimo_historico_resumo jh
617
+ ON j.job_id = jh.job_id
618
+ AND jh.rn = 1
619
+ LEFT JOIN ultimo_historico_step jhs
620
+ ON j.job_id = jhs.job_id
621
+ AND jhs.rn = 1
622
+ WHERE j.name IN ({placeholders})
623
+ ORDER BY ja.start_execution_date
624
+ """
625
+
626
+ conn = get_connection()
627
+ cursor = conn.cursor()
628
+ try:
629
+ cursor.execute(sql, lista_nomes)
630
+ return _format_resultado(cursor)
631
+ except Exception as e:
632
+ return f"ERRO: {str(e)}"
633
+ finally:
634
+ cursor.close()
635
+
636
+
637
+ def ler_funcao(nome: str, banco: str = "") -> str:
638
+ """Retorna o codigo-fonte de uma funcao.
639
+
640
+ Args:
641
+ nome: Nome da funcao. Use 'schema.nome' ou apenas 'nome'.
642
+ banco: Nome do banco de dados (opcional).
643
+ """
644
+ schema = "dbo"
645
+ nome_func = nome
646
+ if "." in nome:
647
+ schema, nome_func = nome.split(".", 1)
648
+
649
+ sql = """
650
+ SELECT OBJECT_DEFINITION(OBJECT_ID(? + '.' + ?)) AS CODIGO_FONTE
651
+ """
652
+ conn = get_connection()
653
+ cursor = conn.cursor()
654
+ try:
655
+ _definir_banco(cursor, banco)
656
+ cursor.execute(sql, (schema, nome_func))
657
+ row = cursor.fetchone()
658
+ if row is None or row[0] is None:
659
+ return f"Funcao '{nome}' nao encontrada."
660
+ return row[0]
661
+ except Exception as e:
662
+ return f"ERRO: {str(e)}"
663
+ finally:
664
+ cursor.close()
665
+
666
+
667
+ def criar_indice(comando: str, banco: str = "") -> str:
668
+ """Executa um comando CREATE INDEX no SQL Server.
669
+
670
+ ATENCAO: O usuario DEVE ser questionado e confirmar antes de cada execucao.
671
+ Nunca crie indices sem permissao explicita.
672
+ Usa uma conexao dedicada com timeout de 10 minutos para indices grandes.
673
+
674
+ Args:
675
+ comando: Comando SQL CREATE INDEX completo.
676
+ banco: Nome do banco de dados onde criar o indice (opcional).
677
+ """
678
+ comando_upper = comando.strip().upper()
679
+
680
+ if "CREATE" not in comando_upper:
681
+ return "OPERACAO BLOQUEADA: Apenas comandos CREATE INDEX sao permitidos."
682
+
683
+ if "INDEX" not in comando_upper:
684
+ return "OPERACAO BLOQUEADA: Apenas comandos CREATE INDEX sao permitidos."
685
+
686
+ palavras_bloqueadas_ddl = [
687
+ "INSERT", "UPDATE", "DELETE", "MERGE", "TRUNCATE",
688
+ "EXEC", "EXECUTE", "GRANT", "REVOKE", "BACKUP", "RESTORE",
689
+ "DROP", "ALTER", "DBCC",
690
+ ]
691
+ for palavra in palavras_bloqueadas_ddl:
692
+ if re.search(r'\b' + palavra + r'\b', comando_upper):
693
+ return f"OPERACAO BLOQUEADA: '{palavra}' nao permitido em comandos de indice."
694
+
695
+ for char in [";", "--", "/*", "*/"]:
696
+ if char in comando:
697
+ return "OPERACAO BLOQUEADA: Caracteres especiais nao permitidos."
698
+
699
+ conn_str = get_connection_string()
700
+ conn = None
701
+ cursor = None
702
+ try:
703
+ conn = conectar_pyodbc(conn_str, timeout=600)
704
+ conn.timeout = 600
705
+ conn.autocommit = False
706
+ cursor = conn.cursor()
707
+ cursor.execute("SET XACT_ABORT ON")
708
+ if banco:
709
+ cursor.execute(f"USE [{banco}]")
710
+ cursor.execute(comando)
711
+ erros_sql = _extrair_erros_messages(cursor)
712
+ if erros_sql:
713
+ try:
714
+ conn.rollback()
715
+ except Exception:
716
+ pass
717
+ return "ERRO: " + "; ".join(erros_sql)
718
+ conn.commit()
719
+ return "Comando executado com sucesso."
720
+ except Exception as e:
721
+ if conn:
722
+ try:
723
+ conn.rollback()
724
+ except Exception:
725
+ pass
726
+ return f"ERRO: {str(e)}"
727
+ finally:
728
+ if cursor:
729
+ cursor.close()
730
+ if conn:
731
+ conn.close()
732
+
733
+
734
+ def alterar_procedure(nome: str, script: str, backup_arquivo: str, banco: str = "") -> str:
735
+ """Altera uma stored procedure com backup previo do codigo original.
736
+
737
+ ATENCAO: O usuario DEVE ser questionado e confirmar antes de cada execucao.
738
+ Nunca execute ALTER PROCEDURE sem permissao explicita.
739
+
740
+ Fluxo:
741
+ 1. Le o codigo-fonte atual da procedure
742
+ 2. Salva o backup em 'backup_arquivo'
743
+ 3. Verifica se o backup foi salvo corretamente
744
+ 4. Executa o ALTER PROCEDURE
745
+ 5. Retorna status da operacao
746
+
747
+ Args:
748
+ nome: Nome da procedure. Use 'schema.nome' ou apenas 'nome'.
749
+ script: Script ALTER PROCEDURE completo.
750
+ backup_arquivo: Caminho absoluto do arquivo onde sera salvo o backup.
751
+ banco: Nome do banco de dados (opcional).
752
+ """
753
+ import os as _os
754
+ from datetime import datetime as _dt
755
+
756
+ schema = "dbo"
757
+ nome_proc = nome
758
+ if "." in nome:
759
+ schema, nome_proc = nome.split(".", 1)
760
+
761
+ # 1. Valida identificador
762
+ if not _validar_identificador_sql(nome):
763
+ return "OPERACAO BLOQUEADA: Nome de procedure invalido."
764
+
765
+ # 2. Le codigo atual
766
+ sql_ler = """
767
+ SELECT OBJECT_DEFINITION(OBJECT_ID(? + '.' + ?)) AS CODIGO_FONTE
768
+ """
769
+ conn = get_connection()
770
+ cursor = conn.cursor()
771
+ try:
772
+ _definir_banco(cursor, banco)
773
+ cursor.execute(sql_ler, (schema, nome_proc))
774
+ row = cursor.fetchone()
775
+ if row is None or row[0] is None:
776
+ return f"ERRO: Procedure '{nome}' nao encontrada. Backup abortado."
777
+ codigo_original = row[0]
778
+ except Exception as e:
779
+ return f"ERRO ao ler procedure para backup: {str(e)}"
780
+ finally:
781
+ cursor.close()
782
+
783
+ # 3. Salva backup
784
+ try:
785
+ _os.makedirs(_os.path.dirname(backup_arquivo), exist_ok=True)
786
+ with open(backup_arquivo, "w", encoding="utf-8") as f:
787
+ f.write(f"-- Backup automatico de {schema}.{nome_proc}\n")
788
+ f.write(f"-- Data: {_dt.now().isoformat()}\n")
789
+ f.write(f"-- Banco: {banco or _BANCO_PADRAO}\n")
790
+ f.write("-- ============================================================\n\n")
791
+ f.write(codigo_original)
792
+ except Exception as e:
793
+ return f"ERRO ao salvar backup em '{backup_arquivo}': {str(e)}"
794
+
795
+ # 4. Verifica backup (existe e tem tamanho minimo)
796
+ try:
797
+ tamanho_backup = _os.path.getsize(backup_arquivo)
798
+ if tamanho_backup < 50:
799
+ return f"ERRO: Verificacao de backup falhou. O arquivo '{backup_arquivo}' tem apenas {tamanho_backup} bytes."
800
+ except Exception as e:
801
+ return f"ERRO ao verificar backup: {str(e)}"
802
+
803
+ # 5. Valida script ALTER (apenas verifica se comeca com ALTER PROCEDURE)
804
+ script_upper = script.strip().upper()
805
+ if not script_upper.startswith("ALTER PROCEDURE") and not script_upper.startswith("ALTER PROC"):
806
+ return "OPERACAO BLOQUEADA: O script deve comecar com ALTER PROCEDURE."
807
+
808
+ # 6. Executa ALTER
809
+ cursor = conn.cursor()
810
+ try:
811
+ _definir_banco(cursor, banco)
812
+ cursor.execute(script)
813
+ erros_sql = _extrair_erros_messages(cursor)
814
+ if erros_sql:
815
+ try:
816
+ conn.rollback()
817
+ except Exception:
818
+ pass
819
+ return (
820
+ f"ERRO ao executar ALTER: {'; '.join(erros_sql)}\n\n"
821
+ f"Backup salvo em: {backup_arquivo} (use para restaurar)"
822
+ )
823
+ conn.commit()
824
+ return (
825
+ f"SUCESSO: Procedure '{schema}.{nome_proc}' alterada com sucesso.\n"
826
+ f"Backup salvo em: {backup_arquivo} "
827
+ f"({len(codigo_original)} caracteres)"
828
+ )
829
+ except Exception as e:
830
+ try:
831
+ conn.rollback()
832
+ except Exception:
833
+ pass
834
+ return f"ERRO ao executar ALTER: {str(e)}\n\nBackup salvo em: {backup_arquivo} (use para restaurar)"
835
+ finally:
836
+ cursor.close()
837
+
838
+
839
+ def executar_update(sql: str) -> str:
840
+ """Executa um comando UPDATE no SQL Server.
841
+
842
+ ATENCAO: O usuario DEVE ser questionado e confirmar antes de cada execucao.
843
+ Nunca execute updates sem permissao explicita.
844
+
845
+ Args:
846
+ sql: Comando SQL UPDATE completo. Deve incluir clausula WHERE.
847
+ """
848
+ sql_upper = sql.strip().upper()
849
+
850
+ if not sql_upper.startswith("UPDATE"):
851
+ return "OPERACAO BLOQUEADA: Apenas comandos UPDATE sao permitidos."
852
+
853
+ for palavra in _PALAVRAS_PROIBIDAS:
854
+ if palavra == "UPDATE":
855
+ continue
856
+ if re.search(r'\b' + palavra + r'\b', sql_upper):
857
+ return f"OPERACAO BLOQUEADA: '{palavra}' nao permitido em comandos UPDATE."
858
+
859
+ if ";" in sql:
860
+ return "OPERACAO BLOQUEADA: Comandos encadeados (;) nao sao permitidos."
861
+
862
+ if "--" in sql or "/*" in sql:
863
+ return "OPERACAO BLOQUEADA: Comentarios SQL nao sao permitidos em comandos UPDATE."
864
+
865
+ if "WHERE" not in sql_upper:
866
+ return "OPERACAO BLOQUEADA: UPDATE sem clausula WHERE. Forneca uma condicao WHERE para evitar alteracoes em massa."
867
+
868
+ conn = get_connection()
869
+ cursor = conn.cursor()
870
+ try:
871
+ cursor.execute(sql)
872
+ erros_sql = _extrair_erros_messages(cursor)
873
+ if erros_sql:
874
+ try:
875
+ conn.rollback()
876
+ except Exception:
877
+ pass
878
+ return "ERRO: " + "; ".join(erros_sql)
879
+ conn.commit()
880
+ linhas_afetadas = cursor.rowcount
881
+ return f"UPDATE executado com sucesso. Linhas afetadas: {linhas_afetadas}"
882
+ except Exception as e:
883
+ try:
884
+ conn.rollback()
885
+ except Exception:
886
+ pass
887
+ return f"ERRO: {str(e)}"
888
+ finally:
889
+ cursor.close()
890
+
891
+
892
+ def executar_ddl(sql: str, backup_arquivo: str, banco: str = "", timeout_segundos: int = 600) -> str:
893
+ """Executa um comando DDL (ALTER TABLE, ALTER VIEW, DROP, CREATE, sp_update_jobstep, etc.)
894
+ com backup obrigatorio do script de reversao.
895
+
896
+ ATENCAO: O usuario DEVE ser questionado e confirmar antes de cada execucao.
897
+ Nunca execute DDL sem permissao explicita.
898
+
899
+ Args:
900
+ sql: Comando SQL DDL completo.
901
+ backup_arquivo: Caminho absoluto do arquivo onde sera salvo o script de reversao.
902
+ banco: Nome do banco de dados (opcional).
903
+ timeout_segundos: Tempo maximo de execucao em segundos (default: 600).
904
+ """
905
+ import os as _os
906
+ from datetime import datetime as _dt
907
+
908
+ # 1. Salva o backup (script de reversao fornecido pelo chamador)
909
+ if not backup_arquivo:
910
+ return "ERRO: Parametro 'backup_arquivo' e obrigatorio. Forneca o caminho para salvar o script de reversao."
911
+
912
+ try:
913
+ _os.makedirs(_os.path.dirname(backup_arquivo), exist_ok=True)
914
+ with open(backup_arquivo, "w", encoding="utf-8") as f:
915
+ f.write(f"-- Script de reversao\n")
916
+ f.write(f"-- Data: {_dt.now().isoformat()}\n")
917
+ f.write(f"-- Banco: {banco or _BANCO_PADRAO}\n")
918
+ f.write("-- ATENCAO: Execute este script para desfazer a operacao.\n")
919
+ f.write("-- ============================================================\n\n")
920
+ f.write(sql)
921
+ except Exception as e:
922
+ return f"ERRO ao salvar backup em '{backup_arquivo}': {str(e)}"
923
+
924
+ # 2. Verifica se o backup foi salvo corretamente
925
+ try:
926
+ tamanho_backup = _os.path.getsize(backup_arquivo)
927
+ if tamanho_backup < 30:
928
+ return f"ERRO: Verificacao de backup falhou. O arquivo '{backup_arquivo}' tem apenas {tamanho_backup} bytes."
929
+ except Exception as e:
930
+ return f"ERRO ao verificar backup: {str(e)}"
931
+
932
+ # 3. Executa o DDL com conexao dedicada
933
+ conn_str = get_connection_string()
934
+ conn = None
935
+ cursor = None
936
+ try:
937
+ conn = conectar_pyodbc(conn_str, timeout=timeout_segundos)
938
+ conn.timeout = timeout_segundos
939
+ conn.autocommit = False
940
+ cursor = conn.cursor()
941
+ cursor.execute("SET XACT_ABORT ON")
942
+ if banco:
943
+ cursor.execute(f"USE [{banco}]")
944
+ cursor.execute(sql)
945
+ erros_sql = _extrair_erros_messages(cursor)
946
+ if erros_sql:
947
+ try:
948
+ conn.rollback()
949
+ except Exception:
950
+ pass
951
+ return (
952
+ f"ERRO: {'; '.join(erros_sql)}\n"
953
+ f"Backup salvo em: {backup_arquivo} ({tamanho_backup} bytes)"
954
+ )
955
+ conn.commit()
956
+ return (
957
+ f"SUCESSO: Comando DDL executado com sucesso.\n"
958
+ f"Backup salvo em: {backup_arquivo} ({tamanho_backup} bytes)"
959
+ )
960
+ except Exception as e:
961
+ if conn:
962
+ try:
963
+ conn.rollback()
964
+ except Exception:
965
+ pass
966
+ return f"ERRO: {str(e)}\n\nBackup salvo em: {backup_arquivo} (use para reverter)"
967
+ finally:
968
+ if cursor:
969
+ cursor.close()
970
+ if conn:
971
+ conn.close()