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/__init__.py +0 -0
- mcp_sqlserver/connection.py +146 -0
- mcp_sqlserver/server.py +462 -0
- mcp_sqlserver/tools.py +971 -0
- sqlserver_mcp_tools-0.1.0.dist-info/METADATA +183 -0
- sqlserver_mcp_tools-0.1.0.dist-info/RECORD +10 -0
- sqlserver_mcp_tools-0.1.0.dist-info/WHEEL +5 -0
- sqlserver_mcp_tools-0.1.0.dist-info/entry_points.txt +2 -0
- sqlserver_mcp_tools-0.1.0.dist-info/licenses/LICENSE +21 -0
- sqlserver_mcp_tools-0.1.0.dist-info/top_level.txt +1 -0
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()
|