plain.postgres 0.117.0__tar.gz → 0.118.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.117.0 → plain_postgres-0.118.0}/PKG-INFO +62 -37
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/CHANGELOG.md +27 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/README.md +61 -36
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +1 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/decorators.py +9 -2
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/migrations.py +243 -249
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/sync.py +28 -3
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/autodetector.py +173 -101
- plain_postgres-0.118.0/plain/postgres/migrations/baselines.py +124 -0
- plain_postgres-0.118.0/plain/postgres/migrations/exceptions.py +118 -0
- plain_postgres-0.118.0/plain/postgres/migrations/executor.py +261 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/graph.py +1 -85
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/loader.py +132 -83
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/migration.py +12 -6
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/operations/base.py +6 -6
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/operations/special.py +8 -4
- plain_postgres-0.118.0/plain/postgres/migrations/reset.py +319 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/writer.py +26 -29
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/options.py +11 -5
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/preflight/database.py +36 -55
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/preflight/models.py +1 -1
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sql/compiler.py +5 -7
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/pyproject.toml +1 -1
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0002_test_field_removed.py +2 -1
- plain_postgres-0.118.0/tests/internal/conftest.py +55 -0
- plain_postgres-0.118.0/tests/internal/test_baselines.py +749 -0
- plain_postgres-0.118.0/tests/internal/test_migrations_reset.py +472 -0
- plain_postgres-0.118.0/tests/internal/test_replaces_removed.py +98 -0
- plain_postgres-0.118.0/tests/internal/test_shipped_baselines.py +51 -0
- plain_postgres-0.118.0/tests/internal/test_writer_operation_options.py +28 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_databases.py +8 -2
- plain_postgres-0.117.0/plain/postgres/migrations/exceptions.py +0 -51
- plain_postgres-0.117.0/plain/postgres/migrations/executor.py +0 -183
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/.gitignore +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/CLAUDE.md +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/LICENSE +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/README.md +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/adapters.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/base.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/connection.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/constraints.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/convergence/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/convergence/analysis.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/convergence/corrections.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/convergence/planning.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/databases.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/dialect.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/base.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/binary.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/boolean.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/duration.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/encrypted.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/network.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/numeric.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/primary_key.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/related.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/related_managers.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/temporal.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/text.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/timezones.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/uuid.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/forms.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/indexes.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/context.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/helpers.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/ownership.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/runner.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/types.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/schema.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/meta.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/middleware.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/state.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/preflight/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/preflight/indexes.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/query.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/schema.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/schema_lock.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sources.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sql/query.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/test/database.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/types.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/__init__.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/constraints.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/defaults.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/delete.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/encrypted.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/forms.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/indexes.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/iteration.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/mixins.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/nullability.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/querysets.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/relationships.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/storage_parameters.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/trees.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/urls.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/conftest.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_apply_replan.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_autodetector_not_null_errors.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_autodetector_type_change.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_connection_isolation.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_connection_pool.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_connection_self_heal.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_constraint_violation_error.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_constraints.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_defaults.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_fk.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_indexes.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_nullability.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_timeouts.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_databases_not_on_runtime_path.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_db_expression_defaults.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_diagnose.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_executor_connection_hook.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_fk_characterization.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_health.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_introspection.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_literal_default_persistence.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_management_connection.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_migration_executor.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_otel_metrics.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_rollback_exc_attribution.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_schema_lock.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_schema_timeouts.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_unresolved_relation_refs.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_create_update.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_database_url.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_deferred_loading.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_delete_behaviors.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_encrypted_fields.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_exceptions.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_field_defaults.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_functions_uuid.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_integrity_error_mapping.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_iterator.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_m2m.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_manager_assignment.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_manual_pk.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_mixins.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_modelform_roundtrip.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_only_empty_defaults.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_order_by_expressions.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_queryset_ordered.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_queryset_repr.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_queryset_slicing.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_random_string_field.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_raw_query.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_read_only_transactions.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_related.py +0 -0
- {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_related_instance_filter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: plain.postgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.118.0
|
|
4
4
|
Summary: Model your data and store it in a database.
|
|
5
5
|
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -144,7 +144,7 @@ PLAIN_POSTGRES_MANAGEMENT_URL=postgresql://app@postgres:5432/myapp
|
|
|
144
144
|
|
|
145
145
|
When `POSTGRES_MANAGEMENT_URL` is set, these commands connect through it instead of `POSTGRES_URL`:
|
|
146
146
|
|
|
147
|
-
- `plain migrations create`, `plain migrations apply`, `plain migrations list`, `plain migrations prune
|
|
147
|
+
- `plain migrations create`, `plain migrations apply`, `plain migrations list`, `plain migrations prune`
|
|
148
148
|
- `plain postgres sync`, `plain postgres converge`, `plain postgres schema`
|
|
149
149
|
- `plain postgres diagnose`, `plain postgres drop-unknown-tables`, `plain postgres shell`
|
|
150
150
|
|
|
@@ -603,15 +603,16 @@ Key flags:
|
|
|
603
603
|
|
|
604
604
|
Shared commands (apply equally to structural and data migrations):
|
|
605
605
|
|
|
606
|
-
| Command
|
|
607
|
-
|
|
|
608
|
-
| `plain migrations apply`
|
|
609
|
-
| `plain migrations apply --plan`
|
|
610
|
-
| `plain migrations apply --check`
|
|
611
|
-
| `plain migrations apply --fake`
|
|
612
|
-
| `plain migrations list`
|
|
613
|
-
| `plain migrations
|
|
614
|
-
| `plain migrations prune
|
|
606
|
+
| Command | Purpose |
|
|
607
|
+
| ---------------------------------- | ----------------------------------------------------------------- |
|
|
608
|
+
| `plain migrations apply` | Apply pending migrations |
|
|
609
|
+
| `plain migrations apply --plan` | Preview what would run |
|
|
610
|
+
| `plain migrations apply --check` | Exit non-zero if unapplied migrations exist (for CI) |
|
|
611
|
+
| `plain migrations apply --fake` | Mark as applied without running SQL |
|
|
612
|
+
| `plain migrations list` | View migration status by package |
|
|
613
|
+
| `plain migrations prune` | Remove orphan migration records |
|
|
614
|
+
| `plain migrations prune <package>` | Remove every record for one package (lets its baseline run again) |
|
|
615
|
+
| `plain migrations reset <package>` | Replace the package's history with one baseline |
|
|
615
616
|
|
|
616
617
|
#### Development workflow
|
|
617
618
|
|
|
@@ -626,43 +627,65 @@ Use this when migrations exist only in your local dev environment and haven't be
|
|
|
626
627
|
3. `plain migrations create` — creates a single fresh migration with all the changes
|
|
627
628
|
4. `plain migrations apply --fake` — marks the new migration as applied (the schema is already correct from the old migrations)
|
|
628
629
|
|
|
629
|
-
|
|
630
|
+
Migrations that are committed but not yet deployed anywhere can be consolidated the same way if every developer resets their database; production then applies the consolidated migration normally. Once a migration has reached any deployed environment, use a full reset (below).
|
|
630
631
|
|
|
631
|
-
|
|
632
|
+
#### Baselines
|
|
632
633
|
|
|
633
|
-
|
|
634
|
+
A package whose migration history has been reset ships one **baseline** migration in place of the deleted files. It is an ordinary migration with three extra attributes:
|
|
634
635
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
636
|
+
```python
|
|
637
|
+
class Migration(migrations.Migration):
|
|
638
|
+
supersedes = "0008_add_widgets" # the sentinel: the last deleted migration; its record proves a database is caught up
|
|
639
|
+
retired = (
|
|
640
|
+
"0001_initial",
|
|
641
|
+
...,
|
|
642
|
+
"0008_add_widgets",
|
|
643
|
+
) # every deleted name, so dependencies on them still resolve
|
|
644
|
+
shipped_in = "0.61" # the release that shipped the reset; empty until it has
|
|
645
|
+
dependencies = (("users", "0001_initial"),)
|
|
646
|
+
operations = (migrations.CreateModel(...), ...)
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
What `plain postgres sync` (or `plain migrations apply`) does with it depends only on what the database has recorded:
|
|
650
|
+
|
|
651
|
+
| Database has | Result |
|
|
652
|
+
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
653
|
+
| No records for the package, no tables (fresh, or newly installed) | Runs the baseline like any migration |
|
|
654
|
+
| No records for the package, but its tables exist | Refuses: record it with `apply <pkg> <baseline> --fake` if the schema is current, else drop the tables |
|
|
655
|
+
| The baseline recorded | Nothing |
|
|
656
|
+
| The `supersedes` migration recorded | Records the baseline without running it - one row, once |
|
|
657
|
+
| Records, but not the `supersedes` migration | Refuses: upgrade through the last release that still has that migration |
|
|
658
|
+
| Records, but none of the package's tables | Refuses: `plain migrations prune <pkg>` drops the records; then it runs |
|
|
659
|
+
|
|
660
|
+
`plain migrations apply --plan --check` and `plain postgres sync --check` report a baseline waiting to be recorded as a pending change, and `plain preflight` reports a refusal as an error. Nothing ever deletes records on its own; `prune` stays explicit. A database where the baseline _ran_ (rather than was recorded) cannot roll back to code from before the reset.
|
|
642
661
|
|
|
643
662
|
#### Resetting migrations
|
|
644
663
|
|
|
645
|
-
|
|
664
|
+
Once **every environment** has applied a package's migrations, its history can be collapsed into one baseline:
|
|
646
665
|
|
|
647
|
-
|
|
666
|
+
```bash
|
|
667
|
+
plain migrations reset <package>
|
|
668
|
+
```
|
|
648
669
|
|
|
649
|
-
|
|
650
|
-
- The first migration is named `0001_initial` (the default). If it has a different name, this workflow won't work cleanly.
|
|
670
|
+
The command writes `NNNN_baseline.py` past the current leaf (the package's newest migration) — the package's schema as `CreateModel`s, `supersedes` set to the leaf, `retired` set to every deleted name — and deletes the old files. Commit the new file and the deletions together. Existing databases adopt it with one record on their next `plain postgres sync`; the retired records stay as the rollback path; a fresh database runs it. Other packages' migrations that depended on a deleted name resolve to the baseline; nothing there needs rewriting. Do not add a `prune` step. Before it touches anything it checks, in this order:
|
|
651
671
|
|
|
652
|
-
|
|
672
|
+
- The models agree with the history (`plain migrations create` would write nothing). A change the history doesn't hold would be folded into the baseline, and databases that adopt it would never run it.
|
|
673
|
+
- The package ends in a single leaf, and no earlier baseline is waiting unreleased (see second resets below).
|
|
674
|
+
- The deleted history holds nothing a fresh database would miss. `RunPython`, `RunSQL`, any custom operation, and anything on the database side of `SeparateDatabaseAndState` are listed and refused until each carries `skip_on_reset=True` — "a fresh database can do without this." If it can't (a seed, an extension), move the effect somewhere a fresh database does get it, then mark it.
|
|
675
|
+
- The migrations directory is committed — tracked, unmodified, inside a git repository (so an installed package in site-packages cannot be reset; the check applies to `--dry-run` too). The leaf becomes the baseline's sentinel, so it cannot be something you created a minute ago. No check can prove every environment applied it — the command prints that obligation, and a database that hasn't is refused until it does. If anything goes wrong the output has already printed the one line that puts it back: `git checkout -- <migrations dir> && rm <the new baseline>`.
|
|
676
|
+
- The result loads: dependencies on retired names resolve, the graph has no cycle, the baseline reproduces the models exactly.
|
|
677
|
+
- Nothing the baseline needs is defined inside a migration file. A custom field class or callable written in the history disappears with it; move it into the app first.
|
|
653
678
|
|
|
654
|
-
|
|
655
|
-
2. Delete every file in the package's `migrations/` directory except `__init__.py`.
|
|
656
|
-
3. Run `plain migrations create` to generate a fresh `0001_initial`.
|
|
657
|
-
4. Run `plain migrations prune --yes` to remove stale DB records. The existing `0001_initial` record matches the new file, so the database is immediately up to date.
|
|
658
|
-
5. Verify with `plain postgres schema` (zero issues means the reset is clean) and `plain migrations create --check` (no pending changes).
|
|
659
|
-
6. Commit and deploy. On every other environment, run `plain migrations prune --yes`. No actual SQL runs — it only cleans up migration history records. If `migrations prune` is already in your deploy steps, no changes are needed.
|
|
679
|
+
Options:
|
|
660
680
|
|
|
661
|
-
|
|
681
|
+
- `--shipped-in <version>` — the release this reset ships in, named in the refusal a database that missed the leaf gets, and required before the package can be reset again. Plain's own packages leave it empty and fill it in at release time (a repository test enforces it, and the package's `plain.postgres` minimum is raised to the first release that understands baselines); an app should pass the version or deploy this ships in.
|
|
682
|
+
- `--dry-run` — print the baseline and the deletion list, write nothing.
|
|
662
683
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
684
|
+
**Dependencies.** The baseline depends on each other package its models reference, at the earliest migration of that package where the referenced models exist — and on nothing else. A package that another package pinned early _and_ whose models now point back at that package cannot get a single root: the graph would be a cycle, and the command refuses with it. That is a limit of migration graphs, not of the command; nothing to do with a second package fixes it.
|
|
685
|
+
|
|
686
|
+
**Support boundaries.** Adoption checks that the sentinel is recorded and the tables exist, not columns; editing history behind a released leaf is outside what any check can catch. A data migration in _another_ package that read this package's historical state (a field the baseline no longer has) keeps loading but can fail on a fresh database; the command cannot see it.
|
|
687
|
+
|
|
688
|
+
**Second reset.** Run it again later and the previous baseline joins `retired`. It refuses while the previous baseline's `shipped_in` is empty: nothing shipped it yet, so superseding its name would strand every database still at the original sentinel — once that baseline has shipped everywhere, set its `shipped_in` to the version that shipped it and reset again; otherwise restore the history and reset once.
|
|
666
689
|
|
|
667
690
|
### Data migrations
|
|
668
691
|
|
|
@@ -685,7 +708,9 @@ def forwards(models, schema_editor):
|
|
|
685
708
|
|
|
686
709
|
For large tables, chunk the work (e.g. by ID range) and commit between batches so no single transaction holds locks for too long.
|
|
687
710
|
|
|
688
|
-
|
|
711
|
+
When the package's history is later collapsed (see [Resetting migrations](#resetting-migrations)), `plain migrations reset` refuses while any `RunPython`/`RunSQL` is unmarked. Pass `skip_on_reset=True` once a fresh database can do without its effect; if it can't, move the effect into a seed first.
|
|
712
|
+
|
|
713
|
+
See [Structural migrations](#structural-migrations) for shared commands (`apply`, `list`, `prune`).
|
|
689
714
|
|
|
690
715
|
#### Cascading deletes inside data migrations
|
|
691
716
|
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.118.0](https://github.com/dropseed/plain/releases/plain-postgres@0.118.0) (2026-09-18)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Added baseline migrations. A package whose migration history has been collapsed ships one baseline in place of the deleted files: an ordinary migration with `supersedes` (the last deleted migration, whose record proves a database is caught up), `retired` (every deleted name, so other packages' dependencies on them still resolve), and `shipped_in` (the release that shipped the reset). `plain postgres sync` and `plain migrations apply` decide what to do from the database's records alone — a fresh database runs the baseline, a database that recorded the superseded migration adopts it with a single record write and no DDL, and a database from before the reset, or one whose tables are missing or unrecorded, is refused with a one-line remedy. Nothing deletes records on its own ([b89b33acae](https://github.com/dropseed/plain/commit/b89b33acae), [4eb466fa46](https://github.com/dropseed/plain/commit/4eb466fa46))
|
|
8
|
+
- Added `plain migrations reset <package>`, which writes the baseline (the package's current schema as `CreateModel`s, named past the current leaf) and deletes the old migration files. Before touching anything it checks that the models agree with the history, that the package has a single leaf, that the deleted history holds nothing a fresh database would miss, that the migrations directory is committed and unmodified in git, and that the result loads. `--dry-run` prints the baseline and the deletion list; `--shipped-in <version>` records the release the reset ships in, which is named in the refusal a stale database gets and is required before the package can be reset a second time ([7c6045b1b9](https://github.com/dropseed/plain/commit/7c6045b1b9), [4eb466fa46](https://github.com/dropseed/plain/commit/4eb466fa46))
|
|
9
|
+
- Removed `Migration.replaces` and the `plain migrations squash` command. A migration file that still declares `replaces` now fails to load with a `BadMigrationError` that lists the steps to convert it back to ordinary records ([e154abc239](https://github.com/dropseed/plain/commit/e154abc239))
|
|
10
|
+
- Renamed `Operation.elidable` to `skip_on_reset`; `RunPython` and `RunSQL` take it as `skip_on_reset=True`, and it is serialized into migration files when set. It means "a fresh database can do without this operation's effect" — `plain migrations reset` refuses while any `RunPython`, `RunSQL`, custom operation, or database-side `SeparateDatabaseAndState` in the deleted history is unmarked. The optimizer no longer elides operations across it ([7c6045b1b9](https://github.com/dropseed/plain/commit/7c6045b1b9))
|
|
11
|
+
- `plain migrations prune` takes an optional `<package>` argument that removes every record for that package, which is how you let its baseline run again once the tables are gone. Without an argument it removes orphan records — recorded migrations with no file on disk that no baseline retired — and keeps retired records as the rollback path across a reset. "Stale" became "orphan" in its output ([b89b33acae](https://github.com/dropseed/plain/commit/b89b33acae))
|
|
12
|
+
- `plain migrations list` annotates a baseline waiting to be recorded with `(baseline: will be recorded, not run)` and prints the planner's refusal for a package as a red `!` line. The `[-]` "partially recorded" marker for squashed migrations is gone with `replaces` ([b89b33acae](https://github.com/dropseed/plain/commit/b89b33acae))
|
|
13
|
+
- `plain migrations apply --plan` and `plain postgres sync` report baselines being recorded separately from migrations being run ("Applying 2 migration(s), recording 1 baseline(s)"). `plain migrations apply <package> <migration> --fake` skips the consistent-history check — faking a named migration is how that history gets repaired — and can record a baseline by hand on a database whose tables already exist ([b89b33acae](https://github.com/dropseed/plain/commit/b89b33acae), [4eb466fa46](https://github.com/dropseed/plain/commit/4eb466fa46))
|
|
14
|
+
- Preflight: `postgres.prunable_migrations` now reports orphan records as a warning without telling you to prune them (they are what rollback needs, and a branch switch or a reset leaves them behind on purpose), and the new `postgres.migration_history` check reports as an error any database the planner would refuse, so a deploy finds out before the release phase ([b89b33acae](https://github.com/dropseed/plain/commit/b89b33acae))
|
|
15
|
+
- Added `MigrationsError` as the base class for `AmbiguityError`, `BadMigrationError`, `CircularDependencyError`, `InconsistentMigrationHistory`, `NodeNotFoundError`, and the new `MigrationHistoryError` family (`ResetBoundaryError`, `StaleMigrationRecordsError`, `UnrecordedTablesError`), so a caller that only reports can catch one thing. A dependency on an unknown or unmigrated package raises `BadMigrationError` instead of `ValueError`. Database management commands print a `MigrationHistoryError` as one clean line instead of a traceback ([b89b33acae](https://github.com/dropseed/plain/commit/b89b33acae), [4eb466fa46](https://github.com/dropseed/plain/commit/4eb466fa46))
|
|
16
|
+
- `MigrationExecutor` gained `split_plan()`, which returns a `PendingMigrations(run=, record=)` for a plan, a `record_only` property, and a `repair_baseline` argument on `migration_plan()`; `record_migration()` and `check_replacements()` were removed and a migration's record is now written inside the schema editor's transaction. `MigrationLoader` lost the `replace_migrations` argument and `replacements` attribute and gained `baselines`, `baseline_status`, `retired_to_baseline`, `resolved_dependencies()`, `orphan_records()`, and `with_migrations()`. `MigrationGraph.remove_replaced_nodes()` and `remove_replacement_node()` are gone ([e154abc239](https://github.com/dropseed/plain/commit/e154abc239), [b89b33acae](https://github.com/dropseed/plain/commit/b89b33acae), [4eb466fa46](https://github.com/dropseed/plain/commit/4eb466fa46))
|
|
17
|
+
- The migration writer serializes an operation from what its `deconstruct()` returns — positional arguments as given, then every keyword argument in order — instead of inspecting the operation's `__init__` signature. A custom operation whose `deconstruct()` returns a keyword its `__init__` doesn't name positionally is now written faithfully ([4eb466fa46](https://github.com/dropseed/plain/commit/4eb466fa46))
|
|
18
|
+
- `MigrationAutodetector.arrange_for_graph()` and `deep_deconstruct()` are now module-level functions in `plain.postgres.migrations.autodetector` (`arrange_for_graph` takes `questioner=` as a keyword), alongside the new `detect_model_changes()`, `describe_changes()`, and `package_creation_operations()`. `parse_number()` no longer special-cases `_squashed_` names ([7c6045b1b9](https://github.com/dropseed/plain/commit/7c6045b1b9), [4eb466fa46](https://github.com/dropseed/plain/commit/4eb466fa46))
|
|
19
|
+
- Added `plain.postgres.options.default_db_table()` and `CONVERGENCE_OPTIONS` ([b89b33acae](https://github.com/dropseed/plain/commit/b89b33acae))
|
|
20
|
+
|
|
21
|
+
### Upgrade instructions
|
|
22
|
+
|
|
23
|
+
- If any migration file declares `replaces`, the loader refuses to start and prints the conversion steps: delete the `replaces = ...` line, delete any replaced migration still on disk and repoint dependencies that named it to the replacing migration, run `plain migrations apply <package> <migration> --fake` on every database that had applied all of the replaced migrations, then `plain migrations prune` on each. Do this on staging and production before their next deploy.
|
|
24
|
+
- Replace `elidable=True` with `skip_on_reset=True` on `RunPython` and `RunSQL`, and rename the `elidable` class attribute to `skip_on_reset` on custom operations.
|
|
25
|
+
- Replace `plain migrations squash` in any docs or scripts with `plain migrations reset <package>`. Read the "Resetting migrations" section of the postgres docs first — it deletes files, and every environment must have applied the package's current leaf before the reset ships.
|
|
26
|
+
- Deploy steps that ran `plain migrations prune` after a reset can drop it: records a baseline retired stay on purpose, and `prune` now leaves them alone.
|
|
27
|
+
- If you subclassed or called migration internals: `MigrationLoader(replace_migrations=...)` and `loader.replacements` no longer exist, `MigrationExecutor.record_migration()` and `check_replacements()` are gone, and `MigrationAutodetector.arrange_for_graph()` / `.deep_deconstruct()` are now module-level imports from `plain.postgres.migrations.autodetector`.
|
|
28
|
+
- If you caught `ValueError` from the loader for a dependency on an unknown package, catch `BadMigrationError` (or `MigrationsError`).
|
|
29
|
+
|
|
3
30
|
## [0.117.0](https://github.com/dropseed/plain/releases/plain-postgres@0.117.0) (2026-09-04)
|
|
4
31
|
|
|
5
32
|
### What's changed
|
|
@@ -131,7 +131,7 @@ PLAIN_POSTGRES_MANAGEMENT_URL=postgresql://app@postgres:5432/myapp
|
|
|
131
131
|
|
|
132
132
|
When `POSTGRES_MANAGEMENT_URL` is set, these commands connect through it instead of `POSTGRES_URL`:
|
|
133
133
|
|
|
134
|
-
- `plain migrations create`, `plain migrations apply`, `plain migrations list`, `plain migrations prune
|
|
134
|
+
- `plain migrations create`, `plain migrations apply`, `plain migrations list`, `plain migrations prune`
|
|
135
135
|
- `plain postgres sync`, `plain postgres converge`, `plain postgres schema`
|
|
136
136
|
- `plain postgres diagnose`, `plain postgres drop-unknown-tables`, `plain postgres shell`
|
|
137
137
|
|
|
@@ -590,15 +590,16 @@ Key flags:
|
|
|
590
590
|
|
|
591
591
|
Shared commands (apply equally to structural and data migrations):
|
|
592
592
|
|
|
593
|
-
| Command
|
|
594
|
-
|
|
|
595
|
-
| `plain migrations apply`
|
|
596
|
-
| `plain migrations apply --plan`
|
|
597
|
-
| `plain migrations apply --check`
|
|
598
|
-
| `plain migrations apply --fake`
|
|
599
|
-
| `plain migrations list`
|
|
600
|
-
| `plain migrations
|
|
601
|
-
| `plain migrations prune
|
|
593
|
+
| Command | Purpose |
|
|
594
|
+
| ---------------------------------- | ----------------------------------------------------------------- |
|
|
595
|
+
| `plain migrations apply` | Apply pending migrations |
|
|
596
|
+
| `plain migrations apply --plan` | Preview what would run |
|
|
597
|
+
| `plain migrations apply --check` | Exit non-zero if unapplied migrations exist (for CI) |
|
|
598
|
+
| `plain migrations apply --fake` | Mark as applied without running SQL |
|
|
599
|
+
| `plain migrations list` | View migration status by package |
|
|
600
|
+
| `plain migrations prune` | Remove orphan migration records |
|
|
601
|
+
| `plain migrations prune <package>` | Remove every record for one package (lets its baseline run again) |
|
|
602
|
+
| `plain migrations reset <package>` | Replace the package's history with one baseline |
|
|
602
603
|
|
|
603
604
|
#### Development workflow
|
|
604
605
|
|
|
@@ -613,43 +614,65 @@ Use this when migrations exist only in your local dev environment and haven't be
|
|
|
613
614
|
3. `plain migrations create` — creates a single fresh migration with all the changes
|
|
614
615
|
4. `plain migrations apply --fake` — marks the new migration as applied (the schema is already correct from the old migrations)
|
|
615
616
|
|
|
616
|
-
|
|
617
|
+
Migrations that are committed but not yet deployed anywhere can be consolidated the same way if every developer resets their database; production then applies the consolidated migration normally. Once a migration has reached any deployed environment, use a full reset (below).
|
|
617
618
|
|
|
618
|
-
|
|
619
|
+
#### Baselines
|
|
619
620
|
|
|
620
|
-
|
|
621
|
+
A package whose migration history has been reset ships one **baseline** migration in place of the deleted files. It is an ordinary migration with three extra attributes:
|
|
621
622
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
623
|
+
```python
|
|
624
|
+
class Migration(migrations.Migration):
|
|
625
|
+
supersedes = "0008_add_widgets" # the sentinel: the last deleted migration; its record proves a database is caught up
|
|
626
|
+
retired = (
|
|
627
|
+
"0001_initial",
|
|
628
|
+
...,
|
|
629
|
+
"0008_add_widgets",
|
|
630
|
+
) # every deleted name, so dependencies on them still resolve
|
|
631
|
+
shipped_in = "0.61" # the release that shipped the reset; empty until it has
|
|
632
|
+
dependencies = (("users", "0001_initial"),)
|
|
633
|
+
operations = (migrations.CreateModel(...), ...)
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
What `plain postgres sync` (or `plain migrations apply`) does with it depends only on what the database has recorded:
|
|
637
|
+
|
|
638
|
+
| Database has | Result |
|
|
639
|
+
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
640
|
+
| No records for the package, no tables (fresh, or newly installed) | Runs the baseline like any migration |
|
|
641
|
+
| No records for the package, but its tables exist | Refuses: record it with `apply <pkg> <baseline> --fake` if the schema is current, else drop the tables |
|
|
642
|
+
| The baseline recorded | Nothing |
|
|
643
|
+
| The `supersedes` migration recorded | Records the baseline without running it - one row, once |
|
|
644
|
+
| Records, but not the `supersedes` migration | Refuses: upgrade through the last release that still has that migration |
|
|
645
|
+
| Records, but none of the package's tables | Refuses: `plain migrations prune <pkg>` drops the records; then it runs |
|
|
646
|
+
|
|
647
|
+
`plain migrations apply --plan --check` and `plain postgres sync --check` report a baseline waiting to be recorded as a pending change, and `plain preflight` reports a refusal as an error. Nothing ever deletes records on its own; `prune` stays explicit. A database where the baseline _ran_ (rather than was recorded) cannot roll back to code from before the reset.
|
|
629
648
|
|
|
630
649
|
#### Resetting migrations
|
|
631
650
|
|
|
632
|
-
|
|
651
|
+
Once **every environment** has applied a package's migrations, its history can be collapsed into one baseline:
|
|
633
652
|
|
|
634
|
-
|
|
653
|
+
```bash
|
|
654
|
+
plain migrations reset <package>
|
|
655
|
+
```
|
|
635
656
|
|
|
636
|
-
|
|
637
|
-
- The first migration is named `0001_initial` (the default). If it has a different name, this workflow won't work cleanly.
|
|
657
|
+
The command writes `NNNN_baseline.py` past the current leaf (the package's newest migration) — the package's schema as `CreateModel`s, `supersedes` set to the leaf, `retired` set to every deleted name — and deletes the old files. Commit the new file and the deletions together. Existing databases adopt it with one record on their next `plain postgres sync`; the retired records stay as the rollback path; a fresh database runs it. Other packages' migrations that depended on a deleted name resolve to the baseline; nothing there needs rewriting. Do not add a `prune` step. Before it touches anything it checks, in this order:
|
|
638
658
|
|
|
639
|
-
|
|
659
|
+
- The models agree with the history (`plain migrations create` would write nothing). A change the history doesn't hold would be folded into the baseline, and databases that adopt it would never run it.
|
|
660
|
+
- The package ends in a single leaf, and no earlier baseline is waiting unreleased (see second resets below).
|
|
661
|
+
- The deleted history holds nothing a fresh database would miss. `RunPython`, `RunSQL`, any custom operation, and anything on the database side of `SeparateDatabaseAndState` are listed and refused until each carries `skip_on_reset=True` — "a fresh database can do without this." If it can't (a seed, an extension), move the effect somewhere a fresh database does get it, then mark it.
|
|
662
|
+
- The migrations directory is committed — tracked, unmodified, inside a git repository (so an installed package in site-packages cannot be reset; the check applies to `--dry-run` too). The leaf becomes the baseline's sentinel, so it cannot be something you created a minute ago. No check can prove every environment applied it — the command prints that obligation, and a database that hasn't is refused until it does. If anything goes wrong the output has already printed the one line that puts it back: `git checkout -- <migrations dir> && rm <the new baseline>`.
|
|
663
|
+
- The result loads: dependencies on retired names resolve, the graph has no cycle, the baseline reproduces the models exactly.
|
|
664
|
+
- Nothing the baseline needs is defined inside a migration file. A custom field class or callable written in the history disappears with it; move it into the app first.
|
|
640
665
|
|
|
641
|
-
|
|
642
|
-
2. Delete every file in the package's `migrations/` directory except `__init__.py`.
|
|
643
|
-
3. Run `plain migrations create` to generate a fresh `0001_initial`.
|
|
644
|
-
4. Run `plain migrations prune --yes` to remove stale DB records. The existing `0001_initial` record matches the new file, so the database is immediately up to date.
|
|
645
|
-
5. Verify with `plain postgres schema` (zero issues means the reset is clean) and `plain migrations create --check` (no pending changes).
|
|
646
|
-
6. Commit and deploy. On every other environment, run `plain migrations prune --yes`. No actual SQL runs — it only cleans up migration history records. If `migrations prune` is already in your deploy steps, no changes are needed.
|
|
666
|
+
Options:
|
|
647
667
|
|
|
648
|
-
|
|
668
|
+
- `--shipped-in <version>` — the release this reset ships in, named in the refusal a database that missed the leaf gets, and required before the package can be reset again. Plain's own packages leave it empty and fill it in at release time (a repository test enforces it, and the package's `plain.postgres` minimum is raised to the first release that understands baselines); an app should pass the version or deploy this ships in.
|
|
669
|
+
- `--dry-run` — print the baseline and the deletion list, write nothing.
|
|
649
670
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
671
|
+
**Dependencies.** The baseline depends on each other package its models reference, at the earliest migration of that package where the referenced models exist — and on nothing else. A package that another package pinned early _and_ whose models now point back at that package cannot get a single root: the graph would be a cycle, and the command refuses with it. That is a limit of migration graphs, not of the command; nothing to do with a second package fixes it.
|
|
672
|
+
|
|
673
|
+
**Support boundaries.** Adoption checks that the sentinel is recorded and the tables exist, not columns; editing history behind a released leaf is outside what any check can catch. A data migration in _another_ package that read this package's historical state (a field the baseline no longer has) keeps loading but can fail on a fresh database; the command cannot see it.
|
|
674
|
+
|
|
675
|
+
**Second reset.** Run it again later and the previous baseline joins `retired`. It refuses while the previous baseline's `shipped_in` is empty: nothing shipped it yet, so superseding its name would strand every database still at the original sentinel — once that baseline has shipped everywhere, set its `shipped_in` to the version that shipped it and reset again; otherwise restore the history and reset once.
|
|
653
676
|
|
|
654
677
|
### Data migrations
|
|
655
678
|
|
|
@@ -672,7 +695,9 @@ def forwards(models, schema_editor):
|
|
|
672
695
|
|
|
673
696
|
For large tables, chunk the work (e.g. by ID range) and commit between batches so no single transaction holds locks for too long.
|
|
674
697
|
|
|
675
|
-
|
|
698
|
+
When the package's history is later collapsed (see [Resetting migrations](#resetting-migrations)), `plain migrations reset` refuses while any `RunPython`/`RunSQL` is unmarked. Pass `skip_on_reset=True` once a fresh database can do without its effect; if it can't, move the effect into a seed first.
|
|
699
|
+
|
|
700
|
+
See [Structural migrations](#structural-migrations) for shared commands (`apply`, `list`, `prune`).
|
|
676
701
|
|
|
677
702
|
#### Cascading deletes inside data migrations
|
|
678
703
|
|
|
@@ -99,3 +99,4 @@ Run `uv run plain docs postgres` for check details, thresholds, and production u
|
|
|
99
99
|
- Use `model_options = postgres.Options(...)` not `class Meta`
|
|
100
100
|
- Never format raw SQL strings — always use parameterized queries
|
|
101
101
|
- Migrations are forward-only — no reverse migrations. `RunPython` takes a single callable (no `reverse_code` or `noop`). The callable signature is `fn(models, schema_editor)`, not `fn(apps, schema_editor)`
|
|
102
|
+
- No `squashmigrations`; collapse a package's history with `plain migrations reset <package>`. `RunPython`/`RunSQL` take `skip_on_reset=True` (Django's `elidable`)
|
|
@@ -8,6 +8,7 @@ from typing import Any
|
|
|
8
8
|
import click
|
|
9
9
|
|
|
10
10
|
from ..db import use_management_connection
|
|
11
|
+
from ..migrations.exceptions import MigrationHistoryError
|
|
11
12
|
from ..schema_lock import SchemaLockLost, SchemaLockTimeout, schema_lock
|
|
12
13
|
|
|
13
14
|
|
|
@@ -30,11 +31,17 @@ def database_management_command[F: Callable[..., Any]](f: F) -> F:
|
|
|
30
31
|
database management operations. Inside the command, `get_connection()`
|
|
31
32
|
returns a connection opened against `POSTGRES_MANAGEMENT_URL` (falling
|
|
32
33
|
back to `POSTGRES_URL` when unset).
|
|
34
|
+
|
|
35
|
+
A planner refusal (`MigrationHistoryError`) surfaces as one clean line
|
|
36
|
+
instead of a traceback, the same way `cli_schema_lock` treats lock errors.
|
|
33
37
|
"""
|
|
34
38
|
|
|
35
39
|
@functools.wraps(f)
|
|
36
40
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
try:
|
|
42
|
+
with use_management_connection():
|
|
43
|
+
return f(*args, **kwargs)
|
|
44
|
+
except MigrationHistoryError as e:
|
|
45
|
+
raise click.ClickException(str(e)) from e
|
|
39
46
|
|
|
40
47
|
return wrapper # ty: ignore[invalid-return-type]
|