plain.postgres 0.114.0__tar.gz → 0.116.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.114.0/plain/postgres/README.md → plain_postgres-0.116.0/PKG-INFO +23 -4
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/CHANGELOG.md +43 -0
- plain_postgres-0.114.0/PKG-INFO → plain_postgres-0.116.0/plain/postgres/README.md +10 -17
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/__init__.py +1 -2
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +2 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/base.py +44 -68
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/connection.py +5 -13
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/constraints.py +6 -37
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/convergence/analysis.py +149 -106
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/convergence/corrections.py +65 -25
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/convergence/planning.py +8 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/ddl.py +0 -16
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/deletion.py +3 -19
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/dialect.py +0 -3
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/base.py +11 -22
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/binary.py +1 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/encrypted.py +1 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/json.py +1 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/network.py +1 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/numeric.py +1 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/primary_key.py +1 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/related.py +46 -13
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/related_managers.py +4 -9
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/reverse_related.py +16 -7
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/temporal.py +1 -2
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/text.py +2 -2
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/timezones.py +1 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/uuid.py +1 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/forms.py +2 -4
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/ownership.py +1 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/introspection/schema.py +2 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/meta.py +50 -89
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/serializer.py +3 -3
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/state.py +2 -2
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/preflight/indexes.py +1 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/query.py +11 -14
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/query_utils.py +5 -5
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/schema.py +1 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/sql/compiler.py +2 -2
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/sql/datastructures.py +3 -3
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/sql/query.py +8 -12
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/test/pytest.py +0 -12
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/utils.py +9 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/pyproject.toml +1 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -13
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/delete.py +2 -9
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_constraints.py +0 -60
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_fk.py +223 -71
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_timeouts.py +1 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_migration_executor.py +34 -1
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_delete_behaviors.py +18 -59
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_integrity_error_mapping.py +27 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/.gitignore +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/CLAUDE.md +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/LICENSE +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/README.md +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/adapters.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/cli/decorators.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/cli/sync.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/convergence/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/databases.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/boolean.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/duration.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/indexes.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/introspection/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/context.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/helpers.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/runner.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/types.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/middleware.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/autodetector.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/preflight/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/preflight/database.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/preflight/models.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/schema_lock.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/sources.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/test/database.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/types.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/__init__.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/constraints.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/defaults.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/encrypted.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/forms.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/indexes.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/iteration.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/mixins.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/nullability.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/querysets.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/relationships.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/storage_parameters.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/trees.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/urls.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/conftest.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_apply_replan.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_autodetector_not_null_errors.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_autodetector_type_change.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_connection_isolation.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_connection_pool.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_connection_self_heal.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_constraint_violation_error.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_convergence.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_defaults.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_indexes.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_nullability.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_databases_not_on_runtime_path.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_db_expression_defaults.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_diagnose.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_executor_connection_hook.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_fk_characterization.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_health.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_introspection.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_literal_default_persistence.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_management_connection.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_otel_metrics.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_rollback_exc_attribution.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_schema_lock.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/internal/test_schema_timeouts.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_create_update.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_database_url.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_databases.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_deferred_loading.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_encrypted_fields.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_exceptions.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_field_defaults.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_functions_uuid.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_iterator.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_m2m.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_manager_assignment.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_manual_pk.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_mixins.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_modelform_roundtrip.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_only_empty_defaults.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_order_by_expressions.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_queryset_repr.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_queryset_slicing.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_random_string_field.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_raw_query.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_read_only_transactions.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_related.py +0 -0
- {plain_postgres-0.114.0 → plain_postgres-0.116.0}/tests/public/test_related_instance_filter.py +0 -0
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: plain.postgres
|
|
3
|
+
Version: 0.116.0
|
|
4
|
+
Summary: Model your data and store it in a database.
|
|
5
|
+
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Requires-Dist: plain<1.0.0,>=0.157.0
|
|
10
|
+
Requires-Dist: psycopg-pool>=3.2
|
|
11
|
+
Requires-Dist: psycopg>=3.2
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
1
14
|
# plain.postgres
|
|
2
15
|
|
|
3
16
|
**Model your data and store it in a database.**
|
|
@@ -983,6 +996,14 @@ The first access to any non-key field loads the whole row in a single query. The
|
|
|
983
996
|
|
|
984
997
|
The partial-instance shortcut is safe because Plain always creates a database foreign-key constraint, so the referenced row is guaranteed to exist.
|
|
985
998
|
|
|
999
|
+
### Constraints are checked immediately
|
|
1000
|
+
|
|
1001
|
+
Every constraint is checked at the write that violates it, never at commit — the same as Postgres's own default. Inserting a child row that points at a parent that doesn't exist yet fails at that `INSERT`, with a traceback pointing at the offending write, and deleting a parent with `RESTRICT` children fails at that `DELETE`. Create parents before children. A cycle of foreign keys needs a nullable back-reference: create both rows, then `update()` the back-reference. Two required foreign keys pointing at each other can never be inserted.
|
|
1002
|
+
|
|
1003
|
+
To swap two rows' values under a unique constraint (reordering by `position`, say), move one row to a temporary value first — a single `UPDATE` that swaps them fails, because Postgres checks uniqueness per row.
|
|
1004
|
+
|
|
1005
|
+
A migration can add a column, backfill it in `RunPython`, and drop or alter columns on the same table, all in one transaction.
|
|
1006
|
+
|
|
986
1007
|
### Reverse relationships
|
|
987
1008
|
|
|
988
1009
|
When you define a `ForeignKey` or `ManyToManyField`, Plain automatically creates a reverse accessor on the related model (like `author.book_set`). You can explicitly declare these reverse relationships using [`ReverseForeignKey`](./fields/reverse_descriptors.py#ReverseForeignKey) and [`ReverseManyToMany`](./fields/reverse_descriptors.py#ReverseManyToMany):
|
|
@@ -1087,7 +1108,7 @@ class User(postgres.Model):
|
|
|
1087
1108
|
|
|
1088
1109
|
Field-level validation happens automatically based on field types and constraints.
|
|
1089
1110
|
|
|
1090
|
-
**The database is authoritative for constraints.** `create()`/`update()` don't pre-check your declared unique/check constraints — they attempt the write, and if Postgres rejects it, translate the `IntegrityError` into a `ValidationError` (routed to the field for single-column uniques, `NON_FIELD_ERRORS` otherwise). You get the same field-level error you'd expect, the write costs no per-constraint `SELECT`, and a raced concurrent insert can't slip through as a 500.
|
|
1111
|
+
**The database is authoritative for constraints.** `create()`/`update()` don't pre-check your declared unique/check constraints — they attempt the write, and if Postgres rejects it, translate the `IntegrityError` into a `ValidationError` (routed to the field for single-column uniques, `NON_FIELD_ERRORS` otherwise). You get the same field-level error you'd expect, the write costs no per-constraint `SELECT`, and a raced concurrent insert can't slip through as a 500. A foreign key pointing at a row that doesn't exist maps the same way, as an error on that field. (`NOT NULL` and a hand-set primary-key collision have no declared constraint to map to and re-raise as the original `IntegrityError`. `create()` always inserts, so passing a stray `id` that already exists is rejected by Postgres as the original `IntegrityError`.)
|
|
1091
1112
|
|
|
1092
1113
|
Because the rejected write reaches the database, it aborts the surrounding transaction. If you catch the `ValidationError` and want to keep using the transaction, wrap the write in `transaction.atomic()` so it rolls back to a savepoint:
|
|
1093
1114
|
|
|
@@ -1101,7 +1122,7 @@ except ValidationError:
|
|
|
1101
1122
|
|
|
1102
1123
|
Forms are the exception: a `ModelForm` pre-checks constraints explicitly (a `validate_constraints()` call in its `_post_clean`) so it can surface every violation at once, then writes via `form.create()`/`form.update()` with validation already done. A direct `create()`/`update()` reports the first violation Postgres hits.
|
|
1103
1124
|
|
|
1104
|
-
This applies to instance writes only. Set-based writes — `QuerySet.update()` and `bulk_create()` — raise the raw `psycopg.IntegrityError`, since there's no instance to attribute the error to. If you retry on a unique conflict, catch both:
|
|
1125
|
+
This applies to instance writes only. Set-based writes — `QuerySet.update()` and `bulk_create()` — raise the raw `psycopg.IntegrityError`, since there's no instance to attribute the error to, and so does a `delete()` blocked by `RESTRICT` children. If you retry on a unique conflict, catch both:
|
|
1105
1126
|
|
|
1106
1127
|
```python
|
|
1107
1128
|
try:
|
|
@@ -1112,8 +1133,6 @@ except (psycopg.IntegrityError, ValidationError):
|
|
|
1112
1133
|
|
|
1113
1134
|
For a plain insert-or-update with no per-row logic, `bulk_create(..., update_conflicts=True, unique_fields=[...])` is an atomic upsert with no race to catch.
|
|
1114
1135
|
|
|
1115
|
-
Two caveats. The mapping covers **immediate** constraints — the default. An explicitly deferred constraint (`UniqueConstraint(deferrable=Deferrable.DEFERRED)`) is checked at commit, _after_ the write returns, so its violation still surfaces as a raw `psycopg.IntegrityError`. And when a row violates several constraints at once, a form's pre-check (or an explicit `validate_constraints()`) reports them all, while a direct `create()`/`update()` gets only the first one the database hits.
|
|
1116
|
-
|
|
1117
1136
|
### Indexes and constraints
|
|
1118
1137
|
|
|
1119
1138
|
You can optimize queries and ensure data integrity with indexes and constraints. These are managed automatically by [convergence](#convergence) — just declare them on the model and run `postgres sync`.
|
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.116.0](https://github.com/dropseed/plain/releases/plain-postgres@0.116.0) (2026-09-04)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- **`Meta` now exposes exactly two forward-field collections: `fields` and `many_to_many`.** `local_fields`, `local_many_to_many`, `concrete_fields`, and `local_concrete_fields` are gone. Plain has no model inheritance and no non-concrete forward fields, so "local" and "concrete" drew distinctions that could never differ — four names for two lists, each with its own cached property and its own filtering pass. `fields` is every column-backed field (primary key first, then sorted by name) and `many_to_many` is every `ManyToManyField` (sorted by name); both now read straight from the storage `add_field()` fills, instead of re-filtering `_get_fields()` on each access ([9f0d0da22d](https://github.com/dropseed/plain/commit/9f0d0da22d))
|
|
8
|
+
- `Meta.fields` is typed as `ImmutableList[ColumnField]` and `Meta.many_to_many` as `ImmutableList[ManyToManyField]`, so the column attributes (`column`, `db_type()`, `primary_key`) are readable off a field without an `isinstance` check first. `add_field()` enforces that by raising `TypeError` for a field that is neither a `ColumnField` nor a `ManyToManyField` ([996c099e2b](https://github.com/dropseed/plain/commit/996c099e2b), [9f0d0da22d](https://github.com/dropseed/plain/commit/9f0d0da22d))
|
|
9
|
+
- The `postgres.non_local_field_reference` preflight check is removed. It rejected an `indexes` or `constraints` entry naming a field "not local to" the model — unreachable without inheritance, since a forward field found on the model is always the model's own ([9f0d0da22d](https://github.com/dropseed/plain/commit/9f0d0da22d))
|
|
10
|
+
- `Field.name` is typed `str` instead of `str | None` — it starts as `""` and becomes the real name in `set_attributes_from_name()`. That drops roughly twenty `assert self.name is not None` guards from the field descriptor, save, and preflight paths. `Field.deconstruct()` (and the `ColumnField`, `DefaultableField`, `ChoicesField`, `RelatedField`, `ForeignKeyField`, and `ManyToManyField` overrides) returns `tuple[str, str, list, dict]` accordingly ([51cb71f758](https://github.com/dropseed/plain/commit/51cb71f758))
|
|
11
|
+
- `get_joining_columns()` moved from `ForeignObjectRel` to `ForeignKeyRel`. It delegates to `field.get_joining_columns(reverse_join=True)`, which only a foreign key has ([51cb71f758](https://github.com/dropseed/plain/commit/51cb71f758))
|
|
12
|
+
- `PathInfo.join_field` is typed `ForeignKeyField | ForeignKeyRel` (was `ForeignKeyField | ForeignObjectRel`), and `select_related_descend()` is a `TypeGuard[RelatedField]` so a passing field narrows at the call site ([51cb71f758](https://github.com/dropseed/plain/commit/51cb71f758))
|
|
13
|
+
- `QuerySet._update()`, `_insert()`, `_batched_insert()`, `UpdateQuery.add_update_fields()`, and `InsertQuery.insert_values()` accept a `Sequence` of fields rather than requiring a `list`, so an `ImmutableList` from `Meta` can be passed straight through ([51cb71f758](https://github.com/dropseed/plain/commit/51cb71f758))
|
|
14
|
+
|
|
15
|
+
### Upgrade instructions
|
|
16
|
+
|
|
17
|
+
- Replace `_model_meta.local_fields`, `_model_meta.concrete_fields`, and `_model_meta.local_concrete_fields` with `_model_meta.fields`. In Plain these were always the same set of fields.
|
|
18
|
+
- Replace `_model_meta.local_many_to_many` with `_model_meta.many_to_many`.
|
|
19
|
+
- Replace `_model_meta._non_pk_concrete_field_names` with `_model_meta._non_pk_field_names`.
|
|
20
|
+
- If you have a custom `Field` subclass, `deconstruct()` now returns `str` as its first element rather than `str | None`, and `self.name` is `""` (not `None`) before the field is contributed to a model class. Update any annotation or `is None` check accordingly.
|
|
21
|
+
- If you called `get_joining_columns()` on a reverse relation, it is now available on `ForeignKeyRel` only.
|
|
22
|
+
|
|
23
|
+
## [0.115.0](https://github.com/dropseed/plain/releases/plain-postgres@0.115.0) (2026-09-02)
|
|
24
|
+
|
|
25
|
+
### What's changed
|
|
26
|
+
|
|
27
|
+
- **Foreign keys are now `NOT DEFERRABLE`** (Postgres's own default) and checked at each write. Plain inherited Django's `DEFERRABLE INITIALLY DEFERRED` foreign keys, added in 2007 so fixtures could contain forward references. Plain has no fixtures, and the deferral was causing real problems: a migration that adds a column, backfills it in `RunPython`, then alters the same table failed with "cannot ALTER TABLE ... because it has pending trigger events" whenever the backfill touched a row, and FK violations only surfaced at `COMMIT` with a traceback pointing at `atomic.__exit__` instead of the offending write ([8cbfa0cb40](https://github.com/dropseed/plain/commit/8cbfa0cb40))
|
|
28
|
+
- Convergence detects an existing `DEFERRABLE` foreign key as drift and fixes it with a catalog-only `ALTER CONSTRAINT ... NOT DEFERRABLE` — no revalidation scan, just a brief `ACCESS EXCLUSIVE` lock bounded by the usual `lock_timeout` — so the first `postgres sync` after upgrading rewrites every FK quickly. A legacy `DEFERRABLE` + `NOT VALID` foreign key gets both corrections in one pass ([8cbfa0cb40](https://github.com/dropseed/plain/commit/8cbfa0cb40))
|
|
29
|
+
- `NO_ACTION` is removed. Its only meaning was deferred checking — use `RESTRICT` ([8cbfa0cb40](https://github.com/dropseed/plain/commit/8cbfa0cb40))
|
|
30
|
+
- A foreign key violation from `create()`/`update()` now surfaces as a `ValidationError` routed to the `ForeignKeyField` — "Author with id 5 does not exist." — with code `invalid_choice` so forms can override it. It lands in the same `IntegrityError` mapping that unique and check constraints already used, so a missing related row reports like any other field error instead of a 500. The message is `ForeignKeyField.does_not_exist_error_message`. Set-based writes (`QuerySet.update()`, `bulk_create()`) and a `delete()` blocked by `RESTRICT` still raise the raw `psycopg` error, since there's no instance to attribute it to ([31df77e665](https://github.com/dropseed/plain/commit/31df77e665))
|
|
31
|
+
- That mapping keys on the foreign key's generated constraint name, so convergence now treats a stale name — left behind by `RenameField`/`RenameModel`, which only rename the column or table — as drift and renames the constraint. Because the write-path mapping depends on those corrections having run, FK renames and the `NOT DEFERRABLE` flip block sync on failure. `generate_fk_constraint_name()` moved to `plain.postgres.utils` and is now the single source of that name for both convergence and the write path ([31df77e665](https://github.com/dropseed/plain/commit/31df77e665))
|
|
32
|
+
- **`UniqueConstraint(deferrable=...)` and the `Deferrable` enum are removed.** With foreign keys immediate, this was the last deferred-constraint concept in Plain, nothing in Plain's own packages used it, and it forced a caveat onto every "constraints are checked at the write" statement while keeping the pending-trigger-events failure alive for one case. Rails (pre-7.1), Ecto, and Laravel never had it for uniques. Every constraint is now checked at the write that violates it, never at commit ([fd1e7f1e13](https://github.com/dropseed/plain/commit/fd1e7f1e13))
|
|
33
|
+
- `DatabaseConnection.check_constraints()` is gone, and the pytest `db` fixture no longer forces deferred constraints to immediate at teardown — there is nothing left to defer ([fd1e7f1e13](https://github.com/dropseed/plain/commit/fd1e7f1e13))
|
|
34
|
+
- Constraint renames now run in convergence pass 1, matching index renames, so anything else planned for the same constraint in that pass addresses it by its new name ([fd1e7f1e13](https://github.com/dropseed/plain/commit/fd1e7f1e13))
|
|
35
|
+
- New README section, "Constraints are checked immediately", covering the patterns this changes: create parents before children, use a nullable back-reference for a foreign key cycle (two required FKs pointing at each other can never be inserted), and move through a temporary value when swapping two rows under a unique constraint, since Postgres checks uniqueness per row even within a single `UPDATE` ([8cbfa0cb40](https://github.com/dropseed/plain/commit/8cbfa0cb40), [fd1e7f1e13](https://github.com/dropseed/plain/commit/fd1e7f1e13))
|
|
36
|
+
|
|
37
|
+
### Upgrade instructions
|
|
38
|
+
|
|
39
|
+
- Replace `NO_ACTION` with `RESTRICT` in your models **and in any historical migration file that references it** — `postgres.NO_ACTION` no longer exists, so a migration importing it fails to load.
|
|
40
|
+
- Remove `deferrable=` from every `UniqueConstraint(...)` and drop any `Deferrable` import. An existing deferred unique constraint in the database shows as CHANGED drift with staged-rollout guidance — nothing is rewritten silently.
|
|
41
|
+
- Run `plain postgres sync` after upgrading. It flips every existing foreign key to `NOT DEFERRABLE` (catalog-only, no table scan) and renames foreign key constraints left stale by past `RenameField`/`RenameModel`.
|
|
42
|
+
- Code that inserted a child row before its parent, or deleted a parent and re-pointed its children later in the same transaction, now raises `ForeignKeyViolation` at that statement rather than succeeding by commit time. Create parents before children; for a foreign key cycle, create both rows and then `update()` the nullable back-reference.
|
|
43
|
+
- If you catch `psycopg.IntegrityError` around `create()`/`update()` to handle a missing related row, catch `ValidationError` instead — or both, if the same block also covers set-based writes.
|
|
44
|
+
- If you called `connection.check_constraints()`, remove the call.
|
|
45
|
+
|
|
3
46
|
## [0.114.0](https://github.com/dropseed/plain/releases/plain-postgres@0.114.0) (2026-08-12)
|
|
4
47
|
|
|
5
48
|
### What's changed
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: plain.postgres
|
|
3
|
-
Version: 0.114.0
|
|
4
|
-
Summary: Model your data and store it in a database.
|
|
5
|
-
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
-
License-Expression: BSD-3-Clause
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Requires-Python: >=3.13
|
|
9
|
-
Requires-Dist: plain<1.0.0,>=0.157.0
|
|
10
|
-
Requires-Dist: psycopg-pool>=3.2
|
|
11
|
-
Requires-Dist: psycopg>=3.2
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
|
|
14
1
|
# plain.postgres
|
|
15
2
|
|
|
16
3
|
**Model your data and store it in a database.**
|
|
@@ -996,6 +983,14 @@ The first access to any non-key field loads the whole row in a single query. The
|
|
|
996
983
|
|
|
997
984
|
The partial-instance shortcut is safe because Plain always creates a database foreign-key constraint, so the referenced row is guaranteed to exist.
|
|
998
985
|
|
|
986
|
+
### Constraints are checked immediately
|
|
987
|
+
|
|
988
|
+
Every constraint is checked at the write that violates it, never at commit — the same as Postgres's own default. Inserting a child row that points at a parent that doesn't exist yet fails at that `INSERT`, with a traceback pointing at the offending write, and deleting a parent with `RESTRICT` children fails at that `DELETE`. Create parents before children. A cycle of foreign keys needs a nullable back-reference: create both rows, then `update()` the back-reference. Two required foreign keys pointing at each other can never be inserted.
|
|
989
|
+
|
|
990
|
+
To swap two rows' values under a unique constraint (reordering by `position`, say), move one row to a temporary value first — a single `UPDATE` that swaps them fails, because Postgres checks uniqueness per row.
|
|
991
|
+
|
|
992
|
+
A migration can add a column, backfill it in `RunPython`, and drop or alter columns on the same table, all in one transaction.
|
|
993
|
+
|
|
999
994
|
### Reverse relationships
|
|
1000
995
|
|
|
1001
996
|
When you define a `ForeignKey` or `ManyToManyField`, Plain automatically creates a reverse accessor on the related model (like `author.book_set`). You can explicitly declare these reverse relationships using [`ReverseForeignKey`](./fields/reverse_descriptors.py#ReverseForeignKey) and [`ReverseManyToMany`](./fields/reverse_descriptors.py#ReverseManyToMany):
|
|
@@ -1100,7 +1095,7 @@ class User(postgres.Model):
|
|
|
1100
1095
|
|
|
1101
1096
|
Field-level validation happens automatically based on field types and constraints.
|
|
1102
1097
|
|
|
1103
|
-
**The database is authoritative for constraints.** `create()`/`update()` don't pre-check your declared unique/check constraints — they attempt the write, and if Postgres rejects it, translate the `IntegrityError` into a `ValidationError` (routed to the field for single-column uniques, `NON_FIELD_ERRORS` otherwise). You get the same field-level error you'd expect, the write costs no per-constraint `SELECT`, and a raced concurrent insert can't slip through as a 500.
|
|
1098
|
+
**The database is authoritative for constraints.** `create()`/`update()` don't pre-check your declared unique/check constraints — they attempt the write, and if Postgres rejects it, translate the `IntegrityError` into a `ValidationError` (routed to the field for single-column uniques, `NON_FIELD_ERRORS` otherwise). You get the same field-level error you'd expect, the write costs no per-constraint `SELECT`, and a raced concurrent insert can't slip through as a 500. A foreign key pointing at a row that doesn't exist maps the same way, as an error on that field. (`NOT NULL` and a hand-set primary-key collision have no declared constraint to map to and re-raise as the original `IntegrityError`. `create()` always inserts, so passing a stray `id` that already exists is rejected by Postgres as the original `IntegrityError`.)
|
|
1104
1099
|
|
|
1105
1100
|
Because the rejected write reaches the database, it aborts the surrounding transaction. If you catch the `ValidationError` and want to keep using the transaction, wrap the write in `transaction.atomic()` so it rolls back to a savepoint:
|
|
1106
1101
|
|
|
@@ -1114,7 +1109,7 @@ except ValidationError:
|
|
|
1114
1109
|
|
|
1115
1110
|
Forms are the exception: a `ModelForm` pre-checks constraints explicitly (a `validate_constraints()` call in its `_post_clean`) so it can surface every violation at once, then writes via `form.create()`/`form.update()` with validation already done. A direct `create()`/`update()` reports the first violation Postgres hits.
|
|
1116
1111
|
|
|
1117
|
-
This applies to instance writes only. Set-based writes — `QuerySet.update()` and `bulk_create()` — raise the raw `psycopg.IntegrityError`, since there's no instance to attribute the error to. If you retry on a unique conflict, catch both:
|
|
1112
|
+
This applies to instance writes only. Set-based writes — `QuerySet.update()` and `bulk_create()` — raise the raw `psycopg.IntegrityError`, since there's no instance to attribute the error to, and so does a `delete()` blocked by `RESTRICT` children. If you retry on a unique conflict, catch both:
|
|
1118
1113
|
|
|
1119
1114
|
```python
|
|
1120
1115
|
try:
|
|
@@ -1125,8 +1120,6 @@ except (psycopg.IntegrityError, ValidationError):
|
|
|
1125
1120
|
|
|
1126
1121
|
For a plain insert-or-update with no per-row logic, `bulk_create(..., update_conflicts=True, unique_fields=[...])` is an atomic upsert with no race to catch.
|
|
1127
1122
|
|
|
1128
|
-
Two caveats. The mapping covers **immediate** constraints — the default. An explicitly deferred constraint (`UniqueConstraint(deferrable=Deferrable.DEFERRED)`) is checked at commit, _after_ the write returns, so its violation still surfaces as a raw `psycopg.IntegrityError`. And when a row violates several constraints at once, a form's pre-check (or an explicit `validate_constraints()`) reports them all, while a direct `create()`/`update()` gets only the first one the database hits.
|
|
1129
|
-
|
|
1130
1123
|
### Indexes and constraints
|
|
1131
1124
|
|
|
1132
1125
|
You can optimize queries and ensure data integrity with indexes and constraints. These are managed automatically by [convergence](#convergence) — just declare them on the model and run `postgres sync`.
|
|
@@ -12,7 +12,7 @@ from .base import Model
|
|
|
12
12
|
from .constraints import CheckConstraint, UniqueConstraint
|
|
13
13
|
from .db import get_connection, use_management_connection
|
|
14
14
|
from .middleware import DatabaseConnectionMiddleware
|
|
15
|
-
from .deletion import CASCADE,
|
|
15
|
+
from .deletion import CASCADE, RESTRICT, SET_NULL
|
|
16
16
|
from .expressions import F
|
|
17
17
|
from .enums import TextChoices
|
|
18
18
|
from .fields import (
|
|
@@ -57,7 +57,6 @@ from . import types
|
|
|
57
57
|
# imported from their specific modules (e.g., plain.postgres.aggregates).
|
|
58
58
|
__all__ = [
|
|
59
59
|
"CASCADE",
|
|
60
|
-
"NO_ACTION",
|
|
61
60
|
"RESTRICT",
|
|
62
61
|
"SET_NULL",
|
|
63
62
|
"BigIntegerField",
|
|
@@ -69,7 +69,7 @@ Use `Model.query` to build querysets (e.g., `User.query.filter(is_active=True)`)
|
|
|
69
69
|
- Use `.values_list()` when you only need specific columns
|
|
70
70
|
- Wrap multi-step writes in `transaction.atomic()`
|
|
71
71
|
- Instance writes are `obj.create()` (always INSERT) and `obj.update()` (always UPDATE; `update(fields=[...])` limits the columns) — there is no `save()`, `force_insert`, or `force_update`. Constructing an instance then `create()`-ing it inserts; a hand-set `id` that collides raises `IntegrityError`.
|
|
72
|
-
- `create()`/`update()` raise `ValidationError` (not raw `psycopg.IntegrityError`) on a declared unique/check constraint violation, even a raced one — the DB enforces it, so inside an open `transaction.atomic()` the violation aborts the transaction (wrap the write in its own `atomic()` to catch and keep using the transaction). Set-based writes (`QuerySet.update()`/`bulk_create()`) raise raw `psycopg.IntegrityError`. Retrying on conflict? `except (psycopg.IntegrityError, ValidationError)`, or `bulk_create(..., update_conflicts=True)`
|
|
72
|
+
- `create()`/`update()` raise `ValidationError` (not raw `psycopg.IntegrityError`) on a declared unique/check constraint violation or a foreign key pointing at a missing row, even a raced one — the DB enforces it, so inside an open `transaction.atomic()` the violation aborts the transaction (wrap the write in its own `atomic()` to catch and keep using the transaction). Set-based writes (`QuerySet.update()`/`bulk_create()`) and `delete()` blocked by `RESTRICT` raise raw `psycopg.IntegrityError`. Retrying on conflict? `except (psycopg.IntegrityError, ValidationError)`, or `bulk_create(..., update_conflicts=True)`
|
|
73
73
|
- Always paginate list queries — unbounded querysets get slower as data grows
|
|
74
74
|
|
|
75
75
|
Run `uv run plain docs postgres` for full patterns with code examples.
|
|
@@ -81,6 +81,7 @@ Run `uv run plain docs postgres` for full patterns with code examples.
|
|
|
81
81
|
- Indexes: `{table}_{column(s)}_idx`
|
|
82
82
|
- Constraints: `{table}_{column(s)}_{type}` (e.g., `_unique`, `_check`)
|
|
83
83
|
- Choose `on_delete` deliberately: CASCADE for owned children, RESTRICT for referenced data, SET_NULL for optional references
|
|
84
|
+
- Foreign keys are checked at each write, not at commit — create parents before children. A backfill followed by DDL on the same table in one migration is fine.
|
|
84
85
|
- No `allow_null` on string fields — use `default=""`. Optional string fields are `required=False, default=""` — the declared default is what lets the column be added to a populated table (`required=False` alone only affects Python-side validation)
|
|
85
86
|
|
|
86
87
|
Run `uv run plain docs postgres` for full patterns with code examples.
|
|
@@ -27,7 +27,6 @@ from plain.postgres.exceptions import (
|
|
|
27
27
|
)
|
|
28
28
|
from plain.postgres.expressions import RawSQL, Value
|
|
29
29
|
from plain.postgres.fields import DATABASE_DEFAULT, Field
|
|
30
|
-
from plain.postgres.fields.base import ColumnField
|
|
31
30
|
from plain.postgres.fields.related import RelatedField
|
|
32
31
|
from plain.postgres.fields.reverse_related import ForeignObjectRel
|
|
33
32
|
from plain.postgres.meta import Meta
|
|
@@ -123,10 +122,6 @@ class Model(metaclass=ModelBase):
|
|
|
123
122
|
for field in meta.fields:
|
|
124
123
|
from plain.postgres.fields.related import RelatedField
|
|
125
124
|
|
|
126
|
-
# meta.fields excludes ManyToManyField, so every iterated field
|
|
127
|
-
# is column-backed and exposes the ColumnField surface.
|
|
128
|
-
assert isinstance(field, ColumnField)
|
|
129
|
-
|
|
130
125
|
is_related_object = False
|
|
131
126
|
# Virtual field
|
|
132
127
|
if field.name not in kwargs and field.column is None:
|
|
@@ -193,16 +188,14 @@ class Model(metaclass=ModelBase):
|
|
|
193
188
|
|
|
194
189
|
@classmethod
|
|
195
190
|
def from_db(cls, field_names: Iterable[str], values: Sequence[Any]) -> Model:
|
|
196
|
-
if len(values) != len(cls._model_meta.
|
|
191
|
+
if len(values) != len(cls._model_meta.fields):
|
|
197
192
|
values_iter = iter(values)
|
|
198
193
|
values = [
|
|
199
194
|
next(values_iter) if f.name in field_names else DEFERRED
|
|
200
|
-
for f in cls._model_meta.
|
|
195
|
+
for f in cls._model_meta.fields
|
|
201
196
|
]
|
|
202
197
|
# Build kwargs dict from field names and values
|
|
203
|
-
field_dict = dict(
|
|
204
|
-
zip((f.name for f in cls._model_meta.concrete_fields), values)
|
|
205
|
-
)
|
|
198
|
+
field_dict = dict(zip((f.name for f in cls._model_meta.fields), values))
|
|
206
199
|
new = cls(_from_db=True, **field_dict)
|
|
207
200
|
new._state.adding = False
|
|
208
201
|
return new
|
|
@@ -279,11 +272,7 @@ class Model(metaclass=ModelBase):
|
|
|
279
272
|
"""
|
|
280
273
|
Return a set containing names of deferred fields for this instance.
|
|
281
274
|
"""
|
|
282
|
-
return {
|
|
283
|
-
f.name
|
|
284
|
-
for f in self._model_meta.concrete_fields
|
|
285
|
-
if f.name not in self.__dict__
|
|
286
|
-
}
|
|
275
|
+
return {f.name for f in self._model_meta.fields if f.name not in self.__dict__}
|
|
287
276
|
|
|
288
277
|
def refresh_from_db(self, fields: list[str] | None = None) -> None:
|
|
289
278
|
"""
|
|
@@ -320,15 +309,13 @@ class Model(metaclass=ModelBase):
|
|
|
320
309
|
db_instance_qs = db_instance_qs.only(*fields)
|
|
321
310
|
elif deferred_fields:
|
|
322
311
|
fields = [
|
|
323
|
-
f.name
|
|
324
|
-
for f in self._model_meta.concrete_fields
|
|
325
|
-
if f.name not in deferred_fields
|
|
312
|
+
f.name for f in self._model_meta.fields if f.name not in deferred_fields
|
|
326
313
|
]
|
|
327
314
|
db_instance_qs = db_instance_qs.only(*fields)
|
|
328
315
|
|
|
329
316
|
db_instance = db_instance_qs.get()
|
|
330
317
|
non_loaded_fields = db_instance.get_deferred_fields()
|
|
331
|
-
for field in self._model_meta.
|
|
318
|
+
for field in self._model_meta.fields:
|
|
332
319
|
if field.name in non_loaded_fields:
|
|
333
320
|
# This field wasn't refreshed - skip ahead.
|
|
334
321
|
continue
|
|
@@ -404,7 +391,7 @@ class Model(metaclass=ModelBase):
|
|
|
404
391
|
if not fields:
|
|
405
392
|
return self # explicit "update nothing" -- no-op
|
|
406
393
|
fields = frozenset(fields)
|
|
407
|
-
field_names = self._model_meta.
|
|
394
|
+
field_names = self._model_meta._non_pk_field_names
|
|
408
395
|
non_model_fields = fields.difference(field_names)
|
|
409
396
|
if non_model_fields:
|
|
410
397
|
raise ValueError(
|
|
@@ -420,7 +407,7 @@ class Model(metaclass=ModelBase):
|
|
|
420
407
|
)
|
|
421
408
|
elif deferred_fields:
|
|
422
409
|
# Loaded via .only()/.defer() -- write just the loaded fields.
|
|
423
|
-
loaded = self._model_meta.
|
|
410
|
+
loaded = self._model_meta._non_pk_field_names - deferred_fields
|
|
424
411
|
if loaded:
|
|
425
412
|
fields = loaded
|
|
426
413
|
self._prepare_related_fields_for_save(
|
|
@@ -438,16 +425,20 @@ class Model(metaclass=ModelBase):
|
|
|
438
425
|
"""
|
|
439
426
|
Map a Postgres constraint violation back to the constraint that raised
|
|
440
427
|
it and return the ValidationError the in-Python check produces for that
|
|
441
|
-
constraint, or None when the violation doesn't
|
|
442
|
-
constraint that can describe it (PK
|
|
443
|
-
NULL — which carries no constraint name —
|
|
444
|
-
re-raise as the original IntegrityError). A PK
|
|
445
|
-
when create() inserts a hand-set id that's
|
|
428
|
+
constraint or foreign key, or None when the violation doesn't
|
|
429
|
+
correspond to a declared constraint that can describe it (PK
|
|
430
|
+
collisions and NOT NULL — which carries no constraint name — fall
|
|
431
|
+
through to None and re-raise as the original IntegrityError). A PK
|
|
432
|
+
collision reaches here when create() inserts a hand-set id that's
|
|
433
|
+
already taken.
|
|
446
434
|
"""
|
|
447
435
|
constraint_name = exc.diag.constraint_name
|
|
448
436
|
if not constraint_name:
|
|
449
437
|
return None
|
|
450
|
-
|
|
438
|
+
meta = self._model_meta
|
|
439
|
+
constraint = meta.constraints_by_name.get(
|
|
440
|
+
constraint_name
|
|
441
|
+
) or meta.foreign_keys_by_constraint_name.get(constraint_name)
|
|
451
442
|
if constraint is None:
|
|
452
443
|
return None
|
|
453
444
|
error = constraint._db_violation_error(self, self.__class__)
|
|
@@ -480,7 +471,7 @@ class Model(metaclass=ModelBase):
|
|
|
480
471
|
fields from RETURNING. Omits id from the INSERT when unset so Postgres
|
|
481
472
|
generates the identity value."""
|
|
482
473
|
meta = self._model_meta
|
|
483
|
-
fields = list(meta.
|
|
474
|
+
fields = list(meta.fields)
|
|
484
475
|
if self.id is None:
|
|
485
476
|
id_field = meta.get_forward_field("id")
|
|
486
477
|
fields = [f for f in fields if f is not id_field]
|
|
@@ -490,7 +481,6 @@ class Model(metaclass=ModelBase):
|
|
|
490
481
|
)
|
|
491
482
|
if results:
|
|
492
483
|
for value, field in zip(results[0], returning_fields):
|
|
493
|
-
assert field.name is not None
|
|
494
484
|
setattr(self, field.name, value)
|
|
495
485
|
|
|
496
486
|
def _update_row(self, fields: Iterable[str] | None) -> None:
|
|
@@ -498,7 +488,7 @@ class Model(metaclass=ModelBase):
|
|
|
498
488
|
row matched -- update() targets an existing row and has no INSERT
|
|
499
489
|
fallback (that's create())."""
|
|
500
490
|
meta = self._model_meta
|
|
501
|
-
non_pks = [f for f in meta.
|
|
491
|
+
non_pks = [f for f in meta.fields if not f.primary_key]
|
|
502
492
|
if fields:
|
|
503
493
|
non_pks = [f for f in non_pks if f.name in fields]
|
|
504
494
|
values = [(f, f.pre_save(self, False)) for f in non_pks]
|
|
@@ -521,7 +511,7 @@ class Model(metaclass=ModelBase):
|
|
|
521
511
|
) -> None:
|
|
522
512
|
# Ensure that a model instance without a PK hasn't been assigned to
|
|
523
513
|
# a ForeignKeyField on this model. If the field is nullable, allowing the save would result in silent data loss.
|
|
524
|
-
for field in self._model_meta.
|
|
514
|
+
for field in self._model_meta.fields:
|
|
525
515
|
if field_names and field.name not in field_names:
|
|
526
516
|
continue
|
|
527
517
|
# If the related field isn't cached, then an instance hasn't been
|
|
@@ -531,11 +521,9 @@ class Model(metaclass=ModelBase):
|
|
|
531
521
|
if not obj:
|
|
532
522
|
continue
|
|
533
523
|
# A pk may have been assigned manually to a model instance not
|
|
534
|
-
# saved to the database
|
|
535
|
-
#
|
|
536
|
-
#
|
|
537
|
-
# constraints aren't supported by the database, there's the
|
|
538
|
-
# unavoidable risk of data corruption.
|
|
524
|
+
# saved to the database, but we allow the write to proceed and
|
|
525
|
+
# rely on the database's foreign key check (mapped to a
|
|
526
|
+
# ValidationError on the field) if the row doesn't exist.
|
|
539
527
|
if obj.id is None:
|
|
540
528
|
raise ValueError(
|
|
541
529
|
f"{operation_name}() prohibited to prevent data loss due to unsaved "
|
|
@@ -570,13 +558,12 @@ class Model(metaclass=ModelBase):
|
|
|
570
558
|
# should always be deletable — custom querysets shape reads, not
|
|
571
559
|
# internal row lifecycle operations.
|
|
572
560
|
#
|
|
573
|
-
# mark_for_rollback_on_error:
|
|
574
|
-
#
|
|
575
|
-
#
|
|
561
|
+
# mark_for_rollback_on_error: RESTRICT violations leave the DB
|
|
562
|
+
# transaction aborted. Mark the connection so outer atomic() blocks
|
|
563
|
+
# see the abort state even if the caller catches IntegrityError.
|
|
576
564
|
with transaction.mark_for_rollback_on_error():
|
|
577
565
|
count = self._model_meta.base_queryset.filter(id=self.id)._raw_delete()
|
|
578
566
|
id_field = self._model_meta.get_forward_field("id")
|
|
579
|
-
assert id_field.name is not None
|
|
580
567
|
setattr(self, id_field.name, None)
|
|
581
568
|
# Only the id is cleared -- every other field value survives so callers
|
|
582
569
|
# can still reference a deleted row (correlate it, log it, check it's
|
|
@@ -609,7 +596,7 @@ class Model(metaclass=ModelBase):
|
|
|
609
596
|
meta = meta or self._model_meta
|
|
610
597
|
return {
|
|
611
598
|
field.name: Value(field.value_from_object(self), field)
|
|
612
|
-
for field in meta.
|
|
599
|
+
for field in meta.fields
|
|
613
600
|
if field.name not in exclude
|
|
614
601
|
}
|
|
615
602
|
|
|
@@ -753,9 +740,9 @@ class Model(metaclass=ModelBase):
|
|
|
753
740
|
def _check_fields(cls) -> list[PreflightResult]:
|
|
754
741
|
"""Perform all field checks."""
|
|
755
742
|
errors: list[PreflightResult] = []
|
|
756
|
-
for field in cls._model_meta.
|
|
743
|
+
for field in cls._model_meta.fields:
|
|
757
744
|
errors.extend(field.preflight(from_model=cls))
|
|
758
|
-
for field in cls._model_meta.
|
|
745
|
+
for field in cls._model_meta.many_to_many:
|
|
759
746
|
errors.extend(field.preflight(from_model=cls))
|
|
760
747
|
return errors
|
|
761
748
|
|
|
@@ -766,7 +753,7 @@ class Model(metaclass=ModelBase):
|
|
|
766
753
|
errors: list[PreflightResult] = []
|
|
767
754
|
seen_intermediary_signatures = []
|
|
768
755
|
|
|
769
|
-
fields = cls._model_meta.
|
|
756
|
+
fields = cls._model_meta.many_to_many
|
|
770
757
|
|
|
771
758
|
# Skip when the target model wasn't found.
|
|
772
759
|
fields = (f for f in fields if isinstance(f.remote_field.model, ModelBase))
|
|
@@ -798,9 +785,7 @@ class Model(metaclass=ModelBase):
|
|
|
798
785
|
def _check_id_field(cls) -> list[PreflightResult]:
|
|
799
786
|
"""Disallow user-defined fields named ``id``."""
|
|
800
787
|
if any(
|
|
801
|
-
f
|
|
802
|
-
for f in cls._model_meta.local_fields
|
|
803
|
-
if f.name == "id" and not f.auto_created
|
|
788
|
+
f for f in cls._model_meta.fields if f.name == "id" and not f.auto_created
|
|
804
789
|
):
|
|
805
790
|
return [
|
|
806
791
|
PreflightResult(
|
|
@@ -817,7 +802,7 @@ class Model(metaclass=ModelBase):
|
|
|
817
802
|
errors: list[PreflightResult] = []
|
|
818
803
|
used_fields = {} # name -> field
|
|
819
804
|
|
|
820
|
-
for f in cls._model_meta.
|
|
805
|
+
for f in cls._model_meta.fields:
|
|
821
806
|
clash = used_fields.get(f.name)
|
|
822
807
|
# Note that we may detect clash between user-defined non-unique
|
|
823
808
|
# field "id" and automatically added unique field "id", both
|
|
@@ -845,7 +830,7 @@ class Model(metaclass=ModelBase):
|
|
|
845
830
|
used_column_names: list[str] = []
|
|
846
831
|
errors: list[PreflightResult] = []
|
|
847
832
|
|
|
848
|
-
for f in cls._model_meta.
|
|
833
|
+
for f in cls._model_meta.fields:
|
|
849
834
|
column_name = f.column
|
|
850
835
|
|
|
851
836
|
# Ensure the column name is not already in use.
|
|
@@ -913,7 +898,7 @@ class Model(metaclass=ModelBase):
|
|
|
913
898
|
@classmethod
|
|
914
899
|
def _check_single_primary_key(cls) -> list[PreflightResult]:
|
|
915
900
|
errors: list[PreflightResult] = []
|
|
916
|
-
if sum(1 for f in cls._model_meta.
|
|
901
|
+
if sum(1 for f in cls._model_meta.fields if f.primary_key) > 1:
|
|
917
902
|
errors.append(
|
|
918
903
|
PreflightResult(
|
|
919
904
|
fix="The model cannot have more than one field with "
|
|
@@ -964,11 +949,11 @@ class Model(metaclass=ModelBase):
|
|
|
964
949
|
include for index in cls.model_options.indexes for include in index.include
|
|
965
950
|
]
|
|
966
951
|
fields += references
|
|
967
|
-
errors.extend(cls.
|
|
952
|
+
errors.extend(cls._check_referenced_fields(fields, "indexes"))
|
|
968
953
|
return errors
|
|
969
954
|
|
|
970
955
|
@classmethod
|
|
971
|
-
def
|
|
956
|
+
def _check_referenced_fields(
|
|
972
957
|
cls, fields: Iterable[str], option: str
|
|
973
958
|
) -> list[PreflightResult]:
|
|
974
959
|
# In order to avoid hitting the relation tree prematurely, we use our
|
|
@@ -1001,15 +986,6 @@ class Model(metaclass=ModelBase):
|
|
|
1001
986
|
id="postgres.m2m_field_in_meta_option",
|
|
1002
987
|
)
|
|
1003
988
|
)
|
|
1004
|
-
elif field not in cls._model_meta.local_fields:
|
|
1005
|
-
errors.append(
|
|
1006
|
-
PreflightResult(
|
|
1007
|
-
fix=f"'{option}' refers to field '{field_name}' which is not local to model "
|
|
1008
|
-
f"'{cls.model_options.object_name}'.",
|
|
1009
|
-
obj=cls,
|
|
1010
|
-
id="postgres.non_local_field_reference",
|
|
1011
|
-
)
|
|
1012
|
-
)
|
|
1013
989
|
return errors
|
|
1014
990
|
|
|
1015
991
|
@classmethod
|
|
@@ -1087,9 +1063,9 @@ class Model(metaclass=ModelBase):
|
|
|
1087
1063
|
meta = cls._model_meta
|
|
1088
1064
|
valid_fields = set(
|
|
1089
1065
|
chain.from_iterable(
|
|
1090
|
-
(f.
|
|
1091
|
-
if
|
|
1092
|
-
else (f.
|
|
1066
|
+
(f.field.related_query_name(),)
|
|
1067
|
+
if isinstance(f, ForeignObjectRel)
|
|
1068
|
+
else (f.name,)
|
|
1093
1069
|
for f in chain(meta.fields, meta.related_objects)
|
|
1094
1070
|
)
|
|
1095
1071
|
)
|
|
@@ -1119,7 +1095,7 @@ class Model(metaclass=ModelBase):
|
|
|
1119
1095
|
# silently truncate, so we check for names that are too long
|
|
1120
1096
|
allowed_len = MAX_NAME_LENGTH
|
|
1121
1097
|
|
|
1122
|
-
for f in cls._model_meta.
|
|
1098
|
+
for f in cls._model_meta.fields:
|
|
1123
1099
|
column_name = f.column
|
|
1124
1100
|
|
|
1125
1101
|
# Check if column name is too long for the database.
|
|
@@ -1133,13 +1109,13 @@ class Model(metaclass=ModelBase):
|
|
|
1133
1109
|
)
|
|
1134
1110
|
)
|
|
1135
1111
|
|
|
1136
|
-
for f in cls._model_meta.
|
|
1112
|
+
for f in cls._model_meta.many_to_many:
|
|
1137
1113
|
# Skip nonexistent models.
|
|
1138
1114
|
if isinstance(f.remote_field.through, str):
|
|
1139
1115
|
continue
|
|
1140
1116
|
|
|
1141
1117
|
# Check if column name for the M2M field is too long for the database.
|
|
1142
|
-
for m2m in f.remote_field.through._model_meta.
|
|
1118
|
+
for m2m in f.remote_field.through._model_meta.fields:
|
|
1143
1119
|
rel_name = m2m.column
|
|
1144
1120
|
if rel_name is not None and len(rel_name) > allowed_len:
|
|
1145
1121
|
errors.append(
|
|
@@ -1233,7 +1209,7 @@ class Model(metaclass=ModelBase):
|
|
|
1233
1209
|
id="postgres.constraint_refers_to_joined_field",
|
|
1234
1210
|
)
|
|
1235
1211
|
)
|
|
1236
|
-
errors.extend(cls.
|
|
1212
|
+
errors.extend(cls._check_referenced_fields(fields, "constraints"))
|
|
1237
1213
|
return errors
|
|
1238
1214
|
|
|
1239
1215
|
|
|
@@ -148,15 +148,6 @@ class DatabaseConnection:
|
|
|
148
148
|
assert self.connection is not None
|
|
149
149
|
self.connection.autocommit = autocommit
|
|
150
150
|
|
|
151
|
-
def check_constraints(self, table_names: list[str] | None = None) -> None:
|
|
152
|
-
"""
|
|
153
|
-
Check constraints by setting them to immediate. Return them to deferred
|
|
154
|
-
afterward.
|
|
155
|
-
"""
|
|
156
|
-
with self.cursor() as cursor:
|
|
157
|
-
cursor.execute("SET CONSTRAINTS ALL IMMEDIATE")
|
|
158
|
-
cursor.execute("SET CONSTRAINTS ALL DEFERRED")
|
|
159
|
-
|
|
160
151
|
def make_debug_cursor(self, cursor: psycopg.Cursor[Any]) -> CursorDebugWrapper:
|
|
161
152
|
return CursorDebugWrapper(cursor, self)
|
|
162
153
|
|
|
@@ -544,9 +535,7 @@ class DatabaseConnection:
|
|
|
544
535
|
tables = set()
|
|
545
536
|
for model in get_migratable_models():
|
|
546
537
|
tables.add(model.model_options.db_table)
|
|
547
|
-
tables.update(
|
|
548
|
-
f.m2m_db_table() for f in model._model_meta.local_many_to_many
|
|
549
|
-
)
|
|
538
|
+
tables.update(f.m2m_db_table() for f in model._model_meta.many_to_many)
|
|
550
539
|
tables = list(tables)
|
|
551
540
|
if only_existing:
|
|
552
541
|
existing_tables = set(self.table_names(include_views=include_views))
|
|
@@ -615,7 +604,8 @@ class DatabaseConnection:
|
|
|
615
604
|
WHERE fka.attrelid = c.confrelid AND fka.attnum = c.confkey[1]),
|
|
616
605
|
c.convalidated,
|
|
617
606
|
pg_get_constraintdef(c.oid),
|
|
618
|
-
c.confdeltype
|
|
607
|
+
c.confdeltype,
|
|
608
|
+
c.condeferrable
|
|
619
609
|
FROM pg_constraint AS c
|
|
620
610
|
JOIN pg_class AS cl ON c.conrelid = cl.oid
|
|
621
611
|
WHERE cl.relname = %s AND pg_catalog.pg_table_is_visible(cl.oid)
|
|
@@ -630,6 +620,7 @@ class DatabaseConnection:
|
|
|
630
620
|
validated,
|
|
631
621
|
constraintdef,
|
|
632
622
|
confdeltype,
|
|
623
|
+
condeferrable,
|
|
633
624
|
) in cursor.fetchall():
|
|
634
625
|
constraints[constraint] = {
|
|
635
626
|
"columns": columns,
|
|
@@ -639,6 +630,7 @@ class DatabaseConnection:
|
|
|
639
630
|
"definition": constraintdef,
|
|
640
631
|
"validated": validated,
|
|
641
632
|
"on_delete_action": confdeltype if kind == "f" else None,
|
|
633
|
+
"deferrable": condeferrable if kind == "f" else None,
|
|
642
634
|
}
|
|
643
635
|
# Now get indexes. Sort order, opclasses, INCLUDE, and predicates all
|
|
644
636
|
# ride along inside `pg_get_indexdef` and are compared via the
|