plain.postgres 0.100.0__tar.gz → 0.102.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 (207) hide show
  1. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/.gitignore +2 -0
  2. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/PKG-INFO +1 -2
  3. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/CHANGELOG.md +25 -0
  4. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/__init__.py +4 -0
  5. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +4 -4
  6. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/base.py +5 -13
  7. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/connection.py +12 -37
  8. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/constraints.py +32 -0
  9. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/convergence/__init__.py +2 -0
  10. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/convergence/analysis.py +398 -236
  11. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/convergence/fixes.py +16 -6
  12. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/base.py +5 -0
  13. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/related_managers.py +6 -6
  14. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/indexes.py +1 -1
  15. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/__init__.py +0 -10
  16. plain_postgres-0.102.0/plain/postgres/introspection/schema.py +201 -0
  17. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/operations/base.py +3 -2
  18. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/query.py +2 -2
  19. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sql/compiler.py +5 -5
  20. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sql/query.py +15 -8
  21. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/pyproject.toml +2 -2
  22. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_constraint_violation_error.py +43 -0
  23. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence_constraints.py +276 -15
  24. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence_indexes.py +453 -41
  25. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence_timeouts.py +5 -2
  26. plain_postgres-0.102.0/tests/test_introspection.py +226 -0
  27. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_literal_default_persistence.py +122 -29
  28. plain_postgres-0.100.0/plain/postgres/introspection/schema.py +0 -584
  29. plain_postgres-0.100.0/tests/test_introspection.py +0 -456
  30. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/CLAUDE.md +0 -0
  31. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/LICENSE +0 -0
  32. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/README.md +0 -0
  33. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/README.md +0 -0
  34. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/adapters.py +0 -0
  35. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  36. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/aggregates.py +0 -0
  37. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/__init__.py +0 -0
  38. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/converge.py +0 -0
  39. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/core.py +0 -0
  40. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/decorators.py +0 -0
  41. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/diagnose.py +0 -0
  42. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/migrations.py +0 -0
  43. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/schema.py +0 -0
  44. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/sync.py +0 -0
  45. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/config.py +0 -0
  46. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/constants.py +0 -0
  47. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/convergence/planning.py +0 -0
  48. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/database_url.py +0 -0
  49. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/db.py +0 -0
  50. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/ddl.py +0 -0
  51. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/default_settings.py +0 -0
  52. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/deletion.py +0 -0
  53. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/dialect.py +0 -0
  54. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/entrypoints.py +0 -0
  55. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/enums.py +0 -0
  56. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/exceptions.py +0 -0
  57. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/expressions.py +0 -0
  58. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/__init__.py +0 -0
  59. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/binary.py +0 -0
  60. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/boolean.py +0 -0
  61. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/duration.py +0 -0
  62. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/encrypted.py +0 -0
  63. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/json.py +0 -0
  64. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/mixins.py +0 -0
  65. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/network.py +0 -0
  66. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/numeric.py +0 -0
  67. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/primary_key.py +0 -0
  68. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/related.py +0 -0
  69. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/related_descriptors.py +0 -0
  70. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/related_lookups.py +0 -0
  71. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
  72. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/reverse_related.py +0 -0
  73. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/temporal.py +0 -0
  74. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/text.py +0 -0
  75. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/timezones.py +0 -0
  76. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/uuid.py +0 -0
  77. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/forms.py +0 -0
  78. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/__init__.py +0 -0
  79. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/comparison.py +0 -0
  80. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/datetime.py +0 -0
  81. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/math.py +0 -0
  82. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/mixins.py +0 -0
  83. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/random.py +0 -0
  84. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/text.py +0 -0
  85. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/uuid.py +0 -0
  86. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/window.py +0 -0
  87. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/__init__.py +0 -0
  88. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
  89. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  90. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
  91. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/context.py +0 -0
  92. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/helpers.py +0 -0
  93. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/ownership.py +0 -0
  94. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/runner.py +0 -0
  95. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/types.py +0 -0
  96. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/lookups.py +0 -0
  97. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/meta.py +0 -0
  98. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/middleware.py +0 -0
  99. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/__init__.py +0 -0
  100. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/autodetector.py +0 -0
  101. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/exceptions.py +0 -0
  102. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/executor.py +0 -0
  103. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/graph.py +0 -0
  104. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/loader.py +0 -0
  105. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/migration.py +0 -0
  106. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/operations/__init__.py +0 -0
  107. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/operations/fields.py +0 -0
  108. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/operations/models.py +0 -0
  109. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/operations/special.py +0 -0
  110. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/optimizer.py +0 -0
  111. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/questioner.py +0 -0
  112. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/recorder.py +0 -0
  113. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/serializer.py +0 -0
  114. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/state.py +0 -0
  115. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/utils.py +0 -0
  116. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/writer.py +0 -0
  117. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/options.py +0 -0
  118. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/otel.py +0 -0
  119. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/preflight.py +0 -0
  120. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/query_utils.py +0 -0
  121. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/registry.py +0 -0
  122. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/schema.py +0 -0
  123. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sources.py +0 -0
  124. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sql/__init__.py +0 -0
  125. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sql/constants.py +0 -0
  126. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sql/datastructures.py +0 -0
  127. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sql/where.py +0 -0
  128. {plain_postgres-0.100.0/tests/app/examples/migrations → plain_postgres-0.102.0/plain/postgres/test}/__init__.py +0 -0
  129. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/test/database.py +0 -0
  130. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/test/pytest.py +0 -0
  131. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/transaction.py +0 -0
  132. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/types.py +0 -0
  133. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/types.pyi +0 -0
  134. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/utils.py +0 -0
  135. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/forms.py +0 -0
  136. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  137. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  138. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  139. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  140. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  141. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  142. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  143. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  144. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  145. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  146. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  147. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  148. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  149. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
  150. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  151. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  152. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  153. {plain_postgres-0.100.0/plain/postgres/test → plain_postgres-0.102.0/tests/app/examples/migrations}/__init__.py +0 -0
  154. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/__init__.py +0 -0
  155. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/constraints.py +0 -0
  156. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/defaults.py +0 -0
  157. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/delete.py +0 -0
  158. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/encrypted.py +0 -0
  159. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/forms.py +0 -0
  160. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/indexes.py +0 -0
  161. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/iteration.py +0 -0
  162. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/mixins.py +0 -0
  163. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/nullability.py +0 -0
  164. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/querysets.py +0 -0
  165. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/relationships.py +0 -0
  166. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/trees.py +0 -0
  167. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/unregistered.py +0 -0
  168. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/urls.py +0 -0
  169. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/views.py +0 -0
  170. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/settings.py +0 -0
  171. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/urls.py +0 -0
  172. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/conftest.py +0 -0
  173. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/conftest_convergence.py +0 -0
  174. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_autodetector_not_null_errors.py +0 -0
  175. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_autodetector_type_change.py +0 -0
  176. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_connection_isolation.py +0 -0
  177. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_connection_lifecycle.py +0 -0
  178. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_connection_pool.py +0 -0
  179. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence.py +0 -0
  180. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence_defaults.py +0 -0
  181. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence_fk.py +0 -0
  182. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence_nullability.py +0 -0
  183. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_database_url.py +0 -0
  184. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_db_expression_defaults.py +0 -0
  185. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_delete_behaviors.py +0 -0
  186. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_diagnose.py +0 -0
  187. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_encrypted_fields.py +0 -0
  188. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_exceptions.py +0 -0
  189. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_executor_connection_hook.py +0 -0
  190. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_field_defaults.py +0 -0
  191. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_functions_uuid.py +0 -0
  192. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_health.py +0 -0
  193. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_iterator.py +0 -0
  194. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_m2m.py +0 -0
  195. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_management_connection.py +0 -0
  196. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_manager_assignment.py +0 -0
  197. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_migration_executor.py +0 -0
  198. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_mixins.py +0 -0
  199. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_modelform_roundtrip.py +0 -0
  200. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_no_callable_defaults.py +0 -0
  201. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_otel_metrics.py +0 -0
  202. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_random_string_field.py +0 -0
  203. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_raw_query.py +0 -0
  204. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_read_only_transactions.py +0 -0
  205. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_related.py +0 -0
  206. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_schema_normalize_type.py +0 -0
  207. {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_schema_timeouts.py +0 -0
@@ -4,6 +4,8 @@
4
4
  *.py[co]
5
5
  __pycache__
6
6
  *.DS_Store
7
+ *.swp
8
+ *.swo
7
9
 
8
10
  /*.code-workspace
9
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.postgres
3
- Version: 0.100.0
3
+ Version: 0.102.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
@@ -9,7 +9,6 @@ Requires-Python: >=3.13
9
9
  Requires-Dist: plain<1.0.0,>=0.134.0
10
10
  Requires-Dist: psycopg-pool>=3.2
11
11
  Requires-Dist: psycopg>=3.2
12
- Requires-Dist: sqlparse>=0.3.1
13
12
  Description-Content-Type: text/markdown
14
13
 
15
14
  # plain.postgres
@@ -1,5 +1,30 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.102.0](https://github.com/dropseed/plain/releases/plain-postgres@0.102.0) (2026-05-05)
4
+
5
+ ### What's changed
6
+
7
+ - **`Model.query` is now bound to `Self` (PEP 673), so subclasses specialize automatically.** `User.query` types as `QuerySet[User]` and `User.query.first()` as `User | None` without per-model annotations. Custom `QuerySet` subclasses (e.g. `TaskQuerySet`) are still preserved by the existing `Self`-returning descriptor. Now-redundant `cast(T, ...)` wrappers in the FK/M2M related managers are gone — `self.model.query.create(...)` already types as `T`. ([0f5b2f66](https://github.com/dropseed/plain/commit/0f5b2f66))
8
+ - **Convergence diffs are now canonicalized through Postgres `pg_get_*` round-trips on a session-private temp table** instead of sqlparse-based text normalization. Both sides of every index/constraint/default comparison are deparsed by Postgres itself, eliminating false-positive drift from formatting differences. Adds `ReadOnlyConnectionError` when the round-trip can't get DDL. The `normalize_check_definition`, `normalize_default_sql`, `normalize_expression`, `normalize_index_definition`, and `normalize_unique_definition` helpers are removed from `plain.postgres.introspection`, and `sqlparse` is no longer a dependency. ([4b42b4d1](https://github.com/dropseed/plain/commit/4b42b4d1))
9
+ - **`CheckConstraint.validate()` now exits early when a referenced field is missing from the value map**, deferring to the field-level error that excluded it. Calling `full_clean()` on a model with both `choices=` and a `CheckConstraint` referencing the same field used to crash with `AssertionError: Field lookups require a model` — the choice error excluded the field, then constraint validation tried to resolve the missing annotation. The walker is exposed as the public `CheckConstraint.referenced_fields()` method. ([d13f47d1](https://github.com/dropseed/plain/commit/d13f47d1))
10
+ - Tightened class-level annotations on `Query.select` and friends, `Operation.atomic`, and `ChoicesField.choices` for ty 0.0.33; replaced the `ModelState` `fields_cache` descriptor with a plain `__init__`. ([4b9d1db1](https://github.com/dropseed/plain/commit/4b9d1db1))
11
+ - Exposes `__version__` from `importlib.metadata` on `plain.postgres`. ([c6cf6edb](https://github.com/dropseed/plain/commit/c6cf6edb))
12
+
13
+ ### Upgrade instructions
14
+
15
+ - If you imported any of `normalize_check_definition`, `normalize_default_sql`, `normalize_expression`, `normalize_index_definition`, or `normalize_unique_definition` from `plain.postgres.introspection`, those helpers are gone — use `pg_get_indexdef` / `pg_get_constraintdef` directly or rely on the new convergence round-trip path.
16
+
17
+ ## [0.101.0](https://github.com/dropseed/plain/releases/plain-postgres@0.101.0) (2026-04-30)
18
+
19
+ ### What's changed
20
+
21
+ - **Validate CHECK constraints in the same converge run that adds them.** `AddConstraintFix` now runs `ALTER TABLE ... ADD CONSTRAINT ... NOT VALID` followed by `ALTER TABLE ... VALIDATE CONSTRAINT` in a single `apply()`. The add is catalog-only (brief lock) and validate uses `SHARE UPDATE EXCLUSIVE` (doesn't block writes), so there's no benefit to deferring validation to a later run. Existing rows are checked before convergence reports success — previously, a CHECK constraint could be added in `NOT VALID` state and the validation step was its own follow-up fix. ([dc7eb8d3c2b7](https://github.com/dropseed/plain/commit/dc7eb8d3c2b7))
22
+ - `plain-postgres` rule references updated for the simpler `plain docs` CLI (no more `--section`). ([e03c3bd8b6d3](https://github.com/dropseed/plain/commit/e03c3bd8b6d3))
23
+
24
+ ### Upgrade instructions
25
+
26
+ - No changes required. The next `plain postgres sync` (or scheduled converge run) on a database with pending CHECK constraints will now both add and validate them in one step instead of two.
27
+
3
28
  ## [0.100.0](https://github.com/dropseed/plain/releases/plain-postgres@0.100.0) (2026-04-28)
4
29
 
5
30
  ### What's changed
@@ -1,3 +1,7 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("plain.postgres")
4
+
1
5
  from .registry import models_registry, register_model # noqa Create the registry first
2
6
  from . import (
3
7
  preflight, # noqa Imported for side effects (registers preflight checks)
@@ -42,7 +42,7 @@ This means: when you add an `Index` or `UniqueConstraint` to a model, no migrati
42
42
 
43
43
  For custom data migrations, use `uv run plain migrations create --empty --name <name>` to scaffold the file.
44
44
 
45
- Run `uv run plain docs postgres --section migrations` for full workflow details.
45
+ Run `uv run plain docs postgres` for full workflow details.
46
46
 
47
47
  ## Querying
48
48
 
@@ -57,7 +57,7 @@ Use `Model.query` to build querysets (e.g., `User.query.filter(is_active=True)`)
57
57
  - Wrap multi-step writes in `transaction.atomic()`
58
58
  - Always paginate list queries — unbounded querysets get slower as data grows
59
59
 
60
- Run `uv run plain docs postgres --section querying` for full patterns with code examples.
60
+ Run `uv run plain docs postgres` for full patterns with code examples.
61
61
 
62
62
  ## Schema Design
63
63
 
@@ -68,13 +68,13 @@ Run `uv run plain docs postgres --section querying` for full patterns with code
68
68
  - Choose `on_delete` deliberately: CASCADE for owned children, RESTRICT for referenced data, SET_NULL for optional references
69
69
  - No `allow_null` on string fields — use `default=""`
70
70
 
71
- Run `uv run plain docs postgres --section constraints` for full patterns with code examples.
71
+ Run `uv run plain docs postgres` for full patterns with code examples.
72
72
 
73
73
  ## Database Doctor
74
74
 
75
75
  Use the `/plain-postgres-doctor` skill to check overall database health — migration sync, schema correctness, and operational health.
76
76
 
77
- Run `uv run plain docs postgres --section diagnostics` for check details, thresholds, and production usage.
77
+ Run `uv run plain docs postgres` for check details, thresholds, and production usage.
78
78
 
79
79
  ## Differences from Django
80
80
 
@@ -4,7 +4,7 @@ import copy
4
4
  import warnings
5
5
  from collections.abc import Iterable, Iterator, Sequence
6
6
  from itertools import chain
7
- from typing import TYPE_CHECKING, Any, cast
7
+ from typing import TYPE_CHECKING, Any, Self, cast
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from plain.postgres.meta import Meta
@@ -68,16 +68,6 @@ class ModelBase(type):
68
68
  return super().__new__(cls, name, bases, attrs, **kwargs)
69
69
 
70
70
 
71
- class ModelStateFieldsCacheDescriptor:
72
- def __get__(
73
- self, instance: ModelState | None, cls: type | None = None
74
- ) -> ModelStateFieldsCacheDescriptor | dict[str, Any]:
75
- if instance is None:
76
- return self
77
- res = instance.fields_cache = {}
78
- return res
79
-
80
-
81
71
  class ModelState:
82
72
  """Store model instance state."""
83
73
 
@@ -86,7 +76,9 @@ class ModelState:
86
76
  # explicit (non-auto) PKs. This impacts validation only; it has no effect
87
77
  # on the actual save.
88
78
  adding = True
89
- fields_cache = ModelStateFieldsCacheDescriptor()
79
+
80
+ def __init__(self) -> None:
81
+ self.fields_cache: dict[str, Any] = {}
90
82
 
91
83
 
92
84
  class Model(metaclass=ModelBase):
@@ -94,7 +86,7 @@ class Model(metaclass=ModelBase):
94
86
  id: int = types.PrimaryKeyField()
95
87
 
96
88
  # Descriptors for other model behavior
97
- query: QuerySet[Model] = QuerySet()
89
+ query: QuerySet[Self] = QuerySet()
98
90
  model_options: Options = Options()
99
91
  _model_meta: Meta = Meta()
100
92
  DoesNotExist = DoesNotExistDescriptor()
@@ -15,7 +15,6 @@ from plain.logs import get_framework_logger
15
15
  from plain.postgres import utils
16
16
  from plain.postgres.dialect import quote_name
17
17
  from plain.postgres.fields import GenericIPAddressField, TimeField, UUIDField
18
- from plain.postgres.indexes import Index
19
18
  from plain.postgres.schema import DatabaseSchemaEditor
20
19
  from plain.postgres.sources import ConnectionSource
21
20
  from plain.postgres.transaction import TransactionManagementError
@@ -63,7 +62,6 @@ class DatabaseConnection:
63
62
 
64
63
  queries_limit: int = 9000
65
64
 
66
- index_default_access_method = "btree"
67
65
  ignored_tables: list[str] = []
68
66
 
69
67
  def __init__(self, source: ConnectionSource):
@@ -607,7 +605,6 @@ class DatabaseConnection:
607
605
  FROM pg_attribute AS fka
608
606
  JOIN pg_class AS fkc ON fka.attrelid = fkc.oid
609
607
  WHERE fka.attrelid = c.confrelid AND fka.attnum = c.confkey[1]),
610
- cl.reloptions,
611
608
  c.convalidated,
612
609
  pg_get_constraintdef(c.oid),
613
610
  c.confdeltype
@@ -622,54 +619,42 @@ class DatabaseConnection:
622
619
  columns,
623
620
  kind,
624
621
  used_cols,
625
- options,
626
622
  validated,
627
623
  constraintdef,
628
624
  confdeltype,
629
625
  ) in cursor.fetchall():
630
626
  constraints[constraint] = {
631
627
  "columns": columns,
632
- "primary_key": kind == "p",
633
- "unique": kind in ["p", "u"],
634
628
  "foreign_key": tuple(used_cols.split(".", 1)) if kind == "f" else None,
635
- "check": kind == "c",
636
629
  "contype": kind,
637
630
  "index": False,
638
631
  "definition": constraintdef,
639
- "options": options,
640
632
  "validated": validated,
641
633
  "on_delete_action": confdeltype if kind == "f" else None,
642
634
  }
643
- # Now get indexes
635
+ # Now get indexes. Sort order, opclasses, INCLUDE, and predicates all
636
+ # ride along inside `pg_get_indexdef` and are compared via the
637
+ # canonical-tail round-trip in convergence — no need to introspect
638
+ # them here as separate columns.
644
639
  cursor.execute(
645
640
  """
646
641
  SELECT
647
642
  indexname,
648
643
  array_agg(attname ORDER BY arridx),
649
644
  indisunique,
650
- indisprimary,
651
- array_agg(ordering ORDER BY arridx),
652
645
  amname,
653
646
  exprdef,
654
- s2.attoptions,
655
- s2.indisvalid
647
+ indisvalid
656
648
  FROM (
657
649
  SELECT
658
650
  c2.relname as indexname, idx.*, attr.attname, am.amname,
659
- pg_get_indexdef(idx.indexrelid) AS exprdef,
660
- CASE am.amname
661
- WHEN %s THEN
662
- CASE (option & 1)
663
- WHEN 1 THEN 'DESC' ELSE 'ASC'
664
- END
665
- END as ordering,
666
- c2.reloptions as attoptions
651
+ pg_get_indexdef(idx.indexrelid) AS exprdef
667
652
  FROM (
668
653
  SELECT *
669
654
  FROM
670
655
  pg_index i,
671
- unnest(i.indkey, i.indoption)
672
- WITH ORDINALITY koi(key, option, arridx)
656
+ unnest(i.indkey)
657
+ WITH ORDINALITY koi(key, arridx)
673
658
  ) idx
674
659
  LEFT JOIN pg_class c ON idx.indrelid = c.oid
675
660
  LEFT JOIN pg_class c2 ON idx.indexrelid = c2.oid
@@ -678,36 +663,26 @@ class DatabaseConnection:
678
663
  pg_attribute attr ON attr.attrelid = c.oid AND attr.attnum = idx.key
679
664
  WHERE c.relname = %s AND pg_catalog.pg_table_is_visible(c.oid)
680
665
  ) s2
681
- GROUP BY indexname, indisunique, indisprimary, amname, exprdef, attoptions, indisvalid;
666
+ GROUP BY
667
+ indexname, indisunique, amname, exprdef, indisvalid;
682
668
  """,
683
- [self.index_default_access_method, table_name],
669
+ [table_name],
684
670
  )
685
671
  for (
686
672
  index,
687
673
  columns,
688
674
  unique,
689
- primary,
690
- orders,
691
675
  type_,
692
676
  definition,
693
- options,
694
677
  valid,
695
678
  ) in cursor.fetchall():
696
679
  if index not in constraints:
697
- basic_index = (
698
- type_ == self.index_default_access_method and options is None
699
- )
700
680
  constraints[index] = {
701
681
  "columns": columns if columns != [None] else [],
702
- "orders": orders if orders != [None] else [],
703
- "primary_key": primary,
704
682
  "unique": unique,
705
- "foreign_key": None,
706
- "check": False,
707
683
  "index": True,
708
- "type": Index.suffix if basic_index else type_,
684
+ "type": type_,
709
685
  "definition": definition,
710
- "options": options,
711
686
  "valid": valid,
712
687
  }
713
688
  return constraints
@@ -5,6 +5,7 @@ from types import NoneType
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from plain.exceptions import ValidationError
8
+ from plain.postgres.constants import LOOKUP_SEP
8
9
  from plain.postgres.ddl import (
9
10
  build_include_sql,
10
11
  compile_expression_sql,
@@ -104,10 +105,41 @@ class CheckConstraint(BaseConstraint):
104
105
  sql += " NOT VALID"
105
106
  return sql
106
107
 
108
+ def referenced_fields(self) -> set[str]:
109
+ """Top-level model field names referenced by `self.check`.
110
+
111
+ Walks lookup keys (`field__regex` → `field`), nested Q nodes, and
112
+ F-expressions in values or other source expressions.
113
+ """
114
+ fields: set[str] = set()
115
+
116
+ def visit(node: Any) -> None:
117
+ if isinstance(node, Q):
118
+ for child in node.children:
119
+ visit(child)
120
+ elif isinstance(node, tuple) and len(node) == 2:
121
+ lookup, value = node
122
+ fields.add(lookup.split(LOOKUP_SEP, 1)[0])
123
+ visit(value)
124
+ elif isinstance(node, F):
125
+ fields.add(node.name.split(LOOKUP_SEP, 1)[0])
126
+ elif hasattr(node, "get_source_expressions"):
127
+ for sub in node.get_source_expressions():
128
+ visit(sub)
129
+
130
+ visit(self.check)
131
+ return fields
132
+
107
133
  def validate(
108
134
  self, model: type[Model], instance: Model, exclude: set[str] | None = None
109
135
  ) -> None:
110
136
  against = instance._get_field_value_map(meta=model._model_meta, exclude=exclude)
137
+ # Skip the check entirely when any field referenced by `self.check` was
138
+ # excluded — the in-Python pipeline can't resolve a missing field's
139
+ # annotation, and surfacing a constraint violation here would just
140
+ # duplicate the field-level error that caused the exclusion.
141
+ if not self.referenced_fields().issubset(against):
142
+ return
111
143
  try:
112
144
  if not Q(self.check).check(against):
113
145
  raise self._build_violation_error()
@@ -11,6 +11,7 @@ from .analysis import (
11
11
  IndexStatus,
12
12
  ModelAnalysis,
13
13
  NullabilityDrift,
14
+ ReadOnlyConnectionError,
14
15
  analyze_model,
15
16
  )
16
17
  from .fixes import (
@@ -65,6 +66,7 @@ __all__ = [
65
66
  "ModelAnalysis",
66
67
  "NullabilityDrift",
67
68
  "PlanItem",
69
+ "ReadOnlyConnectionError",
68
70
  "RebuildIndexFix",
69
71
  "RenameConstraintFix",
70
72
  "RenameIndexFix",