plain.postgres 0.109.0__tar.gz → 0.110.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.
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/PKG-INFO +1 -1
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/CHANGELOG.md +14 -3
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/base.py +5 -8
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/related.py +29 -76
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/related_descriptors.py +3 -31
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/related_lookups.py +24 -18
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/related_managers.py +32 -43
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/reverse_related.py +6 -56
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/forms.py +1 -1
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/lookups.py +14 -9
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/meta.py +1 -2
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/autodetector.py +4 -11
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/query_utils.py +1 -1
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/sql/compiler.py +1 -1
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/sql/datastructures.py +8 -8
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/sql/query.py +10 -13
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/pyproject.toml +1 -1
- plain_postgres-0.110.0/tests/public/test_related_instance_filter.py +89 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/.gitignore +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/CLAUDE.md +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/LICENSE +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/README.md +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/README.md +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/adapters.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/cli/decorators.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/cli/sync.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/connection.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/constraints.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/convergence/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/convergence/analysis.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/convergence/fixes.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/convergence/planning.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/dialect.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/base.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/binary.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/boolean.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/duration.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/encrypted.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/network.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/numeric.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/primary_key.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/temporal.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/text.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/timezones.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/uuid.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/indexes.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/introspection/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/introspection/health/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/introspection/health/context.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/introspection/health/helpers.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/introspection/health/ownership.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/introspection/health/runner.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/introspection/health/types.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/introspection/schema.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/middleware.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/state.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/preflight/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/preflight/database.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/preflight/indexes.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/preflight/models.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/query.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/schema.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/sources.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/test/database.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/types.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/__init__.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/constraints.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/defaults.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/delete.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/encrypted.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/forms.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/indexes.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/iteration.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/mixins.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/nullability.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/querysets.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/relationships.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/storage_parameters.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/trees.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/urls.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/conftest.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_autodetector_not_null_errors.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_autodetector_type_change.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_connection_isolation.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_connection_pool.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_connection_self_heal.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_constraint_violation_error.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_convergence.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_convergence_constraints.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_convergence_defaults.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_convergence_fk.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_convergence_indexes.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_convergence_nullability.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_convergence_timeouts.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_db_expression_defaults.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_diagnose.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_executor_connection_hook.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_fk_characterization.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_health.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_introspection.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_literal_default_persistence.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_management_connection.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_migration_executor.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_otel_metrics.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/internal/test_schema_timeouts.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_create_update.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_database_url.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_deferred_loading.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_delete_behaviors.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_encrypted_fields.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_exceptions.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_field_defaults.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_functions_uuid.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_integrity_error_mapping.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_iterator.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_m2m.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_manager_assignment.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_manual_pk.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_mixins.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_modelform_roundtrip.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_queryset_repr.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_random_string_field.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_raw_query.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_read_only_transactions.py +0 -0
- {plain_postgres-0.109.0 → plain_postgres-0.110.0}/tests/public/test_related.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.110.0](https://github.com/dropseed/plain/releases/plain-postgres@0.110.0) (2026-06-17)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Internal: the foreign-key/related-field internals are collapsed to the single-column-to-`id` model Plain actually uses, removing the inherited Django composite-key and `to_field` scaffolding. `PathInfo.target_fields` (a tuple) becomes a scalar `target_field`, and a `Join` now holds one `join_col` pair instead of a `join_cols` list. The unused multi-column helpers go with it: `RelatedField.resolve_related_fields()`, the `related_fields`/`local_related_fields`/`foreign_related_fields` properties, `ForeignKeyField.get_reverse_joining_columns()`, and `ForeignObjectRel.get_related_field()` / `field_name` / `set_field_name()` / `db_type` / the `multiple` flag (along with its now-dead reverse-cache priming, left over from the long-removed one-to-one relation). `get_local_related_value()`, `get_foreign_related_value()`, and the join-column accessors now return scalars instead of 1-tuples. ([cbd15fb707](https://github.com/dropseed/plain/commit/cbd15fb707))
|
|
8
|
+
- Internal: the related-lookup prep path (`related_lookups.py`, `lookups.py`) now narrows on `RelatedField | ForeignObjectRel` to find a relation's target field instead of duck-typing with `getattr(output_field, "target_field")` / `hasattr(..., "path_infos")`, so the dependency is greppable, type-checked, and fails loudly if it's ever removed rather than silently falling back. ([cbd15fb707](https://github.com/dropseed/plain/commit/cbd15fb707))
|
|
9
|
+
|
|
10
|
+
### Upgrade instructions
|
|
11
|
+
|
|
12
|
+
- No changes required. These are internal ORM query-construction details — how foreign keys, reverse relations, and many-to-many relations behave (filtering, prefetching, related-manager access) is unchanged.
|
|
13
|
+
|
|
3
14
|
## [0.109.0](https://github.com/dropseed/plain/releases/plain-postgres@0.109.0) (2026-06-17)
|
|
4
15
|
|
|
5
16
|
### What's changed
|
|
@@ -14,7 +25,7 @@
|
|
|
14
25
|
- Remove any `db_constraint=False` arguments from `ForeignKeyField`. Foreign keys are always DB-constrained now; an unconstrained FK (e.g. for soft/cross-system references) is no longer supported.
|
|
15
26
|
- Remove `limit_choices_to=` from `ForeignKeyField` / `ManyToManyField` definitions. To constrain the rows offered for a relation, filter the related queryset explicitly in your form or view.
|
|
16
27
|
- Replace any `QuerySet.complex_filter(...)` calls with `.filter(...)` — pass a dict of lookups or a `Q` object directly.
|
|
17
|
-
- Most projects use none of these and need no changes.
|
|
28
|
+
- Most projects use none of these and need no changes.
|
|
18
29
|
|
|
19
30
|
## [0.108.1](https://github.com/dropseed/plain/releases/plain-postgres@0.108.1) (2026-06-09)
|
|
20
31
|
|
|
@@ -57,7 +68,7 @@
|
|
|
57
68
|
|
|
58
69
|
### Upgrade instructions
|
|
59
70
|
|
|
60
|
-
- Replace `form.save()` with `form.create()` in create flows and `form.update()` in update flows.
|
|
71
|
+
- Replace `form.save()` with `form.create()` in create flows and `form.update()` in update flows.
|
|
61
72
|
- `form.update(fields=[...])` is available to limit the columns written (a pass-through to `Model.update(fields=...)`).
|
|
62
73
|
- For the old `form.save(commit=False)` pattern (set extra non-form attributes, then save), assign those attributes on `form.instance` after `is_valid()`, then call `form.create()` / `form.update()` — m2m data is still saved for you, so the separate `save_m2m()` step is no longer needed:
|
|
63
74
|
|
|
@@ -79,7 +90,7 @@
|
|
|
79
90
|
|
|
80
91
|
### Upgrade instructions
|
|
81
92
|
|
|
82
|
-
- Replace `obj.save()` with `obj.create()` for a new row or `obj.update()` for an existing one; `obj.save(update_fields=[...])` becomes `obj.update(fields=[...])`.
|
|
93
|
+
- Replace `obj.save()` with `obj.create()` for a new row or `obj.update()` for an existing one; `obj.save(update_fields=[...])` becomes `obj.update(fields=[...])`.
|
|
83
94
|
- Remove `force_insert=`/`force_update=` — `create()` and `update()` are the explicit forms.
|
|
84
95
|
- Don't pass `id=` to a model constructor; load existing rows with `Model.query.get(id=...)`.
|
|
85
96
|
- Catch `ValidationError` (or `(psycopg.IntegrityError, ValidationError)` when retrying) for unique/check conflicts on instance writes. Inside an open `transaction.atomic()` a violation aborts the transaction — wrap the write in its own `atomic()` to catch and continue.
|
|
@@ -538,9 +538,6 @@ class Model(metaclass=ModelBase):
|
|
|
538
538
|
# constraints aren't supported by the database, there's the
|
|
539
539
|
# unavoidable risk of data corruption.
|
|
540
540
|
if obj.id is None:
|
|
541
|
-
# Remove the object from a related instance cache.
|
|
542
|
-
if not field.remote_field.multiple:
|
|
543
|
-
field.remote_field.delete_cached_value(obj)
|
|
544
541
|
raise ValueError(
|
|
545
542
|
f"{operation_name}() prohibited to prevent data loss due to unsaved "
|
|
546
543
|
f"related object '{field.name}'."
|
|
@@ -549,10 +546,10 @@ class Model(metaclass=ModelBase):
|
|
|
549
546
|
# Set related object if it has been saved after an
|
|
550
547
|
# assignment.
|
|
551
548
|
setattr(self, field.name, obj)
|
|
552
|
-
# If the relationship's
|
|
553
|
-
#
|
|
554
|
-
#
|
|
555
|
-
#
|
|
549
|
+
# If the relationship's key was changed, clear the cached
|
|
550
|
+
# relationship. Compare the cached object's key against the raw
|
|
551
|
+
# key value -- not getattr(self, field.name), which for a
|
|
552
|
+
# foreign key returns the related object, not the key.
|
|
556
553
|
if getattr(obj, field.target_field.name) != field.value_from_object(
|
|
557
554
|
self
|
|
558
555
|
):
|
|
@@ -622,7 +619,7 @@ class Model(metaclass=ModelBase):
|
|
|
622
619
|
raise ValueError(
|
|
623
620
|
f"Unsaved model instance {self!r} cannot be used in an ORM query."
|
|
624
621
|
)
|
|
625
|
-
return getattr(self, field.
|
|
622
|
+
return getattr(self, field.target_field.name)
|
|
626
623
|
|
|
627
624
|
def clean(self) -> None:
|
|
628
625
|
"""
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any, Self, cast
|
|
|
7
7
|
|
|
8
8
|
from plain.postgres.constants import LOOKUP_SEP
|
|
9
9
|
from plain.postgres.deletion import SET_NULL, OnDelete
|
|
10
|
-
from plain.postgres.exceptions import FieldDoesNotExist
|
|
10
|
+
from plain.postgres.exceptions import FieldDoesNotExist
|
|
11
11
|
from plain.postgres.query_utils import PathInfo
|
|
12
12
|
from plain.postgres.utils import make_model_tuple
|
|
13
13
|
from plain.preflight import PreflightResult
|
|
@@ -277,7 +277,6 @@ class RelatedField(FieldCacheMixin, Field):
|
|
|
277
277
|
self.name = self.name or (
|
|
278
278
|
self.remote_field.model.model_options.model_name + "_" + "id"
|
|
279
279
|
)
|
|
280
|
-
self.remote_field.set_field_name()
|
|
281
280
|
|
|
282
281
|
def do_related_class(self, other: type[Model], cls: type[Model]) -> None:
|
|
283
282
|
self.set_attributes_from_rel()
|
|
@@ -294,16 +293,12 @@ class RelatedField(FieldCacheMixin, Field):
|
|
|
294
293
|
@property
|
|
295
294
|
def target_field(self) -> Field:
|
|
296
295
|
"""
|
|
297
|
-
|
|
298
|
-
|
|
296
|
+
The remote field this relation filters against. Read dynamically by the
|
|
297
|
+
related-lookup prep path (`output_field.target_field`). ForeignKeyField
|
|
298
|
+
overrides this with a direct, cached lookup; ManyToManyField uses this
|
|
299
|
+
path-based form.
|
|
299
300
|
"""
|
|
300
|
-
|
|
301
|
-
if len(target_fields) > 1:
|
|
302
|
-
raise FieldError(
|
|
303
|
-
"The relation has multiple target fields, but only single target field "
|
|
304
|
-
"was asked for"
|
|
305
|
-
)
|
|
306
|
-
return target_fields[0]
|
|
301
|
+
return self.path_infos[-1].target_field
|
|
307
302
|
|
|
308
303
|
def get_cache_name(self) -> str:
|
|
309
304
|
assert self.name is not None, "Field name must be set"
|
|
@@ -372,49 +367,28 @@ class ForeignKeyField(ColumnField, RelatedField):
|
|
|
372
367
|
|
|
373
368
|
def __copy__(self) -> ForeignKeyField:
|
|
374
369
|
obj = super().__copy__()
|
|
375
|
-
# Remove
|
|
370
|
+
# Remove cached values that depend on the (possibly not-yet-resolved)
|
|
371
|
+
# remote model.
|
|
372
|
+
obj.__dict__.pop("target_field", None)
|
|
376
373
|
obj.__dict__.pop("path_infos", None)
|
|
377
374
|
obj.__dict__.pop("reverse_path_infos", None)
|
|
378
375
|
return obj
|
|
379
376
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
377
|
+
def get_local_related_value(self, instance: Model) -> Any:
|
|
378
|
+
# The local field is the foreign key itself; read its raw key value, not
|
|
379
|
+
# the related object the descriptor returns.
|
|
380
|
+
return self._get_raw_value(instance)
|
|
383
381
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return
|
|
382
|
+
def get_foreign_related_value(self, instance: Model) -> Any:
|
|
383
|
+
# A foreign key always points at the remote instance's id.
|
|
384
|
+
return instance.id
|
|
387
385
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
rhs_field for lhs_field, rhs_field in self.related_fields if rhs_field
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
def get_local_related_value(self, instance: Model) -> tuple[Any, ...]:
|
|
395
|
-
# The single local field is the foreign key itself.
|
|
396
|
-
if self.primary_key:
|
|
397
|
-
return (instance.id,)
|
|
398
|
-
# Read the raw key value, not the related object the descriptor returns.
|
|
399
|
-
return (self._get_raw_value(instance),)
|
|
400
|
-
|
|
401
|
-
def get_foreign_related_value(self, instance: Model) -> tuple[Any, ...]:
|
|
402
|
-
# Always returns the id of the foreign instance
|
|
403
|
-
return (instance.id,)
|
|
404
|
-
|
|
405
|
-
def get_joining_columns(
|
|
406
|
-
self, reverse_join: bool = False
|
|
407
|
-
) -> tuple[tuple[str, str], ...]:
|
|
408
|
-
# Always returns a single column pair
|
|
386
|
+
def get_joining_columns(self, reverse_join: bool = False) -> tuple[str, str]:
|
|
387
|
+
# A foreign key joins one column pair: this field's column and the
|
|
388
|
+
# target's id column.
|
|
409
389
|
if reverse_join:
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
else:
|
|
413
|
-
from_field, to_field = self.related_fields[0]
|
|
414
|
-
return ((from_field.column, to_field.column),)
|
|
415
|
-
|
|
416
|
-
def get_reverse_joining_columns(self) -> tuple[tuple[str, str], ...]:
|
|
417
|
-
return self.get_joining_columns(reverse_join=True)
|
|
390
|
+
return (self.target_field.column, self.column)
|
|
391
|
+
return (self.column, self.target_field.column)
|
|
418
392
|
|
|
419
393
|
def get_path_info(self, filtered_relation: Any = None) -> list[PathInfo]:
|
|
420
394
|
"""Get path from this field to the related model."""
|
|
@@ -424,7 +398,7 @@ class ForeignKeyField(ColumnField, RelatedField):
|
|
|
424
398
|
PathInfo(
|
|
425
399
|
from_meta=from_meta,
|
|
426
400
|
to_meta=meta,
|
|
427
|
-
|
|
401
|
+
target_field=self.target_field,
|
|
428
402
|
join_field=self,
|
|
429
403
|
m2m=False,
|
|
430
404
|
direct=True,
|
|
@@ -444,9 +418,10 @@ class ForeignKeyField(ColumnField, RelatedField):
|
|
|
444
418
|
PathInfo(
|
|
445
419
|
from_meta=from_meta,
|
|
446
420
|
to_meta=meta,
|
|
447
|
-
|
|
421
|
+
target_field=meta.get_forward_field("id"),
|
|
448
422
|
join_field=self.remote_field,
|
|
449
|
-
|
|
423
|
+
# The reverse of a foreign key always fans out to many rows.
|
|
424
|
+
m2m=True,
|
|
450
425
|
direct=False,
|
|
451
426
|
filtered_relation=filtered_relation,
|
|
452
427
|
)
|
|
@@ -500,26 +475,14 @@ class ForeignKeyField(ColumnField, RelatedField):
|
|
|
500
475
|
def to_python(self, value: Any) -> Any:
|
|
501
476
|
return self.target_field.to_python(value)
|
|
502
477
|
|
|
503
|
-
@
|
|
478
|
+
@cached_property
|
|
504
479
|
def target_field(self) -> Field:
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
def resolve_related_fields(self) -> list[tuple[ForeignKeyField, Field]]:
|
|
480
|
+
"""A foreign key points at exactly one column: the remote model's id."""
|
|
508
481
|
if isinstance(self.remote_field.model, str):
|
|
509
482
|
raise ValueError(
|
|
510
483
|
f"Related model {self.remote_field.model!r} cannot be resolved"
|
|
511
484
|
)
|
|
512
|
-
|
|
513
|
-
to_field = self.remote_field.model._model_meta.get_forward_field("id")
|
|
514
|
-
related_fields: list[tuple[ForeignKeyField, Field]] = [(from_field, to_field)]
|
|
515
|
-
|
|
516
|
-
for from_field, to_field in related_fields:
|
|
517
|
-
if to_field and to_field.model != self.remote_field.model:
|
|
518
|
-
raise FieldError(
|
|
519
|
-
f"'{self.model.model_options.label}.{self.name}' refers to field '{to_field.name}' which is not local to model "
|
|
520
|
-
f"'{self.remote_field.model.model_options.label}'."
|
|
521
|
-
)
|
|
522
|
-
return related_fields
|
|
485
|
+
return self.remote_field.model._model_meta.get_forward_field("id")
|
|
523
486
|
|
|
524
487
|
def set_attributes_from_name(self, name: str) -> None:
|
|
525
488
|
super().set_attributes_from_name(name)
|
|
@@ -574,11 +537,8 @@ class ForeignKeyField(ColumnField, RelatedField):
|
|
|
574
537
|
|
|
575
538
|
def get_col(self, alias: str | None, output_field: Field | None = None) -> Any:
|
|
576
539
|
if output_field is None:
|
|
540
|
+
# A foreign key resolves to its target's id column.
|
|
577
541
|
output_field = self.target_field
|
|
578
|
-
while isinstance(output_field, ForeignKeyField):
|
|
579
|
-
output_field = output_field.target_field
|
|
580
|
-
if output_field is self:
|
|
581
|
-
raise ValueError("Cannot resolve output_field.")
|
|
582
542
|
return super().get_col(alias, output_field)
|
|
583
543
|
|
|
584
544
|
|
|
@@ -1048,13 +1008,6 @@ class ManyToManyField(RelatedField):
|
|
|
1048
1008
|
self._get_m2m_reverse_attr, related, "name"
|
|
1049
1009
|
)
|
|
1050
1010
|
|
|
1051
|
-
get_m2m_rel = partial(self._get_m2m_attr, related, "remote_field")
|
|
1052
|
-
self.m2m_target_field_name = lambda: get_m2m_rel().field_name
|
|
1053
|
-
get_m2m_reverse_rel = partial(
|
|
1054
|
-
self._get_m2m_reverse_attr, related, "remote_field"
|
|
1055
|
-
)
|
|
1056
|
-
self.m2m_reverse_target_field_name = lambda: get_m2m_reverse_rel().field_name
|
|
1057
|
-
|
|
1058
1011
|
def set_attributes_from_rel(self) -> None:
|
|
1059
1012
|
pass
|
|
1060
1013
|
|
{plain_postgres-0.109.0 → plain_postgres-0.110.0}/plain/postgres/fields/related_descriptors.py
RENAMED
|
@@ -23,11 +23,6 @@ reverse foreign key relation.
|
|
|
23
23
|
1. Related instance on the forward side of a foreign key relation:
|
|
24
24
|
``ForwardForeignKeyDescriptor``.
|
|
25
25
|
|
|
26
|
-
Uniqueness of foreign key values is irrelevant to accessing the related
|
|
27
|
-
instance, making the many-to-one and one-to-one cases identical as far as
|
|
28
|
-
the descriptor is concerned. The constraint is checked upstream (unicity
|
|
29
|
-
validation in forms) or downstream (unique indexes in the database).
|
|
30
|
-
|
|
31
26
|
2. Related objects manager for related instances on the forward or reverse
|
|
32
27
|
sides of a many-to-many relation: ``ForwardManyToManyDescriptor``.
|
|
33
28
|
|
|
@@ -94,22 +89,14 @@ class ForwardForeignKeyDescriptor:
|
|
|
94
89
|
|
|
95
90
|
rel_obj_attr = self.field.get_foreign_related_value
|
|
96
91
|
instance_attr = self.field.get_local_related_value
|
|
97
|
-
|
|
98
|
-
related_field = self.field.foreign_related_fields[0]
|
|
99
|
-
remote_field = self.field.remote_field
|
|
92
|
+
related_field = self.field.target_field
|
|
100
93
|
|
|
101
94
|
# A foreign key is single-column, so prefetch with a join-less IN query.
|
|
102
95
|
query = {
|
|
103
|
-
f"{related_field.name}__in": {instance_attr(inst)
|
|
96
|
+
f"{related_field.name}__in": {instance_attr(inst) for inst in instances}
|
|
104
97
|
}
|
|
105
98
|
queryset = queryset.filter(**query)
|
|
106
99
|
|
|
107
|
-
# Since we're going to assign directly in the cache,
|
|
108
|
-
# we must manage the reverse relation cache manually.
|
|
109
|
-
if not remote_field.multiple:
|
|
110
|
-
for rel_obj in queryset:
|
|
111
|
-
instance = instances_dict[rel_obj_attr(rel_obj)]
|
|
112
|
-
remote_field.set_cached_value(rel_obj, instance)
|
|
113
100
|
return (
|
|
114
101
|
queryset,
|
|
115
102
|
rel_obj_attr,
|
|
@@ -152,9 +139,6 @@ class ForwardForeignKeyDescriptor:
|
|
|
152
139
|
# no query. Accessing any other field triggers the full-row
|
|
153
140
|
# deferred load.
|
|
154
141
|
rel_obj = remote_model.from_db([target_name], [pk_value])
|
|
155
|
-
# For a one-to-one relation, prime the reverse cache too.
|
|
156
|
-
if not self.field.remote_field.multiple:
|
|
157
|
-
self.field.remote_field.set_cached_value(rel_obj, instance)
|
|
158
142
|
self.field.set_cached_value(instance, rel_obj)
|
|
159
143
|
|
|
160
144
|
# Checked on every access, including a cached None: a non-nullable
|
|
@@ -186,11 +170,6 @@ class ForwardForeignKeyDescriptor:
|
|
|
186
170
|
remote_field = self.field.remote_field
|
|
187
171
|
|
|
188
172
|
if value is None:
|
|
189
|
-
# Clear the reverse cache on any previously-related object so a
|
|
190
|
-
# later delete of it does not cascade into this instance.
|
|
191
|
-
related = self.field.get_cached_value(instance, default=None)
|
|
192
|
-
if related is not None and not remote_field.multiple:
|
|
193
|
-
remote_field.set_cached_value(related, None)
|
|
194
173
|
instance.__dict__[name] = None
|
|
195
174
|
self.field.set_cached_value(instance, None)
|
|
196
175
|
return
|
|
@@ -199,9 +178,6 @@ class ForwardForeignKeyDescriptor:
|
|
|
199
178
|
# A related model instance: store its key, cache the object.
|
|
200
179
|
instance.__dict__[name] = getattr(value, self.field.target_field.name)
|
|
201
180
|
self.field.set_cached_value(instance, value)
|
|
202
|
-
# For a one-to-one relation, prime the reverse cache.
|
|
203
|
-
if not remote_field.multiple:
|
|
204
|
-
remote_field.set_cached_value(value, instance)
|
|
205
181
|
return
|
|
206
182
|
|
|
207
183
|
if isinstance(value, Model | bool):
|
|
@@ -217,12 +193,8 @@ class ForwardForeignKeyDescriptor:
|
|
|
217
193
|
# A bare related key value (e.g. child.parent = 5).
|
|
218
194
|
new_value = self.field.to_python(value)
|
|
219
195
|
if instance.__dict__.get(name) != new_value:
|
|
220
|
-
# The key actually changed --
|
|
221
|
-
# previously-related object and drop the now-stale forward cache.
|
|
196
|
+
# The key actually changed -- drop the now-stale forward cache.
|
|
222
197
|
# Re-storing the same key (e.g. by clean_fields) keeps the cache.
|
|
223
|
-
related = self.field.get_cached_value(instance, default=None)
|
|
224
|
-
if related is not None and not remote_field.multiple:
|
|
225
|
-
remote_field.set_cached_value(related, None)
|
|
226
198
|
if self.field.is_cached(instance):
|
|
227
199
|
self.field.delete_cached_value(instance)
|
|
228
200
|
instance.__dict__[name] = new_value
|
|
@@ -18,36 +18,43 @@ if TYPE_CHECKING:
|
|
|
18
18
|
from plain.postgres.sql.compiler import SQLCompiler
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def
|
|
22
|
-
|
|
21
|
+
def _related_target_field(lhs: Any) -> Any:
|
|
22
|
+
"""The remote field a relational lhs filters against (e.g. the target's id),
|
|
23
|
+
or None when the lhs is not a relation.
|
|
24
|
+
|
|
25
|
+
Narrowing on the relation types keeps this explicit and type-checked rather
|
|
26
|
+
than probing for a ``path_infos`` attribute with hasattr().
|
|
27
|
+
"""
|
|
23
28
|
from plain.postgres.fields.related import RelatedField
|
|
29
|
+
from plain.postgres.fields.reverse_related import ForeignObjectRel
|
|
30
|
+
|
|
31
|
+
output_field = lhs.output_field
|
|
32
|
+
if isinstance(output_field, RelatedField | ForeignObjectRel):
|
|
33
|
+
return output_field.path_infos[-1].target_field
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_normalized_value(value: Any, lhs: Any) -> Any:
|
|
38
|
+
from plain.postgres import Model
|
|
24
39
|
|
|
25
40
|
if isinstance(value, Model):
|
|
26
41
|
if value.id is None:
|
|
27
42
|
raise ValueError("Model instances passed to related filters must be saved.")
|
|
28
|
-
# FK relations are always single-column
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
while not isinstance(value, source.model) and isinstance(source, RelatedField):
|
|
32
|
-
source = source.remote_field.model._model_meta.get_field(
|
|
33
|
-
source.remote_field.field_name
|
|
34
|
-
)
|
|
35
|
-
return (getattr(value, source.name),)
|
|
36
|
-
if not isinstance(value, tuple):
|
|
37
|
-
return (value,)
|
|
43
|
+
# FK relations are always single-column, targeting the remote `id`.
|
|
44
|
+
source = lhs.output_field.path_infos[-1].target_field
|
|
45
|
+
return getattr(value, source.name)
|
|
38
46
|
return value
|
|
39
47
|
|
|
40
48
|
|
|
41
49
|
class RelatedIn(In):
|
|
42
50
|
def get_prep_lookup(self) -> list[Any]:
|
|
43
51
|
if self.rhs_is_direct_value():
|
|
44
|
-
self.rhs = [get_normalized_value(val, self.lhs)
|
|
52
|
+
self.rhs = [get_normalized_value(val, self.lhs) for val in self.rhs]
|
|
45
53
|
# We need to run the related field's get_prep_value(). Consider
|
|
46
54
|
# case ForeignKeyField to IntegerField given value 'abc'. The
|
|
47
55
|
# ForeignKeyField itself doesn't have validation for non-integers,
|
|
48
56
|
# so we must run validation using the target field.
|
|
49
|
-
if
|
|
50
|
-
target_field = self.lhs.output_field.path_infos[-1].target_fields[-1]
|
|
57
|
+
if target_field := _related_target_field(self.lhs):
|
|
51
58
|
self.rhs = [target_field.get_prep_value(v) for v in self.rhs]
|
|
52
59
|
return super().get_prep_lookup()
|
|
53
60
|
|
|
@@ -61,13 +68,12 @@ class RelatedLookupMixin(Lookup):
|
|
|
61
68
|
|
|
62
69
|
def get_prep_lookup(self) -> Any:
|
|
63
70
|
if not hasattr(self.rhs, "resolve_expression"):
|
|
64
|
-
self.rhs = get_normalized_value(self.rhs, self.lhs)
|
|
71
|
+
self.rhs = get_normalized_value(self.rhs, self.lhs)
|
|
65
72
|
# We need to run the related field's get_prep_value(). Consider case
|
|
66
73
|
# ForeignKeyField to IntegerField given value 'abc'. The ForeignKeyField itself
|
|
67
74
|
# doesn't have validation for non-integers, so we must run validation
|
|
68
75
|
# using the target field.
|
|
69
|
-
if self.prepare_rhs and
|
|
70
|
-
target_field = self.lhs.output_field.path_infos[-1].target_fields[-1]
|
|
76
|
+
if self.prepare_rhs and (target_field := _related_target_field(self.lhs)):
|
|
71
77
|
self.rhs = target_field.get_prep_value(self.rhs)
|
|
72
78
|
|
|
73
79
|
return super().get_prep_lookup()
|
|
@@ -97,12 +97,12 @@ class ReverseForeignKeyManager(BaseRelatedManager[T, QS]):
|
|
|
97
97
|
self.allow_null = self.field.allow_null
|
|
98
98
|
|
|
99
99
|
def _check_fk_val(self) -> None:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
field = self.field.target_field
|
|
101
|
+
if getattr(self.instance, field.name) is None:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f'"{self.instance!r}" needs to have a value for field '
|
|
104
|
+
f'"{field.name}" before this relationship can be used.'
|
|
105
|
+
)
|
|
106
106
|
|
|
107
107
|
def _apply_rel_filters(self, queryset: QuerySet) -> QuerySet:
|
|
108
108
|
"""
|
|
@@ -110,13 +110,10 @@ class ReverseForeignKeyManager(BaseRelatedManager[T, QS]):
|
|
|
110
110
|
"""
|
|
111
111
|
queryset._defer_next_filter = True
|
|
112
112
|
queryset = queryset.filter(**self.core_filters)
|
|
113
|
-
for field in self.field.foreign_related_fields:
|
|
114
|
-
val = getattr(self.instance, field.name)
|
|
115
|
-
if val is None:
|
|
116
|
-
return queryset.none()
|
|
117
|
-
|
|
118
113
|
target_field = self.field.target_field
|
|
119
114
|
rel_obj_id = getattr(self.instance, target_field.name)
|
|
115
|
+
if rel_obj_id is None:
|
|
116
|
+
return queryset.none()
|
|
120
117
|
queryset._known_related_objects = {self.field: {rel_obj_id: self.instance}}
|
|
121
118
|
return queryset
|
|
122
119
|
|
|
@@ -129,9 +126,8 @@ class ReverseForeignKeyManager(BaseRelatedManager[T, QS]):
|
|
|
129
126
|
pass # nothing to clear from cache
|
|
130
127
|
|
|
131
128
|
def get_queryset(self) -> QS:
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
# although having a primary key value isn't a guarantee of that.
|
|
129
|
+
# The instance must have a primary key value to be related against,
|
|
130
|
+
# though that isn't a guarantee it has actually been saved to the DB.
|
|
135
131
|
if self.instance.id is None:
|
|
136
132
|
raise ValueError(
|
|
137
133
|
f"{self.instance.__class__.__name__!r} instance needs to have a "
|
|
@@ -312,8 +308,7 @@ class ManyToManyManager(BaseRelatedManager[T, QS]):
|
|
|
312
308
|
target_field_name: str
|
|
313
309
|
symmetrical: bool
|
|
314
310
|
core_filters: dict[str, Any]
|
|
315
|
-
|
|
316
|
-
related_val: tuple[Any, ...]
|
|
311
|
+
related_val: Any
|
|
317
312
|
|
|
318
313
|
def __init__(
|
|
319
314
|
self,
|
|
@@ -357,22 +352,20 @@ class ManyToManyManager(BaseRelatedManager[T, QS]):
|
|
|
357
352
|
self.through._model_meta.get_forward_field(self.target_field_name),
|
|
358
353
|
)
|
|
359
354
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
self.related_val = self.source_field.get_foreign_related_value(instance)
|
|
370
|
-
if None in self.related_val:
|
|
355
|
+
source_fk = self.source_field
|
|
356
|
+
rh_name = source_fk.target_field.name
|
|
357
|
+
assert rh_name is not None
|
|
358
|
+
self.core_filters = {
|
|
359
|
+
f"{self.query_field_name}__{rh_name}": getattr(instance, rh_name)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
self.related_val = source_fk.get_foreign_related_value(instance)
|
|
363
|
+
if self.related_val is None:
|
|
371
364
|
raise ValueError(
|
|
372
|
-
f'"{instance!r}" needs to have a value for field "{
|
|
365
|
+
f'"{instance!r}" needs to have a value for field "{rh_name}" before '
|
|
373
366
|
"this many-to-many relationship can be used."
|
|
374
367
|
)
|
|
375
|
-
#
|
|
368
|
+
# The instance must have a primary key value to be related against.
|
|
376
369
|
if instance.id is None:
|
|
377
370
|
raise ValueError(
|
|
378
371
|
f"{instance.__class__.__name__!r} instance needs to have a primary key value before "
|
|
@@ -423,20 +416,16 @@ class ManyToManyManager(BaseRelatedManager[T, QS]):
|
|
|
423
416
|
qn = quote_name
|
|
424
417
|
queryset = queryset.extra(
|
|
425
418
|
select={
|
|
426
|
-
f"_prefetch_related_val_{
|
|
427
|
-
for f in fk.local_related_fields
|
|
419
|
+
f"_prefetch_related_val_{fk.name}": f"{qn(join_table)}.{qn(fk.column)}"
|
|
428
420
|
}
|
|
429
421
|
)
|
|
430
422
|
conn = get_connection()
|
|
423
|
+
target_field = fk.target_field
|
|
431
424
|
return (
|
|
432
425
|
queryset,
|
|
433
|
-
lambda result:
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
),
|
|
437
|
-
lambda inst: tuple(
|
|
438
|
-
f.get_db_prep_value(f.value_from_object(inst), conn)
|
|
439
|
-
for f in fk.foreign_related_fields
|
|
426
|
+
lambda result: getattr(result, f"_prefetch_related_val_{fk.name}"),
|
|
427
|
+
lambda inst: target_field.get_db_prep_value(
|
|
428
|
+
target_field.value_from_object(inst), conn
|
|
440
429
|
),
|
|
441
430
|
False,
|
|
442
431
|
self.prefetch_cache_name,
|
|
@@ -472,7 +461,7 @@ class ManyToManyManager(BaseRelatedManager[T, QS]):
|
|
|
472
461
|
new_objs = []
|
|
473
462
|
for obj in objs:
|
|
474
463
|
fk_val = (
|
|
475
|
-
self.target_field.get_foreign_related_value(obj)
|
|
464
|
+
self.target_field.get_foreign_related_value(obj)
|
|
476
465
|
if isinstance(obj, self.model)
|
|
477
466
|
else self.target_field.get_prep_value(obj)
|
|
478
467
|
)
|
|
@@ -525,7 +514,7 @@ class ManyToManyManager(BaseRelatedManager[T, QS]):
|
|
|
525
514
|
) # M2M through model fields are always ForeignKey
|
|
526
515
|
for obj in objs:
|
|
527
516
|
if isinstance(obj, self.model):
|
|
528
|
-
target_id = target_field.get_foreign_related_value(obj)
|
|
517
|
+
target_id = target_field.get_foreign_related_value(obj)
|
|
529
518
|
if target_id is None:
|
|
530
519
|
raise ValueError(
|
|
531
520
|
f'Cannot add "{obj!r}": the value for field "{target_field_name}" is None'
|
|
@@ -548,7 +537,7 @@ class ManyToManyManager(BaseRelatedManager[T, QS]):
|
|
|
548
537
|
"""Return the subset of ids of `objs` that aren't already assigned to this relationship."""
|
|
549
538
|
vals = self.through.query.values_list(target_field_name, flat=True).filter(
|
|
550
539
|
**{
|
|
551
|
-
source_field_name: self.related_val
|
|
540
|
+
source_field_name: self.related_val,
|
|
552
541
|
f"{target_field_name}__in": target_ids,
|
|
553
542
|
}
|
|
554
543
|
)
|
|
@@ -577,7 +566,7 @@ class ManyToManyManager(BaseRelatedManager[T, QS]):
|
|
|
577
566
|
self.through(
|
|
578
567
|
**through_defaults,
|
|
579
568
|
**{
|
|
580
|
-
source_field_name: self.related_val
|
|
569
|
+
source_field_name: self.related_val,
|
|
581
570
|
target_field_name: target_id,
|
|
582
571
|
},
|
|
583
572
|
)
|
|
@@ -595,7 +584,7 @@ class ManyToManyManager(BaseRelatedManager[T, QS]):
|
|
|
595
584
|
old_ids = set()
|
|
596
585
|
for obj in objs:
|
|
597
586
|
if isinstance(obj, self.model):
|
|
598
|
-
fk_val = self.target_field.get_foreign_related_value(obj)
|
|
587
|
+
fk_val = self.target_field.get_foreign_related_value(obj)
|
|
599
588
|
old_ids.add(fk_val)
|
|
600
589
|
else:
|
|
601
590
|
old_ids.add(obj)
|