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.
Files changed (199) hide show
  1. {model2data-1.16.0/model2data.egg-info → model2data-1.16.1}/PKG-INFO +1 -1
  2. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/sql.py +52 -14
  3. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/values.py +63 -26
  4. {model2data-1.16.0 → model2data-1.16.1/model2data.egg-info}/PKG-INFO +1 -1
  5. {model2data-1.16.0 → model2data-1.16.1}/pyproject.toml +1 -1
  6. model2data-1.16.1/tests/test_metrics_values.py +617 -0
  7. model2data-1.16.0/tests/test_metrics_values.py +0 -286
  8. {model2data-1.16.0 → model2data-1.16.1}/LICENSE +0 -0
  9. {model2data-1.16.0 → model2data-1.16.1}/README.md +0 -0
  10. {model2data-1.16.0 → model2data-1.16.1}/README_PYPI.md +0 -0
  11. {model2data-1.16.0 → model2data-1.16.1}/model2data/__init__.py +0 -0
  12. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/__init__.py +0 -0
  13. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/LICENSE +0 -0
  14. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/VENDORED.md +0 -0
  15. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/__init__.py +0 -0
  16. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/__init__.py +0 -0
  17. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/base.py +0 -0
  18. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/column.py +0 -0
  19. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/enum.py +0 -0
  20. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/expression.py +0 -0
  21. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/index.py +0 -0
  22. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/note.py +0 -0
  23. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/project.py +0 -0
  24. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/reference.py +0 -0
  25. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/sticky_note.py +0 -0
  26. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/table.py +0 -0
  27. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/_classes/table_group.py +0 -0
  28. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/classes/__init__.py +0 -0
  29. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/constants.py +0 -0
  30. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/database.py +0 -0
  31. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/__init__.py +0 -0
  32. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/column.py +0 -0
  33. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/common.py +0 -0
  34. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/enum.py +0 -0
  35. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/generic.py +0 -0
  36. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/index.py +0 -0
  37. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/project.py +0 -0
  38. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/records.py +0 -0
  39. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/reference.py +0 -0
  40. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/sticky_note.py +0 -0
  41. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/table.py +0 -0
  42. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/table_group.py +0 -0
  43. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/definitions/table_partial.py +0 -0
  44. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/exceptions.py +0 -0
  45. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/parser/__init__.py +0 -0
  46. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/parser/blueprints.py +0 -0
  47. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/parser/parser.py +0 -0
  48. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/__init__.py +0 -0
  49. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/base.py +0 -0
  50. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/__init__.py +0 -0
  51. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py +0 -0
  52. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/column.py +0 -0
  53. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py +0 -0
  54. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/expression.py +0 -0
  55. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/index.py +0 -0
  56. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/note.py +0 -0
  57. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/project.py +0 -0
  58. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py +0 -0
  59. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py +0 -0
  60. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/sticky_note.py +0 -0
  61. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/table.py +0 -0
  62. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/table_group.py +0 -0
  63. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py +0 -0
  64. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/__init__.py +0 -0
  65. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py +0 -0
  66. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/column.py +0 -0
  67. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/enum.py +0 -0
  68. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/expression.py +0 -0
  69. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/index.py +0 -0
  70. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/note.py +0 -0
  71. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/reference.py +0 -0
  72. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py +0 -0
  73. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/table.py +0 -0
  74. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/renderer/sql/default/utils.py +0 -0
  75. {model2data-1.16.0 → model2data-1.16.1}/model2data/_vendor/pydbml/tools.py +0 -0
  76. {model2data-1.16.0 → model2data-1.16.1}/model2data/cli.py +0 -0
  77. {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/__init__.py +0 -0
  78. {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/hint_tests.py +0 -0
  79. {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/naming.py +0 -0
  80. {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/project.py +0 -0
  81. {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/dbt_project.yml.jinja +0 -0
  82. {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/hint_macros/model2data_hint_tests.sql +0 -0
  83. {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/hint_macros/model2data_parent_tests.sql +0 -0
  84. {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/hint_macros/model2data_when_tests.sql +0 -0
  85. {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/history_macros/model2data_history_tests.sql +0 -0
  86. {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/macros/generate_schema_name.sql +0 -0
  87. {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/templates/profiles.yml.jinja +0 -0
  88. {model2data-1.16.0 → model2data-1.16.1}/model2data/dbt/tests.py +0 -0
  89. {model2data-1.16.0 → model2data-1.16.1}/model2data/defects/__init__.py +0 -0
  90. {model2data-1.16.0 → model2data-1.16.1}/model2data/defects/apply.py +0 -0
  91. {model2data-1.16.0 → model2data-1.16.1}/model2data/defects/checks.py +0 -0
  92. {model2data-1.16.0 → model2data-1.16.1}/model2data/defects/presets.py +0 -0
  93. {model2data-1.16.0 → model2data-1.16.1}/model2data/defects/report.py +0 -0
  94. {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/__init__.py +0 -0
  95. {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/core.py +0 -0
  96. {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/days.py +0 -0
  97. {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/faker.py +0 -0
  98. {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/hints.py +0 -0
  99. {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/history.py +0 -0
  100. {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/kinds.py +0 -0
  101. {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/options.py +0 -0
  102. {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/parents.py +0 -0
  103. {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/relationships.py +0 -0
  104. {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/timeline.py +0 -0
  105. {model2data-1.16.0 → model2data-1.16.1}/model2data/generate/when.py +0 -0
  106. {model2data-1.16.0 → model2data-1.16.1}/model2data/guide/setup.md +0 -0
  107. {model2data-1.16.0 → model2data-1.16.1}/model2data/guide/triage.md +0 -0
  108. {model2data-1.16.0 → model2data-1.16.1}/model2data/guide/tune.md +0 -0
  109. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/__init__.py +0 -0
  110. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/check.py +0 -0
  111. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/columns.py +0 -0
  112. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/dbt.py +0 -0
  113. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/expression.py +0 -0
  114. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/graph.py +0 -0
  115. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/infer.py +0 -0
  116. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/lightdash.py +0 -0
  117. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/ossie.py +0 -0
  118. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/reader.py +0 -0
  119. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/semantic.py +0 -0
  120. {model2data-1.16.0 → model2data-1.16.1}/model2data/metrics/types.py +0 -0
  121. {model2data-1.16.0 → model2data-1.16.1}/model2data/model/__init__.py +0 -0
  122. {model2data-1.16.0 → model2data-1.16.1}/model2data/model/_yaml.py +0 -0
  123. {model2data-1.16.0 → model2data-1.16.1}/model2data/model/dbml.py +0 -0
  124. {model2data-1.16.0 → model2data-1.16.1}/model2data/model/document.py +0 -0
  125. {model2data-1.16.0 → model2data-1.16.1}/model2data/model/dump.py +0 -0
  126. {model2data-1.16.0 → model2data-1.16.1}/model2data/model/engine.py +0 -0
  127. {model2data-1.16.0 → model2data-1.16.1}/model2data/model/errors.py +0 -0
  128. {model2data-1.16.0 → model2data-1.16.1}/model2data/model/reader.py +0 -0
  129. {model2data-1.16.0 → model2data-1.16.1}/model2data/model/types.py +0 -0
  130. {model2data-1.16.0 → model2data-1.16.1}/model2data/model/validate.py +0 -0
  131. {model2data-1.16.0 → model2data-1.16.1}/model2data/output.py +0 -0
  132. {model2data-1.16.0 → model2data-1.16.1}/model2data/parse/__init__.py +0 -0
  133. {model2data-1.16.0 → model2data-1.16.1}/model2data/parse/dbml.py +0 -0
  134. {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/PUBLISHING.md +0 -0
  135. {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/README.md +0 -0
  136. {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/examples/coffee_webshop.metrics.yml +0 -0
  137. {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/examples/coffee_webshop.model2data.yml +0 -0
  138. {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/metrics/README.md +0 -0
  139. {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/metrics/metrics.schema.json +0 -0
  140. {model2data-1.16.0 → model2data-1.16.1}/model2data/spec/model.schema.json +0 -0
  141. {model2data-1.16.0 → model2data-1.16.1}/model2data/utils.py +0 -0
  142. {model2data-1.16.0 → model2data-1.16.1}/model2data.egg-info/SOURCES.txt +0 -0
  143. {model2data-1.16.0 → model2data-1.16.1}/model2data.egg-info/dependency_links.txt +0 -0
  144. {model2data-1.16.0 → model2data-1.16.1}/model2data.egg-info/entry_points.txt +0 -0
  145. {model2data-1.16.0 → model2data-1.16.1}/model2data.egg-info/requires.txt +0 -0
  146. {model2data-1.16.0 → model2data-1.16.1}/model2data.egg-info/top_level.txt +0 -0
  147. {model2data-1.16.0 → model2data-1.16.1}/setup.cfg +0 -0
  148. {model2data-1.16.0 → model2data-1.16.1}/tests/test_as_of_anchor.py +0 -0
  149. {model2data-1.16.0 → model2data-1.16.1}/tests/test_cli.py +0 -0
  150. {model2data-1.16.0 → model2data-1.16.1}/tests/test_cli_model.py +0 -0
  151. {model2data-1.16.0 → model2data-1.16.1}/tests/test_column_time_hints.py +0 -0
  152. {model2data-1.16.0 → model2data-1.16.1}/tests/test_coverage_gaps.py +0 -0
  153. {model2data-1.16.0 → model2data-1.16.1}/tests/test_days.py +0 -0
  154. {model2data-1.16.0 → model2data-1.16.1}/tests/test_dbml_parser.py +0 -0
  155. {model2data-1.16.0 → model2data-1.16.1}/tests/test_dbml_parser_fuzz.py +0 -0
  156. {model2data-1.16.0 → model2data-1.16.1}/tests/test_dbt_integration.py +0 -0
  157. {model2data-1.16.0 → model2data-1.16.1}/tests/test_dbt_naming.py +0 -0
  158. {model2data-1.16.0 → model2data-1.16.1}/tests/test_dbt_project.py +0 -0
  159. {model2data-1.16.0 → model2data-1.16.1}/tests/test_dbt_tests.py +0 -0
  160. {model2data-1.16.0 → model2data-1.16.1}/tests/test_defects.py +0 -0
  161. {model2data-1.16.0 → model2data-1.16.1}/tests/test_defects_dbt.py +0 -0
  162. {model2data-1.16.0 → model2data-1.16.1}/tests/test_defects_replay.py +0 -0
  163. {model2data-1.16.0 → model2data-1.16.1}/tests/test_defects_report.py +0 -0
  164. {model2data-1.16.0 → model2data-1.16.1}/tests/test_defects_review.py +0 -0
  165. {model2data-1.16.0 → model2data-1.16.1}/tests/test_determinism.py +0 -0
  166. {model2data-1.16.0 → model2data-1.16.1}/tests/test_distributions.py +0 -0
  167. {model2data-1.16.0 → model2data-1.16.1}/tests/test_examples_model.py +0 -0
  168. {model2data-1.16.0 → model2data-1.16.1}/tests/test_faker_name_inference.py +0 -0
  169. {model2data-1.16.0 → model2data-1.16.1}/tests/test_fast_paths.py +0 -0
  170. {model2data-1.16.0 → model2data-1.16.1}/tests/test_generation.py +0 -0
  171. {model2data-1.16.0 → model2data-1.16.1}/tests/test_guide.py +0 -0
  172. {model2data-1.16.0 → model2data-1.16.1}/tests/test_hint_tests.py +0 -0
  173. {model2data-1.16.0 → model2data-1.16.1}/tests/test_history.py +0 -0
  174. {model2data-1.16.0 → model2data-1.16.1}/tests/test_issue_lines.py +0 -0
  175. {model2data-1.16.0 → model2data-1.16.1}/tests/test_lone_country.py +0 -0
  176. {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_cli.py +0 -0
  177. {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_dbt.py +0 -0
  178. {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_expression.py +0 -0
  179. {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_lightdash.py +0 -0
  180. {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_ossie.py +0 -0
  181. {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_reader.py +0 -0
  182. {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_semantic.py +0 -0
  183. {model2data-1.16.0 → model2data-1.16.1}/tests/test_metrics_spec.py +0 -0
  184. {model2data-1.16.0 → model2data-1.16.1}/tests/test_model.py +0 -0
  185. {model2data-1.16.0 → model2data-1.16.1}/tests/test_model_dbml.py +0 -0
  186. {model2data-1.16.0 → model2data-1.16.1}/tests/test_model_edges.py +0 -0
  187. {model2data-1.16.0 → model2data-1.16.1}/tests/test_model_messages.py +0 -0
  188. {model2data-1.16.0 → model2data-1.16.1}/tests/test_model_vocabulary.py +0 -0
  189. {model2data-1.16.0 → model2data-1.16.1}/tests/test_options.py +0 -0
  190. {model2data-1.16.0 → model2data-1.16.1}/tests/test_parent_dates.py +0 -0
  191. {model2data-1.16.0 → model2data-1.16.1}/tests/test_release_stress.py +0 -0
  192. {model2data-1.16.0 → model2data-1.16.1}/tests/test_reproducibility.py +0 -0
  193. {model2data-1.16.0 → model2data-1.16.1}/tests/test_row_identity.py +0 -0
  194. {model2data-1.16.0 → model2data-1.16.1}/tests/test_shaping.py +0 -0
  195. {model2data-1.16.0 → model2data-1.16.1}/tests/test_spec_alignment.py +0 -0
  196. {model2data-1.16.0 → model2data-1.16.1}/tests/test_spec_schema.py +0 -0
  197. {model2data-1.16.0 → model2data-1.16.1}/tests/test_table_seeds.py +0 -0
  198. {model2data-1.16.0 → model2data-1.16.1}/tests/test_timeline.py +0 -0
  199. {model2data-1.16.0 → model2data-1.16.1}/tests/test_when.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model2data
3
- Version: 1.16.0
3
+ Version: 1.16.1
4
4
  Summary: Turn a data model into realistic synthetic data, a runnable dbt project and metrics with known values
5
5
  Author: JB Analytica
6
6
  License-Expression: MIT
@@ -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({previous}.{quote(child)} as varchar) = cast({alias}.{quote(parent)} as varchar)"
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 _cast(clauses.alias[table], path.rpartition(".")[2], kind), kind
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 = _cast("t0", metric.column, metric.column_kind)
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 query(semantic: SemanticModel, name: str, relation: Relation, *, by_month: bool = False) -> str:
201
- """A simple metric or row count as a query: one row with its `value`, or, `by_month`, a
202
- row per calendar month of its time column (`month` the month's first day)."""
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.alias[table]}.{quote(column)} as timestamp)) as date)"
211
- return f"select {month} as month, {aggregate} as value\n{joins}\ngroup by 1\norder by 1"
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 _as_text(frame: pd.DataFrame) -> pd.DataFrame:
148
- """`frame` as its seed file reads back: every column text, `""` and `null` null."""
149
- buffer = io.StringIO(frame.to_csv(index=False))
150
- text = pd.read_csv(buffer, dtype=str, keep_default_na=False, na_values=[], na_filter=False)
151
- return text.apply(lambda column: column.map(_null_or_text)).astype(object)
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
- def _null_or_text(value: Any) -> Any:
155
- return None if value == "" or str(value).lower() == "null" else value
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
- names: dict[str, str] = {}
172
- used = {
173
- table
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(f"cast({quote(c)} as varchar) as {quote(c)}" for c in text.columns)
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, metric in semantic.metrics.items():
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 metric.time is not None:
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model2data
3
- Version: 1.16.0
3
+ Version: 1.16.1
4
4
  Summary: Turn a data model into realistic synthetic data, a runnable dbt project and metrics with known values
5
5
  Author: JB Analytica
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "model2data"
7
- version = "1.16.0"
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"