plain.postgres 0.90.0__tar.gz → 0.91.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.90.0 → plain_postgres-0.91.0}/PKG-INFO +10 -14
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/CHANGELOG.md +30 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/README.md +9 -13
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/__init__.py +0 -6
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +5 -4
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +2 -2
- plain_postgres-0.91.0/plain/postgres/cli/converge.py +99 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/cli/core.py +2 -2
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/cli/migrations.py +10 -39
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/cli/schema.py +54 -44
- plain_postgres-0.91.0/plain/postgres/cli/sync.py +173 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/connection.py +43 -8
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/constraints.py +59 -137
- plain_postgres-0.91.0/plain/postgres/convergence/__init__.py +61 -0
- plain_postgres-0.91.0/plain/postgres/convergence/analysis.py +807 -0
- plain_postgres-0.91.0/plain/postgres/convergence/fixes.py +243 -0
- plain_postgres-0.91.0/plain/postgres/convergence/planning.py +235 -0
- plain_postgres-0.91.0/plain/postgres/ddl.py +84 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/dialect.py +2 -1
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/__init__.py +0 -84
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/related.py +1 -16
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/forms.py +0 -3
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/indexes.py +35 -5
- plain_postgres-0.91.0/plain/postgres/introspection/__init__.py +35 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/introspection/health.py +3 -3
- plain_postgres-0.91.0/plain/postgres/introspection/schema.py +212 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/__init__.py +0 -12
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/autodetector.py +1 -178
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/loader.py +1 -1
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/operations/__init__.py +0 -10
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/operations/models.py +0 -327
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/state.py +0 -50
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/schema.py +18 -178
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/types.py +0 -6
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/types.pyi +0 -69
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/pyproject.toml +1 -1
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/migrations/0001_initial.py +0 -6
- plain_postgres-0.91.0/tests/test_convergence.py +1652 -0
- plain_postgres-0.91.0/tests/test_introspection.py +211 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_schema_normalize_type.py +0 -3
- plain_postgres-0.90.0/plain/postgres/backups/cli.py +0 -150
- plain_postgres-0.90.0/plain/postgres/backups/clients.py +0 -94
- plain_postgres-0.90.0/plain/postgres/backups/core.py +0 -172
- plain_postgres-0.90.0/plain/postgres/cli/converge.py +0 -80
- plain_postgres-0.90.0/plain/postgres/introspection/__init__.py +0 -33
- plain_postgres-0.90.0/plain/postgres/introspection/schema.py +0 -282
- plain_postgres-0.90.0/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/.gitignore +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/CLAUDE.md +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/LICENSE +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/README.md +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/base.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/connections.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/encrypted.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/related_managers.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/timezones.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/meta.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/preflight.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/query.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/sql/compiler.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/sql/query.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.90.0/plain/postgres/backups → plain_postgres-0.91.0/plain/postgres/test}/__init__.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/test/utils.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.90.0/plain/postgres/test → plain_postgres-0.91.0/tests/app/examples/migrations}/__init__.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/models.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_connection_isolation.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_database_url.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_delete_behaviors.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_encrypted_fields.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_exceptions.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_iterator.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_manager_assignment.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_models.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_read_only_transactions.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_related_descriptors.py +0 -0
- {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_related_manager_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plain.postgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.91.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
|
|
@@ -447,7 +447,7 @@ Migrations track changes to your models and update the database schema according
|
|
|
447
447
|
### Creating migrations
|
|
448
448
|
|
|
449
449
|
```bash
|
|
450
|
-
plain
|
|
450
|
+
plain migrations create
|
|
451
451
|
```
|
|
452
452
|
|
|
453
453
|
Key flags:
|
|
@@ -463,12 +463,11 @@ Only write migrations by hand if they are custom data migrations.
|
|
|
463
463
|
### Running migrations
|
|
464
464
|
|
|
465
465
|
```bash
|
|
466
|
-
plain
|
|
466
|
+
plain migrations apply
|
|
467
467
|
```
|
|
468
468
|
|
|
469
469
|
Key flags:
|
|
470
470
|
|
|
471
|
-
- `--backup` / `--no-backup` — Create a database backup before applying (default: on in DEBUG)
|
|
472
471
|
- `--plan` — Show what migrations would run without applying them
|
|
473
472
|
- `--check` — Exit non-zero if unapplied migrations exist (for CI)
|
|
474
473
|
- `--fake` — Mark migrations as applied without running them
|
|
@@ -479,7 +478,7 @@ Key flags:
|
|
|
479
478
|
plain migrations list
|
|
480
479
|
```
|
|
481
480
|
|
|
482
|
-
`
|
|
481
|
+
`migrations apply` has no `--list` or `--status` flag. Use `plain migrations list`.
|
|
483
482
|
|
|
484
483
|
- `--format plan` — Show in dependency order instead of grouped by package
|
|
485
484
|
|
|
@@ -493,8 +492,8 @@ Use this when migrations exist only in your local dev environment and haven't be
|
|
|
493
492
|
|
|
494
493
|
1. Delete the intermediate migration files (keep the initial 0001 and any previously committed migrations)
|
|
495
494
|
2. `plain migrations prune --yes` — removes stale DB records for the deleted files
|
|
496
|
-
3. `plain
|
|
497
|
-
4. `plain
|
|
495
|
+
3. `plain migrations create` — creates a single fresh migration with all the changes
|
|
496
|
+
4. `plain migrations apply --fake` — marks the new migration as applied (the schema is already correct from the old migrations)
|
|
498
497
|
|
|
499
498
|
**Consolidating committed migrations (squash):**
|
|
500
499
|
|
|
@@ -523,16 +522,16 @@ Over time a package can accumulate dozens of migrations. Once **every environmen
|
|
|
523
522
|
|
|
524
523
|
1. Run `plain migrations list` locally and verify everything is applied.
|
|
525
524
|
2. Delete every file in the package's `migrations/` directory except `__init__.py`.
|
|
526
|
-
3. Run `plain
|
|
525
|
+
3. Run `plain migrations create` to generate a fresh `0001_initial`.
|
|
527
526
|
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.
|
|
528
|
-
5. Verify with `plain postgres schema` (zero issues means the reset is clean) and `plain
|
|
527
|
+
5. Verify with `plain postgres schema` (zero issues means the reset is clean) and `plain migrations create --check` (no pending changes).
|
|
529
528
|
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.
|
|
530
529
|
|
|
531
530
|
**Things to keep in mind:**
|
|
532
531
|
|
|
533
532
|
- If resetting multiple packages, process depended-on packages first — the new `0001_initial` may have cross-package FK dependencies.
|
|
534
533
|
- Data migrations (`RunPython`) in the deleted history are gone, which is fine since they've already run everywhere.
|
|
535
|
-
- If CI runs `
|
|
534
|
+
- If CI runs `migrations create --check` or `migrations apply --check`, the reset PR must be merged and deployed before those checks pass in other branches.
|
|
536
535
|
|
|
537
536
|
### Other migration commands
|
|
538
537
|
|
|
@@ -578,9 +577,6 @@ class Product(postgres.Model):
|
|
|
578
577
|
- [`IntegerField`](./fields/__init__.py#IntegerField) - Integer
|
|
579
578
|
- [`BigIntegerField`](./fields/__init__.py#BigIntegerField) - Big (8 byte) integer
|
|
580
579
|
- [`SmallIntegerField`](./fields/__init__.py#SmallIntegerField) - Small integer
|
|
581
|
-
- [`PositiveIntegerField`](./fields/__init__.py#PositiveIntegerField) - Positive integer
|
|
582
|
-
- [`PositiveBigIntegerField`](./fields/__init__.py#PositiveBigIntegerField) - Positive big integer
|
|
583
|
-
- [`PositiveSmallIntegerField`](./fields/__init__.py#PositiveSmallIntegerField) - Positive small integer
|
|
584
580
|
- [`FloatField`](./fields/__init__.py#FloatField) - Floating point number
|
|
585
581
|
- [`DecimalField`](./fields/__init__.py#DecimalField) - Fixed precision decimal
|
|
586
582
|
|
|
@@ -1038,7 +1034,7 @@ See [`default_settings.py`](./default_settings.py) for more details.
|
|
|
1038
1034
|
|
|
1039
1035
|
#### How do I add a field to an existing model?
|
|
1040
1036
|
|
|
1041
|
-
Add the field to your model class, then run `plain
|
|
1037
|
+
Add the field to your model class, then run `plain migrations create` to create a migration. If the field is required (no default value and not nullable), you'll be prompted to provide a default value for existing rows.
|
|
1042
1038
|
|
|
1043
1039
|
#### How do I create a unique constraint on multiple fields?
|
|
1044
1040
|
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.91.0](https://github.com/dropseed/plain/releases/plain-postgres@0.91.0) (2026-03-29)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- **New `postgres sync` command** — the primary command for both development and deployment. In DEBUG mode it creates migrations, applies them, and converges. In production it applies migrations and converges. Use `--check` in CI to verify the database is fully synced. ([b026895edc4c](https://github.com/dropseed/plain/commit/b026895edc4c), [b348a5af0867](https://github.com/dropseed/plain/commit/b348a5af0867))
|
|
8
|
+
|
|
9
|
+
- **Indexes and constraints are now managed by convergence, not migrations.** The migration autodetector no longer generates `AddIndex`, `RemoveIndex`, `RenameIndex`, `AddConstraint`, or `RemoveConstraint` operations — these classes have been removed. Convergence (`postgres sync` or `postgres converge`) creates, renames, rebuilds, and validates indexes and constraints using safe strategies: `CREATE INDEX CONCURRENTLY`, `NOT VALID` + `VALIDATE CONSTRAINT` for check constraints, and `CONCURRENTLY` + `USING INDEX` for unique constraints. ([c58b4ba1fec9](https://github.com/dropseed/plain/commit/c58b4ba1fec9), [f6506d263f3f](https://github.com/dropseed/plain/commit/f6506d263f3f), [1f15538b008f](https://github.com/dropseed/plain/commit/1f15538b008f))
|
|
10
|
+
|
|
11
|
+
- **Command renames**: `makemigrations` → `migrations create`, `migrate` → `migrations apply`. The old top-level `makemigrations` and `migrate` shortcuts have been removed. ([adf021688bf3](https://github.com/dropseed/plain/commit/adf021688bf3))
|
|
12
|
+
|
|
13
|
+
- **Removed `--backup` flag from `migrations apply`** — database backups have moved to `plain-dev`. ([50773a50f674](https://github.com/dropseed/plain/commit/50773a50f674))
|
|
14
|
+
|
|
15
|
+
- **Removed `PositiveIntegerField`, `PositiveBigIntegerField`, and `PositiveSmallIntegerField`** — use `IntegerField`, `BigIntegerField`, or `SmallIntegerField` with a `CheckConstraint` if you need positivity enforcement. The `db_check` pipeline has also been removed. ([738a1efbca59](https://github.com/dropseed/plain/commit/738a1efbca59))
|
|
16
|
+
|
|
17
|
+
- **Convergence overhaul** — rewritten into analysis, planning, and execution layers. Now detects index/constraint renames, stale definitions, INVALID indexes, and NOT VALID constraints. Each fix is applied and committed independently so partial failures don't block subsequent fixes. The `--prune` flag has been renamed to `--drop-undeclared`, which distinguishes between indexes (non-blocking) and constraints (blocking) when undeclared objects remain. ([987791d345cb](https://github.com/dropseed/plain/commit/987791d345cb), [66ac1152be0d](https://github.com/dropseed/plain/commit/66ac1152be0d), [f2f46e1a6054](https://github.com/dropseed/plain/commit/f2f46e1a6054), [5bb1472acf0f](https://github.com/dropseed/plain/commit/5bb1472acf0f))
|
|
18
|
+
|
|
19
|
+
- Fixed test database names exceeding Postgres's 63-character identifier limit. ([4a8937ba2758](https://github.com/dropseed/plain/commit/4a8937ba2758))
|
|
20
|
+
|
|
21
|
+
### Upgrade instructions
|
|
22
|
+
|
|
23
|
+
1. **Replace `migrate` with `postgres sync` in deploy scripts and CI.** `postgres sync` applies migrations and runs convergence in a single step. For CI checks, use `postgres sync --check` instead of `migrate --check` / `makemigrations --check`. The lower-level commands are still available as `migrations create` and `migrations apply`.
|
|
24
|
+
|
|
25
|
+
2. **Remove index/constraint operations from migration files.** Delete any `AddIndex`, `RemoveIndex`, `RenameIndex`, `AddConstraint`, and `RemoveConstraint` operations from your migration files — these classes no longer exist and will cause import errors. If removing an operation leaves `operations = []`, delete the migration file and run `plain migrations prune --yes`. Indexes and constraints declared on your models will be created automatically by convergence.
|
|
26
|
+
|
|
27
|
+
3. **Replace `PositiveIntegerField`** (and `PositiveBigIntegerField`, `PositiveSmallIntegerField`) with `IntegerField` (or `BigIntegerField`, `SmallIntegerField`) in both models and migration files. Add a `CheckConstraint` if you need to enforce positive values.
|
|
28
|
+
|
|
29
|
+
4. **Run `plain postgres sync`** after upgrading to create indexes and constraints via convergence.
|
|
30
|
+
|
|
31
|
+
5. If you used `plain postgres backups`, install `plain-dev>=0.60.0` — backups have moved to `plain dev backups`.
|
|
32
|
+
|
|
3
33
|
## [0.90.0](https://github.com/dropseed/plain/releases/plain-postgres@0.90.0) (2026-03-28)
|
|
4
34
|
|
|
5
35
|
### What's changed
|
|
@@ -435,7 +435,7 @@ Migrations track changes to your models and update the database schema according
|
|
|
435
435
|
### Creating migrations
|
|
436
436
|
|
|
437
437
|
```bash
|
|
438
|
-
plain
|
|
438
|
+
plain migrations create
|
|
439
439
|
```
|
|
440
440
|
|
|
441
441
|
Key flags:
|
|
@@ -451,12 +451,11 @@ Only write migrations by hand if they are custom data migrations.
|
|
|
451
451
|
### Running migrations
|
|
452
452
|
|
|
453
453
|
```bash
|
|
454
|
-
plain
|
|
454
|
+
plain migrations apply
|
|
455
455
|
```
|
|
456
456
|
|
|
457
457
|
Key flags:
|
|
458
458
|
|
|
459
|
-
- `--backup` / `--no-backup` — Create a database backup before applying (default: on in DEBUG)
|
|
460
459
|
- `--plan` — Show what migrations would run without applying them
|
|
461
460
|
- `--check` — Exit non-zero if unapplied migrations exist (for CI)
|
|
462
461
|
- `--fake` — Mark migrations as applied without running them
|
|
@@ -467,7 +466,7 @@ Key flags:
|
|
|
467
466
|
plain migrations list
|
|
468
467
|
```
|
|
469
468
|
|
|
470
|
-
`
|
|
469
|
+
`migrations apply` has no `--list` or `--status` flag. Use `plain migrations list`.
|
|
471
470
|
|
|
472
471
|
- `--format plan` — Show in dependency order instead of grouped by package
|
|
473
472
|
|
|
@@ -481,8 +480,8 @@ Use this when migrations exist only in your local dev environment and haven't be
|
|
|
481
480
|
|
|
482
481
|
1. Delete the intermediate migration files (keep the initial 0001 and any previously committed migrations)
|
|
483
482
|
2. `plain migrations prune --yes` — removes stale DB records for the deleted files
|
|
484
|
-
3. `plain
|
|
485
|
-
4. `plain
|
|
483
|
+
3. `plain migrations create` — creates a single fresh migration with all the changes
|
|
484
|
+
4. `plain migrations apply --fake` — marks the new migration as applied (the schema is already correct from the old migrations)
|
|
486
485
|
|
|
487
486
|
**Consolidating committed migrations (squash):**
|
|
488
487
|
|
|
@@ -511,16 +510,16 @@ Over time a package can accumulate dozens of migrations. Once **every environmen
|
|
|
511
510
|
|
|
512
511
|
1. Run `plain migrations list` locally and verify everything is applied.
|
|
513
512
|
2. Delete every file in the package's `migrations/` directory except `__init__.py`.
|
|
514
|
-
3. Run `plain
|
|
513
|
+
3. Run `plain migrations create` to generate a fresh `0001_initial`.
|
|
515
514
|
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.
|
|
516
|
-
5. Verify with `plain postgres schema` (zero issues means the reset is clean) and `plain
|
|
515
|
+
5. Verify with `plain postgres schema` (zero issues means the reset is clean) and `plain migrations create --check` (no pending changes).
|
|
517
516
|
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.
|
|
518
517
|
|
|
519
518
|
**Things to keep in mind:**
|
|
520
519
|
|
|
521
520
|
- If resetting multiple packages, process depended-on packages first — the new `0001_initial` may have cross-package FK dependencies.
|
|
522
521
|
- Data migrations (`RunPython`) in the deleted history are gone, which is fine since they've already run everywhere.
|
|
523
|
-
- If CI runs `
|
|
522
|
+
- If CI runs `migrations create --check` or `migrations apply --check`, the reset PR must be merged and deployed before those checks pass in other branches.
|
|
524
523
|
|
|
525
524
|
### Other migration commands
|
|
526
525
|
|
|
@@ -566,9 +565,6 @@ class Product(postgres.Model):
|
|
|
566
565
|
- [`IntegerField`](./fields/__init__.py#IntegerField) - Integer
|
|
567
566
|
- [`BigIntegerField`](./fields/__init__.py#BigIntegerField) - Big (8 byte) integer
|
|
568
567
|
- [`SmallIntegerField`](./fields/__init__.py#SmallIntegerField) - Small integer
|
|
569
|
-
- [`PositiveIntegerField`](./fields/__init__.py#PositiveIntegerField) - Positive integer
|
|
570
|
-
- [`PositiveBigIntegerField`](./fields/__init__.py#PositiveBigIntegerField) - Positive big integer
|
|
571
|
-
- [`PositiveSmallIntegerField`](./fields/__init__.py#PositiveSmallIntegerField) - Positive small integer
|
|
572
568
|
- [`FloatField`](./fields/__init__.py#FloatField) - Floating point number
|
|
573
569
|
- [`DecimalField`](./fields/__init__.py#DecimalField) - Fixed precision decimal
|
|
574
570
|
|
|
@@ -1026,7 +1022,7 @@ See [`default_settings.py`](./default_settings.py) for more details.
|
|
|
1026
1022
|
|
|
1027
1023
|
#### How do I add a field to an existing model?
|
|
1028
1024
|
|
|
1029
|
-
Add the field to your model class, then run `plain
|
|
1025
|
+
Add the field to your model class, then run `plain migrations create` to create a migration. If the field is required (no default value and not nullable), you'll be prompted to provide a default value for existing rows.
|
|
1030
1026
|
|
|
1031
1027
|
#### How do I create a unique constraint on multiple fields?
|
|
1032
1028
|
|
|
@@ -21,9 +21,6 @@ from .fields import (
|
|
|
21
21
|
FloatField,
|
|
22
22
|
GenericIPAddressField,
|
|
23
23
|
IntegerField,
|
|
24
|
-
PositiveBigIntegerField,
|
|
25
|
-
PositiveIntegerField,
|
|
26
|
-
PositiveSmallIntegerField,
|
|
27
24
|
PrimaryKeyField,
|
|
28
25
|
SmallIntegerField,
|
|
29
26
|
TextField,
|
|
@@ -70,9 +67,6 @@ __all__ = [
|
|
|
70
67
|
"FloatField",
|
|
71
68
|
"GenericIPAddressField",
|
|
72
69
|
"IntegerField",
|
|
73
|
-
"PositiveBigIntegerField",
|
|
74
|
-
"PositiveIntegerField",
|
|
75
|
-
"PositiveSmallIntegerField",
|
|
76
70
|
"PrimaryKeyField",
|
|
77
71
|
"SmallIntegerField",
|
|
78
72
|
"TextField",
|
|
@@ -33,12 +33,13 @@ Get approval before writing any model code or generating migrations.
|
|
|
33
33
|
|
|
34
34
|
## Migrations
|
|
35
35
|
|
|
36
|
-
- `uv run plain
|
|
37
|
-
- `uv run plain
|
|
38
|
-
- `uv run plain migrations
|
|
36
|
+
- `uv run plain postgres sync` — the primary command: makes migrations (in DEBUG), applies them, and converges
|
|
37
|
+
- `uv run plain migrations create` — create migrations (`--dry-run` to preview, `--check` for CI)
|
|
38
|
+
- `uv run plain migrations apply` — apply migrations
|
|
39
|
+
- `uv run plain migrations list` — view status
|
|
39
40
|
- Before committing, consolidate multiple uncommitted migrations into one:
|
|
40
41
|
delete the intermediate files, run `migrations prune --yes` to clean stale DB records,
|
|
41
|
-
run `
|
|
42
|
+
run `migrations create` fresh, then `migrations apply --fake` to mark it applied
|
|
42
43
|
- Use `migrations squash` only for already-committed/deployed migrations — never for dev cleanup
|
|
43
44
|
- Only write migrations by hand for custom data migrations
|
|
44
45
|
|
|
@@ -7,7 +7,7 @@ description: Check overall database health — schema correctness and operationa
|
|
|
7
7
|
|
|
8
8
|
Check database health by running schema and operational checks, then fix any issues found.
|
|
9
9
|
|
|
10
|
-
**All checks are read-only.** Fixes are local code changes. Never run database mutations (`
|
|
10
|
+
**All checks are read-only.** Fixes are local code changes. Never run database mutations (`migrations apply`, direct SQL) without explicit user approval.
|
|
11
11
|
|
|
12
12
|
## 1. Dev only or production too?
|
|
13
13
|
|
|
@@ -37,7 +37,7 @@ The JSON output includes `suggestion` fields for each finding. If findings are o
|
|
|
37
37
|
|
|
38
38
|
## 3. Fix issues
|
|
39
39
|
|
|
40
|
-
Make code and migration changes in the local codebase. For app-owned items, this is typically model changes + `uv run plain
|
|
40
|
+
Make code and migration changes in the local codebase. For app-owned items, this is typically model changes + `uv run plain migrations create`. For unknown tables, present `uv run plain postgres drop-unknown-tables` to the user — it shows what will be dropped and asks for confirmation. Use `--yes` to skip the prompt if the user wants the agent to run it directly. For other unmanaged items, the suggestions include exact DDL — present these to the user for review, do not run SQL directly.
|
|
41
41
|
|
|
42
42
|
## 4. Verify
|
|
43
43
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..convergence import execute_plan, plan_convergence
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.option(
|
|
12
|
+
"--yes",
|
|
13
|
+
"-y",
|
|
14
|
+
is_flag=True,
|
|
15
|
+
help="Skip confirmation prompt.",
|
|
16
|
+
)
|
|
17
|
+
@click.option(
|
|
18
|
+
"--drop-undeclared",
|
|
19
|
+
is_flag=True,
|
|
20
|
+
help="Drop indexes and constraints not declared on any model.",
|
|
21
|
+
)
|
|
22
|
+
def converge(yes: bool, drop_undeclared: bool) -> None:
|
|
23
|
+
"""Fix schema mismatches between models and the database.
|
|
24
|
+
|
|
25
|
+
Detects and fixes:
|
|
26
|
+
- Missing indexes (using CONCURRENTLY)
|
|
27
|
+
- Missing constraints (check, unique)
|
|
28
|
+
- NOT VALID constraints needing validation
|
|
29
|
+
|
|
30
|
+
With --drop-undeclared, also drops indexes and constraints that exist in the
|
|
31
|
+
database but are not declared on any model.
|
|
32
|
+
|
|
33
|
+
Without --drop-undeclared, exits non-zero if undeclared constraints remain
|
|
34
|
+
(constraints affect database behavior). Undeclared indexes are reported but
|
|
35
|
+
do not block success.
|
|
36
|
+
|
|
37
|
+
Each fix is applied and committed independently so partial
|
|
38
|
+
failures don't block subsequent fixes.
|
|
39
|
+
"""
|
|
40
|
+
plan = plan_convergence()
|
|
41
|
+
items = plan.executable(drop_undeclared=drop_undeclared)
|
|
42
|
+
success = True
|
|
43
|
+
|
|
44
|
+
if items:
|
|
45
|
+
click.secho(
|
|
46
|
+
f"{len(items)} fix{'es' if len(items) != 1 else ''} to apply:\n",
|
|
47
|
+
bold=True,
|
|
48
|
+
)
|
|
49
|
+
for item in items:
|
|
50
|
+
click.echo(f" {item.describe()}")
|
|
51
|
+
|
|
52
|
+
click.echo()
|
|
53
|
+
|
|
54
|
+
if not yes:
|
|
55
|
+
if not click.confirm("Apply these changes?"):
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
click.echo()
|
|
59
|
+
|
|
60
|
+
result = execute_plan(items)
|
|
61
|
+
|
|
62
|
+
for r in result.results:
|
|
63
|
+
if r.ok:
|
|
64
|
+
click.echo(f" {r.sql}")
|
|
65
|
+
else:
|
|
66
|
+
click.secho(f" FAILED: {r.item.describe()} — {r.error}", fg="red")
|
|
67
|
+
|
|
68
|
+
click.echo()
|
|
69
|
+
click.secho(result.summary, fg="green" if result.ok else "yellow")
|
|
70
|
+
if not result.ok_for_sync:
|
|
71
|
+
success = False
|
|
72
|
+
|
|
73
|
+
if plan.blocked:
|
|
74
|
+
click.echo()
|
|
75
|
+
click.secho("Schema changes require a staged rollout:", fg="red", bold=True)
|
|
76
|
+
for item in plan.blocked:
|
|
77
|
+
click.secho(f" {item.drift.describe()}", fg="red")
|
|
78
|
+
if item.guidance:
|
|
79
|
+
click.secho(f" {item.guidance}", fg="red", dim=True)
|
|
80
|
+
success = False
|
|
81
|
+
|
|
82
|
+
if not drop_undeclared and plan.blocking_cleanup:
|
|
83
|
+
click.echo()
|
|
84
|
+
click.secho("Undeclared constraints still in database:", fg="red", bold=True)
|
|
85
|
+
for item in plan.blocking_cleanup:
|
|
86
|
+
click.secho(f" {item.describe()}", fg="red")
|
|
87
|
+
click.secho("Rerun with --drop-undeclared to remove them.", fg="red")
|
|
88
|
+
success = False
|
|
89
|
+
|
|
90
|
+
if not drop_undeclared and plan.optional_cleanup:
|
|
91
|
+
click.echo()
|
|
92
|
+
for item in plan.optional_cleanup:
|
|
93
|
+
click.echo(f" {item.describe()}")
|
|
94
|
+
click.echo("Run with --drop-undeclared to remove undeclared indexes.")
|
|
95
|
+
|
|
96
|
+
if not success:
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
elif not items:
|
|
99
|
+
click.secho("Schema is converged — nothing to fix.", fg="green")
|
|
@@ -10,12 +10,12 @@ import psycopg
|
|
|
10
10
|
|
|
11
11
|
from plain.cli import register_cli
|
|
12
12
|
|
|
13
|
-
from ..backups.cli import cli as backups_cli
|
|
14
13
|
from ..db import get_connection
|
|
15
14
|
from ..dialect import quote_name
|
|
16
15
|
from .converge import converge
|
|
17
16
|
from .diagnose import diagnose
|
|
18
17
|
from .schema import schema
|
|
18
|
+
from .sync import sync
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@register_cli("postgres")
|
|
@@ -24,10 +24,10 @@ def cli() -> None:
|
|
|
24
24
|
"""Postgres operations"""
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
cli.add_command(backups_cli)
|
|
28
27
|
cli.add_command(converge)
|
|
29
28
|
cli.add_command(diagnose)
|
|
30
29
|
cli.add_command(schema)
|
|
30
|
+
cli.add_command(sync)
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
@cli.command()
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
|
-
import time
|
|
6
5
|
from typing import TYPE_CHECKING, Any
|
|
7
6
|
|
|
8
7
|
import click
|
|
@@ -10,11 +9,9 @@ import click
|
|
|
10
9
|
from plain.cli import register_cli
|
|
11
10
|
from plain.cli.runtime import common_command
|
|
12
11
|
from plain.packages import packages_registry
|
|
13
|
-
from plain.runtime import settings
|
|
14
12
|
from plain.utils.text import Truncator
|
|
15
13
|
|
|
16
14
|
from .. import migrations
|
|
17
|
-
from ..backups.core import DatabaseBackups
|
|
18
15
|
from ..db import get_connection
|
|
19
16
|
from ..migrations.autodetector import MigrationAutodetector
|
|
20
17
|
from ..migrations.executor import MigrationExecutor
|
|
@@ -42,8 +39,7 @@ def cli() -> None:
|
|
|
42
39
|
|
|
43
40
|
|
|
44
41
|
@common_command
|
|
45
|
-
@
|
|
46
|
-
@cli.command("make")
|
|
42
|
+
@cli.command("create")
|
|
47
43
|
@click.argument("package_labels", nargs=-1)
|
|
48
44
|
@click.option(
|
|
49
45
|
"--dry-run",
|
|
@@ -71,7 +67,7 @@ def cli() -> None:
|
|
|
71
67
|
default=1,
|
|
72
68
|
help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
|
|
73
69
|
)
|
|
74
|
-
def
|
|
70
|
+
def create(
|
|
75
71
|
package_labels: tuple[str, ...],
|
|
76
72
|
dry_run: bool,
|
|
77
73
|
empty: bool,
|
|
@@ -273,7 +269,7 @@ def make(
|
|
|
273
269
|
|
|
274
270
|
# Warn about packages that have models but no migrations directory.
|
|
275
271
|
# These are silently skipped by the autodetector, which can be confusing
|
|
276
|
-
# when setting up a new app (
|
|
272
|
+
# when setting up a new app ("No changes detected").
|
|
277
273
|
unmigrated_with_models = []
|
|
278
274
|
for package_label in sorted(loader.unmigrated_packages):
|
|
279
275
|
module_name, _explicit = MigrationLoader.migrations_module(package_label)
|
|
@@ -296,13 +292,12 @@ def make(
|
|
|
296
292
|
click.echo()
|
|
297
293
|
click.echo(
|
|
298
294
|
"To create initial migrations, add the directory and run "
|
|
299
|
-
+ click.style("plain
|
|
295
|
+
+ click.style("plain migrations create", bold=True)
|
|
300
296
|
+ " again."
|
|
301
297
|
)
|
|
302
298
|
|
|
303
299
|
|
|
304
300
|
@common_command
|
|
305
|
-
@register_cli("migrate", shortcut_for="migrations apply")
|
|
306
301
|
@cli.command("apply")
|
|
307
302
|
@click.argument("package_label", required=False)
|
|
308
303
|
@click.argument("migration_name", required=False)
|
|
@@ -320,13 +315,6 @@ def make(
|
|
|
320
315
|
is_flag=True,
|
|
321
316
|
help="Exits with a non-zero status if unapplied migrations exist and does not actually apply migrations.",
|
|
322
317
|
)
|
|
323
|
-
@click.option(
|
|
324
|
-
"--backup/--no-backup",
|
|
325
|
-
"backup",
|
|
326
|
-
is_flag=True,
|
|
327
|
-
default=None,
|
|
328
|
-
help="Explicitly enable/disable pre-migration backups.",
|
|
329
|
-
)
|
|
330
318
|
@click.option(
|
|
331
319
|
"--no-input",
|
|
332
320
|
"--noinput",
|
|
@@ -350,7 +338,6 @@ def apply(
|
|
|
350
338
|
fake: bool,
|
|
351
339
|
plan: bool,
|
|
352
340
|
check_unapplied: bool,
|
|
353
|
-
backup: bool | None,
|
|
354
341
|
no_input: bool,
|
|
355
342
|
atomic_batch: bool | None,
|
|
356
343
|
quiet: bool,
|
|
@@ -553,26 +540,8 @@ def apply(
|
|
|
553
540
|
if len(migration_plan) > 1:
|
|
554
541
|
atomic_batch_message = f"Running {len(migration_plan)} migrations separately (some have atomic=False)"
|
|
555
542
|
|
|
556
|
-
if
|
|
557
|
-
|
|
558
|
-
if not quiet:
|
|
559
|
-
click.secho("Creating backup: ", bold=True, nl=False)
|
|
560
|
-
click.secho(f"{backup_name}", dim=True, nl=False)
|
|
561
|
-
click.secho("... ", dim=True, nl=False)
|
|
562
|
-
|
|
563
|
-
backups_handler = DatabaseBackups()
|
|
564
|
-
backups_handler.create(
|
|
565
|
-
backup_name,
|
|
566
|
-
source="migrate",
|
|
567
|
-
pg_dump=os.environ.get("PG_DUMP", "pg_dump"),
|
|
568
|
-
)
|
|
569
|
-
|
|
570
|
-
if not quiet:
|
|
571
|
-
click.echo(click.style("OK", fg="green"))
|
|
572
|
-
click.echo() # Add blank line after backup output
|
|
573
|
-
else:
|
|
574
|
-
if not quiet:
|
|
575
|
-
click.echo() # Add blank line after packages/target info
|
|
543
|
+
if not quiet:
|
|
544
|
+
click.echo() # Add blank line before applying
|
|
576
545
|
|
|
577
546
|
if not quiet:
|
|
578
547
|
if atomic_batch_message:
|
|
@@ -625,7 +594,7 @@ def apply(
|
|
|
625
594
|
f"Your models have changes that are not yet reflected in migrations ({packages})."
|
|
626
595
|
)
|
|
627
596
|
click.echo(
|
|
628
|
-
"Run 'plain
|
|
597
|
+
"Run 'plain migrations create' to create migrations for these changes."
|
|
629
598
|
)
|
|
630
599
|
|
|
631
600
|
|
|
@@ -706,7 +675,9 @@ def list_migrations(
|
|
|
706
675
|
if plan_node in recorded_migrations:
|
|
707
676
|
output = f" [X] {title}"
|
|
708
677
|
else:
|
|
709
|
-
title +=
|
|
678
|
+
title += (
|
|
679
|
+
" Run `plain migrations apply` to finish recording."
|
|
680
|
+
)
|
|
710
681
|
output = f" [-] {title}"
|
|
711
682
|
if verbosity >= 2 and hasattr(applied_migration, "applied"):
|
|
712
683
|
output += f" (applied at {applied_migration.applied.strftime('%Y-%m-%d %H:%M:%S')})"
|