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.
Files changed (244) hide show
  1. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/PKG-INFO +62 -37
  2. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/CHANGELOG.md +27 -0
  3. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/README.md +61 -36
  4. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +1 -0
  5. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/decorators.py +9 -2
  6. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/migrations.py +243 -249
  7. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/sync.py +28 -3
  8. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/autodetector.py +173 -101
  9. plain_postgres-0.118.0/plain/postgres/migrations/baselines.py +124 -0
  10. plain_postgres-0.118.0/plain/postgres/migrations/exceptions.py +118 -0
  11. plain_postgres-0.118.0/plain/postgres/migrations/executor.py +261 -0
  12. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/graph.py +1 -85
  13. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/loader.py +132 -83
  14. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/migration.py +12 -6
  15. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/operations/base.py +6 -6
  16. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/operations/special.py +8 -4
  17. plain_postgres-0.118.0/plain/postgres/migrations/reset.py +319 -0
  18. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/writer.py +26 -29
  19. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/options.py +11 -5
  20. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/preflight/database.py +36 -55
  21. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/preflight/models.py +1 -1
  22. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sql/compiler.py +5 -7
  23. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/pyproject.toml +1 -1
  24. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0002_test_field_removed.py +2 -1
  25. plain_postgres-0.118.0/tests/internal/conftest.py +55 -0
  26. plain_postgres-0.118.0/tests/internal/test_baselines.py +749 -0
  27. plain_postgres-0.118.0/tests/internal/test_migrations_reset.py +472 -0
  28. plain_postgres-0.118.0/tests/internal/test_replaces_removed.py +98 -0
  29. plain_postgres-0.118.0/tests/internal/test_shipped_baselines.py +51 -0
  30. plain_postgres-0.118.0/tests/internal/test_writer_operation_options.py +28 -0
  31. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_databases.py +8 -2
  32. plain_postgres-0.117.0/plain/postgres/migrations/exceptions.py +0 -51
  33. plain_postgres-0.117.0/plain/postgres/migrations/executor.py +0 -183
  34. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/.gitignore +0 -0
  35. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/CLAUDE.md +0 -0
  36. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/LICENSE +0 -0
  37. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/README.md +0 -0
  38. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/__init__.py +0 -0
  39. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/adapters.py +0 -0
  40. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  41. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/aggregates.py +0 -0
  42. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/base.py +0 -0
  43. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/__init__.py +0 -0
  44. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/converge.py +0 -0
  45. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/core.py +0 -0
  46. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/diagnose.py +0 -0
  47. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/cli/schema.py +0 -0
  48. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/config.py +0 -0
  49. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/connection.py +0 -0
  50. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/constants.py +0 -0
  51. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/constraints.py +0 -0
  52. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/convergence/__init__.py +0 -0
  53. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/convergence/analysis.py +0 -0
  54. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/convergence/corrections.py +0 -0
  55. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/convergence/planning.py +0 -0
  56. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/database_url.py +0 -0
  57. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/databases.py +0 -0
  58. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/db.py +0 -0
  59. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/ddl.py +0 -0
  60. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/default_settings.py +0 -0
  61. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/deletion.py +0 -0
  62. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/dialect.py +0 -0
  63. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/entrypoints.py +0 -0
  64. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/enums.py +0 -0
  65. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/exceptions.py +0 -0
  66. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/expressions.py +0 -0
  67. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/__init__.py +0 -0
  68. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/base.py +0 -0
  69. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/binary.py +0 -0
  70. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/boolean.py +0 -0
  71. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/duration.py +0 -0
  72. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/encrypted.py +0 -0
  73. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/json.py +0 -0
  74. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/mixins.py +0 -0
  75. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/network.py +0 -0
  76. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/numeric.py +0 -0
  77. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/primary_key.py +0 -0
  78. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/related.py +0 -0
  79. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/related_descriptors.py +0 -0
  80. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/related_lookups.py +0 -0
  81. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/related_managers.py +0 -0
  82. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
  83. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/reverse_related.py +0 -0
  84. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/temporal.py +0 -0
  85. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/text.py +0 -0
  86. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/timezones.py +0 -0
  87. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/fields/uuid.py +0 -0
  88. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/forms.py +0 -0
  89. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/__init__.py +0 -0
  90. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/comparison.py +0 -0
  91. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/datetime.py +0 -0
  92. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/math.py +0 -0
  93. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/mixins.py +0 -0
  94. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/random.py +0 -0
  95. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/text.py +0 -0
  96. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/uuid.py +0 -0
  97. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/functions/window.py +0 -0
  98. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/indexes.py +0 -0
  99. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/__init__.py +0 -0
  100. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/__init__.py +0 -0
  101. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
  102. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  103. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
  104. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/context.py +0 -0
  105. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/helpers.py +0 -0
  106. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/ownership.py +0 -0
  107. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/runner.py +0 -0
  108. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/health/types.py +0 -0
  109. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/introspection/schema.py +0 -0
  110. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/lookups.py +0 -0
  111. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/meta.py +0 -0
  112. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/middleware.py +0 -0
  113. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/__init__.py +0 -0
  114. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/operations/__init__.py +0 -0
  115. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/operations/fields.py +0 -0
  116. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/operations/models.py +0 -0
  117. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/optimizer.py +0 -0
  118. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/questioner.py +0 -0
  119. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/recorder.py +0 -0
  120. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/serializer.py +0 -0
  121. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/state.py +0 -0
  122. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/migrations/utils.py +0 -0
  123. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/otel.py +0 -0
  124. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/preflight/__init__.py +0 -0
  125. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/preflight/indexes.py +0 -0
  126. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/query.py +0 -0
  127. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/query_utils.py +0 -0
  128. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/registry.py +0 -0
  129. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/schema.py +0 -0
  130. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/schema_lock.py +0 -0
  131. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sources.py +0 -0
  132. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sql/__init__.py +0 -0
  133. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sql/constants.py +0 -0
  134. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sql/datastructures.py +0 -0
  135. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sql/query.py +0 -0
  136. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/sql/where.py +0 -0
  137. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/test/__init__.py +0 -0
  138. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/test/database.py +0 -0
  139. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/test/pytest.py +0 -0
  140. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/transaction.py +0 -0
  141. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/types.py +0 -0
  142. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/types.pyi +0 -0
  143. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/plain/postgres/utils.py +0 -0
  144. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/forms.py +0 -0
  145. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  146. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  147. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  148. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  149. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  150. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  151. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  152. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  153. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  154. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  155. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  156. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  157. {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
  158. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  159. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  160. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  161. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
  162. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/migrations/__init__.py +0 -0
  163. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/__init__.py +0 -0
  164. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/constraints.py +0 -0
  165. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/defaults.py +0 -0
  166. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/delete.py +0 -0
  167. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/encrypted.py +0 -0
  168. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/forms.py +0 -0
  169. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/indexes.py +0 -0
  170. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/iteration.py +0 -0
  171. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/mixins.py +0 -0
  172. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/nullability.py +0 -0
  173. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/querysets.py +0 -0
  174. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/relationships.py +0 -0
  175. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/storage_parameters.py +0 -0
  176. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/trees.py +0 -0
  177. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/models/unregistered.py +0 -0
  178. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/urls.py +0 -0
  179. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/examples/views.py +0 -0
  180. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/settings.py +0 -0
  181. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/app/urls.py +0 -0
  182. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/conftest.py +0 -0
  183. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/conftest_convergence.py +0 -0
  184. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_apply_replan.py +0 -0
  185. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_autodetector_not_null_errors.py +0 -0
  186. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_autodetector_type_change.py +0 -0
  187. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_connection_isolation.py +0 -0
  188. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_connection_lifecycle.py +0 -0
  189. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_connection_pool.py +0 -0
  190. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_connection_self_heal.py +0 -0
  191. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_constraint_violation_error.py +0 -0
  192. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence.py +0 -0
  193. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_constraints.py +0 -0
  194. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_defaults.py +0 -0
  195. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_fk.py +0 -0
  196. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_indexes.py +0 -0
  197. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_nullability.py +0 -0
  198. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
  199. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_convergence_timeouts.py +0 -0
  200. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_databases_not_on_runtime_path.py +0 -0
  201. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_db_expression_defaults.py +0 -0
  202. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_diagnose.py +0 -0
  203. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_executor_connection_hook.py +0 -0
  204. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_fk_characterization.py +0 -0
  205. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_health.py +0 -0
  206. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_introspection.py +0 -0
  207. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_literal_default_persistence.py +0 -0
  208. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_management_connection.py +0 -0
  209. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_migration_executor.py +0 -0
  210. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_no_callable_defaults.py +0 -0
  211. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_otel_metrics.py +0 -0
  212. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
  213. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
  214. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
  215. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_rollback_exc_attribution.py +0 -0
  216. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_schema_lock.py +0 -0
  217. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_schema_normalize_type.py +0 -0
  218. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_schema_timeouts.py +0 -0
  219. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/internal/test_unresolved_relation_refs.py +0 -0
  220. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_create_update.py +0 -0
  221. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_database_url.py +0 -0
  222. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_deferred_loading.py +0 -0
  223. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_delete_behaviors.py +0 -0
  224. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_encrypted_fields.py +0 -0
  225. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_exceptions.py +0 -0
  226. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_field_defaults.py +0 -0
  227. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_functions_uuid.py +0 -0
  228. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_integrity_error_mapping.py +0 -0
  229. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_iterator.py +0 -0
  230. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_m2m.py +0 -0
  231. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_manager_assignment.py +0 -0
  232. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_manual_pk.py +0 -0
  233. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_mixins.py +0 -0
  234. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_modelform_roundtrip.py +0 -0
  235. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_only_empty_defaults.py +0 -0
  236. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_order_by_expressions.py +0 -0
  237. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_queryset_ordered.py +0 -0
  238. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_queryset_repr.py +0 -0
  239. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_queryset_slicing.py +0 -0
  240. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_random_string_field.py +0 -0
  241. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_raw_query.py +0 -0
  242. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_read_only_transactions.py +0 -0
  243. {plain_postgres-0.117.0 → plain_postgres-0.118.0}/tests/public/test_related.py +0 -0
  244. {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.117.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`, `plain migrations squash`
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 | 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 squash <pkg> <migration>` | Squash migrations into one |
614
- | `plain migrations prune` | Remove stale migration records |
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
- **Consolidating committed migrations (squash):**
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
- Use this when migrations have already been committed or deployed to other environments.
632
+ #### Baselines
632
633
 
633
- `plain migrations squash <package> <migration>` creates a replacement migration with a `replaces` list. Keep the original files until all environments have migrated past the squash point, then delete them and run `migrations prune`.
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
- **Which method to use:**
636
-
637
- | Scenario | Method |
638
- | ----------------------------------------- | ------------------------------------------------------- |
639
- | Migrations are local only (not committed) | Delete-and-recreate |
640
- | Migrations are committed but not deployed | Delete-and-recreate (if all developers reset) or squash |
641
- | Migrations are deployed to production | Squash or full reset |
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
- Over time a package can accumulate dozens of migrations. Once **every environment** (dev, staging, production) has applied all of them, you can replace the entire history with a single fresh `0001_initial`.
664
+ Once **every environment** has applied a package's migrations, its history can be collapsed into one baseline:
646
665
 
647
- **Prerequisites:**
666
+ ```bash
667
+ plain migrations reset <package>
668
+ ```
648
669
 
649
- - Every environment (dev, staging, production) has applied all existing migrations. If any environment is behind, the reset will break it.
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
- **Steps:**
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
- 1. Run `plain migrations list` locally and verify everything is applied.
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
- **Things to keep in mind:**
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
- - If resetting multiple packages, process depended-on packages first — the new `0001_initial` may have cross-package FK dependencies.
664
- - Data migrations (`RunPython`) in the deleted history are gone, which is fine since they've already run everywhere.
665
- - 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.
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
- See [Structural migrations](#structural-migrations) for shared commands (`apply`, `list`, `squash`, `prune`).
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`, `plain migrations squash`
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 | 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 squash <pkg> <migration>` | Squash migrations into one |
601
- | `plain migrations prune` | Remove stale migration records |
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
- **Consolidating committed migrations (squash):**
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
- Use this when migrations have already been committed or deployed to other environments.
619
+ #### Baselines
619
620
 
620
- `plain migrations squash <package> <migration>` creates a replacement migration with a `replaces` list. Keep the original files until all environments have migrated past the squash point, then delete them and run `migrations prune`.
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
- **Which method to use:**
623
-
624
- | Scenario | Method |
625
- | ----------------------------------------- | ------------------------------------------------------- |
626
- | Migrations are local only (not committed) | Delete-and-recreate |
627
- | Migrations are committed but not deployed | Delete-and-recreate (if all developers reset) or squash |
628
- | Migrations are deployed to production | Squash or full reset |
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
- Over time a package can accumulate dozens of migrations. Once **every environment** (dev, staging, production) has applied all of them, you can replace the entire history with a single fresh `0001_initial`.
651
+ Once **every environment** has applied a package's migrations, its history can be collapsed into one baseline:
633
652
 
634
- **Prerequisites:**
653
+ ```bash
654
+ plain migrations reset <package>
655
+ ```
635
656
 
636
- - Every environment (dev, staging, production) has applied all existing migrations. If any environment is behind, the reset will break it.
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
- **Steps:**
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
- 1. Run `plain migrations list` locally and verify everything is applied.
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
- **Things to keep in mind:**
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
- - If resetting multiple packages, process depended-on packages first — the new `0001_initial` may have cross-package FK dependencies.
651
- - Data migrations (`RunPython`) in the deleted history are gone, which is fine since they've already run everywhere.
652
- - 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.
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
- See [Structural migrations](#structural-migrations) for shared commands (`apply`, `list`, `squash`, `prune`).
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
- with use_management_connection():
38
- return f(*args, **kwargs)
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]