sql-glider 0.1.25__tar.gz → 0.1.27__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_glider-0.1.25 → sql_glider-0.1.27}/PKG-INFO +2 -1
- {sql_glider-0.1.25 → sql_glider-0.1.27}/docs/docs/graph-lineage.md +24 -8
- {sql_glider-0.1.25 → sql_glider-0.1.27}/pyproject.toml +1 -1
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/_version.py +2 -2
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/cli.py +6 -2
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/graph/diagram_formatters.py +123 -10
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/graph/test_diagram_formatters.py +49 -7
- {sql_glider-0.1.25 → sql_glider-0.1.27}/uv.lock +8 -0
- sql_glider-0.1.25/graph.json +0 -5319
- sql_glider-0.1.25/lineage.json +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/.github/workflows/ci.yml +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/.github/workflows/docs.yml +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/.github/workflows/publish.yml +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/.gitignore +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/.python-version +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/ARCHITECTURE.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/CLAUDE.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/LICENSE +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/README.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/docs/.github/workflows/docs.yml +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/docs/docs/catalogs.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/docs/docs/index.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/docs/docs/static/plotly-dash-example.png +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/docs/docs/static/sqlglider-logo-transparent.png +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/docs/docs/templating.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/docs/zensical.toml +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/examples/plotly_viewer.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2025-12-05-column-level-lineage.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2025-12-05-reverse-lineage.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2025-12-06-config-file-support.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2025-12-06-graph-lineage.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2025-12-06-unify-single-multi-query.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2025-12-07-sample-data-model.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2025-12-07-sql-templating.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2025-12-08-tables-command.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2025-12-09-graph-query-paths.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2025-12-13-dissect-command.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2025-12-14-tables-pull-command.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2026-01-25-fix-union-lineage-chain.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2026-01-26-file-scoped-schema-context.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2026-01-28-sparksql-table-extraction.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2026-01-29-no-star-flag.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2026-01-29-resolve-schema.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2026-01-29-schema-pruning-optimization.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2026-01-29-tables-scrape-command.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/plans/2026-02-02-diagram-output-formats.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/README.md +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/business/expire_dim_customer.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/business/load_fact_orders.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/business/load_fact_payments.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/business/merge_dim_customer.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/business/merge_dim_product.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/business/update_dim_customer_metrics.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/complex/conditional_merge.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/complex/cte_insert.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/complex/multi_table_transform.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/dim_customer.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/dim_product.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/fact_orders.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/fact_payments.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/raw_addresses.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/raw_customers.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/raw_order_items.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/raw_orders.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/raw_payments.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/raw_products.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/stg_customers.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/stg_orders.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/stg_payments.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/ddl/stg_products.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/incremental/incr_fact_orders.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/incremental/incr_fact_payments.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/incremental/incr_pres_sales_summary.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/maintenance/delete_expired_customers.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/maintenance/update_product_status.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/presentation/load_pres_customer_360.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/presentation/load_pres_customer_cohort.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/presentation/load_pres_product_performance.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/presentation/load_pres_sales_summary.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/staging/load_stg_customers.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/staging/load_stg_orders.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/staging/load_stg_payments.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sample_data_model/staging/load_stg_products.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/sqlglider.toml.example +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/catalog/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/catalog/base.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/catalog/databricks.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/catalog/registry.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/dissection/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/dissection/analyzer.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/dissection/formatters.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/dissection/models.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/global_models.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/graph/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/graph/builder.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/graph/formatters.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/graph/merge.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/graph/models.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/graph/query.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/graph/serialization.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/lineage/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/lineage/analyzer.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/lineage/formatters.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/schema/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/schema/extractor.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/templating/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/templating/base.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/templating/jinja.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/templating/registry.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/templating/variables.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/utils/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/utils/config.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/utils/file_utils.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/src/sqlglider/utils/schema.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/multi_file_queries/analytics_pipeline.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/multi_file_queries/analytics_pipeline_union_merge.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/multi_file_queries/customers.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/multi_file_queries/orders.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/multi_file_queries/reports.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/multi_file_queries/view_based_merge.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/original_queries/test_cte.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/original_queries/test_cte_query.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/original_queries/test_cte_view_star.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/original_queries/test_generated_column_query.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/original_queries/test_multi.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/original_queries/test_multi_query.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/original_queries/test_single_query.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/original_queries/test_subquery.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/original_queries/test_tables.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/original_queries/test_view.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/original_queries/test_view_window_cte.sql +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/fixtures/sample_manifest.csv +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/catalog/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/catalog/test_base.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/catalog/test_databricks.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/catalog/test_registry.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/dissection/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/dissection/test_analyzer.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/dissection/test_formatters.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/dissection/test_models.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/graph/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/graph/test_builder.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/graph/test_formatters.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/graph/test_merge.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/graph/test_models.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/graph/test_query.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/graph/test_serialization.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/lineage/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/lineage/test_analyzer.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/lineage/test_formatters.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/schema/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/schema/test_extractor.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/templating/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/templating/test_base.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/templating/test_jinja.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/templating/test_registry.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/templating/test_variables.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/test_cli.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/utils/__init__.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/utils/test_config.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/utils/test_file_utils.py +0 -0
- {sql_glider-0.1.25 → sql_glider-0.1.27}/tests/sqlglider/utils/test_schema.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-glider
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.27
|
|
4
4
|
Summary: SQL Utility Toolkit for better understanding, use, and governance of your queries in a native environment.
|
|
5
5
|
Project-URL: Homepage, https://github.com/rycowhi/sql-glider/
|
|
6
6
|
Project-URL: Repository, https://github.com/rycowhi/sql-glider/
|
|
@@ -30,6 +30,7 @@ Provides-Extra: databricks
|
|
|
30
30
|
Requires-Dist: databricks-sdk>=0.20.0; extra == 'databricks'
|
|
31
31
|
Provides-Extra: plotly
|
|
32
32
|
Requires-Dist: plotly>=5.0.0; extra == 'plotly'
|
|
33
|
+
Requires-Dist: pygraphviz>=1.11; extra == 'plotly'
|
|
33
34
|
Description-Content-Type: text/markdown
|
|
34
35
|
|
|
35
36
|
# SQL Glider
|
|
@@ -258,7 +258,7 @@ sqlglider graph query graph.json --downstream orders.order_total -f dot
|
|
|
258
258
|
sqlglider graph query graph.json --upstream total_spent -f plotly
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
-
Query diagrams include color-coded nodes and a legend:
|
|
261
|
+
Query diagrams include color-coded nodes, edge labels showing the source SQL file, and a legend:
|
|
262
262
|
|
|
263
263
|
| Color | Meaning |
|
|
264
264
|
|--------|---------|
|
|
@@ -266,6 +266,8 @@ Query diagrams include color-coded nodes and a legend:
|
|
|
266
266
|
| Teal | Root node (no upstream dependencies) |
|
|
267
267
|
| Violet | Leaf node (no downstream consumers) |
|
|
268
268
|
|
|
269
|
+
Each edge in the diagram is labeled with the SQL filename that defines that relationship, making it easy to trace data flow back to the source code.
|
|
270
|
+
|
|
269
271
|
**Example:** Imagine a pipeline where a `revenue` report column draws from multiple sources through several transformation layers. Querying `--upstream revenue` would produce a diagram like this:
|
|
270
272
|
|
|
271
273
|
```mermaid
|
|
@@ -277,12 +279,12 @@ flowchart TD
|
|
|
277
279
|
staging_orders_tax_amount["staging_orders.tax_amount"]
|
|
278
280
|
mart_orders_total_usd["mart_orders.total_usd"]
|
|
279
281
|
revenue["revenue"]
|
|
280
|
-
raw_orders_amount
|
|
281
|
-
raw_orders_tax
|
|
282
|
-
staging_orders_subtotal
|
|
283
|
-
staging_orders_tax_amount
|
|
284
|
-
raw_exchange_rates_rate
|
|
285
|
-
mart_orders_total_usd
|
|
282
|
+
raw_orders_amount -->|staging_orders.sql| staging_orders_subtotal
|
|
283
|
+
raw_orders_tax -->|staging_orders.sql| staging_orders_tax_amount
|
|
284
|
+
staging_orders_subtotal -->|mart_orders.sql| mart_orders_total_usd
|
|
285
|
+
staging_orders_tax_amount -->|mart_orders.sql| mart_orders_total_usd
|
|
286
|
+
raw_exchange_rates_rate -->|mart_orders.sql| mart_orders_total_usd
|
|
287
|
+
mart_orders_total_usd -->|reports.sql| revenue
|
|
286
288
|
|
|
287
289
|
style revenue fill:#e6a843,stroke:#b8860b,stroke-width:3px
|
|
288
290
|
style raw_orders_amount fill:#4ecdc4,stroke:#2b9e96
|
|
@@ -299,7 +301,7 @@ flowchart TD
|
|
|
299
301
|
style legend_leaf fill:#c084fc,stroke:#7c3aed
|
|
300
302
|
```
|
|
301
303
|
|
|
302
|
-
The amber node is the column you queried (`revenue`), teal nodes are ultimate root sources with no further upstream dependencies (`raw_orders.amount`, `raw_orders.tax`, `raw_exchange_rates.rate`), and intermediate nodes (`staging_orders.*`, `mart_orders.*`) appear in the default style. The legend is included automatically.
|
|
304
|
+
The amber node is the column you queried (`revenue`), teal nodes are ultimate root sources with no further upstream dependencies (`raw_orders.amount`, `raw_orders.tax`, `raw_exchange_rates.rate`), and intermediate nodes (`staging_orders.*`, `mart_orders.*`) appear in the default style. Each edge is labeled with the SQL file that defines that relationship. The legend is included automatically.
|
|
303
305
|
|
|
304
306
|
### Mermaid Markdown Format
|
|
305
307
|
|
|
@@ -327,6 +329,20 @@ The `plotly` format outputs a JSON figure specification that can be loaded into
|
|
|
327
329
|
pip install sql-glider[plotly]
|
|
328
330
|
```
|
|
329
331
|
|
|
332
|
+
!!! tip "Better Layout with Graphviz"
|
|
333
|
+
|
|
334
|
+
For complex graphs with many nodes and edges, installing [Graphviz](https://graphviz.org/download/) on your system enables an optimized layout algorithm that minimizes edge crossings. Without Graphviz, a simpler layout is used that may result in overlapping edges.
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
# Install Graphviz first (system package)
|
|
338
|
+
# Windows: choco install graphviz
|
|
339
|
+
# macOS: brew install graphviz
|
|
340
|
+
# Linux: apt install graphviz graphviz-dev
|
|
341
|
+
|
|
342
|
+
# Then install sql-glider with plotly extra
|
|
343
|
+
pip install sql-glider[plotly]
|
|
344
|
+
```
|
|
345
|
+
|
|
330
346
|
**Example usage with Dash:**
|
|
331
347
|
|
|
332
348
|
```python
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.27'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 27)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -1715,11 +1715,15 @@ def graph_query(
|
|
|
1715
1715
|
elif output_format == "mermaid":
|
|
1716
1716
|
from sqlglider.graph.diagram_formatters import MermaidFormatter
|
|
1717
1717
|
|
|
1718
|
-
print(MermaidFormatter.format_query_result(result))
|
|
1718
|
+
print(MermaidFormatter.format_query_result(result, graph=querier.graph))
|
|
1719
1719
|
elif output_format == "mermaid-markdown":
|
|
1720
1720
|
from sqlglider.graph.diagram_formatters import MermaidMarkdownFormatter
|
|
1721
1721
|
|
|
1722
|
-
print(
|
|
1722
|
+
print(
|
|
1723
|
+
MermaidMarkdownFormatter.format_query_result(
|
|
1724
|
+
result, graph=querier.graph
|
|
1725
|
+
)
|
|
1726
|
+
)
|
|
1723
1727
|
elif output_format == "dot":
|
|
1724
1728
|
from sqlglider.graph.diagram_formatters import DotFormatter
|
|
1725
1729
|
|
|
@@ -102,16 +102,26 @@ class MermaidFormatter:
|
|
|
102
102
|
node_id = _sanitize_mermaid_id(node.identifier)
|
|
103
103
|
lines.append(f' {node_id}["{node.identifier}"]')
|
|
104
104
|
|
|
105
|
-
# Add edges
|
|
105
|
+
# Add edges with file path labels
|
|
106
106
|
for edge in graph.edges:
|
|
107
107
|
src = _sanitize_mermaid_id(edge.source_node)
|
|
108
108
|
tgt = _sanitize_mermaid_id(edge.target_node)
|
|
109
|
-
|
|
109
|
+
# Extract filename from path
|
|
110
|
+
file_name = (
|
|
111
|
+
edge.file_path.split("/")[-1].split("\\")[-1] if edge.file_path else ""
|
|
112
|
+
)
|
|
113
|
+
if file_name:
|
|
114
|
+
lines.append(f" {src} -->|{file_name}| {tgt}")
|
|
115
|
+
else:
|
|
116
|
+
lines.append(f" {src} --> {tgt}")
|
|
110
117
|
|
|
111
118
|
return "\n".join(lines)
|
|
112
119
|
|
|
113
120
|
@staticmethod
|
|
114
|
-
def format_query_result(
|
|
121
|
+
def format_query_result(
|
|
122
|
+
result: LineageQueryResult,
|
|
123
|
+
graph: Optional[LineageGraph] = None,
|
|
124
|
+
) -> str:
|
|
115
125
|
"""Format query result as a Mermaid flowchart with styling.
|
|
116
126
|
|
|
117
127
|
The queried column is highlighted in amber, root nodes in teal,
|
|
@@ -119,6 +129,7 @@ class MermaidFormatter:
|
|
|
119
129
|
|
|
120
130
|
Args:
|
|
121
131
|
result: LineageQueryResult from upstream/downstream query
|
|
132
|
+
graph: Optional LineageGraph for edge file path labels
|
|
122
133
|
|
|
123
134
|
Returns:
|
|
124
135
|
Mermaid diagram string with style directives and legend
|
|
@@ -134,16 +145,27 @@ class MermaidFormatter:
|
|
|
134
145
|
all_nodes = _collect_query_nodes(result)
|
|
135
146
|
edges = _collect_query_edges(result)
|
|
136
147
|
|
|
148
|
+
# Build edge file path lookup if graph is provided
|
|
149
|
+
edge_file_paths: dict[tuple[str, str], str] = {}
|
|
150
|
+
if graph:
|
|
151
|
+
for e in graph.edges:
|
|
152
|
+
edge_file_paths[(e.source_node, e.target_node)] = e.file_path
|
|
153
|
+
|
|
137
154
|
# Declare nodes
|
|
138
155
|
for identifier in sorted(all_nodes):
|
|
139
156
|
node_id = _sanitize_mermaid_id(identifier)
|
|
140
157
|
lines.append(f' {node_id}["{identifier}"]')
|
|
141
158
|
|
|
142
|
-
# Add edges
|
|
159
|
+
# Add edges with optional file path labels
|
|
143
160
|
for src, tgt in sorted(edges):
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
161
|
+
src_id = _sanitize_mermaid_id(src)
|
|
162
|
+
tgt_id = _sanitize_mermaid_id(tgt)
|
|
163
|
+
file_path = edge_file_paths.get((src, tgt), "")
|
|
164
|
+
file_name = file_path.split("/")[-1].split("\\")[-1] if file_path else ""
|
|
165
|
+
if file_name:
|
|
166
|
+
lines.append(f" {src_id} -->|{file_name}| {tgt_id}")
|
|
167
|
+
else:
|
|
168
|
+
lines.append(f" {src_id} --> {tgt_id}")
|
|
147
169
|
|
|
148
170
|
# Style directives
|
|
149
171
|
queried_id = _sanitize_mermaid_id(result.query_column)
|
|
@@ -200,16 +222,20 @@ class MermaidMarkdownFormatter:
|
|
|
200
222
|
return f"```mermaid\n{mermaid}\n```"
|
|
201
223
|
|
|
202
224
|
@staticmethod
|
|
203
|
-
def format_query_result(
|
|
225
|
+
def format_query_result(
|
|
226
|
+
result: LineageQueryResult,
|
|
227
|
+
graph: Optional[LineageGraph] = None,
|
|
228
|
+
) -> str:
|
|
204
229
|
"""Format query result as a Mermaid diagram in a markdown code block.
|
|
205
230
|
|
|
206
231
|
Args:
|
|
207
232
|
result: LineageQueryResult from upstream/downstream query
|
|
233
|
+
graph: Optional LineageGraph for edge file path labels
|
|
208
234
|
|
|
209
235
|
Returns:
|
|
210
236
|
Markdown string with fenced Mermaid diagram
|
|
211
237
|
"""
|
|
212
|
-
mermaid = MermaidFormatter.format_query_result(result)
|
|
238
|
+
mermaid = MermaidFormatter.format_query_result(result, graph=graph)
|
|
213
239
|
return f"```mermaid\n{mermaid}\n```"
|
|
214
240
|
|
|
215
241
|
|
|
@@ -338,7 +364,11 @@ def _compute_layered_layout(
|
|
|
338
364
|
x_spacing: float = 250.0,
|
|
339
365
|
y_spacing: float = 100.0,
|
|
340
366
|
) -> dict[str, tuple[float, float]]:
|
|
341
|
-
"""Compute layered layout positions for nodes using
|
|
367
|
+
"""Compute layered layout positions for nodes using Graphviz's dot algorithm.
|
|
368
|
+
|
|
369
|
+
Uses Graphviz's dot layout engine (Sugiyama algorithm) which minimizes edge
|
|
370
|
+
crossings by using the barycenter heuristic to optimize node ordering within
|
|
371
|
+
each layer.
|
|
342
372
|
|
|
343
373
|
Positions nodes in layers from left to right based on their dependencies.
|
|
344
374
|
Nodes with no incoming edges are placed in layer 0, their dependents in
|
|
@@ -356,6 +386,89 @@ def _compute_layered_layout(
|
|
|
356
386
|
if not nodes:
|
|
357
387
|
return {}
|
|
358
388
|
|
|
389
|
+
try:
|
|
390
|
+
import pygraphviz # noqa: F401 # type: ignore[import-not-found]
|
|
391
|
+
|
|
392
|
+
return _compute_graphviz_layout(nodes, edges, x_spacing, y_spacing)
|
|
393
|
+
except ImportError:
|
|
394
|
+
# Fallback to simple layered layout if pygraphviz not available
|
|
395
|
+
return _compute_simple_layered_layout(nodes, edges, x_spacing, y_spacing)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _compute_graphviz_layout(
|
|
399
|
+
nodes: list[str],
|
|
400
|
+
edges: list[tuple[str, str]],
|
|
401
|
+
x_spacing: float = 250.0,
|
|
402
|
+
y_spacing: float = 100.0,
|
|
403
|
+
) -> dict[str, tuple[float, float]]:
|
|
404
|
+
"""Compute layout using Graphviz's dot algorithm with crossing minimization.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
nodes: List of node identifiers
|
|
408
|
+
edges: List of (source, target) edge tuples
|
|
409
|
+
x_spacing: Horizontal spacing between layers
|
|
410
|
+
y_spacing: Vertical spacing between nodes in the same layer
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Dictionary mapping node identifiers to (x, y) positions
|
|
414
|
+
"""
|
|
415
|
+
import pygraphviz as pgv # type: ignore[import-not-found]
|
|
416
|
+
|
|
417
|
+
# Create directed graph
|
|
418
|
+
g = pgv.AGraph(directed=True, rankdir="LR")
|
|
419
|
+
|
|
420
|
+
# Set graph attributes for spacing
|
|
421
|
+
g.graph_attr["ranksep"] = str(x_spacing / 72.0) # Convert pixels to inches
|
|
422
|
+
g.graph_attr["nodesep"] = str(y_spacing / 72.0)
|
|
423
|
+
|
|
424
|
+
# Add nodes
|
|
425
|
+
for node in nodes:
|
|
426
|
+
g.add_node(node)
|
|
427
|
+
|
|
428
|
+
# Add edges (only for nodes that exist in our node list)
|
|
429
|
+
node_set = set(nodes)
|
|
430
|
+
for src, tgt in edges:
|
|
431
|
+
if src in node_set and tgt in node_set:
|
|
432
|
+
g.add_edge(src, tgt)
|
|
433
|
+
|
|
434
|
+
# Compute layout using dot algorithm
|
|
435
|
+
g.layout(prog="dot")
|
|
436
|
+
|
|
437
|
+
# Extract positions
|
|
438
|
+
positions: dict[str, tuple[float, float]] = {}
|
|
439
|
+
for node in nodes:
|
|
440
|
+
n = g.get_node(node)
|
|
441
|
+
# Position is returned as "x,y" string in points (1/72 inch)
|
|
442
|
+
pos_str = n.attr.get("pos", "0,0")
|
|
443
|
+
if pos_str:
|
|
444
|
+
x_str, y_str = pos_str.split(",")
|
|
445
|
+
# Convert from points to our coordinate system
|
|
446
|
+
x = float(x_str)
|
|
447
|
+
y = float(y_str)
|
|
448
|
+
positions[node] = (x, y)
|
|
449
|
+
|
|
450
|
+
return positions
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _compute_simple_layered_layout(
|
|
454
|
+
nodes: list[str],
|
|
455
|
+
edges: list[tuple[str, str]],
|
|
456
|
+
x_spacing: float = 250.0,
|
|
457
|
+
y_spacing: float = 100.0,
|
|
458
|
+
) -> dict[str, tuple[float, float]]:
|
|
459
|
+
"""Fallback simple layered layout using topological ordering.
|
|
460
|
+
|
|
461
|
+
Used when pygraphviz is not available. Does not minimize edge crossings.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
nodes: List of node identifiers
|
|
465
|
+
edges: List of (source, target) edge tuples
|
|
466
|
+
x_spacing: Horizontal spacing between layers
|
|
467
|
+
y_spacing: Vertical spacing between nodes in the same layer
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Dictionary mapping node identifiers to (x, y) positions
|
|
471
|
+
"""
|
|
359
472
|
# Build adjacency structures
|
|
360
473
|
incoming: dict[str, set[str]] = defaultdict(set)
|
|
361
474
|
outgoing: dict[str, set[str]] = defaultdict(set)
|
|
@@ -235,15 +235,17 @@ class TestMermaidFormatterFullGraph:
|
|
|
235
235
|
|
|
236
236
|
def test_linear_graph_has_edges(self, linear_graph):
|
|
237
237
|
result = MermaidFormatter.format_full_graph(linear_graph)
|
|
238
|
-
|
|
239
|
-
assert "
|
|
238
|
+
# Edges now include file path labels
|
|
239
|
+
assert "a_col -->|q.sql| b_col" in result
|
|
240
|
+
assert "b_col -->|q.sql| c_col" in result
|
|
240
241
|
|
|
241
242
|
def test_diamond_graph_edges(self, diamond_graph):
|
|
242
243
|
result = MermaidFormatter.format_full_graph(diamond_graph)
|
|
243
|
-
|
|
244
|
-
assert "a_col
|
|
245
|
-
assert "
|
|
246
|
-
assert "
|
|
244
|
+
# Edges now include file path labels
|
|
245
|
+
assert "a_col -->|q.sql| b_col" in result
|
|
246
|
+
assert "a_col -->|q.sql| c_col" in result
|
|
247
|
+
assert "b_col -->|q.sql| d_col" in result
|
|
248
|
+
assert "c_col -->|q.sql| d_col" in result
|
|
247
249
|
|
|
248
250
|
def test_starts_with_flowchart(self, linear_graph):
|
|
249
251
|
result = MermaidFormatter.format_full_graph(linear_graph)
|
|
@@ -285,6 +287,45 @@ class TestMermaidFormatterQueryResult:
|
|
|
285
287
|
result = MermaidFormatter.format_query_result(empty_query_result)
|
|
286
288
|
assert "Legend" not in result
|
|
287
289
|
|
|
290
|
+
def test_with_graph_has_edge_labels(self, upstream_query_result):
|
|
291
|
+
"""Test that passing a graph adds edge file path labels."""
|
|
292
|
+
# Create a graph that matches the query result edges
|
|
293
|
+
matching_graph = LineageGraph(
|
|
294
|
+
nodes=[
|
|
295
|
+
GraphNode.from_identifier("source.col", "/path/lineage.sql", 0),
|
|
296
|
+
GraphNode.from_identifier("mid.col", "/path/lineage.sql", 0),
|
|
297
|
+
GraphNode.from_identifier("target.col", "/path/lineage.sql", 0),
|
|
298
|
+
],
|
|
299
|
+
edges=[
|
|
300
|
+
GraphEdge(
|
|
301
|
+
source_node="source.col",
|
|
302
|
+
target_node="mid.col",
|
|
303
|
+
file_path="/path/lineage.sql",
|
|
304
|
+
query_index=0,
|
|
305
|
+
),
|
|
306
|
+
GraphEdge(
|
|
307
|
+
source_node="mid.col",
|
|
308
|
+
target_node="target.col",
|
|
309
|
+
file_path="/path/lineage.sql",
|
|
310
|
+
query_index=0,
|
|
311
|
+
),
|
|
312
|
+
],
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
result = MermaidFormatter.format_query_result(
|
|
316
|
+
upstream_query_result, graph=matching_graph
|
|
317
|
+
)
|
|
318
|
+
# Edges should have file path labels
|
|
319
|
+
assert "-->|lineage.sql|" in result
|
|
320
|
+
|
|
321
|
+
def test_without_graph_no_edge_labels(self, upstream_query_result):
|
|
322
|
+
"""Test that without graph parameter, edges have no labels."""
|
|
323
|
+
result = MermaidFormatter.format_query_result(upstream_query_result)
|
|
324
|
+
# Edges should not have labels (no |...|)
|
|
325
|
+
assert "source_col --> mid_col" in result
|
|
326
|
+
assert "mid_col --> target_col" in result
|
|
327
|
+
assert "-->|" not in result
|
|
328
|
+
|
|
288
329
|
|
|
289
330
|
# --- DotFormatter tests ---
|
|
290
331
|
|
|
@@ -382,7 +423,8 @@ class TestMermaidMarkdownFormatterFullGraph:
|
|
|
382
423
|
def test_contains_mermaid_content(self, linear_graph):
|
|
383
424
|
result = MermaidMarkdownFormatter.format_full_graph(linear_graph)
|
|
384
425
|
assert "flowchart TD" in result
|
|
385
|
-
|
|
426
|
+
# Edges now include file path labels
|
|
427
|
+
assert "a_col -->|q.sql| b_col" in result
|
|
386
428
|
|
|
387
429
|
def test_empty_graph(self, empty_graph):
|
|
388
430
|
result = MermaidMarkdownFormatter.format_full_graph(empty_graph)
|
|
@@ -814,6 +814,12 @@ wheels = [
|
|
|
814
814
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
|
815
815
|
]
|
|
816
816
|
|
|
817
|
+
[[package]]
|
|
818
|
+
name = "pygraphviz"
|
|
819
|
+
version = "1.14"
|
|
820
|
+
source = { registry = "https://pypi.org/simple/" }
|
|
821
|
+
sdist = { url = "https://files.pythonhosted.org/packages/66/ca/823d5c74a73d6b8b08e1f5aea12468ef334f0732c65cbb18df2a7f285c87/pygraphviz-1.14.tar.gz", hash = "sha256:c10df02377f4e39b00ae17c862f4ee7e5767317f1c6b2dfd04cea6acc7fc2bea", size = 106003, upload-time = "2024-09-29T18:31:12.471Z" }
|
|
822
|
+
|
|
817
823
|
[[package]]
|
|
818
824
|
name = "pymdown-extensions"
|
|
819
825
|
version = "10.20.1"
|
|
@@ -1161,6 +1167,7 @@ databricks = [
|
|
|
1161
1167
|
]
|
|
1162
1168
|
plotly = [
|
|
1163
1169
|
{ name = "plotly" },
|
|
1170
|
+
{ name = "pygraphviz" },
|
|
1164
1171
|
]
|
|
1165
1172
|
|
|
1166
1173
|
[package.dev-dependencies]
|
|
@@ -1185,6 +1192,7 @@ requires-dist = [
|
|
|
1185
1192
|
{ name = "jinja2", specifier = ">=3.0.0" },
|
|
1186
1193
|
{ name = "plotly", marker = "extra == 'plotly'", specifier = ">=5.0.0" },
|
|
1187
1194
|
{ name = "pydantic", specifier = ">=2.0.0" },
|
|
1195
|
+
{ name = "pygraphviz", marker = "extra == 'plotly'", specifier = ">=1.11" },
|
|
1188
1196
|
{ name = "rich", specifier = ">=13.0.0" },
|
|
1189
1197
|
{ name = "rustworkx", specifier = ">=0.15.0" },
|
|
1190
1198
|
{ name = "sqlglot", extras = ["rs"], specifier = ">=25.0.0" },
|