plain.postgres 0.113.2__tar.gz → 0.115.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 (233) hide show
  1. plain_postgres-0.113.2/plain/postgres/README.md → plain_postgres-0.115.0/PKG-INFO +75 -24
  2. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/CHANGELOG.md +45 -1
  3. plain_postgres-0.113.2/PKG-INFO → plain_postgres-0.115.0/plain/postgres/README.md +62 -37
  4. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/__init__.py +22 -41
  5. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +5 -2
  6. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/base.py +19 -21
  7. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/cli/converge.py +2 -3
  8. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/cli/core.py +4 -4
  9. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/cli/migrations.py +2 -4
  10. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/cli/sync.py +0 -1
  11. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/connection.py +12 -22
  12. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/constraints.py +13 -44
  13. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/convergence/analysis.py +146 -98
  14. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/convergence/corrections.py +65 -26
  15. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/convergence/planning.py +8 -0
  16. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/databases.py +3 -5
  17. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/ddl.py +0 -17
  18. plain_postgres-0.115.0/plain/postgres/deletion.py +32 -0
  19. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/dialect.py +3 -7
  20. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/exceptions.py +0 -12
  21. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/expressions.py +23 -32
  22. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/base.py +8 -5
  23. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/binary.py +1 -2
  24. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/duration.py +2 -1
  25. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/encrypted.py +3 -2
  26. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/json.py +2 -1
  27. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/network.py +2 -1
  28. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/primary_key.py +2 -1
  29. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/related.py +42 -8
  30. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/related_descriptors.py +5 -6
  31. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/reverse_descriptors.py +5 -3
  32. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/temporal.py +5 -3
  33. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/text.py +4 -3
  34. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/timezones.py +2 -2
  35. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/forms.py +13 -18
  36. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/functions/__init__.py +63 -70
  37. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/functions/datetime.py +5 -5
  38. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/functions/math.py +1 -4
  39. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/functions/mixins.py +2 -3
  40. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/functions/text.py +11 -8
  41. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/indexes.py +9 -9
  42. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/introspection/health/context.py +1 -1
  43. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/introspection/health/runner.py +24 -14
  44. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/introspection/schema.py +2 -0
  45. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/lookups.py +7 -5
  46. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/meta.py +29 -11
  47. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/__init__.py +9 -14
  48. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/autodetector.py +66 -71
  49. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/exceptions.py +0 -12
  50. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/executor.py +1 -1
  51. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/graph.py +3 -2
  52. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/loader.py +4 -5
  53. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/migration.py +13 -13
  54. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/operations/base.py +3 -3
  55. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/operations/fields.py +2 -3
  56. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/operations/models.py +5 -6
  57. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/operations/special.py +2 -2
  58. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/questioner.py +1 -2
  59. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/serializer.py +8 -8
  60. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/state.py +10 -12
  61. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/utils.py +10 -11
  62. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/writer.py +5 -5
  63. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/otel.py +2 -6
  64. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/preflight/database.py +1 -1
  65. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/query.py +17 -21
  66. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/query_utils.py +7 -10
  67. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/registry.py +14 -14
  68. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/schema.py +8 -1
  69. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/schema_lock.py +0 -1
  70. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/sources.py +1 -2
  71. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/sql/__init__.py +11 -12
  72. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/sql/compiler.py +10 -12
  73. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/sql/query.py +13 -13
  74. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/test/database.py +1 -2
  75. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/test/pytest.py +0 -13
  76. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/transaction.py +2 -4
  77. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/utils.py +9 -1
  78. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/pyproject.toml +2 -2
  79. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0001_initial.py +5 -4
  80. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0002_test_field_removed.py +5 -6
  81. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +5 -19
  82. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +6 -6
  83. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +5 -6
  84. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0006_secretstore.py +5 -6
  85. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +5 -6
  86. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +5 -6
  87. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +6 -5
  88. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0010_hideableitem.py +5 -6
  89. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0011_defaultsexample.py +5 -6
  90. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0012_iterationexample.py +5 -6
  91. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +5 -6
  92. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +6 -5
  93. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +6 -5
  94. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0016_formsexample.py +5 -6
  95. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0017_random_string_token.py +5 -6
  96. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/0018_storageparametersexample.py +5 -6
  97. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/constraints.py +2 -1
  98. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/defaults.py +2 -1
  99. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/delete.py +4 -10
  100. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/encrypted.py +2 -1
  101. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/forms.py +2 -1
  102. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/indexes.py +2 -1
  103. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/iteration.py +2 -1
  104. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/mixins.py +2 -1
  105. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/nullability.py +2 -1
  106. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/querysets.py +2 -1
  107. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/relationships.py +2 -1
  108. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/storage_parameters.py +2 -1
  109. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/trees.py +2 -1
  110. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/urls.py +2 -2
  111. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/urls.py +2 -3
  112. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/conftest.py +0 -1
  113. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_autodetector_not_null_errors.py +0 -1
  114. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_autodetector_type_change.py +0 -1
  115. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_connection_isolation.py +0 -3
  116. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_connection_lifecycle.py +3 -4
  117. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_connection_pool.py +1 -2
  118. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_connection_self_heal.py +7 -10
  119. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_constraint_violation_error.py +4 -5
  120. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_convergence.py +0 -1
  121. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_convergence_constraints.py +20 -81
  122. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_convergence_defaults.py +0 -1
  123. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_convergence_fk.py +223 -72
  124. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_convergence_indexes.py +7 -6
  125. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_convergence_nullability.py +0 -1
  126. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_convergence_storage_parameters.py +0 -1
  127. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_convergence_timeouts.py +1 -1
  128. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_db_expression_defaults.py +12 -6
  129. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_diagnose.py +0 -1
  130. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_executor_connection_hook.py +0 -1
  131. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_fk_characterization.py +0 -1
  132. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_health.py +0 -1
  133. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_literal_default_persistence.py +1 -5
  134. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_management_connection.py +2 -4
  135. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_migration_executor.py +34 -2
  136. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_no_callable_defaults.py +0 -1
  137. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_otel_metrics.py +3 -4
  138. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_rollback_exc_attribution.py +6 -10
  139. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_schema_lock.py +15 -14
  140. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_schema_normalize_type.py +0 -1
  141. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_schema_timeouts.py +4 -13
  142. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_create_update.py +9 -10
  143. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_databases.py +0 -1
  144. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_delete_behaviors.py +25 -71
  145. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_encrypted_fields.py +0 -1
  146. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_exceptions.py +0 -1
  147. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_field_defaults.py +8 -7
  148. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_integrity_error_mapping.py +33 -9
  149. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_m2m.py +2 -4
  150. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_manager_assignment.py +0 -1
  151. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_modelform_roundtrip.py +0 -1
  152. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_only_empty_defaults.py +0 -1
  153. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_order_by_expressions.py +0 -1
  154. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_queryset_slicing.py +0 -1
  155. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_random_string_field.py +0 -1
  156. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_read_only_transactions.py +20 -20
  157. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_related.py +1 -2
  158. plain_postgres-0.113.2/plain/postgres/deletion.py +0 -48
  159. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/.gitignore +0 -0
  160. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/CLAUDE.md +0 -0
  161. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/LICENSE +0 -0
  162. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/README.md +0 -0
  163. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/adapters.py +0 -0
  164. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  165. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/aggregates.py +0 -0
  166. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/cli/__init__.py +0 -0
  167. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/cli/decorators.py +0 -0
  168. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/cli/diagnose.py +0 -0
  169. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/cli/schema.py +0 -0
  170. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/config.py +0 -0
  171. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/constants.py +0 -0
  172. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/convergence/__init__.py +0 -0
  173. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/database_url.py +0 -0
  174. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/db.py +0 -0
  175. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/default_settings.py +0 -0
  176. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/entrypoints.py +0 -0
  177. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/enums.py +0 -0
  178. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/__init__.py +2 -2
  179. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/boolean.py +0 -0
  180. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/mixins.py +0 -0
  181. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/numeric.py +1 -1
  182. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/related_lookups.py +0 -0
  183. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/related_managers.py +0 -0
  184. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/reverse_related.py +0 -0
  185. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/fields/uuid.py +0 -0
  186. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/functions/comparison.py +0 -0
  187. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/functions/random.py +0 -0
  188. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/functions/uuid.py +0 -0
  189. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/functions/window.py +0 -0
  190. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/introspection/__init__.py +4 -4
  191. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/introspection/health/__init__.py +0 -0
  192. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
  193. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  194. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
  195. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/introspection/health/helpers.py +0 -0
  196. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/introspection/health/ownership.py +0 -0
  197. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/introspection/health/types.py +0 -0
  198. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/middleware.py +0 -0
  199. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/operations/__init__.py +7 -7
  200. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/optimizer.py +0 -0
  201. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/migrations/recorder.py +2 -2
  202. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/options.py +0 -0
  203. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/preflight/__init__.py +0 -0
  204. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/preflight/indexes.py +0 -0
  205. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/preflight/models.py +0 -0
  206. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/sql/constants.py +0 -0
  207. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/sql/datastructures.py +0 -0
  208. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/sql/where.py +0 -0
  209. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/test/__init__.py +0 -0
  210. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/types.py +2 -2
  211. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/plain/postgres/types.pyi +0 -0
  212. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/forms.py +0 -0
  213. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/migrations/__init__.py +0 -0
  214. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/__init__.py +0 -0
  215. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/models/unregistered.py +0 -0
  216. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/examples/views.py +0 -0
  217. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/app/settings.py +0 -0
  218. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/conftest_convergence.py +0 -0
  219. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_apply_replan.py +0 -0
  220. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_databases_not_on_runtime_path.py +0 -0
  221. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_introspection.py +0 -0
  222. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
  223. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
  224. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
  225. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_database_url.py +0 -0
  226. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_deferred_loading.py +0 -0
  227. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_functions_uuid.py +0 -0
  228. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_iterator.py +0 -0
  229. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_manual_pk.py +0 -0
  230. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_mixins.py +0 -0
  231. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_queryset_repr.py +0 -0
  232. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_raw_query.py +0 -0
  233. {plain_postgres-0.113.2 → plain_postgres-0.115.0}/tests/public/test_related_instance_filter.py +0 -0
@@ -1,3 +1,16 @@
1
+ Metadata-Version: 2.5
2
+ Name: plain.postgres
3
+ Version: 0.115.0
4
+ Summary: Model your data and store it in a database.
5
+ Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
+ License-Expression: BSD-3-Clause
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.13
9
+ Requires-Dist: plain<1.0.0,>=0.157.0
10
+ Requires-Dist: psycopg-pool>=3.2
11
+ Requires-Dist: psycopg>=3.2
12
+ Description-Content-Type: text/markdown
13
+
1
14
  # plain.postgres
2
15
 
3
16
  **Model your data and store it in a database.**
@@ -166,9 +179,8 @@ user = User.query.get(email="test@example.com")
166
179
 
167
180
  # Complex queries with Q objects
168
181
  from plain.postgres import Q
169
- users = User.query.filter(
170
- Q(is_admin=True) | Q(email__endswith="@example.com")
171
- )
182
+
183
+ users = User.query.filter(Q(is_admin=True) | Q(email__endswith="@example.com"))
172
184
 
173
185
  # Ordering
174
186
  users = User.query.order_by("-created_at")
@@ -187,6 +199,7 @@ You can customize [`QuerySet`](./query.py#QuerySet) classes to provide specializ
187
199
  from typing import Self
188
200
  from plain.postgres import types
189
201
 
202
+
190
203
  class PublishedQuerySet(postgres.QuerySet["Article"]):
191
204
  def published_only(self) -> Self:
192
205
  return self.filter(status="published")
@@ -194,6 +207,7 @@ class PublishedQuerySet(postgres.QuerySet["Article"]):
194
207
  def draft_only(self) -> Self:
195
208
  return self.filter(status="draft")
196
209
 
210
+
197
211
  @postgres.register_model
198
212
  class Article(postgres.Model):
199
213
  title: str = types.TextField(max_length=200)
@@ -201,6 +215,7 @@ class Article(postgres.Model):
201
215
 
202
216
  query = PublishedQuerySet()
203
217
 
218
+
204
219
  # Usage - all methods available on Article.query
205
220
  all_articles = Article.query.all()
206
221
  published_articles = Article.query.published_only()
@@ -226,6 +241,7 @@ from __future__ import annotations
226
241
  from plain import postgres
227
242
  from plain.postgres import types
228
243
 
244
+
229
245
  @postgres.register_model
230
246
  class User(postgres.Model):
231
247
  email: str = types.EmailField()
@@ -243,11 +259,14 @@ For complex queries that can't be expressed with the ORM, you can use raw SQL.
243
259
  Use `Model.query.raw()` to execute raw SQL and get model instances back:
244
260
 
245
261
  ```python
246
- users = User.query.raw("""
262
+ users = User.query.raw(
263
+ """
247
264
  SELECT * FROM users
248
265
  WHERE created_at > %s
249
266
  ORDER BY created_at DESC
250
- """, [some_date])
267
+ """,
268
+ [some_date],
269
+ )
251
270
 
252
271
  for user in users:
253
272
  print(user.email) # Full model instance with all fields
@@ -320,6 +339,7 @@ for category in Category.query.all():
320
339
 
321
340
  # Good — single query with annotation
322
341
  from plain.postgres.aggregates import Count
342
+
323
343
  for category in Category.query.annotate(num_products=Count("products")).all():
324
344
  print(category.num_products)
325
345
  ```
@@ -333,6 +353,7 @@ Templates should only render data, never trigger queries. Prepare everything in
333
353
  def get_template_context(self):
334
354
  return {"posts": Post.query.all()} # related lookups happen in template
335
355
 
356
+
336
357
  # Good — eagerly load everything
337
358
  def get_template_context(self):
338
359
  return {"posts": Post.query.select_related("author").prefetch_related("tags").all()}
@@ -356,10 +377,12 @@ emails = list(User.query.values_list("email", flat=True))
356
377
 
357
378
  ```python
358
379
  # Bad
359
- if User.query.filter(is_active=True).count() > 0: ...
380
+ if User.query.filter(is_active=True).count() > 0:
381
+ ...
360
382
 
361
383
  # Good
362
- if User.query.filter(is_active=True).exists(): ...
384
+ if User.query.filter(is_active=True).exists():
385
+ ...
363
386
  ```
364
387
 
365
388
  #### Use `.count()` instead of `len(queryset)`
@@ -466,8 +489,8 @@ Run a block of code in a read-only transaction using `read_only()`. Any write (I
466
489
  from plain.postgres.db import read_only
467
490
 
468
491
  with read_only():
469
- users = User.query.all() # reads work
470
- User.query.create(name="x") # raises psycopg.errors.ReadOnlySqlTransaction
492
+ users = User.query.all() # reads work
493
+ User.query.create(name="x") # raises psycopg.errors.ReadOnlySqlTransaction
471
494
  ```
472
495
 
473
496
  `read_only()` opens a single `BEGIN READ ONLY` transaction for the block. Nested `atomic()` blocks inside become savepoints of the outer read-only transaction and inherit read-only.
@@ -480,10 +503,10 @@ Because the whole block is one transaction, catching a database error inside `re
480
503
  with read_only():
481
504
  try:
482
505
  with atomic():
483
- User.query.create(name="x") # raises, savepoint rolls back
506
+ User.query.create(name="x") # raises, savepoint rolls back
484
507
  except psycopg.errors.ReadOnlySqlTransaction:
485
508
  pass
486
- User.query.count() # still works — outer txn is healthy
509
+ User.query.count() # still works — outer txn is healthy
487
510
  ```
488
511
 
489
512
  ## Schema management
@@ -705,7 +728,9 @@ class User(postgres.Model):
705
728
  ],
706
729
  constraints=[
707
730
  postgres.UniqueConstraint(fields=["email"], name="users_email_uniq"),
708
- postgres.CheckConstraint(check=postgres.Q(age__gte=0), name="users_age_positive"),
731
+ postgres.CheckConstraint(
732
+ check=postgres.Q(age__gte=0), name="users_age_positive"
733
+ ),
709
734
  ],
710
735
  )
711
736
  ```
@@ -806,6 +831,7 @@ from datetime import datetime
806
831
  from plain import postgres
807
832
  from plain.postgres import types
808
833
 
834
+
809
835
  class Product(postgres.Model):
810
836
  # Text fields
811
837
  name: str = types.TextField(max_length=200)
@@ -910,6 +936,7 @@ This is **not** for passwords or tokens you issue — those should be hashed (on
910
936
  from plain import postgres
911
937
  from plain.postgres import types
912
938
 
939
+
913
940
  @postgres.register_model
914
941
  class Integration(postgres.Model):
915
942
  name: str = types.TextField(max_length=100)
@@ -946,6 +973,7 @@ Use [`ForeignKeyField`](./fields/related.py#ForeignKeyField) for many-to-one and
946
973
  from plain import postgres
947
974
  from plain.postgres import types
948
975
 
976
+
949
977
  @postgres.register_model
950
978
  class Book(postgres.Model):
951
979
  title: str = types.TextField(max_length=200)
@@ -959,15 +987,23 @@ Accessing a foreign key gives you the related object without a query — only it
959
987
 
960
988
  ```python
961
989
  book = Book.query.get(id=1)
962
- book.author # no query — a partial Author instance
963
- book.author.id # no query — the foreign key value
964
- book.author.name # one query — loads the rest of the row
990
+ book.author # no query — a partial Author instance
991
+ book.author.id # no query — the foreign key value
992
+ book.author.name # one query — loads the rest of the row
965
993
  ```
966
994
 
967
995
  The first access to any non-key field loads the whole row in a single query. There is no separate `author_id` attribute — `book.author.id` is the foreign key value, and it is type-checked because `book.author` is an `Author`. In loops, use `select_related()` to load related rows up front and avoid a query per row.
968
996
 
969
997
  The partial-instance shortcut is safe because Plain always creates a database foreign-key constraint, so the referenced row is guaranteed to exist.
970
998
 
999
+ ### Constraints are checked immediately
1000
+
1001
+ Every constraint is checked at the write that violates it, never at commit — the same as Postgres's own default. Inserting a child row that points at a parent that doesn't exist yet fails at that `INSERT`, with a traceback pointing at the offending write, and deleting a parent with `RESTRICT` children fails at that `DELETE`. Create parents before children. A cycle of foreign keys needs a nullable back-reference: create both rows, then `update()` the back-reference. Two required foreign keys pointing at each other can never be inserted.
1002
+
1003
+ To swap two rows' values under a unique constraint (reordering by `position`, say), move one row to a temporary value first — a single `UPDATE` that swaps them fails, because Postgres checks uniqueness per row.
1004
+
1005
+ A migration can add a column, backfill it in `RunPython`, and drop or alter columns on the same table, all in one transaction.
1006
+
971
1007
  ### Reverse relationships
972
1008
 
973
1009
  When you define a `ForeignKey` or `ManyToManyField`, Plain automatically creates a reverse accessor on the related model (like `author.book_set`). You can explicitly declare these reverse relationships using [`ReverseForeignKey`](./fields/reverse_descriptors.py#ReverseForeignKey) and [`ReverseManyToMany`](./fields/reverse_descriptors.py#ReverseManyToMany):
@@ -976,17 +1012,20 @@ When you define a `ForeignKey` or `ManyToManyField`, Plain automatically creates
976
1012
  from plain import postgres
977
1013
  from plain.postgres import types
978
1014
 
1015
+
979
1016
  @postgres.register_model
980
1017
  class Author(postgres.Model):
981
1018
  name: str = types.TextField(max_length=200)
982
1019
  # Explicit reverse accessor for all books by this author
983
1020
  books = types.ReverseForeignKey(to="Book", field="author")
984
1021
 
1022
+
985
1023
  @postgres.register_model
986
1024
  class Book(postgres.Model):
987
1025
  title: str = types.TextField(max_length=200)
988
1026
  author: Author = types.ForeignKeyField(Author, on_delete=postgres.CASCADE)
989
1027
 
1028
+
990
1029
  # Usage
991
1030
  author = Author.query.get(name="Jane Doe")
992
1031
  for book in author.books.all():
@@ -1005,11 +1044,13 @@ class Feature(postgres.Model):
1005
1044
  # Explicit reverse accessor for all cars with this feature
1006
1045
  cars = types.ReverseManyToMany(to="Car", field="features")
1007
1046
 
1047
+
1008
1048
  @postgres.register_model
1009
1049
  class Car(postgres.Model):
1010
1050
  model: str = types.TextField(max_length=100)
1011
1051
  features = types.ManyToManyField(Feature)
1012
1052
 
1053
+
1013
1054
  # Usage
1014
1055
  feature = Feature.query.get(name="Sunroof")
1015
1056
  for car in feature.cars.all():
@@ -1029,10 +1070,14 @@ To get type checking for custom QuerySet methods on reverse relations, specify t
1029
1070
 
1030
1071
  ```python
1031
1072
  # Basic usage
1032
- books: types.ReverseForeignKey[Book] = types.ReverseForeignKey(to="Book", field="author")
1073
+ books: types.ReverseForeignKey[Book] = types.ReverseForeignKey(
1074
+ to="Book", field="author"
1075
+ )
1033
1076
 
1034
1077
  # With custom QuerySet for proper method recognition
1035
- books: types.ReverseForeignKey[Book, BookQuerySet] = types.ReverseForeignKey(to="Book", field="author")
1078
+ books: types.ReverseForeignKey[Book, BookQuerySet] = types.ReverseForeignKey(
1079
+ to="Book", field="author"
1080
+ )
1036
1081
 
1037
1082
  # Now type checkers recognize custom methods like .published()
1038
1083
  author.books.query.published()
@@ -1063,7 +1108,7 @@ class User(postgres.Model):
1063
1108
 
1064
1109
  Field-level validation happens automatically based on field types and constraints.
1065
1110
 
1066
- **The database is authoritative for constraints.** `create()`/`update()` don't pre-check your declared unique/check constraints — they attempt the write, and if Postgres rejects it, translate the `IntegrityError` into a `ValidationError` (routed to the field for single-column uniques, `NON_FIELD_ERRORS` otherwise). You get the same field-level error you'd expect, the write costs no per-constraint `SELECT`, and a raced concurrent insert can't slip through as a 500. (FK violations, `NOT NULL`, and a hand-set primary-key collision have no declared constraint to map to and re-raise as the original `IntegrityError`. `create()` always inserts, so passing a stray `id` that already exists is rejected by Postgres as the original `IntegrityError`.)
1111
+ **The database is authoritative for constraints.** `create()`/`update()` don't pre-check your declared unique/check constraints — they attempt the write, and if Postgres rejects it, translate the `IntegrityError` into a `ValidationError` (routed to the field for single-column uniques, `NON_FIELD_ERRORS` otherwise). You get the same field-level error you'd expect, the write costs no per-constraint `SELECT`, and a raced concurrent insert can't slip through as a 500. A foreign key pointing at a row that doesn't exist maps the same way, as an error on that field. (`NOT NULL` and a hand-set primary-key collision have no declared constraint to map to and re-raise as the original `IntegrityError`. `create()` always inserts, so passing a stray `id` that already exists is rejected by Postgres as the original `IntegrityError`.)
1067
1112
 
1068
1113
  Because the rejected write reaches the database, it aborts the surrounding transaction. If you catch the `ValidationError` and want to keep using the transaction, wrap the write in `transaction.atomic()` so it rolls back to a savepoint:
1069
1114
 
@@ -1077,7 +1122,7 @@ except ValidationError:
1077
1122
 
1078
1123
  Forms are the exception: a `ModelForm` pre-checks constraints explicitly (a `validate_constraints()` call in its `_post_clean`) so it can surface every violation at once, then writes via `form.create()`/`form.update()` with validation already done. A direct `create()`/`update()` reports the first violation Postgres hits.
1079
1124
 
1080
- This applies to instance writes only. Set-based writes — `QuerySet.update()` and `bulk_create()` — raise the raw `psycopg.IntegrityError`, since there's no instance to attribute the error to. If you retry on a unique conflict, catch both:
1125
+ This applies to instance writes only. Set-based writes — `QuerySet.update()` and `bulk_create()` — raise the raw `psycopg.IntegrityError`, since there's no instance to attribute the error to, and so does a `delete()` blocked by `RESTRICT` children. If you retry on a unique conflict, catch both:
1081
1126
 
1082
1127
  ```python
1083
1128
  try:
@@ -1088,8 +1133,6 @@ except (psycopg.IntegrityError, ValidationError):
1088
1133
 
1089
1134
  For a plain insert-or-update with no per-row logic, `bulk_create(..., update_conflicts=True, unique_fields=[...])` is an atomic upsert with no race to catch.
1090
1135
 
1091
- Two caveats. The mapping covers **immediate** constraints — the default. An explicitly deferred constraint (`UniqueConstraint(deferrable=Deferrable.DEFERRED)`) is checked at commit, _after_ the write returns, so its violation still surfaces as a raw `psycopg.IntegrityError`. And when a row violates several constraints at once, a form's pre-check (or an explicit `validate_constraints()`) reports them all, while a direct `create()`/`update()` gets only the first one the database hits.
1092
-
1093
1136
  ### Indexes and constraints
1094
1137
 
1095
1138
  You can optimize queries and ensure data integrity with indexes and constraints. These are managed automatically by [convergence](#convergence) — just declare them on the model and run `postgres sync`.
@@ -1176,6 +1219,7 @@ class Order(postgres.Model):
1176
1219
  status: str = types.TextField(max_length=20)
1177
1220
  created_at: datetime = types.DateTimeField()
1178
1221
 
1222
+
1179
1223
  # Good — indexed for common queries
1180
1224
  class Order(postgres.Model):
1181
1225
  status: str = types.TextField(max_length=20)
@@ -1197,6 +1241,7 @@ def create(self):
1197
1241
  raise ValueError("duplicate")
1198
1242
  return super().create()
1199
1243
 
1244
+
1200
1245
  # Good — database-enforced
1201
1246
  model_options = postgres.Options(
1202
1247
  constraints=[postgres.UniqueConstraint(fields=["email"])],
@@ -1209,7 +1254,9 @@ CASCADE for owned children, RESTRICT for referenced data, SET_NULL for optional
1209
1254
 
1210
1255
  ```python
1211
1256
  # Bad — blindly using CASCADE everywhere
1212
- company: Company = types.ForeignKeyField("Company", on_delete=postgres.CASCADE) # deleting company deletes invoices!
1257
+ company: Company = types.ForeignKeyField(
1258
+ "Company", on_delete=postgres.CASCADE
1259
+ ) # deleting company deletes invoices!
1213
1260
 
1214
1261
  # Good — block the delete while invoices reference the company
1215
1262
  company: Company = types.ForeignKeyField("Company", on_delete=postgres.RESTRICT)
@@ -1235,10 +1282,12 @@ Models integrate with [plain.forms](../../../plain-forms/plain/forms/README.md):
1235
1282
  from plain import forms
1236
1283
  from .models import User
1237
1284
 
1285
+
1238
1286
  class UserForm(forms.ModelForm):
1239
1287
  class Meta:
1240
1288
  model = User
1241
- fields = ["email", "is_admin"]
1289
+ fields = ("email", "is_admin")
1290
+
1242
1291
 
1243
1292
  # Usage
1244
1293
  form = UserForm(request=request)
@@ -1499,7 +1548,9 @@ Use `UniqueConstraint` in your model's `model_options`:
1499
1548
  ```python
1500
1549
  model_options = postgres.Options(
1501
1550
  constraints=[
1502
- postgres.UniqueConstraint(fields=["email", "organization"], name="unique_email_per_org"),
1551
+ postgres.UniqueConstraint(
1552
+ fields=["email", "organization"], name="unique_email_per_org"
1553
+ ),
1503
1554
  ],
1504
1555
  )
1505
1556
  ```
@@ -1,5 +1,45 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.115.0](https://github.com/dropseed/plain/releases/plain-postgres@0.115.0) (2026-09-02)
4
+
5
+ ### What's changed
6
+
7
+ - **Foreign keys are now `NOT DEFERRABLE`** (Postgres's own default) and checked at each write. Plain inherited Django's `DEFERRABLE INITIALLY DEFERRED` foreign keys, added in 2007 so fixtures could contain forward references. Plain has no fixtures, and the deferral was causing real problems: a migration that adds a column, backfills it in `RunPython`, then alters the same table failed with "cannot ALTER TABLE ... because it has pending trigger events" whenever the backfill touched a row, and FK violations only surfaced at `COMMIT` with a traceback pointing at `atomic.__exit__` instead of the offending write ([8cbfa0cb40](https://github.com/dropseed/plain/commit/8cbfa0cb40))
8
+ - Convergence detects an existing `DEFERRABLE` foreign key as drift and fixes it with a catalog-only `ALTER CONSTRAINT ... NOT DEFERRABLE` — no revalidation scan, just a brief `ACCESS EXCLUSIVE` lock bounded by the usual `lock_timeout` — so the first `postgres sync` after upgrading rewrites every FK quickly. A legacy `DEFERRABLE` + `NOT VALID` foreign key gets both corrections in one pass ([8cbfa0cb40](https://github.com/dropseed/plain/commit/8cbfa0cb40))
9
+ - `NO_ACTION` is removed. Its only meaning was deferred checking — use `RESTRICT` ([8cbfa0cb40](https://github.com/dropseed/plain/commit/8cbfa0cb40))
10
+ - A foreign key violation from `create()`/`update()` now surfaces as a `ValidationError` routed to the `ForeignKeyField` — "Author with id 5 does not exist." — with code `invalid_choice` so forms can override it. It lands in the same `IntegrityError` mapping that unique and check constraints already used, so a missing related row reports like any other field error instead of a 500. The message is `ForeignKeyField.does_not_exist_error_message`. Set-based writes (`QuerySet.update()`, `bulk_create()`) and a `delete()` blocked by `RESTRICT` still raise the raw `psycopg` error, since there's no instance to attribute it to ([31df77e665](https://github.com/dropseed/plain/commit/31df77e665))
11
+ - That mapping keys on the foreign key's generated constraint name, so convergence now treats a stale name — left behind by `RenameField`/`RenameModel`, which only rename the column or table — as drift and renames the constraint. Because the write-path mapping depends on those corrections having run, FK renames and the `NOT DEFERRABLE` flip block sync on failure. `generate_fk_constraint_name()` moved to `plain.postgres.utils` and is now the single source of that name for both convergence and the write path ([31df77e665](https://github.com/dropseed/plain/commit/31df77e665))
12
+ - **`UniqueConstraint(deferrable=...)` and the `Deferrable` enum are removed.** With foreign keys immediate, this was the last deferred-constraint concept in Plain, nothing in Plain's own packages used it, and it forced a caveat onto every "constraints are checked at the write" statement while keeping the pending-trigger-events failure alive for one case. Rails (pre-7.1), Ecto, and Laravel never had it for uniques. Every constraint is now checked at the write that violates it, never at commit ([fd1e7f1e13](https://github.com/dropseed/plain/commit/fd1e7f1e13))
13
+ - `DatabaseConnection.check_constraints()` is gone, and the pytest `db` fixture no longer forces deferred constraints to immediate at teardown — there is nothing left to defer ([fd1e7f1e13](https://github.com/dropseed/plain/commit/fd1e7f1e13))
14
+ - Constraint renames now run in convergence pass 1, matching index renames, so anything else planned for the same constraint in that pass addresses it by its new name ([fd1e7f1e13](https://github.com/dropseed/plain/commit/fd1e7f1e13))
15
+ - New README section, "Constraints are checked immediately", covering the patterns this changes: create parents before children, use a nullable back-reference for a foreign key cycle (two required FKs pointing at each other can never be inserted), and move through a temporary value when swapping two rows under a unique constraint, since Postgres checks uniqueness per row even within a single `UPDATE` ([8cbfa0cb40](https://github.com/dropseed/plain/commit/8cbfa0cb40), [fd1e7f1e13](https://github.com/dropseed/plain/commit/fd1e7f1e13))
16
+
17
+ ### Upgrade instructions
18
+
19
+ - Replace `NO_ACTION` with `RESTRICT` in your models **and in any historical migration file that references it** — `postgres.NO_ACTION` no longer exists, so a migration importing it fails to load.
20
+ - Remove `deferrable=` from every `UniqueConstraint(...)` and drop any `Deferrable` import. An existing deferred unique constraint in the database shows as CHANGED drift with staged-rollout guidance — nothing is rewritten silently.
21
+ - Run `plain postgres sync` after upgrading. It flips every existing foreign key to `NOT DEFERRABLE` (catalog-only, no table scan) and renames foreign key constraints left stale by past `RenameField`/`RenameModel`.
22
+ - Code that inserted a child row before its parent, or deleted a parent and re-pointed its children later in the same transaction, now raises `ForeignKeyViolation` at that statement rather than succeeding by commit time. Create parents before children; for a foreign key cycle, create both rows and then `update()` the nullable back-reference.
23
+ - If you catch `psycopg.IntegrityError` around `create()`/`update()` to handle a missing related row, catch `ValidationError` instead — or both, if the same block also covers set-based writes.
24
+ - If you called `connection.check_constraints()`, remove the call.
25
+
26
+ ## [0.114.0](https://github.com/dropseed/plain/releases/plain-postgres@0.114.0) (2026-08-12)
27
+
28
+ ### What's changed
29
+
30
+ - Wrong-type arguments now raise `TypeError` instead of `ValueError` across the schema and query APIs: `UniqueConstraint`/`Index` options (`condition`, `deferrable`, `include`, `opclasses`, `fields`), `Extract`/`Trunc` field types, non-bool `isnull` lookups, `RunPython` without a callable, serializer registration, assigning a wrong-model instance (or bool) to a foreign key, and reverse descriptors on non-FK/M2M fields ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
31
+ - `Migration.operations`/`dependencies`/`replaces` are typed as `Sequence`, and `plain migrations create` now writes them as tuples in generated files. Existing list-based migration files keep working ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
32
+ - Migration filename timestamps use the app's `TIME_ZONE` (via `timezone.localtime()`) instead of the machine's local clock, so a team generates consistently named migrations ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
33
+ - Model field `default_validators` is now a tuple and `empty_values` a tuple; `ignored_tables` on the connection is a tuple ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
34
+ - `ModelsRegistry.get_models()` caching was reworked to a per-instance memo (still cleared by `clear_cache()`), replacing a `functools.cache` keyed on the registry instance ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
35
+ - `on_commit()` handler errors are logged with `logger.exception()` — same message and traceback, standard idiom ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
36
+
37
+ ### Upgrade instructions
38
+
39
+ - If you catch `ValueError` around any of the argument-validation cases above, catch `TypeError` instead
40
+ - Newly generated migrations use tuples for `dependencies`/`operations`; existing migration files don't need to change
41
+ - Requires `plain>=0.157.0`
42
+
3
43
  ## [0.113.2](https://github.com/dropseed/plain/releases/plain-postgres@0.113.2) (2026-08-10)
4
44
 
5
45
  ### What's changed
@@ -408,7 +448,11 @@
408
448
 
409
449
  ```python
410
450
  # Before
411
- from plain.postgres.connections import get_connection, read_only, use_management_connection
451
+ from plain.postgres.connections import (
452
+ get_connection,
453
+ read_only,
454
+ use_management_connection,
455
+ )
412
456
 
413
457
  # After
414
458
  from plain.postgres.db import get_connection, read_only, use_management_connection