plain.postgres 0.105.0__tar.gz → 0.106.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 (216) hide show
  1. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/PKG-INFO +41 -13
  2. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/CHANGELOG.md +18 -0
  3. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/README.md +40 -12
  4. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +2 -0
  5. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/base.py +199 -375
  6. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/constraints.py +65 -2
  7. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/related_managers.py +7 -2
  8. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/forms.py +16 -22
  9. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/meta.py +18 -0
  10. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/query.py +9 -30
  11. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/sql/compiler.py +1 -3
  12. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/sql/query.py +1 -4
  13. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/transaction.py +2 -2
  14. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/pyproject.toml +1 -1
  15. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0002_test_field_removed.py +1 -1
  16. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_constraint_violation_error.py +143 -17
  17. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_db_expression_defaults.py +24 -44
  18. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_fk_characterization.py +5 -5
  19. plain_postgres-0.106.0/tests/public/test_create_update.py +256 -0
  20. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_delete_behaviors.py +13 -8
  21. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_encrypted_fields.py +1 -1
  22. plain_postgres-0.106.0/tests/public/test_integrity_error_mapping.py +64 -0
  23. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_m2m.py +9 -6
  24. plain_postgres-0.106.0/tests/public/test_manual_pk.py +48 -0
  25. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_related.py +7 -7
  26. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/.gitignore +0 -0
  27. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/CLAUDE.md +0 -0
  28. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/LICENSE +0 -0
  29. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/README.md +0 -0
  30. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/__init__.py +0 -0
  31. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/adapters.py +0 -0
  32. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  33. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/aggregates.py +0 -0
  34. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/cli/__init__.py +0 -0
  35. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/cli/converge.py +0 -0
  36. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/cli/core.py +0 -0
  37. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/cli/decorators.py +0 -0
  38. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/cli/diagnose.py +0 -0
  39. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/cli/migrations.py +0 -0
  40. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/cli/schema.py +0 -0
  41. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/cli/sync.py +0 -0
  42. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/config.py +0 -0
  43. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/connection.py +0 -0
  44. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/constants.py +0 -0
  45. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/convergence/__init__.py +0 -0
  46. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/convergence/analysis.py +0 -0
  47. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/convergence/fixes.py +0 -0
  48. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/convergence/planning.py +0 -0
  49. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/database_url.py +0 -0
  50. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/db.py +0 -0
  51. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/ddl.py +0 -0
  52. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/default_settings.py +0 -0
  53. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/deletion.py +0 -0
  54. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/dialect.py +0 -0
  55. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/entrypoints.py +0 -0
  56. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/enums.py +0 -0
  57. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/exceptions.py +0 -0
  58. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/expressions.py +0 -0
  59. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/__init__.py +0 -0
  60. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/base.py +0 -0
  61. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/binary.py +0 -0
  62. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/boolean.py +0 -0
  63. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/duration.py +0 -0
  64. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/encrypted.py +0 -0
  65. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/json.py +0 -0
  66. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/mixins.py +0 -0
  67. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/network.py +0 -0
  68. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/numeric.py +0 -0
  69. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/primary_key.py +0 -0
  70. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/related.py +0 -0
  71. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/related_descriptors.py +0 -0
  72. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/related_lookups.py +0 -0
  73. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
  74. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/reverse_related.py +0 -0
  75. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/temporal.py +0 -0
  76. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/text.py +0 -0
  77. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/timezones.py +0 -0
  78. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/fields/uuid.py +0 -0
  79. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/functions/__init__.py +0 -0
  80. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/functions/comparison.py +0 -0
  81. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/functions/datetime.py +0 -0
  82. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/functions/math.py +0 -0
  83. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/functions/mixins.py +0 -0
  84. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/functions/random.py +0 -0
  85. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/functions/text.py +0 -0
  86. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/functions/uuid.py +0 -0
  87. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/functions/window.py +0 -0
  88. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/indexes.py +0 -0
  89. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/introspection/__init__.py +0 -0
  90. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/__init__.py +0 -0
  91. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
  92. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  93. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
  94. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/context.py +0 -0
  95. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/helpers.py +0 -0
  96. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/ownership.py +0 -0
  97. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/runner.py +0 -0
  98. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/types.py +0 -0
  99. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/introspection/schema.py +0 -0
  100. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/lookups.py +0 -0
  101. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/middleware.py +0 -0
  102. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/__init__.py +0 -0
  103. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/autodetector.py +0 -0
  104. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/exceptions.py +0 -0
  105. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/executor.py +0 -0
  106. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/graph.py +0 -0
  107. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/loader.py +0 -0
  108. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/migration.py +0 -0
  109. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/operations/__init__.py +0 -0
  110. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/operations/base.py +0 -0
  111. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/operations/fields.py +0 -0
  112. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/operations/models.py +0 -0
  113. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/operations/special.py +0 -0
  114. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/optimizer.py +0 -0
  115. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/questioner.py +0 -0
  116. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/recorder.py +0 -0
  117. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/serializer.py +0 -0
  118. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/state.py +0 -0
  119. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/utils.py +0 -0
  120. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/migrations/writer.py +0 -0
  121. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/options.py +0 -0
  122. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/otel.py +0 -0
  123. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/preflight.py +0 -0
  124. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/query_utils.py +0 -0
  125. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/registry.py +0 -0
  126. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/schema.py +0 -0
  127. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/sources.py +0 -0
  128. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/sql/__init__.py +0 -0
  129. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/sql/constants.py +0 -0
  130. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/sql/datastructures.py +0 -0
  131. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/sql/where.py +0 -0
  132. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/test/__init__.py +0 -0
  133. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/test/database.py +0 -0
  134. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/test/pytest.py +0 -0
  135. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/types.py +0 -0
  136. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/types.pyi +0 -0
  137. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/plain/postgres/utils.py +0 -0
  138. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/forms.py +0 -0
  139. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  140. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  141. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  142. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  143. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  144. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  145. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  146. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  147. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  148. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  149. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  150. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  151. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
  152. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  153. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  154. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  155. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
  156. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/__init__.py +0 -0
  157. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/__init__.py +0 -0
  158. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/constraints.py +0 -0
  159. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/defaults.py +0 -0
  160. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/delete.py +0 -0
  161. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/encrypted.py +0 -0
  162. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/forms.py +0 -0
  163. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/indexes.py +0 -0
  164. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/iteration.py +0 -0
  165. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/mixins.py +0 -0
  166. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/nullability.py +0 -0
  167. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/querysets.py +0 -0
  168. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/relationships.py +0 -0
  169. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/storage_parameters.py +0 -0
  170. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/trees.py +0 -0
  171. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/models/unregistered.py +0 -0
  172. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/urls.py +0 -0
  173. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/examples/views.py +0 -0
  174. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/settings.py +0 -0
  175. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/app/urls.py +0 -0
  176. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/conftest.py +0 -0
  177. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/conftest_convergence.py +0 -0
  178. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_autodetector_not_null_errors.py +0 -0
  179. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_autodetector_type_change.py +0 -0
  180. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_connection_isolation.py +0 -0
  181. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_connection_lifecycle.py +0 -0
  182. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_connection_pool.py +0 -0
  183. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_convergence.py +0 -0
  184. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_constraints.py +0 -0
  185. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_defaults.py +0 -0
  186. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_fk.py +0 -0
  187. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_indexes.py +0 -0
  188. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_nullability.py +0 -0
  189. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
  190. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_timeouts.py +0 -0
  191. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_diagnose.py +0 -0
  192. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_executor_connection_hook.py +0 -0
  193. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_health.py +0 -0
  194. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_introspection.py +0 -0
  195. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_literal_default_persistence.py +0 -0
  196. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_management_connection.py +0 -0
  197. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_migration_executor.py +0 -0
  198. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_no_callable_defaults.py +0 -0
  199. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_otel_metrics.py +0 -0
  200. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
  201. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
  202. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_schema_normalize_type.py +0 -0
  203. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/internal/test_schema_timeouts.py +0 -0
  204. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_database_url.py +0 -0
  205. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_deferred_loading.py +0 -0
  206. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_exceptions.py +0 -0
  207. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_field_defaults.py +0 -0
  208. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_functions_uuid.py +0 -0
  209. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_iterator.py +0 -0
  210. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_manager_assignment.py +0 -0
  211. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_mixins.py +0 -0
  212. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_modelform_roundtrip.py +0 -0
  213. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_queryset_repr.py +0 -0
  214. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_random_string_field.py +0 -0
  215. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_raw_query.py +0 -0
  216. {plain_postgres-0.105.0 → plain_postgres-0.106.0}/tests/public/test_read_only_transactions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.postgres
3
- Version: 0.105.0
3
+ Version: 0.106.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
@@ -74,7 +74,7 @@ user = User.query.create(
74
74
 
75
75
  # Update a user
76
76
  user.email = "new@example.com"
77
- user.save()
77
+ user.update()
78
78
 
79
79
  # Delete a user
80
80
  user.delete()
@@ -388,12 +388,12 @@ total = User.query.count()
388
388
 
389
389
  #### Use `bulk_create` / `bulk_update` for batch operations
390
390
 
391
- Avoid calling `.save()` in a loop — each call is a separate query.
391
+ Avoid calling `.create()` in a loop — each call is a separate query.
392
392
 
393
393
  ```python
394
394
  # Bad — N INSERT statements
395
395
  for name in names:
396
- Tag(name=name).save()
396
+ Tag(name=name).create()
397
397
 
398
398
  # Good — single INSERT
399
399
  Tag.query.bulk_create([Tag(name=name) for name in names])
@@ -405,7 +405,7 @@ Tag.query.bulk_create([Tag(name=name) for name in names])
405
405
  # Bad — N UPDATE statements
406
406
  for user in User.query.filter(is_active=False):
407
407
  user.is_archived = True
408
- user.save()
408
+ user.update()
409
409
 
410
410
  # Good — single UPDATE statement
411
411
  User.query.filter(is_active=False).update(is_archived=True)
@@ -452,16 +452,16 @@ from plain.postgres import transaction
452
452
 
453
453
  with transaction.atomic():
454
454
  user = User(email="test@example.com")
455
- user.save()
456
- Profile(user=user).save()
457
- # Both saves commit together, or both roll back on error
455
+ user.create()
456
+ Profile(user=user).create()
457
+ # Both writes commit together, or both roll back on error
458
458
  ```
459
459
 
460
460
  Nesting `atomic()` creates savepoints:
461
461
 
462
462
  ```python
463
463
  with transaction.atomic():
464
- user.save()
464
+ user.create()
465
465
  try:
466
466
  with transaction.atomic():
467
467
  risky_operation() # If this fails...
@@ -1033,7 +1033,7 @@ author.books.query.published()
1033
1033
 
1034
1034
  ### Validation
1035
1035
 
1036
- `save()` runs `full_clean()` by default — field validators, model `clean()`, and any constraints with a `validate()` method are all checked, raising `ValidationError` on violation. Pass `clean_and_validate=False` to skip it (e.g. for trusted bulk loads).
1036
+ `create()` and `update()` run `full_clean()` by default — field validators and the model's `clean()` method raising `ValidationError` on violation. Pass `clean_and_validate=False` to skip it (e.g. for trusted bulk loads). Constraints are _not_ pre-checked here; the database enforces them (see below).
1037
1037
 
1038
1038
  ```python
1039
1039
  @postgres.register_model
@@ -1054,6 +1054,33 @@ class User(postgres.Model):
1054
1054
 
1055
1055
  Field-level validation happens automatically based on field types and constraints.
1056
1056
 
1057
+ **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`.)
1058
+
1059
+ 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:
1060
+
1061
+ ```python
1062
+ try:
1063
+ with transaction.atomic():
1064
+ User.query.create(email=taken_email)
1065
+ except ValidationError:
1066
+ ... # report it — the transaction is still usable
1067
+ ```
1068
+
1069
+ 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 saves with validation already done. A direct `create()`/`update()` reports the first violation Postgres hits.
1070
+
1071
+ 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:
1072
+
1073
+ ```python
1074
+ try:
1075
+ obj.create()
1076
+ except (psycopg.IntegrityError, ValidationError):
1077
+ ... # lost a race — reload and retry, or report it
1078
+ ```
1079
+
1080
+ 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.
1081
+
1082
+ 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.
1083
+
1057
1084
  ### Indexes and constraints
1058
1085
 
1059
1086
  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`.
@@ -1076,7 +1103,7 @@ class User(postgres.Model):
1076
1103
  )
1077
1104
  ```
1078
1105
 
1079
- Constraints are also checked during `full_clean()` (which `save()` runs by default see [Validation](#validation)). Pass `violation_error` to customize the resulting `ValidationError`. It accepts anything `ValidationError(...)` accepts — a string, a `{field: message}` dict, or a fully-formed `ValidationError`:
1106
+ Constraints are checked by `validate_constraints()` — run by a `ModelForm` (and any explicit `validate_constraints()` call), but **not** by `full_clean()` (which validates shape only) or a direct `create()`/`update()`, where the database enforces them instead (see [Validation](#validation)). Pass `violation_error` to customize the resulting `ValidationError`. It accepts anything `ValidationError(...)` accepts — a string, a `{field: message}` dict, or a fully-formed `ValidationError`:
1080
1107
 
1081
1108
  ```python
1082
1109
  # Simple message — lands on NON_FIELD_ERRORS
@@ -1155,10 +1182,11 @@ class Order(postgres.Model):
1155
1182
  Enforce uniqueness and data integrity at the database level.
1156
1183
 
1157
1184
  ```python
1158
- # Bad — only validated in Python
1159
- def save(self):
1185
+ # Bad — only validated in Python (racy: two requests can both pass the check)
1186
+ def create(self):
1160
1187
  if MyModel.query.filter(email=self.email).exists():
1161
1188
  raise ValueError("duplicate")
1189
+ return super().create()
1162
1190
 
1163
1191
  # Good — database-enforced
1164
1192
  model_options = postgres.Options(
@@ -1,5 +1,23 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.106.0](https://github.com/dropseed/plain/releases/plain-postgres@0.106.0) (2026-06-03)
4
+
5
+ ### What's changed
6
+
7
+ - **`Model.save()` is replaced by explicit `create()` (always INSERT) and `update()` (always UPDATE).** A call site now says which it does. `create()` inserts a new row and returns `self`; `update()` writes an existing row and returns `self`, raising if no row matched (there is no silent INSERT fallback). `update(fields=[...])` limits the columns written (replacing `save(update_fields=[...])`). `save()`, `save_base()`, and the `force_insert`/`force_update` arguments are removed. ([f75deb3ba2](https://github.com/dropseed/plain/commit/f75deb3ba2))
8
+ - **Constructing a model with `id=` now raises.** A hand-set `id` at construction almost always means "load the existing row" — use `Model.query.get(id=...)`. A freshly constructed instance is unambiguously new. ([9c0acf7bac](https://github.com/dropseed/plain/commit/9c0acf7bac), [661c7806df](https://github.com/dropseed/plain/commit/661c7806df))
9
+ - **Declared unique/check constraint violations now raise `ValidationError`, not raw `psycopg.IntegrityError`** — mapped at the write boundary, so even a violation that races past validation surfaces as the same field-level error a pre-check would raise. The database is now the authority; the pre-check `SELECT` is no longer required for correctness. Set-based writes (`bulk_create`, `QuerySet.update`) still raise raw `psycopg` errors. ([40c97521c5](https://github.com/dropseed/plain/commit/40c97521c5), [bdbdd524e5](https://github.com/dropseed/plain/commit/bdbdd524e5))
10
+ - **`full_clean()` is now shape-only** (`clean_fields()` + `clean()`); the integrity pre-check moved to `validate_constraints()`, called explicitly. `full_clean()`'s `validate_unique`/`validate_constraints` parameters and the `Model.validate_unique()` / `_get_unique_checks` / `_perform_unique_checks` trio are removed; forms call `full_clean()` then `validate_constraints()`. ([ba4b386fb7](https://github.com/dropseed/plain/commit/ba4b386fb7), [65d56945fb](https://github.com/dropseed/plain/commit/65d56945fb))
11
+ - `delete()` clears the instance's `id` and resets it to "new," so `create()` re-inserts it cleanly; the instance's other field values survive the delete. ([f75deb3ba2](https://github.com/dropseed/plain/commit/f75deb3ba2))
12
+
13
+ ### Upgrade instructions
14
+
15
+ - Replace `obj.save()` with `obj.create()` for a new row or `obj.update()` for an existing one; `obj.save(update_fields=[...])` becomes `obj.update(fields=[...])`. The `/plain-upgrade` skill rewrites these for you — the create-vs-update choice is context-dependent, so it reasons per call site.
16
+ - Remove `force_insert=`/`force_update=` — `create()` and `update()` are the explicit forms.
17
+ - Don't pass `id=` to a model constructor; load existing rows with `Model.query.get(id=...)`.
18
+ - Catch `ValidationError` (or `(psycopg.IntegrityError, ValidationError)` when retrying) for unique/check conflicts on instance writes. Inside an open `transaction.atomic()` a violation aborts the transaction — wrap the write in its own `atomic()` to catch and continue.
19
+ - Direct `full_clean()` callers that relied on constraint pre-checking should call `validate_constraints()` explicitly.
20
+
3
21
  ## [0.105.0](https://github.com/dropseed/plain/releases/plain-postgres@0.105.0) (2026-05-25)
4
22
 
5
23
  ### What's changed
@@ -61,7 +61,7 @@ user = User.query.create(
61
61
 
62
62
  # Update a user
63
63
  user.email = "new@example.com"
64
- user.save()
64
+ user.update()
65
65
 
66
66
  # Delete a user
67
67
  user.delete()
@@ -375,12 +375,12 @@ total = User.query.count()
375
375
 
376
376
  #### Use `bulk_create` / `bulk_update` for batch operations
377
377
 
378
- Avoid calling `.save()` in a loop — each call is a separate query.
378
+ Avoid calling `.create()` in a loop — each call is a separate query.
379
379
 
380
380
  ```python
381
381
  # Bad — N INSERT statements
382
382
  for name in names:
383
- Tag(name=name).save()
383
+ Tag(name=name).create()
384
384
 
385
385
  # Good — single INSERT
386
386
  Tag.query.bulk_create([Tag(name=name) for name in names])
@@ -392,7 +392,7 @@ Tag.query.bulk_create([Tag(name=name) for name in names])
392
392
  # Bad — N UPDATE statements
393
393
  for user in User.query.filter(is_active=False):
394
394
  user.is_archived = True
395
- user.save()
395
+ user.update()
396
396
 
397
397
  # Good — single UPDATE statement
398
398
  User.query.filter(is_active=False).update(is_archived=True)
@@ -439,16 +439,16 @@ from plain.postgres import transaction
439
439
 
440
440
  with transaction.atomic():
441
441
  user = User(email="test@example.com")
442
- user.save()
443
- Profile(user=user).save()
444
- # Both saves commit together, or both roll back on error
442
+ user.create()
443
+ Profile(user=user).create()
444
+ # Both writes commit together, or both roll back on error
445
445
  ```
446
446
 
447
447
  Nesting `atomic()` creates savepoints:
448
448
 
449
449
  ```python
450
450
  with transaction.atomic():
451
- user.save()
451
+ user.create()
452
452
  try:
453
453
  with transaction.atomic():
454
454
  risky_operation() # If this fails...
@@ -1020,7 +1020,7 @@ author.books.query.published()
1020
1020
 
1021
1021
  ### Validation
1022
1022
 
1023
- `save()` runs `full_clean()` by default — field validators, model `clean()`, and any constraints with a `validate()` method are all checked, raising `ValidationError` on violation. Pass `clean_and_validate=False` to skip it (e.g. for trusted bulk loads).
1023
+ `create()` and `update()` run `full_clean()` by default — field validators and the model's `clean()` method raising `ValidationError` on violation. Pass `clean_and_validate=False` to skip it (e.g. for trusted bulk loads). Constraints are _not_ pre-checked here; the database enforces them (see below).
1024
1024
 
1025
1025
  ```python
1026
1026
  @postgres.register_model
@@ -1041,6 +1041,33 @@ class User(postgres.Model):
1041
1041
 
1042
1042
  Field-level validation happens automatically based on field types and constraints.
1043
1043
 
1044
+ **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`.)
1045
+
1046
+ 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:
1047
+
1048
+ ```python
1049
+ try:
1050
+ with transaction.atomic():
1051
+ User.query.create(email=taken_email)
1052
+ except ValidationError:
1053
+ ... # report it — the transaction is still usable
1054
+ ```
1055
+
1056
+ 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 saves with validation already done. A direct `create()`/`update()` reports the first violation Postgres hits.
1057
+
1058
+ 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:
1059
+
1060
+ ```python
1061
+ try:
1062
+ obj.create()
1063
+ except (psycopg.IntegrityError, ValidationError):
1064
+ ... # lost a race — reload and retry, or report it
1065
+ ```
1066
+
1067
+ 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.
1068
+
1069
+ 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.
1070
+
1044
1071
  ### Indexes and constraints
1045
1072
 
1046
1073
  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`.
@@ -1063,7 +1090,7 @@ class User(postgres.Model):
1063
1090
  )
1064
1091
  ```
1065
1092
 
1066
- Constraints are also checked during `full_clean()` (which `save()` runs by default see [Validation](#validation)). Pass `violation_error` to customize the resulting `ValidationError`. It accepts anything `ValidationError(...)` accepts — a string, a `{field: message}` dict, or a fully-formed `ValidationError`:
1093
+ Constraints are checked by `validate_constraints()` — run by a `ModelForm` (and any explicit `validate_constraints()` call), but **not** by `full_clean()` (which validates shape only) or a direct `create()`/`update()`, where the database enforces them instead (see [Validation](#validation)). Pass `violation_error` to customize the resulting `ValidationError`. It accepts anything `ValidationError(...)` accepts — a string, a `{field: message}` dict, or a fully-formed `ValidationError`:
1067
1094
 
1068
1095
  ```python
1069
1096
  # Simple message — lands on NON_FIELD_ERRORS
@@ -1142,10 +1169,11 @@ class Order(postgres.Model):
1142
1169
  Enforce uniqueness and data integrity at the database level.
1143
1170
 
1144
1171
  ```python
1145
- # Bad — only validated in Python
1146
- def save(self):
1172
+ # Bad — only validated in Python (racy: two requests can both pass the check)
1173
+ def create(self):
1147
1174
  if MyModel.query.filter(email=self.email).exists():
1148
1175
  raise ValueError("duplicate")
1176
+ return super().create()
1149
1177
 
1150
1178
  # Good — database-enforced
1151
1179
  model_options = postgres.Options(
@@ -75,6 +75,8 @@ Use `Model.query` to build querysets (e.g., `User.query.filter(is_active=True)`)
75
75
  - Use `bulk_create`/`bulk_update` for batch ops, `.update()`/`.delete()` for mass ops
76
76
  - Use `.values_list()` when you only need specific columns
77
77
  - Wrap multi-step writes in `transaction.atomic()`
78
+ - Instance writes are `obj.create()` (always INSERT) and `obj.update()` (always UPDATE; `update(fields=[...])` limits the columns) — there is no `save()`, `force_insert`, or `force_update`. Constructing an instance then `create()`-ing it inserts; a hand-set `id` that collides raises `IntegrityError`.
79
+ - `create()`/`update()` raise `ValidationError` (not raw `psycopg.IntegrityError`) on a declared unique/check constraint violation, even a raced one — the DB enforces it, so inside an open `transaction.atomic()` the violation aborts the transaction (wrap the write in its own `atomic()` to catch and keep using the transaction). Set-based writes (`QuerySet.update()`/`bulk_create()`) raise raw `psycopg.IntegrityError`. Retrying on conflict? `except (psycopg.IntegrityError, ValidationError)`, or `bulk_create(..., update_conflicts=True)`
78
80
  - Always paginate list queries — unbounded querysets get slower as data grows
79
81
 
80
82
  Run `uv run plain docs postgres` for full patterns with code examples.