plain.postgres 0.102.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.102.0/plain/postgres/README.md → plain_postgres-0.103.0/PKG-INFO +45 -11
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/CHANGELOG.md +12 -0
- plain_postgres-0.102.0/PKG-INFO → plain_postgres-0.103.0/plain/postgres/README.md +32 -24
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +1 -1
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/convergence/__init__.py +6 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/convergence/analysis.py +100 -2
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/convergence/fixes.py +42 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/convergence/planning.py +13 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/checks_cumulative.py +205 -9
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/runner.py +19 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/introspection/schema.py +55 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/options.py +11 -4
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/pyproject.toml +1 -1
- plain_postgres-0.103.0/tests/app/examples/migrations/0018_storageparametersexample.py +18 -0
- {plain_postgres-0.102.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.103.0/tests/internal/test_convergence_storage_parameters.py +222 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_diagnose.py +1 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/.gitignore +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/CLAUDE.md +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/LICENSE +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/README.md +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/__init__.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/adapters.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/base.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/cli/decorators.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/cli/sync.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/connection.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/constraints.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/dialect.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/base.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/binary.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/boolean.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/duration.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/encrypted.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/network.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/numeric.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/primary_key.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/related.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/related_managers.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/temporal.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/text.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/timezones.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/fields/uuid.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/forms.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/indexes.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/introspection/__init__.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/__init__.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/context.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/helpers.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/ownership.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/types.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/meta.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/middleware.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/autodetector.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/state.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/preflight.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/query.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/schema.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/sources.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/sql/compiler.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/sql/query.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.102.0/tests/app/examples/migrations → plain_postgres-0.103.0/plain/postgres/test}/__init__.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/test/database.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/types.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {plain_postgres-0.102.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.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.102.0/plain/postgres/test → plain_postgres-0.103.0/tests/app/examples/migrations}/__init__.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/constraints.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/defaults.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/delete.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/encrypted.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/forms.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/indexes.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/iteration.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/mixins.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/nullability.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/querysets.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/relationships.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/trees.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/urls.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/conftest.py +0 -0
- {plain_postgres-0.102.0 → plain_postgres-0.103.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_autodetector_not_null_errors.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_autodetector_type_change.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_connection_isolation.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_connection_pool.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_constraint_violation_error.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_constraints.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_defaults.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_fk.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_indexes.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_nullability.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_timeouts.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_db_expression_defaults.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_executor_connection_hook.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_health.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_introspection.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_literal_default_persistence.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_management_connection.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_migration_executor.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_otel_metrics.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/internal}/test_schema_timeouts.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_database_url.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_delete_behaviors.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_encrypted_fields.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_exceptions.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_field_defaults.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_functions_uuid.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_iterator.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_m2m.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_manager_assignment.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_mixins.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_modelform_roundtrip.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_random_string_field.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_raw_query.py +0 -0
- {plain_postgres-0.102.0/tests → plain_postgres-0.103.0/tests/public}/test_read_only_transactions.py +0 -0
- {plain_postgres-0.102.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,17 @@
|
|
|
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
|
+
|
|
3
15
|
## [0.102.0](https://github.com/dropseed/plain/releases/plain-postgres@0.102.0) (2026-05-05)
|
|
4
16
|
|
|
5
17
|
### What's changed
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: plain.postgres
|
|
3
|
-
Version: 0.102.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
|
-
|
|
14
1
|
# plain.postgres
|
|
15
2
|
|
|
16
3
|
**Model your data and store it in a database.**
|
|
@@ -700,7 +687,7 @@ Or use `RunSQL` with explicit cascade if the relationship is large.
|
|
|
700
687
|
|
|
701
688
|
### Convergence
|
|
702
689
|
|
|
703
|
-
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`.
|
|
704
691
|
|
|
705
692
|
```python
|
|
706
693
|
@postgres.register_model
|
|
@@ -1091,6 +1078,26 @@ A `code` becomes `ValidationError.code` — useful for test assertions, error tr
|
|
|
1091
1078
|
|
|
1092
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.
|
|
1093
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
|
+
|
|
1094
1101
|
### Schema design
|
|
1095
1102
|
|
|
1096
1103
|
#### Index fields used in filters and ordering
|
|
@@ -1290,16 +1297,17 @@ fix in their model code. (In JSON output each finding still carries
|
|
|
1290
1297
|
it.) Each finding still carries the exact SQL in its suggestion for anyone
|
|
1291
1298
|
who wants to act.
|
|
1292
1299
|
|
|
1293
|
-
| Finding | What it reports
|
|
1294
|
-
| ------------------- |
|
|
1295
|
-
| **Stats freshness** | Tables whose planner statistics are missing (never analyzed) or stale
|
|
1296
|
-
| **Vacuum health** | Tables with >10% dead tuples
|
|
1297
|
-
| **
|
|
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) |
|
|
1298
1306
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
findings
|
|
1302
|
-
|
|
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.
|
|
1303
1311
|
|
|
1304
1312
|
### Informational context
|
|
1305
1313
|
|
|
@@ -1348,7 +1356,7 @@ heroku run -a your-app "plain postgres diagnose --json"
|
|
|
1348
1356
|
|
|
1349
1357
|
The `--json` flag must be quoted so Heroku passes it through to the command.
|
|
1350
1358
|
|
|
1351
|
-
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.)
|
|
1352
1360
|
|
|
1353
1361
|
### Preflight checks
|
|
1354
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
|
|
|
@@ -12,6 +12,7 @@ from .analysis import (
|
|
|
12
12
|
ModelAnalysis,
|
|
13
13
|
NullabilityDrift,
|
|
14
14
|
ReadOnlyConnectionError,
|
|
15
|
+
StorageParameterDrift,
|
|
15
16
|
analyze_model,
|
|
16
17
|
)
|
|
17
18
|
from .fixes import (
|
|
@@ -26,8 +27,10 @@ from .fixes import (
|
|
|
26
27
|
RebuildIndexFix,
|
|
27
28
|
RenameConstraintFix,
|
|
28
29
|
RenameIndexFix,
|
|
30
|
+
ResetStorageParameterFix,
|
|
29
31
|
SetColumnDefaultFix,
|
|
30
32
|
SetNotNullFix,
|
|
33
|
+
SetStorageParameterFix,
|
|
31
34
|
ValidateConstraintFix,
|
|
32
35
|
)
|
|
33
36
|
from .planning import (
|
|
@@ -70,8 +73,11 @@ __all__ = [
|
|
|
70
73
|
"RebuildIndexFix",
|
|
71
74
|
"RenameConstraintFix",
|
|
72
75
|
"RenameIndexFix",
|
|
76
|
+
"ResetStorageParameterFix",
|
|
73
77
|
"SetColumnDefaultFix",
|
|
74
78
|
"SetNotNullFix",
|
|
79
|
+
"SetStorageParameterFix",
|
|
80
|
+
"StorageParameterDrift",
|
|
75
81
|
"ValidateConstraintFix",
|
|
76
82
|
"analyze_model",
|
|
77
83
|
"can_auto_fix",
|
|
@@ -191,8 +191,44 @@ class ColumnDefaultDrift:
|
|
|
191
191
|
)
|
|
192
192
|
|
|
193
193
|
|
|
194
|
+
@dataclass
|
|
195
|
+
class StorageParameterDrift:
|
|
196
|
+
"""Mismatch between declared and live `pg_class.reloptions` for a table.
|
|
197
|
+
|
|
198
|
+
`key` carries a `toast.` prefix when the parameter belongs to the table's
|
|
199
|
+
TOAST relation; convergence emits and reads it accordingly.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
kind: DriftKind
|
|
203
|
+
table: str
|
|
204
|
+
key: str
|
|
205
|
+
declared_value: str | None = None
|
|
206
|
+
actual_value: str | None = None
|
|
207
|
+
|
|
208
|
+
def describe(self) -> str:
|
|
209
|
+
match self.kind:
|
|
210
|
+
case DriftKind.MISSING:
|
|
211
|
+
return (
|
|
212
|
+
f"{self.table}: storage parameter {self.key} missing "
|
|
213
|
+
f"(expected {self.declared_value})"
|
|
214
|
+
)
|
|
215
|
+
case DriftKind.CHANGED:
|
|
216
|
+
return (
|
|
217
|
+
f"{self.table}: storage parameter {self.key} mismatch — "
|
|
218
|
+
f"db has {self.actual_value}, model declares "
|
|
219
|
+
f"{self.declared_value}"
|
|
220
|
+
)
|
|
221
|
+
case _:
|
|
222
|
+
return (
|
|
223
|
+
f"{self.table}: storage parameter {self.key} not declared "
|
|
224
|
+
f"(db has {self.actual_value})"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
194
228
|
type ColumnDrift = NullabilityDrift | ColumnDefaultDrift
|
|
195
|
-
type Drift =
|
|
229
|
+
type Drift = (
|
|
230
|
+
IndexDrift | ConstraintDrift | ForeignKeyDrift | ColumnDrift | StorageParameterDrift
|
|
231
|
+
)
|
|
196
232
|
|
|
197
233
|
|
|
198
234
|
# Status objects — analysis results with optional drift
|
|
@@ -236,6 +272,7 @@ class ModelAnalysis:
|
|
|
236
272
|
columns: list[ColumnStatus] = field(default_factory=list)
|
|
237
273
|
indexes: list[IndexStatus] = field(default_factory=list)
|
|
238
274
|
constraints: list[ConstraintStatus] = field(default_factory=list)
|
|
275
|
+
storage_parameter_drifts: list[StorageParameterDrift] = field(default_factory=list)
|
|
239
276
|
|
|
240
277
|
@cached_property
|
|
241
278
|
def drifts(self) -> list[Drift]:
|
|
@@ -249,15 +286,17 @@ class ModelAnalysis:
|
|
|
249
286
|
for con in self.constraints:
|
|
250
287
|
if con.drift:
|
|
251
288
|
result.append(con.drift)
|
|
289
|
+
result.extend(self.storage_parameter_drifts)
|
|
252
290
|
return result
|
|
253
291
|
|
|
254
292
|
@cached_property
|
|
255
293
|
def issue_count(self) -> int:
|
|
256
|
-
"""Total issues (table + columns + indexes + constraints)."""
|
|
294
|
+
"""Total issues (table + columns + indexes + constraints + storage)."""
|
|
257
295
|
count = len(self.table_issues)
|
|
258
296
|
count += sum(1 for col in self.columns if col.issue)
|
|
259
297
|
count += sum(1 for idx in self.indexes if idx.issue)
|
|
260
298
|
count += sum(1 for con in self.constraints if con.issue)
|
|
299
|
+
count += len(self.storage_parameter_drifts)
|
|
261
300
|
return count
|
|
262
301
|
|
|
263
302
|
def to_dict(self) -> dict[str, Any]:
|
|
@@ -300,6 +339,15 @@ class ModelAnalysis:
|
|
|
300
339
|
}
|
|
301
340
|
for con in self.constraints
|
|
302
341
|
],
|
|
342
|
+
"storage_parameter_drifts": [
|
|
343
|
+
{
|
|
344
|
+
"key": d.key,
|
|
345
|
+
"kind": d.kind,
|
|
346
|
+
"declared_value": d.declared_value,
|
|
347
|
+
"actual_value": d.actual_value,
|
|
348
|
+
}
|
|
349
|
+
for d in self.storage_parameter_drifts
|
|
350
|
+
],
|
|
303
351
|
}
|
|
304
352
|
|
|
305
353
|
|
|
@@ -328,9 +376,59 @@ def analyze_model(
|
|
|
328
376
|
columns=_compare_columns(model, db, table_name, cursor),
|
|
329
377
|
indexes=_compare_indexes(cursor, model, db, table_name),
|
|
330
378
|
constraints=_compare_constraints(cursor, model, db, table_name),
|
|
379
|
+
storage_parameter_drifts=_compare_storage_parameters(model, db, table_name),
|
|
331
380
|
)
|
|
332
381
|
|
|
333
382
|
|
|
383
|
+
def _compare_storage_parameters(
|
|
384
|
+
model: type[Model], db: TableState, table: str
|
|
385
|
+
) -> list[StorageParameterDrift]:
|
|
386
|
+
"""Diff declared `model_options.storage_parameters` against `pg_class.reloptions`.
|
|
387
|
+
|
|
388
|
+
Declared keys missing from the DB → MISSING. Mismatched values → CHANGED.
|
|
389
|
+
Live keys not declared → UNDECLARED (so convergence can RESET them, keeping
|
|
390
|
+
the model as the source of truth — matches how indexes/constraints work).
|
|
391
|
+
"""
|
|
392
|
+
declared = model.model_options.storage_parameters
|
|
393
|
+
actual = db.storage_parameters
|
|
394
|
+
drifts: list[StorageParameterDrift] = []
|
|
395
|
+
|
|
396
|
+
for key, declared_value in declared.items():
|
|
397
|
+
actual_value = actual.get(key)
|
|
398
|
+
if actual_value is None:
|
|
399
|
+
drifts.append(
|
|
400
|
+
StorageParameterDrift(
|
|
401
|
+
kind=DriftKind.MISSING,
|
|
402
|
+
table=table,
|
|
403
|
+
key=key,
|
|
404
|
+
declared_value=declared_value,
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
elif actual_value != declared_value:
|
|
408
|
+
drifts.append(
|
|
409
|
+
StorageParameterDrift(
|
|
410
|
+
kind=DriftKind.CHANGED,
|
|
411
|
+
table=table,
|
|
412
|
+
key=key,
|
|
413
|
+
declared_value=declared_value,
|
|
414
|
+
actual_value=actual_value,
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
for key, actual_value in actual.items():
|
|
419
|
+
if key not in declared:
|
|
420
|
+
drifts.append(
|
|
421
|
+
StorageParameterDrift(
|
|
422
|
+
kind=DriftKind.UNDECLARED,
|
|
423
|
+
table=table,
|
|
424
|
+
key=key,
|
|
425
|
+
actual_value=actual_value,
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return drifts
|
|
430
|
+
|
|
431
|
+
|
|
334
432
|
# Column comparison
|
|
335
433
|
|
|
336
434
|
|
|
@@ -4,6 +4,9 @@ from abc import ABC, abstractmethod
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from typing import TYPE_CHECKING, ClassVar
|
|
6
6
|
|
|
7
|
+
import psycopg
|
|
8
|
+
import psycopg.sql
|
|
9
|
+
|
|
7
10
|
from plain.logs import get_framework_logger
|
|
8
11
|
from plain.runtime import settings as plain_settings
|
|
9
12
|
|
|
@@ -555,3 +558,42 @@ class DropIndexFix(Fix):
|
|
|
555
558
|
sql = f"DROP INDEX CONCURRENTLY IF EXISTS {quote_name(self.name)}"
|
|
556
559
|
_execute_autocommit(sql)
|
|
557
560
|
return sql
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@dataclass
|
|
564
|
+
class SetStorageParameterFix(Fix):
|
|
565
|
+
"""Set a single `pg_class.reloptions` parameter (catalog-only, instant)."""
|
|
566
|
+
|
|
567
|
+
pass_order = 2
|
|
568
|
+
|
|
569
|
+
table: str
|
|
570
|
+
key: str
|
|
571
|
+
value: str
|
|
572
|
+
|
|
573
|
+
def describe(self) -> str:
|
|
574
|
+
return f"{self.table}: set storage parameter {self.key} = {self.value}"
|
|
575
|
+
|
|
576
|
+
def apply(self) -> str:
|
|
577
|
+
conn = get_connection()
|
|
578
|
+
quoted = psycopg.sql.quote(self.value, conn.connection)
|
|
579
|
+
sql = f"ALTER TABLE {quote_name(self.table)} SET ({self.key} = {quoted})"
|
|
580
|
+
_execute_and_commit(sql)
|
|
581
|
+
return sql
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@dataclass
|
|
585
|
+
class ResetStorageParameterFix(Fix):
|
|
586
|
+
"""Reset a single `pg_class.reloptions` parameter (catalog-only, instant)."""
|
|
587
|
+
|
|
588
|
+
pass_order = 2
|
|
589
|
+
|
|
590
|
+
table: str
|
|
591
|
+
key: str
|
|
592
|
+
|
|
593
|
+
def describe(self) -> str:
|
|
594
|
+
return f"{self.table}: reset storage parameter {self.key}"
|
|
595
|
+
|
|
596
|
+
def apply(self) -> str:
|
|
597
|
+
sql = f"ALTER TABLE {quote_name(self.table)} RESET ({self.key})"
|
|
598
|
+
_execute_and_commit(sql)
|
|
599
|
+
return sql
|
|
@@ -14,6 +14,7 @@ from .analysis import (
|
|
|
14
14
|
ForeignKeyDrift,
|
|
15
15
|
IndexDrift,
|
|
16
16
|
NullabilityDrift,
|
|
17
|
+
StorageParameterDrift,
|
|
17
18
|
analyze_model,
|
|
18
19
|
)
|
|
19
20
|
from .fixes import (
|
|
@@ -29,8 +30,10 @@ from .fixes import (
|
|
|
29
30
|
RenameConstraintFix,
|
|
30
31
|
RenameIndexFix,
|
|
31
32
|
ReplaceForeignKeyFix,
|
|
33
|
+
ResetStorageParameterFix,
|
|
32
34
|
SetColumnDefaultFix,
|
|
33
35
|
SetNotNullFix,
|
|
36
|
+
SetStorageParameterFix,
|
|
34
37
|
ValidateConstraintFix,
|
|
35
38
|
)
|
|
36
39
|
|
|
@@ -140,6 +143,16 @@ def _plan_drift(drift: Drift) -> PlanItem:
|
|
|
140
143
|
return PlanItem(drift, SetColumnDefaultFix(t, col, default_sql))
|
|
141
144
|
case ColumnDefaultDrift(kind=DriftKind.UNDECLARED, table=t, column=col):
|
|
142
145
|
return PlanItem(drift, DropColumnDefaultFix(t, col))
|
|
146
|
+
case StorageParameterDrift(
|
|
147
|
+
kind=DriftKind.MISSING | DriftKind.CHANGED,
|
|
148
|
+
table=t,
|
|
149
|
+
key=k,
|
|
150
|
+
declared_value=v,
|
|
151
|
+
):
|
|
152
|
+
assert v is not None
|
|
153
|
+
return PlanItem(drift, SetStorageParameterFix(t, k, v))
|
|
154
|
+
case StorageParameterDrift(kind=DriftKind.UNDECLARED, table=t, key=k):
|
|
155
|
+
return PlanItem(drift, ResetStorageParameterFix(t, k))
|
|
143
156
|
case _:
|
|
144
157
|
raise ValueError(f"Unhandled drift: {drift}")
|
|
145
158
|
|