sql-dag-flow 0.4.7__tar.gz → 0.4.8__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.
Files changed (22) hide show
  1. {sql_dag_flow-0.4.7/src/sql_dag_flow.egg-info → sql_dag_flow-0.4.8}/PKG-INFO +7 -4
  2. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/README.md +6 -3
  3. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/pyproject.toml +1 -1
  4. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow/parser.py +114 -0
  5. sql_dag_flow-0.4.7/src/sql_dag_flow/static/assets/index-BhG0Vsh6.js → sql_dag_flow-0.4.8/src/sql_dag_flow/static/assets/index-SW0VVE5U.js +53 -53
  6. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow/static/index.html +1 -1
  7. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8/src/sql_dag_flow.egg-info}/PKG-INFO +7 -4
  8. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow.egg-info/SOURCES.txt +1 -1
  9. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/LICENSE +0 -0
  10. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/MANIFEST.in +0 -0
  11. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/setup.cfg +0 -0
  12. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow/__init__.py +0 -0
  13. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow/main.py +0 -0
  14. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow/static/assets/index-BM6GPh_j.css +0 -0
  15. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow/static/vite.svg +0 -0
  16. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow/test_api_endpoints.py +0 -0
  17. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow/test_parser.py +0 -0
  18. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow/verify_counts.py +0 -0
  19. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow.egg-info/dependency_links.txt +0 -0
  20. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow.egg-info/entry_points.txt +0 -0
  21. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/src/sql_dag_flow.egg-info/requires.txt +0 -0
  22. {sql_dag_flow-0.4.7 → sql_dag_flow-0.4.8}/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.4.7
3
+ Version: 0.4.8
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
@@ -89,12 +89,15 @@ Specially optimized for the **Medallion Architecture** (Bronze, Silver, Gold) an
89
89
  ### 📊 Discovery & Analysis Tools
90
90
  * **Impact Analysis**: Visualize blast radius before making changes. Highlights downstream models, column usage, and risk levels.
91
91
  * **Diff View on Refresh**: Automatically summarizes added, removed, and modified nodes/edges after code changes.
92
- * **Column Usage Tracking**: Schema Preview shows which specific columns are used by downstream consumers.
92
+ * **Column Usage Tracking (Improved in v0.4.8 🔧)**: Schema Preview shows which specific columns are used by downstream consumers. Now tracks both qualified (`u.name`) and unqualified (`name`) column references for accurate counts.
93
93
  * **Staleness Detection**: Automatically flags inactive models (`Last Modified > 90d` = Stale) to help clean up legacy pipelines.
94
94
  * **Business Rule Extraction**: Automatically detects and displays WHERE filters, CASE logic, HAVING clauses, and aggregations from each SQL model.
95
95
  * **Complexity Scoring**: Weighted metric per node (JOINs×3, CTEs×2, Subqueries×3, Filters×1, CASE×2, Aggregations×1, UNIONs×2) with color-coded badges (🟢 Low, 🟡 Medium, 🟠 High, 🔴 Very High). Toggleable via ⚡ button.
96
+ * **Node Comparison**: Select exactly 2 nodes and compare them side-by-side. Highlights differences across metadata, schema columns (shared vs unique), dependencies (Venn-style), complexity scores (with delta indicators), business rules, and SQL content (synced scroll).
96
97
  * **Statistics Panel**: Centered popup with layer distribution bars, edge/source/sink/orphan counts, project/dataset tree, and architecture health validation (now detects **circular dependencies**).
97
- * **Schema Preview**: Extracts columns from DDL, CTAS (`CREATE TABLE AS SELECT`), and `CREATE VIEW` statements.
98
+ * **Schema Extraction (Improved in v0.4.8 🔧)**: Backend AST-based extraction via `sqlglot` handles DDL, CTAS, `CREATE VIEW AS`, CTEs, window functions, CASE expressions, and `SELECT *`.
99
+ * **Batch Hide from Toolbar (New in v0.4.8 🆕)**: Select multiple nodes → click "Hide" in the selection toolbar to hide them all at once.
100
+ * **Discovery Mode Fix (v0.4.8 🔧)**: Ghost nodes from Discovery Mode are now hidden when their connected source nodes are hidden, preventing orphan ghost nodes.
98
101
 
99
102
  ### 🎨 Linear-Inspired UI (New in v0.4.6 ✨)
100
103
  * **Design Token System**: ~80 CSS custom properties for consistent theming across all components.
@@ -139,7 +142,7 @@ Install easily via `pip`:
139
142
  pip install sql-dag-flow
140
143
  ```
141
144
 
142
- To update to the latest version (**v0.4.6**):
145
+ To update to the latest version (**v0.4.8**):
143
146
 
144
147
  ```bash
145
148
  pip install --upgrade sql-dag-flow
@@ -64,12 +64,15 @@ Specially optimized for the **Medallion Architecture** (Bronze, Silver, Gold) an
64
64
  ### 📊 Discovery & Analysis Tools
65
65
  * **Impact Analysis**: Visualize blast radius before making changes. Highlights downstream models, column usage, and risk levels.
66
66
  * **Diff View on Refresh**: Automatically summarizes added, removed, and modified nodes/edges after code changes.
67
- * **Column Usage Tracking**: Schema Preview shows which specific columns are used by downstream consumers.
67
+ * **Column Usage Tracking (Improved in v0.4.8 🔧)**: Schema Preview shows which specific columns are used by downstream consumers. Now tracks both qualified (`u.name`) and unqualified (`name`) column references for accurate counts.
68
68
  * **Staleness Detection**: Automatically flags inactive models (`Last Modified > 90d` = Stale) to help clean up legacy pipelines.
69
69
  * **Business Rule Extraction**: Automatically detects and displays WHERE filters, CASE logic, HAVING clauses, and aggregations from each SQL model.
70
70
  * **Complexity Scoring**: Weighted metric per node (JOINs×3, CTEs×2, Subqueries×3, Filters×1, CASE×2, Aggregations×1, UNIONs×2) with color-coded badges (🟢 Low, 🟡 Medium, 🟠 High, 🔴 Very High). Toggleable via ⚡ button.
71
+ * **Node Comparison**: Select exactly 2 nodes and compare them side-by-side. Highlights differences across metadata, schema columns (shared vs unique), dependencies (Venn-style), complexity scores (with delta indicators), business rules, and SQL content (synced scroll).
71
72
  * **Statistics Panel**: Centered popup with layer distribution bars, edge/source/sink/orphan counts, project/dataset tree, and architecture health validation (now detects **circular dependencies**).
72
- * **Schema Preview**: Extracts columns from DDL, CTAS (`CREATE TABLE AS SELECT`), and `CREATE VIEW` statements.
73
+ * **Schema Extraction (Improved in v0.4.8 🔧)**: Backend AST-based extraction via `sqlglot` handles DDL, CTAS, `CREATE VIEW AS`, CTEs, window functions, CASE expressions, and `SELECT *`.
74
+ * **Batch Hide from Toolbar (New in v0.4.8 🆕)**: Select multiple nodes → click "Hide" in the selection toolbar to hide them all at once.
75
+ * **Discovery Mode Fix (v0.4.8 🔧)**: Ghost nodes from Discovery Mode are now hidden when their connected source nodes are hidden, preventing orphan ghost nodes.
73
76
 
74
77
  ### 🎨 Linear-Inspired UI (New in v0.4.6 ✨)
75
78
  * **Design Token System**: ~80 CSS custom properties for consistent theming across all components.
@@ -114,7 +117,7 @@ Install easily via `pip`:
114
117
  pip install sql-dag-flow
115
118
  ```
116
119
 
117
- To update to the latest version (**v0.4.6**):
120
+ To update to the latest version (**v0.4.8**):
118
121
 
119
122
  ```bash
120
123
  pip install --upgrade sql-dag-flow
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sql-dag-flow"
7
- version = "0.4.7"
7
+ version = "0.4.8"
8
8
  description = "A sophisticated SQL lineage visualization tool for Medallion Architectures."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -5,6 +5,83 @@ import sqlglot
5
5
  from sqlglot import exp
6
6
  import networkx as nx
7
7
 
8
+
9
+ def extract_output_columns(parsed, dialect="bigquery"):
10
+ """
11
+ Extract output column schema from a parsed SQL AST.
12
+ Returns a list of {"name": str, "type": str} dicts.
13
+
14
+ Handles:
15
+ - DDL with explicit column definitions (CREATE TABLE t (col TYPE))
16
+ - CTAS / CREATE VIEW AS SELECT
17
+ - WITH...SELECT (final SELECT after CTEs)
18
+ - Standalone SELECT
19
+ - Window functions, nested functions, CASE expressions
20
+ - SELECT * (returns [{"name": "*", "type": "ALL"}])
21
+ """
22
+ columns = []
23
+
24
+ # Case 1: DDL with explicit column definitions
25
+ if isinstance(parsed, exp.Create):
26
+ schema_node = parsed.this
27
+ if isinstance(schema_node, exp.Schema):
28
+ for col_def in schema_node.expressions:
29
+ if isinstance(col_def, exp.ColumnDef):
30
+ col_name = col_def.name
31
+ col_type_node = col_def.args.get("kind")
32
+ type_str = col_type_node.sql(dialect=dialect) if col_type_node else "UNKNOWN"
33
+ columns.append({"name": col_name, "type": type_str})
34
+ if columns:
35
+ return columns
36
+
37
+ # Case 2: Find the final SELECT statement
38
+ # For CTAS, CREATE VIEW AS, WITH...SELECT, standalone SELECT
39
+ # We want the outermost SELECT that isn't inside a CTE or subquery
40
+ select_node = None
41
+
42
+ if isinstance(parsed, exp.Create):
43
+ # For CTAS / CREATE VIEW AS: get the SELECT inside the CREATE
44
+ inner = parsed.expression
45
+ if inner:
46
+ if isinstance(inner, exp.Select):
47
+ select_node = inner
48
+ elif hasattr(inner, 'find'):
49
+ select_node = inner.find(exp.Select)
50
+
51
+ if not select_node:
52
+ # For standalone queries: find WITH wrapper or direct SELECT
53
+ # Walk to find the top-level Select (not nested in subquery)
54
+ if isinstance(parsed, exp.Select):
55
+ select_node = parsed
56
+ else:
57
+ # Could be a WITH or UNION — find the first select
58
+ select_node = parsed.find(exp.Select)
59
+
60
+ if select_node:
61
+ for expr in select_node.expressions:
62
+ try:
63
+ if isinstance(expr, exp.Star):
64
+ columns.append({"name": "*", "type": "ALL"})
65
+ elif isinstance(expr, exp.Alias):
66
+ alias_name = expr.alias
67
+ inner_expr = expr.this
68
+ type_str = inner_expr.sql(dialect=dialect, pretty=False)
69
+ # Truncate very long expressions for readability
70
+ if len(type_str) > 80:
71
+ type_str = type_str[:77] + "..."
72
+ columns.append({"name": alias_name, "type": type_str})
73
+ elif isinstance(expr, exp.Column):
74
+ columns.append({"name": expr.name, "type": "column"})
75
+ else:
76
+ # Computed expression without alias
77
+ expr_sql = expr.sql(dialect=dialect, pretty=False)
78
+ name = expr_sql[:40] + "..." if len(expr_sql) > 40 else expr_sql
79
+ columns.append({"name": name, "type": "expression"})
80
+ except Exception:
81
+ continue
82
+
83
+ return columns
84
+
8
85
  def parse_sql_files(directory, allowed_subfolders=None, dialect="bigquery"):
9
86
  """
10
87
  Recursively scans a directory for .sql files and parses them.
@@ -328,6 +405,9 @@ def parse_sql_files(directory, allowed_subfolders=None, dialect="bigquery"):
328
405
  alias_map[t_name] = t_full
329
406
 
330
407
  # Extract column references with their table qualifier
408
+ # Also capture unqualified columns and assign them to source tables
409
+ unqualified_columns = set()
410
+
331
411
  for col in parsed.find_all(exp.Column):
332
412
  col_name = col.name
333
413
  col_table = col.table # The table qualifier (alias or name)
@@ -336,6 +416,36 @@ def parse_sql_files(directory, allowed_subfolders=None, dialect="bigquery"):
336
416
  if source not in column_references:
337
417
  column_references[source] = set()
338
418
  column_references[source].add(col_name)
419
+ elif not col_table:
420
+ # Unqualified column — track separately
421
+ unqualified_columns.add(col_name)
422
+
423
+ # Assign unqualified columns to source tables
424
+ # Filter out CTEs and the target table itself (both short and full name)
425
+ source_tables = [
426
+ v for k, v in alias_map.items()
427
+ if v != target_table_name
428
+ and v.split('.')[-1] != target_table_name
429
+ and v not in defined_ctes
430
+ and v.split('.')[-1] not in defined_ctes
431
+ ]
432
+ # Deduplicate (aliases may point to same table)
433
+ source_tables = list(set(source_tables))
434
+
435
+ if unqualified_columns and source_tables:
436
+ if len(source_tables) == 1:
437
+ # Single source: assign all unqualified columns to it
438
+ src = source_tables[0]
439
+ if src not in column_references:
440
+ column_references[src] = set()
441
+ column_references[src].update(unqualified_columns)
442
+ else:
443
+ # Multiple sources: assign to ALL sources (best effort)
444
+ # The UI will show them as "used" which is better than missing
445
+ for src in source_tables:
446
+ if src not in column_references:
447
+ column_references[src] = set()
448
+ column_references[src].update(unqualified_columns)
339
449
 
340
450
  # Convert sets to sorted lists for JSON serialization
341
451
  column_references = {k: sorted(list(v)) for k, v in column_references.items()}
@@ -389,6 +499,7 @@ def parse_sql_files(directory, allowed_subfolders=None, dialect="bigquery"):
389
499
  "content": sql_content,
390
500
  "ctes": defined_ctes,
391
501
  "cte_deps": cte_deps,
502
+ "schema": extract_output_columns(parsed, dialect),
392
503
  "business_rules": business_rules,
393
504
  "complexity": complexity_breakdown,
394
505
  "column_references": column_references,
@@ -637,6 +748,9 @@ def build_graph(tables, discovery_mode=False, expanded_nodes=None):
637
748
  source_node_id = lookup.get(source_ref.split(".")[-1])
638
749
 
639
750
  if source_node_id and source_node_id in all_nodes_data:
751
+ # Skip self-references (a model shouldn't be its own consumer)
752
+ if source_node_id == consumer_id:
753
+ continue
640
754
  if source_node_id not in column_consumers:
641
755
  column_consumers[source_node_id] = {}
642
756
  for col in columns: