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.
Files changed (199) hide show
  1. {model2data-1.16.0/model2data.egg-info → model2data-1.16.2}/PKG-INFO +1 -1
  2. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/sql.py +75 -22
  3. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/values.py +63 -26
  4. {model2data-1.16.0 → model2data-1.16.2/model2data.egg-info}/PKG-INFO +1 -1
  5. {model2data-1.16.0 → model2data-1.16.2}/pyproject.toml +1 -1
  6. model2data-1.16.2/tests/test_metrics_values.py +632 -0
  7. model2data-1.16.0/tests/test_metrics_values.py +0 -286
  8. {model2data-1.16.0 → model2data-1.16.2}/LICENSE +0 -0
  9. {model2data-1.16.0 → model2data-1.16.2}/README.md +0 -0
  10. {model2data-1.16.0 → model2data-1.16.2}/README_PYPI.md +0 -0
  11. {model2data-1.16.0 → model2data-1.16.2}/model2data/__init__.py +0 -0
  12. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/__init__.py +0 -0
  13. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/LICENSE +0 -0
  14. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/VENDORED.md +0 -0
  15. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/__init__.py +0 -0
  16. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/__init__.py +0 -0
  17. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/base.py +0 -0
  18. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/column.py +0 -0
  19. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/enum.py +0 -0
  20. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/expression.py +0 -0
  21. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/index.py +0 -0
  22. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/note.py +0 -0
  23. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/project.py +0 -0
  24. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/reference.py +0 -0
  25. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/sticky_note.py +0 -0
  26. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/table.py +0 -0
  27. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/_classes/table_group.py +0 -0
  28. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/classes/__init__.py +0 -0
  29. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/constants.py +0 -0
  30. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/database.py +0 -0
  31. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/__init__.py +0 -0
  32. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/column.py +0 -0
  33. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/common.py +0 -0
  34. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/enum.py +0 -0
  35. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/generic.py +0 -0
  36. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/index.py +0 -0
  37. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/project.py +0 -0
  38. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/records.py +0 -0
  39. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/reference.py +0 -0
  40. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/sticky_note.py +0 -0
  41. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/table.py +0 -0
  42. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/table_group.py +0 -0
  43. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/definitions/table_partial.py +0 -0
  44. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/exceptions.py +0 -0
  45. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/parser/__init__.py +0 -0
  46. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/parser/blueprints.py +0 -0
  47. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/parser/parser.py +0 -0
  48. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/__init__.py +0 -0
  49. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/base.py +0 -0
  50. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/__init__.py +0 -0
  51. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py +0 -0
  52. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/column.py +0 -0
  53. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py +0 -0
  54. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/expression.py +0 -0
  55. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/index.py +0 -0
  56. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/note.py +0 -0
  57. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/project.py +0 -0
  58. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py +0 -0
  59. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py +0 -0
  60. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/sticky_note.py +0 -0
  61. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/table.py +0 -0
  62. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/table_group.py +0 -0
  63. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py +0 -0
  64. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/__init__.py +0 -0
  65. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py +0 -0
  66. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/column.py +0 -0
  67. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/enum.py +0 -0
  68. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/expression.py +0 -0
  69. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/index.py +0 -0
  70. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/note.py +0 -0
  71. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/reference.py +0 -0
  72. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py +0 -0
  73. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/table.py +0 -0
  74. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/renderer/sql/default/utils.py +0 -0
  75. {model2data-1.16.0 → model2data-1.16.2}/model2data/_vendor/pydbml/tools.py +0 -0
  76. {model2data-1.16.0 → model2data-1.16.2}/model2data/cli.py +0 -0
  77. {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/__init__.py +0 -0
  78. {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/hint_tests.py +0 -0
  79. {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/naming.py +0 -0
  80. {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/project.py +0 -0
  81. {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/dbt_project.yml.jinja +0 -0
  82. {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/hint_macros/model2data_hint_tests.sql +0 -0
  83. {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/hint_macros/model2data_parent_tests.sql +0 -0
  84. {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/hint_macros/model2data_when_tests.sql +0 -0
  85. {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/history_macros/model2data_history_tests.sql +0 -0
  86. {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/macros/generate_schema_name.sql +0 -0
  87. {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/templates/profiles.yml.jinja +0 -0
  88. {model2data-1.16.0 → model2data-1.16.2}/model2data/dbt/tests.py +0 -0
  89. {model2data-1.16.0 → model2data-1.16.2}/model2data/defects/__init__.py +0 -0
  90. {model2data-1.16.0 → model2data-1.16.2}/model2data/defects/apply.py +0 -0
  91. {model2data-1.16.0 → model2data-1.16.2}/model2data/defects/checks.py +0 -0
  92. {model2data-1.16.0 → model2data-1.16.2}/model2data/defects/presets.py +0 -0
  93. {model2data-1.16.0 → model2data-1.16.2}/model2data/defects/report.py +0 -0
  94. {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/__init__.py +0 -0
  95. {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/core.py +0 -0
  96. {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/days.py +0 -0
  97. {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/faker.py +0 -0
  98. {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/hints.py +0 -0
  99. {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/history.py +0 -0
  100. {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/kinds.py +0 -0
  101. {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/options.py +0 -0
  102. {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/parents.py +0 -0
  103. {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/relationships.py +0 -0
  104. {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/timeline.py +0 -0
  105. {model2data-1.16.0 → model2data-1.16.2}/model2data/generate/when.py +0 -0
  106. {model2data-1.16.0 → model2data-1.16.2}/model2data/guide/setup.md +0 -0
  107. {model2data-1.16.0 → model2data-1.16.2}/model2data/guide/triage.md +0 -0
  108. {model2data-1.16.0 → model2data-1.16.2}/model2data/guide/tune.md +0 -0
  109. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/__init__.py +0 -0
  110. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/check.py +0 -0
  111. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/columns.py +0 -0
  112. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/dbt.py +0 -0
  113. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/expression.py +0 -0
  114. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/graph.py +0 -0
  115. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/infer.py +0 -0
  116. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/lightdash.py +0 -0
  117. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/ossie.py +0 -0
  118. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/reader.py +0 -0
  119. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/semantic.py +0 -0
  120. {model2data-1.16.0 → model2data-1.16.2}/model2data/metrics/types.py +0 -0
  121. {model2data-1.16.0 → model2data-1.16.2}/model2data/model/__init__.py +0 -0
  122. {model2data-1.16.0 → model2data-1.16.2}/model2data/model/_yaml.py +0 -0
  123. {model2data-1.16.0 → model2data-1.16.2}/model2data/model/dbml.py +0 -0
  124. {model2data-1.16.0 → model2data-1.16.2}/model2data/model/document.py +0 -0
  125. {model2data-1.16.0 → model2data-1.16.2}/model2data/model/dump.py +0 -0
  126. {model2data-1.16.0 → model2data-1.16.2}/model2data/model/engine.py +0 -0
  127. {model2data-1.16.0 → model2data-1.16.2}/model2data/model/errors.py +0 -0
  128. {model2data-1.16.0 → model2data-1.16.2}/model2data/model/reader.py +0 -0
  129. {model2data-1.16.0 → model2data-1.16.2}/model2data/model/types.py +0 -0
  130. {model2data-1.16.0 → model2data-1.16.2}/model2data/model/validate.py +0 -0
  131. {model2data-1.16.0 → model2data-1.16.2}/model2data/output.py +0 -0
  132. {model2data-1.16.0 → model2data-1.16.2}/model2data/parse/__init__.py +0 -0
  133. {model2data-1.16.0 → model2data-1.16.2}/model2data/parse/dbml.py +0 -0
  134. {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/PUBLISHING.md +0 -0
  135. {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/README.md +0 -0
  136. {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/examples/coffee_webshop.metrics.yml +0 -0
  137. {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/examples/coffee_webshop.model2data.yml +0 -0
  138. {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/metrics/README.md +0 -0
  139. {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/metrics/metrics.schema.json +0 -0
  140. {model2data-1.16.0 → model2data-1.16.2}/model2data/spec/model.schema.json +0 -0
  141. {model2data-1.16.0 → model2data-1.16.2}/model2data/utils.py +0 -0
  142. {model2data-1.16.0 → model2data-1.16.2}/model2data.egg-info/SOURCES.txt +0 -0
  143. {model2data-1.16.0 → model2data-1.16.2}/model2data.egg-info/dependency_links.txt +0 -0
  144. {model2data-1.16.0 → model2data-1.16.2}/model2data.egg-info/entry_points.txt +0 -0
  145. {model2data-1.16.0 → model2data-1.16.2}/model2data.egg-info/requires.txt +0 -0
  146. {model2data-1.16.0 → model2data-1.16.2}/model2data.egg-info/top_level.txt +0 -0
  147. {model2data-1.16.0 → model2data-1.16.2}/setup.cfg +0 -0
  148. {model2data-1.16.0 → model2data-1.16.2}/tests/test_as_of_anchor.py +0 -0
  149. {model2data-1.16.0 → model2data-1.16.2}/tests/test_cli.py +0 -0
  150. {model2data-1.16.0 → model2data-1.16.2}/tests/test_cli_model.py +0 -0
  151. {model2data-1.16.0 → model2data-1.16.2}/tests/test_column_time_hints.py +0 -0
  152. {model2data-1.16.0 → model2data-1.16.2}/tests/test_coverage_gaps.py +0 -0
  153. {model2data-1.16.0 → model2data-1.16.2}/tests/test_days.py +0 -0
  154. {model2data-1.16.0 → model2data-1.16.2}/tests/test_dbml_parser.py +0 -0
  155. {model2data-1.16.0 → model2data-1.16.2}/tests/test_dbml_parser_fuzz.py +0 -0
  156. {model2data-1.16.0 → model2data-1.16.2}/tests/test_dbt_integration.py +0 -0
  157. {model2data-1.16.0 → model2data-1.16.2}/tests/test_dbt_naming.py +0 -0
  158. {model2data-1.16.0 → model2data-1.16.2}/tests/test_dbt_project.py +0 -0
  159. {model2data-1.16.0 → model2data-1.16.2}/tests/test_dbt_tests.py +0 -0
  160. {model2data-1.16.0 → model2data-1.16.2}/tests/test_defects.py +0 -0
  161. {model2data-1.16.0 → model2data-1.16.2}/tests/test_defects_dbt.py +0 -0
  162. {model2data-1.16.0 → model2data-1.16.2}/tests/test_defects_replay.py +0 -0
  163. {model2data-1.16.0 → model2data-1.16.2}/tests/test_defects_report.py +0 -0
  164. {model2data-1.16.0 → model2data-1.16.2}/tests/test_defects_review.py +0 -0
  165. {model2data-1.16.0 → model2data-1.16.2}/tests/test_determinism.py +0 -0
  166. {model2data-1.16.0 → model2data-1.16.2}/tests/test_distributions.py +0 -0
  167. {model2data-1.16.0 → model2data-1.16.2}/tests/test_examples_model.py +0 -0
  168. {model2data-1.16.0 → model2data-1.16.2}/tests/test_faker_name_inference.py +0 -0
  169. {model2data-1.16.0 → model2data-1.16.2}/tests/test_fast_paths.py +0 -0
  170. {model2data-1.16.0 → model2data-1.16.2}/tests/test_generation.py +0 -0
  171. {model2data-1.16.0 → model2data-1.16.2}/tests/test_guide.py +0 -0
  172. {model2data-1.16.0 → model2data-1.16.2}/tests/test_hint_tests.py +0 -0
  173. {model2data-1.16.0 → model2data-1.16.2}/tests/test_history.py +0 -0
  174. {model2data-1.16.0 → model2data-1.16.2}/tests/test_issue_lines.py +0 -0
  175. {model2data-1.16.0 → model2data-1.16.2}/tests/test_lone_country.py +0 -0
  176. {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_cli.py +0 -0
  177. {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_dbt.py +0 -0
  178. {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_expression.py +0 -0
  179. {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_lightdash.py +0 -0
  180. {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_ossie.py +0 -0
  181. {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_reader.py +0 -0
  182. {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_semantic.py +0 -0
  183. {model2data-1.16.0 → model2data-1.16.2}/tests/test_metrics_spec.py +0 -0
  184. {model2data-1.16.0 → model2data-1.16.2}/tests/test_model.py +0 -0
  185. {model2data-1.16.0 → model2data-1.16.2}/tests/test_model_dbml.py +0 -0
  186. {model2data-1.16.0 → model2data-1.16.2}/tests/test_model_edges.py +0 -0
  187. {model2data-1.16.0 → model2data-1.16.2}/tests/test_model_messages.py +0 -0
  188. {model2data-1.16.0 → model2data-1.16.2}/tests/test_model_vocabulary.py +0 -0
  189. {model2data-1.16.0 → model2data-1.16.2}/tests/test_options.py +0 -0
  190. {model2data-1.16.0 → model2data-1.16.2}/tests/test_parent_dates.py +0 -0
  191. {model2data-1.16.0 → model2data-1.16.2}/tests/test_release_stress.py +0 -0
  192. {model2data-1.16.0 → model2data-1.16.2}/tests/test_reproducibility.py +0 -0
  193. {model2data-1.16.0 → model2data-1.16.2}/tests/test_row_identity.py +0 -0
  194. {model2data-1.16.0 → model2data-1.16.2}/tests/test_shaping.py +0 -0
  195. {model2data-1.16.0 → model2data-1.16.2}/tests/test_spec_alignment.py +0 -0
  196. {model2data-1.16.0 → model2data-1.16.2}/tests/test_spec_schema.py +0 -0
  197. {model2data-1.16.0 → model2data-1.16.2}/tests/test_table_seeds.py +0 -0
  198. {model2data-1.16.0 → model2data-1.16.2}/tests/test_timeline.py +0 -0
  199. {model2data-1.16.0 → model2data-1.16.2}/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.2
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
@@ -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 time are on along their many-to-one path with `left join` (a row
9
- whose parent is missing stays, and fails any filter on the parent, as a null
10
- does), and casts every column it reads to the type its kind is read as
11
- (`model2data.metrics.columns.SQL_TYPES`), so the answer does not depend on
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
- def _cast(alias: str, column: str, kind: ColumnKind) -> str:
153
- return f"cast({alias}.{quote(column)} as {SQL_TYPES[kind]})"
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
- class _From:
157
- """The `from` and `join` clauses of a simple metric, and the alias of each table in them."""
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({previous}.{quote(child)} as varchar) = cast({alias}.{quote(parent)} as varchar)"
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
- clauses = _From(metric, relation)
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 _cast(clauses.alias[table], path.rpartition(".")[2], kind), kind
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 = _cast("t0", metric.column, metric.column_kind)
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 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)."""
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.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"
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 _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.2
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.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"