django-data-shape 0.22.0__tar.gz → 0.23.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.
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/.github/workflows/release.yml +1 -1
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/.github/workflows/tests.yml +35 -10
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/.github/workflows/upstream-drift.yml +1 -1
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/CHANGELOG.md +71 -1
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/PKG-INFO +1 -1
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/declaration/check_constraints.py +18 -8
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/declaration/table.py +2 -2
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/relations/fan_out.py +44 -3
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/relations/fan_out_sizes.py +41 -21
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/relations/resolve_fan_out.py +126 -11
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/relations/resolve_paired.py +27 -15
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/scaling/scale_protocol.py +1 -1
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/scaling/scaled_world.py +2 -2
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/utils.py +85 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/version.py +1 -1
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/docs/pytest.md +4 -1
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/docs/relations.md +88 -8
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/docs/statistics.md +24 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/pyproject.toml +1 -1
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/test_check_constraints.py +51 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/loading/test_build.py +78 -1
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/relations/test_fan_out.py +36 -2
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/relations/test_fan_out_sizes.py +124 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/relations/test_paired_parents.py +38 -0
- django_data_shape-0.23.0/tests/relations/test_parents_key_spelling.py +177 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/relations/test_resolve_fan_out.py +222 -2
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/relations/test_resolve_paired.py +3 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/scale_protocol_consumers.py +3 -3
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/scale_protocol_impostors.py +2 -2
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/test_scaled_shape.py +4 -1
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/test_utils.py +86 -1
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/testapp/models.py +26 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/uv.lock +50 -74
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/.github/CODEOWNERS +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/.github/SECURITY.md +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/.github/dependabot.yml +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/.gitignore +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/.pre-commit-config.yaml +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/CLAUDE.md +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/LICENSE +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/Makefile +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/README.md +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/backends/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/backends/require_clone_strategy.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/backends/require_postgres.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/backends/unsupported_backend.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/databases/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/databases/clone_database.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/databases/drop_database.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/databases/require_quotable_name.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/databases/shape_digest.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/databases/template_database.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/databases/unhashable_shape.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/databases/unusable_base.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/declaration/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/declaration/invalid_shape.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/declaration/order_derivations.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/declaration/order_tables.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/declaration/projection.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/declaration/shape.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/declaration/shape_from_factory.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/declaration/sql_value.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/derivations/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/derivations/after.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/derivations/aligned.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/derivations/copied.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/derivations/derivation.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/derivations/derived.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/derivations/given.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/derivations/offset.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/derivations/per_parent.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/derivations/product.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/derivations/scope.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/distributions/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/distributions/ascending.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/distributions/bounded.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/distributions/categorical.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/distributions/constant.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/distributions/distinct.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/distributions/distribution.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/distributions/sequential.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/distributions/skew.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/distributions/uniform.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/distributions/zipf.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/fixtures/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/fixtures/scale_fixture.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/fixtures/shape_fixture.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/fixtures/skip_unless_postgres.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/generation/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/generation/derivation_queried_database.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/generation/generate_rows.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/generation/refuse_queries.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/invariants/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/invariants/check_invariants.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/invariants/invariant.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/invariants/invariant_violated.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/keys/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/keys/disjoint.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/keys/infer_key_strategy.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/keys/is_disjoint.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/keys/key_function.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/keys/key_strategy.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/keys/md5_keys.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/keys/sequential_keys.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/keys/sql_keys.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/keys/uuid_keys.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/loading/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/loading/apply_statistics_targets.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/loading/build.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/loading/shape_not_empty.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/py.typed +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/relations/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/relations/fan_out_plan.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/relations/paired.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/relations/paired_plan.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/relations/world_changed.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/scaling/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/scaling/scaled_shape.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/scaling/shape_referenced.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/types/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/types/build_result.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/types/canonical.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/types/children_per_parent.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/types/table_result.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/docs/derivations.md +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/docs/index.md +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/docs/invariants.md +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/docs/keys.md +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/docs/projections.md +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/docs/reference.md +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/mkdocs.yml +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/scripts/check-changelog +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/scripts/release-publish.sh +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/backends/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/backends/test_require_clone_strategy.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/backends/test_require_postgres.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/conftest.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/conftest_settings.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/databases/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/databases/test_require_quotable_name.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/databases/test_shape_digest.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/databases/test_template_database.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/test_order_derivations.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/test_order_tables.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/test_projection.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/test_projection_ceiling.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/test_projection_sql_percent.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/test_projection_values.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/test_projection_values_references.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/test_projection_values_uuid.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/test_shape.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/test_shape_from_factory.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/declaration/test_table.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/derivations/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/derivations/test_after.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/derivations/test_after_units.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/derivations/test_aligned.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/derivations/test_canonical_arithmetic.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/derivations/test_derived.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/derivations/test_given.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/derivations/test_per_parent.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/distributions/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/distributions/test_constant.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/distributions/test_sequential.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/distributions/test_skew.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/distributions/test_uniform.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/distributions/test_zipf.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/fixtures/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/fixtures/test_fixtures.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/generation/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/generation/test_generate_rows.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/generation/test_refuse_queries.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/invariants/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/invariants/test_invariant.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/keys/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/keys/test_infer_key_strategy.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/keys/test_is_disjoint.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/keys/test_key_function.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/keys/test_md5_keys.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/keys/test_sequential_keys.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/keys/test_uuid_keys.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/loading/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/loading/test_apply_statistics_targets.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/loading/test_build_derivations.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/loading/test_build_graph.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/loading/test_build_invariants.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/loading/test_build_keys.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/loading/test_build_m2m.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/loading/test_build_portable.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/loading/test_build_projection.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/relations/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/relations/test_paired.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/relations/test_paired_plan.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/test_scale_protocol.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/test_scaled_projection_ceiling.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/test_scaled_world.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/test_scaled_world_disjoint_advice.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/test_scaled_world_leaves_undeclared_tables_alone.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/test_scaled_world_over_a_session_world.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/test_scaled_world_over_disjoint_tables.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/test_scaled_world_pending_trigger_events.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/scaling/test_scaled_world_restores_sequences.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/test_documentation.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/test_package_layout.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/test_reference_documents_the_public_surface.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/test_version.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/testapp/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.0}/tests/types/__init__.py +0 -0
- {django_data_shape-0.22.0 → django_data_shape-0.23.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@
|
|
59
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.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@
|
|
23
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.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@
|
|
40
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.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@
|
|
82
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.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@
|
|
202
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.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@
|
|
260
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
261
261
|
with:
|
|
262
262
|
enable-cache: true
|
|
263
263
|
- name: Install Python 3.13
|
|
@@ -273,7 +273,7 @@ jobs:
|
|
|
273
273
|
--cov-report=html:htmlcov
|
|
274
274
|
- name: Upload coverage artifact
|
|
275
275
|
if: always()
|
|
276
|
-
uses: actions/upload-artifact@
|
|
276
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
277
277
|
with:
|
|
278
278
|
name: coverage-postgres-${{ matrix.django-version }}
|
|
279
279
|
path: |
|
|
@@ -339,7 +339,7 @@ jobs:
|
|
|
339
339
|
fi
|
|
340
340
|
- name: Download canonical coverage artifact
|
|
341
341
|
if: steps.gate.outputs.skip != 'true'
|
|
342
|
-
uses: actions/download-artifact@
|
|
342
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
343
343
|
with:
|
|
344
344
|
# The Postgres job is the only run that reaches every line, so it is
|
|
345
345
|
# the only one whose number means anything. Pinned to one cell of it:
|
|
@@ -428,12 +428,37 @@ jobs:
|
|
|
428
428
|
|
|
429
429
|
tests-passed:
|
|
430
430
|
name: tests
|
|
431
|
+
# always(), never !cancelled(): a skipped job reports a skipped check, and
|
|
432
|
+
# branch protection counts a skipped required check as passing.
|
|
431
433
|
if: always()
|
|
432
434
|
needs: [lint, docs, floor, test, test-postgres]
|
|
433
435
|
runs-on: ubuntu-latest
|
|
434
436
|
steps:
|
|
435
|
-
|
|
437
|
+
# Pass on an exact `success` from every job above, rather than failing on
|
|
438
|
+
# a list of bad results: such a list passes whatever it does not name, and
|
|
439
|
+
# GitHub reports a result it does not document. A job no hosted runner
|
|
440
|
+
# ever picks up ("The job was not acquired by Runner of type hosted even
|
|
441
|
+
# after multiple attempts") shows as cancelled on its own check but
|
|
442
|
+
# reaches `needs` as `abandoned`, and a gate that failed only on `failure`
|
|
443
|
+
# and `cancelled` went green over exactly that while the jobs it guards
|
|
444
|
+
# never ran. A matrix reaches `needs` as one result, and one cell that did
|
|
445
|
+
# not succeed is enough to make it something other than `success`.
|
|
446
|
+
#
|
|
447
|
+
# None of the jobs above has an `if:`, so `skipped` can only mean one of
|
|
448
|
+
# them never ran, and it fails like any other result. A job that gains a
|
|
449
|
+
# legitimate skip gets an exception naming it, never `skipped` accepted
|
|
450
|
+
# from all of them.
|
|
451
|
+
#
|
|
452
|
+
# The results come in through `env` rather than being spliced into the
|
|
453
|
+
# script, and every one is printed, so a red gate says which job and what
|
|
454
|
+
# GitHub reported for it.
|
|
455
|
+
- name: Verify every required job succeeded
|
|
456
|
+
env:
|
|
457
|
+
NEEDS: ${{ toJSON(needs) }}
|
|
436
458
|
run: |
|
|
437
|
-
|
|
438
|
-
|
|
459
|
+
echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
|
|
460
|
+
not_ok="$(echo "$NEEDS" | jq -r '[to_entries[] | select(.value.result != "success") | "\(.key)=\(.value.result)"] | join(" ")')"
|
|
461
|
+
if [ -n "$not_ok" ]; then
|
|
462
|
+
echo "::error::required jobs did not succeed: $not_ok"
|
|
463
|
+
exit 1
|
|
439
464
|
fi
|
|
@@ -72,7 +72,7 @@ jobs:
|
|
|
72
72
|
PGDATABASE: django_data_shape
|
|
73
73
|
steps:
|
|
74
74
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
75
|
-
- uses: astral-sh/setup-uv@
|
|
75
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
76
76
|
with:
|
|
77
77
|
enable-cache: false
|
|
78
78
|
|
|
@@ -7,6 +7,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.23.0] — 2026-10-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`FanOut(max_children=)` caps how many children any one parent gets.** A
|
|
15
|
+
power-law fan-out gives its busiest parent a share that the seed moves by a
|
|
16
|
+
factor of five to twelve, and production's tails are bounded by the business,
|
|
17
|
+
so calibrating a world against production needs a ceiling. The cap applies
|
|
18
|
+
to the integer partition: the rows a capped parent gives up are re-spread
|
|
19
|
+
over the uncapped ones by their weights, a childless parent still gets none,
|
|
20
|
+
and a declaration whose parents cannot hold every row under the cap is
|
|
21
|
+
refused with the arithmetic. It is part of the declaration's digest, so a
|
|
22
|
+
cached uncapped world is never served for a capped one, and `fan_out_sizes`
|
|
23
|
+
reports the capped sizes. `max_children=1` is a one-to-one: a skewed
|
|
24
|
+
fan-out with a childless share is accepted on a unique column under it,
|
|
25
|
+
where without it only `FanOut(Constant(1))` with no childless share was.
|
|
26
|
+
- **Documentation:** the relations page has a table of the busiest parent's
|
|
27
|
+
share of the rows for each `Zipf` exponent at 100, 1,000 and 10,000 parents,
|
|
28
|
+
drawn over twenty seeds. A test re-measures it, so it cannot drift from
|
|
29
|
+
what the partition draws.
|
|
30
|
+
- **Documentation:** the statistics page says that anything writing to a built world leaves its
|
|
31
|
+
statistics stale and shows the `ANALYZE` to run, and how to declare a
|
|
32
|
+
placeholder for a column a later step fills.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **`scaled_world` is annotated `-> Generator[int]` instead of
|
|
37
|
+
`-> Iterator[int]`.** `ty` 0.0.86 warns that a `@contextmanager` function
|
|
38
|
+
annotated with `Iterator` is deprecated. Nothing changes at runtime, and a
|
|
39
|
+
caller's annotation of the context manager it returns is unaffected. The
|
|
40
|
+
protocol's docstring example and the type-level fixtures follow.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **A key named in `parents=` as a string is the parent it spells.** The keys
|
|
45
|
+
read back from the parent table are the primary key's own type, so on
|
|
46
|
+
PostgreSQL a UUID named as the string it was loaded from JSON as found its
|
|
47
|
+
row and was then compared unequal to it, and the refusal said the row did not
|
|
48
|
+
exist -- pointing at a rollback rather than at the spelling. Each named key
|
|
49
|
+
now goes through the primary key field's `to_python` before it is compared,
|
|
50
|
+
for `FanOut(parents=)` and `Paired(parents=)` alike. A value the field cannot
|
|
51
|
+
read as a key is refused naming the table, the field and the value, where it
|
|
52
|
+
used to reach the driver, including from a field whose `to_python` raises
|
|
53
|
+
`ValueError` or `TypeError` rather than Django's `ValidationError`. The
|
|
54
|
+
reading is not taken on trust: on an integer key only the integer or its
|
|
55
|
+
string is taken, so `1.5`, `Decimal("1.9")` or a `uuid.UUID`, which `int()`
|
|
56
|
+
would turn into a row nobody named, is refused, as is a `bool` on any key;
|
|
57
|
+
and one row named in two spellings is refused as a repeat.
|
|
58
|
+
- **`parents=` narrows a UUID-keyed parent on SQLite.** The keys went to the
|
|
59
|
+
database as raw parameters, and SQLite has no binding for a UUID, so naming a
|
|
60
|
+
UUID-keyed parent failed there in every spelling. They are now prepared by
|
|
61
|
+
the primary key field, as the ORM prepares them.
|
|
62
|
+
- **`Paired(parents=)` refuses a repeated key.** It had no repeat check at all,
|
|
63
|
+
so a partner named twice was weighed twice without a word.
|
|
64
|
+
- **A negative or non-finite fan-out size is refused.** Nothing checked the
|
|
65
|
+
sign, so `Uniform(-2, 10)` or a `Skew` with a negative key built a partition
|
|
66
|
+
whose sizes were negative or did not add up to the rows, and an infinite size,
|
|
67
|
+
or finite sizes summing past a float, gave rows to childless parents or
|
|
68
|
+
failed inside the arithmetic. Each is now refused naming the table and field.
|
|
69
|
+
- **`fan_out_sizes` raises `WorldChanged` for a parent table that was emptied
|
|
70
|
+
since the build.** A declared parent with no rows left was refused as
|
|
71
|
+
`InvalidShape` saying the parent has no rows, before the check that names a
|
|
72
|
+
moved world could run, so a transactional test truncating a session world --
|
|
73
|
+
a cause the docstring names -- was told its declaration was wrong. Any
|
|
74
|
+
refusal of the partition over a declared parent that no longer holds the
|
|
75
|
+
declared rows is now `WorldChanged`, with the refusal chained as its cause.
|
|
76
|
+
`WorldChanged` is not an `InvalidShape`, so a caller catching `InvalidShape`
|
|
77
|
+
for that case no longer catches it.
|
|
78
|
+
|
|
10
79
|
## [0.22.0] — 2026-10-07
|
|
11
80
|
|
|
12
81
|
### Added
|
|
@@ -1682,7 +1751,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
1682
1751
|
into `COPY FROM STDIN`, which psycopg 2 cannot do without materialising them
|
|
1683
1752
|
first.
|
|
1684
1753
|
|
|
1685
|
-
[Unreleased]: https://github.com/Artui/django-data-shape/compare/v0.
|
|
1754
|
+
[Unreleased]: https://github.com/Artui/django-data-shape/compare/v0.23.0...HEAD
|
|
1755
|
+
[0.23.0]: https://github.com/Artui/django-data-shape/compare/v0.22.0...v0.23.0
|
|
1686
1756
|
[0.22.0]: https://github.com/Artui/django-data-shape/compare/v0.21.0...v0.22.0
|
|
1687
1757
|
[0.21.0]: https://github.com/Artui/django-data-shape/compare/v0.20.0...v0.21.0
|
|
1688
1758
|
[0.20.0]: https://github.com/Artui/django-data-shape/compare/v0.19.0...v0.20.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: django-data-shape
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.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
|
|
@@ -243,8 +243,8 @@ def _check_a_fan_out_beside_a_draw(
|
|
|
243
243
|
:class:`~django_data_shape.distributions.sequential.Sequential` beside a
|
|
244
244
|
fan-out keeps the constraint in every row with nothing arranged around it. A
|
|
245
245
|
partition that gives no parent two rows is the second -- there is then no
|
|
246
|
-
group for two draws to collide inside -- and :func:`_cannot_collide` is
|
|
247
|
-
four conditions
|
|
246
|
+
group for two draws to collide inside -- and :func:`_cannot_collide` is what
|
|
247
|
+
establishes it, by a cap of one or by four conditions together. Neither exemption is "this usually works":
|
|
248
248
|
a shape that merely usually works is the case this refusal is for, and the
|
|
249
249
|
measured ones sit at ten and eleven times out of twenty.
|
|
250
250
|
|
|
@@ -330,9 +330,10 @@ def _check_a_fan_out_on_a_unique_column(table: Table, rows_of: dict[type[Model],
|
|
|
330
330
|
"table's rows over the parent's keys by weight, so a skew putting two rows on one "
|
|
331
331
|
"parent is what it was asked for -- and the unique index then refuses the second, "
|
|
332
332
|
"inside COPY, at a row number that moves when the seed does. A one-to-one is filled "
|
|
333
|
-
"by
|
|
334
|
-
"
|
|
335
|
-
"
|
|
333
|
+
"by max_children=1, which caps every parent at one row whatever the sizes weigh and "
|
|
334
|
+
"leaves childless= free to ask for parents with no row at all; or by "
|
|
335
|
+
"FanOut(Constant(1)) with no childless share over at least as many parents as this "
|
|
336
|
+
"table has rows."
|
|
336
337
|
)
|
|
337
338
|
|
|
338
339
|
|
|
@@ -409,7 +410,13 @@ def _cannot_collide(table: Table, column: str, rows_of: dict[type[Model], int])
|
|
|
409
410
|
partition in which no group holds two rows cannot produce one, whatever the
|
|
410
411
|
other column draws and whatever the seed.
|
|
411
412
|
|
|
412
|
-
|
|
413
|
+
The first way is a cap of one. ``max_children=1`` bounds the partition
|
|
414
|
+
itself, so no parent holds two rows whatever the sizes weigh, whatever the
|
|
415
|
+
childless share, and wherever the parents come from; a declaration whose
|
|
416
|
+
rows outnumber the parents that can hold them is refused when the partition
|
|
417
|
+
is drawn, naming the arithmetic, before a row reaches ``COPY``.
|
|
418
|
+
|
|
419
|
+
Without it, four conditions, and all four are needed. The sizes have to be provably flat
|
|
413
420
|
-- ``Bounded`` with exactly one distinct value, so every parent is weighed
|
|
414
421
|
the same -- because ``_sizes`` divides ``rows`` by a total and hands the
|
|
415
422
|
remainder to the parents with the largest fractional part: flat weights make
|
|
@@ -439,7 +446,7 @@ def _cannot_collide(table: Table, column: str, rows_of: dict[type[Model], int])
|
|
|
439
446
|
|
|
440
447
|
.. warning::
|
|
441
448
|
|
|
442
|
-
**The coverage gate cannot see these
|
|
449
|
+
**The coverage gate cannot see these five conditions, and the tests are
|
|
443
450
|
what hold them.** They are one boolean expression, so they are one branch
|
|
444
451
|
arc: drop any single conjunct and the suite still reports 100% line and
|
|
445
452
|
branch, because the surviving expression is still taken both ways. The
|
|
@@ -447,6 +454,9 @@ def _cannot_collide(table: Table, column: str, rows_of: dict[type[Model], int])
|
|
|
447
454
|
against a tree with that condition removed. If you edit this expression,
|
|
448
455
|
edit those with it -- they are, in order:
|
|
449
456
|
|
|
457
|
+
- ``max_children == 1`` --
|
|
458
|
+
``test_a_cap_of_one_is_a_proof_whatever_the_sizes``, beside the
|
|
459
|
+
``test_a_cap_of_two_is_not`` that keeps the comparison from widening.
|
|
450
460
|
- ``Bounded`` with one value -- ``test_sizes_that_are_not_provably_flat_are_not_exempt``,
|
|
451
461
|
which uses ``Uniform(1, 10)`` sizes because they give a largest group of
|
|
452
462
|
two where ``Constant(1)`` gives one, at identical numbers.
|
|
@@ -460,7 +470,7 @@ def _cannot_collide(table: Table, column: str, rows_of: dict[type[Model], int])
|
|
|
460
470
|
"""
|
|
461
471
|
fan_out = cast("FanOut", table.fields[column])
|
|
462
472
|
parent_rows = _parents_available(table, column, fan_out, rows_of)
|
|
463
|
-
return (
|
|
473
|
+
return fan_out.max_children == 1 or (
|
|
464
474
|
isinstance(fan_out.sizes, Bounded)
|
|
465
475
|
and fan_out.sizes.distinct_values() == 1
|
|
466
476
|
and not fan_out.childless
|
{django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/declaration/table.py
RENAMED
|
@@ -692,8 +692,8 @@ class Table:
|
|
|
692
692
|
f"{because} A relation is declared as "
|
|
693
693
|
f"a fan-out over the parent's real keys -- {required_relations[0]}=FanOut(Zipf()) "
|
|
694
694
|
"for the realistic heavy tail, FanOut(Uniform(1, 10)) for something flatter, and "
|
|
695
|
-
"FanOut(
|
|
696
|
-
"
|
|
695
|
+
"FanOut(Zipf(), max_children=1) for a one-to-one. childless= is the share of "
|
|
696
|
+
"parents nobody references. The parent's "
|
|
697
697
|
"own table has to be built in the same shape or already loaded, because that is "
|
|
698
698
|
"where the keys are read from."
|
|
699
699
|
)
|
{django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/relations/fan_out.py
RENAMED
|
@@ -48,6 +48,23 @@ class FanOut:
|
|
|
48
48
|
cosmetic. Emitting them parent by parent gives a perfectly clustered table
|
|
49
49
|
that no production system has and that flatters every index scan; the
|
|
50
50
|
default interleaves them the way rows really arrive.
|
|
51
|
+
|
|
52
|
+
``max_children`` is a ceiling on any one parent's share. **It lives here and
|
|
53
|
+
not on the size distribution** because the weights are normalised: capping
|
|
54
|
+
a ``Zipf`` weight caps nothing, since the realised count is a share of
|
|
55
|
+
``rows`` that the other weights decide. The cap applies to the integer
|
|
56
|
+
partition, and the rows a capped parent gives up are re-spread over the
|
|
57
|
+
others by their weights; a childless parent still gets none. It exists for
|
|
58
|
+
calibrating against production, whose tails the business bounds. **A cap
|
|
59
|
+
flattens the head into the body**, which the planner sees, so a capped
|
|
60
|
+
fan-out is a different world from the uncapped one rather than a free
|
|
61
|
+
improvement on it -- and choosing a ``Zipf`` exponent that already gives
|
|
62
|
+
production's maximum is often the better fix.
|
|
63
|
+
|
|
64
|
+
The cap counts every row the partition places, the ones ``null`` then
|
|
65
|
+
leaves without a parent included, so a parent at the cap can end with fewer
|
|
66
|
+
non-null children but never more. A cap of one is a one-to-one, whatever
|
|
67
|
+
the sizes weigh.
|
|
51
68
|
"""
|
|
52
69
|
|
|
53
70
|
def __init__(
|
|
@@ -58,6 +75,7 @@ class FanOut:
|
|
|
58
75
|
childless: float = 0.0,
|
|
59
76
|
null: float = 0.0,
|
|
60
77
|
placement: str = "arrival",
|
|
78
|
+
max_children: int | None = None,
|
|
61
79
|
) -> None:
|
|
62
80
|
for name, share in (("childless", childless), ("null", null)):
|
|
63
81
|
if not 0.0 <= share < 1.0:
|
|
@@ -68,6 +86,15 @@ class FanOut:
|
|
|
68
86
|
raise InvalidShape(
|
|
69
87
|
f"FanOut placement must be one of {', '.join(PLACEMENTS)}, got {placement!r}."
|
|
70
88
|
)
|
|
89
|
+
# bool is an int, and a cap of one spelled True has almost certainly
|
|
90
|
+
# been written for a different argument.
|
|
91
|
+
if max_children is not None and (
|
|
92
|
+
isinstance(max_children, bool) or not isinstance(max_children, int) or max_children < 1
|
|
93
|
+
):
|
|
94
|
+
raise InvalidShape(
|
|
95
|
+
f"FanOut max_children is the most children any one parent may have, so it must "
|
|
96
|
+
f"be a whole number of at least 1, got {max_children!r}."
|
|
97
|
+
)
|
|
71
98
|
narrowed = None if parents is None else tuple(parents)
|
|
72
99
|
if narrowed is not None:
|
|
73
100
|
# An empty sequence is falsy, and reading it as "not declared" would
|
|
@@ -97,6 +124,7 @@ class FanOut:
|
|
|
97
124
|
self._childless = childless
|
|
98
125
|
self._null = null
|
|
99
126
|
self._placement = placement
|
|
127
|
+
self._max_children = max_children
|
|
100
128
|
|
|
101
129
|
@property
|
|
102
130
|
def sizes(self) -> Distribution:
|
|
@@ -118,6 +146,10 @@ class FanOut:
|
|
|
118
146
|
def placement(self) -> str:
|
|
119
147
|
return self._placement
|
|
120
148
|
|
|
149
|
+
@property
|
|
150
|
+
def max_children(self) -> int | None:
|
|
151
|
+
return self._max_children
|
|
152
|
+
|
|
121
153
|
def canonical(self) -> object:
|
|
122
154
|
"""The size distribution, the parents it spreads over, and the shares. See ``Canonical``.
|
|
123
155
|
|
|
@@ -125,13 +157,22 @@ class FanOut:
|
|
|
125
157
|
afterwards: two shapes differing only in which parents they cover are
|
|
126
158
|
two different worlds, and the template-database cache keys on this. A
|
|
127
159
|
clone built for one tenant handed to a test asking for another would be
|
|
128
|
-
the cache serving a world nobody declared.
|
|
160
|
+
the cache serving a world nobody declared. ``max_children`` is here for
|
|
161
|
+
the same reason.
|
|
129
162
|
"""
|
|
130
|
-
return (
|
|
163
|
+
return (
|
|
164
|
+
self._sizes,
|
|
165
|
+
self._parents,
|
|
166
|
+
self._childless,
|
|
167
|
+
self._null,
|
|
168
|
+
self._placement,
|
|
169
|
+
self._max_children,
|
|
170
|
+
)
|
|
131
171
|
|
|
132
172
|
def __repr__(self) -> str:
|
|
133
173
|
narrowed = "" if self._parents is None else f"parents={self._parents!r}, "
|
|
174
|
+
capped = "" if self._max_children is None else f", max_children={self._max_children!r}"
|
|
134
175
|
return (
|
|
135
176
|
f"FanOut({self._sizes!r}, {narrowed}childless={self._childless!r}, "
|
|
136
|
-
f"null={self._null!r}, placement={self._placement!r})"
|
|
177
|
+
f"null={self._null!r}, placement={self._placement!r}{capped})"
|
|
137
178
|
)
|
{django_data_shape-0.22.0 → django_data_shape-0.23.0}/django_data_shape/relations/fan_out_sizes.py
RENAMED
|
@@ -104,34 +104,54 @@ def fan_out_sizes(
|
|
|
104
104
|
relation = dict(declaration.relations())[field]
|
|
105
105
|
parent = cast("type[Model]", cast("Field[object, object]", relation).related_model)
|
|
106
106
|
|
|
107
|
-
plan = resolve_fan_out(
|
|
108
|
-
fan_out,
|
|
109
|
-
parent,
|
|
110
|
-
declaration.rows,
|
|
111
|
-
shape.seed,
|
|
112
|
-
declaration.db_table,
|
|
113
|
-
field,
|
|
114
|
-
connections[using],
|
|
115
|
-
)
|
|
116
|
-
keys = plan.parent_keys()
|
|
117
|
-
|
|
118
107
|
# A narrowed fan-out covers the keys it named and no others, so the parent
|
|
119
108
|
# table holding more rows than the partition covers is the declaration
|
|
120
109
|
# working rather than the world moving. The failure WorldChanged exists for
|
|
121
110
|
# is still caught, and caught earlier: a named key that is no longer a row is
|
|
122
|
-
# refused by name when the partition is resolved,
|
|
111
|
+
# refused by name when the partition is resolved, below.
|
|
123
112
|
parent_declaration = (
|
|
124
113
|
None if fan_out.parents is not None else declared.get(parent._meta.db_table)
|
|
125
114
|
)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
115
|
+
try:
|
|
116
|
+
plan = resolve_fan_out(
|
|
117
|
+
fan_out,
|
|
118
|
+
parent,
|
|
119
|
+
declaration.rows,
|
|
120
|
+
shape.seed,
|
|
121
|
+
declaration.db_table,
|
|
122
|
+
field,
|
|
123
|
+
connections[using],
|
|
135
124
|
)
|
|
125
|
+
except InvalidShape as refused:
|
|
126
|
+
# A partition the build drew can still be refused here, over parents
|
|
127
|
+
# that have since gone: fewer of them can no longer hold the rows under
|
|
128
|
+
# max_children, or none of them is left. Blaming the declaration then
|
|
129
|
+
# sends the reader to change a shape that built, so a parent table that
|
|
130
|
+
# no longer holds what the shape declares is named as the cause.
|
|
131
|
+
if isinstance(parent_declaration, Table):
|
|
132
|
+
holds = parent._base_manager.using(using).count()
|
|
133
|
+
if holds != parent_declaration.rows:
|
|
134
|
+
raise _world_changed(
|
|
135
|
+
parent, holds, parent_declaration.rows, model, field
|
|
136
|
+
) from refused
|
|
137
|
+
raise
|
|
138
|
+
keys = plan.parent_keys()
|
|
139
|
+
|
|
140
|
+
if isinstance(parent_declaration, Table) and len(keys) != parent_declaration.rows:
|
|
141
|
+
raise _world_changed(parent, len(keys), parent_declaration.rows, model, field)
|
|
136
142
|
|
|
137
143
|
return ChildrenPerParent(keys, plan.sizes(), fan_out.null)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _world_changed(
|
|
147
|
+
parent: type[Model], holds: int, declares: int, model: type[Model], field: str
|
|
148
|
+
) -> WorldChanged:
|
|
149
|
+
return WorldChanged(
|
|
150
|
+
f"{parent.__name__} holds {holds} rows and this shape declares "
|
|
151
|
+
f"{declares}, so the fan-out for {model.__name__}.{field} would be "
|
|
152
|
+
"spread over parents the children were never spread across. The counts are derived "
|
|
153
|
+
"from the parent keys rather than counted off the child table, so an answer here "
|
|
154
|
+
"would describe a world that was never built. The usual causes are a test that "
|
|
155
|
+
"created more parents before asking, and a transactional test that truncated the "
|
|
156
|
+
"tables a session-scoped fixture built."
|
|
157
|
+
)
|