dataform-dependency-visualizer 0.2.3__tar.gz → 0.2.4__tar.gz
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.
- {dataform_dependency_visualizer-0.2.3 → dataform_dependency_visualizer-0.2.4}/PKG-INFO +16 -5
- {dataform_dependency_visualizer-0.2.3 → dataform_dependency_visualizer-0.2.4}/README.md +10 -2
- {dataform_dependency_visualizer-0.2.3 → dataform_dependency_visualizer-0.2.4}/pyproject.toml +1 -1
- {dataform_dependency_visualizer-0.2.3 → dataform_dependency_visualizer-0.2.4}/src/dataform_viz/dataform_check.py +225 -5
- {dataform_dependency_visualizer-0.2.3 → dataform_dependency_visualizer-0.2.4}/src/dataform_viz/parser.py +21 -3
- {dataform_dependency_visualizer-0.2.3 → dataform_dependency_visualizer-0.2.4}/src/dataform_viz/svg_generator.py +45 -2
- {dataform_dependency_visualizer-0.2.3 → dataform_dependency_visualizer-0.2.4}/LICENSE +0 -0
- {dataform_dependency_visualizer-0.2.3 → dataform_dependency_visualizer-0.2.4}/src/dataform_viz/__init__.py +0 -0
- {dataform_dependency_visualizer-0.2.3 → dataform_dependency_visualizer-0.2.4}/src/dataform_viz/cleanup.py +0 -0
- {dataform_dependency_visualizer-0.2.3 → dataform_dependency_visualizer-0.2.4}/src/dataform_viz/cli.py +0 -0
- {dataform_dependency_visualizer-0.2.3 → dataform_dependency_visualizer-0.2.4}/src/dataform_viz/master_index.py +0 -0
- {dataform_dependency_visualizer-0.2.3 → dataform_dependency_visualizer-0.2.4}/src/dataform_viz/visualizer.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dataform-dependency-visualizer
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Visualize Dataform table dependencies as interactive SVG diagrams
|
|
5
|
-
Home-page: https://github.com/OshigeAkito/dataform-dependency-visualizer
|
|
6
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
7
|
Keywords: dataform,dependencies,visualization,bigquery,sql
|
|
8
8
|
Author: Thamo
|
|
9
9
|
Author-email: thamo@example.com
|
|
@@ -15,8 +15,11 @@ Classifier: Programming Language :: Python :: 3
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
20
|
Classifier: Topic :: Database
|
|
19
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Project-URL: Homepage, https://github.com/OshigeAkito/dataform-dependency-visualizer
|
|
20
23
|
Project-URL: Repository, https://github.com/OshigeAkito/dataform-dependency-visualizer
|
|
21
24
|
Description-Content-Type: text/markdown
|
|
22
25
|
|
|
@@ -32,7 +35,8 @@ Generate beautiful, interactive SVG diagrams showing dependencies between Datafo
|
|
|
32
35
|
|
|
33
36
|
- 📊 **Interactive SVG Diagrams** - Visualize dependencies for each table
|
|
34
37
|
- 🎨 **Color-Coded** - Tables (blue), views (green), operations (orange)
|
|
35
|
-
-
|
|
38
|
+
- � **JOIN Details** - Shows JOIN types and ON conditions for each dependency
|
|
39
|
+
- �🔍 **Master Index** - Browse all tables in one HTML interface
|
|
36
40
|
- 📁 **Schema Organization** - Automatically organized by database schema
|
|
37
41
|
- ⚡ **Pure Python** - No Graphviz required
|
|
38
42
|
|
|
@@ -115,10 +119,11 @@ viz.generate_master_index('output')
|
|
|
115
119
|
Each SVG diagram shows:
|
|
116
120
|
|
|
117
121
|
- **Center (Yellow)**: The table being viewed
|
|
118
|
-
- **Left (Blue)**: Dependencies - tables this reads FROM
|
|
122
|
+
- **Left (Blue)**: Dependencies - tables this reads FROM, with JOIN details shown below each
|
|
119
123
|
- **Right (Green)**: Dependents - tables that read FROM this
|
|
120
124
|
- **Schema Labels**: Database schema for each table
|
|
121
125
|
- **Type Badges**: table, view, incremental, operation
|
|
126
|
+
- **JOIN Information**: Type (LEFT/INNER/RIGHT JOIN) and ON conditions displayed in red below dependency nodes
|
|
122
127
|
|
|
123
128
|
## Output Structure
|
|
124
129
|
|
|
@@ -180,6 +185,12 @@ MIT License - See [LICENSE](LICENSE) file
|
|
|
180
185
|
|
|
181
186
|
## Changelog
|
|
182
187
|
|
|
188
|
+
### v0.2.3 (2026-01-21)
|
|
189
|
+
- **NEW**: JOIN information extraction from SQL queries
|
|
190
|
+
- Display JOIN types (LEFT, INNER, RIGHT, etc.) and ON conditions in SVG diagrams
|
|
191
|
+
- JOIN count summary shown on center table
|
|
192
|
+
- Detailed JOIN info displayed below each dependency node
|
|
193
|
+
|
|
183
194
|
### v0.2.0 (2026-01-13)
|
|
184
195
|
- Added cleanup utility for database references
|
|
185
196
|
- Constant replacement functionality
|
|
@@ -10,7 +10,8 @@ Generate beautiful, interactive SVG diagrams showing dependencies between Datafo
|
|
|
10
10
|
|
|
11
11
|
- 📊 **Interactive SVG Diagrams** - Visualize dependencies for each table
|
|
12
12
|
- 🎨 **Color-Coded** - Tables (blue), views (green), operations (orange)
|
|
13
|
-
-
|
|
13
|
+
- � **JOIN Details** - Shows JOIN types and ON conditions for each dependency
|
|
14
|
+
- �🔍 **Master Index** - Browse all tables in one HTML interface
|
|
14
15
|
- 📁 **Schema Organization** - Automatically organized by database schema
|
|
15
16
|
- ⚡ **Pure Python** - No Graphviz required
|
|
16
17
|
|
|
@@ -93,10 +94,11 @@ viz.generate_master_index('output')
|
|
|
93
94
|
Each SVG diagram shows:
|
|
94
95
|
|
|
95
96
|
- **Center (Yellow)**: The table being viewed
|
|
96
|
-
- **Left (Blue)**: Dependencies - tables this reads FROM
|
|
97
|
+
- **Left (Blue)**: Dependencies - tables this reads FROM, with JOIN details shown below each
|
|
97
98
|
- **Right (Green)**: Dependents - tables that read FROM this
|
|
98
99
|
- **Schema Labels**: Database schema for each table
|
|
99
100
|
- **Type Badges**: table, view, incremental, operation
|
|
101
|
+
- **JOIN Information**: Type (LEFT/INNER/RIGHT JOIN) and ON conditions displayed in red below dependency nodes
|
|
100
102
|
|
|
101
103
|
## Output Structure
|
|
102
104
|
|
|
@@ -158,6 +160,12 @@ MIT License - See [LICENSE](LICENSE) file
|
|
|
158
160
|
|
|
159
161
|
## Changelog
|
|
160
162
|
|
|
163
|
+
### v0.2.3 (2026-01-21)
|
|
164
|
+
- **NEW**: JOIN information extraction from SQL queries
|
|
165
|
+
- Display JOIN types (LEFT, INNER, RIGHT, etc.) and ON conditions in SVG diagrams
|
|
166
|
+
- JOIN count summary shown on center table
|
|
167
|
+
- Detailed JOIN info displayed below each dependency node
|
|
168
|
+
|
|
161
169
|
### v0.2.0 (2026-01-13)
|
|
162
170
|
- Added cleanup utility for database references
|
|
163
171
|
- Constant replacement functionality
|
|
@@ -269,10 +269,213 @@ def normalize_name(target):
|
|
|
269
269
|
return "UNKNOWN"
|
|
270
270
|
return f"{target.get('schema', '')}.{target.get('name', '')}"
|
|
271
271
|
|
|
272
|
+
def parse_joins_from_query(query, dependencies):
|
|
273
|
+
"""Extract JOIN information from SQL query.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
query: SQL query string
|
|
277
|
+
dependencies: List of dependency target objects
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Dictionary mapping dependency name to join info: {dep_name: {'type': 'LEFT JOIN', 'condition': 'a.id = b.id'}}
|
|
281
|
+
"""
|
|
282
|
+
if not query:
|
|
283
|
+
return {}
|
|
284
|
+
|
|
285
|
+
join_info = {}
|
|
286
|
+
|
|
287
|
+
# Normalize query - remove backticks and newlines for easier parsing
|
|
288
|
+
query_normalized = query.replace('`', '').replace('\n', ' ').replace('\r', '')
|
|
289
|
+
|
|
290
|
+
# Extract CTE definitions to map CTE names to their source tables
|
|
291
|
+
# For CTEs with complex content (window functions, nested parentheses), we need a better approach
|
|
292
|
+
# Pattern: WITH cte_name AS (... FROM schema.table_name ...)
|
|
293
|
+
cte_to_source = {} # Maps CTE name to source table
|
|
294
|
+
|
|
295
|
+
# Find all CTE definitions (WITH or , followed by cte_name AS ()
|
|
296
|
+
cte_start_pattern = r'(?:WITH|,)\s+(\w+)\s+AS\s*\('
|
|
297
|
+
cte_starts = list(re.finditer(cte_start_pattern, query_normalized, re.IGNORECASE))
|
|
298
|
+
|
|
299
|
+
for i, match in enumerate(cte_starts):
|
|
300
|
+
cte_name = match.group(1).strip()
|
|
301
|
+
start_pos = match.end() # Position after the opening (
|
|
302
|
+
|
|
303
|
+
# Find the closing parenthesis for this CTE by tracking paren depth
|
|
304
|
+
paren_depth = 1
|
|
305
|
+
pos = start_pos
|
|
306
|
+
cte_end_pos = start_pos
|
|
307
|
+
|
|
308
|
+
while pos < len(query_normalized) and paren_depth > 0:
|
|
309
|
+
if query_normalized[pos] == '(':
|
|
310
|
+
paren_depth += 1
|
|
311
|
+
elif query_normalized[pos] == ')':
|
|
312
|
+
paren_depth -= 1
|
|
313
|
+
if paren_depth == 0:
|
|
314
|
+
cte_end_pos = pos
|
|
315
|
+
break
|
|
316
|
+
pos += 1
|
|
317
|
+
|
|
318
|
+
# Extract the CTE content
|
|
319
|
+
cte_content = query_normalized[start_pos:cte_end_pos]
|
|
320
|
+
|
|
321
|
+
# Look for FROM clauses in the CTE content
|
|
322
|
+
# Pattern: FROM schema.table_name or FROM table_name
|
|
323
|
+
from_pattern = r'FROM\s+([\w.-]+)'
|
|
324
|
+
from_matches = list(re.finditer(from_pattern, cte_content, re.IGNORECASE))
|
|
325
|
+
|
|
326
|
+
if from_matches:
|
|
327
|
+
# Use the first FROM clause (primary table)
|
|
328
|
+
source_table = from_matches[0].group(1).strip()
|
|
329
|
+
cte_to_source[cte_name.lower()] = source_table
|
|
330
|
+
|
|
331
|
+
# Recursively resolve CTEs that reference other CTEs
|
|
332
|
+
# (e.g., annulus_level -> extracted_casing_size -> refined_wisdom_v_csg_tbg)
|
|
333
|
+
max_iterations = 10 # Prevent infinite loops
|
|
334
|
+
for _ in range(max_iterations):
|
|
335
|
+
resolved_any = False
|
|
336
|
+
for cte_name, source_table in list(cte_to_source.items()):
|
|
337
|
+
# Check if source_table is itself a CTE
|
|
338
|
+
if source_table.lower() in cte_to_source:
|
|
339
|
+
# Resolve it
|
|
340
|
+
cte_to_source[cte_name] = cte_to_source[source_table.lower()]
|
|
341
|
+
resolved_any = True
|
|
342
|
+
if not resolved_any:
|
|
343
|
+
break
|
|
344
|
+
|
|
345
|
+
# Build list of dependency table names to look for
|
|
346
|
+
dep_names = []
|
|
347
|
+
dep_name_parts = [] # Store partial name matches for CTEs
|
|
348
|
+
for dep in dependencies:
|
|
349
|
+
if isinstance(dep, dict):
|
|
350
|
+
schema = dep.get('schema', '')
|
|
351
|
+
name = dep.get('name', '')
|
|
352
|
+
full_name = f"{schema}.{name}"
|
|
353
|
+
dep_names.append((full_name, full_name))
|
|
354
|
+
# Also add just the table name for matching
|
|
355
|
+
dep_names.append((name, full_name))
|
|
356
|
+
# Add partial name matching for CTEs (e.g., union_a_ann_barrier -> a_ann_barrier)
|
|
357
|
+
if '_' in name:
|
|
358
|
+
parts = name.split('_')
|
|
359
|
+
# Try various partial combinations
|
|
360
|
+
if name.startswith('union_'):
|
|
361
|
+
# For union_xxx patterns, match xxx
|
|
362
|
+
cte_name = name.replace('union_', '')
|
|
363
|
+
dep_names.append((cte_name, full_name))
|
|
364
|
+
# Try last few parts joined (for patterns like union_a_ann_barrier -> a_ann_barrier)
|
|
365
|
+
if len(parts) > 2:
|
|
366
|
+
for i in range(1, len(parts)):
|
|
367
|
+
partial = '_'.join(parts[i:])
|
|
368
|
+
dep_names.append((partial, full_name))
|
|
369
|
+
|
|
370
|
+
# Regex pattern to match JOIN clauses with ON
|
|
371
|
+
# Captures: (join_type) table_ref [alias] ON condition
|
|
372
|
+
join_on_pattern = r'((?:INNER|LEFT|RIGHT|FULL|CROSS)?\s*(?:OUTER)?\s*JOIN)\s+([\w.-]+)(?:\s+(?:AS\s+)?([\w]+))?\s+ON\s+([^\r\n]+?)(?=\s+(?:WHERE|GROUP|HAVING|ORDER|LIMIT|UNION|FROM|SELECT|INNER\s+JOIN|LEFT\s+JOIN|RIGHT\s+JOIN|FULL\s+JOIN|CROSS\s+JOIN|JOIN|\)|;)\s*|$)'
|
|
373
|
+
|
|
374
|
+
# Regex pattern to match JOIN clauses with USING
|
|
375
|
+
# Captures: (join_type) table_ref [alias] USING (columns)
|
|
376
|
+
join_using_pattern = r'((?:INNER|LEFT|RIGHT|FULL|CROSS)?\s*(?:OUTER)?\s*JOIN)\s+([\w.-]+)(?:\s+(?:AS\s+)?([\w]+))?\s+USING\s*\(([^)]+)\)'
|
|
377
|
+
|
|
378
|
+
# Process ON joins
|
|
379
|
+
matches = re.finditer(join_on_pattern, query_normalized, re.IGNORECASE)
|
|
380
|
+
|
|
381
|
+
# Debug: Count matches
|
|
382
|
+
match_list = list(matches)
|
|
383
|
+
if len(match_list) > 0:
|
|
384
|
+
print(f"DEBUG parse_joins: Found {len(match_list)} ON joins in query")
|
|
385
|
+
matches = iter(match_list) # Convert back to iterator
|
|
386
|
+
|
|
387
|
+
for match in matches:
|
|
388
|
+
join_type = match.group(1).strip().upper() # e.g., "LEFT JOIN"
|
|
389
|
+
table_ref = match.group(2).strip() # e.g., "schema.table_name" or "table_name"
|
|
390
|
+
# alias = match.group(3) # Optional alias
|
|
391
|
+
condition = match.group(4).strip() if match.group(4) else "" # ON condition
|
|
392
|
+
|
|
393
|
+
# Check if table_ref is a CTE - if so, resolve it to the actual source table
|
|
394
|
+
resolved_table_ref = table_ref
|
|
395
|
+
if table_ref.lower() in cte_to_source:
|
|
396
|
+
resolved_table_ref = cte_to_source[table_ref.lower()]
|
|
397
|
+
|
|
398
|
+
# Normalize resolved_table_ref - remove database prefix (e.g., database.schema.table -> schema.table)
|
|
399
|
+
# Format: database-name.schema.table or just schema.table
|
|
400
|
+
resolved_parts = resolved_table_ref.split('.')
|
|
401
|
+
if len(resolved_parts) == 3:
|
|
402
|
+
# Has database prefix, remove it
|
|
403
|
+
resolved_table_ref = f"{resolved_parts[1]}.{resolved_parts[2]}"
|
|
404
|
+
|
|
405
|
+
# Try to match this table_ref to one of our dependencies
|
|
406
|
+
matched_dep = None
|
|
407
|
+
best_match_len = 0
|
|
408
|
+
|
|
409
|
+
for search_name, full_dep_name in dep_names:
|
|
410
|
+
# Check if the search_name matches the resolved table_ref in the JOIN
|
|
411
|
+
# Use case-insensitive comparison and check both directions
|
|
412
|
+
if (search_name.lower() in resolved_table_ref.lower() or
|
|
413
|
+
resolved_table_ref.lower() in search_name.lower() or
|
|
414
|
+
search_name.lower() == resolved_table_ref.lower()):
|
|
415
|
+
# Prefer longer matches (more specific)
|
|
416
|
+
if len(search_name) > best_match_len:
|
|
417
|
+
matched_dep = full_dep_name
|
|
418
|
+
best_match_len = len(search_name)
|
|
419
|
+
|
|
420
|
+
if matched_dep and condition:
|
|
421
|
+
# Clean up condition - remove extra whitespace
|
|
422
|
+
condition = ' '.join(condition.split())
|
|
423
|
+
join_info[matched_dep] = {
|
|
424
|
+
'type': join_type,
|
|
425
|
+
'condition': condition
|
|
426
|
+
}
|
|
427
|
+
# Debug output
|
|
428
|
+
print(f"DEBUG: Matched JOIN: {join_type} {table_ref} -> {matched_dep}")
|
|
429
|
+
|
|
430
|
+
# Process USING joins
|
|
431
|
+
matches = re.finditer(join_using_pattern, query_normalized, re.IGNORECASE)
|
|
432
|
+
|
|
433
|
+
for match in matches:
|
|
434
|
+
join_type = match.group(1).strip().upper() # e.g., "LEFT JOIN"
|
|
435
|
+
table_ref = match.group(2).strip() # e.g., "schema.table_name" or "table_name"
|
|
436
|
+
# alias = match.group(3) # Optional alias
|
|
437
|
+
columns = match.group(4).strip() if match.group(4) else "" # USING columns
|
|
438
|
+
|
|
439
|
+
# Check if table_ref is a CTE - if so, resolve it to the actual source table
|
|
440
|
+
resolved_table_ref = table_ref
|
|
441
|
+
if table_ref.lower() in cte_to_source:
|
|
442
|
+
resolved_table_ref = cte_to_source[table_ref.lower()]
|
|
443
|
+
|
|
444
|
+
# Normalize resolved_table_ref - remove database prefix (e.g., database.schema.table -> schema.table)
|
|
445
|
+
resolved_parts = resolved_table_ref.split('.')
|
|
446
|
+
if len(resolved_parts) == 3:
|
|
447
|
+
# Has database prefix, remove it
|
|
448
|
+
resolved_table_ref = f"{resolved_parts[1]}.{resolved_parts[2]}"
|
|
449
|
+
|
|
450
|
+
# Try to match this table_ref to one of our dependencies
|
|
451
|
+
matched_dep = None
|
|
452
|
+
best_match_len = 0
|
|
453
|
+
|
|
454
|
+
for search_name, full_dep_name in dep_names:
|
|
455
|
+
if (search_name.lower() in resolved_table_ref.lower() or
|
|
456
|
+
resolved_table_ref.lower() in search_name.lower() or
|
|
457
|
+
search_name.lower() == resolved_table_ref.lower()):
|
|
458
|
+
if len(search_name) > best_match_len:
|
|
459
|
+
matched_dep = full_dep_name
|
|
460
|
+
best_match_len = len(search_name)
|
|
461
|
+
|
|
462
|
+
if matched_dep and columns:
|
|
463
|
+
# Format as "USING (columns)"
|
|
464
|
+
condition = f"USING ({columns})"
|
|
465
|
+
join_info[matched_dep] = {
|
|
466
|
+
'type': join_type,
|
|
467
|
+
'condition': condition
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return join_info
|
|
471
|
+
|
|
272
472
|
def main():
|
|
473
|
+
print("DEBUG: main() started")
|
|
273
474
|
graph = get_dataform_graph()
|
|
274
475
|
if not graph:
|
|
476
|
+
print("DEBUG: No graph returned from get_dataform_graph")
|
|
275
477
|
return
|
|
478
|
+
print("DEBUG: Graph loaded successfully")
|
|
276
479
|
|
|
277
480
|
tables = graph.get("tables", [])
|
|
278
481
|
|
|
@@ -288,13 +491,23 @@ def main():
|
|
|
288
491
|
full_name = normalize_name(tgt) # e.g. "dataset.table"
|
|
289
492
|
short_name = tgt.get("name")
|
|
290
493
|
|
|
494
|
+
# Get dependencies
|
|
495
|
+
deps = t.get("dependencyTargets", [])
|
|
496
|
+
query = t.get("query", "")
|
|
497
|
+
|
|
498
|
+
# Debug: Check if query exists for specific table
|
|
499
|
+
if 'cmt_perf' in short_name:
|
|
500
|
+
print(f"DEBUG main: Processing {full_name}, query length: {len(query)}, deps: {len(deps)}")
|
|
501
|
+
|
|
502
|
+
# Parse JOIN information from query
|
|
503
|
+
join_info = parse_joins_from_query(query, deps)
|
|
504
|
+
|
|
291
505
|
# Store using full name as key
|
|
292
506
|
table_lookup[full_name] = {
|
|
293
507
|
"type": t.get("type"),
|
|
294
|
-
"dependencies":
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
"dependents": []
|
|
508
|
+
"dependencies": deps,
|
|
509
|
+
"dependents": [],
|
|
510
|
+
"join_info": join_info
|
|
298
511
|
}
|
|
299
512
|
|
|
300
513
|
# Also store short name pointer if it doesn't conflict?
|
|
@@ -337,7 +550,14 @@ def main():
|
|
|
337
550
|
deps = info['dependencies']
|
|
338
551
|
print(f" Dependencies ({len(deps)}):")
|
|
339
552
|
for d in deps:
|
|
340
|
-
|
|
553
|
+
dep_name = normalize_name(d)
|
|
554
|
+
print(f" <- {dep_name}")
|
|
555
|
+
|
|
556
|
+
# Print JOIN information if available
|
|
557
|
+
join_info = info.get('join_info', {})
|
|
558
|
+
if dep_name in join_info:
|
|
559
|
+
j = join_info[dep_name]
|
|
560
|
+
print(f" {j['type']} ON {j['condition']}")
|
|
341
561
|
|
|
342
562
|
depts = info['dependents']
|
|
343
563
|
print(f" Dependents ({len(depts)}):")
|
|
@@ -14,10 +14,11 @@ def parse_dependencies_report(report_path: str) -> Dict[str, dict]:
|
|
|
14
14
|
report_path: Path to dependencies report file
|
|
15
15
|
|
|
16
16
|
Returns:
|
|
17
|
-
Dictionary mapping table names to their info (type, dependencies, dependents)
|
|
17
|
+
Dictionary mapping table names to their info (type, dependencies, dependents, join_info)
|
|
18
18
|
"""
|
|
19
19
|
tables = {}
|
|
20
20
|
current_table = None
|
|
21
|
+
current_dep = None
|
|
21
22
|
|
|
22
23
|
# Try different encodings
|
|
23
24
|
encodings = ['utf-8', 'utf-16', 'cp1252', 'latin-1']
|
|
@@ -47,10 +48,12 @@ def parse_dependencies_report(report_path: str) -> Dict[str, dict]:
|
|
|
47
48
|
table_name = table_match.group(1)
|
|
48
49
|
table_type = table_match.group(2)
|
|
49
50
|
current_table = table_name
|
|
51
|
+
current_dep = None
|
|
50
52
|
tables[current_table] = {
|
|
51
53
|
'type': table_type,
|
|
52
54
|
'dependencies': [],
|
|
53
|
-
'dependents': []
|
|
55
|
+
'dependents': [],
|
|
56
|
+
'join_info': {}
|
|
54
57
|
}
|
|
55
58
|
continue
|
|
56
59
|
|
|
@@ -59,11 +62,26 @@ def parse_dependencies_report(report_path: str) -> Dict[str, dict]:
|
|
|
59
62
|
dep = line.strip().replace('<- ', '').strip()
|
|
60
63
|
if dep:
|
|
61
64
|
tables[current_table]['dependencies'].append(dep)
|
|
65
|
+
current_dep = dep
|
|
66
|
+
|
|
67
|
+
# Match join info line (indented further, contains JOIN and ON)
|
|
68
|
+
elif current_table and current_dep and 'JOIN' in line and 'ON' in line:
|
|
69
|
+
join_line = line.strip()
|
|
70
|
+
# Parse join info: "LEFT JOIN ON condition"
|
|
71
|
+
join_match = re.match(r'(\w+\s+JOIN)\s+ON\s+(.+)', join_line)
|
|
72
|
+
if join_match:
|
|
73
|
+
join_type = join_match.group(1)
|
|
74
|
+
join_condition = join_match.group(2)
|
|
75
|
+
tables[current_table]['join_info'][current_dep] = {
|
|
76
|
+
'type': join_type,
|
|
77
|
+
'condition': join_condition
|
|
78
|
+
}
|
|
62
79
|
|
|
63
80
|
# Match dependent line
|
|
64
|
-
|
|
81
|
+
elif current_table and '->' in line:
|
|
65
82
|
dep = line.strip().replace('-> ', '').strip()
|
|
66
83
|
if dep:
|
|
67
84
|
tables[current_table]['dependents'].append(dep)
|
|
85
|
+
current_dep = None
|
|
68
86
|
|
|
69
87
|
return tables
|
|
@@ -11,6 +11,7 @@ def parse_dependencies_report(report_path):
|
|
|
11
11
|
"""Parse the dependencies_report.txt file"""
|
|
12
12
|
tables = {}
|
|
13
13
|
current_table = None
|
|
14
|
+
current_dep = None
|
|
14
15
|
|
|
15
16
|
# Try different encodings
|
|
16
17
|
encodings = ['utf-8', 'utf-16', 'cp1252', 'latin-1']
|
|
@@ -36,10 +37,12 @@ def parse_dependencies_report(report_path):
|
|
|
36
37
|
table_name = table_match.group(1)
|
|
37
38
|
table_type = table_match.group(2)
|
|
38
39
|
current_table = table_name
|
|
40
|
+
current_dep = None
|
|
39
41
|
tables[current_table] = {
|
|
40
42
|
'type': table_type,
|
|
41
43
|
'dependencies': [],
|
|
42
|
-
'dependents': []
|
|
44
|
+
'dependents': [],
|
|
45
|
+
'join_info': {}
|
|
43
46
|
}
|
|
44
47
|
continue
|
|
45
48
|
|
|
@@ -48,12 +51,27 @@ def parse_dependencies_report(report_path):
|
|
|
48
51
|
dep = line.strip().replace('<- ', '').strip()
|
|
49
52
|
if dep:
|
|
50
53
|
tables[current_table]['dependencies'].append(dep)
|
|
54
|
+
current_dep = dep
|
|
55
|
+
|
|
56
|
+
# Match join info line (indented further, contains JOIN and ON)
|
|
57
|
+
elif current_table and current_dep and 'JOIN' in line and 'ON' in line:
|
|
58
|
+
join_line = line.strip()
|
|
59
|
+
# Parse join info: "LEFT JOIN ON condition"
|
|
60
|
+
join_match = re.match(r'(\w+\s+JOIN)\s+ON\s+(.+)', join_line)
|
|
61
|
+
if join_match:
|
|
62
|
+
join_type = join_match.group(1)
|
|
63
|
+
join_condition = join_match.group(2)
|
|
64
|
+
tables[current_table]['join_info'][current_dep] = {
|
|
65
|
+
'type': join_type,
|
|
66
|
+
'condition': join_condition
|
|
67
|
+
}
|
|
51
68
|
|
|
52
69
|
# Match dependent line
|
|
53
|
-
|
|
70
|
+
elif current_table and '->' in line:
|
|
54
71
|
dep = line.strip().replace('-> ', '').strip()
|
|
55
72
|
if dep:
|
|
56
73
|
tables[current_table]['dependents'].append(dep)
|
|
74
|
+
current_dep = None
|
|
57
75
|
|
|
58
76
|
return tables
|
|
59
77
|
|
|
@@ -159,6 +177,8 @@ def generate_svg_manual(table_name, table_info, all_tables, svg_file):
|
|
|
159
177
|
' <style>',
|
|
160
178
|
' .node-text { font-family: Arial, sans-serif; font-size: 12px; fill: #333; }',
|
|
161
179
|
' .type-badge { font-family: Arial, sans-serif; font-size: 10px; fill: #666; }',
|
|
180
|
+
' .join-label { font-family: Arial, sans-serif; font-size: 10px; font-weight: bold; }',
|
|
181
|
+
' .join-condition { font-family: Arial, sans-serif; font-size: 9px; }',
|
|
162
182
|
' </style>',
|
|
163
183
|
]
|
|
164
184
|
|
|
@@ -225,6 +245,29 @@ def generate_svg_manual(table_name, table_info, all_tables, svg_file):
|
|
|
225
245
|
# Type badge
|
|
226
246
|
badge_y = pos['y'] + 20 if schema_name else pos['y'] + 15
|
|
227
247
|
svg_lines.append(f' <text x="{pos["x"]}" y="{badge_y}" text-anchor="middle" class="type-badge">{pos["type"]}</text>')
|
|
248
|
+
|
|
249
|
+
# Add JOIN information below dependency nodes (not the center table)
|
|
250
|
+
if node_name != table_name and node_name in dependencies:
|
|
251
|
+
join_info = table_info.get('join_info', {}).get(node_name)
|
|
252
|
+
if join_info:
|
|
253
|
+
join_y = pos['y'] + node_height // 2 + 15
|
|
254
|
+
join_text = f"{join_info['type']}"
|
|
255
|
+
|
|
256
|
+
# Truncate condition if too long
|
|
257
|
+
condition = join_info['condition']
|
|
258
|
+
if len(condition) > 35:
|
|
259
|
+
condition = condition[:32] + '...'
|
|
260
|
+
|
|
261
|
+
# Draw JOIN type
|
|
262
|
+
text_width = len(join_text) * 6
|
|
263
|
+
svg_lines.append(f' <rect x="{pos["x"] - text_width//2 - 4}" y="{join_y - 12}" width="{text_width + 8}" height="14" fill="white" opacity="0.9" rx="2" />')
|
|
264
|
+
svg_lines.append(f' <text x="{pos["x"]}" y="{join_y}" text-anchor="middle" class="join-label" fill="#d32f2f">{join_text}</text>')
|
|
265
|
+
|
|
266
|
+
# Draw JOIN condition on second line
|
|
267
|
+
join_y += 16
|
|
268
|
+
cond_width = len(condition) * 5
|
|
269
|
+
svg_lines.append(f' <rect x="{pos["x"] - cond_width//2 - 4}" y="{join_y - 12}" width="{cond_width + 8}" height="14" fill="white" opacity="0.9" rx="2" />')
|
|
270
|
+
svg_lines.append(f' <text x="{pos["x"]}" y="{join_y}" text-anchor="middle" class="join-condition" fill="#666">{condition}</text>')
|
|
228
271
|
|
|
229
272
|
svg_lines.append('</svg>')
|
|
230
273
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|