model2data 1.10.1__tar.gz → 1.10.4__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 (163) hide show
  1. {model2data-1.10.1/model2data.egg-info → model2data-1.10.4}/PKG-INFO +1 -1
  2. {model2data-1.10.1 → model2data-1.10.4}/README.md +21 -6
  3. {model2data-1.10.1 → model2data-1.10.4}/model2data/defects/apply.py +148 -22
  4. model2data-1.10.4/model2data/defects/checks.py +395 -0
  5. {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/core.py +7 -2
  6. {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/days.py +16 -3
  7. {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/faker.py +312 -9
  8. {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/timeline.py +41 -3
  9. {model2data-1.10.1 → model2data-1.10.4/model2data.egg-info}/PKG-INFO +1 -1
  10. {model2data-1.10.1 → model2data-1.10.4}/model2data.egg-info/SOURCES.txt +2 -0
  11. {model2data-1.10.1 → model2data-1.10.4}/pyproject.toml +1 -1
  12. {model2data-1.10.1 → model2data-1.10.4}/tests/test_cli_model.py +31 -0
  13. {model2data-1.10.1 → model2data-1.10.4}/tests/test_days.py +26 -0
  14. model2data-1.10.4/tests/test_determinism.py +133 -0
  15. model2data-1.10.4/tests/test_fast_paths.py +512 -0
  16. {model2data-1.10.1 → model2data-1.10.4}/tests/test_generation.py +64 -0
  17. model2data-1.10.1/model2data/defects/checks.py +0 -179
  18. {model2data-1.10.1 → model2data-1.10.4}/LICENSE +0 -0
  19. {model2data-1.10.1 → model2data-1.10.4}/README_PYPI.md +0 -0
  20. {model2data-1.10.1 → model2data-1.10.4}/model2data/__init__.py +0 -0
  21. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/__init__.py +0 -0
  22. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/LICENSE +0 -0
  23. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/VENDORED.md +0 -0
  24. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/__init__.py +0 -0
  25. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/__init__.py +0 -0
  26. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/base.py +0 -0
  27. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/column.py +0 -0
  28. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/enum.py +0 -0
  29. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/expression.py +0 -0
  30. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/index.py +0 -0
  31. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/note.py +0 -0
  32. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/project.py +0 -0
  33. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/reference.py +0 -0
  34. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/sticky_note.py +0 -0
  35. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/table.py +0 -0
  36. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/table_group.py +0 -0
  37. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/classes/__init__.py +0 -0
  38. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/constants.py +0 -0
  39. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/database.py +0 -0
  40. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/__init__.py +0 -0
  41. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/column.py +0 -0
  42. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/common.py +0 -0
  43. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/enum.py +0 -0
  44. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/generic.py +0 -0
  45. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/index.py +0 -0
  46. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/project.py +0 -0
  47. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/records.py +0 -0
  48. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/reference.py +0 -0
  49. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/sticky_note.py +0 -0
  50. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/table.py +0 -0
  51. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/table_group.py +0 -0
  52. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/table_partial.py +0 -0
  53. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/exceptions.py +0 -0
  54. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/parser/__init__.py +0 -0
  55. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/parser/blueprints.py +0 -0
  56. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/parser/parser.py +0 -0
  57. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/__init__.py +0 -0
  58. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/base.py +0 -0
  59. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/__init__.py +0 -0
  60. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py +0 -0
  61. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/column.py +0 -0
  62. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py +0 -0
  63. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/expression.py +0 -0
  64. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/index.py +0 -0
  65. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/note.py +0 -0
  66. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/project.py +0 -0
  67. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py +0 -0
  68. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py +0 -0
  69. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/sticky_note.py +0 -0
  70. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/table.py +0 -0
  71. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/table_group.py +0 -0
  72. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py +0 -0
  73. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/__init__.py +0 -0
  74. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py +0 -0
  75. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/column.py +0 -0
  76. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/enum.py +0 -0
  77. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/expression.py +0 -0
  78. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/index.py +0 -0
  79. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/note.py +0 -0
  80. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/reference.py +0 -0
  81. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py +0 -0
  82. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/table.py +0 -0
  83. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/utils.py +0 -0
  84. {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/tools.py +0 -0
  85. {model2data-1.10.1 → model2data-1.10.4}/model2data/cli.py +0 -0
  86. {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/__init__.py +0 -0
  87. {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/hint_tests.py +0 -0
  88. {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/naming.py +0 -0
  89. {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/project.py +0 -0
  90. {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/templates/dbt_project.yml.jinja +0 -0
  91. {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/templates/hint_macros/model2data_hint_tests.sql +0 -0
  92. {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/templates/history_macros/model2data_history_tests.sql +0 -0
  93. {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/templates/macros/generate_schema_name.sql +0 -0
  94. {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/templates/profiles.yml.jinja +0 -0
  95. {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/tests.py +0 -0
  96. {model2data-1.10.1 → model2data-1.10.4}/model2data/defects/__init__.py +0 -0
  97. {model2data-1.10.1 → model2data-1.10.4}/model2data/defects/presets.py +0 -0
  98. {model2data-1.10.1 → model2data-1.10.4}/model2data/defects/report.py +0 -0
  99. {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/__init__.py +0 -0
  100. {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/hints.py +0 -0
  101. {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/history.py +0 -0
  102. {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/kinds.py +0 -0
  103. {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/options.py +0 -0
  104. {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/relationships.py +0 -0
  105. {model2data-1.10.1 → model2data-1.10.4}/model2data/model/__init__.py +0 -0
  106. {model2data-1.10.1 → model2data-1.10.4}/model2data/model/_yaml.py +0 -0
  107. {model2data-1.10.1 → model2data-1.10.4}/model2data/model/dbml.py +0 -0
  108. {model2data-1.10.1 → model2data-1.10.4}/model2data/model/document.py +0 -0
  109. {model2data-1.10.1 → model2data-1.10.4}/model2data/model/dump.py +0 -0
  110. {model2data-1.10.1 → model2data-1.10.4}/model2data/model/engine.py +0 -0
  111. {model2data-1.10.1 → model2data-1.10.4}/model2data/model/errors.py +0 -0
  112. {model2data-1.10.1 → model2data-1.10.4}/model2data/model/reader.py +0 -0
  113. {model2data-1.10.1 → model2data-1.10.4}/model2data/model/types.py +0 -0
  114. {model2data-1.10.1 → model2data-1.10.4}/model2data/model/validate.py +0 -0
  115. {model2data-1.10.1 → model2data-1.10.4}/model2data/output.py +0 -0
  116. {model2data-1.10.1 → model2data-1.10.4}/model2data/parse/__init__.py +0 -0
  117. {model2data-1.10.1 → model2data-1.10.4}/model2data/parse/dbml.py +0 -0
  118. {model2data-1.10.1 → model2data-1.10.4}/model2data/spec/PUBLISHING.md +0 -0
  119. {model2data-1.10.1 → model2data-1.10.4}/model2data/spec/README.md +0 -0
  120. {model2data-1.10.1 → model2data-1.10.4}/model2data/spec/examples/coffee_webshop.model2data.yml +0 -0
  121. {model2data-1.10.1 → model2data-1.10.4}/model2data/spec/model.schema.json +0 -0
  122. {model2data-1.10.1 → model2data-1.10.4}/model2data/utils.py +0 -0
  123. {model2data-1.10.1 → model2data-1.10.4}/model2data.egg-info/dependency_links.txt +0 -0
  124. {model2data-1.10.1 → model2data-1.10.4}/model2data.egg-info/entry_points.txt +0 -0
  125. {model2data-1.10.1 → model2data-1.10.4}/model2data.egg-info/requires.txt +0 -0
  126. {model2data-1.10.1 → model2data-1.10.4}/model2data.egg-info/top_level.txt +0 -0
  127. {model2data-1.10.1 → model2data-1.10.4}/setup.cfg +0 -0
  128. {model2data-1.10.1 → model2data-1.10.4}/tests/test_as_of_anchor.py +0 -0
  129. {model2data-1.10.1 → model2data-1.10.4}/tests/test_cli.py +0 -0
  130. {model2data-1.10.1 → model2data-1.10.4}/tests/test_column_time_hints.py +0 -0
  131. {model2data-1.10.1 → model2data-1.10.4}/tests/test_coverage_gaps.py +0 -0
  132. {model2data-1.10.1 → model2data-1.10.4}/tests/test_dbml_parser.py +0 -0
  133. {model2data-1.10.1 → model2data-1.10.4}/tests/test_dbml_parser_fuzz.py +0 -0
  134. {model2data-1.10.1 → model2data-1.10.4}/tests/test_dbt_integration.py +0 -0
  135. {model2data-1.10.1 → model2data-1.10.4}/tests/test_dbt_naming.py +0 -0
  136. {model2data-1.10.1 → model2data-1.10.4}/tests/test_dbt_project.py +0 -0
  137. {model2data-1.10.1 → model2data-1.10.4}/tests/test_dbt_tests.py +0 -0
  138. {model2data-1.10.1 → model2data-1.10.4}/tests/test_defects.py +0 -0
  139. {model2data-1.10.1 → model2data-1.10.4}/tests/test_defects_dbt.py +0 -0
  140. {model2data-1.10.1 → model2data-1.10.4}/tests/test_defects_replay.py +0 -0
  141. {model2data-1.10.1 → model2data-1.10.4}/tests/test_defects_report.py +0 -0
  142. {model2data-1.10.1 → model2data-1.10.4}/tests/test_defects_review.py +0 -0
  143. {model2data-1.10.1 → model2data-1.10.4}/tests/test_distributions.py +0 -0
  144. {model2data-1.10.1 → model2data-1.10.4}/tests/test_examples_model.py +0 -0
  145. {model2data-1.10.1 → model2data-1.10.4}/tests/test_faker_name_inference.py +0 -0
  146. {model2data-1.10.1 → model2data-1.10.4}/tests/test_hint_tests.py +0 -0
  147. {model2data-1.10.1 → model2data-1.10.4}/tests/test_history.py +0 -0
  148. {model2data-1.10.1 → model2data-1.10.4}/tests/test_issue_lines.py +0 -0
  149. {model2data-1.10.1 → model2data-1.10.4}/tests/test_lone_country.py +0 -0
  150. {model2data-1.10.1 → model2data-1.10.4}/tests/test_model.py +0 -0
  151. {model2data-1.10.1 → model2data-1.10.4}/tests/test_model_dbml.py +0 -0
  152. {model2data-1.10.1 → model2data-1.10.4}/tests/test_model_edges.py +0 -0
  153. {model2data-1.10.1 → model2data-1.10.4}/tests/test_model_messages.py +0 -0
  154. {model2data-1.10.1 → model2data-1.10.4}/tests/test_model_vocabulary.py +0 -0
  155. {model2data-1.10.1 → model2data-1.10.4}/tests/test_options.py +0 -0
  156. {model2data-1.10.1 → model2data-1.10.4}/tests/test_release_stress.py +0 -0
  157. {model2data-1.10.1 → model2data-1.10.4}/tests/test_reproducibility.py +0 -0
  158. {model2data-1.10.1 → model2data-1.10.4}/tests/test_row_identity.py +0 -0
  159. {model2data-1.10.1 → model2data-1.10.4}/tests/test_shaping.py +0 -0
  160. {model2data-1.10.1 → model2data-1.10.4}/tests/test_spec_alignment.py +0 -0
  161. {model2data-1.10.1 → model2data-1.10.4}/tests/test_spec_schema.py +0 -0
  162. {model2data-1.10.1 → model2data-1.10.4}/tests/test_table_seeds.py +0 -0
  163. {model2data-1.10.1 → model2data-1.10.4}/tests/test_timeline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model2data
3
- Version: 1.10.1
3
+ Version: 1.10.4
4
4
  Summary: Generate analytics-ready datasets from DBML models
5
5
  Author: JB Analytica
6
6
  License-Expression: MIT
@@ -49,9 +49,9 @@ access required.
49
49
  `sku ean13`) to pick its generator outright when the name is wrong for the data.
50
50
  - **Relationship-preserving.** Foreign keys resolve to real parent rows; tables are generated in
51
51
  dependency order.
52
- - **Deterministic.** Pass `--seed` and the same schema always produces the same data — safe to
53
- commit fixtures, safe to diff across CI runs. Add `--as-of` to pin the date the data is anchored
54
- on, and the run reproduces on any later day rather than only on the day it first ran.
52
+ - **Deterministic, byte for byte.** The same model, `--seed` and `--as-of` (and the same options)
53
+ give the same files, byte for byte, on any machine and in every 1.x release — safe to commit
54
+ fixtures, safe to diff across CI runs and upgrades. See [The determinism promise](#the-determinism-promise).
55
55
  - **Re-rollable one table at a time.** `--table-seed orders=7` regenerates a single table and
56
56
  leaves every other table byte-identical, so you can keep the four tables that look right.
57
57
  - **A real dbt project, not just CSVs.** Seeds, staging models that `ref()` them, schema tests,
@@ -222,14 +222,14 @@ jobs:
222
222
  ```
223
223
 
224
224
  Inputs: `files` (glob, default `**/*.model2data.yml`), `version` (default: the release the tag
225
- points at, e.g. `1.10.1`; a specifier such as `>=1.10,<2` also works), `python-version` (3.12).
225
+ points at, e.g. `1.10.4`; a specifier such as `>=1.10,<2` also works), `python-version` (3.12).
226
226
 
227
227
  **pre-commit**
228
228
 
229
229
  ```yaml
230
230
  repos:
231
231
  - repo: https://github.com/JB-Analytica/model2data
232
- rev: v1.10.1
232
+ rev: v1.10.4
233
233
  hooks:
234
234
  - id: model2data-validate
235
235
  ```
@@ -243,7 +243,7 @@ model2data:
243
243
  - if: $CI_PIPELINE_SOURCE == "merge_request_event"
244
244
  changes: ["**/*.model2data.yml"]
245
245
  script:
246
- - pip install model2data==1.10.1
246
+ - pip install model2data==1.10.4
247
247
  - model2data validate --glob "**/*.model2data.yml"
248
248
  ```
249
249
 
@@ -309,6 +309,21 @@ fixture safe to commit:
309
309
  model2data --file examples/ecommerce.model2data.yml --rows 200 --seed 42 --as-of 2026-01-31
310
310
  ```
311
311
 
312
+ ### The determinism promise
313
+
314
+ The same model, seed and `--as-of` — with the same options (`--rows`, `--rows-for`,
315
+ `--table-seed`, `--locale`, `--defects`, `--days`, …) — produce the same files, byte for byte:
316
+
317
+ - on any machine and operating system, and on every supported Python version;
318
+ - with any Faker or pandas version model2data accepts;
319
+ - in every 1.x release. A release may make generation faster or add options, but never changes
320
+ what an existing model, seed and set of options produce. A fix that cannot keep that waits for
321
+ a new major version, and the changelog says so.
322
+
323
+ `tests/test_determinism.py` holds the file hashes of a spread of models, presets, multi-day runs
324
+ and locales, and runs on every pull request — against the newest Faker and pandas too — so a
325
+ change to a single output byte fails the build rather than reaching a release.
326
+
312
327
  If one table comes out wrong and the rest looks right, `--table-seed` re-rolls just that table.
313
328
  Every other table's seed CSV stays byte-identical, and children of the re-rolled table still
314
329
  reference rows that exist, so there's nothing to re-check but the table you asked to change:
@@ -64,7 +64,7 @@ from model2data import __version__
64
64
  from model2data.dbt.hint_tests import DEFAULT_TOLERANCE
65
65
  from model2data.dbt.naming import dbt_names, for_dbt
66
66
  from model2data.dbt.tests import DbtTest, dbt_tests
67
- from model2data.defects.checks import as_seeded, failing
67
+ from model2data.defects.checks import Columns, as_seeded, failing, fails
68
68
  from model2data.defects.report import AppliedDefect, DefectsReport, ExpectedFailure
69
69
  from model2data.generate import kinds
70
70
  from model2data.generate.days import DayResult, TableDay
@@ -123,6 +123,8 @@ class _Table:
123
123
  # so no defect undoes another and every count holds.
124
124
  self.claimed: dict[str, set[int]] = {}
125
125
  self.patches: list[_Patch] = []
126
+ # What `referenced` found, by the columns asked about: the clean frames never change.
127
+ self._referenced: dict[tuple[str, ...], frozenset[int]] = {}
126
128
  rows = len(self.frame)
127
129
  if results is not None and key not in results[0].tables:
128
130
  results = None # a table the days do not hold: a history table
@@ -162,15 +164,20 @@ class _Table:
162
164
 
163
165
  def referenced(self, columns: Sequence[str]) -> set[int]:
164
166
  """The rows whose value in one of `columns` some child row holds."""
165
- found: set[int] = set()
166
- for ref in self.refs:
167
- if ref["target_table"] != self.key or ref["target_column"] not in columns:
168
- continue
169
- child = self.frames[ref["source_table"]][ref["source_column"]].tolist()
170
- held = {_comparable(v) for v in child if _present(v)}
171
- values = self.frame[ref["target_column"]].tolist()
172
- found.update(p for p, v in enumerate(values) if _present(v) and _comparable(v) in held)
173
- return found
167
+ asked = tuple(columns)
168
+ if asked not in self._referenced:
169
+ found: set[int] = set()
170
+ for ref in self.refs:
171
+ if ref["target_table"] != self.key or ref["target_column"] not in asked:
172
+ continue
173
+ child = self.frames[ref["source_table"]][ref["source_column"]].tolist()
174
+ held = {_comparable(v) for v in child if _present(v)}
175
+ values = self.frame[ref["target_column"]].tolist()
176
+ found.update(
177
+ p for p, v in enumerate(values) if _present(v) and _comparable(v) in held
178
+ )
179
+ self._referenced[asked] = frozenset(found)
180
+ return set(self._referenced[asked])
174
181
 
175
182
  def free(self, candidates: list[int], columns: Sequence[str]) -> list[int]:
176
183
  """`candidates` without the rows an earlier defect broke one of `columns` in."""
@@ -218,6 +225,9 @@ class _Table:
218
225
 
219
226
 
220
227
  def _present(value: Any) -> bool:
228
+ kind = type(value)
229
+ if kind is str or kind is int:
230
+ return True
221
231
  return (
222
232
  value is not None
223
233
  and not (isinstance(value, float) and math.isnan(value))
@@ -227,6 +237,9 @@ def _present(value: Any) -> bool:
227
237
 
228
238
  def _comparable(value: Any) -> Any:
229
239
  """A value as it compares to the warehouse: 7, 7.0 and numpy's 7 are one value."""
240
+ kind = type(value)
241
+ if kind is str or kind is int:
242
+ return value
230
243
  item = getattr(value, "item", None)
231
244
  if callable(item):
232
245
  value = item()
@@ -290,7 +303,9 @@ def _duplicate_keys(table: _Table, defect: Defect, rng: random.Random, count: in
290
303
  if not columns:
291
304
  return _Outcome(None, note=f"{table.key} has no primary key to repeat")
292
305
  values = {name: table.frame[name].tolist() for name in columns}
293
- complete = [p for p in range(len(table.frame)) if all(_present(values[c][p]) for c in columns)]
306
+ complete = [
307
+ p for p, row in enumerate(zip(*values.values(), strict=True)) if all(map(_present, row))
308
+ ]
294
309
  avoid = table.referenced(columns)
295
310
  wanted = min(count, max(len(complete) - 1, 0))
296
311
  targets = _choose(rng, table.free(complete, columns), wanted, avoid)
@@ -668,15 +683,54 @@ def requested_rows(defect: Defect, rows: int) -> int:
668
683
  def _patched(
669
684
  frame: pd.DataFrame, patches: Sequence[_Patch], day: Optional[int] = None
670
685
  ) -> pd.DataFrame:
671
- """A copy of `frame` with `patches` set, those from day `day` on when it is given."""
686
+ """A copy of `frame` with `patches` set, those from day `day` on when it is given.
687
+
688
+ A column of objects is set as a list, in one go; any other cell by cell, as
689
+ `_set` does, for what pandas makes of a value its column's type cannot hold.
690
+ """
672
691
  out = frame.copy()
692
+ by_column: dict[str, list[_Patch]] = {}
673
693
  for patch in patches:
674
694
  if patch.position >= len(out) or (day is not None and patch.since > day):
675
695
  continue
676
- _set(out, patch.position, patch.column, patch.value)
696
+ by_column.setdefault(patch.column, []).append(patch)
697
+ for column, cells in by_column.items():
698
+ if out[column].dtype == object:
699
+ values = out[column].tolist()
700
+ for patch in cells:
701
+ values[patch.position] = patch.value
702
+ out[column] = pd.Series(values, index=out.index, dtype=object)
703
+ continue
704
+ if isinstance(out[column].dtype, _NULLABLE_INTEGERS) and all(
705
+ type(patch.value) is int for patch in cells
706
+ ):
707
+ # What `_set` does to each cell, done to all of them at once.
708
+ final = {patch.position: patch.value for patch in cells}
709
+ array = out[column].array.copy()
710
+ try:
711
+ array[list(final)] = list(final.values())
712
+ except (TypeError, ValueError):
713
+ pass # too large for the column: cell by cell, as before
714
+ else:
715
+ out[column] = pd.Series(array, index=out.index)
716
+ continue
717
+ for patch in cells:
718
+ _set(out, patch.position, column, patch.value)
677
719
  return out
678
720
 
679
721
 
722
+ _NULLABLE_INTEGERS = (
723
+ pd.Int8Dtype,
724
+ pd.Int16Dtype,
725
+ pd.Int32Dtype,
726
+ pd.Int64Dtype,
727
+ pd.UInt8Dtype,
728
+ pd.UInt16Dtype,
729
+ pd.UInt32Dtype,
730
+ pd.UInt64Dtype,
731
+ )
732
+
733
+
680
734
  def _set(frame: pd.DataFrame, position: int, column: str, value: Any) -> None:
681
735
  index = frame.columns.get_loc(column)
682
736
  try:
@@ -823,18 +877,41 @@ def _report(
823
877
  dbt_tables, dbt_refs = for_dbt(inputs.tables, inputs.refs, names)
824
878
  tests = dbt_tests(dbt_tables, dbt_refs, hint_tests=hint_tests, test_tolerance=test_tolerance)
825
879
  keys = {name: key for key, name in names.items()}
880
+ cache = Columns()
826
881
  seeded_clean = {names[key]: as_seeded(frame) for key, frame in clean.items()}
827
- seeded_broken = {names[key]: as_seeded(frame) for key, frame in broken.items()}
828
- failing_clean = failing(tests, seeded_clean)
829
- failing_broken = failing(tests, seeded_broken)
882
+ seeded_broken = dict(seeded_clean)
883
+ done: dict[str, tuple[pd.DataFrame, Sequence[_Patch]]] = {}
884
+ for key, frame in broken.items():
885
+ if frame is not clean.get(key):
886
+ patches = [p for k, _, _, o in applied if k == key for p in o.patches]
887
+ seeded = _seeded_patched(clean[key], seeded_clean[names[key]], patches, cache)
888
+ seeded_broken[names[key]] = seeded
889
+ done[key] = (seeded, patches)
890
+ fails_clean = [fails(test, seeded_clean, cache=cache) for test in tests]
891
+ failing_clean = {test.name for test, failed in zip(tests, fails_clean, strict=True) if failed}
892
+ failing_broken = failing(tests, seeded_broken, cache=cache)
830
893
  alone: list[set[str]] = []
831
894
  for key, _, _, outcome in applied:
832
895
  if not outcome.patches:
833
896
  alone.append(set())
834
897
  continue
898
+ # A test that does not read this table fails as it does on the clean data.
899
+ name = names[key]
835
900
  seeded = dict(seeded_clean)
836
- seeded[names[key]] = as_seeded(_patched(clean[key], outcome.patches))
837
- alone.append(failing(tests, seeded))
901
+ seeded[name] = _seeded_patched(
902
+ clean[key], seeded_clean[name], outcome.patches, cache, done.get(key)
903
+ )
904
+ alone.append(
905
+ {
906
+ test.name
907
+ for test, failed in zip(tests, fails_clean, strict=True)
908
+ if (
909
+ fails(test, seeded, cache=cache)
910
+ if test.table == name or (test.parent is not None and test.parent[0] == name)
911
+ else failed
912
+ )
913
+ }
914
+ )
838
915
 
839
916
  expected: list[ExpectedFailure] = []
840
917
  by_defect: list[list[str]] = [[] for _ in applied]
@@ -871,6 +948,52 @@ def _report(
871
948
  return report
872
949
 
873
950
 
951
+ def _seeded_patched(
952
+ frame: pd.DataFrame,
953
+ base: pd.DataFrame,
954
+ patches: Sequence[_Patch],
955
+ cache: Columns,
956
+ done: Optional[tuple[pd.DataFrame, Sequence[_Patch]]] = None,
957
+ ) -> pd.DataFrame:
958
+ """`as_seeded(_patched(frame, patches))`, given `base`, `as_seeded(frame)`.
959
+
960
+ Only the columns `patches` set are written out and read back, as a column
961
+ reads back the same whichever columns stand beside it; the others, and what
962
+ `cache` knows of them, are `base`'s. `done` is another seeded frame of
963
+ `frame` and the patches it was made with: a column those patches set exactly
964
+ as `patches` do is taken from it rather than written out again.
965
+ """
966
+ mine = _by_column(patches)
967
+ theirs = _by_column(done[1]) if done is not None else {}
968
+ reuse = [
969
+ column
970
+ for column, own in mine.items()
971
+ if len(own) == len(theirs.get(column, ()))
972
+ and all(a is b for a, b in zip(own, theirs[column], strict=True))
973
+ ]
974
+ fresh = [column for column in mine if column not in reuse]
975
+ seeded = base.copy()
976
+ if fresh:
977
+ part = as_seeded(
978
+ _patched(frame[fresh], [patch for patch in patches if patch.column in fresh])
979
+ )
980
+ for column in fresh:
981
+ seeded[column] = part[column]
982
+ cache.share(seeded, base, [column for column in base.columns if column not in mine])
983
+ if done is not None and reuse:
984
+ for column in reuse:
985
+ seeded[column] = done[0][column]
986
+ cache.share(seeded, done[0], reuse)
987
+ return seeded
988
+
989
+
990
+ def _by_column(patches: Sequence[_Patch]) -> dict[str, list[_Patch]]:
991
+ by_column: dict[str, list[_Patch]] = {}
992
+ for patch in patches:
993
+ by_column.setdefault(patch.column, []).append(patch)
994
+ return by_column
995
+
996
+
874
997
  def _culprits(
875
998
  test: DbtTest,
876
999
  alone: list[set[str]],
@@ -932,15 +1055,17 @@ def _recount(defect: Defect, outcome: _Outcome, frame: Optional[pd.DataFrame]) -
932
1055
  by_row: dict[int, list[_Patch]] = {}
933
1056
  for patch in outcome.patches:
934
1057
  by_row.setdefault(patch.position, []).append(patch)
1058
+ columns = sorted({patch.column for patch in outcome.patches})
1059
+ cells = {column: frame[column].tolist() for column in columns}
935
1060
  held = [
936
1061
  p
937
1062
  for p in outcome.positions
938
- if all(_same(frame[c.column].iloc[p], c.value) for c in by_row.get(p, []))
1063
+ if all(_same(cells[c.column][p], c.value) for c in by_row.get(p, []))
939
1064
  ]
940
1065
  if defect.type == "duplicate_keys":
941
- columns = sorted({patch.column for patch in outcome.patches})
942
1066
  keys = [
943
- tuple(_comparable(v) for v in row) for row in frame[columns].itertuples(index=False)
1067
+ tuple(map(_comparable, row))
1068
+ for row in zip(*(cells[column] for column in columns), strict=True)
944
1069
  ]
945
1070
  counts: dict[tuple, int] = {}
946
1071
  for key in keys:
@@ -998,7 +1123,8 @@ def _applied(
998
1123
  # A defect with no table to break (a history not kept) broke no row.
999
1124
  primary = primary_key(inputs.tables[key]) if key in inputs.tables else []
1000
1125
  numbers = [position + 1 for position in outcome.positions]
1001
- keys = [[_native(frame[name].iloc[p]) for name in primary] for p in outcome.positions]
1126
+ held = {name: frame[name].tolist() for name in primary} if outcome.positions else {}
1127
+ keys = [[_native(held[name][p]) for name in primary] for p in outcome.positions]
1002
1128
  rows: list[Any] = [values[0] if len(values) == 1 else values for values in keys]
1003
1129
  row_key: Optional[list[str]] = list(primary) or None
1004
1130
  if not primary: