coltrace 0.1.0.dev8__tar.gz → 0.1.0.dev10__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.
- {coltrace-0.1.0.dev8/src/coltrace.egg-info → coltrace-0.1.0.dev10}/PKG-INFO +5 -2
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/README.md +4 -1
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/backlog.md +14 -1
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/compatibility-checkpoint.md +136 -8
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/decisions.md +18 -0
- coltrace-0.1.0.dev10/docs/extraction-architectures.md +66 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/lineage-json.md +6 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/supported-patterns.md +114 -2
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/troubleshooting.md +27 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/README.md +2 -1
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/main_orchestrator.py +10 -0
- coltrace-0.1.0.dev10/examples/databricks_project/pipeline_a/transform/combine_sources.py +9 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/notebook_cell.py +9 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/pyproject.toml +54 -54
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/__init__.py +22 -22
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/analysis.py +1697 -76
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/graph.py +500 -461
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/lineage_models.py +122 -122
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/models.py +92 -92
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/source_readers.py +87 -83
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/sql_sources.py +268 -16
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10/src/coltrace.egg-info}/PKG-INFO +5 -2
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace.egg-info/SOURCES.txt +5 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_databricks_example.py +321 -280
- coltrace-0.1.0.dev10/tests/integration/test_extraction_architectures.py +277 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_notebook_reporting.py +11 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_public_project_patterns.py +7 -2
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_resolution_boundaries.py +239 -239
- coltrace-0.1.0.dev10/tests/integration/test_result_containers.py +222 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_sql_templates.py +1 -1
- coltrace-0.1.0.dev10/tests/integration/test_unified_pipeline_shapes.py +516 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_sql_sources.py +509 -419
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/LICENSE +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/MANIFEST.in +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/accuracy.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/architecture.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/column-history.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/column-journey-checkpoint.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/database-patterns.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/database-sources.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/discovery-json.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/graph-workspace-checkpoint.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/history-checkpoint.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/mvp-checkpoint.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/nlp-checkpoint.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/other-column-checkpoint.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/pandas-sources-proposal.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/phase-1.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/read-capture.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/source-checkpoint.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/database_pipeline/README.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/database_pipeline/build_query.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/database_pipeline/pipeline.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/database_pipeline/pyproject.toml +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/main_orchestrator.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/__init__.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/extract/__init__.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/extract/primary_extract.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/extract/quality_extract.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/publish/__init__.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/publish/serialize_webapp.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/transform/__init__.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/transform/priority_rank.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/shared/__init__.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/shared/config.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/shared/token_tracker.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/shared/topic_changes_extract.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/shared/warehouse.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/pricing_pipeline/extract/prices.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/pricing_pipeline/features/pricing.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/pricing_pipeline/pipeline.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/pricing_pipeline/transform/pricing.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/read_capture/README.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/read_capture/project/input.csv +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/read_capture/project/pipeline.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/read_capture/run_capture.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/robustness_pipeline/README.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/robustness_pipeline/extract.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/robustness_pipeline/pipeline.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/robustness_pipeline/transform.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/sample_pipeline/prices.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/text_pipeline/README.md +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/text_pipeline/pipeline.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/text_pipeline/processing.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/text_pipeline/readers.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/text_pipeline/text.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/setup.cfg +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/__main__.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/callbacks.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/capture.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/cli.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/collector.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/compatibility.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/exporters.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/history.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/journey.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/nosql_sources.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/notebooks.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/pandas_api.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/py.typed +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/scanner.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/source_models.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/templates/graph.html +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/visual_layout.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace.egg-info/dependency_links.txt +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace.egg-info/entry_points.txt +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace.egg-info/requires.txt +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace.egg-info/top_level.txt +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/fixtures/sample_pipeline/extract/prices.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/fixtures/sample_pipeline/features/pricing.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/fixtures/sample_pipeline/pipeline.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/fixtures/sample_pipeline/transform/pricing.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_annotation_effects.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_callback_roles.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_capture_workflow.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_cli.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_column_story.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_compatibility_cli.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_conditional_flow.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_database_sources.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_journey_boundaries.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_lineage_acceptance.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_nosql_extraction.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_other_column_stories.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_pandas_boundaries.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_pandas_observation.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_production_patterns.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_project_compatibility.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_reader_inventory.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_scan_project.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_source_config.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_source_precision.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_sql_reader_boundaries.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_text_pipeline.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_analysis.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_callbacks.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_collector.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_compatibility.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_exporters.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_graph.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_graph_workspace.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_history.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_journey.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_nosql_sources.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_notebooks.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_read_capture.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_scanner.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_source_models.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_source_presentation.py +0 -0
- {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_visual_layout.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coltrace
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev10
|
|
4
4
|
Summary: Static Pandas column lineage, impact analysis, and local dependency graphs
|
|
5
5
|
Author: Cristiano Diniz
|
|
6
6
|
License-Expression: MIT
|
|
@@ -32,7 +32,7 @@ column versions across direct function calls. Static scanning never imports or
|
|
|
32
32
|
executes the project or reads its input data. Optional read capture observes
|
|
33
33
|
column labels while you explicitly execute your own pipeline.
|
|
34
34
|
|
|
35
|
-
**Development preview: `0.1.0.
|
|
35
|
+
**Development preview: `0.1.0.dev10`.** The supported subset is deliberately small.
|
|
36
36
|
Unsupported operations produce diagnostics and may interrupt lineage. A graph
|
|
37
37
|
with no edge is not proof that a dependency does not exist.
|
|
38
38
|
|
|
@@ -65,6 +65,9 @@ Query filters are control metadata rather than value dependencies of every
|
|
|
65
65
|
output. `spark.sql(build_query(table)).toPandas()` keeps its SQL text even with
|
|
66
66
|
an injected, unresolved session; SQL assembled from f-strings, `.format`, `%`
|
|
67
67
|
and joins keeps its known text, with unknown parts shown as `{expression}`.
|
|
68
|
+
`SELECT *`, `UNION`, tables read by name (`spark.table`, Snowpark
|
|
69
|
+
`session.table`, `dbt.ref`) and SQL files read inside the project are modeled
|
|
70
|
+
too; see [extraction architectures](docs/extraction-architectures.md).
|
|
68
71
|
|
|
69
72
|
`report.readers` inventories resolved `pd.read_*` calls with file, line, context,
|
|
70
73
|
return shape and resulting DataFrame identity. File schemas are unknown unless
|
|
@@ -6,7 +6,7 @@ column versions across direct function calls. Static scanning never imports or
|
|
|
6
6
|
executes the project or reads its input data. Optional read capture observes
|
|
7
7
|
column labels while you explicitly execute your own pipeline.
|
|
8
8
|
|
|
9
|
-
**Development preview: `0.1.0.
|
|
9
|
+
**Development preview: `0.1.0.dev10`.** The supported subset is deliberately small.
|
|
10
10
|
Unsupported operations produce diagnostics and may interrupt lineage. A graph
|
|
11
11
|
with no edge is not proof that a dependency does not exist.
|
|
12
12
|
|
|
@@ -39,6 +39,9 @@ Query filters are control metadata rather than value dependencies of every
|
|
|
39
39
|
output. `spark.sql(build_query(table)).toPandas()` keeps its SQL text even with
|
|
40
40
|
an injected, unresolved session; SQL assembled from f-strings, `.format`, `%`
|
|
41
41
|
and joins keeps its known text, with unknown parts shown as `{expression}`.
|
|
42
|
+
`SELECT *`, `UNION`, tables read by name (`spark.table`, Snowpark
|
|
43
|
+
`session.table`, `dbt.ref`) and SQL files read inside the project are modeled
|
|
44
|
+
too; see [extraction architectures](docs/extraction-architectures.md).
|
|
42
45
|
|
|
43
46
|
`report.readers` inventories resolved `pd.read_*` calls with file, line, context,
|
|
44
47
|
return shape and resulting DataFrame identity. File schemas are unknown unless
|
|
@@ -71,11 +71,24 @@ not a capability of the published release.
|
|
|
71
71
|
| CT-105 | Ranked candidates for ambiguous names | Implemented dev8: `LineageGraph.candidates`, notebook `ON_AMBIGUOUS` |
|
|
72
72
|
| CT-106 | Cursor `execute` then `fetch*` on separate statements | Proposed: remember the last SQL text executed on a resolved cursor name |
|
|
73
73
|
| CT-107 | Jinja/dbt templates (`{{ ref() }}`) | Proposed: out of scope until a real project needs it |
|
|
74
|
+
| CT-108 | Dict entries, exact unrolling, accumulators | Implemented dev9 |
|
|
75
|
+
| CT-109 | Filters along the call chain | Implemented dev9 |
|
|
76
|
+
| CT-110 | Open `SELECT *` schemas, UNION, tables by name, SQL files | Implemented dev9 |
|
|
77
|
+
| CT-111 | Extraction census tool | Done in the session scratchpad; promote to `tools/` if it becomes a regular check |
|
|
78
|
+
| CT-112 | `%%bigquery df` cell magic as a SQL source | Proposed (Jupyter/Colab users) |
|
|
79
|
+
| CT-113 | `global`/`nonlocal`, `globals()` reads, per-statement budgets | Implemented dev10 |
|
|
80
|
+
| CT-114 | Row and group iteration, records, scalar value flow | Implemented dev10 |
|
|
81
|
+
| CT-115 | Labels chosen among known strings (read, write, `get`) | Implemented dev10 |
|
|
82
|
+
| CT-116 | Row-wise apply with labeled Series, tuple joins, record exports, introspection builtins | Implemented dev10 |
|
|
83
|
+
| CT-117 | BigQuery `UNNEST`, STRUCT paths, `JOIN USING` | Implemented dev10 |
|
|
84
|
+
| CT-118 | Executor fan-out (`ThreadPoolExecutor.submit`, `as_completed`, `map`) and `tqdm` wrappers | Proposed: seen in LLM enrichment steps of the unified pipeline |
|
|
85
|
+
| CT-119 | Control dependencies of dict values built under guards | Proposed |
|
|
86
|
+
| CT-120 | Series index operations (`s.index[mask]`, `Series.get(label)`) after aggregation | Proposed: seen in a Pareto topic selection |
|
|
74
87
|
| CT-095 | Lazy materialization | Proposed: avoid eager pass copies of every known column on each row operation for very wide frames |
|
|
75
88
|
|
|
76
89
|
## Next checkpoint
|
|
77
90
|
|
|
78
|
-
1. Rerun the
|
|
91
|
+
1. Rerun the private pipeline with dev10 and the current notebook cell: select the target database column and confirm that the prioritization outputs and the published exports appear downstream; classify any remaining break.
|
|
79
92
|
2. Inspect the generated HTML's pan/zoom/selection behavior in a normal browser.
|
|
80
93
|
3. Run ColTrace against a small real pipeline with a manually checked expected graph.
|
|
81
94
|
4. Classify missing/incorrect relations by semantic rule, using [accuracy gates](accuracy.md).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Project compatibility checkpoint
|
|
2
2
|
|
|
3
|
-
Local checkpoint: `0.1.0.
|
|
4
|
-
history). Publication is a separate step.
|
|
3
|
+
Local checkpoint: `0.1.0.dev10`, 2026-11-05 (dev7 to dev9 sections below are
|
|
4
|
+
kept as history). Publication is a separate step.
|
|
5
5
|
This work responds to an anonymized compatibility report produced inside the
|
|
6
6
|
user's company. The private project was not accessed, copied or executed; every
|
|
7
7
|
fixture below is a new synthetic reproduction of a reported technical shape.
|
|
@@ -90,8 +90,10 @@ input, not measured accuracy on the private project.
|
|
|
90
90
|
|
|
91
91
|
## Validation
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
dev9: 1,024 tests pass (984 in dev8; dev9 adds container, accumulator, filter
|
|
94
|
+
and extraction-shape tests). dev8: 984 (941 in dev7, plus the SQL boundary
|
|
95
|
+
tests; SQL control tests require database-table columns consumed as row
|
|
96
|
+
controls).
|
|
95
97
|
Ruff lint/format and strict mypy pass. Lineage snapshots are schema `0.6`
|
|
96
98
|
(adds `dataframes[].kind`); `0.4` and `0.5` snapshots load as DataFrame frames.
|
|
97
99
|
|
|
@@ -207,6 +209,129 @@ gaps after it are now reported (+74 `CT_OPAQUE_CALL`); disabling the new text
|
|
|
207
209
|
rules reproduces the dev7 numbers exactly. owid analysis time is unchanged
|
|
208
210
|
(about 39 s). Hand-written precision checks: 14/14 expected, 0/9 forbidden.
|
|
209
211
|
|
|
212
|
+
## Fourth round: the dev8 run and an extraction census (dev9)
|
|
213
|
+
|
|
214
|
+
The user ran dev8 on the private project. Extraction now worked (3,140 column
|
|
215
|
+
versions, 0 errors; physical table fields appeared among modeled names), but
|
|
216
|
+
the run used a pre-dev7 copy of the notebook cell, which printed every
|
|
217
|
+
informational note; the platform truncated the output, and the cell stopped at
|
|
218
|
+
"Column not found with these filters". The visible diagnostics after extraction
|
|
219
|
+
pointed at results kept in dicts and combined with `acc = None` accumulators
|
|
220
|
+
(`tot.merge(...)` on an unresolved receiver, `CT_UNRESOLVED_RECEIVER` runs,
|
|
221
|
+
`pd.concat` and `groupby` on lost frames).
|
|
222
|
+
|
|
223
|
+
Synthetic reproductions confirmed three defects: a dict filled by subscript was
|
|
224
|
+
still the empty literal, so loops over it were skipped silently; iterating
|
|
225
|
+
`frames.items()` invalidated the frames; and a loop inspected once never saw the
|
|
226
|
+
accumulator's `merge` branch. A fourth issue was selection: a database column
|
|
227
|
+
belonged only to the function of its first reading site (often the injected
|
|
228
|
+
`run_query` closure), so a `function=` filter on the extractor excluded it.
|
|
229
|
+
All four are fixed; the synthetic example now includes a dict of results
|
|
230
|
+
combined by an accumulator (`combine_sources.py`).
|
|
231
|
+
|
|
232
|
+
The user also asked for a map of extraction architectures. A static census of
|
|
233
|
+
public projects classified every extraction call shape and compared it with
|
|
234
|
+
ColTrace's output; the result is [extraction architectures](extraction-architectures.md).
|
|
235
|
+
Shapes fixed in this round: `SELECT *` open schemas, `UNION`, tables read by
|
|
236
|
+
name, `.result()` and row-only steps before conversion, literal projections,
|
|
237
|
+
keyword options on conversions, SQL files, and SQL clients whose name looked
|
|
238
|
+
like a DataFrame.
|
|
239
|
+
|
|
240
|
+
Measured on the census projects, extraction sites with SQL fields went from 43
|
|
241
|
+
to 79 and inferred frames of unresolved origin from 68 to 35; conversions whose
|
|
242
|
+
result is never used as a DataFrame stay unmodeled by design (ADR-057, ADR-070).
|
|
243
|
+
On the earlier public harness nothing crashed, every snapshot round-trips, and
|
|
244
|
+
the hand-written precision checks stay at 14/14 expected and 0/9 forbidden.
|
|
245
|
+
From dev9 identical diagnostics are reported once, so warning counts are not
|
|
246
|
+
comparable with earlier rounds (owid: 858 distinct warnings; recommenders: 538).
|
|
247
|
+
owid models more code (16,317 frames and 67,450 column versions, from 15,416
|
|
248
|
+
and 66,426) because paths built from `__file__`, `os.path.join` and known
|
|
249
|
+
`**kwargs` now resolve and known-length loops are unrolled.
|
|
250
|
+
|
|
251
|
+
## Fifth round: the anonymized unified pipeline (dev10)
|
|
252
|
+
|
|
253
|
+
The user shared an anonymized copy of the whole pipeline with the output of the
|
|
254
|
+
last run, because the target column still did not flow end to end. It is a
|
|
255
|
+
notebook orchestrator that runs two KPI pipelines with `%run`; each extracts
|
|
256
|
+
from BigQuery through injected `run_query` closures, builds weekly facts, trends
|
|
257
|
+
(rank-based trend tests and a change-point library) and a prioritization score,
|
|
258
|
+
then publishes JSON-in-script files and an LLM-assisted reading. The copy was
|
|
259
|
+
analyzed statically outside the repository; nothing from it is part of the
|
|
260
|
+
repository, and the fixtures below are synthetic.
|
|
261
|
+
|
|
262
|
+
Measured with the same script on that copy:
|
|
263
|
+
|
|
264
|
+
| | dev9 (published wheel) | dev10 |
|
|
265
|
+
| --- | --- | --- |
|
|
266
|
+
| Frames / column versions | 404 / 3,189 | 599 / 6,419 |
|
|
267
|
+
| Extraction sources / table frames | 24 / 7 | 40 / 12 |
|
|
268
|
+
| Target database column | not found | 19 downstream columns, through the score to the published export |
|
|
269
|
+
| A database field of the second KPI | not found | 45 downstream columns |
|
|
270
|
+
| `CT_ANALYSIS_LIMIT` | 30 | 0 |
|
|
271
|
+
| `CT_UNRESOLVED_RECEIVER` | 42 | 8 |
|
|
272
|
+
| `CT_UNMODELED_RESULT` | 20 | 1 |
|
|
273
|
+
| `CT_OPAQUE_CALL` | 10 | 6 |
|
|
274
|
+
| `CT_CONDITIONAL_RETURN` | 9 | 2 |
|
|
275
|
+
| `CT_UNRESOLVED_CALL` | 7 | 1 |
|
|
276
|
+
| `CT_DYNAMIC_COLUMN` | 2 | 1 |
|
|
277
|
+
| `CT_PARTIAL_SQL` | 10 | 15 (more SQL is reached; 39 before `UNNEST`/`USING` support) |
|
|
278
|
+
|
|
279
|
+
Causes, in pipeline order (each is a test in
|
|
280
|
+
`tests/integration/test_unified_pipeline_shapes.py`):
|
|
281
|
+
|
|
282
|
+
1. The first KPI's extract cached its client in a module global
|
|
283
|
+
(`global _client`). Functions declaring `global` were rejected, so the
|
|
284
|
+
warehouse source was never created and the target column did not exist.
|
|
285
|
+
2. Early notebook cells spent the conditional-path budget
|
|
286
|
+
(30 `CT_ANALYSIS_LIMIT`), which reset bindings in later cells.
|
|
287
|
+
3. The orchestrator read `%run` results through `globals().get(...)`.
|
|
288
|
+
4. Trends iterate groups and rows, collect dicts, build frames from records,
|
|
289
|
+
and compute scalars with `list(...values)`, `float`, `round`, `max`, `zip`
|
|
290
|
+
and helpers that return labels under guards.
|
|
291
|
+
5. The main break: a mask helper picks the period column by a parameter
|
|
292
|
+
(`"month" if kind == ... else "quarter"`). The dynamic read invalidated the
|
|
293
|
+
facts frame just before the ranking, so every ranked value had an opaque
|
|
294
|
+
origin.
|
|
295
|
+
6. The score is computed by a row-wise `apply` returning `pd.Series({...})`,
|
|
296
|
+
attached with `pd.concat(axis=1)`.
|
|
297
|
+
7. The ranking returns `(dict, flag)` on several paths, builds the dict with a
|
|
298
|
+
comprehension over `to_dict(orient="records")`, and the caller reads it with
|
|
299
|
+
`.get`.
|
|
300
|
+
8. `getattr(frame, "columns", [])` and `isinstance(frame, ...)` were opaque
|
|
301
|
+
calls that invalidated the trend frames.
|
|
302
|
+
9. The second KPI passed `run_query(sql)` straight to a post-processing
|
|
303
|
+
function, so its SQL was never attached to the frame.
|
|
304
|
+
10. `np.asarray(values)` passed to a change-point library invalidated the trend
|
|
305
|
+
series, and a guarded `return {"flag": False}` was turned into a scalar.
|
|
306
|
+
11. The second KPI's SQL uses `FROM t AS s, UNNEST(s.items) AS c`, STRUCT fields
|
|
307
|
+
and `LEFT JOIN ... USING (key)`.
|
|
308
|
+
|
|
309
|
+
What remains in that copy: LLM judge steps (thread-pool fan-out, `tqdm`, the LLM
|
|
310
|
+
calls themselves), a topic selection that indexes an aggregated Series by its
|
|
311
|
+
index, and SQL names that anonymization collapsed into duplicates.
|
|
312
|
+
|
|
313
|
+
### Validation
|
|
314
|
+
|
|
315
|
+
1,044 tests pass (15 new integration tests in `test_unified_pipeline_shapes.py`
|
|
316
|
+
and 5 new SQL unit tests); Ruff and strict mypy pass. On the public harness
|
|
317
|
+
(13 projects, round 14) nothing crashes, every snapshot round-trips, and the
|
|
318
|
+
hand-written precision checks stay at 14/14 expected and 0/9 forbidden.
|
|
319
|
+
Distinct warnings went from 1,859 to 1,849 in total while more code is modeled
|
|
320
|
+
(owid: 67,450 to 83,918 column versions and 858 to 909 warnings; recommenders:
|
|
321
|
+
3,495 to 4,179 column versions and 538 to 496 warnings). Analysis time is
|
|
322
|
+
unchanged when projects run alone (owid 54 s). Under memory pressure, owid's
|
|
323
|
+
572 MB snapshot round-trip in the harness took hours; it is a harness cost, not
|
|
324
|
+
an analysis one.
|
|
325
|
+
|
|
326
|
+
The harness caught one regression before release: scalars computed from cells
|
|
327
|
+
made a library call that also received Series look like a scalar function,
|
|
328
|
+
which hid an opaque call and the frame built from its result. It is fixed and
|
|
329
|
+
covered by `test_unknown_call_receiving_column_views_stays_opaque`. On the
|
|
330
|
+
extraction census, warnings fell in 9 of 18 projects (for example 67 to 39 and
|
|
331
|
+
41 to 15) and rose by one in two, where newly modeled frames reach unknown
|
|
332
|
+
library calls; one of those calls was `load_table_from_dataframe`, which is now
|
|
333
|
+
a recorded export.
|
|
334
|
+
|
|
210
335
|
## Remaining boundaries
|
|
211
336
|
|
|
212
337
|
This is still bounded abstract interpretation, not a general control-flow solver.
|
|
@@ -215,10 +340,13 @@ with zero iterations. `groupby(...).apply`, `stack`/`unstack`, `crosstab` and
|
|
|
215
340
|
row-wise callbacks with side effects remain unmodeled results. Calls to unknown
|
|
216
341
|
helpers that receive a DataFrame still interrupt its provenance unless declared
|
|
217
342
|
as `observers`. Merge suffixes and
|
|
218
|
-
row multiplicity are not resolved. Async functions, reflection
|
|
219
|
-
|
|
343
|
+
row multiplicity are not resolved. Async functions, reflection and properties
|
|
344
|
+
remain unresolved (`global` and `nonlocal` are analyzed from dev10). Executor
|
|
345
|
+
fan-out (`ThreadPoolExecutor.submit`, `as_completed`) and `tqdm` wrappers are
|
|
346
|
+
opaque. `.ipynb` JSON files, live cells and
|
|
220
347
|
`dbutils.notebook.run` (a separate namespace) are not inputs. Opaque calls that
|
|
221
348
|
receive a DataFrame still interrupt its provenance.
|
|
222
349
|
|
|
223
|
-
The next acceptance input is a rerun of the
|
|
224
|
-
|
|
350
|
+
The next acceptance input is a rerun of the private pipeline with dev10 and
|
|
351
|
+
the current notebook cell, selecting the target database column and confirming
|
|
352
|
+
the prioritization outputs and published exports downstream.
|
|
@@ -69,6 +69,24 @@ choices made within the user's authorization.
|
|
|
69
69
|
| ADR-062 | Labeled placeholders and last-resort fragment removal | Unknown interpolations are shown as their Python expression; unparsable dynamic fragments are dropped only after every dialect fails, and the schema becomes incomplete |
|
|
70
70
|
| ADR-063 | Documented conversions keep their receiver's SQL | `spark.sql(text).toPandas()` describes the SELECT rows; a transformation in between drops the query |
|
|
71
71
|
| ADR-064 | Ranked, never merged, ambiguous candidates | Called code before templates and the earliest origin first; a database column and its same-name SQL output form one selectable family |
|
|
72
|
+
| ADR-065 | Track dict entries; unroll known-length collections | Results kept in dicts were lost silently; exact iteration keeps each frame's identity |
|
|
73
|
+
| ADR-066 | Inspect a second loop iteration for `None` accumulators | `acc = None` patterns need the state one iteration produces; bounded to loops that carry a `None` |
|
|
74
|
+
| ADR-067 | Filters match database columns along the call chain | A table column is used by every extractor that reads it, not only by the closure where SQL runs |
|
|
75
|
+
| ADR-068 | `SELECT *` is an open table schema | Used columns become table columns; lookups through merges of different frames never add columns to a table |
|
|
76
|
+
| ADR-069 | Read SQL files as data | Only inside the analyzed root, SQL/text suffixes, bounded size; never executed |
|
|
77
|
+
| ADR-070 | Keep ADR-057 for conversions | A census suggested promoting every `toPandas()` after SQL; precision tests and ADR-057 keep usage, annotations or contracts as the evidence |
|
|
78
|
+
| ADR-071 | One record per distinct diagnostic | Unrolled iterations and repeated calling contexts produced identical records (the private run showed one line repeated dozens of times); identical records carry no extra information |
|
|
79
|
+
| ADR-072 | Analyze `global` and `nonlocal` | A cached module-level warehouse client was lost because functions declaring `global` were rejected; declared names read the module namespace, resolved calls write back, templates never do |
|
|
80
|
+
| ADR-073 | Budgets per top-level statement | Long notebooks spent the path budget in early cells and cut later ones; each top-level module statement gets the full budget |
|
|
81
|
+
| ADR-074 | Values computed from cells are derived scalars | Builtins, NumPy, `math` and subscripts over cells carry dependencies and never alias a frame; guarded scalar returns depend on the guard's columns, containers keep their structure |
|
|
82
|
+
| ADR-075 | Records are frames; record exports yield records | List-of-dict records build open-schema frames; `to_dict(orient="records")` stays an export and also yields row dicts whose values are cells |
|
|
83
|
+
| ADR-076 | Labels chosen among known strings | A column picked by a parameter invalidated the whole frame; reading every possible column (inferred) is sound and bounded to 8 strings; writes behave like branch joins |
|
|
84
|
+
| ADR-077 | Row-wise apply with labeled Series builds columns | Pandas returns a DataFrame whose columns are the labels; each column reads the row fields of its value |
|
|
85
|
+
| ADR-078 | Join fixed-width tuples by position | `return {}, False` and `return result, ok` lost both positions when merged as unordered members |
|
|
86
|
+
| ADR-079 | Introspection builtins are pure | `isinstance`, `hasattr`, `callable` and literal `getattr` were opaque calls that invalidated frames |
|
|
87
|
+
| ADR-080 | Promote unresolved results at DataFrame parameters | Same evidence as ADR-057, observed in the callee; one frame per result object, so later uses share its identity |
|
|
88
|
+
| ADR-081 | BigQuery `UNNEST`, STRUCT paths and `USING` | Nested fields are columns named by their path; `USING` keys are controls of both sides and an unqualified key is their `COALESCE`; physical tables are assumed to hold a key only as the single remaining candidate |
|
|
89
|
+
| ADR-082 | Warehouse writers are exports | `load_table_from_dataframe`, `write_pandas` and `to_gbq` read the frame they receive; treating them as opaque invalidated it |
|
|
72
90
|
|
|
73
91
|
The initial codename was FeatureTrace. The early architecture proposed reusable
|
|
74
92
|
function summaries; direct contextual interpretation is used first because it
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Extraction architectures
|
|
2
|
+
|
|
3
|
+
How data enters a Pandas pipeline varies more than any other part of it. This
|
|
4
|
+
page maps the extraction shapes ColTrace has met in a real Databricks project
|
|
5
|
+
and in a static census of public projects, and states what the dev9 analysis
|
|
6
|
+
does with each one. Nothing is executed: SQL text, SQL files and notebook
|
|
7
|
+
source are read as data.
|
|
8
|
+
|
|
9
|
+
The census (November 2026) covered Airflow/Astro SDK DAGs, Prefect recipes,
|
|
10
|
+
Dagster and Hamilton examples, the BigQuery Python samples, Snowpark demos, dbt
|
|
11
|
+
Python models, Databricks talks, Kaggle SQL course notebooks, Feast examples, a
|
|
12
|
+
Google Cloud data-engineering course and a small personal ETL repository.
|
|
13
|
+
|
|
14
|
+
## What a recognized extraction produces
|
|
15
|
+
|
|
16
|
+
- A **source** record with the SQL text (unknown interpolations shown as
|
|
17
|
+
`{expression}`), relations, output fields and row controls.
|
|
18
|
+
- A **DataFrame** whose columns are written by the source.
|
|
19
|
+
- A **database table** frame for every physical relation the SQL reads, with one
|
|
20
|
+
column node per physical column. Selecting such a column (for example a field
|
|
21
|
+
used only inside `SUM(...)` or a `WHERE`) traces it into the DataFrame.
|
|
22
|
+
|
|
23
|
+
## Map
|
|
24
|
+
|
|
25
|
+
| Shape | Example | dev9 treatment |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| Pandas SQL readers | `pd.read_sql(sql, con)`, `read_sql_query`, `read_gbq` | Source with fields; dialect from reader or fallback |
|
|
28
|
+
| Connector chains | `bigquery.Client().query(sql).to_dataframe()`, `snowflake ... cursor().execute(sql).fetch_pandas_all()`, `duckdb.sql(sql).df()` | Source with fields when the connector object is resolved |
|
|
29
|
+
| Injected callables | `run_query(sql)` closure or parameter | Followed through closures; otherwise inferred from DataFrame use, SQL fields seeded |
|
|
30
|
+
| Unresolved session or client | `spark.sql(sql).toPandas()`, `client.query(sql).result().to_dataframe(...)`, `conn.execute(sql).fetchdf()` | SQL kept through `.result()` and the conversion; a frame once used as one, returned under a `pd.DataFrame` annotation, or declared by a contract |
|
|
31
|
+
| Tables read by name | `spark.table("db.t")`, `spark.read.table(...)`, Snowpark `session.table("T")`, dbt `dbt.ref("m")`/`dbt.source("s", "t")`, BigQuery `client.list_rows(t)` | Open schema of that table (as `SELECT *`) |
|
|
32
|
+
| Row-only steps before conversion | `.filter(...)`, `.where`, `.limit(n)`, `.distinct()`, `.orderBy`, `.sample` | SQL or table kept |
|
|
33
|
+
| Literal projections before conversion | `.select("A", "B")`, `.drop("SECRET")` | Field list narrowed; a literal `select` makes the schema complete |
|
|
34
|
+
| Spark column expressions | `.withColumn(...)`, `.groupBy(...).agg(...)`, `select(F.col(...))` | SQL dropped: the SELECT list no longer describes the result |
|
|
35
|
+
| `SELECT *` / `t.*` / `* EXCEPT (...)` over one table | `pd.read_sql("SELECT * FROM t", con)` | Open schema: columns the code uses become columns of `t`; merges with other frames never add columns to `t` |
|
|
36
|
+
| `UNION [ALL]` | `SELECT a FROM t UNION SELECT b FROM u` | Labels from the first branch, inputs from every branch by position |
|
|
37
|
+
| f-strings, `.format`, `%`, `+`, `join` | `f"... FROM {TABLE} WHERE d >= '{start}'"` | Known constants, config values and parameter defaults folded; unknown parts shown as `{expression}` |
|
|
38
|
+
| Dynamic fragments | `f"... WHERE x = 1 {extra}"` | Unplaceable fragment ignored as a last resort, with a note |
|
|
39
|
+
| Vendor dialects | BigQuery backticks, Databricks, Snowflake | Reader or call-shape dialect, `[tool.coltrace].sql_dialect`, then fallbacks, with a note |
|
|
40
|
+
| SQL files | `open("sql/q.sql").read()`, `(Path(__file__).parent / "sql" / "q.sql").read_text().format(**params)` | Read as data when inside the analyzed root, SQL/text suffix, at most 200 KB |
|
|
41
|
+
| DB-API parameters | `%s`, `%(name)s`, `?`, `:name`, `@name` | Read as parameters, never as columns |
|
|
42
|
+
| Results kept in containers | `results = {}`; `results["a"] = df`; `for k, d in results.items()` | Dict entries tracked; loops over known-length collections unrolled exactly |
|
|
43
|
+
| Accumulators | `acc = None`; `acc = d if acc is None else acc.merge(d)` | A second iteration is inspected; `None` dropped where the name is used as a frame |
|
|
44
|
+
|
|
45
|
+
## Boundaries
|
|
46
|
+
|
|
47
|
+
| Shape | Why | What to do |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| Jinja/dbt SQL templates (`{{ ref() }}`, `{% if %}`) | Template languages are not SQL | Keep a rendered example, or declare a source contract/schema |
|
|
50
|
+
| `cursor.execute(sql)` and `cursor.fetchall()` in separate statements | The query is not attached to the later fetch | Use `pd.read_sql`, or a contract |
|
|
51
|
+
| `%%bigquery df` / `%sql` cell magics | Neutralized as notebook magics | Use the client API in Python cells |
|
|
52
|
+
| Feature stores (`store.get_historical_features(...).to_df()`) | Feature references are not SQL | Declare a source schema |
|
|
53
|
+
| Unknown SDK returning Pandas | No SQL text or table name | `[[tool.coltrace.sources]]` contract with `pandas_method` if needed |
|
|
54
|
+
| Ibis, Polars, Dask, bigframes transformations | Different libraries | Lineage starts at the Pandas conversion |
|
|
55
|
+
|
|
56
|
+
Spelling alone never creates a DataFrame (ADR-057): `x.toPandas()` on an
|
|
57
|
+
unknown `x`, or a conversion whose result is never used as a frame, stays
|
|
58
|
+
unresolved. A source contract makes any of these explicit:
|
|
59
|
+
|
|
60
|
+
```toml
|
|
61
|
+
[[tool.coltrace.sources]]
|
|
62
|
+
callable = "spark.sql"
|
|
63
|
+
kind = "sql"
|
|
64
|
+
dialect = "databricks"
|
|
65
|
+
pandas_method = "toPandas"
|
|
66
|
+
```
|
|
@@ -243,6 +243,12 @@ fully named relation is one identity across the whole snapshot.
|
|
|
243
243
|
`load_report` upgrades 0.4 and 0.5 snapshots by adding `kind: "dataframe"` to
|
|
244
244
|
their frames; 0.2/0.3 migrations are unchanged.
|
|
245
245
|
|
|
246
|
+
From dev10 (still schema 0.6), a nested database field is a `table` column
|
|
247
|
+
named by its path: `items.score` for `UNNEST(s.items) AS c` with `c.score`, and
|
|
248
|
+
`info.flag` for the STRUCT access `s.info.flag`. The diagnostic code
|
|
249
|
+
`CT_LABEL_CHOICE` (info) marks a column read or written through a label chosen
|
|
250
|
+
among known strings.
|
|
251
|
+
|
|
246
252
|
## Changes in 0.5
|
|
247
253
|
|
|
248
254
|
Schema 0.5 adds the diagnostic severity `"info"` for recorded inferences and
|
|
@@ -419,6 +419,117 @@ raises `AmbiguousColumnError` for distinct lineages (its `candidates` use the
|
|
|
419
419
|
same order); ranking never merges them. The notebook cell continues with the
|
|
420
420
|
first candidate by default (`ON_AMBIGUOUS = "first"`).
|
|
421
421
|
|
|
422
|
+
## Values, labels and records across a notebook pipeline (dev10)
|
|
423
|
+
|
|
424
|
+
The dev9 run on an anonymized two-KPI notebook pipeline lost the target column
|
|
425
|
+
between the warehouse extract and the prioritization score. Each cause below has
|
|
426
|
+
a synthetic reproduction in `tests/integration/test_unified_pipeline_shapes.py`.
|
|
427
|
+
|
|
428
|
+
- **Cached clients and globals.** Functions that declare `global` or `nonlocal`
|
|
429
|
+
are analyzed: a module-level singleton (`_client = None` filled on first use)
|
|
430
|
+
keeps its connector identity, and resolved calls write declared globals back
|
|
431
|
+
to the module (symbolic templates never do). `globals().get("NAME")` and
|
|
432
|
+
`globals()["NAME"]` read the namespace, as notebooks do after `%run`.
|
|
433
|
+
- **Rows and groups.** `for _, row in df.iterrows()`, `itertuples()` and
|
|
434
|
+
`for key, group in df.groupby(...)` bind row cells and group frames;
|
|
435
|
+
`row["c"]`, `row.c` and `row.get("c")` read the column.
|
|
436
|
+
- **Records.** `pd.DataFrame(list_of_dicts)` with known string keys builds a
|
|
437
|
+
frame whose columns are the possible values of each key (inferred, open
|
|
438
|
+
schema). `df.to_dict(orient="records")` is still recorded as an export and
|
|
439
|
+
now also yields row dicts whose values are cells of each column, so
|
|
440
|
+
`{key: row for row in records}` followed by `rows.get(key)["c"]` keeps
|
|
441
|
+
lineage. A dict comprehension is a tracked dict: one inspected entry stands
|
|
442
|
+
for every entry, and its unknown key may match any lookup.
|
|
443
|
+
- **Scalar value flow.** `float`, `int`, `round`, `sum`, `min`, `max`, `abs`,
|
|
444
|
+
`list`, `tuple`, `sorted`, `set`, `.values`, `.tolist()`, `.item()`,
|
|
445
|
+
subscripts and `math.*` carry the dependencies of the cells they read
|
|
446
|
+
(inferred). NumPy and scalar Pandas functions over cells or lists of cells
|
|
447
|
+
(`np.asarray(values)`, `np.mean(x[:i])`, `pd.isna(v)`) return derived values,
|
|
448
|
+
not views of a frame, so passing them to an unresolved library no longer
|
|
449
|
+
invalidates the source frame. A scalar returned under `if slope < 0:` depends
|
|
450
|
+
on the guard's columns (control dependency, inferred); constant containers
|
|
451
|
+
returned under a guard keep their structure.
|
|
452
|
+
- **Pairs and shared dicts.** `zip` and `enumerate` pair known members (an
|
|
453
|
+
unknown list pairs with unknown values); `record["q"] = q` on a dict collected
|
|
454
|
+
by `append` updates every alias of that dict.
|
|
455
|
+
- **Labels chosen at run time.** A value that is one of a few known strings
|
|
456
|
+
(`"a" if flag else "b"`, `if/else` assignments, `LABELS[kind]` over a constant
|
|
457
|
+
mapping or list; at most 8 strings) is a label choice. `df[col]`,
|
|
458
|
+
`df.loc[rows, col]` and `df.get(col)` read each possible column (inferred,
|
|
459
|
+
`CT_LABEL_CHOICE`); a label absent from a closed schema cannot be the chosen
|
|
460
|
+
one. `df[col] = v` gives each candidate column an inferred version that reads
|
|
461
|
+
its previous value and `v`, like a branch join. Before dev10 such a read
|
|
462
|
+
invalidated the whole frame (`CT_DYNAMIC_COLUMN`).
|
|
463
|
+
- **Constant Series.** `pd.Series([False] * len(df), index=df.index)` is a
|
|
464
|
+
Series without column inputs, aligned with `df`; joined with a mask (the
|
|
465
|
+
fallback of a mask helper), it is used as a mask.
|
|
466
|
+
- **Row-wise apply with labeled values.** `df.apply(f, axis=1)` where `f`
|
|
467
|
+
returns `pd.Series({"a": x, "b": y})` builds a frame with one column per
|
|
468
|
+
label, each derived from the row fields its value reads;
|
|
469
|
+
`pd.concat([df, that], axis=1)` attaches the columns.
|
|
470
|
+
- **Tuple returns.** Fixed-width tuples returned on alternative paths
|
|
471
|
+
(`return {}, False` and `return result, ok`) join position by position.
|
|
472
|
+
- **Introspection.** `isinstance(df, ...)`, `hasattr(df, "x")`, `callable(f)`
|
|
473
|
+
and `getattr(obj, "name"[, default])` with a literal name neither read nor
|
|
474
|
+
change column data. `getattr(df, "columns", [])` is `df.columns`; module,
|
|
475
|
+
class and instance attributes resolve as attribute reads.
|
|
476
|
+
- **Unresolved results as arguments.** An unresolved conversion result
|
|
477
|
+
(`client.query(sql).to_dataframe()` on an unknown client) passed straight to a
|
|
478
|
+
parameter used as a DataFrame (`postprocess(run_query(sql))`) is promoted with
|
|
479
|
+
its SQL there (ADR-057 evidence: the callee's DataFrame use), once per result.
|
|
480
|
+
- **Warehouse writers.** `client.load_table_from_dataframe(df, table)`,
|
|
481
|
+
`write_pandas(conn, df, table)` and `pandas_gbq.to_gbq(df, table)` are
|
|
482
|
+
recorded exports, like `spark.createDataFrame(df)`.
|
|
483
|
+
- **Budgets.** The conditional-path and call budgets apply per top-level module
|
|
484
|
+
statement, so a long notebook is not cut by its earlier cells.
|
|
485
|
+
- **BigQuery SQL.** In `FROM t AS s, UNNEST(s.items) AS c`, `c.score` is the
|
|
486
|
+
nested column `items.score` of `t` and `c` alone is the array column;
|
|
487
|
+
`UNNEST(arr) AS u(x)` makes `x` the element. `s.info.flag` is the nested
|
|
488
|
+
column `info.flag`. In `JOIN x USING (k)`, `k` is a control of both sides: a
|
|
489
|
+
CTE or derived table holds `k` when it outputs it, and a physical table when it
|
|
490
|
+
is the only remaining candidate; an unqualified `k` reads both sides
|
|
491
|
+
(`COALESCE`). Before dev10 every `USING` made the summary partial.
|
|
492
|
+
|
|
493
|
+
Still outside after dev10: executor fan-out (`ThreadPoolExecutor.submit`,
|
|
494
|
+
`as_completed`, `executor.map`) and `tqdm` wrappers are opaque; control
|
|
495
|
+
dependencies of dict values built under guards (`{"flag": True}` versus
|
|
496
|
+
`{"flag": False}`) are not recorded; LLM calls are opaque by nature.
|
|
497
|
+
|
|
498
|
+
## Containers, accumulators and extraction shapes (dev9)
|
|
499
|
+
|
|
500
|
+
The dev8 run on the private project showed lineage lost right after extraction,
|
|
501
|
+
where results are kept in dicts and combined. A census of public projects then
|
|
502
|
+
mapped extraction shapes; see [extraction architectures](extraction-architectures.md).
|
|
503
|
+
|
|
504
|
+
- **Dicts of frames** are tracked entry by entry: literals, `d[k] = df`,
|
|
505
|
+
`d[k]`, `d.get(k)`, `items()`, `values()`, `keys()`, `update`, `setdefault`,
|
|
506
|
+
`pop`, and `f(**known_dict)`. A key that is not statically known may select
|
|
507
|
+
any entry (inferred). Before dev9 a dict filled by subscript stayed the empty
|
|
508
|
+
literal, so loops over it were skipped without a diagnostic.
|
|
509
|
+
- **Loops** over a list, tuple or dict of known length (up to 64 items) are
|
|
510
|
+
unrolled exactly, binding each frame in turn. A literal empty iterable is
|
|
511
|
+
skipped; a name bound to an empty list or dict is still inspected, because it
|
|
512
|
+
may have been filled through an untracked path.
|
|
513
|
+
- **Accumulators**: when a loop assigns a name that is `None` before it, a
|
|
514
|
+
second iteration is inspected so `acc.merge(part)` is seen; a join of `None`
|
|
515
|
+
and frames is the frame wherever the name is used as one.
|
|
516
|
+
- **Selection filters**: `function=`/`module=` match a database column from any
|
|
517
|
+
function that reads it, including the callers on the analysis call chain (an
|
|
518
|
+
extractor that calls an injected `run_query`).
|
|
519
|
+
- **`SELECT *`** over one table is an open schema of that table; **UNION**
|
|
520
|
+
branches are combined by position; **tables read by name**
|
|
521
|
+
(`spark.table`, `session.table`, `dbt.ref`, `dbt.source`, `list_rows`) are
|
|
522
|
+
`SELECT *` of that table; `.result()` and row-only Spark/Snowpark steps keep the
|
|
523
|
+
SQL; literal `select`/`drop` narrow it; conversions accept keyword options.
|
|
524
|
+
- **SQL files** inside the analyzed root are read as text (`open(...).read()`,
|
|
525
|
+
`Path.read_text()`, `Path(__file__).parent / "sql"`, `os.path.join`), never
|
|
526
|
+
executed, and reported with `CT_SQL_FILE`. `__file__` is the module path
|
|
527
|
+
relative to the analyzed root, so snapshots never record local paths.
|
|
528
|
+
- Identical diagnostics (same code, message, location and scope) are reported
|
|
529
|
+
once.
|
|
530
|
+
- `client.query(SQL_CONSTANT)` on a name that looked like a DataFrame is a SQL
|
|
531
|
+
client call: `DataFrame.query` never takes SQL text.
|
|
532
|
+
|
|
422
533
|
## Public project patterns (dev7, second round)
|
|
423
534
|
|
|
424
535
|
These rules come from static runs over public pipelines (Kedro starters, MLOps
|
|
@@ -497,8 +608,9 @@ invalidate frames they receive.
|
|
|
497
608
|
|
|
498
609
|
## Remaining unknown behavior
|
|
499
610
|
|
|
500
|
-
`groupby(...).apply`, `stack`/`unstack`, `crosstab`, unlisted Series methods
|
|
501
|
-
and other unsupported calls are
|
|
611
|
+
`groupby(...).apply`, `stack`/`unstack`, `crosstab`, unlisted Series methods,
|
|
612
|
+
executor fan-out (`ThreadPoolExecutor.submit`) and other unsupported calls are
|
|
613
|
+
opaque or unmodeled. Explicit column
|
|
502
614
|
arguments can still be recorded as consumption, but unknown returned values do
|
|
503
615
|
not acquire imaginary dependencies. Unknown calls can mutate arguments; affected
|
|
504
616
|
state is invalidated.
|
|
@@ -114,6 +114,16 @@ unresolved facts. Reusing one after upgrading will not show newly supported edge
|
|
|
114
114
|
- Keep helper scripts and outputs out of the analysis with `.coltraceignore`
|
|
115
115
|
(gitignore syntax at the analyzed root) or `[tool.coltrace].exclude`.
|
|
116
116
|
|
|
117
|
+
## Notebook output is truncated or still says "Observed names"
|
|
118
|
+
|
|
119
|
+
Use the current `examples/notebook_cell.py`. Cells copied before dev7 print every
|
|
120
|
+
informational note (thousands of `CT_CONDITIONAL_FLOW` lines), which notebook
|
|
121
|
+
platforms truncate, and label modeled names as "Observed names". The current
|
|
122
|
+
cell prints counts, grouped warnings, ranked candidates and, when `FUNCTION` or
|
|
123
|
+
`MODULE` filters exclude a name, the identities that exist without them. Keep
|
|
124
|
+
helper scripts (for example a one-off patch script) out of the analysis with
|
|
125
|
+
`.coltraceignore`.
|
|
126
|
+
|
|
117
127
|
## A column used inside SQL is not found
|
|
118
128
|
|
|
119
129
|
Since dev8, columns read by SQL text are `table` columns and can be selected
|
|
@@ -133,6 +143,23 @@ directly. If one is still missing:
|
|
|
133
143
|
candidate is the most likely origin. Pass `--column-id` (CLI) or `COLUMN_ID`
|
|
134
144
|
(notebook cell) to choose another.
|
|
135
145
|
|
|
146
|
+
## A column stops at a helper that picks the column by a parameter
|
|
147
|
+
|
|
148
|
+
Before dev10, `col = "month" if kind == "m" else "quarter"` followed by
|
|
149
|
+
`df[col]` invalidated the frame (`CT_DYNAMIC_COLUMN`, then `CT_OPAQUE_CALL` on
|
|
150
|
+
the next method). From dev10 the read is a label choice: each possible column
|
|
151
|
+
is read with inferred evidence and the note `CT_LABEL_CHOICE` names the site. A
|
|
152
|
+
label built from unknown text (user input, an f-string with unknown parts) is
|
|
153
|
+
still dynamic; replace it with a constant mapping or a conditional over known
|
|
154
|
+
labels.
|
|
155
|
+
|
|
156
|
+
## A score computed in a row-wise `apply` has no inputs
|
|
157
|
+
|
|
158
|
+
`df.apply(f, axis=1)` is modeled when `f` resolves and returns a value or
|
|
159
|
+
`pd.Series({...})` with literal labels. Labels built at run time, callbacks that
|
|
160
|
+
write to the row, and `groupby(...).apply` stay unmodeled
|
|
161
|
+
(`CT_UNMODELED_RESULT`).
|
|
162
|
+
|
|
136
163
|
## A helper call interrupts lineage
|
|
137
164
|
|
|
138
165
|
`CT_OPAQUE_CALL` marks a call ColTrace cannot resolve that receives a DataFrame
|
|
@@ -12,6 +12,7 @@ business rules.
|
|
|
12
12
|
| `shared/config.py` | Configured column lists and default parameters |
|
|
13
13
|
| `pipeline_a/extract/primary_extract.py` | f-string SQL builder, `if df.empty: return df` guard, loop over configured columns |
|
|
14
14
|
| `pipeline_a/extract/quality_extract.py` | SQL assembled from f-strings with BigQuery backticks, a helper fragment, an unknown metric column and an uncalled extractor (analyzed as a template) |
|
|
15
|
+
| `pipeline_a/transform/combine_sources.py` | Results kept in a dict and combined with an `acc = None` accumulator and `merge` |
|
|
15
16
|
| `shared/topic_changes_extract.py` | `spark.sql(build_query(days)).toPandas()` and `sdf = spark.sql(...); sdf.toPandas()` with an injected session |
|
|
16
17
|
| `pipeline_a/transform/priority_rank.py` | Scalar parameters, `np.where`, `isin` masks, filters, `sort_values().head(n)` |
|
|
17
18
|
| `pipeline_a/publish/serialize_webapp.py` | Projection, `to_dict(orient="records")`, a writer class |
|
|
@@ -29,7 +30,7 @@ python -m coltrace trace contact_attempts --from .coltrace/databricks/scan.json
|
|
|
29
30
|
python -m coltrace graph priority_score --from .coltrace/databricks/scan.json --output .coltrace/databricks/priority_score.html
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
Expected (dev8 also
|
|
33
|
+
Expected (dev8 and dev9 also cover the follow-up reports): all 16 files parse, the SQL SELECT list becomes the source of six
|
|
33
34
|
columns, and `n_contacts` flows into `volume_share`, `priority_score` and
|
|
34
35
|
`published_score`, which reach the Parquet export and the serialized view. Row
|
|
35
36
|
filter and sort-key columns are recorded as control, not as value inputs.
|
|
@@ -12,6 +12,7 @@ from shared.warehouse import make_run_query
|
|
|
12
12
|
from pipeline_a.extract.primary_extract import extract_primary
|
|
13
13
|
from pipeline_a.extract.quality_extract import extract_quality
|
|
14
14
|
from pipeline_a.publish.serialize_webapp import serialize
|
|
15
|
+
from pipeline_a.transform.combine_sources import combine
|
|
15
16
|
from pipeline_a.transform.priority_rank import prioritize
|
|
16
17
|
|
|
17
18
|
# COMMAND ----------
|
|
@@ -32,3 +33,12 @@ payload_a = serialize(ranked_a, "/dbfs/tmp/pipeline_a.json")
|
|
|
32
33
|
quality_a = extract_quality(run_query)
|
|
33
34
|
topics_a = extract_topic_changes(spark, days=7)
|
|
34
35
|
quality_a.merge(topics_a, on="session_id", how="left").to_parquet("/dbfs/tmp/quality_a.parquet")
|
|
36
|
+
|
|
37
|
+
# COMMAND ----------
|
|
38
|
+
|
|
39
|
+
results = {}
|
|
40
|
+
results["handled"] = quality_a[["session_id", "n_attempts"]]
|
|
41
|
+
results["waiting"] = extract_quality(run_query, metric="wait_seconds")[["session_id", "n_attempts"]]
|
|
42
|
+
combined = combine(results)
|
|
43
|
+
combined["attempts_total"] = combined["attempts_handled"] + combined["attempts_waiting"]
|
|
44
|
+
combined.to_parquet("/dbfs/tmp/attempts.parquet")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Results collected in a dict and combined with an ``acc = None`` accumulator."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def combine(results):
|
|
5
|
+
total = None
|
|
6
|
+
for name, part in results.items():
|
|
7
|
+
part = part.rename(columns={"n_attempts": f"attempts_{name}"})
|
|
8
|
+
total = part if total is None else total.merge(part, on="session_id", how="outer")
|
|
9
|
+
return total
|
|
@@ -121,6 +121,15 @@ def test_column():
|
|
|
121
121
|
print(f"Continuing with the first identity: {selected.id}")
|
|
122
122
|
except ColumnNotFoundError:
|
|
123
123
|
print("Column not found: " + missing_column_message(report, COLUMN_ID or COLUMN))
|
|
124
|
+
if not COLUMN_ID and (FUNCTION or MODULE):
|
|
125
|
+
unfiltered = graph.candidates(COLUMN)
|
|
126
|
+
if unfiltered:
|
|
127
|
+
print("Without FUNCTION/MODULE filters these identities exist:")
|
|
128
|
+
for item in unfiltered[:10]:
|
|
129
|
+
print(
|
|
130
|
+
f" {item.id} | {item.origin} | {item.module}::{item.scope} | "
|
|
131
|
+
f"{item.location.path}:{item.location.line}"
|
|
132
|
+
)
|
|
124
133
|
print("Database columns read by SQL text are selectable too (origin=table).")
|
|
125
134
|
names = sorted({item.name for item in report.columns})
|
|
126
135
|
print("Modeled static column names (not runtime df.columns):", ", ".join(names[:30]))
|