model2data 1.16.0__tar.gz → 1.16.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {model2data-1.16.0/model2data.egg-info → model2data-1.16.2}/PKG-INFO +1 -1
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/sql.py +75 -22
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/values.py +63 -26
- {model2data-1.16.0 → model2data-1.16.2/model2data.egg-info}/PKG-INFO +1 -1
- {model2data-1.16.0 → model2data-1.16.2}/pyproject.toml +1 -1
- model2data-1.16.2/tests/test_metrics_values.py +632 -0
- model2data-1.16.0/tests/test_metrics_values.py +0 -286
- {model2data-1.16.0 → model2data-1.16.2}/LICENSE +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/README.md +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/README_PYPI.md +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/LICENSE +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/VENDORED.md +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/base.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/column.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/enum.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/expression.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/index.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/note.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/project.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/reference.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/sticky_note.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/table.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/table_group.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/classes/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/constants.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/database.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/column.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/common.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/enum.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/generic.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/index.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/project.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/records.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/reference.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/sticky_note.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/table.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/table_group.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/table_partial.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/exceptions.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/parser/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/parser/blueprints.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/parser/parser.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/base.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/column.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/expression.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/index.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/note.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/project.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/sticky_note.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/table.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/table_group.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/column.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/enum.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/expression.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/index.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/note.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/reference.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/table.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/utils.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/tools.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/cli.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/hint_tests.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/naming.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/project.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/dbt_project.yml.jinja +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/hint_macros/model2data_hint_tests.sql +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/hint_macros/model2data_parent_tests.sql +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/hint_macros/model2data_when_tests.sql +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/history_macros/model2data_history_tests.sql +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/macros/generate_schema_name.sql +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/profiles.yml.jinja +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/tests.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/defects/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/defects/apply.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/defects/checks.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/defects/presets.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/defects/report.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/core.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/days.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/faker.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/hints.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/history.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/kinds.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/options.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/parents.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/relationships.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/timeline.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/when.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/guide/setup.md +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/guide/triage.md +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/guide/tune.md +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/check.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/columns.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/dbt.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/expression.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/graph.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/infer.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/lightdash.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/ossie.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/reader.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/semantic.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/types.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/model/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/model/_yaml.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/model/dbml.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/model/document.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/model/dump.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/model/engine.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/model/errors.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/model/reader.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/model/types.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/model/validate.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/output.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/parse/__init__.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/parse/dbml.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/PUBLISHING.md +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/README.md +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/examples/coffee_webshop.metrics.yml +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/examples/coffee_webshop.model2data.yml +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/metrics/README.md +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/metrics/metrics.schema.json +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/model.schema.json +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data/utils.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data.egg-info/SOURCES.txt +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data.egg-info/dependency_links.txt +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data.egg-info/entry_points.txt +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data.egg-info/requires.txt +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/model2data.egg-info/top_level.txt +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/setup.cfg +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_as_of_anchor.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_cli.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_cli_model.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_column_time_hints.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_coverage_gaps.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_days.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_dbml_parser.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_dbml_parser_fuzz.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_dbt_integration.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_dbt_naming.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_dbt_project.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_dbt_tests.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_defects.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_defects_dbt.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_defects_replay.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_defects_report.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_defects_review.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_determinism.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_distributions.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_examples_model.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_faker_name_inference.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_fast_paths.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_generation.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_guide.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_hint_tests.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_history.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_issue_lines.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_lone_country.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_cli.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_dbt.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_expression.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_lightdash.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_ossie.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_reader.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_semantic.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_spec.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_model.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_model_dbml.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_model_edges.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_model_messages.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_model_vocabulary.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_options.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_parent_dates.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_release_stress.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_reproducibility.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_row_identity.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_shaping.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_spec_alignment.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_spec_schema.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_table_seeds.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_timeline.py +0 -0
- {model2data-1.16.0 → model2data-1.16.2}/tests/test_when.py +0 -0
|
@@ -5,11 +5,11 @@ Two dialects of the same thing:
|
|
|
5
5
|
- **warehouse SQL** (`query`, `scalar`), which the known values run on DuckDB
|
|
6
6
|
and the generated dbt tests run on DuckDB or Postgres. It reads relations a
|
|
7
7
|
caller names (a DuckDB table, a dbt `ref()`), joins the tables a metric's
|
|
8
|
-
filter and
|
|
9
|
-
whose parent is missing stays, and fails any filter on the
|
|
10
|
-
does), and casts every column it reads to the type its kind
|
|
11
|
-
(`model2data.metrics.columns.SQL_TYPES`), so the answer does not
|
|
12
|
-
how a CSV loader guessed the column's type.
|
|
8
|
+
filter is on (and, by month, its time) along their many-to-one path with
|
|
9
|
+
`left join` (a row whose parent is missing stays, and fails any filter on the
|
|
10
|
+
parent, as a null does), and casts every column it reads to the type its kind
|
|
11
|
+
is read as (`model2data.metrics.columns.SQL_TYPES`), so the answer does not
|
|
12
|
+
depend on how a CSV loader guessed the column's type.
|
|
13
13
|
- **ANSI SQL for Ossie** (`ansi`), over `dataset.field` names, with no joins:
|
|
14
14
|
an Ossie consumer joins along the relationships the document declares.
|
|
15
15
|
|
|
@@ -149,19 +149,27 @@ def aggregate_sql(
|
|
|
149
149
|
# ---------------------------------------------------------------------------
|
|
150
150
|
# Warehouse SQL
|
|
151
151
|
# ---------------------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
class _From:
|
|
153
|
+
"""The `from` and `join` clauses of a simple metric, and the alias of each table in them.
|
|
154
154
|
|
|
155
|
+
Joins only the tables in `needed` (of `metric.joins`, in its order): a table the
|
|
156
|
+
query reads no column of stays out, so a parent with a duplicated key cannot repeat
|
|
157
|
+
the metric's rows in a query that does not need it.
|
|
155
158
|
|
|
156
|
-
|
|
157
|
-
|
|
159
|
+
Every column the query reads is written by `column`, which also records it in `read`
|
|
160
|
+
(by table key), so `columns_read` knows them from the same code that writes them.
|
|
161
|
+
"""
|
|
158
162
|
|
|
159
|
-
def __init__(self, metric: ResolvedMetric, relation: Relation):
|
|
163
|
+
def __init__(self, metric: ResolvedMetric, relation: Relation, needed: set[str]):
|
|
160
164
|
assert metric.table is not None
|
|
161
165
|
self.lines = [f"from {relation(metric.table)} as t0"]
|
|
162
166
|
self.alias = {metric.table: "t0"}
|
|
167
|
+
self.read: dict[str, set[str]] = {metric.table: set()}
|
|
163
168
|
by_prefix: dict[tuple, str] = {(): "t0"}
|
|
169
|
+
table_of = {"t0": metric.table}
|
|
164
170
|
for table, path in metric.joins.items():
|
|
171
|
+
if table not in needed:
|
|
172
|
+
continue
|
|
165
173
|
for length in range(1, len(path) + 1):
|
|
166
174
|
prefix = path[:length]
|
|
167
175
|
if prefix in by_prefix:
|
|
@@ -169,46 +177,91 @@ class _From:
|
|
|
169
177
|
alias = f"t{len(by_prefix)}"
|
|
170
178
|
previous = by_prefix[path[: length - 1]]
|
|
171
179
|
step = path[length - 1]
|
|
180
|
+
table_of[alias] = step.parent
|
|
172
181
|
on = " and ".join(
|
|
173
|
-
f"cast({
|
|
182
|
+
f"cast({self._at(previous, table_of[previous], child)} as varchar)"
|
|
183
|
+
f" = cast({self._at(alias, step.parent, parent)} as varchar)"
|
|
174
184
|
for child, parent in zip(step.child_columns, step.parent_columns, strict=True)
|
|
175
185
|
)
|
|
176
186
|
self.lines.append(f"left join {relation(step.parent)} as {alias} on {on}")
|
|
177
187
|
by_prefix[prefix] = alias
|
|
178
188
|
self.alias[table] = by_prefix[path]
|
|
179
189
|
|
|
190
|
+
def _at(self, alias: str, table: str, column: str) -> str:
|
|
191
|
+
self.read.setdefault(table, set()).add(column)
|
|
192
|
+
return f"{alias}.{quote(column)}"
|
|
193
|
+
|
|
194
|
+
def column(self, table: str, column: str) -> str:
|
|
195
|
+
"""`column` of `table` (a table key) as the query reads it, through its alias."""
|
|
196
|
+
return self._at(self.alias[table], table, column)
|
|
197
|
+
|
|
198
|
+
def cast(self, table: str, column: str, kind: ColumnKind) -> str:
|
|
199
|
+
return f"cast({self.column(table, column)} as {SQL_TYPES[kind]})"
|
|
200
|
+
|
|
180
201
|
|
|
181
202
|
def _parts(
|
|
182
|
-
semantic: SemanticModel, metric: ResolvedMetric, relation: Relation
|
|
203
|
+
semantic: SemanticModel, metric: ResolvedMetric, relation: Relation, by_month: bool
|
|
183
204
|
) -> tuple[str, _From]:
|
|
184
|
-
|
|
205
|
+
# The total joins what its filter reads; by month, the time column's table as well.
|
|
206
|
+
needed = (
|
|
207
|
+
{condition.column.rpartition(".")[0] for condition in metric.where.conditions()}
|
|
208
|
+
if metric.where is not None
|
|
209
|
+
else set()
|
|
210
|
+
)
|
|
211
|
+
if by_month:
|
|
212
|
+
assert metric.time is not None
|
|
213
|
+
needed.add(metric.time.rpartition(".")[0])
|
|
214
|
+
clauses = _From(metric, relation, needed)
|
|
185
215
|
|
|
186
216
|
def column_of(path: str) -> tuple[str, ColumnKind]:
|
|
187
217
|
table, column = find_column(semantic.model, path)
|
|
188
218
|
assert table is not None and column is not None
|
|
189
219
|
kind = column_kind(semantic.model, column)
|
|
190
|
-
return
|
|
220
|
+
return clauses.cast(table, path.rpartition(".")[2], kind), kind
|
|
191
221
|
|
|
192
222
|
condition = where_sql(metric.where, column_of) if metric.where is not None else None
|
|
193
223
|
value = None
|
|
194
224
|
if metric.kind == "simple":
|
|
225
|
+
assert metric.table is not None
|
|
195
226
|
assert metric.column is not None and metric.column_kind is not None
|
|
196
|
-
value =
|
|
227
|
+
value = clauses.cast(metric.table, metric.column, metric.column_kind)
|
|
197
228
|
return aggregate_sql(metric, value, condition), clauses
|
|
198
229
|
|
|
199
230
|
|
|
200
|
-
def
|
|
201
|
-
|
|
202
|
-
|
|
231
|
+
def _query(
|
|
232
|
+
semantic: SemanticModel, name: str, relation: Relation, by_month: bool
|
|
233
|
+
) -> tuple[str, _From]:
|
|
203
234
|
metric = semantic.metrics[name]
|
|
204
|
-
aggregate, clauses = _parts(semantic, metric, relation)
|
|
235
|
+
aggregate, clauses = _parts(semantic, metric, relation, by_month)
|
|
205
236
|
joins = "\n".join(clauses.lines)
|
|
206
237
|
if not by_month:
|
|
207
|
-
return f"select {aggregate} as value\n{joins}"
|
|
238
|
+
return f"select {aggregate} as value\n{joins}", clauses
|
|
208
239
|
assert metric.time is not None
|
|
209
240
|
table, column = metric.time.rpartition(".")[0], metric.time.rpartition(".")[2]
|
|
210
|
-
month = f"cast(date_trunc('month', cast({clauses.
|
|
211
|
-
return
|
|
241
|
+
month = f"cast(date_trunc('month', cast({clauses.column(table, column)} as timestamp)) as date)"
|
|
242
|
+
return (
|
|
243
|
+
f"select {month} as month, {aggregate} as value\n{joins}\ngroup by 1\norder by 1",
|
|
244
|
+
clauses,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def query(semantic: SemanticModel, name: str, relation: Relation, *, by_month: bool = False) -> str:
|
|
249
|
+
"""A simple metric or row count as a query: one row with its `value`, or, `by_month`, a
|
|
250
|
+
row per calendar month of its time column (`month` the month's first day)."""
|
|
251
|
+
return _query(semantic, name, relation, by_month)[0]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def columns_read(semantic: SemanticModel, name: str) -> dict[str, set[str]]:
|
|
255
|
+
"""Every column `query` reads for a simple metric or row count, by table key, its
|
|
256
|
+
`by_month` query's time column included: the columns its aggregate, filter, time and
|
|
257
|
+
joins name. Every table the query reads is a key, the metric's own table always, even
|
|
258
|
+
when no column of it is named (a row count with no filter: `count(*)`).
|
|
259
|
+
|
|
260
|
+
Recorded while the query is written rather than found in its text, so it cannot
|
|
261
|
+
miss a column however the column is quoted or aliased.
|
|
262
|
+
"""
|
|
263
|
+
metric = semantic.metrics[name]
|
|
264
|
+
return _query(semantic, name, str, by_month=metric.time is not None)[1].read
|
|
212
265
|
|
|
213
266
|
|
|
214
267
|
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.2"
|
|
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"
|