plain.postgres 0.101.0__tar.gz → 0.103.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 (210) hide show
  1. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/.gitignore +2 -0
  2. plain_postgres-0.101.0/plain/postgres/README.md → plain_postgres-0.103.0/PKG-INFO +45 -11
  3. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/CHANGELOG.md +26 -0
  4. plain_postgres-0.101.0/PKG-INFO → plain_postgres-0.103.0/plain/postgres/README.md +32 -25
  5. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/__init__.py +4 -0
  6. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +1 -1
  7. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/base.py +5 -13
  8. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/connection.py +12 -37
  9. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/constraints.py +32 -0
  10. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/convergence/__init__.py +8 -0
  11. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/convergence/analysis.py +498 -238
  12. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/convergence/fixes.py +42 -0
  13. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/convergence/planning.py +13 -0
  14. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/base.py +5 -0
  15. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/related_managers.py +6 -6
  16. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/indexes.py +1 -1
  17. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/__init__.py +0 -10
  18. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/checks_cumulative.py +205 -9
  19. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/runner.py +19 -0
  20. plain_postgres-0.103.0/plain/postgres/introspection/schema.py +256 -0
  21. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/base.py +3 -2
  22. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/options.py +11 -4
  23. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/query.py +2 -2
  24. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sql/compiler.py +5 -5
  25. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sql/query.py +15 -8
  26. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/pyproject.toml +2 -2
  27. plain_postgres-0.103.0/tests/app/examples/migrations/0018_storageparametersexample.py +18 -0
  28. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/__init__.py +1 -0
  29. plain_postgres-0.103.0/tests/app/examples/models/storage_parameters.py +14 -0
  30. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_constraint_violation_error.py +43 -0
  31. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_constraints.py +274 -0
  32. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_indexes.py +453 -41
  33. plain_postgres-0.103.0/tests/internal/test_convergence_storage_parameters.py +222 -0
  34. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_diagnose.py +1 -0
  35. plain_postgres-0.103.0/tests/internal/test_introspection.py +226 -0
  36. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_literal_default_persistence.py +122 -29
  37. plain_postgres-0.101.0/plain/postgres/introspection/schema.py +0 -584
  38. plain_postgres-0.101.0/tests/test_introspection.py +0 -456
  39. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/CLAUDE.md +0 -0
  40. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/LICENSE +0 -0
  41. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/README.md +0 -0
  42. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/adapters.py +0 -0
  43. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  44. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/aggregates.py +0 -0
  45. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/__init__.py +0 -0
  46. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/converge.py +0 -0
  47. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/core.py +0 -0
  48. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/decorators.py +0 -0
  49. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/diagnose.py +0 -0
  50. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/migrations.py +0 -0
  51. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/schema.py +0 -0
  52. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/cli/sync.py +0 -0
  53. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/config.py +0 -0
  54. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/constants.py +0 -0
  55. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/database_url.py +0 -0
  56. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/db.py +0 -0
  57. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/ddl.py +0 -0
  58. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/default_settings.py +0 -0
  59. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/deletion.py +0 -0
  60. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/dialect.py +0 -0
  61. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/entrypoints.py +0 -0
  62. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/enums.py +0 -0
  63. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/exceptions.py +0 -0
  64. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/expressions.py +0 -0
  65. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/__init__.py +0 -0
  66. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/binary.py +0 -0
  67. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/boolean.py +0 -0
  68. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/duration.py +0 -0
  69. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/encrypted.py +0 -0
  70. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/json.py +0 -0
  71. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/mixins.py +0 -0
  72. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/network.py +0 -0
  73. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/numeric.py +0 -0
  74. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/primary_key.py +0 -0
  75. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/related.py +0 -0
  76. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/related_descriptors.py +0 -0
  77. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/related_lookups.py +0 -0
  78. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
  79. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/reverse_related.py +0 -0
  80. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/temporal.py +0 -0
  81. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/text.py +0 -0
  82. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/timezones.py +0 -0
  83. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/fields/uuid.py +0 -0
  84. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/forms.py +0 -0
  85. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/__init__.py +0 -0
  86. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/comparison.py +0 -0
  87. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/datetime.py +0 -0
  88. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/math.py +0 -0
  89. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/mixins.py +0 -0
  90. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/random.py +0 -0
  91. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/text.py +0 -0
  92. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/uuid.py +0 -0
  93. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/functions/window.py +0 -0
  94. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/__init__.py +0 -0
  95. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  96. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
  97. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/context.py +0 -0
  98. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/helpers.py +0 -0
  99. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/ownership.py +0 -0
  100. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/introspection/health/types.py +0 -0
  101. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/lookups.py +0 -0
  102. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/meta.py +0 -0
  103. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/middleware.py +0 -0
  104. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/__init__.py +0 -0
  105. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/autodetector.py +0 -0
  106. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/exceptions.py +0 -0
  107. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/executor.py +0 -0
  108. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/graph.py +0 -0
  109. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/loader.py +0 -0
  110. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/migration.py +0 -0
  111. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/__init__.py +0 -0
  112. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/fields.py +0 -0
  113. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/models.py +0 -0
  114. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/operations/special.py +0 -0
  115. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/optimizer.py +0 -0
  116. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/questioner.py +0 -0
  117. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/recorder.py +0 -0
  118. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/serializer.py +0 -0
  119. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/state.py +0 -0
  120. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/utils.py +0 -0
  121. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/migrations/writer.py +0 -0
  122. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/otel.py +0 -0
  123. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/preflight.py +0 -0
  124. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/query_utils.py +0 -0
  125. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/registry.py +0 -0
  126. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/schema.py +0 -0
  127. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sources.py +0 -0
  128. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sql/__init__.py +0 -0
  129. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sql/constants.py +0 -0
  130. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sql/datastructures.py +0 -0
  131. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/sql/where.py +0 -0
  132. {plain_postgres-0.101.0/tests/app/examples/migrations → plain_postgres-0.103.0/plain/postgres/test}/__init__.py +0 -0
  133. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/test/database.py +0 -0
  134. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/test/pytest.py +0 -0
  135. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/transaction.py +0 -0
  136. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/types.py +0 -0
  137. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/types.pyi +0 -0
  138. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/plain/postgres/utils.py +0 -0
  139. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/forms.py +0 -0
  140. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  141. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  142. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  143. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  144. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  145. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  146. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  147. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  148. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  149. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  150. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  151. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  152. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  153. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
  154. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  155. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  156. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  157. {plain_postgres-0.101.0/plain/postgres/test → plain_postgres-0.103.0/tests/app/examples/migrations}/__init__.py +0 -0
  158. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/constraints.py +0 -0
  159. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/defaults.py +0 -0
  160. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/delete.py +0 -0
  161. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/encrypted.py +0 -0
  162. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/forms.py +0 -0
  163. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/indexes.py +0 -0
  164. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/iteration.py +0 -0
  165. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/mixins.py +0 -0
  166. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/nullability.py +0 -0
  167. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/querysets.py +0 -0
  168. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/relationships.py +0 -0
  169. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/trees.py +0 -0
  170. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/models/unregistered.py +0 -0
  171. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/urls.py +0 -0
  172. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/examples/views.py +0 -0
  173. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/settings.py +0 -0
  174. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/app/urls.py +0 -0
  175. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/conftest.py +0 -0
  176. {plain_postgres-0.101.0 → plain_postgres-0.103.0}/tests/conftest_convergence.py +0 -0
  177. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_autodetector_not_null_errors.py +0 -0
  178. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_autodetector_type_change.py +0 -0
  179. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_connection_isolation.py +0 -0
  180. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_connection_lifecycle.py +0 -0
  181. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_connection_pool.py +0 -0
  182. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence.py +0 -0
  183. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_defaults.py +0 -0
  184. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_fk.py +0 -0
  185. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_nullability.py +0 -0
  186. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_convergence_timeouts.py +0 -0
  187. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_db_expression_defaults.py +0 -0
  188. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_executor_connection_hook.py +0 -0
  189. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_health.py +0 -0
  190. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_management_connection.py +0 -0
  191. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_migration_executor.py +0 -0
  192. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_no_callable_defaults.py +0 -0
  193. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_otel_metrics.py +0 -0
  194. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_schema_normalize_type.py +0 -0
  195. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/internal}/test_schema_timeouts.py +0 -0
  196. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_database_url.py +0 -0
  197. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_delete_behaviors.py +0 -0
  198. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_encrypted_fields.py +0 -0
  199. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_exceptions.py +0 -0
  200. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_field_defaults.py +0 -0
  201. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_functions_uuid.py +0 -0
  202. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_iterator.py +0 -0
  203. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_m2m.py +0 -0
  204. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_manager_assignment.py +0 -0
  205. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_mixins.py +0 -0
  206. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_modelform_roundtrip.py +0 -0
  207. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_random_string_field.py +0 -0
  208. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_raw_query.py +0 -0
  209. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_read_only_transactions.py +0 -0
  210. {plain_postgres-0.101.0/tests → plain_postgres-0.103.0/tests/public}/test_related.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,3 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: plain.postgres
3
+ Version: 0.103.0
4
+ Summary: Model your data and store it in a database.
5
+ Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
+ License-Expression: BSD-3-Clause
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.13
9
+ Requires-Dist: plain<1.0.0,>=0.134.0
10
+ Requires-Dist: psycopg-pool>=3.2
11
+ Requires-Dist: psycopg>=3.2
12
+ Description-Content-Type: text/markdown
13
+
1
14
  # plain.postgres
2
15
 
3
16
  **Model your data and store it in a database.**
@@ -687,7 +700,7 @@ Or use `RunSQL` with explicit cascade if the relationship is large.
687
700
 
688
701
  ### Convergence
689
702
 
690
- Convergence compares the indexes, constraints, foreign keys, and nullability declared on your models against what actually exists in the database, then applies fixes to make them match. You don't need to create migrations for these — just declare them on your model and run `postgres sync`.
703
+ Convergence compares the indexes, constraints, foreign keys, nullability, and [storage parameters](#storage-parameters) declared on your models against what actually exists in the database, then applies fixes to make them match. You don't need to create migrations for these — just declare them on your model and run `postgres sync`.
691
704
 
692
705
  ```python
693
706
  @postgres.register_model
@@ -1078,6 +1091,26 @@ A `code` becomes `ValidationError.code` — useful for test assertions, error tr
1078
1091
 
1079
1092
  `UniqueConstraint` accepts the same `violation_error`. With a single-field unique constraint, a string `violation_error="That email is taken."` auto-routes to that field; otherwise (multi-field, expressions, or a CheckConstraint) errors land on `NON_FIELD_ERRORS` unless you pass the dict form. See [BaseConstraint](./constraints.py#BaseConstraint) for the full signature.
1080
1093
 
1094
+ ### Storage parameters
1095
+
1096
+ Per-table Postgres [storage parameters](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS) (`pg_class.reloptions`) are declared on `model_options` and managed by [convergence](#convergence) — `postgres sync` issues the `ALTER TABLE … SET/RESET (...)` to make the live table match. They're not serialized into migrations.
1097
+
1098
+ ```python
1099
+ class CachedItem(postgres.Model):
1100
+ ...
1101
+
1102
+ model_options = postgres.Options(
1103
+ storage_parameters={
1104
+ # Tighter autovacuum on a churn-heavy table
1105
+ "autovacuum_vacuum_scale_factor": 0.1,
1106
+ # TOAST has its own autovacuum schedule — prefix with `toast.`
1107
+ "toast.autovacuum_vacuum_scale_factor": 0.05,
1108
+ },
1109
+ )
1110
+ ```
1111
+
1112
+ Models are the source of truth: undeclared parameters set on the live table are reset on the next sync. Use this for autovacuum tuning, `fillfactor`, TOAST options, etc. — anything you'd otherwise apply by hand with `ALTER TABLE … SET (...)`.
1113
+
1081
1114
  ### Schema design
1082
1115
 
1083
1116
  #### Index fields used in filters and ordering
@@ -1277,16 +1310,17 @@ fix in their model code. (In JSON output each finding still carries
1277
1310
  it.) Each finding still carries the exact SQL in its suggestion for anyone
1278
1311
  who wants to act.
1279
1312
 
1280
- | Finding | What it reports |
1281
- | ------------------- | -------------------------------------------------------------------------------- |
1282
- | **Stats freshness** | Tables whose planner statistics are missing (never analyzed) or stale |
1283
- | **Vacuum health** | Tables with >10% dead tuples |
1284
- | **Index bloat** | btree indexes with significant estimated wasted space (≥10 MB, ioguix estimator) |
1313
+ | Finding | What it reports |
1314
+ | ------------------- | ------------------------------------------------------------------------------------------ |
1315
+ | **Stats freshness** | Tables whose planner statistics are missing (never analyzed) or stale |
1316
+ | **Vacuum health** | Tables with >10% dead tuples |
1317
+ | **Table bloat** | Tables with significant estimated wasted space (≥100 MB AND ≥25% bloat, ioguix estimator) |
1318
+ | **Index bloat** | btree indexes with significant estimated wasted space (≥100 MB AND ≥30%, ioguix estimator) |
1285
1319
 
1286
- If a future release exposes per-table autovacuum / fillfactor parameters in
1287
- `model_options` (see the `postgres-model-storage-parameters` arc), these
1288
- findings can graduate back to the warning tier because the remedy will be
1289
- expressible in code.
1320
+ Now that per-table autovacuum / fillfactor knobs are expressible in
1321
+ [storage parameters](#storage-parameters) on `model_options`, these
1322
+ findings may graduate back to the warning tier in a future release the
1323
+ remedy is now in code.
1290
1324
 
1291
1325
  ### Informational context
1292
1326
 
@@ -1335,7 +1369,7 @@ heroku run -a your-app "plain postgres diagnose --json"
1335
1369
 
1336
1370
  The `--json` flag must be quoted so Heroku passes it through to the command.
1337
1371
 
1338
- Cumulative-stat checks (`stats_freshness`, `vacuum_health`, `unused_indexes`, `missing_index_candidates`, `index_bloat`) need cumulative stat history after the last reset to be reliable. Check the `stats_reset` informational to see how much history you have. (Note: this list spans both the warning and operational tiers — the common thread is that all five depend on counters that `pg_stat_reset()` wipes.)
1372
+ Cumulative-stat checks (`stats_freshness`, `vacuum_health`, `unused_indexes`, `missing_index_candidates`, `table_bloat`, `index_bloat`) need cumulative stat history after the last reset to be reliable. Check the `stats_reset` informational to see how much history you have. (Note: this list spans both the warning and operational tiers — the common thread is that all five depend on counters that `pg_stat_reset()` wipes.)
1339
1373
 
1340
1374
  ### Preflight checks
1341
1375
 
@@ -1,5 +1,31 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.103.0](https://github.com/dropseed/plain/releases/plain-postgres@0.103.0) (2026-05-06)
4
+
5
+ ### What's changed
6
+
7
+ - **New `storage_parameters` on `model_options`, managed by convergence.** Declare per-table Postgres storage parameters (`pg_class.reloptions`) on the model — autovacuum tuning, `fillfactor`, TOAST options, anything you'd otherwise set with `ALTER TABLE … SET (...)` — and `plain postgres sync` reconciles them via instant catalog-only `ALTER TABLE … SET / RESET (...)` statements. Models are the source of truth: parameters set on the live table that aren't declared on the model get reset, matching how indexes and constraints work. TOAST parameters use a `toast.` prefix (`toast.autovacuum_vacuum_scale_factor`) and are stored on the toast relation. Storage parameters are not serialized into migrations. New public API: `StorageParameterDrift`, `SetStorageParameterFix`, `ResetStorageParameterFix`. ([7fe40f72](https://github.com/dropseed/plain/commit/7fe40f72))
8
+ - **New `table_bloat` health check.** Estimates per-table page-level bloat using the ioguix estimator (same heuristic as pghero). Complements `vacuum_health`: dead-tuple counts only show what autovacuum hasn't reclaimed yet, but a table that's been vacuumed regularly can still carry gigabytes of bloat because plain `VACUUM` marks pages reusable without returning space to the OS. Surfaces tables with both >100 MB wasted bytes AND >25% bloat ratio, with `pg_repack` / `pg_squeeze` / `VACUUM FULL` suggestions. Cross-check caveats now link `vacuum_health` and `table_bloat` findings on the same table. ([ee9dc1d5](https://github.com/dropseed/plain/commit/ee9dc1d5))
9
+ - **Tightened `index_bloat` thresholds.** Now requires both >100 MB wasted bytes AND >30% bloat ratio (was 10 MB only). The previous floor surfaced too many small, healthy indexes; the higher percentage bar reflects that `REINDEX CONCURRENTLY` is cheap so it's only worth flagging genuinely degraded indexes. Results are also capped at 100 rows per check. ([ee9dc1d5](https://github.com/dropseed/plain/commit/ee9dc1d5))
10
+
11
+ ### Upgrade instructions
12
+
13
+ - No changes required. To opt into the new `storage_parameters` API, declare them on `model_options = postgres.Options(storage_parameters={...})` and run `plain postgres sync`. After upgrading, expect previously-noisy `index_bloat` findings to disappear (now require ≥100 MB AND ≥30%) and the new `table_bloat` check to appear in `plain postgres diagnose`.
14
+
15
+ ## [0.102.0](https://github.com/dropseed/plain/releases/plain-postgres@0.102.0) (2026-05-05)
16
+
17
+ ### What's changed
18
+
19
+ - **`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))
20
+ - **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))
21
+ - **`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))
22
+ - 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))
23
+ - Exposes `__version__` from `importlib.metadata` on `plain.postgres`. ([c6cf6edb](https://github.com/dropseed/plain/commit/c6cf6edb))
24
+
25
+ ### Upgrade instructions
26
+
27
+ - 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.
28
+
3
29
  ## [0.101.0](https://github.com/dropseed/plain/releases/plain-postgres@0.101.0) (2026-04-30)
4
30
 
5
31
  ### What's changed
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: plain.postgres
3
- Version: 0.101.0
4
- Summary: Model your data and store it in a database.
5
- Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
- License-Expression: BSD-3-Clause
7
- License-File: LICENSE
8
- Requires-Python: >=3.13
9
- Requires-Dist: plain<1.0.0,>=0.134.0
10
- Requires-Dist: psycopg-pool>=3.2
11
- Requires-Dist: psycopg>=3.2
12
- Requires-Dist: sqlparse>=0.3.1
13
- Description-Content-Type: text/markdown
14
-
15
1
  # plain.postgres
16
2
 
17
3
  **Model your data and store it in a database.**
@@ -701,7 +687,7 @@ Or use `RunSQL` with explicit cascade if the relationship is large.
701
687
 
702
688
  ### Convergence
703
689
 
704
- Convergence compares the indexes, constraints, foreign keys, and nullability declared on your models against what actually exists in the database, then applies fixes to make them match. You don't need to create migrations for these — just declare them on your model and run `postgres sync`.
690
+ Convergence compares the indexes, constraints, foreign keys, nullability, and [storage parameters](#storage-parameters) declared on your models against what actually exists in the database, then applies fixes to make them match. You don't need to create migrations for these — just declare them on your model and run `postgres sync`.
705
691
 
706
692
  ```python
707
693
  @postgres.register_model
@@ -1092,6 +1078,26 @@ A `code` becomes `ValidationError.code` — useful for test assertions, error tr
1092
1078
 
1093
1079
  `UniqueConstraint` accepts the same `violation_error`. With a single-field unique constraint, a string `violation_error="That email is taken."` auto-routes to that field; otherwise (multi-field, expressions, or a CheckConstraint) errors land on `NON_FIELD_ERRORS` unless you pass the dict form. See [BaseConstraint](./constraints.py#BaseConstraint) for the full signature.
1094
1080
 
1081
+ ### Storage parameters
1082
+
1083
+ Per-table Postgres [storage parameters](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS) (`pg_class.reloptions`) are declared on `model_options` and managed by [convergence](#convergence) — `postgres sync` issues the `ALTER TABLE … SET/RESET (...)` to make the live table match. They're not serialized into migrations.
1084
+
1085
+ ```python
1086
+ class CachedItem(postgres.Model):
1087
+ ...
1088
+
1089
+ model_options = postgres.Options(
1090
+ storage_parameters={
1091
+ # Tighter autovacuum on a churn-heavy table
1092
+ "autovacuum_vacuum_scale_factor": 0.1,
1093
+ # TOAST has its own autovacuum schedule — prefix with `toast.`
1094
+ "toast.autovacuum_vacuum_scale_factor": 0.05,
1095
+ },
1096
+ )
1097
+ ```
1098
+
1099
+ Models are the source of truth: undeclared parameters set on the live table are reset on the next sync. Use this for autovacuum tuning, `fillfactor`, TOAST options, etc. — anything you'd otherwise apply by hand with `ALTER TABLE … SET (...)`.
1100
+
1095
1101
  ### Schema design
1096
1102
 
1097
1103
  #### Index fields used in filters and ordering
@@ -1291,16 +1297,17 @@ fix in their model code. (In JSON output each finding still carries
1291
1297
  it.) Each finding still carries the exact SQL in its suggestion for anyone
1292
1298
  who wants to act.
1293
1299
 
1294
- | Finding | What it reports |
1295
- | ------------------- | -------------------------------------------------------------------------------- |
1296
- | **Stats freshness** | Tables whose planner statistics are missing (never analyzed) or stale |
1297
- | **Vacuum health** | Tables with >10% dead tuples |
1298
- | **Index bloat** | btree indexes with significant estimated wasted space (≥10 MB, ioguix estimator) |
1300
+ | Finding | What it reports |
1301
+ | ------------------- | ------------------------------------------------------------------------------------------ |
1302
+ | **Stats freshness** | Tables whose planner statistics are missing (never analyzed) or stale |
1303
+ | **Vacuum health** | Tables with >10% dead tuples |
1304
+ | **Table bloat** | Tables with significant estimated wasted space (≥100 MB AND ≥25% bloat, ioguix estimator) |
1305
+ | **Index bloat** | btree indexes with significant estimated wasted space (≥100 MB AND ≥30%, ioguix estimator) |
1299
1306
 
1300
- If a future release exposes per-table autovacuum / fillfactor parameters in
1301
- `model_options` (see the `postgres-model-storage-parameters` arc), these
1302
- findings can graduate back to the warning tier because the remedy will be
1303
- expressible in code.
1307
+ Now that per-table autovacuum / fillfactor knobs are expressible in
1308
+ [storage parameters](#storage-parameters) on `model_options`, these
1309
+ findings may graduate back to the warning tier in a future release the
1310
+ remedy is now in code.
1304
1311
 
1305
1312
  ### Informational context
1306
1313
 
@@ -1349,7 +1356,7 @@ heroku run -a your-app "plain postgres diagnose --json"
1349
1356
 
1350
1357
  The `--json` flag must be quoted so Heroku passes it through to the command.
1351
1358
 
1352
- Cumulative-stat checks (`stats_freshness`, `vacuum_health`, `unused_indexes`, `missing_index_candidates`, `index_bloat`) need cumulative stat history after the last reset to be reliable. Check the `stats_reset` informational to see how much history you have. (Note: this list spans both the warning and operational tiers — the common thread is that all five depend on counters that `pg_stat_reset()` wipes.)
1359
+ Cumulative-stat checks (`stats_freshness`, `vacuum_health`, `unused_indexes`, `missing_index_candidates`, `table_bloat`, `index_bloat`) need cumulative stat history after the last reset to be reliable. Check the `stats_reset` informational to see how much history you have. (Note: this list spans both the warning and operational tiers — the common thread is that all five depend on counters that `pg_stat_reset()` wipes.)
1353
1360
 
1354
1361
  ### Preflight checks
1355
1362
 
@@ -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)
@@ -36,7 +36,7 @@ Get approval before writing any model code or generating migrations.
36
36
  `uv run plain postgres sync` runs three steps: create migrations → apply migrations → converge schema.
37
37
 
38
38
  - **Migrations** handle tables and columns (CreateModel, AddField, AlterField, etc.)
39
- - **Convergence** handles indexes, constraints, and FK constraints — declared on the model but NOT serialized into migration files. (FK _columns_ like `team_id bigint` are created by migrations; the actual `FOREIGN KEY` constraint is added by convergence.)
39
+ - **Convergence** handles indexes, constraints, FK constraints, and storage parameters — declared on the model but NOT serialized into migration files. (FK _columns_ like `team_id bigint` are created by migrations; the actual `FOREIGN KEY` constraint is added by convergence.)
40
40
 
41
41
  This means: when you add an `Index` or `UniqueConstraint` to a model, no migration is generated. The converge step reads the live model class and syncs the database directly. Don't worry about serializing constraint expressions (like `Lower()`) for migrations — they never go there.
42
42
 
@@ -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,8 @@ from .analysis import (
11
11
  IndexStatus,
12
12
  ModelAnalysis,
13
13
  NullabilityDrift,
14
+ ReadOnlyConnectionError,
15
+ StorageParameterDrift,
14
16
  analyze_model,
15
17
  )
16
18
  from .fixes import (
@@ -25,8 +27,10 @@ from .fixes import (
25
27
  RebuildIndexFix,
26
28
  RenameConstraintFix,
27
29
  RenameIndexFix,
30
+ ResetStorageParameterFix,
28
31
  SetColumnDefaultFix,
29
32
  SetNotNullFix,
33
+ SetStorageParameterFix,
30
34
  ValidateConstraintFix,
31
35
  )
32
36
  from .planning import (
@@ -65,11 +69,15 @@ __all__ = [
65
69
  "ModelAnalysis",
66
70
  "NullabilityDrift",
67
71
  "PlanItem",
72
+ "ReadOnlyConnectionError",
68
73
  "RebuildIndexFix",
69
74
  "RenameConstraintFix",
70
75
  "RenameIndexFix",
76
+ "ResetStorageParameterFix",
71
77
  "SetColumnDefaultFix",
72
78
  "SetNotNullFix",
79
+ "SetStorageParameterFix",
80
+ "StorageParameterDrift",
73
81
  "ValidateConstraintFix",
74
82
  "analyze_model",
75
83
  "can_auto_fix",