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.
Files changed (135) hide show
  1. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/PKG-INFO +10 -14
  2. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/CHANGELOG.md +30 -0
  3. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/README.md +9 -13
  4. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/__init__.py +0 -6
  5. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +5 -4
  6. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +2 -2
  7. plain_postgres-0.91.0/plain/postgres/cli/converge.py +99 -0
  8. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/cli/core.py +2 -2
  9. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/cli/migrations.py +10 -39
  10. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/cli/schema.py +54 -44
  11. plain_postgres-0.91.0/plain/postgres/cli/sync.py +173 -0
  12. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/connection.py +43 -8
  13. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/constraints.py +59 -137
  14. plain_postgres-0.91.0/plain/postgres/convergence/__init__.py +61 -0
  15. plain_postgres-0.91.0/plain/postgres/convergence/analysis.py +807 -0
  16. plain_postgres-0.91.0/plain/postgres/convergence/fixes.py +243 -0
  17. plain_postgres-0.91.0/plain/postgres/convergence/planning.py +235 -0
  18. plain_postgres-0.91.0/plain/postgres/ddl.py +84 -0
  19. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/dialect.py +2 -1
  20. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/__init__.py +0 -84
  21. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/related.py +1 -16
  22. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/forms.py +0 -3
  23. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/indexes.py +35 -5
  24. plain_postgres-0.91.0/plain/postgres/introspection/__init__.py +35 -0
  25. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/introspection/health.py +3 -3
  26. plain_postgres-0.91.0/plain/postgres/introspection/schema.py +212 -0
  27. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/__init__.py +0 -12
  28. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/autodetector.py +1 -178
  29. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/loader.py +1 -1
  30. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/operations/__init__.py +0 -10
  31. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/operations/models.py +0 -327
  32. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/state.py +0 -50
  33. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/schema.py +18 -178
  34. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/types.py +0 -6
  35. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/types.pyi +0 -69
  36. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/pyproject.toml +1 -1
  37. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/migrations/0001_initial.py +0 -6
  38. plain_postgres-0.91.0/tests/test_convergence.py +1652 -0
  39. plain_postgres-0.91.0/tests/test_introspection.py +211 -0
  40. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_schema_normalize_type.py +0 -3
  41. plain_postgres-0.90.0/plain/postgres/backups/cli.py +0 -150
  42. plain_postgres-0.90.0/plain/postgres/backups/clients.py +0 -94
  43. plain_postgres-0.90.0/plain/postgres/backups/core.py +0 -172
  44. plain_postgres-0.90.0/plain/postgres/cli/converge.py +0 -80
  45. plain_postgres-0.90.0/plain/postgres/introspection/__init__.py +0 -33
  46. plain_postgres-0.90.0/plain/postgres/introspection/schema.py +0 -282
  47. plain_postgres-0.90.0/tests/app/examples/migrations/__init__.py +0 -0
  48. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/.gitignore +0 -0
  49. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/CLAUDE.md +0 -0
  50. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/LICENSE +0 -0
  51. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/README.md +0 -0
  52. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/aggregates.py +0 -0
  53. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/base.py +0 -0
  54. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/cli/__init__.py +0 -0
  55. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/cli/diagnose.py +0 -0
  56. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/config.py +0 -0
  57. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/connections.py +0 -0
  58. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/constants.py +0 -0
  59. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/database_url.py +0 -0
  60. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/db.py +0 -0
  61. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/default_settings.py +0 -0
  62. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/deletion.py +0 -0
  63. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/entrypoints.py +0 -0
  64. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/enums.py +0 -0
  65. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/exceptions.py +0 -0
  66. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/expressions.py +0 -0
  67. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/encrypted.py +0 -0
  68. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/json.py +0 -0
  69. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/mixins.py +0 -0
  70. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/related_descriptors.py +0 -0
  71. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/related_lookups.py +0 -0
  72. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/related_managers.py +0 -0
  73. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
  74. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/reverse_related.py +0 -0
  75. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/fields/timezones.py +0 -0
  76. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/__init__.py +0 -0
  77. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/comparison.py +0 -0
  78. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/datetime.py +0 -0
  79. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/math.py +0 -0
  80. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/mixins.py +0 -0
  81. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/text.py +0 -0
  82. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/functions/window.py +0 -0
  83. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/lookups.py +0 -0
  84. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/meta.py +0 -0
  85. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/exceptions.py +0 -0
  86. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/executor.py +0 -0
  87. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/graph.py +0 -0
  88. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/migration.py +0 -0
  89. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/operations/base.py +0 -0
  90. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/operations/fields.py +0 -0
  91. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/operations/special.py +0 -0
  92. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/optimizer.py +0 -0
  93. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/questioner.py +0 -0
  94. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/recorder.py +0 -0
  95. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/serializer.py +0 -0
  96. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/utils.py +0 -0
  97. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/migrations/writer.py +0 -0
  98. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/options.py +0 -0
  99. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/otel.py +0 -0
  100. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/preflight.py +0 -0
  101. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/query.py +0 -0
  102. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/query_utils.py +0 -0
  103. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/registry.py +0 -0
  104. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/sql/__init__.py +0 -0
  105. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/sql/compiler.py +0 -0
  106. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/sql/constants.py +0 -0
  107. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/sql/datastructures.py +0 -0
  108. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/sql/query.py +0 -0
  109. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/sql/where.py +0 -0
  110. {plain_postgres-0.90.0/plain/postgres/backups → plain_postgres-0.91.0/plain/postgres/test}/__init__.py +0 -0
  111. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/test/pytest.py +0 -0
  112. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/test/utils.py +0 -0
  113. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/transaction.py +0 -0
  114. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/plain/postgres/utils.py +0 -0
  115. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  116. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  117. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  118. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  119. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  120. {plain_postgres-0.90.0/plain/postgres/test → plain_postgres-0.91.0/tests/app/examples/migrations}/__init__.py +0 -0
  121. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/examples/models.py +0 -0
  122. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/settings.py +0 -0
  123. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/app/urls.py +0 -0
  124. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_connection_isolation.py +0 -0
  125. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_connection_lifecycle.py +0 -0
  126. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_database_url.py +0 -0
  127. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_delete_behaviors.py +0 -0
  128. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_encrypted_fields.py +0 -0
  129. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_exceptions.py +0 -0
  130. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_iterator.py +0 -0
  131. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_manager_assignment.py +0 -0
  132. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_models.py +0 -0
  133. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_read_only_transactions.py +0 -0
  134. {plain_postgres-0.90.0 → plain_postgres-0.91.0}/tests/test_related_descriptors.py +0 -0
  135. {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.90.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 makemigrations
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 migrate --backup
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
- `migrate` has no `--list` or `--status` flag. Use `plain migrations list`.
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 makemigrations` — creates a single fresh migration with all the changes
497
- 4. `plain migrate --fake` — marks the new migration as applied (the schema is already correct from the old migrations)
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 makemigrations` to generate a fresh `0001_initial`.
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 makemigrations --check` (no pending changes).
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 `makemigrations --check` or `migrate --check`, the reset PR must be merged and deployed before those checks pass in other branches.
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 makemigrations` 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.
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 makemigrations
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 migrate --backup
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
- `migrate` has no `--list` or `--status` flag. Use `plain migrations list`.
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 makemigrations` — creates a single fresh migration with all the changes
485
- 4. `plain migrate --fake` — marks the new migration as applied (the schema is already correct from the old migrations)
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 makemigrations` to generate a fresh `0001_initial`.
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 makemigrations --check` (no pending changes).
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 `makemigrations --check` or `migrate --check`, the reset PR must be merged and deployed before those checks pass in other branches.
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 makemigrations` 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.
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 makemigrations` — create migrations (`--dry-run` to preview, `--check` for CI)
37
- - `uv run plain migrate --backup` — apply migrations
38
- - `uv run plain migrations list` — view status (not `migrate --list`)
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 `makemigrations` fresh, then `migrate --fake` to mark it applied
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 (`migrate`, direct SQL) without explicit user approval.
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 makemigrations`. 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.
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
- @register_cli("makemigrations", shortcut_for="migrations make")
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 make(
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 (makemigrations says "No changes detected").
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 makemigrations", bold=True)
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 backup or (backup is None and settings.DEBUG):
557
- backup_name = time.strftime("%Y%m%d_%H%M%S")
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 makemigrations' to create migrations for these changes."
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 += " Run `plain migrate` to finish recording."
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')})"