plain.postgres 0.103.0__tar.gz → 0.103.1__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 (208) hide show
  1. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/PKG-INFO +1 -1
  2. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/CHANGELOG.md +10 -0
  3. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/introspection/health/checks_structural.py +50 -30
  4. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/preflight.py +49 -15
  5. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/pyproject.toml +1 -1
  6. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_diagnose.py +69 -0
  7. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/.gitignore +0 -0
  8. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/CLAUDE.md +0 -0
  9. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/LICENSE +0 -0
  10. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/README.md +0 -0
  11. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/README.md +0 -0
  12. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/__init__.py +0 -0
  13. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/adapters.py +0 -0
  14. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
  15. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  16. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/aggregates.py +0 -0
  17. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/base.py +0 -0
  18. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/cli/__init__.py +0 -0
  19. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/cli/converge.py +0 -0
  20. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/cli/core.py +0 -0
  21. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/cli/decorators.py +0 -0
  22. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/cli/diagnose.py +0 -0
  23. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/cli/migrations.py +0 -0
  24. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/cli/schema.py +0 -0
  25. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/cli/sync.py +0 -0
  26. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/config.py +0 -0
  27. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/connection.py +0 -0
  28. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/constants.py +0 -0
  29. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/constraints.py +0 -0
  30. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/convergence/__init__.py +0 -0
  31. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/convergence/analysis.py +0 -0
  32. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/convergence/fixes.py +0 -0
  33. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/convergence/planning.py +0 -0
  34. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/database_url.py +0 -0
  35. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/db.py +0 -0
  36. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/ddl.py +0 -0
  37. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/default_settings.py +0 -0
  38. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/deletion.py +0 -0
  39. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/dialect.py +0 -0
  40. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/entrypoints.py +0 -0
  41. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/enums.py +0 -0
  42. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/exceptions.py +0 -0
  43. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/expressions.py +0 -0
  44. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/__init__.py +0 -0
  45. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/base.py +0 -0
  46. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/binary.py +0 -0
  47. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/boolean.py +0 -0
  48. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/duration.py +0 -0
  49. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/encrypted.py +0 -0
  50. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/json.py +0 -0
  51. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/mixins.py +0 -0
  52. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/network.py +0 -0
  53. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/numeric.py +0 -0
  54. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/primary_key.py +0 -0
  55. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/related.py +0 -0
  56. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/related_descriptors.py +0 -0
  57. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/related_lookups.py +0 -0
  58. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/related_managers.py +0 -0
  59. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/reverse_descriptors.py +0 -0
  60. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/reverse_related.py +0 -0
  61. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/temporal.py +0 -0
  62. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/text.py +0 -0
  63. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/timezones.py +0 -0
  64. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/fields/uuid.py +0 -0
  65. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/forms.py +0 -0
  66. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/functions/__init__.py +0 -0
  67. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/functions/comparison.py +0 -0
  68. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/functions/datetime.py +0 -0
  69. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/functions/math.py +0 -0
  70. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/functions/mixins.py +0 -0
  71. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/functions/random.py +0 -0
  72. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/functions/text.py +0 -0
  73. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/functions/uuid.py +0 -0
  74. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/functions/window.py +0 -0
  75. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/indexes.py +0 -0
  76. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/introspection/__init__.py +0 -0
  77. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/introspection/health/__init__.py +0 -0
  78. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
  79. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  80. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/introspection/health/context.py +0 -0
  81. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/introspection/health/helpers.py +0 -0
  82. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/introspection/health/ownership.py +0 -0
  83. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/introspection/health/runner.py +0 -0
  84. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/introspection/health/types.py +0 -0
  85. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/introspection/schema.py +0 -0
  86. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/lookups.py +0 -0
  87. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/meta.py +0 -0
  88. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/middleware.py +0 -0
  89. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/__init__.py +0 -0
  90. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/autodetector.py +0 -0
  91. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/exceptions.py +0 -0
  92. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/executor.py +0 -0
  93. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/graph.py +0 -0
  94. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/loader.py +0 -0
  95. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/migration.py +0 -0
  96. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/operations/__init__.py +0 -0
  97. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/operations/base.py +0 -0
  98. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/operations/fields.py +0 -0
  99. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/operations/models.py +0 -0
  100. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/operations/special.py +0 -0
  101. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/optimizer.py +0 -0
  102. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/questioner.py +0 -0
  103. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/recorder.py +0 -0
  104. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/serializer.py +0 -0
  105. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/state.py +0 -0
  106. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/utils.py +0 -0
  107. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/migrations/writer.py +0 -0
  108. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/options.py +0 -0
  109. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/otel.py +0 -0
  110. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/query.py +0 -0
  111. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/query_utils.py +0 -0
  112. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/registry.py +0 -0
  113. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/schema.py +0 -0
  114. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/sources.py +0 -0
  115. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/sql/__init__.py +0 -0
  116. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/sql/compiler.py +0 -0
  117. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/sql/constants.py +0 -0
  118. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/sql/datastructures.py +0 -0
  119. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/sql/query.py +0 -0
  120. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/sql/where.py +0 -0
  121. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/test/__init__.py +0 -0
  122. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/test/database.py +0 -0
  123. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/test/pytest.py +0 -0
  124. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/transaction.py +0 -0
  125. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/types.py +0 -0
  126. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/types.pyi +0 -0
  127. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/plain/postgres/utils.py +0 -0
  128. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/forms.py +0 -0
  129. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0001_initial.py +0 -0
  130. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  131. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  132. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  133. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  134. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  135. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  136. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  137. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  138. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  139. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  140. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  141. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  142. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
  143. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  144. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  145. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  146. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
  147. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/migrations/__init__.py +0 -0
  148. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/__init__.py +0 -0
  149. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/constraints.py +0 -0
  150. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/defaults.py +0 -0
  151. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/delete.py +0 -0
  152. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/encrypted.py +0 -0
  153. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/forms.py +0 -0
  154. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/indexes.py +0 -0
  155. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/iteration.py +0 -0
  156. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/mixins.py +0 -0
  157. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/nullability.py +0 -0
  158. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/querysets.py +0 -0
  159. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/relationships.py +0 -0
  160. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/storage_parameters.py +0 -0
  161. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/trees.py +0 -0
  162. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/models/unregistered.py +0 -0
  163. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/urls.py +0 -0
  164. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/examples/views.py +0 -0
  165. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/settings.py +0 -0
  166. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/app/urls.py +0 -0
  167. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/conftest.py +0 -0
  168. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/conftest_convergence.py +0 -0
  169. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_autodetector_not_null_errors.py +0 -0
  170. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_autodetector_type_change.py +0 -0
  171. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_connection_isolation.py +0 -0
  172. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_connection_lifecycle.py +0 -0
  173. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_connection_pool.py +0 -0
  174. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_constraint_violation_error.py +0 -0
  175. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_convergence.py +0 -0
  176. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_convergence_constraints.py +0 -0
  177. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_convergence_defaults.py +0 -0
  178. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_convergence_fk.py +0 -0
  179. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_convergence_indexes.py +0 -0
  180. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_convergence_nullability.py +0 -0
  181. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_convergence_storage_parameters.py +0 -0
  182. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_convergence_timeouts.py +0 -0
  183. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_db_expression_defaults.py +0 -0
  184. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_executor_connection_hook.py +0 -0
  185. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_health.py +0 -0
  186. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_introspection.py +0 -0
  187. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_literal_default_persistence.py +0 -0
  188. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_management_connection.py +0 -0
  189. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_migration_executor.py +0 -0
  190. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_no_callable_defaults.py +0 -0
  191. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_otel_metrics.py +0 -0
  192. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_schema_normalize_type.py +0 -0
  193. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/internal/test_schema_timeouts.py +0 -0
  194. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_database_url.py +0 -0
  195. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_delete_behaviors.py +0 -0
  196. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_encrypted_fields.py +0 -0
  197. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_exceptions.py +0 -0
  198. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_field_defaults.py +0 -0
  199. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_functions_uuid.py +0 -0
  200. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_iterator.py +0 -0
  201. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_m2m.py +0 -0
  202. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_manager_assignment.py +0 -0
  203. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_mixins.py +0 -0
  204. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_modelform_roundtrip.py +0 -0
  205. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_random_string_field.py +0 -0
  206. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_raw_query.py +0 -0
  207. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_read_only_transactions.py +0 -0
  208. {plain_postgres-0.103.0 → plain_postgres-0.103.1}/tests/public/test_related.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.postgres
3
- Version: 0.103.0
3
+ Version: 0.103.1
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
@@ -1,5 +1,15 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.103.1](https://github.com/dropseed/plain/releases/plain-postgres@0.103.1) (2026-05-06)
4
+
5
+ ### What's changed
6
+
7
+ - **`postgres.duplicate_indexes` now flags exact-column duplicates**, not just prefix-redundancy. Previously the check required the redundant index to be strictly shorter than the index covering it, so an `Index(fields=["x"])` declared next to a same-column `UniqueConstraint(fields=["x"])` slipped past — even though the unique-backed btree already covers the same lookups and enforces uniqueness. The check (and the matching preflight) now flag the non-unique side of a same-column pair. Two non-unique indexes on identical columns flag the alphabetically later name (deterministic). ([253513b9](https://github.com/dropseed/plain/commit/253513b9))
8
+
9
+ ### Upgrade instructions
10
+
11
+ - No changes required. After upgrading, `plain postgres diagnose` and `plain preflight` may surface previously-undetected duplicate indexes — drop the redundant `Index(...)` declaration on the model and run `plain postgres sync`.
12
+
3
13
  ## [0.103.0](https://github.com/dropseed/plain/releases/plain-postgres@0.103.0) (2026-05-06)
4
14
 
5
15
  ### What's changed
@@ -66,7 +66,17 @@ def check_invalid_indexes(
66
66
  def check_duplicate_indexes(
67
67
  cursor: Any, table_owners: dict[str, TableOwner]
68
68
  ) -> CheckResult:
69
- """Indexes where one is a column-prefix of another on the same table.
69
+ """Indexes redundant with another on the same table.
70
+
71
+ Two flavors are flagged:
72
+
73
+ - **Prefix duplicate** — a non-unique index whose columns are a strict
74
+ prefix of another index's. The longer index covers the same queries.
75
+ - **Exact duplicate** — same columns and access method as another index.
76
+ If the pair contains a unique-backed btree, the non-unique one is the
77
+ redundant one (the unique already covers the queries and enforces
78
+ uniqueness). If both are non-unique, the alphabetically later name
79
+ gets flagged (deterministic).
70
80
 
71
81
  Each index column's canonical definition comes from
72
82
  ``pg_get_indexdef(indexrelid, k, false)`` — that text includes the
@@ -112,46 +122,56 @@ def check_duplicate_indexes(
112
122
  for table_name, indexes in by_table.items():
113
123
  for i, idx_a in enumerate(indexes):
114
124
  for idx_b in indexes[i + 1 :]:
115
- # Check both directions: is either a prefix of the other?
125
+ # Try both orderings prefix-redundancy is asymmetric, and
126
+ # exact-duplicate flagging picks one side deterministically.
116
127
  for shorter, longer in [(idx_a, idx_b), (idx_b, idx_a)]:
117
128
  name_s, am_s, defs_s, unique_s, size_s = shorter
118
- name_l, am_l, defs_l, _, _ = longer
129
+ name_l, am_l, defs_l, unique_l, _ = longer
119
130
  # Different access methods serve different operators
120
- # (e.g. hash supports `=` only, btree supports ordering),
121
- # and unique indexes serve a constraint purpose.
122
- if (
123
- name_s not in flagged
124
- and am_s == am_l
125
- and not unique_s
131
+ # (e.g. hash supports `=` only, btree supports ordering).
132
+ if name_s in flagged or am_s != am_l:
133
+ continue
134
+
135
+ is_prefix_dup = (
136
+ not unique_s
126
137
  and len(defs_s) < len(defs_l)
127
138
  and defs_l[: len(defs_s)] == defs_s
128
- ):
129
- source, package, model_class, model_file = _table_info(
130
- table_name, table_owners
131
- )
132
- app_suggestion = f'Remove "{name_s}" from model indexes/constraints, then run plain postgres sync'
139
+ )
140
+ is_exact_dup = (
141
+ defs_s == defs_l
142
+ and not unique_s
143
+ and (unique_l or name_s > name_l)
144
+ )
145
+
146
+ if not (is_prefix_dup or is_exact_dup):
147
+ continue
148
+
149
+ source, package, model_class, model_file = _table_info(
150
+ table_name, table_owners
151
+ )
152
+ app_suggestion = f'Remove "{name_s}" from model indexes/constraints, then run plain postgres sync'
133
153
 
134
- items.append(
135
- CheckItem(
136
- table=table_name,
137
- name=name_s,
138
- detail=f"{size_s}, redundant with {name_l}",
154
+ items.append(
155
+ CheckItem(
156
+ table=table_name,
157
+ name=name_s,
158
+ detail=f"{size_s}, redundant with {name_l}",
159
+ source=source,
160
+ package=package,
161
+ model_class=model_class,
162
+ model_file=model_file,
163
+ suggestion=_index_suggestion(
139
164
  source=source,
140
165
  package=package,
141
166
  model_class=model_class,
142
167
  model_file=model_file,
143
- suggestion=_index_suggestion(
144
- source=source,
145
- package=package,
146
- model_class=model_class,
147
- model_file=model_file,
148
- app_suggestion=app_suggestion,
149
- unmanaged_suggestion=f'DROP INDEX CONCURRENTLY "{name_s}";',
150
- ),
151
- caveats=[],
152
- )
168
+ app_suggestion=app_suggestion,
169
+ unmanaged_suggestion=f'DROP INDEX CONCURRENTLY "{name_s}";',
170
+ ),
171
+ caveats=[],
153
172
  )
154
- flagged.add(name_s)
173
+ )
174
+ flagged.add(name_s)
155
175
 
156
176
  return CheckResult(
157
177
  name="duplicate_indexes",
@@ -412,7 +412,12 @@ class CheckMissingFKIndexes(PreflightCheck):
412
412
 
413
413
  @register_check("postgres.duplicate_indexes")
414
414
  class CheckDuplicateIndexes(PreflightCheck):
415
- """Warns about indexes that are prefix-redundant with other indexes or constraints."""
415
+ """Warns about indexes redundant with other indexes or constraints.
416
+
417
+ Catches both prefix-redundancy (a 1-column index shadowed by a wider
418
+ composite) and exact-column duplicates (an `Index(fields=["x"])` that
419
+ duplicates a same-column `UniqueConstraint`).
420
+ """
416
421
 
417
422
  def run(self) -> list[PreflightResult]:
418
423
  results = []
@@ -425,23 +430,52 @@ class CheckDuplicateIndexes(PreflightCheck):
425
430
  for idx_b in all_indexes[i + 1 :]:
426
431
  for shorter, longer in [(idx_a, idx_b), (idx_b, idx_a)]:
427
432
  s_name, s_fields, s_unique = shorter
428
- l_name, l_fields, _ = longer
429
- if (
430
- s_name not in flagged
433
+ l_name, l_fields, l_unique = longer
434
+
435
+ if s_name in flagged:
436
+ continue
437
+
438
+ is_prefix_dup = (
439
+ not s_unique
431
440
  and len(s_fields) < len(l_fields)
432
441
  and l_fields[: len(s_fields)] == s_fields
442
+ )
443
+ is_exact_dup = (
444
+ s_fields == l_fields
433
445
  and not s_unique
434
- ):
435
- results.append(
436
- PreflightResult(
437
- fix=f"Index '{s_name}' on [{', '.join(s_fields)}] "
438
- f"is redundant with '{l_name}' on [{', '.join(l_fields)}]. "
439
- f"The longer index covers the same queries.",
440
- obj=model.model_options.label,
441
- id="postgres.duplicate_index",
442
- warning=True,
443
- )
446
+ and (l_unique or s_name > l_name)
447
+ )
448
+
449
+ if not (is_prefix_dup or is_exact_dup):
450
+ continue
451
+
452
+ if is_prefix_dup:
453
+ fix = (
454
+ f"Index '{s_name}' on [{', '.join(s_fields)}] "
455
+ f"is redundant with '{l_name}' on [{', '.join(l_fields)}]. "
456
+ f"The longer index covers the same queries."
457
+ )
458
+ elif l_unique:
459
+ fix = (
460
+ f"Index '{s_name}' on [{', '.join(s_fields)}] "
461
+ f"is redundant with '{l_name}' on the same columns. "
462
+ f"The unique-backed index already covers these queries."
463
+ )
464
+ else:
465
+ fix = (
466
+ f"Index '{s_name}' on [{', '.join(s_fields)}] "
467
+ f"is an exact duplicate of '{l_name}'. "
468
+ f"Drop one of them."
444
469
  )
445
- flagged.add(s_name)
470
+
471
+ results.append(
472
+ PreflightResult(
473
+ fix=fix,
474
+ obj=model.model_options.label,
475
+ id="postgres.duplicate_index",
476
+ warning=True,
477
+ )
478
+ )
479
+ flagged.add(s_name)
446
480
 
447
481
  return results
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.postgres"
3
- version = "0.103.0"
3
+ version = "0.103.1"
4
4
  description = "Model your data and store it in a database."
5
5
  authors = [{ name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev" }]
6
6
  readme = "README.md"
@@ -266,6 +266,75 @@ class TestStructuralScenarios:
266
266
  f"expected no duplicates flagged across access methods, got {flagged}"
267
267
  )
268
268
 
269
+ def test_duplicate_indexes_detected_when_non_unique_shadows_unique(self) -> None:
270
+ """A non-unique `Index(fields=["x"])` declared alongside a same-column
271
+ `UniqueConstraint(fields=["x"])` is pure overhead — the unique-backed
272
+ btree already covers the same queries. Flag the non-unique side."""
273
+ _execute(
274
+ 'CREATE TABLE "_diag_dup_exact" ('
275
+ '"id" serial PRIMARY KEY, "uuid" uuid NOT NULL)'
276
+ )
277
+ _execute(
278
+ 'CREATE UNIQUE INDEX "_diag_dup_exact_unique_uuid" '
279
+ 'ON "_diag_dup_exact" ("uuid")'
280
+ )
281
+ _execute(
282
+ 'CREATE INDEX "_diag_dup_exact_uuid_idx" ON "_diag_dup_exact" ("uuid")'
283
+ )
284
+
285
+ conn = get_connection()
286
+ with conn.cursor() as cursor:
287
+ result = check_duplicate_indexes(cursor, {})
288
+
289
+ flagged = [i for i in result["items"] if i["table"] == "_diag_dup_exact"]
290
+ assert len(flagged) == 1, (
291
+ f"expected one duplicate on _diag_dup_exact, got {flagged}"
292
+ )
293
+ assert flagged[0]["name"] == "_diag_dup_exact_uuid_idx"
294
+ assert "_diag_dup_exact_unique_uuid" in flagged[0]["detail"]
295
+
296
+ def test_duplicate_indexes_detected_when_two_non_unique_match_exactly(
297
+ self,
298
+ ) -> None:
299
+ """Two non-unique indexes on identical columns — flag the alphabetically
300
+ later name (deterministic) so we don't double-report or oscillate."""
301
+ _execute('CREATE TABLE "_diag_dup_pair" ("id" serial PRIMARY KEY, "x" int)')
302
+ _execute('CREATE INDEX "_diag_dup_pair_a_idx" ON "_diag_dup_pair" ("x")')
303
+ _execute('CREATE INDEX "_diag_dup_pair_z_idx" ON "_diag_dup_pair" ("x")')
304
+
305
+ conn = get_connection()
306
+ with conn.cursor() as cursor:
307
+ result = check_duplicate_indexes(cursor, {})
308
+
309
+ flagged = [i for i in result["items"] if i["table"] == "_diag_dup_pair"]
310
+ assert len(flagged) == 1, (
311
+ f"expected one duplicate on _diag_dup_pair, got {flagged}"
312
+ )
313
+ assert flagged[0]["name"] == "_diag_dup_pair_z_idx"
314
+
315
+ def test_duplicate_indexes_not_flagged_for_two_unique_same_columns(self) -> None:
316
+ """Two unique indexes on identical columns is a Postgres-level redundancy
317
+ but neither is "the" overhead — both enforce uniqueness. Don't flag."""
318
+ _execute(
319
+ 'CREATE TABLE "_diag_dup_two_uniq" ('
320
+ '"id" serial PRIMARY KEY, "x" int NOT NULL)'
321
+ )
322
+ _execute(
323
+ 'CREATE UNIQUE INDEX "_diag_dup_two_uniq_a" ON "_diag_dup_two_uniq" ("x")'
324
+ )
325
+ _execute(
326
+ 'CREATE UNIQUE INDEX "_diag_dup_two_uniq_b" ON "_diag_dup_two_uniq" ("x")'
327
+ )
328
+
329
+ conn = get_connection()
330
+ with conn.cursor() as cursor:
331
+ result = check_duplicate_indexes(cursor, {})
332
+
333
+ flagged = [i for i in result["items"] if i["table"] == "_diag_dup_two_uniq"]
334
+ assert flagged == [], (
335
+ f"expected no duplicates flagged for two unique same-column indexes, got {flagged}"
336
+ )
337
+
269
338
  def test_duplicate_indexes_not_flagged_when_longer_starts_with_expression(
270
339
  self,
271
340
  ) -> None: