plain.postgres 0.116.0__tar.gz → 0.117.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.116.0 → plain_postgres-0.117.0}/PKG-INFO +1 -1
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/CHANGELOG.md +26 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/base.py +17 -13
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/expressions.py +14 -2
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/base.py +0 -2
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/related.py +48 -45
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/reverse_related.py +34 -11
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/meta.py +6 -7
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/autodetector.py +23 -23
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/graph.py +7 -1
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/state.py +22 -18
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/utils.py +7 -2
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/query.py +8 -13
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/pyproject.toml +1 -1
- plain_postgres-0.117.0/tests/internal/test_unresolved_relation_refs.py +144 -0
- plain_postgres-0.117.0/tests/public/test_queryset_ordered.py +14 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/.gitignore +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/CLAUDE.md +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/LICENSE +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/README.md +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/README.md +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/adapters.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/cli/decorators.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/cli/sync.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/connection.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/constraints.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/convergence/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/convergence/analysis.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/convergence/corrections.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/convergence/planning.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/databases.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/dialect.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/binary.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/boolean.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/duration.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/encrypted.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/network.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/numeric.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/primary_key.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/related_managers.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/temporal.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/text.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/timezones.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/fields/uuid.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/forms.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/indexes.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/introspection/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/context.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/helpers.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/ownership.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/runner.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/types.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/introspection/schema.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/middleware.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/preflight/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/preflight/database.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/preflight/indexes.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/preflight/models.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/schema.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/schema_lock.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/sources.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/sql/compiler.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/sql/query.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/test/database.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/types.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/__init__.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/constraints.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/defaults.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/delete.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/encrypted.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/forms.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/indexes.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/iteration.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/mixins.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/nullability.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/querysets.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/relationships.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/storage_parameters.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/trees.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/urls.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/conftest.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_apply_replan.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_autodetector_not_null_errors.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_autodetector_type_change.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_connection_isolation.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_connection_pool.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_connection_self_heal.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_constraint_violation_error.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_convergence.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_constraints.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_defaults.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_fk.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_indexes.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_nullability.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_timeouts.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_databases_not_on_runtime_path.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_db_expression_defaults.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_diagnose.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_executor_connection_hook.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_fk_characterization.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_health.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_introspection.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_literal_default_persistence.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_management_connection.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_migration_executor.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_otel_metrics.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_rollback_exc_attribution.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_schema_lock.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/internal/test_schema_timeouts.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_create_update.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_database_url.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_databases.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_deferred_loading.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_delete_behaviors.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_encrypted_fields.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_exceptions.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_field_defaults.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_functions_uuid.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_integrity_error_mapping.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_iterator.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_m2m.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_manager_assignment.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_manual_pk.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_mixins.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_modelform_roundtrip.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_only_empty_defaults.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_order_by_expressions.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_queryset_repr.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_queryset_slicing.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_random_string_field.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_raw_query.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_read_only_transactions.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_related.py +0 -0
- {plain_postgres-0.116.0 → plain_postgres-0.117.0}/tests/public/test_related_instance_filter.py +0 -0
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.117.0](https://github.com/dropseed/plain/releases/plain-postgres@0.117.0) (2026-09-04)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Fixed `QuerySet.ordered`, which raised `AttributeError: 'Meta' object has no attribute 'ordering'` for any queryset without an explicit `order_by()`. It was reading the model's default ordering off `_model_meta` instead of `model_options`, so the property only ever worked on the one branch that never reached the lookup. It now correctly reports `True` when the model declares `Meta.ordering` and `False` when it doesn't ([d9406dfc6f](https://github.com/dropseed/plain/commit/d9406dfc6f))
|
|
8
|
+
- Unresolved relation references are now separate attributes from resolved model classes. `rel.model` and `ManyToManyRel.through` used to hold whatever you passed to the field — a `"package.Model"` string early on, the class after `lazy_related_operation` overwrote it — so every reader had to guess which it had, and the guessing was done inconsistently (`isinstance(..., str)` in some places, `isinstance(..., ModelBase)` in others, `hasattr(..., "_model_meta")` in others still). The raw reference now lives on `rel.model_ref` and `rel.through_ref`, and `rel.model` / `rel.through` are read-only properties that return the resolved class or raise `TypeError` if it isn't resolved yet. The `TypeError` is deliberate: an `AttributeError` would be swallowed by `hasattr`/`getattr` and silently read as "no model" ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
|
|
9
|
+
- `ForeignKeyField.target_field` dropped its own unresolved-reference `TypeError` — the `rel.model` property raises it now, with a message naming the field ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
|
|
10
|
+
- `ManyToManyField._check_through_fields` reports an unresolved target by name instead of crashing on `related_model.model_options.object_name`, so a `through_fields` mistake on a not-yet-resolved relation produces the intended preflight message ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
|
|
11
|
+
- `ModelState` now raises `TypeError` (was `ValueError`) when a field's `to` or `through` refers to a model class instead of a string reference, and detects it by checking the reference directly rather than probing for `_model_meta` ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
|
|
12
|
+
- Removed `Field.concrete`. It was set to `self.column is not None` right after `column` was assigned the field's name, so it was `True` on every forward field and the checks that consulted it were dead. `bulk_create()` and `bulk_update()` now say what they actually reject — "bulk_update() cannot be used with many-to-many fields." rather than "can only be used with concrete fields." — and `Model.save(update_fields=...)` drops "non-concrete fields" from its error message. `ForeignObjectRel.concrete` and the unreachable virtual-field skip in `Model.__init__` are gone too ([af56244677](https://github.com/dropseed/plain/commit/af56244677))
|
|
13
|
+
- `OrderBy.asc()` and `.desc()` return the `OrderBy` instead of `None`, matching every other expression's `asc`/`desc` and making `expr.desc().asc()` and inline use work. They still take no options — passing `nulls_first` or `nulls_last` raises `TypeError` pointing you at the attributes on the `OrderBy` itself, rather than being silently ignored ([f00e707ed0](https://github.com/dropseed/plain/commit/f00e707ed0))
|
|
14
|
+
- Preflight expression checks no longer crash when a lookup path continues past a non-relation field — for example `Meta.ordering = ("name__nope",)` on a text field. The traversal now raises `FieldDoesNotExist` for the trailing part, which preflight already reports, instead of an `AttributeError` on `None` ([d9406dfc6f](https://github.com/dropseed/plain/commit/d9406dfc6f))
|
|
15
|
+
- `MigrationGraph.make_state()` raises `NodeNotFoundError` when the plan hits a placeholder node, instead of an `AttributeError` on `None` ([d9406dfc6f](https://github.com/dropseed/plain/commit/d9406dfc6f))
|
|
16
|
+
- The migration autodetector and `ProjectState` narrow on `RelatedField` / `ManyToManyField` / `ManyToManyRel` instead of `hasattr(field, "remote_field")` and `getattr(remote_field, "through", None)`. Rename handling only rewrites a reference when the old field is the same kind of relation, so a field swapped between a plain relation and an M2M during a rename no longer copies a mismatched reference across ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
|
|
17
|
+
- `ProjectState._get_related_models` reads a forward relation's target from `remote_field.model_ref` rather than `f.model` — which returned the model the field is _declared on_, not the one it points at ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
|
|
18
|
+
- `Meta._expire_cache` propagation to a related model no longer swallows `AttributeError` to handle the unresolved-string case; it checks `model_ref` up front, so a genuine `AttributeError` inside `_expire_cache` surfaces ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
|
|
19
|
+
|
|
20
|
+
### Upgrade instructions
|
|
21
|
+
|
|
22
|
+
- If you read `rel.model` or `rel.through` (via `field.remote_field`) in code that runs before models are fully loaded — preflight checks, `contribute_to_class`, custom fields, migration operations — switch to `rel.model_ref` / `rel.through_ref` and check `isinstance(ref, str)`. After model loading completes, `rel.model` and `rel.through` behave as before.
|
|
23
|
+
- If you _assigned_ to `rel.model` or `rel.through`, assign to `rel.model_ref` / `rel.through_ref` instead — the plain names are now read-only properties.
|
|
24
|
+
- If you referenced `Field.concrete` or `ForeignObjectRel.concrete`, remove the check. `concrete` was `True` for every forward field and `False` only on reverse relations, which `get_fields()` doesn't return unless you pass `include_reverse=True`.
|
|
25
|
+
- If you asserted on the exact text of the `bulk_create()`, `bulk_update()`, or `save(update_fields=...)` errors, update the expected strings.
|
|
26
|
+
- If you caught `ValueError` from `ModelState` for a model-class `to`/`through` reference, catch `TypeError`.
|
|
27
|
+
- `OrderBy.asc()`/`.desc()` returning `self` is additive — existing calls that discarded the return value are unaffected.
|
|
28
|
+
|
|
3
29
|
## [0.116.0](https://github.com/dropseed/plain/releases/plain-postgres@0.116.0) (2026-09-04)
|
|
4
30
|
|
|
5
31
|
### What's changed
|
|
@@ -123,9 +123,6 @@ class Model(metaclass=ModelBase):
|
|
|
123
123
|
from plain.postgres.fields.related import RelatedField
|
|
124
124
|
|
|
125
125
|
is_related_object = False
|
|
126
|
-
# Virtual field
|
|
127
|
-
if field.name not in kwargs and field.column is None:
|
|
128
|
-
continue
|
|
129
126
|
if isinstance(field, RelatedField) and isinstance(
|
|
130
127
|
field.remote_field, ForeignObjectRel
|
|
131
128
|
):
|
|
@@ -395,8 +392,8 @@ class Model(metaclass=ModelBase):
|
|
|
395
392
|
non_model_fields = fields.difference(field_names)
|
|
396
393
|
if non_model_fields:
|
|
397
394
|
raise ValueError(
|
|
398
|
-
"The following fields do not exist in this model
|
|
399
|
-
"fields
|
|
395
|
+
"The following fields do not exist in this model or are m2m "
|
|
396
|
+
"fields: "
|
|
400
397
|
f"{', '.join(sorted(non_model_fields))}"
|
|
401
398
|
)
|
|
402
399
|
deferred_in_update = fields & deferred_fields
|
|
@@ -755,11 +752,14 @@ class Model(metaclass=ModelBase):
|
|
|
755
752
|
|
|
756
753
|
fields = cls._model_meta.many_to_many
|
|
757
754
|
|
|
758
|
-
# Skip when the target model wasn't found
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
755
|
+
# Skip when the target or relationship model wasn't found; the field's
|
|
756
|
+
# own preflight reports those.
|
|
757
|
+
fields = (
|
|
758
|
+
f
|
|
759
|
+
for f in fields
|
|
760
|
+
if not isinstance(f.remote_field.model_ref, str)
|
|
761
|
+
and not isinstance(f.remote_field.through_ref, str)
|
|
762
|
+
)
|
|
763
763
|
|
|
764
764
|
for f in fields:
|
|
765
765
|
signature = (
|
|
@@ -1033,7 +1033,11 @@ class Model(metaclass=ModelBase):
|
|
|
1033
1033
|
fld = None
|
|
1034
1034
|
for part in field.split(LOOKUP_SEP):
|
|
1035
1035
|
try:
|
|
1036
|
-
|
|
1036
|
+
if _cls is None:
|
|
1037
|
+
# The previous part was not a relation, so there is
|
|
1038
|
+
# no model left to look the next part up on.
|
|
1039
|
+
raise FieldDoesNotExist(part)
|
|
1040
|
+
fld = _cls._model_meta.get_field(part)
|
|
1037
1041
|
if isinstance(fld, RelatedField):
|
|
1038
1042
|
_cls = fld.path_infos[-1].to_meta.model
|
|
1039
1043
|
else:
|
|
@@ -1111,11 +1115,11 @@ class Model(metaclass=ModelBase):
|
|
|
1111
1115
|
|
|
1112
1116
|
for f in cls._model_meta.many_to_many:
|
|
1113
1117
|
# Skip nonexistent models.
|
|
1114
|
-
if isinstance(f.remote_field.
|
|
1118
|
+
if isinstance(f.remote_field.through_ref, str):
|
|
1115
1119
|
continue
|
|
1116
1120
|
|
|
1117
1121
|
# Check if column name for the M2M field is too long for the database.
|
|
1118
|
-
for m2m in f.remote_field.
|
|
1122
|
+
for m2m in f.remote_field.through_ref._model_meta.fields:
|
|
1119
1123
|
rel_name = m2m.column
|
|
1120
1124
|
if rel_name is not None and len(rel_name) > allowed_len:
|
|
1121
1125
|
errors.append(
|
|
@@ -1698,11 +1698,23 @@ class OrderBy(Expression):
|
|
|
1698
1698
|
self.nulls_last = None
|
|
1699
1699
|
return self
|
|
1700
1700
|
|
|
1701
|
-
def asc(self) ->
|
|
1701
|
+
def asc(self, **kwargs: Any) -> OrderBy:
|
|
1702
|
+
if kwargs:
|
|
1703
|
+
raise TypeError(
|
|
1704
|
+
"OrderBy.asc() takes no options; set nulls_first/nulls_last on "
|
|
1705
|
+
"the OrderBy itself."
|
|
1706
|
+
)
|
|
1702
1707
|
self.descending = False
|
|
1708
|
+
return self
|
|
1703
1709
|
|
|
1704
|
-
def desc(self) ->
|
|
1710
|
+
def desc(self, **kwargs: Any) -> OrderBy:
|
|
1711
|
+
if kwargs:
|
|
1712
|
+
raise TypeError(
|
|
1713
|
+
"OrderBy.desc() takes no options; set nulls_first/nulls_last on "
|
|
1714
|
+
"the OrderBy itself."
|
|
1715
|
+
)
|
|
1705
1716
|
self.descending = True
|
|
1717
|
+
return self
|
|
1706
1718
|
|
|
1707
1719
|
|
|
1708
1720
|
class Window(Expression):
|
|
@@ -115,7 +115,6 @@ class Field[T](RegisterLookupMixin):
|
|
|
115
115
|
name: str
|
|
116
116
|
# Set by set_attributes_from_name (called by contribute_to_class)
|
|
117
117
|
column: str
|
|
118
|
-
concrete: bool
|
|
119
118
|
# Set by contribute_to_class
|
|
120
119
|
model: type[Model]
|
|
121
120
|
|
|
@@ -376,7 +375,6 @@ class Field[T](RegisterLookupMixin):
|
|
|
376
375
|
# The database column is the field name. A foreign key overrides this
|
|
377
376
|
# method only to append the "_id" suffix to `column`.
|
|
378
377
|
self.column = self.name
|
|
379
|
-
self.concrete = self.column is not None
|
|
380
378
|
|
|
381
379
|
def contribute_to_class(self, cls: type[Model], name: str) -> None:
|
|
382
380
|
"""
|
|
@@ -166,13 +166,13 @@ class RelatedField(FieldCacheMixin, Field):
|
|
|
166
166
|
|
|
167
167
|
def _check_relation_model_exists(self) -> list[PreflightResult]:
|
|
168
168
|
rel_is_missing = (
|
|
169
|
-
self.remote_field.
|
|
169
|
+
self.remote_field.model_ref not in self.meta.models_registry.get_models()
|
|
170
170
|
)
|
|
171
|
-
rel_is_string = isinstance(self.remote_field.
|
|
171
|
+
rel_is_string = isinstance(self.remote_field.model_ref, str)
|
|
172
172
|
model_name = (
|
|
173
|
-
self.remote_field.
|
|
173
|
+
self.remote_field.model_ref
|
|
174
174
|
if rel_is_string
|
|
175
|
-
else self.remote_field.
|
|
175
|
+
else self.remote_field.model_ref.model_options.object_name
|
|
176
176
|
)
|
|
177
177
|
if rel_is_missing and rel_is_string:
|
|
178
178
|
return [
|
|
@@ -189,13 +189,11 @@ class RelatedField(FieldCacheMixin, Field):
|
|
|
189
189
|
|
|
190
190
|
def _check_clashes(self) -> list[PreflightResult]:
|
|
191
191
|
"""Check accessor and reverse query name clashes."""
|
|
192
|
-
from plain.postgres.base import ModelBase
|
|
193
|
-
|
|
194
192
|
errors: list[PreflightResult] = []
|
|
195
193
|
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
if
|
|
194
|
+
# Skip if the target model is still an unresolved string reference;
|
|
195
|
+
# _check_relation_model_exists reports that case.
|
|
196
|
+
if isinstance(self.remote_field.model_ref, str):
|
|
199
197
|
return []
|
|
200
198
|
|
|
201
199
|
# Consider that we are checking field `Model.foreign` and the models
|
|
@@ -259,13 +257,13 @@ class RelatedField(FieldCacheMixin, Field):
|
|
|
259
257
|
def resolve_related_class(
|
|
260
258
|
model: type[Model], related: type[Model], field: RelatedField
|
|
261
259
|
) -> None:
|
|
262
|
-
field.remote_field.
|
|
260
|
+
field.remote_field.model_ref = related
|
|
263
261
|
field.do_related_class(related, model)
|
|
264
262
|
|
|
265
263
|
lazy_related_operation(
|
|
266
264
|
resolve_related_class,
|
|
267
265
|
cls,
|
|
268
|
-
self.remote_field.
|
|
266
|
+
self.remote_field.model_ref,
|
|
269
267
|
field=self,
|
|
270
268
|
)
|
|
271
269
|
|
|
@@ -456,14 +454,14 @@ class ForeignKeyField(ColumnField, RelatedField):
|
|
|
456
454
|
name, path, args, kwargs = super().deconstruct()
|
|
457
455
|
kwargs["on_delete"] = self.remote_field.on_delete
|
|
458
456
|
|
|
459
|
-
if isinstance(self.remote_field.
|
|
460
|
-
if "." in self.remote_field.
|
|
461
|
-
package_label, model_name = self.remote_field.
|
|
457
|
+
if isinstance(self.remote_field.model_ref, str):
|
|
458
|
+
if "." in self.remote_field.model_ref:
|
|
459
|
+
package_label, model_name = self.remote_field.model_ref.split(".")
|
|
462
460
|
kwargs["to"] = f"{package_label}.{model_name.lower()}"
|
|
463
461
|
else:
|
|
464
|
-
kwargs["to"] = self.remote_field.
|
|
462
|
+
kwargs["to"] = self.remote_field.model_ref.lower()
|
|
465
463
|
else:
|
|
466
|
-
kwargs["to"] = self.remote_field.
|
|
464
|
+
kwargs["to"] = self.remote_field.model_ref.model_options.label_lower
|
|
467
465
|
|
|
468
466
|
return name, path, args, kwargs
|
|
469
467
|
|
|
@@ -473,10 +471,6 @@ class ForeignKeyField(ColumnField, RelatedField):
|
|
|
473
471
|
@cached_property
|
|
474
472
|
def target_field(self) -> Field:
|
|
475
473
|
"""A foreign key points at exactly one column: the remote model's id."""
|
|
476
|
-
if isinstance(self.remote_field.model, str):
|
|
477
|
-
raise TypeError(
|
|
478
|
-
f"Related model {self.remote_field.model!r} cannot be resolved"
|
|
479
|
-
)
|
|
480
474
|
return self.remote_field.model._model_meta.get_forward_field("id")
|
|
481
475
|
|
|
482
476
|
def set_attributes_from_name(self, name: str) -> None:
|
|
@@ -640,14 +634,17 @@ class ManyToManyField(RelatedField):
|
|
|
640
634
|
def _check_relationship_model(
|
|
641
635
|
self, from_model: type[Model] | None = None, **kwargs: Any
|
|
642
636
|
) -> list[PreflightResult]:
|
|
643
|
-
|
|
644
|
-
|
|
637
|
+
through_ref = self.remote_field.through_ref
|
|
638
|
+
if isinstance(through_ref, str):
|
|
639
|
+
qualified_model_name = through_ref
|
|
645
640
|
else:
|
|
646
|
-
qualified_model_name =
|
|
641
|
+
qualified_model_name = (
|
|
642
|
+
f"{through_ref.model_options.package_label}.{through_ref.__name__}"
|
|
643
|
+
)
|
|
647
644
|
|
|
648
645
|
errors = []
|
|
649
646
|
|
|
650
|
-
if
|
|
647
|
+
if through_ref not in self.meta.models_registry.get_models():
|
|
651
648
|
# The relationship model is not installed.
|
|
652
649
|
errors.append(
|
|
653
650
|
PreflightResult(
|
|
@@ -668,7 +665,7 @@ class ManyToManyField(RelatedField):
|
|
|
668
665
|
"where the field is attached to."
|
|
669
666
|
)
|
|
670
667
|
# Set some useful local variables
|
|
671
|
-
to_model = resolve_relation(from_model, self.remote_field.
|
|
668
|
+
to_model = resolve_relation(from_model, self.remote_field.model_ref)
|
|
672
669
|
from_model_name = from_model.model_options.object_name
|
|
673
670
|
if isinstance(to_model, str):
|
|
674
671
|
to_model_name = to_model
|
|
@@ -681,7 +678,7 @@ class ManyToManyField(RelatedField):
|
|
|
681
678
|
# Count foreign keys in intermediate model
|
|
682
679
|
if self_referential:
|
|
683
680
|
seen_self = sum(
|
|
684
|
-
from_model == field.remote_field.
|
|
681
|
+
from_model == field.remote_field.model_ref
|
|
685
682
|
for field in self.remote_field.through._model_meta.fields
|
|
686
683
|
if isinstance(field, RelatedField)
|
|
687
684
|
)
|
|
@@ -704,12 +701,12 @@ class ManyToManyField(RelatedField):
|
|
|
704
701
|
else:
|
|
705
702
|
# Count foreign keys in relationship model
|
|
706
703
|
seen_from = sum(
|
|
707
|
-
from_model == field.remote_field.
|
|
704
|
+
from_model == field.remote_field.model_ref
|
|
708
705
|
for field in self.remote_field.through._model_meta.fields
|
|
709
706
|
if isinstance(field, RelatedField)
|
|
710
707
|
)
|
|
711
708
|
seen_to = sum(
|
|
712
|
-
to_model == field.remote_field.
|
|
709
|
+
to_model == field.remote_field.model_ref
|
|
713
710
|
for field in self.remote_field.through._model_meta.fields
|
|
714
711
|
if isinstance(field, RelatedField)
|
|
715
712
|
)
|
|
@@ -797,7 +794,7 @@ class ManyToManyField(RelatedField):
|
|
|
797
794
|
source, through, target = (
|
|
798
795
|
from_model,
|
|
799
796
|
self.remote_field.through,
|
|
800
|
-
self.remote_field.
|
|
797
|
+
self.remote_field.model_ref,
|
|
801
798
|
)
|
|
802
799
|
source_field_name, target_field_name = self.remote_field.through_fields[
|
|
803
800
|
:2
|
|
@@ -807,18 +804,24 @@ class ManyToManyField(RelatedField):
|
|
|
807
804
|
(source_field_name, source),
|
|
808
805
|
(target_field_name, target),
|
|
809
806
|
):
|
|
807
|
+
# The target may still be an unresolved "package.Model" string.
|
|
808
|
+
related_model_name = (
|
|
809
|
+
related_model
|
|
810
|
+
if isinstance(related_model, str)
|
|
811
|
+
else related_model.model_options.object_name
|
|
812
|
+
)
|
|
810
813
|
possible_field_names: list[str] = []
|
|
811
814
|
for f in through._model_meta.fields:
|
|
812
815
|
if (
|
|
813
|
-
|
|
814
|
-
and
|
|
816
|
+
isinstance(f, RelatedField)
|
|
817
|
+
and f.remote_field.model_ref == related_model
|
|
815
818
|
):
|
|
816
819
|
possible_field_names.append(f.name)
|
|
817
820
|
if possible_field_names:
|
|
818
821
|
fix = (
|
|
819
822
|
"Did you mean one of the following foreign keys to '{}': "
|
|
820
823
|
"{}?".format(
|
|
821
|
-
|
|
824
|
+
related_model_name,
|
|
822
825
|
", ".join(possible_field_names),
|
|
823
826
|
)
|
|
824
827
|
)
|
|
@@ -838,11 +841,11 @@ class ManyToManyField(RelatedField):
|
|
|
838
841
|
else:
|
|
839
842
|
if not (
|
|
840
843
|
isinstance(field, RelatedField)
|
|
841
|
-
and field.remote_field.
|
|
844
|
+
and field.remote_field.model_ref == related_model
|
|
842
845
|
):
|
|
843
846
|
errors.append(
|
|
844
847
|
PreflightResult(
|
|
845
|
-
fix=f"'{through.model_options.object_name}.{field_name}' is not a foreign key to '{
|
|
848
|
+
fix=f"'{through.model_options.object_name}.{field_name}' is not a foreign key to '{related_model_name}'. {fix}",
|
|
846
849
|
obj=self,
|
|
847
850
|
id="fields.m2m_through_field_not_fk_to_model",
|
|
848
851
|
)
|
|
@@ -851,7 +854,7 @@ class ManyToManyField(RelatedField):
|
|
|
851
854
|
return errors
|
|
852
855
|
|
|
853
856
|
def _check_table_uniqueness(self, **kwargs: Any) -> list[PreflightResult]:
|
|
854
|
-
if isinstance(self.remote_field.
|
|
857
|
+
if isinstance(self.remote_field.through_ref, str):
|
|
855
858
|
return []
|
|
856
859
|
registered_tables = {
|
|
857
860
|
model.model_options.db_table: model
|
|
@@ -880,19 +883,19 @@ class ManyToManyField(RelatedField):
|
|
|
880
883
|
name, path, args, kwargs = super().deconstruct()
|
|
881
884
|
|
|
882
885
|
# Lowercase model names as they should be treated as case-insensitive.
|
|
883
|
-
if isinstance(self.remote_field.
|
|
884
|
-
if "." in self.remote_field.
|
|
885
|
-
package_label, model_name = self.remote_field.
|
|
886
|
+
if isinstance(self.remote_field.model_ref, str):
|
|
887
|
+
if "." in self.remote_field.model_ref:
|
|
888
|
+
package_label, model_name = self.remote_field.model_ref.split(".")
|
|
886
889
|
kwargs["to"] = f"{package_label}.{model_name.lower()}"
|
|
887
890
|
else:
|
|
888
|
-
kwargs["to"] = self.remote_field.
|
|
891
|
+
kwargs["to"] = self.remote_field.model_ref.lower()
|
|
889
892
|
else:
|
|
890
|
-
kwargs["to"] = self.remote_field.
|
|
893
|
+
kwargs["to"] = self.remote_field.model_ref.model_options.label_lower
|
|
891
894
|
|
|
892
|
-
if isinstance(self.remote_field.
|
|
893
|
-
kwargs["through"] = self.remote_field.
|
|
895
|
+
if isinstance(self.remote_field.through_ref, str):
|
|
896
|
+
kwargs["through"] = self.remote_field.through_ref
|
|
894
897
|
else:
|
|
895
|
-
kwargs["through"] = self.remote_field.
|
|
898
|
+
kwargs["through"] = self.remote_field.through_ref.model_options.label
|
|
896
899
|
|
|
897
900
|
return name, path, args, kwargs
|
|
898
901
|
|
|
@@ -990,12 +993,12 @@ class ManyToManyField(RelatedField):
|
|
|
990
993
|
def resolve_through_model(
|
|
991
994
|
_: Any, model: type[Model], field: ManyToManyField
|
|
992
995
|
) -> None:
|
|
993
|
-
field.remote_field.
|
|
996
|
+
field.remote_field.through_ref = model
|
|
994
997
|
|
|
995
998
|
lazy_related_operation(
|
|
996
999
|
resolve_through_model,
|
|
997
1000
|
cls,
|
|
998
|
-
self.remote_field.
|
|
1001
|
+
self.remote_field.through_ref,
|
|
999
1002
|
field=self,
|
|
1000
1003
|
)
|
|
1001
1004
|
|
|
@@ -41,7 +41,6 @@ class ForeignObjectRel(FieldCacheMixin):
|
|
|
41
41
|
|
|
42
42
|
# Field flags
|
|
43
43
|
auto_created = True
|
|
44
|
-
concrete = False
|
|
45
44
|
|
|
46
45
|
# Reverse relations are always nullable (Plain can't enforce that a
|
|
47
46
|
# foreign key on the related model points to this model).
|
|
@@ -49,8 +48,11 @@ class ForeignObjectRel(FieldCacheMixin):
|
|
|
49
48
|
empty_strings_allowed = False
|
|
50
49
|
|
|
51
50
|
# Type annotations for instance attributes
|
|
52
|
-
model: type[Model]
|
|
53
51
|
on_delete: OnDelete | None
|
|
52
|
+
# The target model as given to the field: a "package.Model" string (or
|
|
53
|
+
# "self") until lazy_related_operation resolves it to the class. Read
|
|
54
|
+
# `model` for the resolved class.
|
|
55
|
+
model_ref: str | type[Model]
|
|
54
56
|
|
|
55
57
|
def __init__(
|
|
56
58
|
self,
|
|
@@ -65,9 +67,7 @@ class ForeignObjectRel(FieldCacheMixin):
|
|
|
65
67
|
# route every `self.field` read through its __get__. ForeignKeyRel
|
|
66
68
|
# and ManyToManyRel below narrow this further, the same way.
|
|
67
69
|
self.field: RelatedField = field
|
|
68
|
-
|
|
69
|
-
# (see related.py:250 where field.remote_field.model is overwritten)
|
|
70
|
-
self.model = to # ty: ignore[invalid-assignment]
|
|
70
|
+
self.model_ref = to
|
|
71
71
|
self.related_query_name = related_query_name
|
|
72
72
|
self.on_delete = on_delete
|
|
73
73
|
|
|
@@ -77,6 +77,18 @@ class ForeignObjectRel(FieldCacheMixin):
|
|
|
77
77
|
# __init__ as the field doesn't have its model yet. Calling these methods
|
|
78
78
|
# before field.contribute_to_class() has been called will result in
|
|
79
79
|
# AttributeError
|
|
80
|
+
@property
|
|
81
|
+
def model(self) -> type[Model]:
|
|
82
|
+
"""The resolved target model class."""
|
|
83
|
+
# Deliberately not AttributeError: getattr(rel, "model", None) and
|
|
84
|
+
# hasattr() would swallow that and silently report "no model".
|
|
85
|
+
if isinstance(self.model_ref, str):
|
|
86
|
+
raise TypeError(
|
|
87
|
+
f"Related model {self.model_ref!r} for {self.field!r} has not been "
|
|
88
|
+
"resolved yet; use model_ref to read the unresolved reference."
|
|
89
|
+
)
|
|
90
|
+
return self.model_ref
|
|
91
|
+
|
|
80
92
|
@cached_property
|
|
81
93
|
def name(self) -> str:
|
|
82
94
|
return self.field.related_query_name()
|
|
@@ -113,7 +125,7 @@ class ForeignObjectRel(FieldCacheMixin):
|
|
|
113
125
|
def identity(self) -> tuple[Any, ...]:
|
|
114
126
|
return (
|
|
115
127
|
self.field,
|
|
116
|
-
self.
|
|
128
|
+
self.model_ref,
|
|
117
129
|
self.related_query_name,
|
|
118
130
|
self.on_delete,
|
|
119
131
|
self.symmetrical,
|
|
@@ -203,8 +215,10 @@ class ManyToManyRel(ForeignObjectRel):
|
|
|
203
215
|
"""
|
|
204
216
|
|
|
205
217
|
# Type annotations for instance attributes
|
|
206
|
-
through: type[Model]
|
|
207
218
|
through_fields: tuple[str, str] | None
|
|
219
|
+
# The intermediary model as given to the field; a string until
|
|
220
|
+
# lazy_related_operation resolves it. Read `through` for the class.
|
|
221
|
+
through_ref: str | type[Model]
|
|
208
222
|
|
|
209
223
|
def __init__(
|
|
210
224
|
self,
|
|
@@ -226,9 +240,7 @@ class ManyToManyRel(ForeignObjectRel):
|
|
|
226
240
|
# annotation).
|
|
227
241
|
self.field: ManyToManyField
|
|
228
242
|
|
|
229
|
-
|
|
230
|
-
# (see related.py:1143 where field.remote_field.through is overwritten)
|
|
231
|
-
self.through = through # ty: ignore[invalid-assignment]
|
|
243
|
+
self.through_ref = through
|
|
232
244
|
self.through_fields = through_fields
|
|
233
245
|
|
|
234
246
|
self.symmetrical = symmetrical
|
|
@@ -236,6 +248,17 @@ class ManyToManyRel(ForeignObjectRel):
|
|
|
236
248
|
@property
|
|
237
249
|
def identity(self) -> tuple[Any, ...]:
|
|
238
250
|
return super().identity + (
|
|
239
|
-
self.
|
|
251
|
+
self.through_ref,
|
|
240
252
|
make_hashable(self.through_fields),
|
|
241
253
|
)
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def through(self) -> type[Model]:
|
|
257
|
+
"""The resolved intermediary model class."""
|
|
258
|
+
# Deliberately not AttributeError, for the same reason as `model`.
|
|
259
|
+
if isinstance(self.through_ref, str):
|
|
260
|
+
raise TypeError(
|
|
261
|
+
f"Through model {self.through_ref!r} for {self.field!r} has not been "
|
|
262
|
+
"resolved yet; use through_ref to read the unresolved reference."
|
|
263
|
+
)
|
|
264
|
+
return self.through_ref
|
|
@@ -178,11 +178,10 @@ class Meta:
|
|
|
178
178
|
# ideally, we'd just ask for field.related_model. However, related_model
|
|
179
179
|
# is a cached property, and all the models haven't been loaded yet, so
|
|
180
180
|
# we need to make sure we don't cache a string reference.
|
|
181
|
-
if isinstance(field, RelatedField)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
pass
|
|
181
|
+
if isinstance(field, RelatedField):
|
|
182
|
+
remote_model = field.remote_field.model_ref
|
|
183
|
+
if not isinstance(remote_model, str):
|
|
184
|
+
remote_model._model_meta._expire_cache(forward=False)
|
|
186
185
|
self._expire_cache()
|
|
187
186
|
else:
|
|
188
187
|
self._expire_cache(reverse=False)
|
|
@@ -326,8 +325,8 @@ class Meta:
|
|
|
326
325
|
if isinstance(f, RelatedField)
|
|
327
326
|
)
|
|
328
327
|
for f in fields_with_relations:
|
|
329
|
-
if not isinstance(f.remote_field.
|
|
330
|
-
remote_label = f.remote_field.
|
|
328
|
+
if not isinstance(f.remote_field.model_ref, str):
|
|
329
|
+
remote_label = f.remote_field.model_ref.model_options.label
|
|
331
330
|
related_objects_graph[remote_label].append(f)
|
|
332
331
|
|
|
333
332
|
for model in all_models:
|
|
@@ -139,7 +139,7 @@ class MigrationAutodetector:
|
|
|
139
139
|
fields_def = []
|
|
140
140
|
for name, field in sorted(fields.items()):
|
|
141
141
|
deconstruction = self.deep_deconstruct(field)
|
|
142
|
-
if isinstance(field, RelatedField) and field.remote_field.
|
|
142
|
+
if isinstance(field, RelatedField) and field.remote_field.model_ref:
|
|
143
143
|
deconstruction[2].pop("to", None)
|
|
144
144
|
fields_def.append(deconstruction)
|
|
145
145
|
return fields_def
|
|
@@ -244,11 +244,9 @@ class MigrationAutodetector:
|
|
|
244
244
|
)
|
|
245
245
|
old_model_state = self.from_state.models[package_label, old_model_name]
|
|
246
246
|
for field_name, field in old_model_state.fields.items():
|
|
247
|
-
if
|
|
248
|
-
field.remote_field, "through", None
|
|
249
|
-
):
|
|
247
|
+
if isinstance(field, ManyToManyField):
|
|
250
248
|
through_key = resolve_relation(
|
|
251
|
-
field.remote_field.
|
|
249
|
+
field.remote_field.through_ref,
|
|
252
250
|
package_label,
|
|
253
251
|
model_name,
|
|
254
252
|
)
|
|
@@ -537,7 +535,7 @@ class MigrationAutodetector:
|
|
|
537
535
|
related_fields = {}
|
|
538
536
|
for field_name, field in model_state.fields.items():
|
|
539
537
|
if isinstance(field, RelatedField):
|
|
540
|
-
if field.remote_field.
|
|
538
|
+
if field.remote_field.model_ref:
|
|
541
539
|
related_fields[field_name] = field
|
|
542
540
|
if isinstance(field.remote_field, ManyToManyRel):
|
|
543
541
|
related_fields[field_name] = field
|
|
@@ -639,7 +637,7 @@ class MigrationAutodetector:
|
|
|
639
637
|
related_fields = {}
|
|
640
638
|
for field_name, field in model_state.fields.items():
|
|
641
639
|
if isinstance(field, RelatedField):
|
|
642
|
-
if field.remote_field.
|
|
640
|
+
if field.remote_field.model_ref:
|
|
643
641
|
related_fields[field_name] = field
|
|
644
642
|
if isinstance(field.remote_field, ManyToManyRel):
|
|
645
643
|
related_fields[field_name] = field
|
|
@@ -720,7 +718,7 @@ class MigrationAutodetector:
|
|
|
720
718
|
if (
|
|
721
719
|
isinstance(field, RelatedField)
|
|
722
720
|
and field.remote_field
|
|
723
|
-
and field.remote_field.
|
|
721
|
+
and field.remote_field.model_ref
|
|
724
722
|
and "to" in old_field_dec[2]
|
|
725
723
|
):
|
|
726
724
|
old_rel_to = old_field_dec[2]["to"]
|
|
@@ -790,7 +788,7 @@ class MigrationAutodetector:
|
|
|
790
788
|
(package_label, model_name, field_name, False)
|
|
791
789
|
]
|
|
792
790
|
# Fields that are foreignkeys/m2ms depend on stuff.
|
|
793
|
-
if isinstance(field, RelatedField) and field.remote_field.
|
|
791
|
+
if isinstance(field, RelatedField) and field.remote_field.model_ref:
|
|
794
792
|
dependencies.extend(
|
|
795
793
|
self._get_dependencies_for_foreign_key(
|
|
796
794
|
package_label,
|
|
@@ -888,16 +886,16 @@ class MigrationAutodetector:
|
|
|
888
886
|
dependencies: list[tuple[str, str, str | None, bool | str]] = []
|
|
889
887
|
# Implement any model renames on relations; these are handled by RenameModel
|
|
890
888
|
# so we need to exclude them from the comparison
|
|
891
|
-
if
|
|
892
|
-
new_field.remote_field, "model", None
|
|
893
|
-
):
|
|
889
|
+
if isinstance(new_field, RelatedField):
|
|
894
890
|
rename_key = resolve_relation(
|
|
895
|
-
new_field.remote_field.
|
|
891
|
+
new_field.remote_field.model_ref,
|
|
896
892
|
package_label,
|
|
897
893
|
model_name,
|
|
898
894
|
)
|
|
899
|
-
if rename_key in self.renamed_models
|
|
900
|
-
|
|
895
|
+
if rename_key in self.renamed_models and isinstance(
|
|
896
|
+
old_field, RelatedField
|
|
897
|
+
):
|
|
898
|
+
new_field.remote_field.model_ref = old_field.remote_field.model_ref
|
|
901
899
|
dependencies.extend(
|
|
902
900
|
self._get_dependencies_for_foreign_key(
|
|
903
901
|
package_label,
|
|
@@ -906,16 +904,18 @@ class MigrationAutodetector:
|
|
|
906
904
|
self.to_state,
|
|
907
905
|
)
|
|
908
906
|
)
|
|
909
|
-
if
|
|
910
|
-
new_field.remote_field, "through", None
|
|
911
|
-
):
|
|
907
|
+
if isinstance(new_field, ManyToManyField):
|
|
912
908
|
rename_key = resolve_relation(
|
|
913
|
-
new_field.remote_field.
|
|
909
|
+
new_field.remote_field.through_ref,
|
|
914
910
|
package_label,
|
|
915
911
|
model_name,
|
|
916
912
|
)
|
|
917
|
-
if rename_key in self.renamed_models
|
|
918
|
-
|
|
913
|
+
if rename_key in self.renamed_models and isinstance(
|
|
914
|
+
old_field, ManyToManyField
|
|
915
|
+
):
|
|
916
|
+
new_field.remote_field.through_ref = (
|
|
917
|
+
old_field.remote_field.through_ref
|
|
918
|
+
)
|
|
919
919
|
old_field_dec = self.deep_deconstruct(old_field)
|
|
920
920
|
new_field_dec = self.deep_deconstruct(new_field)
|
|
921
921
|
# Exclude non_migration_attrs (allow_null, default, on_delete, choices,
|
|
@@ -1004,7 +1004,7 @@ class MigrationAutodetector:
|
|
|
1004
1004
|
) -> list[tuple[str, str, str | None, bool | str]]:
|
|
1005
1005
|
remote_field_model = None
|
|
1006
1006
|
if isinstance(field, RelatedField):
|
|
1007
|
-
remote_field_model = field.remote_field.
|
|
1007
|
+
remote_field_model = field.remote_field.model_ref
|
|
1008
1008
|
else:
|
|
1009
1009
|
relations = project_state.relations[package_label, model_name]
|
|
1010
1010
|
for (remote_package_label, remote_model_name), fields in relations.items():
|
|
@@ -1027,7 +1027,7 @@ class MigrationAutodetector:
|
|
|
1027
1027
|
field.remote_field, ManyToManyRel
|
|
1028
1028
|
):
|
|
1029
1029
|
through_package_label, through_object_name = resolve_relation(
|
|
1030
|
-
field.remote_field.
|
|
1030
|
+
field.remote_field.through_ref,
|
|
1031
1031
|
package_label,
|
|
1032
1032
|
model_name,
|
|
1033
1033
|
)
|