baucli 1.0.1__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.
- baucli/__init__.py +8 -0
- baucli/apex_rebuild.py +1458 -0
- baucli/apexlang.py +317 -0
- baucli/api.py +914 -0
- baucli/cli.py +7754 -0
- baucli/config.py +244 -0
- baucli/db/__init__.py +117 -0
- baucli/db/mssql.py +443 -0
- baucli/db/mysql.py +388 -0
- baucli/db/oracle.py +1294 -0
- baucli/db/postgresql.py +482 -0
- baucli/docmig.py +149 -0
- baucli/git.py +214 -0
- baucli/legacy/__init__.py +9 -0
- baucli/legacy/delphi.py +365 -0
- baucli/mcp_server.py +657 -0
- baucli/project.py +360 -0
- baucli/rag_embed.py +26 -0
- baucli/sample_scan.py +105 -0
- baucli/scope_scan.py +272 -0
- baucli/screenshot.py +316 -0
- baucli/seed_check.py +190 -0
- baucli/stress.py +409 -0
- baucli/sync.py +846 -0
- baucli-1.0.1.dist-info/METADATA +179 -0
- baucli-1.0.1.dist-info/RECORD +29 -0
- baucli-1.0.1.dist-info/WHEEL +5 -0
- baucli-1.0.1.dist-info/entry_points.txt +2 -0
- baucli-1.0.1.dist-info/top_level.txt +1 -0
baucli/db/mysql.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""BAUCLI — MySQL database extractor.
|
|
2
|
+
|
|
3
|
+
Uses mysql-connector-python.
|
|
4
|
+
Extracts:
|
|
5
|
+
- Functions from INFORMATION_SCHEMA.ROUTINES + SHOW CREATE FUNCTION
|
|
6
|
+
- Procedures from INFORMATION_SCHEMA.ROUTINES + SHOW CREATE PROCEDURE
|
|
7
|
+
- Triggers from INFORMATION_SCHEMA.TRIGGERS + SHOW CREATE TRIGGER
|
|
8
|
+
- Views from INFORMATION_SCHEMA.VIEWS + SHOW CREATE VIEW
|
|
9
|
+
- Table metadata from INFORMATION_SCHEMA.COLUMNS, TABLE_CONSTRAINTS, etc.
|
|
10
|
+
|
|
11
|
+
In MySQL, schema = database (they are the same concept).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from baucli.db import BaseExtractor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MySQLExtractor(BaseExtractor):
|
|
19
|
+
"""MySQL database extractor using mysql-connector-python."""
|
|
20
|
+
|
|
21
|
+
engine = "MYSQL"
|
|
22
|
+
|
|
23
|
+
def connect(self):
|
|
24
|
+
import mysql.connector
|
|
25
|
+
from baucli import __version__
|
|
26
|
+
self._conn = mysql.connector.connect(
|
|
27
|
+
host=self.host,
|
|
28
|
+
port=self.port,
|
|
29
|
+
database=self.database,
|
|
30
|
+
user=self.user,
|
|
31
|
+
password=self.password,
|
|
32
|
+
charset='utf8mb4',
|
|
33
|
+
connection_timeout=30,
|
|
34
|
+
)
|
|
35
|
+
# Set application name via session variable
|
|
36
|
+
try:
|
|
37
|
+
cur = self._conn.cursor()
|
|
38
|
+
cur.execute(f"SET @baucli = 'BAUCLI v{__version__}'")
|
|
39
|
+
cur.close()
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def disconnect(self):
|
|
44
|
+
if self._conn:
|
|
45
|
+
try:
|
|
46
|
+
self._conn.close()
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
self._conn = None
|
|
50
|
+
|
|
51
|
+
def test_connection(self) -> dict:
|
|
52
|
+
cur = self._conn.cursor()
|
|
53
|
+
cur.execute("SELECT VERSION()")
|
|
54
|
+
row = cur.fetchone()
|
|
55
|
+
version = row[0] if row else "Unknown"
|
|
56
|
+
cur.execute("SELECT DATABASE()")
|
|
57
|
+
row = cur.fetchone()
|
|
58
|
+
db_name = row[0] if row else "Unknown"
|
|
59
|
+
cur.execute("SELECT CURRENT_USER()")
|
|
60
|
+
row = cur.fetchone()
|
|
61
|
+
current_user = row[0] if row else "Unknown"
|
|
62
|
+
cur.close()
|
|
63
|
+
return {
|
|
64
|
+
"status": "ok",
|
|
65
|
+
"engine": "MYSQL",
|
|
66
|
+
"version": version,
|
|
67
|
+
"db_name": db_name,
|
|
68
|
+
"current_schema": current_user,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def get_native_identifier(self) -> Optional[str]:
|
|
72
|
+
try:
|
|
73
|
+
cur = self._conn.cursor()
|
|
74
|
+
cur.execute("SELECT @@server_uuid")
|
|
75
|
+
row = cur.fetchone()
|
|
76
|
+
cur.close()
|
|
77
|
+
return str(row[0]) if row else None
|
|
78
|
+
except Exception:
|
|
79
|
+
try:
|
|
80
|
+
cur = self._conn.cursor()
|
|
81
|
+
cur.execute("SELECT @@server_id")
|
|
82
|
+
row = cur.fetchone()
|
|
83
|
+
cur.close()
|
|
84
|
+
return str(row[0]) if row else None
|
|
85
|
+
except Exception:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def count_objects(self, schema: str) -> int:
|
|
89
|
+
cur = self._conn.cursor()
|
|
90
|
+
cur.execute("""
|
|
91
|
+
SELECT COUNT(*) FROM INFORMATION_SCHEMA.ROUTINES
|
|
92
|
+
WHERE ROUTINE_SCHEMA = %s
|
|
93
|
+
""", (schema,))
|
|
94
|
+
cnt = cur.fetchone()[0]
|
|
95
|
+
# Add triggers
|
|
96
|
+
cur.execute("""
|
|
97
|
+
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TRIGGERS
|
|
98
|
+
WHERE TRIGGER_SCHEMA = %s
|
|
99
|
+
""", (schema,))
|
|
100
|
+
cnt += cur.fetchone()[0]
|
|
101
|
+
cur.close()
|
|
102
|
+
return cnt
|
|
103
|
+
|
|
104
|
+
def count_tables(self, schema: str) -> int:
|
|
105
|
+
cur = self._conn.cursor()
|
|
106
|
+
cur.execute("""
|
|
107
|
+
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES
|
|
108
|
+
WHERE TABLE_SCHEMA = %s AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')
|
|
109
|
+
""", (schema,))
|
|
110
|
+
cnt = cur.fetchone()[0]
|
|
111
|
+
cur.close()
|
|
112
|
+
return cnt
|
|
113
|
+
|
|
114
|
+
def extract_sequences(self, schema: str) -> list:
|
|
115
|
+
"""MySQL nao tem objetos SEQUENCE (usa AUTO_INCREMENT em coluna).
|
|
116
|
+
|
|
117
|
+
DOCSEQ fica vazio para MySQL — nada a enviar. (MariaDB 10.3+ tem
|
|
118
|
+
SEQUENCE, mas fica fora do escopo desta fase.)
|
|
119
|
+
"""
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
def extract_objects(self, schema: str, since_date: str = None, on_progress=None, name_filter: str = None) -> list:
|
|
123
|
+
"""Extract functions, procedures, triggers, and views from MySQL.
|
|
124
|
+
|
|
125
|
+
In MySQL, schema = database name.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
schema: Database name
|
|
129
|
+
since_date: Filter by LAST_ALTERED (for routines) or CREATED (for triggers)
|
|
130
|
+
"""
|
|
131
|
+
result = []
|
|
132
|
+
cur = self._conn.cursor()
|
|
133
|
+
|
|
134
|
+
# ═══ Functions ═══
|
|
135
|
+
date_filter = ""
|
|
136
|
+
params = [schema]
|
|
137
|
+
if since_date:
|
|
138
|
+
date_filter = "AND LAST_ALTERED >= %s"
|
|
139
|
+
params.append(since_date[:19]) # Trim to YYYY-MM-DDTHH:MI:SS
|
|
140
|
+
|
|
141
|
+
cur.execute(f"""
|
|
142
|
+
SELECT ROUTINE_NAME, ROUTINE_TYPE,
|
|
143
|
+
REPLACE(CAST(LAST_ALTERED AS CHAR), ' ', 'T') AS LAST_DDL,
|
|
144
|
+
ROUTINE_COMMENT
|
|
145
|
+
FROM INFORMATION_SCHEMA.ROUTINES
|
|
146
|
+
WHERE ROUTINE_SCHEMA = %s
|
|
147
|
+
AND ROUTINE_TYPE IN ('FUNCTION', 'PROCEDURE')
|
|
148
|
+
{date_filter}
|
|
149
|
+
ORDER BY ROUTINE_TYPE, ROUTINE_NAME
|
|
150
|
+
""", params)
|
|
151
|
+
|
|
152
|
+
routines = cur.fetchall()
|
|
153
|
+
total_routines = len(routines)
|
|
154
|
+
for idx, (name, rtype, last_ddl, comment) in enumerate(routines):
|
|
155
|
+
if on_progress:
|
|
156
|
+
on_progress(idx + 1, total_routines, name)
|
|
157
|
+
try:
|
|
158
|
+
cur.execute(f"SHOW CREATE {rtype} `{schema}`.`{name}`")
|
|
159
|
+
row = cur.fetchone()
|
|
160
|
+
# SHOW CREATE returns: (name, sql_mode, create_stmt, ...)
|
|
161
|
+
source = row[2] if row and len(row) > 2 else None
|
|
162
|
+
if not source and row and len(row) > 1:
|
|
163
|
+
source = row[1]
|
|
164
|
+
except Exception:
|
|
165
|
+
source = None
|
|
166
|
+
|
|
167
|
+
if source:
|
|
168
|
+
entry = {
|
|
169
|
+
"name": name,
|
|
170
|
+
"type": rtype,
|
|
171
|
+
"source": source,
|
|
172
|
+
"last_ddl": last_ddl,
|
|
173
|
+
}
|
|
174
|
+
if comment:
|
|
175
|
+
entry["description"] = comment
|
|
176
|
+
result.append(entry)
|
|
177
|
+
|
|
178
|
+
# ═══ Triggers ═══
|
|
179
|
+
trig_params = [schema]
|
|
180
|
+
trig_date_filter = ""
|
|
181
|
+
if since_date:
|
|
182
|
+
trig_date_filter = "AND CREATED >= %s"
|
|
183
|
+
trig_params.append(since_date[:19])
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
cur.execute(f"""
|
|
187
|
+
SELECT TRIGGER_NAME,
|
|
188
|
+
EVENT_OBJECT_TABLE,
|
|
189
|
+
ACTION_TIMING,
|
|
190
|
+
EVENT_MANIPULATION,
|
|
191
|
+
REPLACE(CAST(CREATED AS CHAR), ' ', 'T') AS LAST_DDL
|
|
192
|
+
FROM INFORMATION_SCHEMA.TRIGGERS
|
|
193
|
+
WHERE TRIGGER_SCHEMA = %s
|
|
194
|
+
{trig_date_filter}
|
|
195
|
+
ORDER BY TRIGGER_NAME
|
|
196
|
+
""", trig_params)
|
|
197
|
+
|
|
198
|
+
triggers = cur.fetchall()
|
|
199
|
+
for name, table_name, timing, event, last_ddl in triggers:
|
|
200
|
+
try:
|
|
201
|
+
cur.execute(f"SHOW CREATE TRIGGER `{schema}`.`{name}`")
|
|
202
|
+
row = cur.fetchone()
|
|
203
|
+
# SHOW CREATE TRIGGER returns: (name, sql_mode, create_stmt, ...)
|
|
204
|
+
source = row[2] if row and len(row) > 2 else None
|
|
205
|
+
except Exception:
|
|
206
|
+
source = None
|
|
207
|
+
|
|
208
|
+
if source:
|
|
209
|
+
result.append({
|
|
210
|
+
"name": name,
|
|
211
|
+
"type": "TRIGGER",
|
|
212
|
+
"source": source,
|
|
213
|
+
"last_ddl": last_ddl,
|
|
214
|
+
"description": f"Trigger on {table_name} ({timing} {event})",
|
|
215
|
+
})
|
|
216
|
+
except Exception:
|
|
217
|
+
pass # Skip triggers if query fails
|
|
218
|
+
|
|
219
|
+
# Views are extracted via extract_tables (as DOCTAB entries, not PL/SQL objects)
|
|
220
|
+
|
|
221
|
+
cur.close()
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
def extract_tables(self, schema: str, since_date: str = None, on_progress=None, name_filter: str = None) -> list:
|
|
225
|
+
"""Extract table metadata from INFORMATION_SCHEMA.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
schema: Database name (MySQL schema = database)
|
|
229
|
+
since_date: Filter by UPDATE_TIME
|
|
230
|
+
"""
|
|
231
|
+
result = []
|
|
232
|
+
cur = self._conn.cursor()
|
|
233
|
+
|
|
234
|
+
# Get tables AND views
|
|
235
|
+
date_filter = ""
|
|
236
|
+
params = [schema]
|
|
237
|
+
if since_date:
|
|
238
|
+
date_filter = "AND (t.UPDATE_TIME >= %s OR t.CREATE_TIME >= %s)"
|
|
239
|
+
params.extend([since_date[:19], since_date[:19]])
|
|
240
|
+
|
|
241
|
+
cur.execute(f"""
|
|
242
|
+
SELECT t.TABLE_NAME,
|
|
243
|
+
REPLACE(CAST(COALESCE(t.UPDATE_TIME, t.CREATE_TIME) AS CHAR), ' ', 'T') AS LAST_DDL,
|
|
244
|
+
t.TABLE_COMMENT,
|
|
245
|
+
t.TABLE_TYPE
|
|
246
|
+
FROM INFORMATION_SCHEMA.TABLES t
|
|
247
|
+
WHERE t.TABLE_SCHEMA = %s
|
|
248
|
+
AND t.TABLE_TYPE IN ('BASE TABLE', 'VIEW')
|
|
249
|
+
{date_filter}
|
|
250
|
+
ORDER BY t.TABLE_TYPE, t.TABLE_NAME
|
|
251
|
+
""", params)
|
|
252
|
+
|
|
253
|
+
tables = cur.fetchall()
|
|
254
|
+
total_tbl = len(tables)
|
|
255
|
+
|
|
256
|
+
if total_tbl == 0:
|
|
257
|
+
cur.close()
|
|
258
|
+
return []
|
|
259
|
+
|
|
260
|
+
CHUNK = 500
|
|
261
|
+
|
|
262
|
+
# ═══ Pre-load columns in chunks ═══
|
|
263
|
+
all_columns = {}
|
|
264
|
+
table_names = [t[0] for t in tables]
|
|
265
|
+
col_count = 0
|
|
266
|
+
|
|
267
|
+
import time as _time
|
|
268
|
+
total_chunks = (len(table_names) + CHUNK - 1) // CHUNK
|
|
269
|
+
chunk_times = []
|
|
270
|
+
|
|
271
|
+
for ci, cs in enumerate(range(0, len(table_names), CHUNK)):
|
|
272
|
+
chunk = table_names[cs:cs + CHUNK]
|
|
273
|
+
t0 = _time.time()
|
|
274
|
+
|
|
275
|
+
eta = ""
|
|
276
|
+
if len(chunk_times) >= 2:
|
|
277
|
+
avg = sum(chunk_times) / len(chunk_times)
|
|
278
|
+
remaining = total_chunks - ci
|
|
279
|
+
eta_s = int(remaining * avg)
|
|
280
|
+
eta = f" ETA {eta_s//60}m{eta_s%60:02d}s" if eta_s >= 60 else f" ETA {eta_s}s"
|
|
281
|
+
|
|
282
|
+
if on_progress:
|
|
283
|
+
on_progress(0, total_tbl, f"loading columns {cs}/{total_tbl}{eta}")
|
|
284
|
+
|
|
285
|
+
placeholders = ','.join(['%s'] * len(chunk))
|
|
286
|
+
cur.execute(f"""
|
|
287
|
+
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE,
|
|
288
|
+
COLUMN_DEFAULT, COLUMN_COMMENT, COLUMN_KEY, ORDINAL_POSITION
|
|
289
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
290
|
+
WHERE TABLE_SCHEMA = %s AND TABLE_NAME IN ({placeholders})
|
|
291
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION
|
|
292
|
+
""", tuple([schema] + chunk))
|
|
293
|
+
for row in cur.fetchall():
|
|
294
|
+
tname = row[0]
|
|
295
|
+
if tname not in all_columns:
|
|
296
|
+
all_columns[tname] = []
|
|
297
|
+
all_columns[tname].append({
|
|
298
|
+
"name": row[1], "type": row[2], "nullable": row[3],
|
|
299
|
+
"default": str(row[4]) if row[4] is not None else None,
|
|
300
|
+
"comment": row[5] if row[5] else None,
|
|
301
|
+
"is_pk": "Y" if row[6] == "PRI" else "N",
|
|
302
|
+
})
|
|
303
|
+
col_count += 1
|
|
304
|
+
chunk_times.append(_time.time() - t0)
|
|
305
|
+
|
|
306
|
+
if on_progress:
|
|
307
|
+
on_progress(0, total_tbl, f"columns done ({col_count})")
|
|
308
|
+
|
|
309
|
+
if on_progress:
|
|
310
|
+
on_progress(0, total_tbl, "loading constraints...")
|
|
311
|
+
|
|
312
|
+
# ═══ Pre-load ALL constraints ═══
|
|
313
|
+
all_constraints = {}
|
|
314
|
+
try:
|
|
315
|
+
cur.execute("""
|
|
316
|
+
SELECT tc.TABLE_NAME, tc.CONSTRAINT_NAME, tc.CONSTRAINT_TYPE,
|
|
317
|
+
rc.REFERENCED_TABLE_NAME
|
|
318
|
+
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
|
319
|
+
LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
|
|
320
|
+
ON rc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
|
|
321
|
+
AND rc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
|
|
322
|
+
AND rc.TABLE_NAME = tc.TABLE_NAME
|
|
323
|
+
WHERE tc.TABLE_SCHEMA = %s
|
|
324
|
+
AND tc.CONSTRAINT_TYPE IN ('UNIQUE', 'FOREIGN KEY', 'CHECK')
|
|
325
|
+
ORDER BY tc.TABLE_NAME, tc.CONSTRAINT_TYPE
|
|
326
|
+
""", (schema,))
|
|
327
|
+
for tname, cname, ctype, ref_table in cur.fetchall():
|
|
328
|
+
if tname not in all_constraints:
|
|
329
|
+
all_constraints[tname] = []
|
|
330
|
+
bau_type = {'UNIQUE': 'U', 'FOREIGN KEY': 'R', 'CHECK': 'C'}.get(ctype, ctype)
|
|
331
|
+
c = {"name": cname, "type": bau_type}
|
|
332
|
+
if ref_table:
|
|
333
|
+
c["ref_table"] = ref_table
|
|
334
|
+
all_constraints[tname].append(c)
|
|
335
|
+
except Exception:
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
if on_progress:
|
|
339
|
+
on_progress(0, total_tbl, "building entries...")
|
|
340
|
+
|
|
341
|
+
# ═══ Build results per table ═══
|
|
342
|
+
for idx, (table_name, last_ddl, table_comment, mysql_type) in enumerate(tables):
|
|
343
|
+
if on_progress:
|
|
344
|
+
on_progress(idx + 1, total_tbl, table_name)
|
|
345
|
+
bau_type = 'VIEW' if mysql_type == 'VIEW' else 'TABLE'
|
|
346
|
+
|
|
347
|
+
# View source (per-object — small number)
|
|
348
|
+
view_source = None
|
|
349
|
+
if bau_type == 'VIEW':
|
|
350
|
+
try:
|
|
351
|
+
cur.execute("""
|
|
352
|
+
SELECT VIEW_DEFINITION FROM INFORMATION_SCHEMA.VIEWS
|
|
353
|
+
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
|
|
354
|
+
""", (schema, table_name))
|
|
355
|
+
row = cur.fetchone()
|
|
356
|
+
if row and row[0]:
|
|
357
|
+
view_source = f"CREATE OR REPLACE VIEW `{schema}`.`{table_name}` AS\n{row[0]}"
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
# Columns from pre-loaded dict
|
|
362
|
+
columns = []
|
|
363
|
+
for c in all_columns.get(table_name, []):
|
|
364
|
+
col = {"name": c["name"], "type": c["type"], "nullable": c["nullable"], "is_pk": c["is_pk"]}
|
|
365
|
+
if c["default"]:
|
|
366
|
+
col["default"] = c["default"]
|
|
367
|
+
if c["comment"]:
|
|
368
|
+
col["comment"] = c["comment"]
|
|
369
|
+
columns.append(col)
|
|
370
|
+
|
|
371
|
+
constraints = all_constraints.get(table_name, [])
|
|
372
|
+
|
|
373
|
+
entry = {
|
|
374
|
+
"name": table_name,
|
|
375
|
+
"type": bau_type,
|
|
376
|
+
"last_ddl": last_ddl,
|
|
377
|
+
"columns": columns,
|
|
378
|
+
}
|
|
379
|
+
if table_comment:
|
|
380
|
+
entry["comment"] = table_comment
|
|
381
|
+
if constraints:
|
|
382
|
+
entry["constraints"] = constraints
|
|
383
|
+
if view_source:
|
|
384
|
+
entry["source"] = view_source
|
|
385
|
+
result.append(entry)
|
|
386
|
+
|
|
387
|
+
cur.close()
|
|
388
|
+
return result
|