plain.postgres 0.115.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.115.0 → plain_postgres-0.116.0}/PKG-INFO +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/CHANGELOG.md +20 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/base.py +28 -54
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/connection.py +1 -3
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/constraints.py +2 -2
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/convergence/analysis.py +3 -8
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/base.py +11 -22
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/binary.py +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/encrypted.py +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/json.py +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/network.py +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/numeric.py +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/primary_key.py +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/related.py +9 -11
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/related_managers.py +4 -9
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/reverse_related.py +16 -7
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/temporal.py +1 -2
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/text.py +2 -2
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/timezones.py +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/uuid.py +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/forms.py +2 -4
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/ownership.py +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/meta.py +34 -89
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/state.py +2 -2
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/preflight/indexes.py +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/query.py +8 -11
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/query_utils.py +5 -5
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/schema.py +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sql/compiler.py +2 -2
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sql/datastructures.py +3 -3
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sql/query.py +8 -12
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/pyproject.toml +1 -1
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/.gitignore +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/CLAUDE.md +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/LICENSE +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/README.md +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/README.md +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/adapters.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/decorators.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/sync.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/convergence/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/convergence/corrections.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/convergence/planning.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/databases.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/dialect.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/boolean.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/duration.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/indexes.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/context.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/helpers.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/runner.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/types.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/schema.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/middleware.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/autodetector.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/preflight/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/preflight/database.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/preflight/models.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/schema_lock.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sources.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/test/database.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/types.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {plain_postgres-0.115.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.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/__init__.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/constraints.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/defaults.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/delete.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/encrypted.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/forms.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/indexes.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/iteration.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/mixins.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/nullability.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/querysets.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/relationships.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/storage_parameters.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/trees.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/urls.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/conftest.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_apply_replan.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_autodetector_not_null_errors.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_autodetector_type_change.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_connection_isolation.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_connection_pool.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_connection_self_heal.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_constraint_violation_error.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_constraints.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_defaults.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_fk.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_indexes.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_nullability.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_timeouts.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_databases_not_on_runtime_path.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_db_expression_defaults.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_diagnose.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_executor_connection_hook.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_fk_characterization.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_health.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_introspection.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_literal_default_persistence.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_management_connection.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_migration_executor.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_otel_metrics.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_rollback_exc_attribution.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_schema_lock.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_schema_timeouts.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_create_update.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_database_url.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_databases.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_deferred_loading.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_delete_behaviors.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_encrypted_fields.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_exceptions.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_field_defaults.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_functions_uuid.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_integrity_error_mapping.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_iterator.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_m2m.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_manager_assignment.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_manual_pk.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_mixins.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_modelform_roundtrip.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_only_empty_defaults.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_order_by_expressions.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_queryset_repr.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_queryset_slicing.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_random_string_field.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_raw_query.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_read_only_transactions.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_related.py +0 -0
- {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_related_instance_filter.py +0 -0
|
@@ -1,5 +1,25 @@
|
|
|
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
|
+
|
|
3
23
|
## [0.115.0](https://github.com/dropseed/plain/releases/plain-postgres@0.115.0) (2026-09-02)
|
|
4
24
|
|
|
5
25
|
### What's changed
|
|
@@ -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(
|
|
@@ -484,7 +471,7 @@ class Model(metaclass=ModelBase):
|
|
|
484
471
|
fields from RETURNING. Omits id from the INSERT when unset so Postgres
|
|
485
472
|
generates the identity value."""
|
|
486
473
|
meta = self._model_meta
|
|
487
|
-
fields = list(meta.
|
|
474
|
+
fields = list(meta.fields)
|
|
488
475
|
if self.id is None:
|
|
489
476
|
id_field = meta.get_forward_field("id")
|
|
490
477
|
fields = [f for f in fields if f is not id_field]
|
|
@@ -494,7 +481,6 @@ class Model(metaclass=ModelBase):
|
|
|
494
481
|
)
|
|
495
482
|
if results:
|
|
496
483
|
for value, field in zip(results[0], returning_fields):
|
|
497
|
-
assert field.name is not None
|
|
498
484
|
setattr(self, field.name, value)
|
|
499
485
|
|
|
500
486
|
def _update_row(self, fields: Iterable[str] | None) -> None:
|
|
@@ -502,7 +488,7 @@ class Model(metaclass=ModelBase):
|
|
|
502
488
|
row matched -- update() targets an existing row and has no INSERT
|
|
503
489
|
fallback (that's create())."""
|
|
504
490
|
meta = self._model_meta
|
|
505
|
-
non_pks = [f for f in meta.
|
|
491
|
+
non_pks = [f for f in meta.fields if not f.primary_key]
|
|
506
492
|
if fields:
|
|
507
493
|
non_pks = [f for f in non_pks if f.name in fields]
|
|
508
494
|
values = [(f, f.pre_save(self, False)) for f in non_pks]
|
|
@@ -525,7 +511,7 @@ class Model(metaclass=ModelBase):
|
|
|
525
511
|
) -> None:
|
|
526
512
|
# Ensure that a model instance without a PK hasn't been assigned to
|
|
527
513
|
# a ForeignKeyField on this model. If the field is nullable, allowing the save would result in silent data loss.
|
|
528
|
-
for field in self._model_meta.
|
|
514
|
+
for field in self._model_meta.fields:
|
|
529
515
|
if field_names and field.name not in field_names:
|
|
530
516
|
continue
|
|
531
517
|
# If the related field isn't cached, then an instance hasn't been
|
|
@@ -578,7 +564,6 @@ class Model(metaclass=ModelBase):
|
|
|
578
564
|
with transaction.mark_for_rollback_on_error():
|
|
579
565
|
count = self._model_meta.base_queryset.filter(id=self.id)._raw_delete()
|
|
580
566
|
id_field = self._model_meta.get_forward_field("id")
|
|
581
|
-
assert id_field.name is not None
|
|
582
567
|
setattr(self, id_field.name, None)
|
|
583
568
|
# Only the id is cleared -- every other field value survives so callers
|
|
584
569
|
# can still reference a deleted row (correlate it, log it, check it's
|
|
@@ -611,7 +596,7 @@ class Model(metaclass=ModelBase):
|
|
|
611
596
|
meta = meta or self._model_meta
|
|
612
597
|
return {
|
|
613
598
|
field.name: Value(field.value_from_object(self), field)
|
|
614
|
-
for field in meta.
|
|
599
|
+
for field in meta.fields
|
|
615
600
|
if field.name not in exclude
|
|
616
601
|
}
|
|
617
602
|
|
|
@@ -755,9 +740,9 @@ class Model(metaclass=ModelBase):
|
|
|
755
740
|
def _check_fields(cls) -> list[PreflightResult]:
|
|
756
741
|
"""Perform all field checks."""
|
|
757
742
|
errors: list[PreflightResult] = []
|
|
758
|
-
for field in cls._model_meta.
|
|
743
|
+
for field in cls._model_meta.fields:
|
|
759
744
|
errors.extend(field.preflight(from_model=cls))
|
|
760
|
-
for field in cls._model_meta.
|
|
745
|
+
for field in cls._model_meta.many_to_many:
|
|
761
746
|
errors.extend(field.preflight(from_model=cls))
|
|
762
747
|
return errors
|
|
763
748
|
|
|
@@ -768,7 +753,7 @@ class Model(metaclass=ModelBase):
|
|
|
768
753
|
errors: list[PreflightResult] = []
|
|
769
754
|
seen_intermediary_signatures = []
|
|
770
755
|
|
|
771
|
-
fields = cls._model_meta.
|
|
756
|
+
fields = cls._model_meta.many_to_many
|
|
772
757
|
|
|
773
758
|
# Skip when the target model wasn't found.
|
|
774
759
|
fields = (f for f in fields if isinstance(f.remote_field.model, ModelBase))
|
|
@@ -800,9 +785,7 @@ class Model(metaclass=ModelBase):
|
|
|
800
785
|
def _check_id_field(cls) -> list[PreflightResult]:
|
|
801
786
|
"""Disallow user-defined fields named ``id``."""
|
|
802
787
|
if any(
|
|
803
|
-
f
|
|
804
|
-
for f in cls._model_meta.local_fields
|
|
805
|
-
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
|
|
806
789
|
):
|
|
807
790
|
return [
|
|
808
791
|
PreflightResult(
|
|
@@ -819,7 +802,7 @@ class Model(metaclass=ModelBase):
|
|
|
819
802
|
errors: list[PreflightResult] = []
|
|
820
803
|
used_fields = {} # name -> field
|
|
821
804
|
|
|
822
|
-
for f in cls._model_meta.
|
|
805
|
+
for f in cls._model_meta.fields:
|
|
823
806
|
clash = used_fields.get(f.name)
|
|
824
807
|
# Note that we may detect clash between user-defined non-unique
|
|
825
808
|
# field "id" and automatically added unique field "id", both
|
|
@@ -847,7 +830,7 @@ class Model(metaclass=ModelBase):
|
|
|
847
830
|
used_column_names: list[str] = []
|
|
848
831
|
errors: list[PreflightResult] = []
|
|
849
832
|
|
|
850
|
-
for f in cls._model_meta.
|
|
833
|
+
for f in cls._model_meta.fields:
|
|
851
834
|
column_name = f.column
|
|
852
835
|
|
|
853
836
|
# Ensure the column name is not already in use.
|
|
@@ -915,7 +898,7 @@ class Model(metaclass=ModelBase):
|
|
|
915
898
|
@classmethod
|
|
916
899
|
def _check_single_primary_key(cls) -> list[PreflightResult]:
|
|
917
900
|
errors: list[PreflightResult] = []
|
|
918
|
-
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:
|
|
919
902
|
errors.append(
|
|
920
903
|
PreflightResult(
|
|
921
904
|
fix="The model cannot have more than one field with "
|
|
@@ -966,11 +949,11 @@ class Model(metaclass=ModelBase):
|
|
|
966
949
|
include for index in cls.model_options.indexes for include in index.include
|
|
967
950
|
]
|
|
968
951
|
fields += references
|
|
969
|
-
errors.extend(cls.
|
|
952
|
+
errors.extend(cls._check_referenced_fields(fields, "indexes"))
|
|
970
953
|
return errors
|
|
971
954
|
|
|
972
955
|
@classmethod
|
|
973
|
-
def
|
|
956
|
+
def _check_referenced_fields(
|
|
974
957
|
cls, fields: Iterable[str], option: str
|
|
975
958
|
) -> list[PreflightResult]:
|
|
976
959
|
# In order to avoid hitting the relation tree prematurely, we use our
|
|
@@ -1003,15 +986,6 @@ class Model(metaclass=ModelBase):
|
|
|
1003
986
|
id="postgres.m2m_field_in_meta_option",
|
|
1004
987
|
)
|
|
1005
988
|
)
|
|
1006
|
-
elif field not in cls._model_meta.local_fields:
|
|
1007
|
-
errors.append(
|
|
1008
|
-
PreflightResult(
|
|
1009
|
-
fix=f"'{option}' refers to field '{field_name}' which is not local to model "
|
|
1010
|
-
f"'{cls.model_options.object_name}'.",
|
|
1011
|
-
obj=cls,
|
|
1012
|
-
id="postgres.non_local_field_reference",
|
|
1013
|
-
)
|
|
1014
|
-
)
|
|
1015
989
|
return errors
|
|
1016
990
|
|
|
1017
991
|
@classmethod
|
|
@@ -1089,9 +1063,9 @@ class Model(metaclass=ModelBase):
|
|
|
1089
1063
|
meta = cls._model_meta
|
|
1090
1064
|
valid_fields = set(
|
|
1091
1065
|
chain.from_iterable(
|
|
1092
|
-
(f.
|
|
1093
|
-
if
|
|
1094
|
-
else (f.
|
|
1066
|
+
(f.field.related_query_name(),)
|
|
1067
|
+
if isinstance(f, ForeignObjectRel)
|
|
1068
|
+
else (f.name,)
|
|
1095
1069
|
for f in chain(meta.fields, meta.related_objects)
|
|
1096
1070
|
)
|
|
1097
1071
|
)
|
|
@@ -1121,7 +1095,7 @@ class Model(metaclass=ModelBase):
|
|
|
1121
1095
|
# silently truncate, so we check for names that are too long
|
|
1122
1096
|
allowed_len = MAX_NAME_LENGTH
|
|
1123
1097
|
|
|
1124
|
-
for f in cls._model_meta.
|
|
1098
|
+
for f in cls._model_meta.fields:
|
|
1125
1099
|
column_name = f.column
|
|
1126
1100
|
|
|
1127
1101
|
# Check if column name is too long for the database.
|
|
@@ -1135,13 +1109,13 @@ class Model(metaclass=ModelBase):
|
|
|
1135
1109
|
)
|
|
1136
1110
|
)
|
|
1137
1111
|
|
|
1138
|
-
for f in cls._model_meta.
|
|
1112
|
+
for f in cls._model_meta.many_to_many:
|
|
1139
1113
|
# Skip nonexistent models.
|
|
1140
1114
|
if isinstance(f.remote_field.through, str):
|
|
1141
1115
|
continue
|
|
1142
1116
|
|
|
1143
1117
|
# Check if column name for the M2M field is too long for the database.
|
|
1144
|
-
for m2m in f.remote_field.through._model_meta.
|
|
1118
|
+
for m2m in f.remote_field.through._model_meta.fields:
|
|
1145
1119
|
rel_name = m2m.column
|
|
1146
1120
|
if rel_name is not None and len(rel_name) > allowed_len:
|
|
1147
1121
|
errors.append(
|
|
@@ -1235,7 +1209,7 @@ class Model(metaclass=ModelBase):
|
|
|
1235
1209
|
id="postgres.constraint_refers_to_joined_field",
|
|
1236
1210
|
)
|
|
1237
1211
|
)
|
|
1238
|
-
errors.extend(cls.
|
|
1212
|
+
errors.extend(cls._check_referenced_fields(fields, "constraints"))
|
|
1239
1213
|
return errors
|
|
1240
1214
|
|
|
1241
1215
|
|
|
@@ -535,9 +535,7 @@ class DatabaseConnection:
|
|
|
535
535
|
tables = set()
|
|
536
536
|
for model in get_migratable_models():
|
|
537
537
|
tables.add(model.model_options.db_table)
|
|
538
|
-
tables.update(
|
|
539
|
-
f.m2m_db_table() for f in model._model_meta.local_many_to_many
|
|
540
|
-
)
|
|
538
|
+
tables.update(f.m2m_db_table() for f in model._model_meta.many_to_many)
|
|
541
539
|
tables = list(tables)
|
|
542
540
|
if only_existing:
|
|
543
541
|
existing_tables = set(self.table_names(include_views=include_views))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from types import NoneType
|
|
4
|
-
from typing import TYPE_CHECKING, Any
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from plain.exceptions import ValidationError
|
|
7
7
|
from plain.postgres.constants import LOOKUP_SEP
|
|
@@ -454,7 +454,7 @@ class UniqueConstraint(BaseConstraint):
|
|
|
454
454
|
field_names[-1] = f"and {field_names[-1]}"
|
|
455
455
|
# Comma-join when more than two, otherwise just space-join.
|
|
456
456
|
sep = ", " if len(field_names) > 2 else " "
|
|
457
|
-
params["field_label"] = sep.join(
|
|
457
|
+
params["field_label"] = sep.join(field_names)
|
|
458
458
|
|
|
459
459
|
# Use the first field's message format.
|
|
460
460
|
message = meta.get_forward_field(unique_check[0]).unique_error_message
|
|
@@ -21,7 +21,6 @@ from ..ddl import (
|
|
|
21
21
|
compile_literal_default_sql,
|
|
22
22
|
)
|
|
23
23
|
from ..dialect import quote_name
|
|
24
|
-
from ..fields.base import ColumnField
|
|
25
24
|
from ..fields.related import ForeignKeyField
|
|
26
25
|
from ..indexes import Index
|
|
27
26
|
from ..introspection import (
|
|
@@ -586,9 +585,7 @@ def _compare_columns(
|
|
|
586
585
|
statuses: list[ColumnStatus] = []
|
|
587
586
|
expected_col_names: set[str] = set()
|
|
588
587
|
|
|
589
|
-
for f in model._model_meta.
|
|
590
|
-
if not isinstance(f, ColumnField):
|
|
591
|
-
continue
|
|
588
|
+
for f in model._model_meta.fields:
|
|
592
589
|
db_type = f.db_type()
|
|
593
590
|
if db_type is None:
|
|
594
591
|
continue
|
|
@@ -635,8 +632,6 @@ def _compare_columns(
|
|
|
635
632
|
if f.primary_key:
|
|
636
633
|
pk_suffix = f.db_type_suffix() or ""
|
|
637
634
|
|
|
638
|
-
# Fields reached via local_fields are always contributed (name set).
|
|
639
|
-
assert f.name is not None
|
|
640
635
|
statuses.append(
|
|
641
636
|
ColumnStatus(
|
|
642
637
|
name=f.column,
|
|
@@ -1193,7 +1188,7 @@ def _compare_check_constraints(
|
|
|
1193
1188
|
# ignored rather than surfaced as undeclared user constraints.
|
|
1194
1189
|
internal_checks = {
|
|
1195
1190
|
generate_notnull_check_name(table, f.column)
|
|
1196
|
-
for f in model._model_meta.
|
|
1191
|
+
for f in model._model_meta.fields
|
|
1197
1192
|
if f.db_type() is not None
|
|
1198
1193
|
}
|
|
1199
1194
|
|
|
@@ -1245,7 +1240,7 @@ def _compare_foreign_keys(
|
|
|
1245
1240
|
# Expected FKs from model fields, keyed by shape (column, target_table,
|
|
1246
1241
|
# target_column) — the DB-side identity, since names can lag a rename.
|
|
1247
1242
|
expected_fks: dict[tuple[str, str, str], ForeignKeyField] = {}
|
|
1248
|
-
for f in model._model_meta.
|
|
1243
|
+
for f in model._model_meta.fields:
|
|
1249
1244
|
if isinstance(f, ForeignKeyField):
|
|
1250
1245
|
to_table = f.target_field.model.model_options.db_table
|
|
1251
1246
|
expected_fks[(f.column, to_table, f.target_field.column)] = f
|
|
@@ -110,8 +110,9 @@ class Field[T](RegisterLookupMixin):
|
|
|
110
110
|
cast_db_type_sql: str | None = None
|
|
111
111
|
|
|
112
112
|
# Instance attributes set during field lifecycle
|
|
113
|
-
# Set by __init__
|
|
114
|
-
|
|
113
|
+
# Set by __init__; becomes the real field name once contributed to a
|
|
114
|
+
# model class (set_attributes_from_name, called by contribute_to_class)
|
|
115
|
+
name: str
|
|
115
116
|
# Set by set_attributes_from_name (called by contribute_to_class)
|
|
116
117
|
column: str
|
|
117
118
|
concrete: bool
|
|
@@ -134,7 +135,7 @@ class Field[T](RegisterLookupMixin):
|
|
|
134
135
|
non_migration_attrs: tuple[str, ...] = ()
|
|
135
136
|
|
|
136
137
|
def __init__(self) -> None:
|
|
137
|
-
self.name =
|
|
138
|
+
self.name = "" # Set by set_attributes_from_name
|
|
138
139
|
self.primary_key = False
|
|
139
140
|
self.auto_created = False
|
|
140
141
|
|
|
@@ -151,8 +152,8 @@ class Field[T](RegisterLookupMixin):
|
|
|
151
152
|
def __repr__(self) -> str:
|
|
152
153
|
"""Display the module, class, and name of the field."""
|
|
153
154
|
path = f"{self.__class__.__module__}.{self.__class__.__qualname__}"
|
|
154
|
-
name = getattr(self, "name",
|
|
155
|
-
if name
|
|
155
|
+
name = getattr(self, "name", "")
|
|
156
|
+
if name:
|
|
156
157
|
return f"<{path}: {name}>"
|
|
157
158
|
return f"<{path}>"
|
|
158
159
|
|
|
@@ -164,7 +165,6 @@ class Field[T](RegisterLookupMixin):
|
|
|
164
165
|
Check if field name is valid, i.e. 1) does not end with an
|
|
165
166
|
underscore, 2) does not contain "__" and 3) is not "id".
|
|
166
167
|
"""
|
|
167
|
-
assert self.name is not None, "Field name must be set before checking"
|
|
168
168
|
if self.name.endswith("_"):
|
|
169
169
|
return [
|
|
170
170
|
PreflightResult(
|
|
@@ -215,7 +215,7 @@ class Field[T](RegisterLookupMixin):
|
|
|
215
215
|
"""
|
|
216
216
|
return sql, params
|
|
217
217
|
|
|
218
|
-
def deconstruct(self) -> tuple[str
|
|
218
|
+
def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
|
|
219
219
|
"""
|
|
220
220
|
Return enough information to recreate the field as a 4-tuple:
|
|
221
221
|
|
|
@@ -259,7 +259,6 @@ class Field[T](RegisterLookupMixin):
|
|
|
259
259
|
cls_name = self.__class__.__qualname__
|
|
260
260
|
if getattr(_postgres_root, cls_name, None) is self.__class__:
|
|
261
261
|
path = f"plain.postgres.{cls_name}"
|
|
262
|
-
# Note: self.name can be None during migration state rendering when fields are cloned
|
|
263
262
|
return (self.name, path, [], keywords)
|
|
264
263
|
|
|
265
264
|
def clone(self) -> Self:
|
|
@@ -305,7 +304,6 @@ class Field[T](RegisterLookupMixin):
|
|
|
305
304
|
# values - so, this is very close to normal pickle.
|
|
306
305
|
state = self.__dict__.copy()
|
|
307
306
|
return _empty, (self.__class__,), state
|
|
308
|
-
assert self.name is not None
|
|
309
307
|
options = model.model_options
|
|
310
308
|
return _load_field, (
|
|
311
309
|
options.package_label,
|
|
@@ -394,7 +392,6 @@ class Field[T](RegisterLookupMixin):
|
|
|
394
392
|
# Field is its own descriptor; make sure it is set on the class so
|
|
395
393
|
# attribute access hits __get__/__set__.
|
|
396
394
|
if self.column:
|
|
397
|
-
assert self.name is not None
|
|
398
395
|
setattr(cls, self.name, self)
|
|
399
396
|
|
|
400
397
|
# Descriptor protocol implementation
|
|
@@ -422,7 +419,6 @@ class Field[T](RegisterLookupMixin):
|
|
|
422
419
|
return self
|
|
423
420
|
|
|
424
421
|
# Instance access - get value from instance dict
|
|
425
|
-
assert self.name is not None
|
|
426
422
|
data = instance.__dict__
|
|
427
423
|
field_name = self.name
|
|
428
424
|
|
|
@@ -432,9 +428,7 @@ class Field[T](RegisterLookupMixin):
|
|
|
432
428
|
# instance cheap to use beyond its primary key.
|
|
433
429
|
if field_name not in data:
|
|
434
430
|
missing = [
|
|
435
|
-
f.name
|
|
436
|
-
for f in instance._model_meta.concrete_fields
|
|
437
|
-
if f.name not in data
|
|
431
|
+
f.name for f in instance._model_meta.fields if f.name not in data
|
|
438
432
|
]
|
|
439
433
|
instance.refresh_from_db(fields=missing)
|
|
440
434
|
|
|
@@ -472,7 +466,6 @@ class Field[T](RegisterLookupMixin):
|
|
|
472
466
|
stored = self.to_python(value)
|
|
473
467
|
|
|
474
468
|
# Store in instance dict
|
|
475
|
-
assert self.name is not None
|
|
476
469
|
instance.__dict__[self.name] = stored
|
|
477
470
|
|
|
478
471
|
def __delete__(self, instance: Model) -> None:
|
|
@@ -481,7 +474,6 @@ class Field[T](RegisterLookupMixin):
|
|
|
481
474
|
|
|
482
475
|
Removes the value from instance.__dict__.
|
|
483
476
|
"""
|
|
484
|
-
assert self.name is not None
|
|
485
477
|
try:
|
|
486
478
|
del instance.__dict__[self.name]
|
|
487
479
|
except KeyError:
|
|
@@ -491,7 +483,6 @@ class Field[T](RegisterLookupMixin):
|
|
|
491
483
|
|
|
492
484
|
def pre_save(self, model_instance: Model, add: bool) -> T | None:
|
|
493
485
|
"""Return field's value just before saving."""
|
|
494
|
-
assert self.name is not None
|
|
495
486
|
return getattr(model_instance, self.name)
|
|
496
487
|
|
|
497
488
|
def get_prep_value(self, value: Any) -> Any:
|
|
@@ -544,12 +535,10 @@ class Field[T](RegisterLookupMixin):
|
|
|
544
535
|
return self.get_db_default_expression() is not None
|
|
545
536
|
|
|
546
537
|
def save_form_data(self, instance: Model, data: Any) -> None:
|
|
547
|
-
assert self.name is not None
|
|
548
538
|
setattr(instance, self.name, data)
|
|
549
539
|
|
|
550
540
|
def value_from_object(self, obj: Model) -> T | None:
|
|
551
541
|
"""Return the value of this field in the given model instance."""
|
|
552
|
-
assert self.name is not None
|
|
553
542
|
return getattr(obj, self.name)
|
|
554
543
|
|
|
555
544
|
|
|
@@ -653,7 +642,7 @@ class ColumnField[T](Field[T]):
|
|
|
653
642
|
return None
|
|
654
643
|
return self._default_empty_value
|
|
655
644
|
|
|
656
|
-
def deconstruct(self) -> tuple[str
|
|
645
|
+
def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
|
|
657
646
|
name, path, args, kwargs = super().deconstruct()
|
|
658
647
|
if self.required is not True:
|
|
659
648
|
kwargs["required"] = self.required
|
|
@@ -750,7 +739,7 @@ class DefaultableField[T](ColumnField[T]):
|
|
|
750
739
|
# shared state across instances.
|
|
751
740
|
return copy.deepcopy(self.default)
|
|
752
741
|
|
|
753
|
-
def deconstruct(self) -> tuple[str
|
|
742
|
+
def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
|
|
754
743
|
name, path, args, kwargs = super().deconstruct()
|
|
755
744
|
if self.default is not NOT_PROVIDED:
|
|
756
745
|
kwargs["default"] = self.default
|
|
@@ -891,7 +880,7 @@ class ChoicesField[T](DefaultableField[T]):
|
|
|
891
880
|
)
|
|
892
881
|
super().validate(value, model_instance)
|
|
893
882
|
|
|
894
|
-
def deconstruct(self) -> tuple[str
|
|
883
|
+
def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
|
|
895
884
|
name, path, args, kwargs = super().deconstruct()
|
|
896
885
|
if self.choices is not None:
|
|
897
886
|
choices = self.choices
|
|
@@ -41,7 +41,7 @@ class BinaryField[
|
|
|
41
41
|
if self.max_length is not None:
|
|
42
42
|
self.validators.append(MaxLengthValidator(self.max_length))
|
|
43
43
|
|
|
44
|
-
def deconstruct(self) -> tuple[str
|
|
44
|
+
def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
|
|
45
45
|
name, path, args, kwargs = super().deconstruct()
|
|
46
46
|
if self.max_length is not None:
|
|
47
47
|
kwargs["max_length"] = self.max_length
|
|
@@ -14,7 +14,7 @@ except ImportError:
|
|
|
14
14
|
InvalidToken = None # ty: ignore[invalid-assignment]
|
|
15
15
|
MultiFernet = None # ty: ignore[invalid-assignment]
|
|
16
16
|
hashes = None # ty: ignore[invalid-assignment]
|
|
17
|
-
PBKDF2HMAC = None
|
|
17
|
+
PBKDF2HMAC = None
|
|
18
18
|
|
|
19
19
|
from plain.postgres.lookups import Exact, IsNull
|
|
20
20
|
from plain.runtime import settings
|
|
@@ -54,7 +54,7 @@ class JSONField(DefaultableField):
|
|
|
54
54
|
validators=validators,
|
|
55
55
|
)
|
|
56
56
|
|
|
57
|
-
def deconstruct(self) -> tuple[str
|
|
57
|
+
def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
|
|
58
58
|
name, path, args, kwargs = super().deconstruct()
|
|
59
59
|
if self.encoder is not None:
|
|
60
60
|
kwargs["encoder"] = self.encoder
|
|
@@ -63,7 +63,7 @@ class GenericIPAddressField[T: (str, str | None) = str](DefaultableField[T]):
|
|
|
63
63
|
]
|
|
64
64
|
return []
|
|
65
65
|
|
|
66
|
-
def deconstruct(self) -> tuple[str
|
|
66
|
+
def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
|
|
67
67
|
name, path, args, kwargs = super().deconstruct()
|
|
68
68
|
if self.unpack_ipv4 is not False:
|
|
69
69
|
kwargs["unpack_ipv4"] = self.unpack_ipv4
|
|
@@ -238,7 +238,7 @@ class DecimalField[T: (decimal.Decimal, decimal.Decimal | None) = decimal.Decima
|
|
|
238
238
|
def context(self) -> decimal.Context:
|
|
239
239
|
return decimal.Context(prec=self.max_digits)
|
|
240
240
|
|
|
241
|
-
def deconstruct(self) -> tuple[str
|
|
241
|
+
def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
|
|
242
242
|
name, path, args, kwargs = super().deconstruct()
|
|
243
243
|
if self.max_digits is not None:
|
|
244
244
|
kwargs["max_digits"] = self.max_digits
|
|
@@ -41,7 +41,7 @@ class PrimaryKeyField(ColumnField[int]):
|
|
|
41
41
|
# for every other field type.
|
|
42
42
|
return [e for e in errors if e.id != "fields.reserved_field_name_id"]
|
|
43
43
|
|
|
44
|
-
def deconstruct(self) -> tuple[str
|
|
44
|
+
def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
|
|
45
45
|
return (
|
|
46
46
|
self.name,
|
|
47
47
|
"plain.postgres.PrimaryKeyField",
|