plain.postgres 0.113.2__tar.gz → 0.114.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 (232) hide show
  1. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/PKG-INFO +55 -23
  2. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/CHANGELOG.md +22 -1
  3. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/README.md +52 -20
  4. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/__init__.py +22 -40
  5. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +3 -1
  6. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/base.py +3 -7
  7. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/converge.py +2 -3
  8. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/core.py +4 -4
  9. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/migrations.py +2 -4
  10. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/sync.py +0 -1
  11. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/connection.py +8 -12
  12. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/constraints.py +10 -10
  13. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/convergence/corrections.py +0 -1
  14. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/databases.py +3 -5
  15. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/ddl.py +0 -1
  16. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/deletion.py +1 -1
  17. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/dialect.py +3 -4
  18. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/exceptions.py +0 -12
  19. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/expressions.py +23 -32
  20. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/base.py +8 -5
  21. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/binary.py +1 -2
  22. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/duration.py +2 -1
  23. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/encrypted.py +3 -2
  24. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/json.py +2 -1
  25. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/network.py +2 -1
  26. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/primary_key.py +2 -1
  27. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/related.py +5 -6
  28. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/related_descriptors.py +5 -6
  29. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/reverse_descriptors.py +5 -3
  30. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/temporal.py +5 -3
  31. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/text.py +4 -3
  32. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/timezones.py +2 -2
  33. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/forms.py +13 -18
  34. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/__init__.py +63 -70
  35. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/datetime.py +5 -5
  36. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/math.py +1 -4
  37. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/mixins.py +2 -3
  38. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/text.py +11 -8
  39. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/indexes.py +9 -9
  40. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/context.py +1 -1
  41. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/runner.py +24 -14
  42. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/lookups.py +7 -5
  43. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/meta.py +12 -10
  44. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/__init__.py +9 -14
  45. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/autodetector.py +66 -71
  46. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/exceptions.py +0 -12
  47. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/executor.py +1 -1
  48. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/graph.py +3 -2
  49. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/loader.py +4 -5
  50. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/migration.py +13 -13
  51. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/operations/base.py +3 -3
  52. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/operations/fields.py +2 -3
  53. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/operations/models.py +5 -6
  54. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/operations/special.py +2 -2
  55. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/questioner.py +1 -2
  56. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/serializer.py +5 -5
  57. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/state.py +10 -12
  58. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/utils.py +10 -11
  59. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/writer.py +5 -5
  60. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/otel.py +2 -6
  61. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/preflight/database.py +1 -1
  62. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/query.py +14 -18
  63. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/query_utils.py +7 -10
  64. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/registry.py +14 -14
  65. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/schema.py +8 -1
  66. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/schema_lock.py +0 -1
  67. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sources.py +1 -2
  68. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sql/__init__.py +11 -12
  69. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sql/compiler.py +10 -12
  70. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sql/query.py +13 -13
  71. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/test/database.py +1 -2
  72. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/test/pytest.py +1 -2
  73. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/transaction.py +2 -4
  74. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/utils.py +0 -1
  75. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/pyproject.toml +2 -2
  76. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0001_initial.py +5 -4
  77. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0002_test_field_removed.py +5 -6
  78. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +5 -6
  79. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +6 -6
  80. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +5 -6
  81. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0006_secretstore.py +5 -6
  82. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +5 -6
  83. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +5 -6
  84. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +6 -5
  85. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0010_hideableitem.py +5 -6
  86. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0011_defaultsexample.py +5 -6
  87. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0012_iterationexample.py +5 -6
  88. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +5 -6
  89. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +6 -5
  90. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +6 -5
  91. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0016_formsexample.py +5 -6
  92. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0017_random_string_token.py +5 -6
  93. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0018_storageparametersexample.py +5 -6
  94. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/constraints.py +2 -1
  95. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/defaults.py +2 -1
  96. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/delete.py +2 -1
  97. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/encrypted.py +2 -1
  98. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/forms.py +2 -1
  99. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/indexes.py +2 -1
  100. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/iteration.py +2 -1
  101. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/mixins.py +2 -1
  102. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/nullability.py +2 -1
  103. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/querysets.py +2 -1
  104. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/relationships.py +2 -1
  105. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/storage_parameters.py +2 -1
  106. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/trees.py +2 -1
  107. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/urls.py +2 -2
  108. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/urls.py +2 -3
  109. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/conftest.py +0 -1
  110. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_autodetector_not_null_errors.py +0 -1
  111. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_autodetector_type_change.py +0 -1
  112. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_connection_isolation.py +0 -3
  113. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_connection_lifecycle.py +3 -4
  114. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_connection_pool.py +1 -2
  115. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_connection_self_heal.py +7 -10
  116. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_constraint_violation_error.py +4 -5
  117. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence.py +0 -1
  118. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_constraints.py +20 -21
  119. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_defaults.py +0 -1
  120. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_fk.py +0 -1
  121. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_indexes.py +7 -6
  122. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_nullability.py +0 -1
  123. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_storage_parameters.py +0 -1
  124. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_timeouts.py +0 -1
  125. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_db_expression_defaults.py +12 -6
  126. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_diagnose.py +0 -1
  127. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_executor_connection_hook.py +0 -1
  128. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_fk_characterization.py +0 -1
  129. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_health.py +0 -1
  130. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_literal_default_persistence.py +1 -5
  131. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_management_connection.py +2 -4
  132. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_migration_executor.py +0 -1
  133. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_no_callable_defaults.py +0 -1
  134. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_otel_metrics.py +3 -4
  135. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_rollback_exc_attribution.py +6 -10
  136. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_schema_lock.py +15 -14
  137. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_schema_normalize_type.py +0 -1
  138. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_schema_timeouts.py +4 -13
  139. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_create_update.py +9 -10
  140. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_databases.py +0 -1
  141. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_delete_behaviors.py +11 -16
  142. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_encrypted_fields.py +0 -1
  143. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_exceptions.py +0 -1
  144. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_field_defaults.py +8 -7
  145. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_integrity_error_mapping.py +6 -9
  146. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_m2m.py +2 -4
  147. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_manager_assignment.py +0 -1
  148. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_modelform_roundtrip.py +0 -1
  149. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_only_empty_defaults.py +0 -1
  150. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_order_by_expressions.py +0 -1
  151. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_queryset_slicing.py +0 -1
  152. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_random_string_field.py +0 -1
  153. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_read_only_transactions.py +20 -20
  154. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_related.py +1 -2
  155. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/.gitignore +0 -0
  156. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/CLAUDE.md +0 -0
  157. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/LICENSE +0 -0
  158. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/README.md +0 -0
  159. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/adapters.py +0 -0
  160. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  161. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/aggregates.py +0 -0
  162. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/__init__.py +0 -0
  163. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/decorators.py +0 -0
  164. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/diagnose.py +0 -0
  165. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/schema.py +0 -0
  166. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/config.py +0 -0
  167. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/constants.py +0 -0
  168. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/convergence/__init__.py +0 -0
  169. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/convergence/analysis.py +0 -0
  170. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/convergence/planning.py +0 -0
  171. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/database_url.py +0 -0
  172. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/db.py +0 -0
  173. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/default_settings.py +0 -0
  174. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/entrypoints.py +0 -0
  175. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/enums.py +0 -0
  176. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/__init__.py +2 -2
  177. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/boolean.py +0 -0
  178. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/mixins.py +0 -0
  179. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/numeric.py +1 -1
  180. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/related_lookups.py +0 -0
  181. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/related_managers.py +0 -0
  182. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/reverse_related.py +0 -0
  183. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/uuid.py +0 -0
  184. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/comparison.py +0 -0
  185. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/random.py +0 -0
  186. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/uuid.py +0 -0
  187. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/window.py +0 -0
  188. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/__init__.py +4 -4
  189. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/__init__.py +0 -0
  190. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
  191. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  192. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
  193. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/helpers.py +0 -0
  194. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/ownership.py +0 -0
  195. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/types.py +0 -0
  196. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/schema.py +0 -0
  197. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/middleware.py +0 -0
  198. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/operations/__init__.py +7 -7
  199. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/optimizer.py +0 -0
  200. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/recorder.py +2 -2
  201. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/options.py +0 -0
  202. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/preflight/__init__.py +0 -0
  203. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/preflight/indexes.py +0 -0
  204. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/preflight/models.py +0 -0
  205. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sql/constants.py +0 -0
  206. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sql/datastructures.py +0 -0
  207. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sql/where.py +0 -0
  208. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/test/__init__.py +0 -0
  209. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/types.py +2 -2
  210. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/types.pyi +0 -0
  211. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/forms.py +0 -0
  212. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/__init__.py +0 -0
  213. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/__init__.py +0 -0
  214. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/unregistered.py +0 -0
  215. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/views.py +0 -0
  216. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/settings.py +0 -0
  217. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/conftest_convergence.py +0 -0
  218. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_apply_replan.py +0 -0
  219. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_databases_not_on_runtime_path.py +0 -0
  220. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_introspection.py +0 -0
  221. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
  222. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
  223. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
  224. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_database_url.py +0 -0
  225. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_deferred_loading.py +0 -0
  226. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_functions_uuid.py +0 -0
  227. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_iterator.py +0 -0
  228. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_manual_pk.py +0 -0
  229. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_mixins.py +0 -0
  230. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_queryset_repr.py +0 -0
  231. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_raw_query.py +0 -0
  232. {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_related_instance_filter.py +0 -0
@@ -1,12 +1,12 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: plain.postgres
3
- Version: 0.113.2
3
+ Version: 0.114.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
7
7
  License-File: LICENSE
8
8
  Requires-Python: >=3.13
9
- Requires-Dist: plain<1.0.0,>=0.134.0
9
+ Requires-Dist: plain<1.0.0,>=0.157.0
10
10
  Requires-Dist: psycopg-pool>=3.2
11
11
  Requires-Dist: psycopg>=3.2
12
12
  Description-Content-Type: text/markdown
@@ -179,9 +179,8 @@ user = User.query.get(email="test@example.com")
179
179
 
180
180
  # Complex queries with Q objects
181
181
  from plain.postgres import Q
182
- users = User.query.filter(
183
- Q(is_admin=True) | Q(email__endswith="@example.com")
184
- )
182
+
183
+ users = User.query.filter(Q(is_admin=True) | Q(email__endswith="@example.com"))
185
184
 
186
185
  # Ordering
187
186
  users = User.query.order_by("-created_at")
@@ -200,6 +199,7 @@ You can customize [`QuerySet`](./query.py#QuerySet) classes to provide specializ
200
199
  from typing import Self
201
200
  from plain.postgres import types
202
201
 
202
+
203
203
  class PublishedQuerySet(postgres.QuerySet["Article"]):
204
204
  def published_only(self) -> Self:
205
205
  return self.filter(status="published")
@@ -207,6 +207,7 @@ class PublishedQuerySet(postgres.QuerySet["Article"]):
207
207
  def draft_only(self) -> Self:
208
208
  return self.filter(status="draft")
209
209
 
210
+
210
211
  @postgres.register_model
211
212
  class Article(postgres.Model):
212
213
  title: str = types.TextField(max_length=200)
@@ -214,6 +215,7 @@ class Article(postgres.Model):
214
215
 
215
216
  query = PublishedQuerySet()
216
217
 
218
+
217
219
  # Usage - all methods available on Article.query
218
220
  all_articles = Article.query.all()
219
221
  published_articles = Article.query.published_only()
@@ -239,6 +241,7 @@ from __future__ import annotations
239
241
  from plain import postgres
240
242
  from plain.postgres import types
241
243
 
244
+
242
245
  @postgres.register_model
243
246
  class User(postgres.Model):
244
247
  email: str = types.EmailField()
@@ -256,11 +259,14 @@ For complex queries that can't be expressed with the ORM, you can use raw SQL.
256
259
  Use `Model.query.raw()` to execute raw SQL and get model instances back:
257
260
 
258
261
  ```python
259
- users = User.query.raw("""
262
+ users = User.query.raw(
263
+ """
260
264
  SELECT * FROM users
261
265
  WHERE created_at > %s
262
266
  ORDER BY created_at DESC
263
- """, [some_date])
267
+ """,
268
+ [some_date],
269
+ )
264
270
 
265
271
  for user in users:
266
272
  print(user.email) # Full model instance with all fields
@@ -333,6 +339,7 @@ for category in Category.query.all():
333
339
 
334
340
  # Good — single query with annotation
335
341
  from plain.postgres.aggregates import Count
342
+
336
343
  for category in Category.query.annotate(num_products=Count("products")).all():
337
344
  print(category.num_products)
338
345
  ```
@@ -346,6 +353,7 @@ Templates should only render data, never trigger queries. Prepare everything in
346
353
  def get_template_context(self):
347
354
  return {"posts": Post.query.all()} # related lookups happen in template
348
355
 
356
+
349
357
  # Good — eagerly load everything
350
358
  def get_template_context(self):
351
359
  return {"posts": Post.query.select_related("author").prefetch_related("tags").all()}
@@ -369,10 +377,12 @@ emails = list(User.query.values_list("email", flat=True))
369
377
 
370
378
  ```python
371
379
  # Bad
372
- if User.query.filter(is_active=True).count() > 0: ...
380
+ if User.query.filter(is_active=True).count() > 0:
381
+ ...
373
382
 
374
383
  # Good
375
- if User.query.filter(is_active=True).exists(): ...
384
+ if User.query.filter(is_active=True).exists():
385
+ ...
376
386
  ```
377
387
 
378
388
  #### Use `.count()` instead of `len(queryset)`
@@ -479,8 +489,8 @@ Run a block of code in a read-only transaction using `read_only()`. Any write (I
479
489
  from plain.postgres.db import read_only
480
490
 
481
491
  with read_only():
482
- users = User.query.all() # reads work
483
- 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
484
494
  ```
485
495
 
486
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.
@@ -493,10 +503,10 @@ Because the whole block is one transaction, catching a database error inside `re
493
503
  with read_only():
494
504
  try:
495
505
  with atomic():
496
- User.query.create(name="x") # raises, savepoint rolls back
506
+ User.query.create(name="x") # raises, savepoint rolls back
497
507
  except psycopg.errors.ReadOnlySqlTransaction:
498
508
  pass
499
- User.query.count() # still works — outer txn is healthy
509
+ User.query.count() # still works — outer txn is healthy
500
510
  ```
501
511
 
502
512
  ## Schema management
@@ -718,7 +728,9 @@ class User(postgres.Model):
718
728
  ],
719
729
  constraints=[
720
730
  postgres.UniqueConstraint(fields=["email"], name="users_email_uniq"),
721
- 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
+ ),
722
734
  ],
723
735
  )
724
736
  ```
@@ -819,6 +831,7 @@ from datetime import datetime
819
831
  from plain import postgres
820
832
  from plain.postgres import types
821
833
 
834
+
822
835
  class Product(postgres.Model):
823
836
  # Text fields
824
837
  name: str = types.TextField(max_length=200)
@@ -923,6 +936,7 @@ This is **not** for passwords or tokens you issue — those should be hashed (on
923
936
  from plain import postgres
924
937
  from plain.postgres import types
925
938
 
939
+
926
940
  @postgres.register_model
927
941
  class Integration(postgres.Model):
928
942
  name: str = types.TextField(max_length=100)
@@ -959,6 +973,7 @@ Use [`ForeignKeyField`](./fields/related.py#ForeignKeyField) for many-to-one and
959
973
  from plain import postgres
960
974
  from plain.postgres import types
961
975
 
976
+
962
977
  @postgres.register_model
963
978
  class Book(postgres.Model):
964
979
  title: str = types.TextField(max_length=200)
@@ -972,9 +987,9 @@ Accessing a foreign key gives you the related object without a query — only it
972
987
 
973
988
  ```python
974
989
  book = Book.query.get(id=1)
975
- book.author # no query — a partial Author instance
976
- book.author.id # no query — the foreign key value
977
- 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
978
993
  ```
979
994
 
980
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.
@@ -989,17 +1004,20 @@ When you define a `ForeignKey` or `ManyToManyField`, Plain automatically creates
989
1004
  from plain import postgres
990
1005
  from plain.postgres import types
991
1006
 
1007
+
992
1008
  @postgres.register_model
993
1009
  class Author(postgres.Model):
994
1010
  name: str = types.TextField(max_length=200)
995
1011
  # Explicit reverse accessor for all books by this author
996
1012
  books = types.ReverseForeignKey(to="Book", field="author")
997
1013
 
1014
+
998
1015
  @postgres.register_model
999
1016
  class Book(postgres.Model):
1000
1017
  title: str = types.TextField(max_length=200)
1001
1018
  author: Author = types.ForeignKeyField(Author, on_delete=postgres.CASCADE)
1002
1019
 
1020
+
1003
1021
  # Usage
1004
1022
  author = Author.query.get(name="Jane Doe")
1005
1023
  for book in author.books.all():
@@ -1018,11 +1036,13 @@ class Feature(postgres.Model):
1018
1036
  # Explicit reverse accessor for all cars with this feature
1019
1037
  cars = types.ReverseManyToMany(to="Car", field="features")
1020
1038
 
1039
+
1021
1040
  @postgres.register_model
1022
1041
  class Car(postgres.Model):
1023
1042
  model: str = types.TextField(max_length=100)
1024
1043
  features = types.ManyToManyField(Feature)
1025
1044
 
1045
+
1026
1046
  # Usage
1027
1047
  feature = Feature.query.get(name="Sunroof")
1028
1048
  for car in feature.cars.all():
@@ -1042,10 +1062,14 @@ To get type checking for custom QuerySet methods on reverse relations, specify t
1042
1062
 
1043
1063
  ```python
1044
1064
  # Basic usage
1045
- books: types.ReverseForeignKey[Book] = types.ReverseForeignKey(to="Book", field="author")
1065
+ books: types.ReverseForeignKey[Book] = types.ReverseForeignKey(
1066
+ to="Book", field="author"
1067
+ )
1046
1068
 
1047
1069
  # With custom QuerySet for proper method recognition
1048
- books: types.ReverseForeignKey[Book, BookQuerySet] = types.ReverseForeignKey(to="Book", field="author")
1070
+ books: types.ReverseForeignKey[Book, BookQuerySet] = types.ReverseForeignKey(
1071
+ to="Book", field="author"
1072
+ )
1049
1073
 
1050
1074
  # Now type checkers recognize custom methods like .published()
1051
1075
  author.books.query.published()
@@ -1189,6 +1213,7 @@ class Order(postgres.Model):
1189
1213
  status: str = types.TextField(max_length=20)
1190
1214
  created_at: datetime = types.DateTimeField()
1191
1215
 
1216
+
1192
1217
  # Good — indexed for common queries
1193
1218
  class Order(postgres.Model):
1194
1219
  status: str = types.TextField(max_length=20)
@@ -1210,6 +1235,7 @@ def create(self):
1210
1235
  raise ValueError("duplicate")
1211
1236
  return super().create()
1212
1237
 
1238
+
1213
1239
  # Good — database-enforced
1214
1240
  model_options = postgres.Options(
1215
1241
  constraints=[postgres.UniqueConstraint(fields=["email"])],
@@ -1222,7 +1248,9 @@ CASCADE for owned children, RESTRICT for referenced data, SET_NULL for optional
1222
1248
 
1223
1249
  ```python
1224
1250
  # Bad — blindly using CASCADE everywhere
1225
- company: Company = types.ForeignKeyField("Company", on_delete=postgres.CASCADE) # deleting company deletes invoices!
1251
+ company: Company = types.ForeignKeyField(
1252
+ "Company", on_delete=postgres.CASCADE
1253
+ ) # deleting company deletes invoices!
1226
1254
 
1227
1255
  # Good — block the delete while invoices reference the company
1228
1256
  company: Company = types.ForeignKeyField("Company", on_delete=postgres.RESTRICT)
@@ -1248,10 +1276,12 @@ Models integrate with [plain.forms](../../../plain-forms/plain/forms/README.md):
1248
1276
  from plain import forms
1249
1277
  from .models import User
1250
1278
 
1279
+
1251
1280
  class UserForm(forms.ModelForm):
1252
1281
  class Meta:
1253
1282
  model = User
1254
- fields = ["email", "is_admin"]
1283
+ fields = ("email", "is_admin")
1284
+
1255
1285
 
1256
1286
  # Usage
1257
1287
  form = UserForm(request=request)
@@ -1512,7 +1542,9 @@ Use `UniqueConstraint` in your model's `model_options`:
1512
1542
  ```python
1513
1543
  model_options = postgres.Options(
1514
1544
  constraints=[
1515
- postgres.UniqueConstraint(fields=["email", "organization"], name="unique_email_per_org"),
1545
+ postgres.UniqueConstraint(
1546
+ fields=["email", "organization"], name="unique_email_per_org"
1547
+ ),
1516
1548
  ],
1517
1549
  )
1518
1550
  ```
@@ -1,5 +1,22 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.114.0](https://github.com/dropseed/plain/releases/plain-postgres@0.114.0) (2026-08-12)
4
+
5
+ ### What's changed
6
+
7
+ - 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))
8
+ - `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))
9
+ - 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))
10
+ - 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))
11
+ - `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))
12
+ - `on_commit()` handler errors are logged with `logger.exception()` — same message and traceback, standard idiom ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
13
+
14
+ ### Upgrade instructions
15
+
16
+ - If you catch `ValueError` around any of the argument-validation cases above, catch `TypeError` instead
17
+ - Newly generated migrations use tuples for `dependencies`/`operations`; existing migration files don't need to change
18
+ - Requires `plain>=0.157.0`
19
+
3
20
  ## [0.113.2](https://github.com/dropseed/plain/releases/plain-postgres@0.113.2) (2026-08-10)
4
21
 
5
22
  ### What's changed
@@ -408,7 +425,11 @@
408
425
 
409
426
  ```python
410
427
  # Before
411
- from plain.postgres.connections import get_connection, read_only, use_management_connection
428
+ from plain.postgres.connections import (
429
+ get_connection,
430
+ read_only,
431
+ use_management_connection,
432
+ )
412
433
 
413
434
  # After
414
435
  from plain.postgres.db import get_connection, read_only, use_management_connection
@@ -166,9 +166,8 @@ user = User.query.get(email="test@example.com")
166
166
 
167
167
  # Complex queries with Q objects
168
168
  from plain.postgres import Q
169
- users = User.query.filter(
170
- Q(is_admin=True) | Q(email__endswith="@example.com")
171
- )
169
+
170
+ users = User.query.filter(Q(is_admin=True) | Q(email__endswith="@example.com"))
172
171
 
173
172
  # Ordering
174
173
  users = User.query.order_by("-created_at")
@@ -187,6 +186,7 @@ You can customize [`QuerySet`](./query.py#QuerySet) classes to provide specializ
187
186
  from typing import Self
188
187
  from plain.postgres import types
189
188
 
189
+
190
190
  class PublishedQuerySet(postgres.QuerySet["Article"]):
191
191
  def published_only(self) -> Self:
192
192
  return self.filter(status="published")
@@ -194,6 +194,7 @@ class PublishedQuerySet(postgres.QuerySet["Article"]):
194
194
  def draft_only(self) -> Self:
195
195
  return self.filter(status="draft")
196
196
 
197
+
197
198
  @postgres.register_model
198
199
  class Article(postgres.Model):
199
200
  title: str = types.TextField(max_length=200)
@@ -201,6 +202,7 @@ class Article(postgres.Model):
201
202
 
202
203
  query = PublishedQuerySet()
203
204
 
205
+
204
206
  # Usage - all methods available on Article.query
205
207
  all_articles = Article.query.all()
206
208
  published_articles = Article.query.published_only()
@@ -226,6 +228,7 @@ from __future__ import annotations
226
228
  from plain import postgres
227
229
  from plain.postgres import types
228
230
 
231
+
229
232
  @postgres.register_model
230
233
  class User(postgres.Model):
231
234
  email: str = types.EmailField()
@@ -243,11 +246,14 @@ For complex queries that can't be expressed with the ORM, you can use raw SQL.
243
246
  Use `Model.query.raw()` to execute raw SQL and get model instances back:
244
247
 
245
248
  ```python
246
- users = User.query.raw("""
249
+ users = User.query.raw(
250
+ """
247
251
  SELECT * FROM users
248
252
  WHERE created_at > %s
249
253
  ORDER BY created_at DESC
250
- """, [some_date])
254
+ """,
255
+ [some_date],
256
+ )
251
257
 
252
258
  for user in users:
253
259
  print(user.email) # Full model instance with all fields
@@ -320,6 +326,7 @@ for category in Category.query.all():
320
326
 
321
327
  # Good — single query with annotation
322
328
  from plain.postgres.aggregates import Count
329
+
323
330
  for category in Category.query.annotate(num_products=Count("products")).all():
324
331
  print(category.num_products)
325
332
  ```
@@ -333,6 +340,7 @@ Templates should only render data, never trigger queries. Prepare everything in
333
340
  def get_template_context(self):
334
341
  return {"posts": Post.query.all()} # related lookups happen in template
335
342
 
343
+
336
344
  # Good — eagerly load everything
337
345
  def get_template_context(self):
338
346
  return {"posts": Post.query.select_related("author").prefetch_related("tags").all()}
@@ -356,10 +364,12 @@ emails = list(User.query.values_list("email", flat=True))
356
364
 
357
365
  ```python
358
366
  # Bad
359
- if User.query.filter(is_active=True).count() > 0: ...
367
+ if User.query.filter(is_active=True).count() > 0:
368
+ ...
360
369
 
361
370
  # Good
362
- if User.query.filter(is_active=True).exists(): ...
371
+ if User.query.filter(is_active=True).exists():
372
+ ...
363
373
  ```
364
374
 
365
375
  #### Use `.count()` instead of `len(queryset)`
@@ -466,8 +476,8 @@ Run a block of code in a read-only transaction using `read_only()`. Any write (I
466
476
  from plain.postgres.db import read_only
467
477
 
468
478
  with read_only():
469
- users = User.query.all() # reads work
470
- User.query.create(name="x") # raises psycopg.errors.ReadOnlySqlTransaction
479
+ users = User.query.all() # reads work
480
+ User.query.create(name="x") # raises psycopg.errors.ReadOnlySqlTransaction
471
481
  ```
472
482
 
473
483
  `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 +490,10 @@ Because the whole block is one transaction, catching a database error inside `re
480
490
  with read_only():
481
491
  try:
482
492
  with atomic():
483
- User.query.create(name="x") # raises, savepoint rolls back
493
+ User.query.create(name="x") # raises, savepoint rolls back
484
494
  except psycopg.errors.ReadOnlySqlTransaction:
485
495
  pass
486
- User.query.count() # still works — outer txn is healthy
496
+ User.query.count() # still works — outer txn is healthy
487
497
  ```
488
498
 
489
499
  ## Schema management
@@ -705,7 +715,9 @@ class User(postgres.Model):
705
715
  ],
706
716
  constraints=[
707
717
  postgres.UniqueConstraint(fields=["email"], name="users_email_uniq"),
708
- postgres.CheckConstraint(check=postgres.Q(age__gte=0), name="users_age_positive"),
718
+ postgres.CheckConstraint(
719
+ check=postgres.Q(age__gte=0), name="users_age_positive"
720
+ ),
709
721
  ],
710
722
  )
711
723
  ```
@@ -806,6 +818,7 @@ from datetime import datetime
806
818
  from plain import postgres
807
819
  from plain.postgres import types
808
820
 
821
+
809
822
  class Product(postgres.Model):
810
823
  # Text fields
811
824
  name: str = types.TextField(max_length=200)
@@ -910,6 +923,7 @@ This is **not** for passwords or tokens you issue — those should be hashed (on
910
923
  from plain import postgres
911
924
  from plain.postgres import types
912
925
 
926
+
913
927
  @postgres.register_model
914
928
  class Integration(postgres.Model):
915
929
  name: str = types.TextField(max_length=100)
@@ -946,6 +960,7 @@ Use [`ForeignKeyField`](./fields/related.py#ForeignKeyField) for many-to-one and
946
960
  from plain import postgres
947
961
  from plain.postgres import types
948
962
 
963
+
949
964
  @postgres.register_model
950
965
  class Book(postgres.Model):
951
966
  title: str = types.TextField(max_length=200)
@@ -959,9 +974,9 @@ Accessing a foreign key gives you the related object without a query — only it
959
974
 
960
975
  ```python
961
976
  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
977
+ book.author # no query — a partial Author instance
978
+ book.author.id # no query — the foreign key value
979
+ book.author.name # one query — loads the rest of the row
965
980
  ```
966
981
 
967
982
  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.
@@ -976,17 +991,20 @@ When you define a `ForeignKey` or `ManyToManyField`, Plain automatically creates
976
991
  from plain import postgres
977
992
  from plain.postgres import types
978
993
 
994
+
979
995
  @postgres.register_model
980
996
  class Author(postgres.Model):
981
997
  name: str = types.TextField(max_length=200)
982
998
  # Explicit reverse accessor for all books by this author
983
999
  books = types.ReverseForeignKey(to="Book", field="author")
984
1000
 
1001
+
985
1002
  @postgres.register_model
986
1003
  class Book(postgres.Model):
987
1004
  title: str = types.TextField(max_length=200)
988
1005
  author: Author = types.ForeignKeyField(Author, on_delete=postgres.CASCADE)
989
1006
 
1007
+
990
1008
  # Usage
991
1009
  author = Author.query.get(name="Jane Doe")
992
1010
  for book in author.books.all():
@@ -1005,11 +1023,13 @@ class Feature(postgres.Model):
1005
1023
  # Explicit reverse accessor for all cars with this feature
1006
1024
  cars = types.ReverseManyToMany(to="Car", field="features")
1007
1025
 
1026
+
1008
1027
  @postgres.register_model
1009
1028
  class Car(postgres.Model):
1010
1029
  model: str = types.TextField(max_length=100)
1011
1030
  features = types.ManyToManyField(Feature)
1012
1031
 
1032
+
1013
1033
  # Usage
1014
1034
  feature = Feature.query.get(name="Sunroof")
1015
1035
  for car in feature.cars.all():
@@ -1029,10 +1049,14 @@ To get type checking for custom QuerySet methods on reverse relations, specify t
1029
1049
 
1030
1050
  ```python
1031
1051
  # Basic usage
1032
- books: types.ReverseForeignKey[Book] = types.ReverseForeignKey(to="Book", field="author")
1052
+ books: types.ReverseForeignKey[Book] = types.ReverseForeignKey(
1053
+ to="Book", field="author"
1054
+ )
1033
1055
 
1034
1056
  # With custom QuerySet for proper method recognition
1035
- books: types.ReverseForeignKey[Book, BookQuerySet] = types.ReverseForeignKey(to="Book", field="author")
1057
+ books: types.ReverseForeignKey[Book, BookQuerySet] = types.ReverseForeignKey(
1058
+ to="Book", field="author"
1059
+ )
1036
1060
 
1037
1061
  # Now type checkers recognize custom methods like .published()
1038
1062
  author.books.query.published()
@@ -1176,6 +1200,7 @@ class Order(postgres.Model):
1176
1200
  status: str = types.TextField(max_length=20)
1177
1201
  created_at: datetime = types.DateTimeField()
1178
1202
 
1203
+
1179
1204
  # Good — indexed for common queries
1180
1205
  class Order(postgres.Model):
1181
1206
  status: str = types.TextField(max_length=20)
@@ -1197,6 +1222,7 @@ def create(self):
1197
1222
  raise ValueError("duplicate")
1198
1223
  return super().create()
1199
1224
 
1225
+
1200
1226
  # Good — database-enforced
1201
1227
  model_options = postgres.Options(
1202
1228
  constraints=[postgres.UniqueConstraint(fields=["email"])],
@@ -1209,7 +1235,9 @@ CASCADE for owned children, RESTRICT for referenced data, SET_NULL for optional
1209
1235
 
1210
1236
  ```python
1211
1237
  # Bad — blindly using CASCADE everywhere
1212
- company: Company = types.ForeignKeyField("Company", on_delete=postgres.CASCADE) # deleting company deletes invoices!
1238
+ company: Company = types.ForeignKeyField(
1239
+ "Company", on_delete=postgres.CASCADE
1240
+ ) # deleting company deletes invoices!
1213
1241
 
1214
1242
  # Good — block the delete while invoices reference the company
1215
1243
  company: Company = types.ForeignKeyField("Company", on_delete=postgres.RESTRICT)
@@ -1235,10 +1263,12 @@ Models integrate with [plain.forms](../../../plain-forms/plain/forms/README.md):
1235
1263
  from plain import forms
1236
1264
  from .models import User
1237
1265
 
1266
+
1238
1267
  class UserForm(forms.ModelForm):
1239
1268
  class Meta:
1240
1269
  model = User
1241
- fields = ["email", "is_admin"]
1270
+ fields = ("email", "is_admin")
1271
+
1242
1272
 
1243
1273
  # Usage
1244
1274
  form = UserForm(request=request)
@@ -1499,7 +1529,9 @@ Use `UniqueConstraint` in your model's `model_options`:
1499
1529
  ```python
1500
1530
  model_options = postgres.Options(
1501
1531
  constraints=[
1502
- postgres.UniqueConstraint(fields=["email", "organization"], name="unique_email_per_org"),
1532
+ postgres.UniqueConstraint(
1533
+ fields=["email", "organization"], name="unique_email_per_org"
1534
+ ),
1503
1535
  ],
1504
1536
  )
1505
1537
  ```
@@ -56,65 +56,47 @@ from . import types
56
56
  # Advanced query-time features (aggregates, expressions, etc.) should be
57
57
  # imported from their specific modules (e.g., plain.postgres.aggregates).
58
58
  __all__ = [
59
- # From constraints
60
- "CheckConstraint",
61
- "UniqueConstraint",
62
- # From enums
63
- "TextChoices",
64
- # From fields
59
+ "CASCADE",
60
+ "NO_ACTION",
61
+ "RESTRICT",
62
+ "SET_NULL",
65
63
  "BigIntegerField",
66
64
  "BinaryField",
67
65
  "BooleanField",
66
+ "CheckConstraint",
67
+ "DatabaseConnectionMiddleware",
68
68
  "DateField",
69
69
  "DateTimeField",
70
70
  "DecimalField",
71
71
  "DurationField",
72
72
  "EmailField",
73
+ "F",
73
74
  "FloatField",
75
+ "ForeignKeyField",
74
76
  "GenericIPAddressField",
77
+ "Index",
75
78
  "IntegerField",
79
+ "JSONField",
80
+ "ManyToManyField",
81
+ "Model",
82
+ "Options",
76
83
  "PrimaryKeyField",
84
+ "Q",
85
+ "QuerySet",
77
86
  "RandomStringField",
87
+ "ReverseForeignKey",
88
+ "ReverseManyToMany",
78
89
  "SmallIntegerField",
90
+ "TextChoices",
79
91
  "TextField",
80
92
  "TimeField",
93
+ "TimeZoneField",
81
94
  "URLField",
82
95
  "UUIDField",
83
- # From fields.json
84
- "JSONField",
85
- # From fields.timezones
86
- "TimeZoneField",
87
- # From indexes
88
- "Index",
89
- # From deletion
90
- "CASCADE",
91
- "NO_ACTION",
92
- "RESTRICT",
93
- "SET_NULL",
94
- # From options
95
- "Options",
96
- # From query
97
- "QuerySet",
98
- # From query_utils
99
- "Q",
100
- # From expressions
101
- "F",
102
- # From base
103
- "Model",
104
- # From fields.related
105
- "ForeignKeyField",
106
- "ManyToManyField",
107
- # From fields.reverse_descriptors
108
- "ReverseForeignKey",
109
- "ReverseManyToMany",
110
- # From db
96
+ "UniqueConstraint",
111
97
  "get_connection",
112
- "use_management_connection",
113
- # From middleware
114
- "DatabaseConnectionMiddleware",
115
- # From registry
116
- "register_model",
117
98
  "models_registry",
118
- # Typed field imports
99
+ "register_model",
119
100
  "types",
101
+ "use_management_connection",
120
102
  ]
@@ -23,7 +23,9 @@ can't infer the target type from the string — annotate explicitly so
23
23
  instance access keeps its type:
24
24
 
25
25
  ```python
26
- parent: TreeNode | None = types.ForeignKeyField("self", on_delete=postgres.CASCADE, allow_null=True)
26
+ parent: TreeNode | None = types.ForeignKeyField(
27
+ "self", on_delete=postgres.CASCADE, allow_null=True
28
+ )
27
29
  ```
28
30
 
29
31
  For `JSONField` and `EncryptedJSONField`, the stub returns `Any` (the