django-data-shape 0.20.0__tar.gz → 0.22.0__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 (217) hide show
  1. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/.github/workflows/release.yml +1 -1
  2. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/.github/workflows/tests.yml +5 -5
  3. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/.github/workflows/upstream-drift.yml +79 -7
  4. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/CHANGELOG.md +387 -1
  5. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/CLAUDE.md +19 -0
  6. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/PKG-INFO +10 -1
  7. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/README.md +9 -0
  8. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/__init__.py +36 -32
  9. django_data_shape-0.22.0/django_data_shape/backends/__init__.py +1 -0
  10. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/backends}/require_clone_strategy.py +2 -2
  11. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/backends}/require_postgres.py +1 -1
  12. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/backends}/unsupported_backend.py +1 -1
  13. django_data_shape-0.22.0/django_data_shape/databases/__init__.py +1 -0
  14. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/databases}/clone_database.py +16 -4
  15. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/databases}/drop_database.py +13 -1
  16. django_data_shape-0.22.0/django_data_shape/databases/require_quotable_name.py +45 -0
  17. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/databases}/shape_digest.py +7 -7
  18. django_data_shape-0.22.0/django_data_shape/databases/template_database.py +993 -0
  19. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/databases}/unhashable_shape.py +1 -1
  20. django_data_shape-0.22.0/django_data_shape/databases/unusable_base.py +33 -0
  21. django_data_shape-0.22.0/django_data_shape/declaration/__init__.py +1 -0
  22. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/declaration}/check_constraints.py +9 -9
  23. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/declaration}/order_derivations.py +1 -1
  24. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/declaration}/order_tables.py +6 -6
  25. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/declaration}/projection.py +121 -13
  26. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/declaration}/shape.py +12 -12
  27. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/declaration}/shape_from_factory.py +1 -1
  28. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/declaration}/sql_value.py +55 -1
  29. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/declaration}/table.py +7 -7
  30. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/derivations/after.py +1 -1
  31. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/derivations/derived.py +2 -2
  32. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/derivations/given.py +1 -1
  33. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/derivations/offset.py +1 -1
  34. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/derivations/per_parent.py +2 -2
  35. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/derivations/product.py +2 -2
  36. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/distributions/categorical.py +1 -1
  37. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/distributions/distinct.py +2 -2
  38. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/distributions/skew.py +1 -1
  39. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/distributions/uniform.py +1 -1
  40. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/distributions/zipf.py +1 -1
  41. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/fixtures/scale_fixture.py +24 -12
  42. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/fixtures/shape_fixture.py +20 -12
  43. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/fixtures/skip_unless_postgres.py +3 -3
  44. django_data_shape-0.22.0/django_data_shape/generation/__init__.py +1 -0
  45. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/generation}/generate_rows.py +4 -4
  46. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/generation}/refuse_queries.py +1 -1
  47. django_data_shape-0.22.0/django_data_shape/invariants/__init__.py +1 -0
  48. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/invariants}/check_invariants.py +3 -3
  49. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/invariants}/invariant.py +2 -2
  50. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/invariants}/invariant_violated.py +1 -1
  51. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/keys/disjoint.py +7 -0
  52. django_data_shape-0.22.0/django_data_shape/keys/is_disjoint.py +31 -0
  53. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/keys/key_function.py +1 -1
  54. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/keys/md5_keys.py +1 -1
  55. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/keys/sequential_keys.py +1 -1
  56. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/keys/sql_keys.py +1 -1
  57. django_data_shape-0.22.0/django_data_shape/loading/__init__.py +1 -0
  58. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/loading}/apply_statistics_targets.py +58 -5
  59. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/loading}/build.py +44 -31
  60. django_data_shape-0.22.0/django_data_shape/relations/__init__.py +1 -0
  61. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/relations}/fan_out.py +2 -2
  62. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/relations}/fan_out_sizes.py +17 -16
  63. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/relations}/paired.py +3 -3
  64. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/relations}/resolve_fan_out.py +15 -4
  65. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/relations}/resolve_paired.py +4 -4
  66. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/relations}/world_changed.py +2 -2
  67. django_data_shape-0.22.0/django_data_shape/scaling/__init__.py +1 -0
  68. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/scaling}/scale_protocol.py +1 -1
  69. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/scaling}/scaled_shape.py +5 -5
  70. django_data_shape-0.22.0/django_data_shape/scaling/scaled_world.py +867 -0
  71. django_data_shape-0.22.0/django_data_shape/scaling/shape_referenced.py +41 -0
  72. django_data_shape-0.22.0/django_data_shape/types/__init__.py +1 -0
  73. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/types}/build_result.py +1 -1
  74. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/types}/canonical.py +2 -2
  75. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/types}/children_per_parent.py +1 -1
  76. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/utils.py +1 -1
  77. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/version.py +1 -1
  78. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/docs/keys.md +7 -0
  79. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/docs/projections.md +96 -0
  80. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/docs/pytest.md +133 -18
  81. django_data_shape-0.22.0/docs/reference.md +143 -0
  82. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/docs/relations.md +7 -6
  83. django_data_shape-0.22.0/docs/statistics.md +506 -0
  84. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/pyproject.toml +1 -1
  85. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/backends}/test_require_clone_strategy.py +1 -1
  86. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/backends}/test_require_postgres.py +1 -1
  87. django_data_shape-0.22.0/tests/databases/test_require_quotable_name.py +102 -0
  88. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/databases}/test_shape_digest.py +1 -1
  89. django_data_shape-0.22.0/tests/databases/test_template_database.py +2078 -0
  90. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/declaration}/test_order_derivations.py +1 -1
  91. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/declaration}/test_order_tables.py +1 -1
  92. django_data_shape-0.22.0/tests/declaration/test_projection_values_references.py +195 -0
  93. django_data_shape-0.22.0/tests/declaration/test_projection_values_uuid.py +153 -0
  94. django_data_shape-0.22.0/tests/distributions/__init__.py +0 -0
  95. django_data_shape-0.22.0/tests/fixtures/__init__.py +0 -0
  96. django_data_shape-0.22.0/tests/generation/__init__.py +0 -0
  97. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/generation}/test_generate_rows.py +2 -2
  98. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/generation}/test_refuse_queries.py +1 -1
  99. django_data_shape-0.22.0/tests/invariants/__init__.py +0 -0
  100. django_data_shape-0.22.0/tests/keys/__init__.py +0 -0
  101. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/keys}/test_infer_key_strategy.py +1 -1
  102. django_data_shape-0.22.0/tests/keys/test_is_disjoint.py +89 -0
  103. django_data_shape-0.22.0/tests/loading/__init__.py +0 -0
  104. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/loading}/test_apply_statistics_targets.py +180 -6
  105. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/loading}/test_build.py +25 -0
  106. django_data_shape-0.22.0/tests/relations/__init__.py +0 -0
  107. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/relations}/test_paired_plan.py +2 -2
  108. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/relations}/test_resolve_fan_out.py +1 -1
  109. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/relations}/test_resolve_paired.py +2 -2
  110. django_data_shape-0.22.0/tests/scaling/__init__.py +0 -0
  111. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/scaling}/test_scale_protocol.py +3 -3
  112. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/scaling}/test_scaled_world.py +139 -7
  113. django_data_shape-0.22.0/tests/scaling/test_scaled_world_disjoint_advice.py +166 -0
  114. django_data_shape-0.22.0/tests/scaling/test_scaled_world_leaves_undeclared_tables_alone.py +672 -0
  115. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/scaling}/test_scaled_world_over_a_session_world.py +44 -1
  116. django_data_shape-0.22.0/tests/scaling/test_scaled_world_over_disjoint_tables.py +482 -0
  117. django_data_shape-0.22.0/tests/scaling/test_scaled_world_pending_trigger_events.py +382 -0
  118. django_data_shape-0.22.0/tests/test_package_layout.py +77 -0
  119. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/test_reference_documents_the_public_surface.py +1 -1
  120. django_data_shape-0.22.0/tests/testapp/__init__.py +0 -0
  121. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/testapp/models.py +246 -0
  122. django_data_shape-0.22.0/tests/types/__init__.py +0 -0
  123. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/uv.lock +74 -74
  124. django_data_shape-0.20.0/django_data_shape/scaled_world.py +0 -176
  125. django_data_shape-0.20.0/django_data_shape/template_database.py +0 -281
  126. django_data_shape-0.20.0/docs/reference.md +0 -139
  127. django_data_shape-0.20.0/docs/statistics.md +0 -274
  128. django_data_shape-0.20.0/tests/test_template_database.py +0 -443
  129. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/.github/CODEOWNERS +0 -0
  130. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/.github/SECURITY.md +0 -0
  131. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/.github/dependabot.yml +0 -0
  132. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/.gitignore +0 -0
  133. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/.pre-commit-config.yaml +0 -0
  134. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/LICENSE +0 -0
  135. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/Makefile +0 -0
  136. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/declaration}/invalid_shape.py +0 -0
  137. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/derivations/__init__.py +0 -0
  138. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/derivations/aligned.py +0 -0
  139. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/derivations/copied.py +0 -0
  140. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/derivations/derivation.py +0 -0
  141. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/derivations/scope.py +0 -0
  142. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/distributions/__init__.py +0 -0
  143. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/distributions/ascending.py +0 -0
  144. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/distributions/bounded.py +0 -0
  145. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/distributions/constant.py +0 -0
  146. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/distributions/distribution.py +0 -0
  147. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/distributions/sequential.py +0 -0
  148. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/fixtures/__init__.py +0 -0
  149. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/generation}/derivation_queried_database.py +0 -0
  150. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/keys/__init__.py +0 -0
  151. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/keys}/infer_key_strategy.py +0 -0
  152. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/keys/key_strategy.py +0 -0
  153. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/keys/uuid_keys.py +0 -0
  154. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/loading}/shape_not_empty.py +0 -0
  155. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/django_data_shape/py.typed +0 -0
  156. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/relations}/fan_out_plan.py +0 -0
  157. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/relations}/paired_plan.py +0 -0
  158. {django_data_shape-0.20.0/django_data_shape → django_data_shape-0.22.0/django_data_shape/types}/table_result.py +0 -0
  159. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/docs/derivations.md +0 -0
  160. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/docs/index.md +0 -0
  161. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/docs/invariants.md +0 -0
  162. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/mkdocs.yml +0 -0
  163. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/scripts/check-changelog +0 -0
  164. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/scripts/release-publish.sh +0 -0
  165. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/__init__.py +0 -0
  166. {django_data_shape-0.20.0/tests/derivations → django_data_shape-0.22.0/tests/backends}/__init__.py +0 -0
  167. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/conftest.py +0 -0
  168. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/conftest_settings.py +0 -0
  169. {django_data_shape-0.20.0/tests/distributions → django_data_shape-0.22.0/tests/databases}/__init__.py +0 -0
  170. {django_data_shape-0.20.0/tests/keys → django_data_shape-0.22.0/tests/declaration}/__init__.py +0 -0
  171. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/declaration}/test_check_constraints.py +0 -0
  172. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/declaration}/test_projection.py +0 -0
  173. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/declaration}/test_projection_ceiling.py +0 -0
  174. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/declaration}/test_projection_sql_percent.py +0 -0
  175. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/declaration}/test_projection_values.py +0 -0
  176. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/declaration}/test_shape.py +0 -0
  177. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/declaration}/test_shape_from_factory.py +0 -0
  178. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/declaration}/test_table.py +0 -0
  179. {django_data_shape-0.20.0/tests/testapp → django_data_shape-0.22.0/tests/derivations}/__init__.py +0 -0
  180. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/derivations/test_after.py +0 -0
  181. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/derivations/test_after_units.py +0 -0
  182. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/derivations/test_aligned.py +0 -0
  183. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/derivations/test_canonical_arithmetic.py +0 -0
  184. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/derivations/test_derived.py +0 -0
  185. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/derivations/test_given.py +0 -0
  186. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/derivations/test_per_parent.py +0 -0
  187. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/distributions/test_constant.py +0 -0
  188. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/distributions/test_sequential.py +0 -0
  189. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/distributions/test_skew.py +0 -0
  190. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/distributions/test_uniform.py +0 -0
  191. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/distributions/test_zipf.py +0 -0
  192. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/fixtures}/test_fixtures.py +0 -0
  193. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/invariants}/test_invariant.py +0 -0
  194. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/keys/test_key_function.py +0 -0
  195. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/keys/test_md5_keys.py +0 -0
  196. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/keys/test_sequential_keys.py +0 -0
  197. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/keys/test_uuid_keys.py +0 -0
  198. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/loading}/test_build_derivations.py +0 -0
  199. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/loading}/test_build_graph.py +0 -0
  200. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/loading}/test_build_invariants.py +0 -0
  201. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/loading}/test_build_keys.py +0 -0
  202. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/loading}/test_build_m2m.py +0 -0
  203. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/loading}/test_build_portable.py +0 -0
  204. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/loading}/test_build_projection.py +0 -0
  205. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/relations}/test_fan_out.py +0 -0
  206. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/relations}/test_fan_out_sizes.py +0 -0
  207. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/relations}/test_paired.py +0 -0
  208. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/relations}/test_paired_parents.py +0 -0
  209. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/scaling}/scale_protocol_consumers.py +0 -0
  210. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/scaling}/scale_protocol_impostors.py +0 -0
  211. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/scaling}/test_scaled_projection_ceiling.py +0 -0
  212. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/scaling}/test_scaled_shape.py +0 -0
  213. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/scaling}/test_scaled_world_restores_sequences.py +0 -0
  214. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/test_documentation.py +0 -0
  215. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/test_utils.py +0 -0
  216. {django_data_shape-0.20.0 → django_data_shape-0.22.0}/tests/test_version.py +0 -0
  217. {django_data_shape-0.20.0/tests → django_data_shape-0.22.0/tests/types}/test_children_per_parent.py +0 -0
@@ -56,7 +56,7 @@ jobs:
56
56
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
57
57
  with:
58
58
  fetch-depth: 0
59
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
59
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
60
60
  with:
61
61
  enable-cache: true
62
62
  - name: Install
@@ -20,7 +20,7 @@ jobs:
20
20
  runs-on: ubuntu-latest
21
21
  steps:
22
22
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
23
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
24
24
  with:
25
25
  enable-cache: true
26
26
  - name: Install
@@ -37,7 +37,7 @@ jobs:
37
37
  runs-on: ubuntu-latest
38
38
  steps:
39
39
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
40
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
40
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
41
41
  with:
42
42
  enable-cache: true
43
43
  - name: Install with docs group
@@ -79,7 +79,7 @@ jobs:
79
79
  PGDATABASE: django_data_shape
80
80
  steps:
81
81
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
82
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
82
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
83
83
  with:
84
84
  enable-cache: true
85
85
  # A floor is a claim about the *oldest* version that works, and a lockfile
@@ -199,7 +199,7 @@ jobs:
199
199
 
200
200
  steps:
201
201
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
202
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
202
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
203
203
  with:
204
204
  enable-cache: true
205
205
  - name: Install Python ${{ matrix.python-version }}
@@ -257,7 +257,7 @@ jobs:
257
257
  PGDATABASE: django_data_shape
258
258
  steps:
259
259
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
260
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
260
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
261
261
  with:
262
262
  enable-cache: true
263
263
  - name: Install Python 3.13
@@ -41,9 +41,38 @@ jobs:
41
41
  resolve-latest:
42
42
  name: resolve unpinned + test
43
43
  runs-on: ubuntu-latest
44
+ # The same service tests.yml gives its own jobs. Without it this workflow
45
+ # resolved the newest of everything, installed it, and then failed every
46
+ # Postgres-backed test on `connection refused` -- so it has never once
47
+ # reported on upstream drift, only on its own missing database. A job that
48
+ # cannot pass cannot fail informatively either: the issue it opens each week
49
+ # names a resolve that was never the problem.
50
+ services:
51
+ postgres:
52
+ image: postgres:17
53
+ env:
54
+ POSTGRES_USER: postgres
55
+ POSTGRES_PASSWORD: postgres
56
+ POSTGRES_DB: django_data_shape
57
+ ports:
58
+ - 5432:5432
59
+ options: >-
60
+ --health-cmd pg_isready
61
+ --health-interval 10s
62
+ --health-timeout 5s
63
+ --health-retries 5
64
+ # Mirrors tests.yml. The unpinned resolve includes psycopg, so a release of
65
+ # it that breaks the connection is exactly the drift this is here to find,
66
+ # and it can only be found against a real server.
67
+ env:
68
+ PGHOST: localhost
69
+ PGPORT: "5432"
70
+ PGUSER: postgres
71
+ PGPASSWORD: postgres
72
+ PGDATABASE: django_data_shape
44
73
  steps:
45
74
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
46
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
75
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
47
76
  with:
48
77
  enable-cache: false
49
78
 
@@ -63,27 +92,70 @@ jobs:
63
92
  run: git --no-pager diff --stat uv.lock || true
64
93
 
65
94
  - name: Install
95
+ id: install
66
96
  run: uv sync --all-groups --all-extras
67
97
 
68
98
  - name: Show the resolved versions
69
99
  run: uv pip list
70
100
 
101
+ # A resolve can succeed by backing off. When the newest release of a
102
+ # direct dependency conflicts with something else this project requires,
103
+ # uv takes the newest version that does resolve and logs an ordinary
104
+ # "Updated" line, so the suite below passes against a release nobody
105
+ # chose. That is how django-ag-ui's run went green with pydantic-ai-slim
106
+ # at 2.46 and 2.54 out: from 2.47 its [ag-ui] extra caps ag-ui-protocol
107
+ # below the 1.0 django-ag-ui requires. This compares every direct
108
+ # dependency with PyPI, fails on one held below a release that has been
109
+ # out longer than the Dependabot cooldown, and asks uv what holds it. A
110
+ # ceiling declared in pyproject.toml is honoured, which is how a hold is
111
+ # accepted on purpose. One copy for every repo, pinned like any action.
112
+ - name: Check no direct dependency is held back
113
+ id: held-back
114
+ uses: Artui/held-back-check@b64bbb88fd63c9c96675caba477cce13714de93e # v0.1.0
115
+
116
+ # Runs whether or not the check above failed: they answer different
117
+ # questions, and a hold is no reason to stop measuring what did resolve.
71
118
  - name: Test
119
+ id: test
120
+ if: ${{ !cancelled() && steps.install.outcome == 'success' }}
72
121
  run: uv run pytest
73
122
 
74
123
  - name: Open or update an issue on failure
75
- if: failure()
124
+ # Main only. A dispatch on any other branch is a trial of that branch's
125
+ # manifest, and an issue saying the unpinned resolve is failing would be
126
+ # filed against the wrong tree.
127
+ if: failure() && github.ref == 'refs/heads/main'
128
+ env:
129
+ HELD_BACK: ${{ steps.held-back.outcome }}
130
+ TESTS: ${{ steps.test.outcome }}
131
+ REPORT: ${{ steps.held-back.outputs.report }}
76
132
  uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
77
133
  with:
78
134
  script: |
135
+ const fs = require("fs");
79
136
  const title = "upstream drift: the unpinned resolve is failing";
137
+ const run = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
138
+ const sections = [];
139
+ if (process.env.TESTS === "failure") {
140
+ sections.push([
141
+ "The weekly unpinned resolve failed. A dependency released a version",
142
+ "that `pyproject.toml` admits and the suite does not survive.",
143
+ "",
144
+ "The `git diff --stat uv.lock` step in that run names what moved.",
145
+ ].join("\n"));
146
+ }
147
+ if (process.env.HELD_BACK === "failure") {
148
+ sections.push(fs.existsSync(process.env.REPORT)
149
+ ? fs.readFileSync(process.env.REPORT, "utf8")
150
+ : "The held-back check failed before writing its report; the run's log says why.");
151
+ }
152
+ if (!sections.length) {
153
+ sections.push("A step before the suite failed; the run's log names it.");
154
+ }
80
155
  const body = [
81
- "The weekly unpinned resolve failed. A dependency released a version",
82
- "that `pyproject.toml` admits and the suite does not survive.",
83
- "",
84
- `Run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
156
+ ...sections,
85
157
  "",
86
- "The `git diff --stat uv.lock` step in that run names what moved.",
158
+ `Run: ${run}`,
87
159
  "",
88
160
  "Fix forward (adopt the new version) or add a floor/ceiling that says",
89
161
  "why, but do not silence the job — it is the control that lets this",
@@ -7,6 +7,390 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.22.0] — 2026-10-07
11
+
12
+ ### Added
13
+
14
+ - **`template_database(shape, base=...)` starts a template from a database
15
+ that is already migrated.** A template was always built into an empty
16
+ database that `migrate` replayed the whole history into, and on a project
17
+ with a long one that replay is most of the cost: a consumer with about 330
18
+ migrations measured thirteen and a half minutes of `migrate` against about a
19
+ minute to build a 4.4-million-row shape, paid again by every template a
20
+ change to the declaration makes. With a base, the template starts as
21
+ `CREATE DATABASE ... TEMPLATE <base>`: the tables `migrate` builds for apps
22
+ without migrations are rebuilt from the models, and everything else the base
23
+ holds is carried as it is. Apps with migrations are brought forward by their
24
+ history: `migrate --run-syncdb` still runs over the copy, applying whatever
25
+ the base has yet to and firing `post_migrate`. Apps without migrations have
26
+ no history, so the tables `run_syncdb` makes for them -- their managed
27
+ models' tables and the many-to-many tables Django creates for those models'
28
+ relations, read the way `run_syncdb` reads them, router and models module
29
+ included, and found under the name PostgreSQL stores, which cuts a
30
+ `db_table` longer than 63 bytes -- are dropped from the copy first and made
31
+ again from the current models, as in an empty database. `run_syncdb` never alters an existing table, so a table the
32
+ base made from an older model would otherwise survive under a key naming the
33
+ new one. The base's rows in those tables do not carry over; the rest of its
34
+ rows, an unmanaged model's table and the tables of an app no longer installed
35
+ are carried. A foreign key or view in the base that points into a rebuilt
36
+ table stops the drop, which runs without `CASCADE` so that nothing is
37
+ removed silently, and is refused as `UnusableBase` from the copy. So is a
38
+ rebuilt table the connecting role may not drop: `DROP TABLE` needs ownership
39
+ of the table or of its schema, and a copy keeps every owner the base gave, so
40
+ the message names each such table, its owner and the remedies -- connect as
41
+ the owner or a member of it that inherits its privileges, reassign the tables
42
+ in the base, or pass `base=None`. The check runs in the copy rather than the
43
+ base, because the role that makes the copy owns it and so holds the
44
+ privileges of `pg_database_owner`, which owns `public` from PostgreSQL 15: a
45
+ role owning none of those tables can still drop them from its own copy, and
46
+ is not refused.
47
+ `base=None`, the default, behaves and keys exactly as before.
48
+
49
+ **A base behind the migrations on disk is migrated forward in the copy; only
50
+ a history migrating cannot repair is refused**, with the new `UnusableBase`,
51
+ before anything is created. Migrating forward always ends at the checkout's schema for the
52
+ apps with migrations, whatever prefix of their history the base holds, and
53
+ the others are rebuilt, so the key stays sound without the
54
+ base's applied migrations entering it -- the same reason Django's own
55
+ PostgreSQL `TEST: {"TEMPLATE": ...}` can migrate a copy forward. A base far
56
+ behind pays its `migrate` once per key, never more than building from empty,
57
+ and the base a project has most often, one behind because a branch added a
58
+ migration, builds rather than being refused. *Ahead* is an applied migration
59
+ of an installed app that no migration on disk is or replaces: of the
60
+ histories a base can record, the one migrating cannot bring to the
61
+ checkout's schema, since a branch
62
+ migration that adds only an index would otherwise build silently and skew
63
+ plan assertions. The rows a squash leaves behind while it still lists them in
64
+ `replaces`, once all of them are applied, and the rows of an app that is no
65
+ longer installed, are not ahead. The message names up to three migrations
66
+ and both remedies: `manage.py migrate <app> --prune` for rows left by
67
+ squashed migrations deleted after their squash's `replaces` was removed, and
68
+ migrating the base back or recreating it for a migration from another
69
+ branch. Where `--prune` would decline, because a squash on disk still lists
70
+ in `replaces` a migration the base records as applied and that is gone from
71
+ disk -- one of the app being pruned from Django 5.1, of any app before -- it
72
+ names the squash and says to finish it first, and nowhere else, since
73
+ finishing a squash whose replaced files are still on disk splits the app
74
+ into two leaf migrations. Two
75
+ histories `migrate` would mishandle are refused too: a squash applied to the
76
+ base only in part, when a replaced migration it has yet to apply is gone
77
+ from disk -- Django then runs neither the squash nor the rest of what it
78
+ replaces, so the template would silently lack them, read off the graph
79
+ Django's loader builds, so that from Django 6.0 a squash of a squash is
80
+ judged over everything under it -- and a base holding the tables of an app
81
+ with a migration on disk that it records no applied migration for, which
82
+ `migrate` would try to create again: one with no `django_migrations` table,
83
+ one restored from `pg_dump --schema-only`, which brings that table back
84
+ empty, or one made before the app had migrations. An app whose migrations
85
+ would create nothing, such as a `0001_initial` holding only
86
+ `SeparateDatabaseAndState` state operations, is refused the same way,
87
+ although `migrate` would accept it. A base that does not
88
+ exist, one that does not accept connections, a name holding a double quote
89
+ -- which Django's quoting cannot carry intact, so the database checked and
90
+ the database copied could differ -- and a template this package made or the
91
+ partial of one are each refused by name rather than failing inside Django or
92
+ the server. Only a name the package generates counts as a template, so a
93
+ database of your own whose name starts `data_shape_` is not mistaken for one.
94
+
95
+ The key also takes the base's name and its database oid, so dropping and
96
+ recreating a base -- the usual way one restored from a dump is refreshed,
97
+ with the rows of `django_migrations` alongside the schema -- is a new
98
+ template. The base is checked on a cache hit too. Rows
99
+ the base holds become template content; changing them in place, by hand or
100
+ by migrating the base, neither of which moves its name or oid, is not seen
101
+ by the key, and is stated beside the
102
+ existing `RunSQL` gap with the same remedy, `drop_database`. One case no
103
+ check can see is stated too: a migration regenerated under a name the base
104
+ has already applied is skipped by `migrate`, so the copy keeps the version
105
+ the base ran; recreating the base is the remedy. Nor can one more: SQL an
106
+ applied migration of an app with migrations ran against a table of an app
107
+ without them -- an index, a trigger, a policy, a grant or a comment -- is
108
+ lost when the copy rebuilds that table, and recreating the base does not
109
+ help, so that template is built from empty with `base=None`. The process's
110
+ own connection is closed before the copy, so a project whose test database
111
+ is the base can pass it as one.
112
+
113
+ ### Changed
114
+
115
+ - **Every module the package owns now lives in a subpackage named for a
116
+ concern.** The root held forty-five modules against four to eight in the
117
+ sibling libraries, and no rule had been broken: the structural rules all
118
+ governed a *file* -- one exported symbol, named after it, fully annotated --
119
+ and none governed a directory, so a package could satisfy every one of them
120
+ and still put everything it does in one place. The root now holds
121
+ `__init__.py`, `version.py` and `utils.py`.
122
+
123
+ The new directories are `declaration/` (what a user writes, and everything
124
+ that refuses it before a row exists), `relations/` (fan-out and paired edges,
125
+ declared and resolved against the keys that exist), `generation/`, `loading/`,
126
+ `invariants/`, `databases/`, `backends/`, `scaling/` and `types/`.
127
+ `infer_key_strategy` moved into the existing `keys/`. There is no
128
+ `exceptions/`: an exception stays beside the code that raises it, so
129
+ `DerivationQueriedDatabase` sits with the guard that raises it and
130
+ `UnsupportedBackend` with the two backend gates.
131
+
132
+ **No public import changes.** `__all__` is unchanged, name for name and in the
133
+ same order, and every symbol is still imported the same way -- the only paths
134
+ that moved are the ones a caller was never meant to reach into. The
135
+ `django_data_shape.fixtures` surface is untouched.
136
+
137
+ - **`tests/test_package_layout.py` gates the layout**, asserting that the root
138
+ holds only the allowlisted modules and that no module name is used at two
139
+ levels. Prose is what failed here: a convention that names only files is
140
+ obeyed exactly as far as files go, and the drift it permitted was invisible
141
+ for twenty-one releases.
142
+
143
+ ### Fixed
144
+
145
+ - **A scaled world no longer empties tables it does not declare, including
146
+ parents reached through a chain of foreign keys.** On PostgreSQL the emptying
147
+ was `TRUNCATE ... CASCADE`, and `CASCADE` follows foreign keys by schema,
148
+ transitively, whatever the rows hold. In an application whose billing points
149
+ back at its clubs -- a section belongs to a club, a fee to a section, and a
150
+ club references its selected fee -- a shape declaring only `Section` emptied
151
+ the clubs too. A fan-out narrowed to a club the caller had made, with
152
+ `parents=`, was then refused with `InvalidShape` for naming a key that was
153
+ "not a row of" the club table, blaming the caller's key for the package's own
154
+ statement; without `parents=` nothing was refused, and tables the caller had
155
+ filled were silently empty for the life of the block.
156
+
157
+ A world now removes the rows of its declared tables and nothing else, and no
158
+ statement it issues changes a table its shape does not declare, even inside
159
+ the block. On PostgreSQL it reads, from `pg_constraint`, every table
160
+ referencing a declared table that holds rows -- the set `CASCADE` would have
161
+ taken -- and when none of those holds rows it issues one `TRUNCATE` that
162
+ lists them all, without `CASCADE`: nothing outside the declaration loses a
163
+ row, and a list that ever missed a table would be refused by PostgreSQL
164
+ rather than silently widened. That is the route a session world under a
165
+ scaled world over the same graph takes every time. Otherwise each declared
166
+ table is emptied by `DELETE`, children first, and every undeclared row stays
167
+ where it was. A `DELETE` fires row-level `DELETE` triggers, which `TRUNCATE`
168
+ did not: a trigger on a declared table runs inside the world, and what it
169
+ writes elsewhere is rolled back with the block, while a `BEFORE DELETE`
170
+ trigger that keeps its rows leaves the build refusing the table with
171
+ `ShapeNotEmpty`. A world over empty tables issues no emptying statement and
172
+ fires no foreign-key checks.
173
+
174
+ A declared table with `Disjoint` keys is still left alone and built beside --
175
+ unless it holds rows and has a foreign key into a declared table the world
176
+ empties, directly or through another such table. Then it is emptied too, since
177
+ its rows are declared rows and cannot outlive the parents they point at. In
178
+ 0.21.0 `CASCADE` emptied such a table on PostgreSQL, along with everything
179
+ else referencing the declared tables, and off PostgreSQL its rows were left
180
+ pointing at parents the world had deleted. The foreign keys that decide it
181
+ are the models', so a `ForeignKey(db_constraint=False)` pulls its table in as
182
+ well. A `Disjoint` table pointing at nothing the world empties keeps its
183
+ rows, which over a session world declaring it are the session's, and the
184
+ world builds beside them with keys of its own. `UuidKeys` and `Md5Keys` make
185
+ each key from the row and a stream derived from the seed, which scaling
186
+ keeps, so in 0.21.0 a world over a session world built from the same shape
187
+ made the session's keys and failed on the primary key. Every `Disjoint`
188
+ table a world builds is now handed a stream of the world's own, never the
189
+ one `build()` uses for the same seed and table, so inside a world its keys
190
+ are not the ones `build()` gives the same declaration. That stream does not
191
+ depend on the factor, so row *i* has one key at every factor, as an integer
192
+ key does, and a world opened inside another over the same table draws from
193
+ another stream again. Foreign keys into such a table need nothing, since a
194
+ fan-out reads its parents' keys from the table. And a world now reads
195
+ `Disjoint` as `build()` does: a key strategy implementing the protocol whose
196
+ `is_disjoint_from_existing_rows()` answers no is emptied like any other,
197
+ where the world used to leave its rows in place for the build to refuse with
198
+ `ShapeNotEmpty`.
199
+
200
+ **One behaviour changes: a scaled world declaring a parent over rows whose
201
+ undeclared children reference it is now refused**, where 0.21.0 silently
202
+ emptied them on PostgreSQL and left them pointing at nothing elsewhere.
203
+ Removing the parent's rows would leave those references pointing at nothing
204
+ or take the children along, so the world raises the new `ShapeReferenced`
205
+ before removing anything, naming each reference as
206
+ `referencing_table.column -> declared_table` and the ways out: declare that
207
+ table in the shape too, so its rows are the world's, or do not create those
208
+ rows in the test. Where following it would end the refusal, it also offers
209
+ `Disjoint` keys, so the world builds beside the rows already there: only
210
+ where every declared table it names has a primary key that accepts the UUIDs
211
+ `UuidKeys` and `Md5Keys` make, so never an integer key or one that is itself
212
+ a foreign key, and only where none of them is a projected table, has
213
+ `Disjoint` keys already, or has a foreign key into another table being
214
+ emptied, which would pull it back into the emptying. A scaled
215
+ world over part of a session world's graph meets it the same way, and the
216
+ remedy is the same. A foreign key left null is not a reference, and only a
217
+ foreign key the database enforces is seen: `ForeignKey(db_constraint=False)`
218
+ and `GenericForeignKey` are invisible to it. The refusal is made on every
219
+ backend: PostgreSQL's catalogue answers it there, and Django's introspection
220
+ everywhere else. It runs before any `DELETE` on both, which is what keeps a
221
+ `DELETE` inside the declaration: one can reach past its table through a
222
+ database-level `ON DELETE`, such as the one Django 6.1's `DB_CASCADE`
223
+ creates, but by then no row outside the tables being emptied references
224
+ them, so all it can reach is declared rows being removed anyway. Introspection reports each column of a
225
+ composite foreign key on its own, so off PostgreSQL such a key counts as soon
226
+ as any column is set; Django never creates one.
227
+
228
+ The `parents=` refusal names the one way a key can still vanish inside a
229
+ world: a parent table declared in the same shape, which the world empties
230
+ before building. The `scaled_world`, `scale_fixture` and `shape_fixture`
231
+ docstrings and the pytest page describe the rule, where the two fixtures'
232
+ docstrings still said a scaled world could not build over a session world at
233
+ all.
234
+
235
+ - **A scaled world builds over child rows the caller wrote earlier in the same
236
+ transaction.** On PostgreSQL it failed on the way in with `cannot TRUNCATE
237
+ "..." because it has pending trigger events`. Django creates PostgreSQL
238
+ foreign keys `DEFERRABLE INITIALLY DEFERRED`, so a child row leaves its
239
+ foreign-key check queued until commit, and PostgreSQL refuses to truncate a
240
+ table with checks still pending. An insert into a parent alone queues nothing,
241
+ which is why only a caller who wrote a child met it; the usual way in is a
242
+ factory's `SubFactory` in a test's own setup.
243
+
244
+ `scaled_world` now fires the pending checks before a `TRUNCATE`. That changes
245
+ only *when* they run: a row that genuinely violates a constraint now raises
246
+ `IntegrityError` at world entry, naming the constraint, rather than when the
247
+ enclosing transaction next checks. The `DELETE` route the entry above adds,
248
+ taken on PostgreSQL when an undeclared table holds rows, fires them after its
249
+ `DELETE` statements instead, so a row outside the declaration that breaks a
250
+ constraint raises at entry on either route. The two routes part on an orphan
251
+ the caller wrote in a declared table: the `TRUNCATE` route raises for it,
252
+ while the `DELETE` route removes it first, PostgreSQL skips the check of a row
253
+ that is gone, and the world builds. Once the checks are fired, on either
254
+ route, every deferrable constraint is deferred for the rest of the block, one
255
+ declared `INITIALLY IMMEDIATE` included; the mode is transaction state, so
256
+ the rollback of the world's own block restores the caller's. Off PostgreSQL
257
+ nothing is fired, because nothing there refuses a pending check.
258
+
259
+ **`build()` sets a declared statistics target over checks still queued in its
260
+ transaction**, where PostgreSQL refused the `ALTER TABLE ... SET STATISTICS`
261
+ with `cannot ALTER TABLE "..." because it has pending trigger events`. A table
262
+ with `Disjoint` keys is built beside the rows already there, so a row the
263
+ caller wrote into it earlier in the transaction left its check queued on the
264
+ very table the build then altered -- and a scaled world over such a table
265
+ empties nothing, so it met the same refusal. Each row a world's `DELETE`
266
+ removes from a referenced table queues a check too. `apply_statistics_targets`
267
+ now fires the pending checks before its first `ALTER TABLE`, and only for a
268
+ table that declares a target, so a table declaring none issues no statement
269
+ more. A row that breaks a constraint raises `IntegrityError` there, naming
270
+ it. Firing them ends with `SET CONSTRAINTS ALL DEFERRED`, so a build inside a
271
+ transaction of the caller's leaves every deferrable constraint deferred until
272
+ that transaction ends.
273
+
274
+ The docstring claimed this package never issues a destructive statement
275
+ against a table it did not fill. Beside `CASCADE` that was untrue. What holds
276
+ now, and what the docstring says, is that no statement a world issues changes
277
+ a table its shape does not declare: the only statement naming an undeclared
278
+ table is a `TRUNCATE` of tables that hold no rows.
279
+
280
+ The PostgreSQL statement count a capture around a world sees over empty
281
+ tables is nineteen, the same at every factor; over a session world declaring
282
+ the same tables, emptying them first adds five more where another table
283
+ references them, four where none does, also the same at every factor. The
284
+ figure quoted in the `scaled_world` and `scale_fixture` docstrings
285
+ and in the pytest page had drifted from the measured one in all three places;
286
+ they now quote both, and a test holds them to it. The `execute_wrapper` figure
287
+ the `scaled_world` docstring quotes beside it, seventeen -- two fewer, because
288
+ `COPY` reaches the debug cursor's log but not the wrapper hook -- is now
289
+ measured and held to the prose the same way.
290
+
291
+ - **The `ShapeNotEmpty` message and the pytest page describe the refusal the
292
+ package makes.** Since 0.17.0 a scaled world empties the tables it declares
293
+ inside the transaction it rolls back, so it builds over a session world's
294
+ rows -- yet the message still said a scaled world cannot build over them, and
295
+ the pytest page's "The two fixtures do not share a table" still called that
296
+ composition refused and prescribed different models. The message now names
297
+ the causes that remain: a second build over a session world's rows, another
298
+ `shape_fixture` or `build()` called directly, and a template started from a
299
+ base database that holds rows in a declared table. Its remedies are a scaled
300
+ world, different models, or emptying the table, in the base if that is where
301
+ the rows came from. The page's section is now "A scaled world can sit over a
302
+ session world", quoting the new message, and a test holds the quotation to
303
+ the message raised.
304
+
305
+ - **Statistics targets are looked up under the name Django created the table
306
+ with.** `build()` read each column's current target through `regclass`,
307
+ binding the model's raw `db_table`, and `regclass` input is parsed as an
308
+ identifier: a capital letter folded to lower case and a dot split the name
309
+ into a schema and a table. A model whose `db_table` held either stopped the
310
+ build with "relation does not exist" before a row was written, or, where a
311
+ table of the folded name existed, was checked against that table's targets,
312
+ so a shape its own column could not record went through unrefused. The name
313
+ is now quoted by the connection, as Django's `CREATE TABLE` quoted it, which
314
+ also resolves a `db_table` written already quoted, such as
315
+ `'"schema"."table"'`.
316
+
317
+ - **`drop_database` and `clone_database` refuse a database name holding a
318
+ double quote**, with `ValueError`, before any statement. Django's
319
+ `quote_name` wraps a name in double quotes without escaping one inside it,
320
+ and passes a name already wrapped in them through unchanged, so a statement
321
+ could name a different database from the one the same string names as a
322
+ value: `drop_database('"x"')` looked up a database literally named `"x"`,
323
+ reported that it was not there, and dropped `x`, and a quote followed by a
324
+ clause made the clause part of the statement. `template_database` already
325
+ refused such a base; all three now share one check, and no name this package
326
+ makes holds a double quote.
327
+
328
+ ## [0.21.0] — 2026-09-06
329
+
330
+ ### Added
331
+
332
+ - **`values=` expressions can name one another, as `{values.<name>}`.** A
333
+ projected table's measure columns are usually related to each other -- a
334
+ requested amount and an approved one, a quantity and a total, an amount and
335
+ the rate derived from it -- and an expression could not say so. The
336
+ relationship had to be restated from whatever both columns were computed from,
337
+ with coefficients chosen so that it happens to hold: a reader could only find
338
+ the invariant by doing the arithmetic, and nothing rechecked it when either
339
+ expression was edited.
340
+
341
+ The name is dotted rather than a bare `{requested_amount}` because `{per}` and
342
+ `{source}` already occupy that space and a model is entitled to a column
343
+ called `per`. Only `values=` entries are referenceable -- a copied column is
344
+ already reachable as `{source}.name`, and the primary key is the
345
+ `row_number()` window itself and is reachable nowhere.
346
+
347
+ **It is substitution, not sharing**, which is what the spelling says: the
348
+ referenced expression is written out again, parenthesised, and the database
349
+ evaluates it once per reference. That is free for a deterministic expression,
350
+ and every expression here already had to be deterministic -- `template_database`
351
+ reuses a database keyed on the declaration and nothing else. What a reference
352
+ adds is that a volatile expression now disagrees with itself *within one
353
+ build*, so the documentation pairs the feature with an `Invariant` rather than
354
+ leaving the rule stated and unchecked.
355
+
356
+ A subquery computing each expression once was declined: the key is a
357
+ `row_number()` window whose outer `ORDER BY` decides where rows physically
358
+ land, so nesting moves the one thing this package exists to control -- and a
359
+ shape using no reference would have had its statement changed to buy a feature
360
+ it does not use. Substitution leaves every existing shape's SQL byte-identical.
361
+
362
+ A reference that names nothing, and a cycle, are refused at declaration time
363
+ and name the path. A name that is a *copied* column is refused separately and
364
+ hands over `{source}.name`, because that is a mistake about the spelling
365
+ rather than about the column.
366
+
367
+ ### Fixed
368
+
369
+ - **The documented `SqlValue` example was a type error on UUID-keyed models.**
370
+ `({per}.id * 31 + {source}.id * 17) % 5 + 1` works on Django's default
371
+ `BigAutoField` and has no operator at all on a schema whose models carry
372
+ `id = UUIDField(primary_key=True)` -- which a shared abstract base makes an
373
+ ordinary layout rather than an unusual one. So the first thing such a reader
374
+ copied out of the documentation did not run, and it failed from inside a
375
+ generated statement at build time rather than at declaration.
376
+
377
+ The guide and the docstring now carry a worked example for UUID keys, and it
378
+ is **executed by the suite** rather than asserted: the documentation's Python
379
+ blocks are only parsed, and an expression is a string literal that parses
380
+ perfectly, which is exactly how this shipped. Two parts of the replacement are
381
+ load-bearing and fail rarely enough to reach production -- `::bigint` before
382
+ `abs`, because `hashtext` returns `int4` and `abs(-2147483648)` is `integer
383
+ out of range`; and `abs` at all, because PostgreSQL's `%` keeps the sign of
384
+ the dividend, so a measure column would hold negatives and every plan over it
385
+ would still look fine.
386
+
387
+ - **The projections guide implied UUID keys were settled by one sentence about
388
+ `sql=`.** That sentence is about the *projected table's own* primary key. The
389
+ tables named by `per=` and `copying=` may be keyed however they like, and the
390
+ care an expression over one of them needs is a different subject that the page
391
+ did not cover -- so a reader with UUID keys throughout read a paragraph that
392
+ appeared to address them and was sent onward into the broken example.
393
+
10
394
  ## [0.20.0] — 2026-09-05
11
395
 
12
396
  ### Added
@@ -1298,7 +1682,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1298
1682
  into `COPY FROM STDIN`, which psycopg 2 cannot do without materialising them
1299
1683
  first.
1300
1684
 
1301
- [Unreleased]: https://github.com/Artui/django-data-shape/compare/v0.20.0...HEAD
1685
+ [Unreleased]: https://github.com/Artui/django-data-shape/compare/v0.22.0...HEAD
1686
+ [0.22.0]: https://github.com/Artui/django-data-shape/compare/v0.21.0...v0.22.0
1687
+ [0.21.0]: https://github.com/Artui/django-data-shape/compare/v0.20.0...v0.21.0
1302
1688
  [0.20.0]: https://github.com/Artui/django-data-shape/compare/v0.19.0...v0.20.0
1303
1689
  [0.19.0]: https://github.com/Artui/django-data-shape/compare/v0.18.1...v0.19.0
1304
1690
  [0.18.1]: https://github.com/Artui/django-data-shape/compare/v0.18.0...v0.18.1
@@ -51,6 +51,25 @@ make docs-build # mkdocs build --strict
51
51
  - `__init__.py` is the only re-export point, and holds no logic.
52
52
  - Every annotated module starts with `from __future__ import annotations`. Ruff's
53
53
  `required-imports` enforces this rather than leaving it to memory.
54
+ - **The package root is a table of contents, not a drawer.** It holds
55
+ `__init__.py`, `version.py` and `utils.py`; everything this package does lives
56
+ in a subpackage named for a concern. Those are `declaration/` (what a user
57
+ writes and what refuses it), `relations/`, `generation/`, `loading/`,
58
+ `invariants/`, `databases/`, `backends/`, `scaling/`, the vocabulary
59
+ directories `derivations/`, `distributions/` and `keys/`, the value carriers in
60
+ `types/`, and the pytest surface in `fixtures/`. Three modules on one concern
61
+ earn a directory; below three they join the neighbouring concern rather than
62
+ sitting at the root. A name says what the code does, so `helpers/`, `core/`,
63
+ `common/` and `misc/` are refused -- each is the flat root again one level
64
+ down. An exception lives beside the code that raises it, or, where many
65
+ modules raise it, in the subpackage whose failure it names, which is why there
66
+ is no `exceptions/`.
67
+
68
+ This rule is asserted in `tests/test_package_layout.py` rather than trusted,
69
+ because prose is what failed the first time: the six rules above all govern a
70
+ *file*, every one of them was satisfied, and forty-two modules still piled up
71
+ at the package root. The test gates both halves -- the root allowlist, and
72
+ that no module name is used at two levels.
54
73
 
55
74
  ## The rules the design rests on
56
75
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: django-data-shape
3
- Version: 0.20.0
3
+ Version: 0.22.0
4
4
  Summary: A realistically shaped test database from Django models: declare cardinality, skew and fan-out, load by COPY, and make the query planner believe it.
5
5
  Project-URL: Homepage, https://github.com/Artui/django-data-shape
6
6
  Project-URL: Repository, https://github.com/Artui/django-data-shape
@@ -268,6 +268,15 @@ clone_database(template, "test_myapp", replace=True)
268
268
 
269
269
  The template is named after a content hash of the declaration, the schema, the
270
270
  relevant settings and this package's version, so a stale one is never asked for.
271
+ A project with a long migration history can start templates from a database it
272
+ already keeps migrated, `template_database(shape, base="myproject_base")`, rather
273
+ than replaying every migration into an empty one. Apps with migrations are
274
+ migrated forward in the copy, which always ends at the checkout's schema; the
275
+ tables `migrate` builds for apps without them are rebuilt from the models, so the
276
+ base's rows in those do not carry over; and everything else the base holds, its
277
+ other rows and its unmanaged tables among them, is carried as it is. Only a
278
+ history no forward migration can fix, such as a base ahead of the migrations on
279
+ disk, is refused.
271
280
  A shape holding a `Derived` or a `KeyFunction` is refused rather than hashed --
272
281
  there is no honest digest of a callable, and every way of guessing one agrees
273
282
  while the data has changed. See
@@ -225,6 +225,15 @@ clone_database(template, "test_myapp", replace=True)
225
225
 
226
226
  The template is named after a content hash of the declaration, the schema, the
227
227
  relevant settings and this package's version, so a stale one is never asked for.
228
+ A project with a long migration history can start templates from a database it
229
+ already keeps migrated, `template_database(shape, base="myproject_base")`, rather
230
+ than replaying every migration into an empty one. Apps with migrations are
231
+ migrated forward in the copy, which always ends at the checkout's schema; the
232
+ tables `migrate` builds for apps without them are rebuilt from the models, so the
233
+ base's rows in those do not carry over; and everything else the base holds, its
234
+ other rows and its unmanaged tables among them, is carried as it is. Only a
235
+ history no forward migration can fix, such as a base ahead of the migrations on
236
+ disk, is refused.
228
237
  A shape holding a `Derived` or a `KeyFunction` is refused rather than hashed --
229
238
  there is no honest digest of a callable, and every way of guessing one agrees
230
239
  while the data has changed. See