sql-dag-flow 0.5.0__tar.gz → 0.5.2__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.
- {sql_dag_flow-0.5.0/src/sql_dag_flow.egg-info → sql_dag_flow-0.5.2}/PKG-INFO +3 -2
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/README.md +2 -1
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/pyproject.toml +1 -1
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow/parser.py +97 -55
- sql_dag_flow-0.5.0/src/sql_dag_flow/static/assets/index-ZylJfG6u.js → sql_dag_flow-0.5.2/src/sql_dag_flow/static/assets/index-BPI03jTK.js +21 -21
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow/static/index.html +1 -1
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2/src/sql_dag_flow.egg-info}/PKG-INFO +3 -2
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow.egg-info/SOURCES.txt +1 -1
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/LICENSE +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/MANIFEST.in +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/setup.cfg +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow/__init__.py +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow/main.py +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow/static/assets/index-BM6GPh_j.css +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow/static/vite.svg +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow/test_api_endpoints.py +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow/test_parser.py +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow/verify_counts.py +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow.egg-info/dependency_links.txt +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow.egg-info/entry_points.txt +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow.egg-info/requires.txt +0 -0
- {sql_dag_flow-0.5.0 → sql_dag_flow-0.5.2}/src/sql_dag_flow.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-dag-flow
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: A sophisticated SQL lineage visualization tool for Medallion Architectures.
|
|
5
5
|
Author-email: Flavio Sandoval <dsandovalflavio@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -98,6 +98,7 @@ Specially optimized for the **Medallion Architecture** (Bronze, Silver, Gold) an
|
|
|
98
98
|
* **Schema Extraction**: Backend AST-based extraction via `sqlglot` handles DDL, CTAS, `CREATE VIEW AS`, CTEs, window functions, CASE expressions, and `SELECT *`.
|
|
99
99
|
* **Column-Level Lineage (New in v0.4.9 🆕)**: Traces how each output column derives from source columns. Shows transformation chain (e.g., `order_timestamp ← orders_raw.order_date via CAST(... AS DATETIME)`).
|
|
100
100
|
* **SQL Syntax Validation (New in v0.4.9 🆕)**: Detects SQL parse errors and displays structured warnings with line/column references. Shows ⚠️ badge on nodes with syntax issues.
|
|
101
|
+
* **Large DAG Support & Safe Cycle Detection (New in v0.5.1 🚀)**: Optimized cycle detection algorithm prevents backend hanging and "Failed to fetch" browser errors when analyzing massive projects (50+ nodes). Employs quick DAG verifications and bounded iterator loops.
|
|
101
102
|
* **Batch Hide from Toolbar**: Select multiple nodes → click "Hide" in the selection toolbar to hide them all at once.
|
|
102
103
|
* **Discovery Mode Fix**: Ghost nodes from Discovery Mode are now hidden when their connected source nodes are hidden, preventing orphan ghost nodes.
|
|
103
104
|
|
|
@@ -144,7 +145,7 @@ Install easily via `pip`:
|
|
|
144
145
|
pip install sql-dag-flow
|
|
145
146
|
```
|
|
146
147
|
|
|
147
|
-
To update to the latest version (**v0.
|
|
148
|
+
To update to the latest version (**v0.5.1**):
|
|
148
149
|
|
|
149
150
|
```bash
|
|
150
151
|
pip install --upgrade sql-dag-flow
|
|
@@ -73,6 +73,7 @@ Specially optimized for the **Medallion Architecture** (Bronze, Silver, Gold) an
|
|
|
73
73
|
* **Schema Extraction**: Backend AST-based extraction via `sqlglot` handles DDL, CTAS, `CREATE VIEW AS`, CTEs, window functions, CASE expressions, and `SELECT *`.
|
|
74
74
|
* **Column-Level Lineage (New in v0.4.9 🆕)**: Traces how each output column derives from source columns. Shows transformation chain (e.g., `order_timestamp ← orders_raw.order_date via CAST(... AS DATETIME)`).
|
|
75
75
|
* **SQL Syntax Validation (New in v0.4.9 🆕)**: Detects SQL parse errors and displays structured warnings with line/column references. Shows ⚠️ badge on nodes with syntax issues.
|
|
76
|
+
* **Large DAG Support & Safe Cycle Detection (New in v0.5.1 🚀)**: Optimized cycle detection algorithm prevents backend hanging and "Failed to fetch" browser errors when analyzing massive projects (50+ nodes). Employs quick DAG verifications and bounded iterator loops.
|
|
76
77
|
* **Batch Hide from Toolbar**: Select multiple nodes → click "Hide" in the selection toolbar to hide them all at once.
|
|
77
78
|
* **Discovery Mode Fix**: Ghost nodes from Discovery Mode are now hidden when their connected source nodes are hidden, preventing orphan ghost nodes.
|
|
78
79
|
|
|
@@ -119,7 +120,7 @@ Install easily via `pip`:
|
|
|
119
120
|
pip install sql-dag-flow
|
|
120
121
|
```
|
|
121
122
|
|
|
122
|
-
To update to the latest version (**v0.
|
|
123
|
+
To update to the latest version (**v0.5.1**):
|
|
123
124
|
|
|
124
125
|
```bash
|
|
125
126
|
pip install --upgrade sql-dag-flow
|
|
@@ -566,18 +566,28 @@ def parse_sql_files(directory, allowed_subfolders=None, dialect="bigquery"):
|
|
|
566
566
|
global_schema["default"][label] = col_dict
|
|
567
567
|
|
|
568
568
|
# Re-extract column_references using qualify_columns for precision
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
569
|
+
# Protected with per-table time budget to prevent hanging on large projects
|
|
570
|
+
qualify_tables = [(tid, tdata) for tid, tdata in tables.items()
|
|
571
|
+
if not tdata.get("error") and tdata.get("content")]
|
|
572
|
+
total_qualify = len(qualify_tables)
|
|
573
|
+
|
|
574
|
+
for idx, (tid, tdata) in enumerate(qualify_tables):
|
|
575
|
+
table_start = time.time()
|
|
576
|
+
if idx % 10 == 0 and total_qualify > 10:
|
|
577
|
+
print(f" Qualifying columns: {idx}/{total_qualify} tables...")
|
|
572
578
|
try:
|
|
573
579
|
parsed = sqlglot.parse_one(tdata["content"], read=dialect)
|
|
574
580
|
|
|
575
|
-
# Try to qualify columns using the global schema
|
|
581
|
+
# Try to qualify columns using the global schema (with 2s budget)
|
|
576
582
|
try:
|
|
577
583
|
qualified_ast = sqlglot_qualify_columns(parsed.copy(), schema=global_schema, dialect=dialect)
|
|
578
584
|
except Exception:
|
|
579
585
|
qualified_ast = parsed # Fallback to unqualified
|
|
580
586
|
|
|
587
|
+
# Check time budget
|
|
588
|
+
if time.time() - table_start > 2.0:
|
|
589
|
+
continue # Skip extraction if qualification took too long
|
|
590
|
+
|
|
581
591
|
# Re-extract column references from qualified AST
|
|
582
592
|
alias_map = {}
|
|
583
593
|
defined_ctes = set()
|
|
@@ -634,55 +644,83 @@ def parse_sql_files(directory, allowed_subfolders=None, dialect="bigquery"):
|
|
|
634
644
|
except Exception:
|
|
635
645
|
pass # Keep original column_references
|
|
636
646
|
|
|
647
|
+
if total_qualify > 10:
|
|
648
|
+
print(f" Qualifying columns: {total_qualify}/{total_qualify} done.")
|
|
649
|
+
|
|
637
650
|
# ===== Column-Level Lineage =====
|
|
638
651
|
# For each table, trace how each output column derives from source columns
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
+
# Protected: skip if project is very large (>100 tables) and cap columns per table
|
|
653
|
+
MAX_LINEAGE_TABLES = 100
|
|
654
|
+
MAX_COLS_PER_TABLE = 30
|
|
655
|
+
LINEAGE_TIME_BUDGET = 1.5 # seconds per table
|
|
656
|
+
|
|
657
|
+
lineage_tables = [(tid, tdata) for tid, tdata in tables.items()
|
|
658
|
+
if not tdata.get("error") and tdata.get("content") and tdata.get("schema")]
|
|
659
|
+
total_lineage = len(lineage_tables)
|
|
660
|
+
|
|
661
|
+
if total_lineage > MAX_LINEAGE_TABLES:
|
|
662
|
+
print(f" Skipping column lineage: {total_lineage} tables exceeds limit of {MAX_LINEAGE_TABLES}")
|
|
663
|
+
else:
|
|
664
|
+
for idx, (tid, tdata) in enumerate(lineage_tables):
|
|
665
|
+
table_start = time.time()
|
|
666
|
+
if idx % 10 == 0 and total_lineage > 10:
|
|
667
|
+
print(f" Column lineage: {idx}/{total_lineage} tables...")
|
|
668
|
+
|
|
669
|
+
schema_cols = tdata.get("schema", [])
|
|
670
|
+
if not schema_cols:
|
|
652
671
|
continue
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
source_table = ""
|
|
665
|
-
if child.source and isinstance(child.source, exp.Table):
|
|
666
|
-
source_table = child.source.name
|
|
667
|
-
if child.source.db:
|
|
668
|
-
source_table = f"{child.source.db}.{child.source.name}"
|
|
669
|
-
|
|
670
|
-
transform = ""
|
|
671
|
-
if child.expression and str(child.expression) != source_col:
|
|
672
|
-
transform = str(child.expression)
|
|
672
|
+
|
|
673
|
+
column_lineage = {} # col_name -> [{source_table, source_column, transform}]
|
|
674
|
+
sql_content = tdata["content"]
|
|
675
|
+
|
|
676
|
+
cols_processed = 0
|
|
677
|
+
for col_info in schema_cols:
|
|
678
|
+
# Check time budget
|
|
679
|
+
if time.time() - table_start > LINEAGE_TIME_BUDGET:
|
|
680
|
+
break
|
|
681
|
+
if cols_processed >= MAX_COLS_PER_TABLE:
|
|
682
|
+
break
|
|
673
683
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
684
|
+
col_name = col_info["name"]
|
|
685
|
+
if col_name == "*":
|
|
686
|
+
continue
|
|
687
|
+
cols_processed += 1
|
|
688
|
+
try:
|
|
689
|
+
from sqlglot.lineage import lineage as sqlglot_lineage
|
|
690
|
+
node = sqlglot_lineage(
|
|
691
|
+
col_name, sql_content,
|
|
692
|
+
schema=global_schema,
|
|
693
|
+
dialect=dialect
|
|
694
|
+
)
|
|
695
|
+
sources = []
|
|
696
|
+
for child in node.downstream:
|
|
697
|
+
source_col = child.name
|
|
698
|
+
# Extract the source table from the expression
|
|
699
|
+
source_table = ""
|
|
700
|
+
if child.source and isinstance(child.source, exp.Table):
|
|
701
|
+
source_table = child.source.name
|
|
702
|
+
if child.source.db:
|
|
703
|
+
source_table = f"{child.source.db}.{child.source.name}"
|
|
704
|
+
|
|
705
|
+
transform = ""
|
|
706
|
+
if child.expression and str(child.expression) != source_col:
|
|
707
|
+
transform = str(child.expression)
|
|
708
|
+
|
|
709
|
+
sources.append({
|
|
710
|
+
"source_table": source_table,
|
|
711
|
+
"source_column": source_col,
|
|
712
|
+
"transform": transform
|
|
713
|
+
})
|
|
714
|
+
if sources:
|
|
715
|
+
column_lineage[col_name] = sources
|
|
716
|
+
except Exception:
|
|
717
|
+
pass # Lineage not available for this column
|
|
718
|
+
|
|
719
|
+
if column_lineage:
|
|
720
|
+
tdata["column_lineage"] = column_lineage
|
|
683
721
|
|
|
684
|
-
if
|
|
685
|
-
|
|
722
|
+
if total_lineage > 10:
|
|
723
|
+
print(f" Column lineage: {total_lineage}/{total_lineage} done.")
|
|
686
724
|
|
|
687
725
|
return tables
|
|
688
726
|
|
|
@@ -961,13 +999,17 @@ def build_graph(tables, discovery_mode=False, expanded_nodes=None):
|
|
|
961
999
|
# ===== Cycle Detection =====
|
|
962
1000
|
cycles = []
|
|
963
1001
|
try:
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
for
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1002
|
+
# Check if DAG first, as simple_cycles can take exponential time on large graphs
|
|
1003
|
+
if not nx.is_directed_acyclic_graph(G):
|
|
1004
|
+
cycle_iter = nx.simple_cycles(G)
|
|
1005
|
+
for i, cycle in enumerate(cycle_iter):
|
|
1006
|
+
if i >= 20: # Cap at 20 cycles to prevent UI freezes & Memory/CPU overload
|
|
1007
|
+
break
|
|
1008
|
+
cycle_labels = []
|
|
1009
|
+
for nid in cycle:
|
|
1010
|
+
label = all_nodes_data.get(nid, {}).get("label", nid)
|
|
1011
|
+
cycle_labels.append({"id": nid, "label": label})
|
|
1012
|
+
cycles.append(cycle_labels)
|
|
971
1013
|
except Exception:
|
|
972
1014
|
pass
|
|
973
1015
|
|