model2data 1.16.0__tar.gz → 1.16.1__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.
- {model2data-1.16.0/model2data.egg-info → model2data-1.16.1}/PKG-INFO +1 -1
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/sql.py +52 -14
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/values.py +63 -26
- {model2data-1.16.0 → model2data-1.16.1/model2data.egg-info}/PKG-INFO +1 -1
- {model2data-1.16.0 → model2data-1.16.1}/pyproject.toml +1 -1
- model2data-1.16.1/tests/test_metrics_values.py +617 -0
- model2data-1.16.0/tests/test_metrics_values.py +0 -286
- {model2data-1.16.0 → model2data-1.16.1}/LICENSE +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/README.md +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/README_PYPI.md +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/LICENSE +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/VENDORED.md +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/base.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/column.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/enum.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/expression.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/index.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/note.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/project.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/reference.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/sticky_note.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/table.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/table_group.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/classes/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/constants.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/database.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/column.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/common.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/enum.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/generic.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/index.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/project.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/records.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/reference.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/sticky_note.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/table.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/table_group.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/table_partial.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/exceptions.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/parser/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/parser/blueprints.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/parser/parser.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/base.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/column.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/expression.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/index.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/note.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/project.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/sticky_note.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/table.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/table_group.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/column.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/enum.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/expression.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/index.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/note.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/reference.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/table.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/utils.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/tools.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/cli.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/hint_tests.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/naming.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/project.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/dbt_project.yml.jinja +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/hint_macros/model2data_hint_tests.sql +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/hint_macros/model2data_parent_tests.sql +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/hint_macros/model2data_when_tests.sql +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/history_macros/model2data_history_tests.sql +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/macros/generate_schema_name.sql +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/profiles.yml.jinja +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/tests.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/defects/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/defects/apply.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/defects/checks.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/defects/presets.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/defects/report.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/core.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/days.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/faker.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/hints.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/history.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/kinds.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/options.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/parents.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/relationships.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/timeline.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/when.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/guide/setup.md +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/guide/triage.md +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/guide/tune.md +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/check.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/columns.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/dbt.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/expression.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/graph.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/infer.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/lightdash.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/ossie.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/reader.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/semantic.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/types.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/model/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/model/_yaml.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/model/dbml.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/model/document.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/model/dump.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/model/engine.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/model/errors.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/model/reader.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/model/types.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/model/validate.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/output.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/parse/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/parse/dbml.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/PUBLISHING.md +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/README.md +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/examples/coffee_webshop.metrics.yml +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/examples/coffee_webshop.model2data.yml +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/metrics/README.md +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/metrics/metrics.schema.json +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/model.schema.json +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data/utils.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data.egg-info/SOURCES.txt +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data.egg-info/dependency_links.txt +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data.egg-info/entry_points.txt +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data.egg-info/requires.txt +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/model2data.egg-info/top_level.txt +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/setup.cfg +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_as_of_anchor.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_cli.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_cli_model.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_column_time_hints.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_coverage_gaps.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_days.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_dbml_parser.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_dbml_parser_fuzz.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_dbt_integration.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_dbt_naming.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_dbt_project.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_dbt_tests.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_defects.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_defects_dbt.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_defects_replay.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_defects_report.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_defects_review.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_determinism.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_distributions.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_examples_model.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_faker_name_inference.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_fast_paths.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_generation.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_guide.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_hint_tests.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_history.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_issue_lines.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_lone_country.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_cli.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_dbt.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_expression.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_lightdash.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_ossie.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_reader.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_semantic.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_spec.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_model.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_model_dbml.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_model_edges.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_model_messages.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_model_vocabulary.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_options.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_parent_dates.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_release_stress.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_reproducibility.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_row_identity.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_shaping.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_spec_alignment.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_spec_schema.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_table_seeds.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_timeline.py +0 -0
- {model2data-1.16.0 → model2data-1.16.1}/tests/test_when.py +0 -0
|
@@ -149,18 +149,20 @@ def aggregate_sql(
|
|
|
149
149
|
# ---------------------------------------------------------------------------
|
|
150
150
|
# Warehouse SQL
|
|
151
151
|
# ---------------------------------------------------------------------------
|
|
152
|
-
def _cast(alias: str, column: str, kind: ColumnKind) -> str:
|
|
153
|
-
return f"cast({alias}.{quote(column)} as {SQL_TYPES[kind]})"
|
|
154
|
-
|
|
155
|
-
|
|
156
152
|
class _From:
|
|
157
|
-
"""The `from` and `join` clauses of a simple metric, and the alias of each table in them.
|
|
153
|
+
"""The `from` and `join` clauses of a simple metric, and the alias of each table in them.
|
|
154
|
+
|
|
155
|
+
Every column the query reads is written by `column`, which also records it in `read`
|
|
156
|
+
(by table key), so `columns_read` knows them from the same code that writes them.
|
|
157
|
+
"""
|
|
158
158
|
|
|
159
159
|
def __init__(self, metric: ResolvedMetric, relation: Relation):
|
|
160
160
|
assert metric.table is not None
|
|
161
161
|
self.lines = [f"from {relation(metric.table)} as t0"]
|
|
162
162
|
self.alias = {metric.table: "t0"}
|
|
163
|
+
self.read: dict[str, set[str]] = {metric.table: set()}
|
|
163
164
|
by_prefix: dict[tuple, str] = {(): "t0"}
|
|
165
|
+
table_of = {"t0": metric.table}
|
|
164
166
|
for table, path in metric.joins.items():
|
|
165
167
|
for length in range(1, len(path) + 1):
|
|
166
168
|
prefix = path[:length]
|
|
@@ -169,14 +171,27 @@ class _From:
|
|
|
169
171
|
alias = f"t{len(by_prefix)}"
|
|
170
172
|
previous = by_prefix[path[: length - 1]]
|
|
171
173
|
step = path[length - 1]
|
|
174
|
+
table_of[alias] = step.parent
|
|
172
175
|
on = " and ".join(
|
|
173
|
-
f"cast({
|
|
176
|
+
f"cast({self._at(previous, table_of[previous], child)} as varchar)"
|
|
177
|
+
f" = cast({self._at(alias, step.parent, parent)} as varchar)"
|
|
174
178
|
for child, parent in zip(step.child_columns, step.parent_columns, strict=True)
|
|
175
179
|
)
|
|
176
180
|
self.lines.append(f"left join {relation(step.parent)} as {alias} on {on}")
|
|
177
181
|
by_prefix[prefix] = alias
|
|
178
182
|
self.alias[table] = by_prefix[path]
|
|
179
183
|
|
|
184
|
+
def _at(self, alias: str, table: str, column: str) -> str:
|
|
185
|
+
self.read.setdefault(table, set()).add(column)
|
|
186
|
+
return f"{alias}.{quote(column)}"
|
|
187
|
+
|
|
188
|
+
def column(self, table: str, column: str) -> str:
|
|
189
|
+
"""`column` of `table` (a table key) as the query reads it, through its alias."""
|
|
190
|
+
return self._at(self.alias[table], table, column)
|
|
191
|
+
|
|
192
|
+
def cast(self, table: str, column: str, kind: ColumnKind) -> str:
|
|
193
|
+
return f"cast({self.column(table, column)} as {SQL_TYPES[kind]})"
|
|
194
|
+
|
|
180
195
|
|
|
181
196
|
def _parts(
|
|
182
197
|
semantic: SemanticModel, metric: ResolvedMetric, relation: Relation
|
|
@@ -187,28 +202,51 @@ def _parts(
|
|
|
187
202
|
table, column = find_column(semantic.model, path)
|
|
188
203
|
assert table is not None and column is not None
|
|
189
204
|
kind = column_kind(semantic.model, column)
|
|
190
|
-
return
|
|
205
|
+
return clauses.cast(table, path.rpartition(".")[2], kind), kind
|
|
191
206
|
|
|
192
207
|
condition = where_sql(metric.where, column_of) if metric.where is not None else None
|
|
193
208
|
value = None
|
|
194
209
|
if metric.kind == "simple":
|
|
210
|
+
assert metric.table is not None
|
|
195
211
|
assert metric.column is not None and metric.column_kind is not None
|
|
196
|
-
value =
|
|
212
|
+
value = clauses.cast(metric.table, metric.column, metric.column_kind)
|
|
197
213
|
return aggregate_sql(metric, value, condition), clauses
|
|
198
214
|
|
|
199
215
|
|
|
200
|
-
def
|
|
201
|
-
|
|
202
|
-
|
|
216
|
+
def _query(
|
|
217
|
+
semantic: SemanticModel, name: str, relation: Relation, by_month: bool
|
|
218
|
+
) -> tuple[str, _From]:
|
|
203
219
|
metric = semantic.metrics[name]
|
|
204
220
|
aggregate, clauses = _parts(semantic, metric, relation)
|
|
205
221
|
joins = "\n".join(clauses.lines)
|
|
206
222
|
if not by_month:
|
|
207
|
-
return f"select {aggregate} as value\n{joins}"
|
|
223
|
+
return f"select {aggregate} as value\n{joins}", clauses
|
|
208
224
|
assert metric.time is not None
|
|
209
225
|
table, column = metric.time.rpartition(".")[0], metric.time.rpartition(".")[2]
|
|
210
|
-
month = f"cast(date_trunc('month', cast({clauses.
|
|
211
|
-
return
|
|
226
|
+
month = f"cast(date_trunc('month', cast({clauses.column(table, column)} as timestamp)) as date)"
|
|
227
|
+
return (
|
|
228
|
+
f"select {month} as month, {aggregate} as value\n{joins}\ngroup by 1\norder by 1",
|
|
229
|
+
clauses,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def query(semantic: SemanticModel, name: str, relation: Relation, *, by_month: bool = False) -> str:
|
|
234
|
+
"""A simple metric or row count as a query: one row with its `value`, or, `by_month`, a
|
|
235
|
+
row per calendar month of its time column (`month` the month's first day)."""
|
|
236
|
+
return _query(semantic, name, relation, by_month)[0]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def columns_read(semantic: SemanticModel, name: str) -> dict[str, set[str]]:
|
|
240
|
+
"""Every column `query` reads for a simple metric or row count, by table key, its
|
|
241
|
+
`by_month` query's time column included: the columns its aggregate, filter, time and
|
|
242
|
+
joins name. Every table the query reads is a key, the metric's own table always, even
|
|
243
|
+
when no column of it is named (a row count with no filter: `count(*)`).
|
|
244
|
+
|
|
245
|
+
Recorded while the query is written rather than found in its text, so it cannot
|
|
246
|
+
miss a column however the column is quoted or aliased.
|
|
247
|
+
"""
|
|
248
|
+
metric = semantic.metrics[name]
|
|
249
|
+
return _query(semantic, name, str, by_month=metric.time is not None)[1].read
|
|
212
250
|
|
|
213
251
|
|
|
214
252
|
def scalar(semantic: SemanticModel, name: str, relation: Relation) -> str:
|
|
@@ -13,6 +13,10 @@ How the values are computed:
|
|
|
13
13
|
any case) being null as dbt's seed loader reads them. Each column a metric
|
|
14
14
|
touches is then cast to the SQL type of its kind, in the same SQL the dbt
|
|
15
15
|
tests run (`model2data.metrics.sql`). DuckDB runs it, on one thread.
|
|
16
|
+
- Only the columns some metric's query reads are read (`columns_read`), so a
|
|
17
|
+
wide table costs what its few metric columns cost. That gives the same
|
|
18
|
+
values: a column's CSV text depends on that column alone, and a query cannot
|
|
19
|
+
see a column it does not name.
|
|
16
20
|
- Simple metrics and row counts are SQL aggregates. A ratio is its
|
|
17
21
|
numerator's total over its denominator's, null when the denominator is null
|
|
18
22
|
or zero; a derived metric is its expression over its inputs' totals, with
|
|
@@ -44,7 +48,7 @@ import pandas as pd
|
|
|
44
48
|
from model2data.metrics import expression as expr
|
|
45
49
|
from model2data.metrics.check import SPEC_VERSION
|
|
46
50
|
from model2data.metrics.semantic import ResolvedMetric, SemanticModel
|
|
47
|
-
from model2data.metrics.sql import query, quote
|
|
51
|
+
from model2data.metrics.sql import columns_read, query, quote
|
|
48
52
|
|
|
49
53
|
Number = Union[int, Decimal]
|
|
50
54
|
_PLACES = Decimal("0.000001")
|
|
@@ -144,15 +148,42 @@ def _empty(metric: ResolvedMetric) -> Optional[Number]:
|
|
|
144
148
|
return 0 if metric.kind == "count" or metric.agg in ("count", "count_distinct") else None
|
|
145
149
|
|
|
146
150
|
|
|
147
|
-
def
|
|
148
|
-
"""`frame` as its seed file reads back: every
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
def _text_columns(frame: pd.DataFrame, columns: list[Any]) -> pd.DataFrame:
|
|
152
|
+
"""`columns` of `frame` as its seed file reads them back: every value the text of its
|
|
153
|
+
CSV field, nulls left as `""` (`_NULL_OR_TEXT` makes them null in DuckDB).
|
|
154
|
+
|
|
155
|
+
pandas writes each column of a CSV by that column's own type and values, and quotes
|
|
156
|
+
each field on its own, so a column's text is the same whether the other columns are
|
|
157
|
+
written beside it or not. One column with an empty field is written as `""`, not as
|
|
158
|
+
a blank line, so no row is lost when a single column is written. Read as `object`
|
|
159
|
+
rather than `str`: with no NA filtering every field is a Python string either way, and
|
|
160
|
+
pandas 3 reads `str` as its own string type, which DuckDB cannot scan.
|
|
161
|
+
"""
|
|
162
|
+
buffer = io.StringIO(frame[columns].to_csv(index=False))
|
|
163
|
+
return pd.read_csv(buffer, dtype=object, keep_default_na=False, na_values=[], na_filter=False)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# What dbt's seed loader reads as null: an empty field, or `null` in any case. The letters
|
|
167
|
+
# are spelled out rather than compared with `lower()`, which folds non-ASCII letters too:
|
|
168
|
+
# in Python only `n`, `u`, `l` and their capitals lower to those letters, so these are
|
|
169
|
+
# exactly the fields for which `field.lower() == "null"`.
|
|
170
|
+
_NULL_OR_TEXT = (
|
|
171
|
+
"case when {value} = '' or regexp_full_match({value}, '[Nn][Uu][Ll][Ll]') "
|
|
172
|
+
"then null else {value} end"
|
|
173
|
+
)
|
|
174
|
+
|
|
152
175
|
|
|
176
|
+
def _kept(frame: pd.DataFrame, read: set[str]) -> list[Any]:
|
|
177
|
+
"""The columns of `frame` to load, in its order: those the queries read.
|
|
153
178
|
|
|
154
|
-
|
|
155
|
-
|
|
179
|
+
DuckDB matches identifiers without regard to case, so a column is kept when any name
|
|
180
|
+
read folds to its name; keeping one more than needed costs only memory. A table whose
|
|
181
|
+
rows a metric counts without naming any of its columns (`count(*)`) keeps its first
|
|
182
|
+
column, which has a value, or a null, on every row, so the count is the same.
|
|
183
|
+
"""
|
|
184
|
+
folded = {name.casefold() for name in read}
|
|
185
|
+
kept = [column for column in frame.columns if str(column).casefold() in folded]
|
|
186
|
+
return kept or list(frame.columns[:1])
|
|
156
187
|
|
|
157
188
|
|
|
158
189
|
def known_values(
|
|
@@ -165,35 +196,41 @@ def known_values(
|
|
|
165
196
|
"""Every metric of `semantic` over `frames` (by table key: what the seeds hold)."""
|
|
166
197
|
import duckdb
|
|
167
198
|
|
|
199
|
+
computed = [
|
|
200
|
+
name for name, metric in semantic.metrics.items() if metric.kind in ("simple", "count")
|
|
201
|
+
]
|
|
202
|
+
read: dict[str, set[str]] = {}
|
|
203
|
+
for name in computed:
|
|
204
|
+
for table, columns in columns_read(semantic, name).items():
|
|
205
|
+
read.setdefault(table, set()).update(columns)
|
|
206
|
+
keys = [table for table in semantic.entities if table in read]
|
|
207
|
+
for key in keys:
|
|
208
|
+
if key not in frames:
|
|
209
|
+
raise ValueError(f"no generated rows for table {key!r}, which a metric reads")
|
|
210
|
+
names = {key: f"m2d_{index}" for index, key in enumerate(keys)}
|
|
211
|
+
relation = names.__getitem__
|
|
212
|
+
|
|
168
213
|
connection = duckdb.connect(":memory:")
|
|
169
214
|
try:
|
|
170
215
|
connection.execute("set threads = 1")
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
for metric in semantic.metrics.values()
|
|
175
|
-
if metric.table is not None
|
|
176
|
-
for table in (metric.table, *metric.joins)
|
|
177
|
-
}
|
|
178
|
-
for index, key in enumerate(table for table in semantic.entities if table in used):
|
|
179
|
-
if key not in frames:
|
|
180
|
-
raise ValueError(f"no generated rows for table {key!r}, which a metric reads")
|
|
181
|
-
names[key] = f"m2d_{index}"
|
|
182
|
-
text = _as_text(frames[key])
|
|
216
|
+
for key in keys:
|
|
217
|
+
# One table's text at a time: it is freed once DuckDB holds its own copy.
|
|
218
|
+
text = _text_columns(frames[key], _kept(frames[key], read[key]))
|
|
183
219
|
connection.register(f"{names[key]}_frame", text)
|
|
184
|
-
columns = ", ".join(
|
|
220
|
+
columns = ", ".join(
|
|
221
|
+
f"{_NULL_OR_TEXT.format(value=f'cast({quote(c)} as varchar)')} as {quote(c)}"
|
|
222
|
+
for c in text.columns
|
|
223
|
+
)
|
|
185
224
|
connection.execute(
|
|
186
225
|
f"create table {names[key]} as select {columns} from {names[key]}_frame"
|
|
187
226
|
)
|
|
188
227
|
connection.unregister(f"{names[key]}_frame")
|
|
228
|
+
del text
|
|
189
229
|
raw: dict[str, tuple[Any, Optional[dict[str, Any]]]] = {}
|
|
190
|
-
for name
|
|
191
|
-
if metric.kind not in ("simple", "count"):
|
|
192
|
-
continue
|
|
193
|
-
relation = names.__getitem__
|
|
230
|
+
for name in computed:
|
|
194
231
|
total = connection.execute(query(semantic, name, relation)).fetchone()
|
|
195
232
|
by_month = None
|
|
196
|
-
if
|
|
233
|
+
if semantic.metrics[name].time is not None:
|
|
197
234
|
rows = connection.execute(query(semantic, name, relation, by_month=True)).fetchall()
|
|
198
235
|
by_month = {
|
|
199
236
|
month.strftime("%Y-%m"): value for month, value in rows if month is not None
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "model2data"
|
|
7
|
-
version = "1.16.
|
|
7
|
+
version = "1.16.1"
|
|
8
8
|
description = "Turn a data model into realistic synthetic data, a runnable dbt project and metrics with known values"
|
|
9
9
|
readme = "README_PYPI.md"
|
|
10
10
|
requires-python = ">=3.10"
|