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/mssql.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""BAUCLI — SQL Server database extractor.
|
|
2
|
+
|
|
3
|
+
Uses python-tds (pytds) — pure Python, no compilation needed.
|
|
4
|
+
Works on Windows, macOS, Linux without FreeTDS or Visual C++.
|
|
5
|
+
Extracts:
|
|
6
|
+
- Functions from sys.objects + OBJECT_DEFINITION()
|
|
7
|
+
- Procedures from sys.objects + OBJECT_DEFINITION()
|
|
8
|
+
- Triggers from sys.triggers + OBJECT_DEFINITION()
|
|
9
|
+
- Views from sys.views + OBJECT_DEFINITION()
|
|
10
|
+
- Table metadata from INFORMATION_SCHEMA.COLUMNS, sys.indexes, etc.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Optional
|
|
14
|
+
from baucli.db import BaseExtractor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MSSQLExtractor(BaseExtractor):
|
|
18
|
+
"""SQL Server database extractor using pytds."""
|
|
19
|
+
|
|
20
|
+
engine = "MSSQL"
|
|
21
|
+
|
|
22
|
+
def connect(self):
|
|
23
|
+
import pytds
|
|
24
|
+
from baucli import __version__
|
|
25
|
+
self._conn = pytds.connect(
|
|
26
|
+
server=self.host,
|
|
27
|
+
port=self.port,
|
|
28
|
+
database=self.database,
|
|
29
|
+
user=self.user,
|
|
30
|
+
password=self.password,
|
|
31
|
+
appname=f"BAUCLI v{__version__}",
|
|
32
|
+
login_timeout=60,
|
|
33
|
+
timeout=14400, # 4 hours query timeout for large schemas
|
|
34
|
+
autocommit=True,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def disconnect(self):
|
|
38
|
+
if self._conn:
|
|
39
|
+
try:
|
|
40
|
+
self._conn.close()
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
self._conn = None
|
|
44
|
+
|
|
45
|
+
def test_connection(self) -> dict:
|
|
46
|
+
cur = self._conn.cursor()
|
|
47
|
+
cur.execute("SELECT @@VERSION")
|
|
48
|
+
row = cur.fetchone()
|
|
49
|
+
version = row[0] if row else "Unknown"
|
|
50
|
+
cur.execute("SELECT DB_NAME()")
|
|
51
|
+
row = cur.fetchone()
|
|
52
|
+
db_name = row[0] if row else "Unknown"
|
|
53
|
+
cur.execute("SELECT SCHEMA_NAME()")
|
|
54
|
+
row = cur.fetchone()
|
|
55
|
+
current_schema = row[0] if row else "dbo"
|
|
56
|
+
cur.close()
|
|
57
|
+
return {
|
|
58
|
+
"status": "ok",
|
|
59
|
+
"engine": "MSSQL",
|
|
60
|
+
"version": version.split('\n')[0] if version else "Unknown",
|
|
61
|
+
"db_name": db_name,
|
|
62
|
+
"current_schema": current_schema,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
def get_native_identifier(self) -> Optional[str]:
|
|
66
|
+
try:
|
|
67
|
+
cur = self._conn.cursor()
|
|
68
|
+
cur.execute("SELECT SERVERPROPERTY('ServerName')")
|
|
69
|
+
row = cur.fetchone()
|
|
70
|
+
cur.close()
|
|
71
|
+
return str(row[0]) if row else None
|
|
72
|
+
except Exception:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def count_objects(self, schema: str) -> int:
|
|
76
|
+
cur = self._conn.cursor()
|
|
77
|
+
cur.execute("""
|
|
78
|
+
SELECT COUNT(*) FROM sys.objects o
|
|
79
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
80
|
+
WHERE s.name = %s AND o.type IN ('P', 'FN', 'IF', 'TF', 'AF', 'TR')
|
|
81
|
+
AND o.is_ms_shipped = 0
|
|
82
|
+
""", (schema,))
|
|
83
|
+
cnt = cur.fetchone()[0]
|
|
84
|
+
cur.close()
|
|
85
|
+
return cnt
|
|
86
|
+
|
|
87
|
+
def count_tables(self, schema: str) -> int:
|
|
88
|
+
cur = self._conn.cursor()
|
|
89
|
+
cur.execute("""
|
|
90
|
+
SELECT COUNT(*) FROM sys.objects o
|
|
91
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
92
|
+
WHERE s.name = %s AND o.type IN ('U', 'V') AND o.is_ms_shipped = 0
|
|
93
|
+
""", (schema,))
|
|
94
|
+
cnt = cur.fetchone()[0]
|
|
95
|
+
cur.close()
|
|
96
|
+
return cnt
|
|
97
|
+
|
|
98
|
+
def extract_sequences(self, schema: str) -> list:
|
|
99
|
+
"""Sequences do schema (sys.sequences, SQL Server 2012+).
|
|
100
|
+
|
|
101
|
+
IDENTITY columns NAO sao sequences (o vinculo de identity nao entra
|
|
102
|
+
aqui). O servidor persiste os metadados e roda DISCOVER (default de
|
|
103
|
+
coluna `NEXT VALUE FOR seq`).
|
|
104
|
+
"""
|
|
105
|
+
cur = self._conn.cursor()
|
|
106
|
+
try:
|
|
107
|
+
cur.execute("""
|
|
108
|
+
SELECT sch.name, seq.name, seq.minimum_value, seq.maximum_value,
|
|
109
|
+
seq.increment, seq.is_cycling, seq.cache_size, seq.current_value
|
|
110
|
+
FROM sys.sequences seq
|
|
111
|
+
JOIN sys.schemas sch ON sch.schema_id = seq.schema_id
|
|
112
|
+
WHERE sch.name = %s
|
|
113
|
+
""", (schema,))
|
|
114
|
+
rows = cur.fetchall()
|
|
115
|
+
except Exception:
|
|
116
|
+
rows = []
|
|
117
|
+
cur.close()
|
|
118
|
+
|
|
119
|
+
def _n(v):
|
|
120
|
+
return int(v) if v is not None else None
|
|
121
|
+
|
|
122
|
+
out = []
|
|
123
|
+
for r in rows:
|
|
124
|
+
cyc = "Y" if (r[5] in (1, True)) else "N"
|
|
125
|
+
out.append({"owner": r[0], "sequence_name": r[1],
|
|
126
|
+
"min_value": _n(r[2]), "max_value": _n(r[3]),
|
|
127
|
+
"increment_by": _n(r[4]),
|
|
128
|
+
"cycle_flag": cyc, "order_flag": "N",
|
|
129
|
+
"cache_size": _n(r[6]), "last_number": _n(r[7])})
|
|
130
|
+
return out
|
|
131
|
+
|
|
132
|
+
def extract_objects(self, schema: str, since_date: str = None, on_progress=None, name_filter: str = None) -> list:
|
|
133
|
+
"""Extract functions, procedures, triggers from SQL Server.
|
|
134
|
+
|
|
135
|
+
Uses two-pass strategy: fast listing query, then OBJECT_DEFINITION per object
|
|
136
|
+
for real-time progress reporting.
|
|
137
|
+
"""
|
|
138
|
+
result = []
|
|
139
|
+
cur = self._conn.cursor()
|
|
140
|
+
|
|
141
|
+
# ═══ Functions, Procedures ═══
|
|
142
|
+
date_filter = ""
|
|
143
|
+
params = [schema]
|
|
144
|
+
if since_date:
|
|
145
|
+
date_filter = "AND o.modify_date >= %s"
|
|
146
|
+
params.append(since_date[:19])
|
|
147
|
+
|
|
148
|
+
# Pass 1: fast listing (no OBJECT_DEFINITION — instant)
|
|
149
|
+
cur.execute(f"""
|
|
150
|
+
SELECT o.name,
|
|
151
|
+
CASE o.type
|
|
152
|
+
WHEN 'P' THEN 'PROCEDURE'
|
|
153
|
+
WHEN 'FN' THEN 'FUNCTION'
|
|
154
|
+
WHEN 'IF' THEN 'FUNCTION'
|
|
155
|
+
WHEN 'TF' THEN 'FUNCTION'
|
|
156
|
+
WHEN 'AF' THEN 'FUNCTION'
|
|
157
|
+
END AS type,
|
|
158
|
+
CONVERT(VARCHAR(19), o.modify_date, 126) AS last_ddl,
|
|
159
|
+
o.object_id
|
|
160
|
+
FROM sys.objects o
|
|
161
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
162
|
+
WHERE s.name = %s
|
|
163
|
+
AND o.type IN ('P', 'FN', 'IF', 'TF', 'AF')
|
|
164
|
+
AND o.is_ms_shipped = 0
|
|
165
|
+
{date_filter}
|
|
166
|
+
ORDER BY o.type, o.name
|
|
167
|
+
""", tuple(params))
|
|
168
|
+
|
|
169
|
+
obj_list = cur.fetchall()
|
|
170
|
+
total_obj = len(obj_list)
|
|
171
|
+
|
|
172
|
+
# Pass 2: get source per object (progress callback per item)
|
|
173
|
+
for idx, (name, obj_type, last_ddl, obj_id) in enumerate(obj_list):
|
|
174
|
+
if on_progress:
|
|
175
|
+
on_progress(idx + 1, total_obj, name)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
cur.execute("SELECT CAST(OBJECT_DEFINITION(%s) AS NVARCHAR(MAX))", (obj_id,))
|
|
179
|
+
row = cur.fetchone()
|
|
180
|
+
source = row[0] if row and row[0] else None
|
|
181
|
+
except Exception:
|
|
182
|
+
source = None
|
|
183
|
+
|
|
184
|
+
if source:
|
|
185
|
+
result.append({
|
|
186
|
+
"name": name,
|
|
187
|
+
"type": obj_type,
|
|
188
|
+
"source": source,
|
|
189
|
+
"last_ddl": last_ddl,
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
# ═══ Triggers ═══
|
|
193
|
+
trig_params = [schema]
|
|
194
|
+
trig_date_filter = ""
|
|
195
|
+
if since_date:
|
|
196
|
+
trig_date_filter = "AND t.modify_date >= %s"
|
|
197
|
+
trig_params.append(since_date[:19])
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
cur.execute(f"""
|
|
201
|
+
SELECT t.name,
|
|
202
|
+
CONVERT(VARCHAR(19), t.modify_date, 126) AS last_ddl,
|
|
203
|
+
CAST(OBJECT_DEFINITION(t.object_id) AS NVARCHAR(MAX)) AS source,
|
|
204
|
+
OBJECT_NAME(t.parent_id) AS table_name,
|
|
205
|
+
CASE WHEN t.is_instead_of_trigger = 1 THEN 'INSTEAD OF' ELSE 'AFTER' END AS timing
|
|
206
|
+
FROM sys.triggers t
|
|
207
|
+
JOIN sys.objects o ON o.object_id = t.parent_id
|
|
208
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
209
|
+
WHERE s.name = %s
|
|
210
|
+
AND t.is_ms_shipped = 0
|
|
211
|
+
{trig_date_filter}
|
|
212
|
+
ORDER BY t.name
|
|
213
|
+
""", tuple(trig_params))
|
|
214
|
+
|
|
215
|
+
trg_rows = cur.fetchall()
|
|
216
|
+
for name, last_ddl, source, table_name, timing in trg_rows:
|
|
217
|
+
if on_progress:
|
|
218
|
+
on_progress(total_obj + len([r for r in result if r['type']=='TRIGGER']) + 1,
|
|
219
|
+
total_obj + len(trg_rows), name)
|
|
220
|
+
if source:
|
|
221
|
+
result.append({
|
|
222
|
+
"name": name,
|
|
223
|
+
"type": "TRIGGER",
|
|
224
|
+
"source": source,
|
|
225
|
+
"last_ddl": last_ddl,
|
|
226
|
+
"description": f"Trigger on {table_name} ({timing})",
|
|
227
|
+
})
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
cur.close()
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
def extract_tables(self, schema: str, since_date: str = None, on_progress=None, name_filter: str = None) -> list:
|
|
235
|
+
"""Extract table metadata from INFORMATION_SCHEMA and sys views.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
schema: Schema name (e.g. 'dbo')
|
|
239
|
+
since_date: Filter by modify_date
|
|
240
|
+
"""
|
|
241
|
+
result = []
|
|
242
|
+
cur = self._conn.cursor()
|
|
243
|
+
|
|
244
|
+
# Get tables AND views
|
|
245
|
+
date_filter = ""
|
|
246
|
+
params = [schema]
|
|
247
|
+
if since_date:
|
|
248
|
+
date_filter = "AND o.modify_date >= %s"
|
|
249
|
+
params.append(since_date[:19])
|
|
250
|
+
|
|
251
|
+
cur.execute(f"""
|
|
252
|
+
SELECT o.name AS TABLE_NAME,
|
|
253
|
+
CONVERT(VARCHAR(19), o.modify_date, 126) AS last_ddl,
|
|
254
|
+
CASE o.type WHEN 'U' THEN 'TABLE' WHEN 'V' THEN 'VIEW' END AS table_type,
|
|
255
|
+
CAST(OBJECT_DEFINITION(o.object_id) AS NVARCHAR(MAX)) AS view_source
|
|
256
|
+
FROM sys.objects o
|
|
257
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
258
|
+
WHERE s.name = %s
|
|
259
|
+
AND o.type IN ('U', 'V')
|
|
260
|
+
AND o.is_ms_shipped = 0
|
|
261
|
+
{date_filter}
|
|
262
|
+
ORDER BY o.type, o.name
|
|
263
|
+
""", tuple(params))
|
|
264
|
+
|
|
265
|
+
tables = cur.fetchall()
|
|
266
|
+
total_tbl = len(tables)
|
|
267
|
+
|
|
268
|
+
if total_tbl == 0:
|
|
269
|
+
cur.close()
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
# Report progress during pre-load phases
|
|
273
|
+
CHUNK = 500 # Process in chunks for large schemas
|
|
274
|
+
import time as _time
|
|
275
|
+
|
|
276
|
+
# ═══ Pre-load columns in chunks ═══
|
|
277
|
+
all_columns = {}
|
|
278
|
+
col_count = 0
|
|
279
|
+
table_names = [t[0] for t in tables]
|
|
280
|
+
total_chunks = (len(table_names) + CHUNK - 1) // CHUNK
|
|
281
|
+
chunk_times = []
|
|
282
|
+
|
|
283
|
+
for ci, cs in enumerate(range(0, len(table_names), CHUNK)):
|
|
284
|
+
chunk = table_names[cs:cs + CHUNK]
|
|
285
|
+
t0 = _time.time()
|
|
286
|
+
|
|
287
|
+
eta = ""
|
|
288
|
+
if len(chunk_times) >= 2:
|
|
289
|
+
avg = sum(chunk_times) / len(chunk_times)
|
|
290
|
+
remaining = total_chunks - ci
|
|
291
|
+
eta_s = int(remaining * avg)
|
|
292
|
+
eta = f" ETA {eta_s//60}m{eta_s%60:02d}s" if eta_s >= 60 else f" ETA {eta_s}s"
|
|
293
|
+
|
|
294
|
+
if on_progress:
|
|
295
|
+
on_progress(0, total_tbl, f"loading columns {cs}/{total_tbl}{eta}")
|
|
296
|
+
|
|
297
|
+
placeholders = ','.join(['%s'] * len(chunk))
|
|
298
|
+
cur.execute(f"""
|
|
299
|
+
SELECT c.TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE,
|
|
300
|
+
CASE
|
|
301
|
+
WHEN c.DATA_TYPE IN ('varchar','nvarchar','char','nchar')
|
|
302
|
+
THEN c.DATA_TYPE + '(' + CASE WHEN c.CHARACTER_MAXIMUM_LENGTH = -1 THEN 'MAX' ELSE CAST(c.CHARACTER_MAXIMUM_LENGTH AS VARCHAR) END + ')'
|
|
303
|
+
WHEN c.DATA_TYPE IN ('decimal','numeric')
|
|
304
|
+
THEN c.DATA_TYPE + '(' + CAST(c.NUMERIC_PRECISION AS VARCHAR) + ',' + CAST(c.NUMERIC_SCALE AS VARCHAR) + ')'
|
|
305
|
+
ELSE c.DATA_TYPE
|
|
306
|
+
END AS full_type,
|
|
307
|
+
c.IS_NULLABLE, c.COLUMN_DEFAULT, c.ORDINAL_POSITION
|
|
308
|
+
FROM INFORMATION_SCHEMA.COLUMNS c
|
|
309
|
+
WHERE c.TABLE_SCHEMA = %s AND c.TABLE_NAME IN ({placeholders})
|
|
310
|
+
ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION
|
|
311
|
+
""", tuple([schema] + chunk))
|
|
312
|
+
for row in cur.fetchall():
|
|
313
|
+
tname = row[0]
|
|
314
|
+
if tname not in all_columns:
|
|
315
|
+
all_columns[tname] = []
|
|
316
|
+
all_columns[tname].append({
|
|
317
|
+
"name": row[1], "type": row[3],
|
|
318
|
+
"nullable": row[4], "default": str(row[5]) if row[5] else None,
|
|
319
|
+
"ordinal": row[6],
|
|
320
|
+
})
|
|
321
|
+
col_count += 1
|
|
322
|
+
chunk_times.append(_time.time() - t0)
|
|
323
|
+
|
|
324
|
+
if on_progress:
|
|
325
|
+
on_progress(0, total_tbl, f"columns done ({col_count}), loading PKs...")
|
|
326
|
+
|
|
327
|
+
# ═══ Pre-load ALL primary keys ═══
|
|
328
|
+
pk_cols = set()
|
|
329
|
+
try:
|
|
330
|
+
cur.execute("""
|
|
331
|
+
SELECT ku.TABLE_NAME, ku.COLUMN_NAME
|
|
332
|
+
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
|
333
|
+
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku
|
|
334
|
+
ON ku.CONSTRAINT_NAME = tc.CONSTRAINT_NAME AND ku.TABLE_SCHEMA = tc.TABLE_SCHEMA
|
|
335
|
+
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = %s
|
|
336
|
+
""", (schema,))
|
|
337
|
+
for tname, cname in cur.fetchall():
|
|
338
|
+
pk_cols.add(f"{tname}.{cname}")
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
if on_progress:
|
|
343
|
+
on_progress(0, total_tbl, f"PKs loaded ({len(pk_cols)}), loading comments...")
|
|
344
|
+
|
|
345
|
+
# ═══ Pre-load ALL table comments (extended properties) ═══
|
|
346
|
+
table_comments = {}
|
|
347
|
+
try:
|
|
348
|
+
cur.execute("""
|
|
349
|
+
SELECT o.name, CAST(ep.value AS VARCHAR(500))
|
|
350
|
+
FROM sys.extended_properties ep
|
|
351
|
+
JOIN sys.objects o ON o.object_id = ep.major_id
|
|
352
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
353
|
+
WHERE s.name = %s AND ep.name = 'MS_Description' AND ep.minor_id = 0
|
|
354
|
+
""", (schema,))
|
|
355
|
+
for tname, comment in cur.fetchall():
|
|
356
|
+
table_comments[tname] = comment
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
if on_progress:
|
|
361
|
+
on_progress(0, total_tbl, f"comments loaded, loading col comments...")
|
|
362
|
+
|
|
363
|
+
# ═══ Pre-load ALL column comments ═══
|
|
364
|
+
col_comments = {}
|
|
365
|
+
try:
|
|
366
|
+
cur.execute("""
|
|
367
|
+
SELECT o.name AS table_name, c.name AS column_name, CAST(ep.value AS VARCHAR(500))
|
|
368
|
+
FROM sys.extended_properties ep
|
|
369
|
+
JOIN sys.columns c ON c.object_id = ep.major_id AND c.column_id = ep.minor_id
|
|
370
|
+
JOIN sys.objects o ON o.object_id = ep.major_id
|
|
371
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
372
|
+
WHERE s.name = %s AND ep.name = 'MS_Description' AND ep.minor_id > 0
|
|
373
|
+
""", (schema,))
|
|
374
|
+
for tname, cname, comment in cur.fetchall():
|
|
375
|
+
col_comments[f"{tname}.{cname}"] = comment
|
|
376
|
+
except Exception:
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
if on_progress:
|
|
380
|
+
on_progress(0, total_tbl, f"loading constraints...")
|
|
381
|
+
|
|
382
|
+
# ═══ Pre-load ALL constraints ═══
|
|
383
|
+
all_constraints = {}
|
|
384
|
+
try:
|
|
385
|
+
cur.execute("""
|
|
386
|
+
SELECT tc.TABLE_NAME, tc.CONSTRAINT_NAME, tc.CONSTRAINT_TYPE,
|
|
387
|
+
STRING_AGG(ku.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY ku.ORDINAL_POSITION) AS columns
|
|
388
|
+
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
|
389
|
+
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku
|
|
390
|
+
ON ku.CONSTRAINT_NAME = tc.CONSTRAINT_NAME AND ku.TABLE_SCHEMA = tc.TABLE_SCHEMA
|
|
391
|
+
WHERE tc.TABLE_SCHEMA = %s
|
|
392
|
+
GROUP BY tc.TABLE_NAME, tc.CONSTRAINT_NAME, tc.CONSTRAINT_TYPE
|
|
393
|
+
ORDER BY tc.TABLE_NAME, tc.CONSTRAINT_TYPE, tc.CONSTRAINT_NAME
|
|
394
|
+
""", (schema,))
|
|
395
|
+
for tname, cname, ctype, cols in cur.fetchall():
|
|
396
|
+
if tname not in all_constraints:
|
|
397
|
+
all_constraints[tname] = []
|
|
398
|
+
all_constraints[tname].append({
|
|
399
|
+
"name": cname, "type": ctype, "columns": cols,
|
|
400
|
+
})
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
if on_progress:
|
|
405
|
+
on_progress(0, total_tbl, f"all metadata loaded, building {total_tbl} entries...")
|
|
406
|
+
|
|
407
|
+
# ═══ Build results per table ═══
|
|
408
|
+
for idx, (table_name, last_ddl, table_type, view_src) in enumerate(tables):
|
|
409
|
+
if on_progress:
|
|
410
|
+
on_progress(idx + 1, total_tbl, table_name)
|
|
411
|
+
|
|
412
|
+
# Columns from pre-loaded dict
|
|
413
|
+
columns = []
|
|
414
|
+
for c in all_columns.get(table_name, []):
|
|
415
|
+
col = {"name": c["name"], "type": c["type"], "nullable": c["nullable"]}
|
|
416
|
+
if f"{table_name}.{c['name']}" in pk_cols:
|
|
417
|
+
col["is_pk"] = "Y"
|
|
418
|
+
if c["default"]:
|
|
419
|
+
col["default"] = c["default"]
|
|
420
|
+
comment = col_comments.get(f"{table_name}.{c['name']}")
|
|
421
|
+
if comment:
|
|
422
|
+
col["comment"] = comment
|
|
423
|
+
columns.append(col)
|
|
424
|
+
|
|
425
|
+
table_comment = table_comments.get(table_name)
|
|
426
|
+
constraints = all_constraints.get(table_name, [])
|
|
427
|
+
|
|
428
|
+
entry = {
|
|
429
|
+
"name": table_name,
|
|
430
|
+
"type": table_type,
|
|
431
|
+
"last_ddl": last_ddl,
|
|
432
|
+
"columns": columns,
|
|
433
|
+
}
|
|
434
|
+
if table_comment:
|
|
435
|
+
entry["comment"] = table_comment
|
|
436
|
+
if constraints:
|
|
437
|
+
entry["constraints"] = constraints
|
|
438
|
+
if view_src:
|
|
439
|
+
entry["source"] = view_src
|
|
440
|
+
result.append(entry)
|
|
441
|
+
|
|
442
|
+
cur.close()
|
|
443
|
+
return result
|