plain.postgres 0.101.0__tar.gz → 0.103.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.101.0 → plain_postgres-0.103.0}/.gitignore +2 -0
- plain_postgres-0.101.0/plain/postgres/README.md → plain_postgres-0.103.0/PKG-INFO +45 -11
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/CHANGELOG.md +26 -0
- plain_postgres-0.101.0/PKG-INFO → plain_postgres-0.103.0/plain/postgres/README.md +32 -25
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/__init__.py +4 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +1 -1
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/base.py +5 -13
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/connection.py +12 -37
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/constraints.py +32 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/convergence/__init__.py +8 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/convergence/analysis.py +498 -238
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/convergence/fixes.py +42 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/convergence/planning.py +13 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/base.py +5 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/related_managers.py +6 -6
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/indexes.py +1 -1
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/__init__.py +0 -10
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/checks_cumulative.py +205 -9
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/runner.py +19 -0
- plain_postgres-0.103.0/plain/postgres/introspection/schema.py +256 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/base.py +3 -2
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/options.py +11 -4
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/query.py +2 -2
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sql/compiler.py +5 -5
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sql/query.py +15 -8
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/pyproject.toml +2 -2
- plain_postgres-0.103.0/tests/app/examples/migrations/0018_storageparametersexample.py +18 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/__init__.py +1 -0
- plain_postgres-0.103.0/tests/app/examples/models/storage_parameters.py +14 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_constraint_violation_error.py +43 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_constraints.py +274 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_indexes.py +453 -41
- plain_postgres-0.103.0/tests/internal/test_convergence_storage_parameters.py +222 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_diagnose.py +1 -0
- plain_postgres-0.103.0/tests/internal/test_introspection.py +226 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_literal_default_persistence.py +122 -29
- plain_postgres-0.101.0/plain/postgres/introspection/schema.py +0 -584
- plain_postgres-0.101.0/tests/test_introspection.py +0 -456
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/CLAUDE.md +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/LICENSE +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/README.md +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/adapters.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/decorators.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/sync.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/dialect.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/binary.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/boolean.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/duration.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/encrypted.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/network.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/numeric.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/primary_key.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/related.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/temporal.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/text.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/timezones.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/uuid.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/forms.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/__init__.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/context.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/helpers.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/ownership.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/types.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/meta.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/middleware.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/autodetector.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/state.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/preflight.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/schema.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sources.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.101.0/tests/app/examples/migrations → plain_postgres-0.103.0/plain/postgres/test}/__init__.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/test/database.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/types.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.101.0/plain/postgres/test → plain_postgres-0.103.0/tests/app/examples/migrations}/__init__.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/constraints.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/defaults.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/delete.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/encrypted.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/forms.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/indexes.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/iteration.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/mixins.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/nullability.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/querysets.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/relationships.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/trees.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/urls.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/conftest.py +0 -0
- {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_autodetector_not_null_errors.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_autodetector_type_change.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_connection_isolation.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_connection_pool.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_defaults.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_fk.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_nullability.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_timeouts.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_db_expression_defaults.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_executor_connection_hook.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_health.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_management_connection.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_migration_executor.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_otel_metrics.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_schema_timeouts.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_database_url.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_delete_behaviors.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_encrypted_fields.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_exceptions.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_field_defaults.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_functions_uuid.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_iterator.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_m2m.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_manager_assignment.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_mixins.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_modelform_roundtrip.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_random_string_field.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_raw_query.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_read_only_transactions.py +0 -0
- {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_related.py +0 -0
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plain.postgres
|
|
3
|
+
Version: 0.103.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.134.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.**
|
|
@@ -687,7 +700,7 @@ Or use `RunSQL` with explicit cascade if the relationship is large.
|
|
|
687
700
|
|
|
688
701
|
### Convergence
|
|
689
702
|
|
|
690
|
-
Convergence compares the indexes, constraints, foreign keys, and
|
|
703
|
+
Convergence compares the indexes, constraints, foreign keys, nullability, and [storage parameters](#storage-parameters) declared on your models against what actually exists in the database, then applies fixes to make them match. You don't need to create migrations for these — just declare them on your model and run `postgres sync`.
|
|
691
704
|
|
|
692
705
|
```python
|
|
693
706
|
@postgres.register_model
|
|
@@ -1078,6 +1091,26 @@ A `code` becomes `ValidationError.code` — useful for test assertions, error tr
|
|
|
1078
1091
|
|
|
1079
1092
|
`UniqueConstraint` accepts the same `violation_error`. With a single-field unique constraint, a string `violation_error="That email is taken."` auto-routes to that field; otherwise (multi-field, expressions, or a CheckConstraint) errors land on `NON_FIELD_ERRORS` unless you pass the dict form. See [BaseConstraint](./constraints.py#BaseConstraint) for the full signature.
|
|
1080
1093
|
|
|
1094
|
+
### Storage parameters
|
|
1095
|
+
|
|
1096
|
+
Per-table Postgres [storage parameters](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS) (`pg_class.reloptions`) are declared on `model_options` and managed by [convergence](#convergence) — `postgres sync` issues the `ALTER TABLE … SET/RESET (...)` to make the live table match. They're not serialized into migrations.
|
|
1097
|
+
|
|
1098
|
+
```python
|
|
1099
|
+
class CachedItem(postgres.Model):
|
|
1100
|
+
...
|
|
1101
|
+
|
|
1102
|
+
model_options = postgres.Options(
|
|
1103
|
+
storage_parameters={
|
|
1104
|
+
# Tighter autovacuum on a churn-heavy table
|
|
1105
|
+
"autovacuum_vacuum_scale_factor": 0.1,
|
|
1106
|
+
# TOAST has its own autovacuum schedule — prefix with `toast.`
|
|
1107
|
+
"toast.autovacuum_vacuum_scale_factor": 0.05,
|
|
1108
|
+
},
|
|
1109
|
+
)
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
Models are the source of truth: undeclared parameters set on the live table are reset on the next sync. Use this for autovacuum tuning, `fillfactor`, TOAST options, etc. — anything you'd otherwise apply by hand with `ALTER TABLE … SET (...)`.
|
|
1113
|
+
|
|
1081
1114
|
### Schema design
|
|
1082
1115
|
|
|
1083
1116
|
#### Index fields used in filters and ordering
|
|
@@ -1277,16 +1310,17 @@ fix in their model code. (In JSON output each finding still carries
|
|
|
1277
1310
|
it.) Each finding still carries the exact SQL in its suggestion for anyone
|
|
1278
1311
|
who wants to act.
|
|
1279
1312
|
|
|
1280
|
-
| Finding | What it reports
|
|
1281
|
-
| ------------------- |
|
|
1282
|
-
| **Stats freshness** | Tables whose planner statistics are missing (never analyzed) or stale
|
|
1283
|
-
| **Vacuum health** | Tables with >10% dead tuples
|
|
1284
|
-
| **
|
|
1313
|
+
| Finding | What it reports |
|
|
1314
|
+
| ------------------- | ------------------------------------------------------------------------------------------ |
|
|
1315
|
+
| **Stats freshness** | Tables whose planner statistics are missing (never analyzed) or stale |
|
|
1316
|
+
| **Vacuum health** | Tables with >10% dead tuples |
|
|
1317
|
+
| **Table bloat** | Tables with significant estimated wasted space (≥100 MB AND ≥25% bloat, ioguix estimator) |
|
|
1318
|
+
| **Index bloat** | btree indexes with significant estimated wasted space (≥100 MB AND ≥30%, ioguix estimator) |
|
|
1285
1319
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
findings
|
|
1289
|
-
|
|
1320
|
+
Now that per-table autovacuum / fillfactor knobs are expressible in
|
|
1321
|
+
[storage parameters](#storage-parameters) on `model_options`, these
|
|
1322
|
+
findings may graduate back to the warning tier in a future release — the
|
|
1323
|
+
remedy is now in code.
|
|
1290
1324
|
|
|
1291
1325
|
### Informational context
|
|
1292
1326
|
|
|
@@ -1335,7 +1369,7 @@ heroku run -a your-app "plain postgres diagnose --json"
|
|
|
1335
1369
|
|
|
1336
1370
|
The `--json` flag must be quoted so Heroku passes it through to the command.
|
|
1337
1371
|
|
|
1338
|
-
Cumulative-stat checks (`stats_freshness`, `vacuum_health`, `unused_indexes`, `missing_index_candidates`, `index_bloat`) need cumulative stat history after the last reset to be reliable. Check the `stats_reset` informational to see how much history you have. (Note: this list spans both the warning and operational tiers — the common thread is that all five depend on counters that `pg_stat_reset()` wipes.)
|
|
1372
|
+
Cumulative-stat checks (`stats_freshness`, `vacuum_health`, `unused_indexes`, `missing_index_candidates`, `table_bloat`, `index_bloat`) need cumulative stat history after the last reset to be reliable. Check the `stats_reset` informational to see how much history you have. (Note: this list spans both the warning and operational tiers — the common thread is that all five depend on counters that `pg_stat_reset()` wipes.)
|
|
1339
1373
|
|
|
1340
1374
|
### Preflight checks
|
|
1341
1375
|
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.103.0](https://github.com/dropseed/plain/releases/plain-postgres@0.103.0) (2026-05-06)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- **New `storage_parameters` on `model_options`, managed by convergence.** Declare per-table Postgres storage parameters (`pg_class.reloptions`) on the model — autovacuum tuning, `fillfactor`, TOAST options, anything you'd otherwise set with `ALTER TABLE … SET (...)` — and `plain postgres sync` reconciles them via instant catalog-only `ALTER TABLE … SET / RESET (...)` statements. Models are the source of truth: parameters set on the live table that aren't declared on the model get reset, matching how indexes and constraints work. TOAST parameters use a `toast.` prefix (`toast.autovacuum_vacuum_scale_factor`) and are stored on the toast relation. Storage parameters are not serialized into migrations. New public API: `StorageParameterDrift`, `SetStorageParameterFix`, `ResetStorageParameterFix`. ([7fe40f72](https://github.com/dropseed/plain/commit/7fe40f72))
|
|
8
|
+
- **New `table_bloat` health check.** Estimates per-table page-level bloat using the ioguix estimator (same heuristic as pghero). Complements `vacuum_health`: dead-tuple counts only show what autovacuum hasn't reclaimed yet, but a table that's been vacuumed regularly can still carry gigabytes of bloat because plain `VACUUM` marks pages reusable without returning space to the OS. Surfaces tables with both >100 MB wasted bytes AND >25% bloat ratio, with `pg_repack` / `pg_squeeze` / `VACUUM FULL` suggestions. Cross-check caveats now link `vacuum_health` and `table_bloat` findings on the same table. ([ee9dc1d5](https://github.com/dropseed/plain/commit/ee9dc1d5))
|
|
9
|
+
- **Tightened `index_bloat` thresholds.** Now requires both >100 MB wasted bytes AND >30% bloat ratio (was 10 MB only). The previous floor surfaced too many small, healthy indexes; the higher percentage bar reflects that `REINDEX CONCURRENTLY` is cheap so it's only worth flagging genuinely degraded indexes. Results are also capped at 100 rows per check. ([ee9dc1d5](https://github.com/dropseed/plain/commit/ee9dc1d5))
|
|
10
|
+
|
|
11
|
+
### Upgrade instructions
|
|
12
|
+
|
|
13
|
+
- No changes required. To opt into the new `storage_parameters` API, declare them on `model_options = postgres.Options(storage_parameters={...})` and run `plain postgres sync`. After upgrading, expect previously-noisy `index_bloat` findings to disappear (now require ≥100 MB AND ≥30%) and the new `table_bloat` check to appear in `plain postgres diagnose`.
|
|
14
|
+
|
|
15
|
+
## [0.102.0](https://github.com/dropseed/plain/releases/plain-postgres@0.102.0) (2026-05-05)
|
|
16
|
+
|
|
17
|
+
### What's changed
|
|
18
|
+
|
|
19
|
+
- **`Model.query` is now bound to `Self` (PEP 673), so subclasses specialize automatically.** `User.query` types as `QuerySet[User]` and `User.query.first()` as `User | None` without per-model annotations. Custom `QuerySet` subclasses (e.g. `TaskQuerySet`) are still preserved by the existing `Self`-returning descriptor. Now-redundant `cast(T, ...)` wrappers in the FK/M2M related managers are gone — `self.model.query.create(...)` already types as `T`. ([0f5b2f66](https://github.com/dropseed/plain/commit/0f5b2f66))
|
|
20
|
+
- **Convergence diffs are now canonicalized through Postgres `pg_get_*` round-trips on a session-private temp table** instead of sqlparse-based text normalization. Both sides of every index/constraint/default comparison are deparsed by Postgres itself, eliminating false-positive drift from formatting differences. Adds `ReadOnlyConnectionError` when the round-trip can't get DDL. The `normalize_check_definition`, `normalize_default_sql`, `normalize_expression`, `normalize_index_definition`, and `normalize_unique_definition` helpers are removed from `plain.postgres.introspection`, and `sqlparse` is no longer a dependency. ([4b42b4d1](https://github.com/dropseed/plain/commit/4b42b4d1))
|
|
21
|
+
- **`CheckConstraint.validate()` now exits early when a referenced field is missing from the value map**, deferring to the field-level error that excluded it. Calling `full_clean()` on a model with both `choices=` and a `CheckConstraint` referencing the same field used to crash with `AssertionError: Field lookups require a model` — the choice error excluded the field, then constraint validation tried to resolve the missing annotation. The walker is exposed as the public `CheckConstraint.referenced_fields()` method. ([d13f47d1](https://github.com/dropseed/plain/commit/d13f47d1))
|
|
22
|
+
- Tightened class-level annotations on `Query.select` and friends, `Operation.atomic`, and `ChoicesField.choices` for ty 0.0.33; replaced the `ModelState` `fields_cache` descriptor with a plain `__init__`. ([4b9d1db1](https://github.com/dropseed/plain/commit/4b9d1db1))
|
|
23
|
+
- Exposes `__version__` from `importlib.metadata` on `plain.postgres`. ([c6cf6edb](https://github.com/dropseed/plain/commit/c6cf6edb))
|
|
24
|
+
|
|
25
|
+
### Upgrade instructions
|
|
26
|
+
|
|
27
|
+
- If you imported any of `normalize_check_definition`, `normalize_default_sql`, `normalize_expression`, `normalize_index_definition`, or `normalize_unique_definition` from `plain.postgres.introspection`, those helpers are gone — use `pg_get_indexdef` / `pg_get_constraintdef` directly or rely on the new convergence round-trip path.
|
|
28
|
+
|
|
3
29
|
## [0.101.0](https://github.com/dropseed/plain/releases/plain-postgres@0.101.0) (2026-04-30)
|
|
4
30
|
|
|
5
31
|
### What's changed
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: plain.postgres
|
|
3
|
-
Version: 0.101.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.134.0
|
|
10
|
-
Requires-Dist: psycopg-pool>=3.2
|
|
11
|
-
Requires-Dist: psycopg>=3.2
|
|
12
|
-
Requires-Dist: sqlparse>=0.3.1
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
|
|
15
1
|
# plain.postgres
|
|
16
2
|
|
|
17
3
|
**Model your data and store it in a database.**
|
|
@@ -701,7 +687,7 @@ Or use `RunSQL` with explicit cascade if the relationship is large.
|
|
|
701
687
|
|
|
702
688
|
### Convergence
|
|
703
689
|
|
|
704
|
-
Convergence compares the indexes, constraints, foreign keys, and
|
|
690
|
+
Convergence compares the indexes, constraints, foreign keys, nullability, and [storage parameters](#storage-parameters) declared on your models against what actually exists in the database, then applies fixes to make them match. You don't need to create migrations for these — just declare them on your model and run `postgres sync`.
|
|
705
691
|
|
|
706
692
|
```python
|
|
707
693
|
@postgres.register_model
|
|
@@ -1092,6 +1078,26 @@ A `code` becomes `ValidationError.code` — useful for test assertions, error tr
|
|
|
1092
1078
|
|
|
1093
1079
|
`UniqueConstraint` accepts the same `violation_error`. With a single-field unique constraint, a string `violation_error="That email is taken."` auto-routes to that field; otherwise (multi-field, expressions, or a CheckConstraint) errors land on `NON_FIELD_ERRORS` unless you pass the dict form. See [BaseConstraint](./constraints.py#BaseConstraint) for the full signature.
|
|
1094
1080
|
|
|
1081
|
+
### Storage parameters
|
|
1082
|
+
|
|
1083
|
+
Per-table Postgres [storage parameters](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS) (`pg_class.reloptions`) are declared on `model_options` and managed by [convergence](#convergence) — `postgres sync` issues the `ALTER TABLE … SET/RESET (...)` to make the live table match. They're not serialized into migrations.
|
|
1084
|
+
|
|
1085
|
+
```python
|
|
1086
|
+
class CachedItem(postgres.Model):
|
|
1087
|
+
...
|
|
1088
|
+
|
|
1089
|
+
model_options = postgres.Options(
|
|
1090
|
+
storage_parameters={
|
|
1091
|
+
# Tighter autovacuum on a churn-heavy table
|
|
1092
|
+
"autovacuum_vacuum_scale_factor": 0.1,
|
|
1093
|
+
# TOAST has its own autovacuum schedule — prefix with `toast.`
|
|
1094
|
+
"toast.autovacuum_vacuum_scale_factor": 0.05,
|
|
1095
|
+
},
|
|
1096
|
+
)
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
Models are the source of truth: undeclared parameters set on the live table are reset on the next sync. Use this for autovacuum tuning, `fillfactor`, TOAST options, etc. — anything you'd otherwise apply by hand with `ALTER TABLE … SET (...)`.
|
|
1100
|
+
|
|
1095
1101
|
### Schema design
|
|
1096
1102
|
|
|
1097
1103
|
#### Index fields used in filters and ordering
|
|
@@ -1291,16 +1297,17 @@ fix in their model code. (In JSON output each finding still carries
|
|
|
1291
1297
|
it.) Each finding still carries the exact SQL in its suggestion for anyone
|
|
1292
1298
|
who wants to act.
|
|
1293
1299
|
|
|
1294
|
-
| Finding | What it reports
|
|
1295
|
-
| ------------------- |
|
|
1296
|
-
| **Stats freshness** | Tables whose planner statistics are missing (never analyzed) or stale
|
|
1297
|
-
| **Vacuum health** | Tables with >10% dead tuples
|
|
1298
|
-
| **
|
|
1300
|
+
| Finding | What it reports |
|
|
1301
|
+
| ------------------- | ------------------------------------------------------------------------------------------ |
|
|
1302
|
+
| **Stats freshness** | Tables whose planner statistics are missing (never analyzed) or stale |
|
|
1303
|
+
| **Vacuum health** | Tables with >10% dead tuples |
|
|
1304
|
+
| **Table bloat** | Tables with significant estimated wasted space (≥100 MB AND ≥25% bloat, ioguix estimator) |
|
|
1305
|
+
| **Index bloat** | btree indexes with significant estimated wasted space (≥100 MB AND ≥30%, ioguix estimator) |
|
|
1299
1306
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
findings
|
|
1303
|
-
|
|
1307
|
+
Now that per-table autovacuum / fillfactor knobs are expressible in
|
|
1308
|
+
[storage parameters](#storage-parameters) on `model_options`, these
|
|
1309
|
+
findings may graduate back to the warning tier in a future release — the
|
|
1310
|
+
remedy is now in code.
|
|
1304
1311
|
|
|
1305
1312
|
### Informational context
|
|
1306
1313
|
|
|
@@ -1349,7 +1356,7 @@ heroku run -a your-app "plain postgres diagnose --json"
|
|
|
1349
1356
|
|
|
1350
1357
|
The `--json` flag must be quoted so Heroku passes it through to the command.
|
|
1351
1358
|
|
|
1352
|
-
Cumulative-stat checks (`stats_freshness`, `vacuum_health`, `unused_indexes`, `missing_index_candidates`, `index_bloat`) need cumulative stat history after the last reset to be reliable. Check the `stats_reset` informational to see how much history you have. (Note: this list spans both the warning and operational tiers — the common thread is that all five depend on counters that `pg_stat_reset()` wipes.)
|
|
1359
|
+
Cumulative-stat checks (`stats_freshness`, `vacuum_health`, `unused_indexes`, `missing_index_candidates`, `table_bloat`, `index_bloat`) need cumulative stat history after the last reset to be reliable. Check the `stats_reset` informational to see how much history you have. (Note: this list spans both the warning and operational tiers — the common thread is that all five depend on counters that `pg_stat_reset()` wipes.)
|
|
1353
1360
|
|
|
1354
1361
|
### Preflight checks
|
|
1355
1362
|
|
|
@@ -36,7 +36,7 @@ Get approval before writing any model code or generating migrations.
|
|
|
36
36
|
`uv run plain postgres sync` runs three steps: create migrations → apply migrations → converge schema.
|
|
37
37
|
|
|
38
38
|
- **Migrations** handle tables and columns (CreateModel, AddField, AlterField, etc.)
|
|
39
|
-
- **Convergence** handles indexes, constraints,
|
|
39
|
+
- **Convergence** handles indexes, constraints, FK constraints, and storage parameters — declared on the model but NOT serialized into migration files. (FK _columns_ like `team_id bigint` are created by migrations; the actual `FOREIGN KEY` constraint is added by convergence.)
|
|
40
40
|
|
|
41
41
|
This means: when you add an `Index` or `UniqueConstraint` to a model, no migration is generated. The converge step reads the live model class and syncs the database directly. Don't worry about serializing constraint expressions (like `Lower()`) for migrations — they never go there.
|
|
42
42
|
|
|
@@ -4,7 +4,7 @@ import copy
|
|
|
4
4
|
import warnings
|
|
5
5
|
from collections.abc import Iterable, Iterator, Sequence
|
|
6
6
|
from itertools import chain
|
|
7
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Self, cast
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from plain.postgres.meta import Meta
|
|
@@ -68,16 +68,6 @@ class ModelBase(type):
|
|
|
68
68
|
return super().__new__(cls, name, bases, attrs, **kwargs)
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
class ModelStateFieldsCacheDescriptor:
|
|
72
|
-
def __get__(
|
|
73
|
-
self, instance: ModelState | None, cls: type | None = None
|
|
74
|
-
) -> ModelStateFieldsCacheDescriptor | dict[str, Any]:
|
|
75
|
-
if instance is None:
|
|
76
|
-
return self
|
|
77
|
-
res = instance.fields_cache = {}
|
|
78
|
-
return res
|
|
79
|
-
|
|
80
|
-
|
|
81
71
|
class ModelState:
|
|
82
72
|
"""Store model instance state."""
|
|
83
73
|
|
|
@@ -86,7 +76,9 @@ class ModelState:
|
|
|
86
76
|
# explicit (non-auto) PKs. This impacts validation only; it has no effect
|
|
87
77
|
# on the actual save.
|
|
88
78
|
adding = True
|
|
89
|
-
|
|
79
|
+
|
|
80
|
+
def __init__(self) -> None:
|
|
81
|
+
self.fields_cache: dict[str, Any] = {}
|
|
90
82
|
|
|
91
83
|
|
|
92
84
|
class Model(metaclass=ModelBase):
|
|
@@ -94,7 +86,7 @@ class Model(metaclass=ModelBase):
|
|
|
94
86
|
id: int = types.PrimaryKeyField()
|
|
95
87
|
|
|
96
88
|
# Descriptors for other model behavior
|
|
97
|
-
query: QuerySet[
|
|
89
|
+
query: QuerySet[Self] = QuerySet()
|
|
98
90
|
model_options: Options = Options()
|
|
99
91
|
_model_meta: Meta = Meta()
|
|
100
92
|
DoesNotExist = DoesNotExistDescriptor()
|
|
@@ -15,7 +15,6 @@ from plain.logs import get_framework_logger
|
|
|
15
15
|
from plain.postgres import utils
|
|
16
16
|
from plain.postgres.dialect import quote_name
|
|
17
17
|
from plain.postgres.fields import GenericIPAddressField, TimeField, UUIDField
|
|
18
|
-
from plain.postgres.indexes import Index
|
|
19
18
|
from plain.postgres.schema import DatabaseSchemaEditor
|
|
20
19
|
from plain.postgres.sources import ConnectionSource
|
|
21
20
|
from plain.postgres.transaction import TransactionManagementError
|
|
@@ -63,7 +62,6 @@ class DatabaseConnection:
|
|
|
63
62
|
|
|
64
63
|
queries_limit: int = 9000
|
|
65
64
|
|
|
66
|
-
index_default_access_method = "btree"
|
|
67
65
|
ignored_tables: list[str] = []
|
|
68
66
|
|
|
69
67
|
def __init__(self, source: ConnectionSource):
|
|
@@ -607,7 +605,6 @@ class DatabaseConnection:
|
|
|
607
605
|
FROM pg_attribute AS fka
|
|
608
606
|
JOIN pg_class AS fkc ON fka.attrelid = fkc.oid
|
|
609
607
|
WHERE fka.attrelid = c.confrelid AND fka.attnum = c.confkey[1]),
|
|
610
|
-
cl.reloptions,
|
|
611
608
|
c.convalidated,
|
|
612
609
|
pg_get_constraintdef(c.oid),
|
|
613
610
|
c.confdeltype
|
|
@@ -622,54 +619,42 @@ class DatabaseConnection:
|
|
|
622
619
|
columns,
|
|
623
620
|
kind,
|
|
624
621
|
used_cols,
|
|
625
|
-
options,
|
|
626
622
|
validated,
|
|
627
623
|
constraintdef,
|
|
628
624
|
confdeltype,
|
|
629
625
|
) in cursor.fetchall():
|
|
630
626
|
constraints[constraint] = {
|
|
631
627
|
"columns": columns,
|
|
632
|
-
"primary_key": kind == "p",
|
|
633
|
-
"unique": kind in ["p", "u"],
|
|
634
628
|
"foreign_key": tuple(used_cols.split(".", 1)) if kind == "f" else None,
|
|
635
|
-
"check": kind == "c",
|
|
636
629
|
"contype": kind,
|
|
637
630
|
"index": False,
|
|
638
631
|
"definition": constraintdef,
|
|
639
|
-
"options": options,
|
|
640
632
|
"validated": validated,
|
|
641
633
|
"on_delete_action": confdeltype if kind == "f" else None,
|
|
642
634
|
}
|
|
643
|
-
# Now get indexes
|
|
635
|
+
# Now get indexes. Sort order, opclasses, INCLUDE, and predicates all
|
|
636
|
+
# ride along inside `pg_get_indexdef` and are compared via the
|
|
637
|
+
# canonical-tail round-trip in convergence — no need to introspect
|
|
638
|
+
# them here as separate columns.
|
|
644
639
|
cursor.execute(
|
|
645
640
|
"""
|
|
646
641
|
SELECT
|
|
647
642
|
indexname,
|
|
648
643
|
array_agg(attname ORDER BY arridx),
|
|
649
644
|
indisunique,
|
|
650
|
-
indisprimary,
|
|
651
|
-
array_agg(ordering ORDER BY arridx),
|
|
652
645
|
amname,
|
|
653
646
|
exprdef,
|
|
654
|
-
|
|
655
|
-
s2.indisvalid
|
|
647
|
+
indisvalid
|
|
656
648
|
FROM (
|
|
657
649
|
SELECT
|
|
658
650
|
c2.relname as indexname, idx.*, attr.attname, am.amname,
|
|
659
|
-
pg_get_indexdef(idx.indexrelid) AS exprdef
|
|
660
|
-
CASE am.amname
|
|
661
|
-
WHEN %s THEN
|
|
662
|
-
CASE (option & 1)
|
|
663
|
-
WHEN 1 THEN 'DESC' ELSE 'ASC'
|
|
664
|
-
END
|
|
665
|
-
END as ordering,
|
|
666
|
-
c2.reloptions as attoptions
|
|
651
|
+
pg_get_indexdef(idx.indexrelid) AS exprdef
|
|
667
652
|
FROM (
|
|
668
653
|
SELECT *
|
|
669
654
|
FROM
|
|
670
655
|
pg_index i,
|
|
671
|
-
unnest(i.indkey
|
|
672
|
-
WITH ORDINALITY koi(key,
|
|
656
|
+
unnest(i.indkey)
|
|
657
|
+
WITH ORDINALITY koi(key, arridx)
|
|
673
658
|
) idx
|
|
674
659
|
LEFT JOIN pg_class c ON idx.indrelid = c.oid
|
|
675
660
|
LEFT JOIN pg_class c2 ON idx.indexrelid = c2.oid
|
|
@@ -678,36 +663,26 @@ class DatabaseConnection:
|
|
|
678
663
|
pg_attribute attr ON attr.attrelid = c.oid AND attr.attnum = idx.key
|
|
679
664
|
WHERE c.relname = %s AND pg_catalog.pg_table_is_visible(c.oid)
|
|
680
665
|
) s2
|
|
681
|
-
GROUP BY
|
|
666
|
+
GROUP BY
|
|
667
|
+
indexname, indisunique, amname, exprdef, indisvalid;
|
|
682
668
|
""",
|
|
683
|
-
[
|
|
669
|
+
[table_name],
|
|
684
670
|
)
|
|
685
671
|
for (
|
|
686
672
|
index,
|
|
687
673
|
columns,
|
|
688
674
|
unique,
|
|
689
|
-
primary,
|
|
690
|
-
orders,
|
|
691
675
|
type_,
|
|
692
676
|
definition,
|
|
693
|
-
options,
|
|
694
677
|
valid,
|
|
695
678
|
) in cursor.fetchall():
|
|
696
679
|
if index not in constraints:
|
|
697
|
-
basic_index = (
|
|
698
|
-
type_ == self.index_default_access_method and options is None
|
|
699
|
-
)
|
|
700
680
|
constraints[index] = {
|
|
701
681
|
"columns": columns if columns != [None] else [],
|
|
702
|
-
"orders": orders if orders != [None] else [],
|
|
703
|
-
"primary_key": primary,
|
|
704
682
|
"unique": unique,
|
|
705
|
-
"foreign_key": None,
|
|
706
|
-
"check": False,
|
|
707
683
|
"index": True,
|
|
708
|
-
"type":
|
|
684
|
+
"type": type_,
|
|
709
685
|
"definition": definition,
|
|
710
|
-
"options": options,
|
|
711
686
|
"valid": valid,
|
|
712
687
|
}
|
|
713
688
|
return constraints
|
|
@@ -5,6 +5,7 @@ from types import NoneType
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
from plain.exceptions import ValidationError
|
|
8
|
+
from plain.postgres.constants import LOOKUP_SEP
|
|
8
9
|
from plain.postgres.ddl import (
|
|
9
10
|
build_include_sql,
|
|
10
11
|
compile_expression_sql,
|
|
@@ -104,10 +105,41 @@ class CheckConstraint(BaseConstraint):
|
|
|
104
105
|
sql += " NOT VALID"
|
|
105
106
|
return sql
|
|
106
107
|
|
|
108
|
+
def referenced_fields(self) -> set[str]:
|
|
109
|
+
"""Top-level model field names referenced by `self.check`.
|
|
110
|
+
|
|
111
|
+
Walks lookup keys (`field__regex` → `field`), nested Q nodes, and
|
|
112
|
+
F-expressions in values or other source expressions.
|
|
113
|
+
"""
|
|
114
|
+
fields: set[str] = set()
|
|
115
|
+
|
|
116
|
+
def visit(node: Any) -> None:
|
|
117
|
+
if isinstance(node, Q):
|
|
118
|
+
for child in node.children:
|
|
119
|
+
visit(child)
|
|
120
|
+
elif isinstance(node, tuple) and len(node) == 2:
|
|
121
|
+
lookup, value = node
|
|
122
|
+
fields.add(lookup.split(LOOKUP_SEP, 1)[0])
|
|
123
|
+
visit(value)
|
|
124
|
+
elif isinstance(node, F):
|
|
125
|
+
fields.add(node.name.split(LOOKUP_SEP, 1)[0])
|
|
126
|
+
elif hasattr(node, "get_source_expressions"):
|
|
127
|
+
for sub in node.get_source_expressions():
|
|
128
|
+
visit(sub)
|
|
129
|
+
|
|
130
|
+
visit(self.check)
|
|
131
|
+
return fields
|
|
132
|
+
|
|
107
133
|
def validate(
|
|
108
134
|
self, model: type[Model], instance: Model, exclude: set[str] | None = None
|
|
109
135
|
) -> None:
|
|
110
136
|
against = instance._get_field_value_map(meta=model._model_meta, exclude=exclude)
|
|
137
|
+
# Skip the check entirely when any field referenced by `self.check` was
|
|
138
|
+
# excluded — the in-Python pipeline can't resolve a missing field's
|
|
139
|
+
# annotation, and surfacing a constraint violation here would just
|
|
140
|
+
# duplicate the field-level error that caused the exclusion.
|
|
141
|
+
if not self.referenced_fields().issubset(against):
|
|
142
|
+
return
|
|
111
143
|
try:
|
|
112
144
|
if not Q(self.check).check(against):
|
|
113
145
|
raise self._build_violation_error()
|
|
@@ -11,6 +11,8 @@ from .analysis import (
|
|
|
11
11
|
IndexStatus,
|
|
12
12
|
ModelAnalysis,
|
|
13
13
|
NullabilityDrift,
|
|
14
|
+
ReadOnlyConnectionError,
|
|
15
|
+
StorageParameterDrift,
|
|
14
16
|
analyze_model,
|
|
15
17
|
)
|
|
16
18
|
from .fixes import (
|
|
@@ -25,8 +27,10 @@ from .fixes import (
|
|
|
25
27
|
RebuildIndexFix,
|
|
26
28
|
RenameConstraintFix,
|
|
27
29
|
RenameIndexFix,
|
|
30
|
+
ResetStorageParameterFix,
|
|
28
31
|
SetColumnDefaultFix,
|
|
29
32
|
SetNotNullFix,
|
|
33
|
+
SetStorageParameterFix,
|
|
30
34
|
ValidateConstraintFix,
|
|
31
35
|
)
|
|
32
36
|
from .planning import (
|
|
@@ -65,11 +69,15 @@ __all__ = [
|
|
|
65
69
|
"ModelAnalysis",
|
|
66
70
|
"NullabilityDrift",
|
|
67
71
|
"PlanItem",
|
|
72
|
+
"ReadOnlyConnectionError",
|
|
68
73
|
"RebuildIndexFix",
|
|
69
74
|
"RenameConstraintFix",
|
|
70
75
|
"RenameIndexFix",
|
|
76
|
+
"ResetStorageParameterFix",
|
|
71
77
|
"SetColumnDefaultFix",
|
|
72
78
|
"SetNotNullFix",
|
|
79
|
+
"SetStorageParameterFix",
|
|
80
|
+
"StorageParameterDrift",
|
|
73
81
|
"ValidateConstraintFix",
|
|
74
82
|
"analyze_model",
|
|
75
83
|
"can_auto_fix",
|