plain.postgres 0.96.0__tar.gz → 0.97.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 (191) hide show
  1. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/PKG-INFO +61 -33
  2. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/CHANGELOG.md +30 -0
  3. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/README.md +60 -32
  4. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/__init__.py +2 -1
  5. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/converge.py +2 -0
  6. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/core.py +3 -0
  7. plain_postgres-0.97.0/plain/postgres/cli/decorators.py +24 -0
  8. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/diagnose.py +2 -0
  9. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/migrations.py +6 -0
  10. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/schema.py +2 -0
  11. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/sync.py +2 -0
  12. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/connection.py +37 -384
  13. plain_postgres-0.97.0/plain/postgres/connections.py +126 -0
  14. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/database_url.py +24 -2
  15. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/db.py +2 -1
  16. plain_postgres-0.97.0/plain/postgres/default_settings.py +38 -0
  17. plain_postgres-0.97.0/plain/postgres/test/database.py +150 -0
  18. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/test/pytest.py +24 -24
  19. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/utils.py +2 -21
  20. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/pyproject.toml +1 -1
  21. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_connection_lifecycle.py +25 -18
  22. plain_postgres-0.97.0/tests/test_management_connection.py +105 -0
  23. plain_postgres-0.96.0/plain/postgres/connections.py +0 -98
  24. plain_postgres-0.96.0/plain/postgres/default_settings.py +0 -56
  25. plain_postgres-0.96.0/plain/postgres/test/utils.py +0 -18
  26. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/.gitignore +0 -0
  27. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/CLAUDE.md +0 -0
  28. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/LICENSE +0 -0
  29. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/README.md +0 -0
  30. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
  31. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  32. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/aggregates.py +0 -0
  33. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/base.py +0 -0
  34. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/cli/__init__.py +0 -0
  35. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/config.py +0 -0
  36. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/constants.py +0 -0
  37. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/constraints.py +0 -0
  38. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/convergence/__init__.py +0 -0
  39. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/convergence/analysis.py +0 -0
  40. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/convergence/fixes.py +0 -0
  41. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/convergence/planning.py +0 -0
  42. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/ddl.py +0 -0
  43. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/deletion.py +0 -0
  44. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/dialect.py +0 -0
  45. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/entrypoints.py +0 -0
  46. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/enums.py +0 -0
  47. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/exceptions.py +0 -0
  48. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/expressions.py +0 -0
  49. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/__init__.py +0 -0
  50. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/base.py +0 -0
  51. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/binary.py +0 -0
  52. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/boolean.py +0 -0
  53. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/duration.py +0 -0
  54. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/encrypted.py +0 -0
  55. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/json.py +0 -0
  56. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/mixins.py +0 -0
  57. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/network.py +0 -0
  58. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/numeric.py +0 -0
  59. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/primary_key.py +0 -0
  60. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/related.py +0 -0
  61. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/related_descriptors.py +0 -0
  62. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/related_lookups.py +0 -0
  63. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/related_managers.py +0 -0
  64. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
  65. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/reverse_related.py +0 -0
  66. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/temporal.py +0 -0
  67. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/text.py +0 -0
  68. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/timezones.py +0 -0
  69. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/fields/uuid.py +0 -0
  70. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/forms.py +0 -0
  71. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/__init__.py +0 -0
  72. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/comparison.py +0 -0
  73. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/datetime.py +0 -0
  74. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/math.py +0 -0
  75. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/mixins.py +0 -0
  76. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/random.py +0 -0
  77. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/text.py +0 -0
  78. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/uuid.py +0 -0
  79. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/functions/window.py +0 -0
  80. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/indexes.py +0 -0
  81. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/introspection/__init__.py +0 -0
  82. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/introspection/health.py +0 -0
  83. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/introspection/schema.py +0 -0
  84. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/lookups.py +0 -0
  85. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/meta.py +0 -0
  86. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/__init__.py +0 -0
  87. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/autodetector.py +0 -0
  88. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/exceptions.py +0 -0
  89. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/executor.py +0 -0
  90. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/graph.py +0 -0
  91. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/loader.py +0 -0
  92. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/migration.py +0 -0
  93. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/operations/__init__.py +0 -0
  94. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/operations/base.py +0 -0
  95. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/operations/fields.py +0 -0
  96. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/operations/models.py +0 -0
  97. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/operations/special.py +0 -0
  98. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/optimizer.py +0 -0
  99. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/questioner.py +0 -0
  100. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/recorder.py +0 -0
  101. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/serializer.py +0 -0
  102. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/state.py +0 -0
  103. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/utils.py +0 -0
  104. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/migrations/writer.py +0 -0
  105. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/options.py +0 -0
  106. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/otel.py +0 -0
  107. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/preflight.py +0 -0
  108. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/query.py +0 -0
  109. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/query_utils.py +0 -0
  110. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/registry.py +0 -0
  111. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/schema.py +0 -0
  112. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/sql/__init__.py +0 -0
  113. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/sql/compiler.py +0 -0
  114. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/sql/constants.py +0 -0
  115. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/sql/datastructures.py +0 -0
  116. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/sql/query.py +0 -0
  117. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/sql/where.py +0 -0
  118. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/test/__init__.py +0 -0
  119. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/transaction.py +0 -0
  120. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/types.py +0 -0
  121. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/plain/postgres/types.pyi +0 -0
  122. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/forms.py +0 -0
  123. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  124. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  125. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  126. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  127. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  128. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  129. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  130. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  131. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  132. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  133. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  134. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  135. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  136. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
  137. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  138. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  139. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  140. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/migrations/__init__.py +0 -0
  141. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/__init__.py +0 -0
  142. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/constraints.py +0 -0
  143. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/defaults.py +0 -0
  144. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/delete.py +0 -0
  145. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/encrypted.py +0 -0
  146. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/forms.py +0 -0
  147. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/indexes.py +0 -0
  148. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/iteration.py +0 -0
  149. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/mixins.py +0 -0
  150. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/nullability.py +0 -0
  151. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/querysets.py +0 -0
  152. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/relationships.py +0 -0
  153. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/trees.py +0 -0
  154. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/models/unregistered.py +0 -0
  155. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/urls.py +0 -0
  156. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/examples/views.py +0 -0
  157. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/settings.py +0 -0
  158. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/app/urls.py +0 -0
  159. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/conftest_convergence.py +0 -0
  160. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_autodetector_not_null_errors.py +0 -0
  161. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_autodetector_type_change.py +0 -0
  162. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_connection_isolation.py +0 -0
  163. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence.py +0 -0
  164. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence_constraints.py +0 -0
  165. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence_defaults.py +0 -0
  166. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence_fk.py +0 -0
  167. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence_indexes.py +0 -0
  168. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence_nullability.py +0 -0
  169. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_convergence_timeouts.py +0 -0
  170. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_database_url.py +0 -0
  171. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_db_expression_defaults.py +0 -0
  172. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_delete_behaviors.py +0 -0
  173. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_encrypted_fields.py +0 -0
  174. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_exceptions.py +0 -0
  175. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_field_defaults.py +0 -0
  176. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_functions_uuid.py +0 -0
  177. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_introspection.py +0 -0
  178. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_iterator.py +0 -0
  179. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_literal_default_persistence.py +0 -0
  180. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_m2m.py +0 -0
  181. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_manager_assignment.py +0 -0
  182. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_migration_executor.py +0 -0
  183. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_mixins.py +0 -0
  184. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_modelform_roundtrip.py +0 -0
  185. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_no_callable_defaults.py +0 -0
  186. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_random_string_field.py +0 -0
  187. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_raw_query.py +0 -0
  188. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_read_only_transactions.py +0 -0
  189. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_related.py +0 -0
  190. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_schema_normalize_type.py +0 -0
  191. {plain_postgres-0.96.0 → plain_postgres-0.97.0}/tests/test_schema_timeouts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.postgres
3
- Version: 0.96.0
3
+ Version: 0.97.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
@@ -82,26 +82,63 @@ admin_users = User.query.filter(is_admin=True)
82
82
 
83
83
  ## Database connection
84
84
 
85
- To connect to a database, you can provide a `DATABASE_URL` environment variable:
85
+ Configure the database with a single URL. The canonical Plain setting is `POSTGRES_URL`:
86
+
87
+ ```python
88
+ # app/settings.py
89
+ POSTGRES_URL = "postgresql://user:password@localhost:5432/dbname"
90
+ ```
91
+
92
+ Or via environment variable:
93
+
94
+ ```sh
95
+ PLAIN_POSTGRES_URL=postgresql://user:password@localhost:5432/dbname
96
+ ```
97
+
98
+ Plain also reads the `DATABASE_URL` environment variable as a fallback — it's the widely-used convention for Postgres connection strings, so most hosting setups work without extra configuration:
86
99
 
87
100
  ```sh
88
101
  DATABASE_URL=postgresql://user:password@localhost:5432/dbname
89
102
  ```
90
103
 
91
- Or you can set the individual `POSTGRES_*` settings (via `PLAIN_POSTGRES_*` environment variables or in `app/settings.py`):
104
+ Precedence (highest to lowest): `PLAIN_POSTGRES_URL` `POSTGRES_URL` in `settings.py` `DATABASE_URL` environment variable.
92
105
 
93
- ```python
94
- # app/settings.py
95
- POSTGRES_HOST = "localhost"
96
- POSTGRES_PORT = 5432
97
- POSTGRES_DATABASE = "dbname"
98
- POSTGRES_USER = "user"
99
- POSTGRES_PASSWORD = "password"
106
+ The URL supports any libpq connection parameter as a query string — for example `?sslmode=require&application_name=web&connect_timeout=10`. These are parsed and passed through to the driver.
107
+
108
+ To explicitly disable the database (e.g. during Docker builds where no database is available), set the URL to the string `none`:
109
+
110
+ ```sh
111
+ PLAIN_POSTGRES_URL=none
100
112
  ```
101
113
 
102
- If `DATABASE_URL` is set, it takes priority and the individual connection settings are parsed from it.
114
+ ### Bypassing a connection pooler for management operations
115
+
116
+ Transaction-mode poolers (PlanetScale, Supabase's pooler, Neon's pooler, standalone pgbouncer in transaction mode) can't run DDL, long transactions, or `pg_dump`. To work around this, set a second URL that management commands use to reach Postgres directly:
117
+
118
+ ```sh
119
+ PLAIN_POSTGRES_URL=postgresql://app@pooler:6432/myapp
120
+ PLAIN_POSTGRES_MANAGEMENT_URL=postgresql://app@postgres:5432/myapp
121
+ ```
122
+
123
+ When `POSTGRES_MANAGEMENT_URL` is set, these commands connect through it instead of `POSTGRES_URL`:
124
+
125
+ - `plain migrations create`, `plain migrations apply`, `plain migrations list`, `plain migrations prune`, `plain migrations squash`
126
+ - `plain postgres sync`, `plain postgres converge`, `plain postgres schema`
127
+ - `plain postgres diagnose`, `plain postgres drop-unknown-tables`, `plain postgres shell`
128
+
129
+ When it's unset, all commands use `POSTGRES_URL` — there's no behavior change for existing apps.
130
+
131
+ To route custom code through the management connection, use the `use_management_connection()` context manager:
132
+
133
+ ```python
134
+ from plain.postgres import use_management_connection
135
+
136
+ with use_management_connection():
137
+ # Any get_connection() / ORM calls inside this block use POSTGRES_MANAGEMENT_URL.
138
+ run_custom_schema_change()
139
+ ```
103
140
 
104
- To explicitly disable the database (e.g. during Docker builds where no database is available), set `DATABASE_URL=none`.
141
+ You _can_ point the two URLs at different Postgres roles — e.g. a least-privilege DML role for runtime and a DDL-capable role for management. Plain does not currently automate the grant/ownership plumbing that split requires (default privileges for newly-created tables, ownership reassignment, preflight checks that the runtime role can see the schema). If you adopt that pattern, you're responsible for wiring those up yourself.
105
142
 
106
143
  **PostgreSQL is the only supported database.** You need to install a PostgreSQL driver separately — [psycopg](https://www.psycopg.org/) is recommended:
107
144
 
@@ -1206,27 +1243,18 @@ These are static, code-level checks that catch issues before you deploy. The `di
1206
1243
 
1207
1244
  ## Settings
1208
1245
 
1209
- Connection settings are configured via `DATABASE_URL` or individual `POSTGRES_*` settings.
1210
-
1211
- When `DATABASE_URL` is set, it is parsed into the individual connection settings automatically. When `DATABASE_URL` is not set, the connection settings are required individually.
1212
-
1213
- Set `DATABASE_URL=none` to explicitly disable the database (e.g. during Docker image builds).
1214
-
1215
- | Setting | Type | Default | Env var |
1216
- | ---------------------------------------- | ------------- | ------- | ---------------------------------------------- |
1217
- | `POSTGRES_HOST` | `str` | | `PLAIN_POSTGRES_HOST` |
1218
- | `POSTGRES_PORT` | `int \| None` | `None` | `PLAIN_POSTGRES_PORT` |
1219
- | `POSTGRES_DATABASE` | `str` | | `PLAIN_POSTGRES_DATABASE` |
1220
- | `POSTGRES_USER` | `str` | | `PLAIN_POSTGRES_USER` |
1221
- | `POSTGRES_PASSWORD` | `Secret[str]` | — | `PLAIN_POSTGRES_PASSWORD` |
1222
- | `POSTGRES_CONN_MAX_AGE` | `int` | `600` | `PLAIN_POSTGRES_CONN_MAX_AGE` |
1223
- | `POSTGRES_CONN_HEALTH_CHECKS` | `bool` | `True` | `PLAIN_POSTGRES_CONN_HEALTH_CHECKS` |
1224
- | `POSTGRES_OPTIONS` | `dict` | `{}` | — |
1225
- | `POSTGRES_TIME_ZONE` | `str \| None` | `None` | `PLAIN_POSTGRES_TIME_ZONE` |
1226
- | `POSTGRES_MIGRATION_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_LOCK_TIMEOUT` |
1227
- | `POSTGRES_MIGRATION_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT` |
1228
- | `POSTGRES_CONVERGENCE_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_LOCK_TIMEOUT` |
1229
- | `POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` |
1246
+ The connection is configured with a single URL (`POSTGRES_URL`). `DATABASE_URL` is read as a platform-compat fallback. Set the URL to `none` to explicitly disable the database (e.g. during Docker image builds).
1247
+
1248
+ | Setting | Type | Default | Env var |
1249
+ | ---------------------------------------- | ------------- | ----------------------- | ---------------------------------------------- |
1250
+ | `POSTGRES_URL` | `Secret[str]` | `$DATABASE_URL` or `""` | `PLAIN_POSTGRES_URL` |
1251
+ | `POSTGRES_MANAGEMENT_URL` | `Secret[str]` | `""` | `PLAIN_POSTGRES_MANAGEMENT_URL` |
1252
+ | `POSTGRES_CONN_MAX_AGE` | `int` | `600` | `PLAIN_POSTGRES_CONN_MAX_AGE` |
1253
+ | `POSTGRES_CONN_HEALTH_CHECKS` | `bool` | `True` | `PLAIN_POSTGRES_CONN_HEALTH_CHECKS` |
1254
+ | `POSTGRES_MIGRATION_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_LOCK_TIMEOUT` |
1255
+ | `POSTGRES_MIGRATION_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT` |
1256
+ | `POSTGRES_CONVERGENCE_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_LOCK_TIMEOUT` |
1257
+ | `POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` |
1230
1258
 
1231
1259
  See [`default_settings.py`](./default_settings.py) for more details.
1232
1260
 
@@ -1,5 +1,35 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.97.0](https://github.com/dropseed/plain/releases/plain-postgres@0.97.0) (2026-04-21)
4
+
5
+ ### What's changed
6
+
7
+ - **Replaced individual `POSTGRES_*` connection fields with a single `POSTGRES_URL` setting.** `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DATABASE`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_OPTIONS`, and `POSTGRES_TIME_ZONE` are gone — configure the connection with one URL (e.g. `postgresql://user:pass@host:5432/db?sslmode=require`). `DATABASE_URL` is still read as a fallback. Set the URL to `none` to explicitly disable the database (e.g. during Docker image builds). ([770a74606463](https://github.com/dropseed/plain/commit/770a74606463))
8
+ - **Added `POSTGRES_MANAGEMENT_URL` for routing DDL through a separate connection.** When set, `plain migrations create|apply|list|prune|squash`, `plain postgres sync|converge|schema|diagnose|drop-unknown-tables|shell` connect through this URL instead of `POSTGRES_URL`. Use it to bypass transaction-mode poolers (PlanetScale, Supabase's pooler, Neon's pooler, pgbouncer) for schema changes, long transactions, and `pg_dump`. A new `use_management_connection()` context manager routes custom code through the same connection. When unset, all commands use `POSTGRES_URL` — no behavior change for existing apps. ([d1cc9630d049](https://github.com/dropseed/plain/commit/d1cc9630d049))
9
+ - **Extracted the test-database lifecycle off `DatabaseConnection`.** Test setup/teardown now lives in `plain.postgres.test` instead of coupling it to the runtime connection class. ([ea67f82c746c](https://github.com/dropseed/plain/commit/ea67f82c746c))
10
+ - **Removed thin psycopg re-export wrappers.** Internal code now imports directly from `psycopg` rather than the redundant Plain-level passthroughs. ([d1cb74100e0d](https://github.com/dropseed/plain/commit/d1cb74100e0d))
11
+
12
+ ### Upgrade instructions
13
+
14
+ - **Replace individual `POSTGRES_*` settings with `POSTGRES_URL`** in `app/settings.py` (or `PLAIN_POSTGRES_URL` in the environment). For example:
15
+
16
+ ```python
17
+ # Before
18
+ POSTGRES_HOST = "localhost"
19
+ POSTGRES_PORT = 5432
20
+ POSTGRES_DATABASE = "myapp"
21
+ POSTGRES_USER = "app"
22
+ POSTGRES_PASSWORD = "secret"
23
+
24
+ # After
25
+ POSTGRES_URL = "postgresql://app:secret@localhost:5432/myapp"
26
+ ```
27
+
28
+ Apps that already set `DATABASE_URL` in the environment don't need any change.
29
+
30
+ - **If `POSTGRES_OPTIONS` or `POSTGRES_TIME_ZONE` were set**, move them into the URL as query parameters (e.g. `?application_name=web&timezone=UTC`).
31
+ - **If you run behind a transaction-mode pooler**, consider setting `POSTGRES_MANAGEMENT_URL` to a direct-to-Postgres connection string so `plain migrations` and `plain postgres sync` can issue DDL.
32
+
3
33
  ## [0.96.0](https://github.com/dropseed/plain/releases/plain-postgres@0.96.0) (2026-04-17)
4
34
 
5
35
  ### What's changed
@@ -70,26 +70,63 @@ admin_users = User.query.filter(is_admin=True)
70
70
 
71
71
  ## Database connection
72
72
 
73
- To connect to a database, you can provide a `DATABASE_URL` environment variable:
73
+ Configure the database with a single URL. The canonical Plain setting is `POSTGRES_URL`:
74
+
75
+ ```python
76
+ # app/settings.py
77
+ POSTGRES_URL = "postgresql://user:password@localhost:5432/dbname"
78
+ ```
79
+
80
+ Or via environment variable:
81
+
82
+ ```sh
83
+ PLAIN_POSTGRES_URL=postgresql://user:password@localhost:5432/dbname
84
+ ```
85
+
86
+ Plain also reads the `DATABASE_URL` environment variable as a fallback — it's the widely-used convention for Postgres connection strings, so most hosting setups work without extra configuration:
74
87
 
75
88
  ```sh
76
89
  DATABASE_URL=postgresql://user:password@localhost:5432/dbname
77
90
  ```
78
91
 
79
- Or you can set the individual `POSTGRES_*` settings (via `PLAIN_POSTGRES_*` environment variables or in `app/settings.py`):
92
+ Precedence (highest to lowest): `PLAIN_POSTGRES_URL` `POSTGRES_URL` in `settings.py` `DATABASE_URL` environment variable.
80
93
 
81
- ```python
82
- # app/settings.py
83
- POSTGRES_HOST = "localhost"
84
- POSTGRES_PORT = 5432
85
- POSTGRES_DATABASE = "dbname"
86
- POSTGRES_USER = "user"
87
- POSTGRES_PASSWORD = "password"
94
+ The URL supports any libpq connection parameter as a query string — for example `?sslmode=require&application_name=web&connect_timeout=10`. These are parsed and passed through to the driver.
95
+
96
+ To explicitly disable the database (e.g. during Docker builds where no database is available), set the URL to the string `none`:
97
+
98
+ ```sh
99
+ PLAIN_POSTGRES_URL=none
88
100
  ```
89
101
 
90
- If `DATABASE_URL` is set, it takes priority and the individual connection settings are parsed from it.
102
+ ### Bypassing a connection pooler for management operations
103
+
104
+ Transaction-mode poolers (PlanetScale, Supabase's pooler, Neon's pooler, standalone pgbouncer in transaction mode) can't run DDL, long transactions, or `pg_dump`. To work around this, set a second URL that management commands use to reach Postgres directly:
105
+
106
+ ```sh
107
+ PLAIN_POSTGRES_URL=postgresql://app@pooler:6432/myapp
108
+ PLAIN_POSTGRES_MANAGEMENT_URL=postgresql://app@postgres:5432/myapp
109
+ ```
110
+
111
+ When `POSTGRES_MANAGEMENT_URL` is set, these commands connect through it instead of `POSTGRES_URL`:
112
+
113
+ - `plain migrations create`, `plain migrations apply`, `plain migrations list`, `plain migrations prune`, `plain migrations squash`
114
+ - `plain postgres sync`, `plain postgres converge`, `plain postgres schema`
115
+ - `plain postgres diagnose`, `plain postgres drop-unknown-tables`, `plain postgres shell`
116
+
117
+ When it's unset, all commands use `POSTGRES_URL` — there's no behavior change for existing apps.
118
+
119
+ To route custom code through the management connection, use the `use_management_connection()` context manager:
120
+
121
+ ```python
122
+ from plain.postgres import use_management_connection
123
+
124
+ with use_management_connection():
125
+ # Any get_connection() / ORM calls inside this block use POSTGRES_MANAGEMENT_URL.
126
+ run_custom_schema_change()
127
+ ```
91
128
 
92
- To explicitly disable the database (e.g. during Docker builds where no database is available), set `DATABASE_URL=none`.
129
+ You _can_ point the two URLs at different Postgres roles — e.g. a least-privilege DML role for runtime and a DDL-capable role for management. Plain does not currently automate the grant/ownership plumbing that split requires (default privileges for newly-created tables, ownership reassignment, preflight checks that the runtime role can see the schema). If you adopt that pattern, you're responsible for wiring those up yourself.
93
130
 
94
131
  **PostgreSQL is the only supported database.** You need to install a PostgreSQL driver separately — [psycopg](https://www.psycopg.org/) is recommended:
95
132
 
@@ -1194,27 +1231,18 @@ These are static, code-level checks that catch issues before you deploy. The `di
1194
1231
 
1195
1232
  ## Settings
1196
1233
 
1197
- Connection settings are configured via `DATABASE_URL` or individual `POSTGRES_*` settings.
1198
-
1199
- When `DATABASE_URL` is set, it is parsed into the individual connection settings automatically. When `DATABASE_URL` is not set, the connection settings are required individually.
1200
-
1201
- Set `DATABASE_URL=none` to explicitly disable the database (e.g. during Docker image builds).
1202
-
1203
- | Setting | Type | Default | Env var |
1204
- | ---------------------------------------- | ------------- | ------- | ---------------------------------------------- |
1205
- | `POSTGRES_HOST` | `str` | | `PLAIN_POSTGRES_HOST` |
1206
- | `POSTGRES_PORT` | `int \| None` | `None` | `PLAIN_POSTGRES_PORT` |
1207
- | `POSTGRES_DATABASE` | `str` | | `PLAIN_POSTGRES_DATABASE` |
1208
- | `POSTGRES_USER` | `str` | | `PLAIN_POSTGRES_USER` |
1209
- | `POSTGRES_PASSWORD` | `Secret[str]` | — | `PLAIN_POSTGRES_PASSWORD` |
1210
- | `POSTGRES_CONN_MAX_AGE` | `int` | `600` | `PLAIN_POSTGRES_CONN_MAX_AGE` |
1211
- | `POSTGRES_CONN_HEALTH_CHECKS` | `bool` | `True` | `PLAIN_POSTGRES_CONN_HEALTH_CHECKS` |
1212
- | `POSTGRES_OPTIONS` | `dict` | `{}` | — |
1213
- | `POSTGRES_TIME_ZONE` | `str \| None` | `None` | `PLAIN_POSTGRES_TIME_ZONE` |
1214
- | `POSTGRES_MIGRATION_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_LOCK_TIMEOUT` |
1215
- | `POSTGRES_MIGRATION_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT` |
1216
- | `POSTGRES_CONVERGENCE_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_LOCK_TIMEOUT` |
1217
- | `POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` |
1234
+ The connection is configured with a single URL (`POSTGRES_URL`). `DATABASE_URL` is read as a platform-compat fallback. Set the URL to `none` to explicitly disable the database (e.g. during Docker image builds).
1235
+
1236
+ | Setting | Type | Default | Env var |
1237
+ | ---------------------------------------- | ------------- | ----------------------- | ---------------------------------------------- |
1238
+ | `POSTGRES_URL` | `Secret[str]` | `$DATABASE_URL` or `""` | `PLAIN_POSTGRES_URL` |
1239
+ | `POSTGRES_MANAGEMENT_URL` | `Secret[str]` | `""` | `PLAIN_POSTGRES_MANAGEMENT_URL` |
1240
+ | `POSTGRES_CONN_MAX_AGE` | `int` | `600` | `PLAIN_POSTGRES_CONN_MAX_AGE` |
1241
+ | `POSTGRES_CONN_HEALTH_CHECKS` | `bool` | `True` | `PLAIN_POSTGRES_CONN_HEALTH_CHECKS` |
1242
+ | `POSTGRES_MIGRATION_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_LOCK_TIMEOUT` |
1243
+ | `POSTGRES_MIGRATION_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT` |
1244
+ | `POSTGRES_CONVERGENCE_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_LOCK_TIMEOUT` |
1245
+ | `POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` |
1218
1246
 
1219
1247
  See [`default_settings.py`](./default_settings.py) for more details.
1220
1248
 
@@ -6,7 +6,7 @@ from . import (
6
6
  # Imports that would create circular imports if sorted
7
7
  from .base import Model
8
8
  from .constraints import CheckConstraint, UniqueConstraint
9
- from .db import get_connection
9
+ from .db import get_connection, use_management_connection
10
10
  from .deletion import CASCADE, NO_ACTION, RESTRICT, SET_NULL
11
11
  from .expressions import F
12
12
  from .enums import TextChoices
@@ -104,6 +104,7 @@ __all__ = [
104
104
  "ReverseManyToMany",
105
105
  # From db
106
106
  "get_connection",
107
+ "use_management_connection",
107
108
  # From registry
108
109
  "register_model",
109
110
  "models_registry",
@@ -5,6 +5,7 @@ import sys
5
5
  import click
6
6
 
7
7
  from ..convergence import execute_plan, plan_convergence
8
+ from .decorators import database_management_command
8
9
 
9
10
 
10
11
  @click.command()
@@ -14,6 +15,7 @@ from ..convergence import execute_plan, plan_convergence
14
15
  is_flag=True,
15
16
  help="Skip confirmation prompt.",
16
17
  )
18
+ @database_management_command
17
19
  def converge(yes: bool) -> None:
18
20
  """Fix schema mismatches between models and the database.
19
21
 
@@ -13,6 +13,7 @@ from plain.cli import register_cli
13
13
  from ..db import get_connection
14
14
  from ..dialect import quote_name
15
15
  from .converge import converge
16
+ from .decorators import database_management_command
16
17
  from .diagnose import diagnose
17
18
  from .schema import schema
18
19
  from .sync import sync
@@ -32,6 +33,7 @@ cli.add_command(sync)
32
33
 
33
34
  @cli.command()
34
35
  @click.argument("parameters", nargs=-1)
36
+ @database_management_command
35
37
  def shell(parameters: tuple[str, ...]) -> None:
36
38
  """Open an interactive database shell"""
37
39
  conn = get_connection()
@@ -67,6 +69,7 @@ def shell(parameters: tuple[str, ...]) -> None:
67
69
  is_flag=True,
68
70
  help="Skip confirmation prompt.",
69
71
  )
72
+ @database_management_command
70
73
  def drop_unknown_tables(yes: bool) -> None:
71
74
  """Drop all tables not associated with a Plain model"""
72
75
  from ..introspection import get_unknown_tables
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
7
+ from ..db import use_management_connection
8
+
9
+
10
+ def database_management_command[F: Callable[..., Any]](f: F) -> F:
11
+ """Run a click command's body through `use_management_connection()`.
12
+
13
+ Apply to CLI commands that perform schema changes, migrations, or other
14
+ database management operations. Inside the command, `get_connection()`
15
+ returns a connection opened against `POSTGRES_MANAGEMENT_URL` (falling
16
+ back to `POSTGRES_URL` when unset).
17
+ """
18
+
19
+ @functools.wraps(f)
20
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
21
+ with use_management_connection():
22
+ return f(*args, **kwargs)
23
+
24
+ return wrapper # ty: ignore[invalid-return-type]
@@ -8,6 +8,7 @@ import click
8
8
 
9
9
  from ..db import get_connection
10
10
  from ..introspection import CheckItem, CheckResult, build_table_owners, run_all_checks
11
+ from .decorators import database_management_command
11
12
 
12
13
  STATUS_SYMBOLS = {
13
14
  "ok": ("✓", "green"),
@@ -182,6 +183,7 @@ def format_human(
182
183
  @click.option(
183
184
  "--all", "show_all", is_flag=True, help="Include package issues in detail"
184
185
  )
186
+ @database_management_command
185
187
  def diagnose(output_json: bool, show_all: bool) -> None:
186
188
  """Run health checks against the database"""
187
189
  conn = get_connection()
@@ -27,6 +27,7 @@ from ..migrations.recorder import MigrationRecorder
27
27
  from ..migrations.state import ModelState, ProjectState
28
28
  from ..migrations.writer import MigrationWriter
29
29
  from ..registry import models_registry
30
+ from .decorators import database_management_command
30
31
 
31
32
  if TYPE_CHECKING:
32
33
  from ..connection import DatabaseConnection
@@ -68,6 +69,7 @@ def cli() -> None:
68
69
  default=1,
69
70
  help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
70
71
  )
72
+ @database_management_command
71
73
  def create(
72
74
  package_labels: tuple[str, ...],
73
75
  dry_run: bool,
@@ -345,6 +347,7 @@ def create(
345
347
  is_flag=True,
346
348
  help="Suppress migration output (used for test database creation).",
347
349
  )
350
+ @database_management_command
348
351
  def apply(
349
352
  package_label: str | None,
350
353
  migration_name: str | None,
@@ -638,6 +641,7 @@ def apply(
638
641
  default=1,
639
642
  help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
640
643
  )
644
+ @database_management_command
641
645
  def list_migrations(
642
646
  package_labels: tuple[str, ...], format: str, verbosity: int
643
647
  ) -> None:
@@ -777,6 +781,7 @@ def list_migrations(
777
781
  is_flag=True,
778
782
  help="Skip confirmation prompt.",
779
783
  )
784
+ @database_management_command
780
785
  def prune(yes: bool) -> None:
781
786
  """Remove stale migration records from the database"""
782
787
  # Load migrations from disk and database
@@ -899,6 +904,7 @@ def prune(yes: bool) -> None:
899
904
  default=1,
900
905
  help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
901
906
  )
907
+ @database_management_command
902
908
  def squash(
903
909
  package_label: str,
904
910
  start_migration_name: str | None,
@@ -10,6 +10,7 @@ from ..convergence.planning import can_auto_fix
10
10
  from ..db import get_connection
11
11
  from ..introspection import MANAGED_CONSTRAINT_TYPES, get_unknown_tables
12
12
  from ..registry import models_registry
13
+ from .decorators import database_management_command
13
14
 
14
15
 
15
16
  def _ok() -> None:
@@ -108,6 +109,7 @@ def _render_model(analysis: ModelAnalysis) -> None:
108
109
  @click.command()
109
110
  @click.argument("model_label", required=False)
110
111
  @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
112
+ @database_management_command
111
113
  def schema(model_label: str | None, output_json: bool) -> None:
112
114
  """Show database schema from models, compared against the actual database"""
113
115
  models = models_registry.get_models()
@@ -7,6 +7,7 @@ import click
7
7
  from plain.runtime import settings
8
8
 
9
9
  from ..convergence import execute_plan, plan_convergence
10
+ from .decorators import database_management_command
10
11
 
11
12
 
12
13
  @click.command()
@@ -15,6 +16,7 @@ from ..convergence import execute_plan, plan_convergence
15
16
  is_flag=True,
16
17
  help="Exit with non-zero status if sync would make any database changes.",
17
18
  )
19
+ @database_management_command
18
20
  def sync(check: bool) -> None:
19
21
  """Sync the database schema with models.
20
22