plain.postgres 0.115.0__tar.gz → 0.116.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/PKG-INFO +1 -1
  2. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/CHANGELOG.md +20 -0
  3. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/base.py +28 -54
  4. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/connection.py +1 -3
  5. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/constraints.py +2 -2
  6. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/convergence/analysis.py +3 -8
  7. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/base.py +11 -22
  8. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/binary.py +1 -1
  9. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/encrypted.py +1 -1
  10. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/json.py +1 -1
  11. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/network.py +1 -1
  12. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/numeric.py +1 -1
  13. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/primary_key.py +1 -1
  14. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/related.py +9 -11
  15. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/related_managers.py +4 -9
  16. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/reverse_related.py +16 -7
  17. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/temporal.py +1 -2
  18. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/text.py +2 -2
  19. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/timezones.py +1 -1
  20. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/uuid.py +1 -1
  21. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/forms.py +2 -4
  22. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/ownership.py +1 -1
  23. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/meta.py +34 -89
  24. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/state.py +2 -2
  25. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/preflight/indexes.py +1 -1
  26. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/query.py +8 -11
  27. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/query_utils.py +5 -5
  28. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/schema.py +1 -1
  29. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sql/compiler.py +2 -2
  30. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sql/datastructures.py +3 -3
  31. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sql/query.py +8 -12
  32. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/pyproject.toml +1 -1
  33. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/.gitignore +0 -0
  34. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/CLAUDE.md +0 -0
  35. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/LICENSE +0 -0
  36. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/README.md +0 -0
  37. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/README.md +0 -0
  38. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/__init__.py +0 -0
  39. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/adapters.py +0 -0
  40. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
  41. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  42. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/aggregates.py +0 -0
  43. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/__init__.py +0 -0
  44. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/converge.py +0 -0
  45. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/core.py +0 -0
  46. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/decorators.py +0 -0
  47. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/diagnose.py +0 -0
  48. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/migrations.py +0 -0
  49. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/schema.py +0 -0
  50. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/cli/sync.py +0 -0
  51. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/config.py +0 -0
  52. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/constants.py +0 -0
  53. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/convergence/__init__.py +0 -0
  54. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/convergence/corrections.py +0 -0
  55. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/convergence/planning.py +0 -0
  56. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/database_url.py +0 -0
  57. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/databases.py +0 -0
  58. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/db.py +0 -0
  59. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/ddl.py +0 -0
  60. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/default_settings.py +0 -0
  61. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/deletion.py +0 -0
  62. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/dialect.py +0 -0
  63. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/entrypoints.py +0 -0
  64. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/enums.py +0 -0
  65. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/exceptions.py +0 -0
  66. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/expressions.py +0 -0
  67. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/__init__.py +0 -0
  68. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/boolean.py +0 -0
  69. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/duration.py +0 -0
  70. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/mixins.py +0 -0
  71. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/related_descriptors.py +0 -0
  72. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/related_lookups.py +0 -0
  73. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
  74. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/__init__.py +0 -0
  75. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/comparison.py +0 -0
  76. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/datetime.py +0 -0
  77. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/math.py +0 -0
  78. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/mixins.py +0 -0
  79. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/random.py +0 -0
  80. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/text.py +0 -0
  81. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/uuid.py +0 -0
  82. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/functions/window.py +0 -0
  83. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/indexes.py +0 -0
  84. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/__init__.py +0 -0
  85. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/__init__.py +0 -0
  86. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
  87. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  88. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
  89. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/context.py +0 -0
  90. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/helpers.py +0 -0
  91. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/runner.py +0 -0
  92. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/health/types.py +0 -0
  93. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/introspection/schema.py +0 -0
  94. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/lookups.py +0 -0
  95. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/middleware.py +0 -0
  96. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/__init__.py +0 -0
  97. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/autodetector.py +0 -0
  98. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/exceptions.py +0 -0
  99. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/executor.py +0 -0
  100. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/graph.py +0 -0
  101. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/loader.py +0 -0
  102. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/migration.py +0 -0
  103. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/__init__.py +0 -0
  104. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/base.py +0 -0
  105. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/fields.py +0 -0
  106. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/models.py +0 -0
  107. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/operations/special.py +0 -0
  108. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/optimizer.py +0 -0
  109. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/questioner.py +0 -0
  110. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/recorder.py +0 -0
  111. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/serializer.py +0 -0
  112. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/utils.py +0 -0
  113. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/migrations/writer.py +0 -0
  114. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/options.py +0 -0
  115. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/otel.py +0 -0
  116. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/preflight/__init__.py +0 -0
  117. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/preflight/database.py +0 -0
  118. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/preflight/models.py +0 -0
  119. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/registry.py +0 -0
  120. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/schema_lock.py +0 -0
  121. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sources.py +0 -0
  122. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sql/__init__.py +0 -0
  123. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sql/constants.py +0 -0
  124. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/sql/where.py +0 -0
  125. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/test/__init__.py +0 -0
  126. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/test/database.py +0 -0
  127. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/test/pytest.py +0 -0
  128. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/transaction.py +0 -0
  129. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/types.py +0 -0
  130. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/types.pyi +0 -0
  131. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/plain/postgres/utils.py +0 -0
  132. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/forms.py +0 -0
  133. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  134. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  135. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  136. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  137. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  138. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  139. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  140. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  141. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  142. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  143. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  144. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  145. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  146. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
  147. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  148. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  149. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  150. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
  151. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/migrations/__init__.py +0 -0
  152. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/__init__.py +0 -0
  153. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/constraints.py +0 -0
  154. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/defaults.py +0 -0
  155. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/delete.py +0 -0
  156. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/encrypted.py +0 -0
  157. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/forms.py +0 -0
  158. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/indexes.py +0 -0
  159. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/iteration.py +0 -0
  160. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/mixins.py +0 -0
  161. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/nullability.py +0 -0
  162. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/querysets.py +0 -0
  163. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/relationships.py +0 -0
  164. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/storage_parameters.py +0 -0
  165. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/trees.py +0 -0
  166. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/models/unregistered.py +0 -0
  167. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/urls.py +0 -0
  168. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/examples/views.py +0 -0
  169. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/settings.py +0 -0
  170. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/app/urls.py +0 -0
  171. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/conftest.py +0 -0
  172. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/conftest_convergence.py +0 -0
  173. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_apply_replan.py +0 -0
  174. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_autodetector_not_null_errors.py +0 -0
  175. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_autodetector_type_change.py +0 -0
  176. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_connection_isolation.py +0 -0
  177. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_connection_lifecycle.py +0 -0
  178. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_connection_pool.py +0 -0
  179. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_connection_self_heal.py +0 -0
  180. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_constraint_violation_error.py +0 -0
  181. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence.py +0 -0
  182. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_constraints.py +0 -0
  183. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_defaults.py +0 -0
  184. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_fk.py +0 -0
  185. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_indexes.py +0 -0
  186. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_nullability.py +0 -0
  187. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
  188. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_convergence_timeouts.py +0 -0
  189. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_databases_not_on_runtime_path.py +0 -0
  190. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_db_expression_defaults.py +0 -0
  191. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_diagnose.py +0 -0
  192. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_executor_connection_hook.py +0 -0
  193. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_fk_characterization.py +0 -0
  194. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_health.py +0 -0
  195. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_introspection.py +0 -0
  196. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_literal_default_persistence.py +0 -0
  197. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_management_connection.py +0 -0
  198. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_migration_executor.py +0 -0
  199. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_no_callable_defaults.py +0 -0
  200. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_otel_metrics.py +0 -0
  201. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
  202. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
  203. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
  204. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_rollback_exc_attribution.py +0 -0
  205. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_schema_lock.py +0 -0
  206. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_schema_normalize_type.py +0 -0
  207. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/internal/test_schema_timeouts.py +0 -0
  208. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_create_update.py +0 -0
  209. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_database_url.py +0 -0
  210. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_databases.py +0 -0
  211. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_deferred_loading.py +0 -0
  212. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_delete_behaviors.py +0 -0
  213. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_encrypted_fields.py +0 -0
  214. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_exceptions.py +0 -0
  215. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_field_defaults.py +0 -0
  216. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_functions_uuid.py +0 -0
  217. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_integrity_error_mapping.py +0 -0
  218. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_iterator.py +0 -0
  219. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_m2m.py +0 -0
  220. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_manager_assignment.py +0 -0
  221. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_manual_pk.py +0 -0
  222. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_mixins.py +0 -0
  223. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_modelform_roundtrip.py +0 -0
  224. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_only_empty_defaults.py +0 -0
  225. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_order_by_expressions.py +0 -0
  226. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_queryset_repr.py +0 -0
  227. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_queryset_slicing.py +0 -0
  228. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_random_string_field.py +0 -0
  229. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_raw_query.py +0 -0
  230. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_read_only_transactions.py +0 -0
  231. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_related.py +0 -0
  232. {plain_postgres-0.115.0 → plain_postgres-0.116.0}/tests/public/test_related_instance_filter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: plain.postgres
3
- Version: 0.115.0
3
+ Version: 0.116.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
@@ -1,5 +1,25 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.116.0](https://github.com/dropseed/plain/releases/plain-postgres@0.116.0) (2026-09-04)
4
+
5
+ ### What's changed
6
+
7
+ - **`Meta` now exposes exactly two forward-field collections: `fields` and `many_to_many`.** `local_fields`, `local_many_to_many`, `concrete_fields`, and `local_concrete_fields` are gone. Plain has no model inheritance and no non-concrete forward fields, so "local" and "concrete" drew distinctions that could never differ — four names for two lists, each with its own cached property and its own filtering pass. `fields` is every column-backed field (primary key first, then sorted by name) and `many_to_many` is every `ManyToManyField` (sorted by name); both now read straight from the storage `add_field()` fills, instead of re-filtering `_get_fields()` on each access ([9f0d0da22d](https://github.com/dropseed/plain/commit/9f0d0da22d))
8
+ - `Meta.fields` is typed as `ImmutableList[ColumnField]` and `Meta.many_to_many` as `ImmutableList[ManyToManyField]`, so the column attributes (`column`, `db_type()`, `primary_key`) are readable off a field without an `isinstance` check first. `add_field()` enforces that by raising `TypeError` for a field that is neither a `ColumnField` nor a `ManyToManyField` ([996c099e2b](https://github.com/dropseed/plain/commit/996c099e2b), [9f0d0da22d](https://github.com/dropseed/plain/commit/9f0d0da22d))
9
+ - The `postgres.non_local_field_reference` preflight check is removed. It rejected an `indexes` or `constraints` entry naming a field "not local to" the model — unreachable without inheritance, since a forward field found on the model is always the model's own ([9f0d0da22d](https://github.com/dropseed/plain/commit/9f0d0da22d))
10
+ - `Field.name` is typed `str` instead of `str | None` — it starts as `""` and becomes the real name in `set_attributes_from_name()`. That drops roughly twenty `assert self.name is not None` guards from the field descriptor, save, and preflight paths. `Field.deconstruct()` (and the `ColumnField`, `DefaultableField`, `ChoicesField`, `RelatedField`, `ForeignKeyField`, and `ManyToManyField` overrides) returns `tuple[str, str, list, dict]` accordingly ([51cb71f758](https://github.com/dropseed/plain/commit/51cb71f758))
11
+ - `get_joining_columns()` moved from `ForeignObjectRel` to `ForeignKeyRel`. It delegates to `field.get_joining_columns(reverse_join=True)`, which only a foreign key has ([51cb71f758](https://github.com/dropseed/plain/commit/51cb71f758))
12
+ - `PathInfo.join_field` is typed `ForeignKeyField | ForeignKeyRel` (was `ForeignKeyField | ForeignObjectRel`), and `select_related_descend()` is a `TypeGuard[RelatedField]` so a passing field narrows at the call site ([51cb71f758](https://github.com/dropseed/plain/commit/51cb71f758))
13
+ - `QuerySet._update()`, `_insert()`, `_batched_insert()`, `UpdateQuery.add_update_fields()`, and `InsertQuery.insert_values()` accept a `Sequence` of fields rather than requiring a `list`, so an `ImmutableList` from `Meta` can be passed straight through ([51cb71f758](https://github.com/dropseed/plain/commit/51cb71f758))
14
+
15
+ ### Upgrade instructions
16
+
17
+ - Replace `_model_meta.local_fields`, `_model_meta.concrete_fields`, and `_model_meta.local_concrete_fields` with `_model_meta.fields`. In Plain these were always the same set of fields.
18
+ - Replace `_model_meta.local_many_to_many` with `_model_meta.many_to_many`.
19
+ - Replace `_model_meta._non_pk_concrete_field_names` with `_model_meta._non_pk_field_names`.
20
+ - If you have a custom `Field` subclass, `deconstruct()` now returns `str` as its first element rather than `str | None`, and `self.name` is `""` (not `None`) before the field is contributed to a model class. Update any annotation or `is None` check accordingly.
21
+ - If you called `get_joining_columns()` on a reverse relation, it is now available on `ForeignKeyRel` only.
22
+
3
23
  ## [0.115.0](https://github.com/dropseed/plain/releases/plain-postgres@0.115.0) (2026-09-02)
4
24
 
5
25
  ### What's changed
@@ -27,7 +27,6 @@ from plain.postgres.exceptions import (
27
27
  )
28
28
  from plain.postgres.expressions import RawSQL, Value
29
29
  from plain.postgres.fields import DATABASE_DEFAULT, Field
30
- from plain.postgres.fields.base import ColumnField
31
30
  from plain.postgres.fields.related import RelatedField
32
31
  from plain.postgres.fields.reverse_related import ForeignObjectRel
33
32
  from plain.postgres.meta import Meta
@@ -123,10 +122,6 @@ class Model(metaclass=ModelBase):
123
122
  for field in meta.fields:
124
123
  from plain.postgres.fields.related import RelatedField
125
124
 
126
- # meta.fields excludes ManyToManyField, so every iterated field
127
- # is column-backed and exposes the ColumnField surface.
128
- assert isinstance(field, ColumnField)
129
-
130
125
  is_related_object = False
131
126
  # Virtual field
132
127
  if field.name not in kwargs and field.column is None:
@@ -193,16 +188,14 @@ class Model(metaclass=ModelBase):
193
188
 
194
189
  @classmethod
195
190
  def from_db(cls, field_names: Iterable[str], values: Sequence[Any]) -> Model:
196
- if len(values) != len(cls._model_meta.concrete_fields):
191
+ if len(values) != len(cls._model_meta.fields):
197
192
  values_iter = iter(values)
198
193
  values = [
199
194
  next(values_iter) if f.name in field_names else DEFERRED
200
- for f in cls._model_meta.concrete_fields
195
+ for f in cls._model_meta.fields
201
196
  ]
202
197
  # Build kwargs dict from field names and values
203
- field_dict = dict(
204
- zip((f.name for f in cls._model_meta.concrete_fields), values)
205
- )
198
+ field_dict = dict(zip((f.name for f in cls._model_meta.fields), values))
206
199
  new = cls(_from_db=True, **field_dict)
207
200
  new._state.adding = False
208
201
  return new
@@ -279,11 +272,7 @@ class Model(metaclass=ModelBase):
279
272
  """
280
273
  Return a set containing names of deferred fields for this instance.
281
274
  """
282
- return {
283
- f.name
284
- for f in self._model_meta.concrete_fields
285
- if f.name not in self.__dict__
286
- }
275
+ return {f.name for f in self._model_meta.fields if f.name not in self.__dict__}
287
276
 
288
277
  def refresh_from_db(self, fields: list[str] | None = None) -> None:
289
278
  """
@@ -320,15 +309,13 @@ class Model(metaclass=ModelBase):
320
309
  db_instance_qs = db_instance_qs.only(*fields)
321
310
  elif deferred_fields:
322
311
  fields = [
323
- f.name
324
- for f in self._model_meta.concrete_fields
325
- if f.name not in deferred_fields
312
+ f.name for f in self._model_meta.fields if f.name not in deferred_fields
326
313
  ]
327
314
  db_instance_qs = db_instance_qs.only(*fields)
328
315
 
329
316
  db_instance = db_instance_qs.get()
330
317
  non_loaded_fields = db_instance.get_deferred_fields()
331
- for field in self._model_meta.concrete_fields:
318
+ for field in self._model_meta.fields:
332
319
  if field.name in non_loaded_fields:
333
320
  # This field wasn't refreshed - skip ahead.
334
321
  continue
@@ -404,7 +391,7 @@ class Model(metaclass=ModelBase):
404
391
  if not fields:
405
392
  return self # explicit "update nothing" -- no-op
406
393
  fields = frozenset(fields)
407
- field_names = self._model_meta._non_pk_concrete_field_names
394
+ field_names = self._model_meta._non_pk_field_names
408
395
  non_model_fields = fields.difference(field_names)
409
396
  if non_model_fields:
410
397
  raise ValueError(
@@ -420,7 +407,7 @@ class Model(metaclass=ModelBase):
420
407
  )
421
408
  elif deferred_fields:
422
409
  # Loaded via .only()/.defer() -- write just the loaded fields.
423
- loaded = self._model_meta._non_pk_concrete_field_names - deferred_fields
410
+ loaded = self._model_meta._non_pk_field_names - deferred_fields
424
411
  if loaded:
425
412
  fields = loaded
426
413
  self._prepare_related_fields_for_save(
@@ -484,7 +471,7 @@ class Model(metaclass=ModelBase):
484
471
  fields from RETURNING. Omits id from the INSERT when unset so Postgres
485
472
  generates the identity value."""
486
473
  meta = self._model_meta
487
- fields = list(meta.local_concrete_fields)
474
+ fields = list(meta.fields)
488
475
  if self.id is None:
489
476
  id_field = meta.get_forward_field("id")
490
477
  fields = [f for f in fields if f is not id_field]
@@ -494,7 +481,6 @@ class Model(metaclass=ModelBase):
494
481
  )
495
482
  if results:
496
483
  for value, field in zip(results[0], returning_fields):
497
- assert field.name is not None
498
484
  setattr(self, field.name, value)
499
485
 
500
486
  def _update_row(self, fields: Iterable[str] | None) -> None:
@@ -502,7 +488,7 @@ class Model(metaclass=ModelBase):
502
488
  row matched -- update() targets an existing row and has no INSERT
503
489
  fallback (that's create())."""
504
490
  meta = self._model_meta
505
- non_pks = [f for f in meta.local_concrete_fields if not f.primary_key]
491
+ non_pks = [f for f in meta.fields if not f.primary_key]
506
492
  if fields:
507
493
  non_pks = [f for f in non_pks if f.name in fields]
508
494
  values = [(f, f.pre_save(self, False)) for f in non_pks]
@@ -525,7 +511,7 @@ class Model(metaclass=ModelBase):
525
511
  ) -> None:
526
512
  # Ensure that a model instance without a PK hasn't been assigned to
527
513
  # a ForeignKeyField on this model. If the field is nullable, allowing the save would result in silent data loss.
528
- for field in self._model_meta.concrete_fields:
514
+ for field in self._model_meta.fields:
529
515
  if field_names and field.name not in field_names:
530
516
  continue
531
517
  # If the related field isn't cached, then an instance hasn't been
@@ -578,7 +564,6 @@ class Model(metaclass=ModelBase):
578
564
  with transaction.mark_for_rollback_on_error():
579
565
  count = self._model_meta.base_queryset.filter(id=self.id)._raw_delete()
580
566
  id_field = self._model_meta.get_forward_field("id")
581
- assert id_field.name is not None
582
567
  setattr(self, id_field.name, None)
583
568
  # Only the id is cleared -- every other field value survives so callers
584
569
  # can still reference a deleted row (correlate it, log it, check it's
@@ -611,7 +596,7 @@ class Model(metaclass=ModelBase):
611
596
  meta = meta or self._model_meta
612
597
  return {
613
598
  field.name: Value(field.value_from_object(self), field)
614
- for field in meta.local_concrete_fields
599
+ for field in meta.fields
615
600
  if field.name not in exclude
616
601
  }
617
602
 
@@ -755,9 +740,9 @@ class Model(metaclass=ModelBase):
755
740
  def _check_fields(cls) -> list[PreflightResult]:
756
741
  """Perform all field checks."""
757
742
  errors: list[PreflightResult] = []
758
- for field in cls._model_meta.local_fields:
743
+ for field in cls._model_meta.fields:
759
744
  errors.extend(field.preflight(from_model=cls))
760
- for field in cls._model_meta.local_many_to_many:
745
+ for field in cls._model_meta.many_to_many:
761
746
  errors.extend(field.preflight(from_model=cls))
762
747
  return errors
763
748
 
@@ -768,7 +753,7 @@ class Model(metaclass=ModelBase):
768
753
  errors: list[PreflightResult] = []
769
754
  seen_intermediary_signatures = []
770
755
 
771
- fields = cls._model_meta.local_many_to_many
756
+ fields = cls._model_meta.many_to_many
772
757
 
773
758
  # Skip when the target model wasn't found.
774
759
  fields = (f for f in fields if isinstance(f.remote_field.model, ModelBase))
@@ -800,9 +785,7 @@ class Model(metaclass=ModelBase):
800
785
  def _check_id_field(cls) -> list[PreflightResult]:
801
786
  """Disallow user-defined fields named ``id``."""
802
787
  if any(
803
- f
804
- for f in cls._model_meta.local_fields
805
- if f.name == "id" and not f.auto_created
788
+ f for f in cls._model_meta.fields if f.name == "id" and not f.auto_created
806
789
  ):
807
790
  return [
808
791
  PreflightResult(
@@ -819,7 +802,7 @@ class Model(metaclass=ModelBase):
819
802
  errors: list[PreflightResult] = []
820
803
  used_fields = {} # name -> field
821
804
 
822
- for f in cls._model_meta.local_fields:
805
+ for f in cls._model_meta.fields:
823
806
  clash = used_fields.get(f.name)
824
807
  # Note that we may detect clash between user-defined non-unique
825
808
  # field "id" and automatically added unique field "id", both
@@ -847,7 +830,7 @@ class Model(metaclass=ModelBase):
847
830
  used_column_names: list[str] = []
848
831
  errors: list[PreflightResult] = []
849
832
 
850
- for f in cls._model_meta.local_fields:
833
+ for f in cls._model_meta.fields:
851
834
  column_name = f.column
852
835
 
853
836
  # Ensure the column name is not already in use.
@@ -915,7 +898,7 @@ class Model(metaclass=ModelBase):
915
898
  @classmethod
916
899
  def _check_single_primary_key(cls) -> list[PreflightResult]:
917
900
  errors: list[PreflightResult] = []
918
- if sum(1 for f in cls._model_meta.local_fields if f.primary_key) > 1:
901
+ if sum(1 for f in cls._model_meta.fields if f.primary_key) > 1:
919
902
  errors.append(
920
903
  PreflightResult(
921
904
  fix="The model cannot have more than one field with "
@@ -966,11 +949,11 @@ class Model(metaclass=ModelBase):
966
949
  include for index in cls.model_options.indexes for include in index.include
967
950
  ]
968
951
  fields += references
969
- errors.extend(cls._check_local_fields(fields, "indexes"))
952
+ errors.extend(cls._check_referenced_fields(fields, "indexes"))
970
953
  return errors
971
954
 
972
955
  @classmethod
973
- def _check_local_fields(
956
+ def _check_referenced_fields(
974
957
  cls, fields: Iterable[str], option: str
975
958
  ) -> list[PreflightResult]:
976
959
  # In order to avoid hitting the relation tree prematurely, we use our
@@ -1003,15 +986,6 @@ class Model(metaclass=ModelBase):
1003
986
  id="postgres.m2m_field_in_meta_option",
1004
987
  )
1005
988
  )
1006
- elif field not in cls._model_meta.local_fields:
1007
- errors.append(
1008
- PreflightResult(
1009
- fix=f"'{option}' refers to field '{field_name}' which is not local to model "
1010
- f"'{cls.model_options.object_name}'.",
1011
- obj=cls,
1012
- id="postgres.non_local_field_reference",
1013
- )
1014
- )
1015
989
  return errors
1016
990
 
1017
991
  @classmethod
@@ -1089,9 +1063,9 @@ class Model(metaclass=ModelBase):
1089
1063
  meta = cls._model_meta
1090
1064
  valid_fields = set(
1091
1065
  chain.from_iterable(
1092
- (f.name,)
1093
- if not (f.auto_created and not f.concrete)
1094
- else (f.field.related_query_name(),)
1066
+ (f.field.related_query_name(),)
1067
+ if isinstance(f, ForeignObjectRel)
1068
+ else (f.name,)
1095
1069
  for f in chain(meta.fields, meta.related_objects)
1096
1070
  )
1097
1071
  )
@@ -1121,7 +1095,7 @@ class Model(metaclass=ModelBase):
1121
1095
  # silently truncate, so we check for names that are too long
1122
1096
  allowed_len = MAX_NAME_LENGTH
1123
1097
 
1124
- for f in cls._model_meta.local_fields:
1098
+ for f in cls._model_meta.fields:
1125
1099
  column_name = f.column
1126
1100
 
1127
1101
  # Check if column name is too long for the database.
@@ -1135,13 +1109,13 @@ class Model(metaclass=ModelBase):
1135
1109
  )
1136
1110
  )
1137
1111
 
1138
- for f in cls._model_meta.local_many_to_many:
1112
+ for f in cls._model_meta.many_to_many:
1139
1113
  # Skip nonexistent models.
1140
1114
  if isinstance(f.remote_field.through, str):
1141
1115
  continue
1142
1116
 
1143
1117
  # Check if column name for the M2M field is too long for the database.
1144
- for m2m in f.remote_field.through._model_meta.local_fields:
1118
+ for m2m in f.remote_field.through._model_meta.fields:
1145
1119
  rel_name = m2m.column
1146
1120
  if rel_name is not None and len(rel_name) > allowed_len:
1147
1121
  errors.append(
@@ -1235,7 +1209,7 @@ class Model(metaclass=ModelBase):
1235
1209
  id="postgres.constraint_refers_to_joined_field",
1236
1210
  )
1237
1211
  )
1238
- errors.extend(cls._check_local_fields(fields, "constraints"))
1212
+ errors.extend(cls._check_referenced_fields(fields, "constraints"))
1239
1213
  return errors
1240
1214
 
1241
1215
 
@@ -535,9 +535,7 @@ class DatabaseConnection:
535
535
  tables = set()
536
536
  for model in get_migratable_models():
537
537
  tables.add(model.model_options.db_table)
538
- tables.update(
539
- f.m2m_db_table() for f in model._model_meta.local_many_to_many
540
- )
538
+ tables.update(f.m2m_db_table() for f in model._model_meta.many_to_many)
541
539
  tables = list(tables)
542
540
  if only_existing:
543
541
  existing_tables = set(self.table_names(include_views=include_views))
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from types import NoneType
4
- from typing import TYPE_CHECKING, Any, cast
4
+ from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from plain.exceptions import ValidationError
7
7
  from plain.postgres.constants import LOOKUP_SEP
@@ -454,7 +454,7 @@ class UniqueConstraint(BaseConstraint):
454
454
  field_names[-1] = f"and {field_names[-1]}"
455
455
  # Comma-join when more than two, otherwise just space-join.
456
456
  sep = ", " if len(field_names) > 2 else " "
457
- params["field_label"] = sep.join(cast(list[str], field_names))
457
+ params["field_label"] = sep.join(field_names)
458
458
 
459
459
  # Use the first field's message format.
460
460
  message = meta.get_forward_field(unique_check[0]).unique_error_message
@@ -21,7 +21,6 @@ from ..ddl import (
21
21
  compile_literal_default_sql,
22
22
  )
23
23
  from ..dialect import quote_name
24
- from ..fields.base import ColumnField
25
24
  from ..fields.related import ForeignKeyField
26
25
  from ..indexes import Index
27
26
  from ..introspection import (
@@ -586,9 +585,7 @@ def _compare_columns(
586
585
  statuses: list[ColumnStatus] = []
587
586
  expected_col_names: set[str] = set()
588
587
 
589
- for f in model._model_meta.local_fields:
590
- if not isinstance(f, ColumnField):
591
- continue
588
+ for f in model._model_meta.fields:
592
589
  db_type = f.db_type()
593
590
  if db_type is None:
594
591
  continue
@@ -635,8 +632,6 @@ def _compare_columns(
635
632
  if f.primary_key:
636
633
  pk_suffix = f.db_type_suffix() or ""
637
634
 
638
- # Fields reached via local_fields are always contributed (name set).
639
- assert f.name is not None
640
635
  statuses.append(
641
636
  ColumnStatus(
642
637
  name=f.column,
@@ -1193,7 +1188,7 @@ def _compare_check_constraints(
1193
1188
  # ignored rather than surfaced as undeclared user constraints.
1194
1189
  internal_checks = {
1195
1190
  generate_notnull_check_name(table, f.column)
1196
- for f in model._model_meta.local_fields
1191
+ for f in model._model_meta.fields
1197
1192
  if f.db_type() is not None
1198
1193
  }
1199
1194
 
@@ -1245,7 +1240,7 @@ def _compare_foreign_keys(
1245
1240
  # Expected FKs from model fields, keyed by shape (column, target_table,
1246
1241
  # target_column) — the DB-side identity, since names can lag a rename.
1247
1242
  expected_fks: dict[tuple[str, str, str], ForeignKeyField] = {}
1248
- for f in model._model_meta.local_fields:
1243
+ for f in model._model_meta.fields:
1249
1244
  if isinstance(f, ForeignKeyField):
1250
1245
  to_table = f.target_field.model.model_options.db_table
1251
1246
  expected_fks[(f.column, to_table, f.target_field.column)] = f
@@ -110,8 +110,9 @@ class Field[T](RegisterLookupMixin):
110
110
  cast_db_type_sql: str | None = None
111
111
 
112
112
  # Instance attributes set during field lifecycle
113
- # Set by __init__
114
- name: str | None
113
+ # Set by __init__; becomes the real field name once contributed to a
114
+ # model class (set_attributes_from_name, called by contribute_to_class)
115
+ name: str
115
116
  # Set by set_attributes_from_name (called by contribute_to_class)
116
117
  column: str
117
118
  concrete: bool
@@ -134,7 +135,7 @@ class Field[T](RegisterLookupMixin):
134
135
  non_migration_attrs: tuple[str, ...] = ()
135
136
 
136
137
  def __init__(self) -> None:
137
- self.name = None # Set by set_attributes_from_name
138
+ self.name = "" # Set by set_attributes_from_name
138
139
  self.primary_key = False
139
140
  self.auto_created = False
140
141
 
@@ -151,8 +152,8 @@ class Field[T](RegisterLookupMixin):
151
152
  def __repr__(self) -> str:
152
153
  """Display the module, class, and name of the field."""
153
154
  path = f"{self.__class__.__module__}.{self.__class__.__qualname__}"
154
- name = getattr(self, "name", None)
155
- if name is not None:
155
+ name = getattr(self, "name", "")
156
+ if name:
156
157
  return f"<{path}: {name}>"
157
158
  return f"<{path}>"
158
159
 
@@ -164,7 +165,6 @@ class Field[T](RegisterLookupMixin):
164
165
  Check if field name is valid, i.e. 1) does not end with an
165
166
  underscore, 2) does not contain "__" and 3) is not "id".
166
167
  """
167
- assert self.name is not None, "Field name must be set before checking"
168
168
  if self.name.endswith("_"):
169
169
  return [
170
170
  PreflightResult(
@@ -215,7 +215,7 @@ class Field[T](RegisterLookupMixin):
215
215
  """
216
216
  return sql, params
217
217
 
218
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
218
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
219
219
  """
220
220
  Return enough information to recreate the field as a 4-tuple:
221
221
 
@@ -259,7 +259,6 @@ class Field[T](RegisterLookupMixin):
259
259
  cls_name = self.__class__.__qualname__
260
260
  if getattr(_postgres_root, cls_name, None) is self.__class__:
261
261
  path = f"plain.postgres.{cls_name}"
262
- # Note: self.name can be None during migration state rendering when fields are cloned
263
262
  return (self.name, path, [], keywords)
264
263
 
265
264
  def clone(self) -> Self:
@@ -305,7 +304,6 @@ class Field[T](RegisterLookupMixin):
305
304
  # values - so, this is very close to normal pickle.
306
305
  state = self.__dict__.copy()
307
306
  return _empty, (self.__class__,), state
308
- assert self.name is not None
309
307
  options = model.model_options
310
308
  return _load_field, (
311
309
  options.package_label,
@@ -394,7 +392,6 @@ class Field[T](RegisterLookupMixin):
394
392
  # Field is its own descriptor; make sure it is set on the class so
395
393
  # attribute access hits __get__/__set__.
396
394
  if self.column:
397
- assert self.name is not None
398
395
  setattr(cls, self.name, self)
399
396
 
400
397
  # Descriptor protocol implementation
@@ -422,7 +419,6 @@ class Field[T](RegisterLookupMixin):
422
419
  return self
423
420
 
424
421
  # Instance access - get value from instance dict
425
- assert self.name is not None
426
422
  data = instance.__dict__
427
423
  field_name = self.name
428
424
 
@@ -432,9 +428,7 @@ class Field[T](RegisterLookupMixin):
432
428
  # instance cheap to use beyond its primary key.
433
429
  if field_name not in data:
434
430
  missing = [
435
- f.name
436
- for f in instance._model_meta.concrete_fields
437
- if f.name not in data
431
+ f.name for f in instance._model_meta.fields if f.name not in data
438
432
  ]
439
433
  instance.refresh_from_db(fields=missing)
440
434
 
@@ -472,7 +466,6 @@ class Field[T](RegisterLookupMixin):
472
466
  stored = self.to_python(value)
473
467
 
474
468
  # Store in instance dict
475
- assert self.name is not None
476
469
  instance.__dict__[self.name] = stored
477
470
 
478
471
  def __delete__(self, instance: Model) -> None:
@@ -481,7 +474,6 @@ class Field[T](RegisterLookupMixin):
481
474
 
482
475
  Removes the value from instance.__dict__.
483
476
  """
484
- assert self.name is not None
485
477
  try:
486
478
  del instance.__dict__[self.name]
487
479
  except KeyError:
@@ -491,7 +483,6 @@ class Field[T](RegisterLookupMixin):
491
483
 
492
484
  def pre_save(self, model_instance: Model, add: bool) -> T | None:
493
485
  """Return field's value just before saving."""
494
- assert self.name is not None
495
486
  return getattr(model_instance, self.name)
496
487
 
497
488
  def get_prep_value(self, value: Any) -> Any:
@@ -544,12 +535,10 @@ class Field[T](RegisterLookupMixin):
544
535
  return self.get_db_default_expression() is not None
545
536
 
546
537
  def save_form_data(self, instance: Model, data: Any) -> None:
547
- assert self.name is not None
548
538
  setattr(instance, self.name, data)
549
539
 
550
540
  def value_from_object(self, obj: Model) -> T | None:
551
541
  """Return the value of this field in the given model instance."""
552
- assert self.name is not None
553
542
  return getattr(obj, self.name)
554
543
 
555
544
 
@@ -653,7 +642,7 @@ class ColumnField[T](Field[T]):
653
642
  return None
654
643
  return self._default_empty_value
655
644
 
656
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
645
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
657
646
  name, path, args, kwargs = super().deconstruct()
658
647
  if self.required is not True:
659
648
  kwargs["required"] = self.required
@@ -750,7 +739,7 @@ class DefaultableField[T](ColumnField[T]):
750
739
  # shared state across instances.
751
740
  return copy.deepcopy(self.default)
752
741
 
753
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
742
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
754
743
  name, path, args, kwargs = super().deconstruct()
755
744
  if self.default is not NOT_PROVIDED:
756
745
  kwargs["default"] = self.default
@@ -891,7 +880,7 @@ class ChoicesField[T](DefaultableField[T]):
891
880
  )
892
881
  super().validate(value, model_instance)
893
882
 
894
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
883
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
895
884
  name, path, args, kwargs = super().deconstruct()
896
885
  if self.choices is not None:
897
886
  choices = self.choices
@@ -41,7 +41,7 @@ class BinaryField[
41
41
  if self.max_length is not None:
42
42
  self.validators.append(MaxLengthValidator(self.max_length))
43
43
 
44
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
44
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
45
45
  name, path, args, kwargs = super().deconstruct()
46
46
  if self.max_length is not None:
47
47
  kwargs["max_length"] = self.max_length
@@ -14,7 +14,7 @@ except ImportError:
14
14
  InvalidToken = None # ty: ignore[invalid-assignment]
15
15
  MultiFernet = None # ty: ignore[invalid-assignment]
16
16
  hashes = None # ty: ignore[invalid-assignment]
17
- PBKDF2HMAC = None # ty: ignore[invalid-assignment]
17
+ PBKDF2HMAC = None
18
18
 
19
19
  from plain.postgres.lookups import Exact, IsNull
20
20
  from plain.runtime import settings
@@ -54,7 +54,7 @@ class JSONField(DefaultableField):
54
54
  validators=validators,
55
55
  )
56
56
 
57
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
57
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
58
58
  name, path, args, kwargs = super().deconstruct()
59
59
  if self.encoder is not None:
60
60
  kwargs["encoder"] = self.encoder
@@ -63,7 +63,7 @@ class GenericIPAddressField[T: (str, str | None) = str](DefaultableField[T]):
63
63
  ]
64
64
  return []
65
65
 
66
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
66
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
67
67
  name, path, args, kwargs = super().deconstruct()
68
68
  if self.unpack_ipv4 is not False:
69
69
  kwargs["unpack_ipv4"] = self.unpack_ipv4
@@ -238,7 +238,7 @@ class DecimalField[T: (decimal.Decimal, decimal.Decimal | None) = decimal.Decima
238
238
  def context(self) -> decimal.Context:
239
239
  return decimal.Context(prec=self.max_digits)
240
240
 
241
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
241
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
242
242
  name, path, args, kwargs = super().deconstruct()
243
243
  if self.max_digits is not None:
244
244
  kwargs["max_digits"] = self.max_digits
@@ -41,7 +41,7 @@ class PrimaryKeyField(ColumnField[int]):
41
41
  # for every other field type.
42
42
  return [e for e in errors if e.id != "fields.reserved_field_name_id"]
43
43
 
44
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
44
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
45
45
  return (
46
46
  self.name,
47
47
  "plain.postgres.PrimaryKeyField",