plain.postgres 0.115.0__tar.gz → 0.117.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 (234) hide show
  1. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/PKG-INFO +1 -1
  2. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/CHANGELOG.md +46 -0
  3. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/base.py +44 -66
  4. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/connection.py +1 -3
  5. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/constraints.py +2 -2
  6. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/convergence/analysis.py +3 -8
  7. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/expressions.py +14 -2
  8. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/base.py +11 -24
  9. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/binary.py +1 -1
  10. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/encrypted.py +1 -1
  11. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/json.py +1 -1
  12. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/network.py +1 -1
  13. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/numeric.py +1 -1
  14. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/primary_key.py +1 -1
  15. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/related.py +57 -56
  16. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/related_managers.py +4 -9
  17. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/reverse_related.py +50 -18
  18. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/temporal.py +1 -2
  19. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/text.py +2 -2
  20. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/timezones.py +1 -1
  21. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/uuid.py +1 -1
  22. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/forms.py +2 -4
  23. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/ownership.py +1 -1
  24. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/meta.py +40 -96
  25. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/autodetector.py +23 -23
  26. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/graph.py +7 -1
  27. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/state.py +24 -20
  28. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/utils.py +7 -2
  29. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/preflight/indexes.py +1 -1
  30. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/query.py +16 -24
  31. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/query_utils.py +5 -5
  32. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/schema.py +1 -1
  33. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/sql/compiler.py +2 -2
  34. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/sql/datastructures.py +3 -3
  35. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/sql/query.py +8 -12
  36. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/pyproject.toml +1 -1
  37. plain_postgres-0.117.0/tests/internal/test_unresolved_relation_refs.py +144 -0
  38. plain_postgres-0.117.0/tests/public/test_queryset_ordered.py +14 -0
  39. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/.gitignore +0 -0
  40. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/CLAUDE.md +0 -0
  41. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/LICENSE +0 -0
  42. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/README.md +0 -0
  43. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/README.md +0 -0
  44. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/__init__.py +0 -0
  45. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/adapters.py +0 -0
  46. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
  47. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  48. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/aggregates.py +0 -0
  49. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/cli/__init__.py +0 -0
  50. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/cli/converge.py +0 -0
  51. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/cli/core.py +0 -0
  52. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/cli/decorators.py +0 -0
  53. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/cli/diagnose.py +0 -0
  54. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/cli/migrations.py +0 -0
  55. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/cli/schema.py +0 -0
  56. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/cli/sync.py +0 -0
  57. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/config.py +0 -0
  58. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/constants.py +0 -0
  59. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/convergence/__init__.py +0 -0
  60. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/convergence/corrections.py +0 -0
  61. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/convergence/planning.py +0 -0
  62. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/database_url.py +0 -0
  63. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/databases.py +0 -0
  64. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/db.py +0 -0
  65. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/ddl.py +0 -0
  66. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/default_settings.py +0 -0
  67. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/deletion.py +0 -0
  68. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/dialect.py +0 -0
  69. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/entrypoints.py +0 -0
  70. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/enums.py +0 -0
  71. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/exceptions.py +0 -0
  72. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/__init__.py +0 -0
  73. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/boolean.py +0 -0
  74. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/duration.py +0 -0
  75. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/mixins.py +0 -0
  76. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/related_descriptors.py +0 -0
  77. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/related_lookups.py +0 -0
  78. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
  79. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/functions/__init__.py +0 -0
  80. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/functions/comparison.py +0 -0
  81. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/functions/datetime.py +0 -0
  82. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/functions/math.py +0 -0
  83. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/functions/mixins.py +0 -0
  84. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/functions/random.py +0 -0
  85. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/functions/text.py +0 -0
  86. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/functions/uuid.py +0 -0
  87. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/functions/window.py +0 -0
  88. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/indexes.py +0 -0
  89. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/introspection/__init__.py +0 -0
  90. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/__init__.py +0 -0
  91. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
  92. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  93. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
  94. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/context.py +0 -0
  95. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/helpers.py +0 -0
  96. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/runner.py +0 -0
  97. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/introspection/health/types.py +0 -0
  98. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/introspection/schema.py +0 -0
  99. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/lookups.py +0 -0
  100. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/middleware.py +0 -0
  101. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/__init__.py +0 -0
  102. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/exceptions.py +0 -0
  103. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/executor.py +0 -0
  104. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/loader.py +0 -0
  105. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/migration.py +0 -0
  106. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/operations/__init__.py +0 -0
  107. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/operations/base.py +0 -0
  108. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/operations/fields.py +0 -0
  109. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/operations/models.py +0 -0
  110. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/operations/special.py +0 -0
  111. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/optimizer.py +0 -0
  112. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/questioner.py +0 -0
  113. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/recorder.py +0 -0
  114. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/serializer.py +0 -0
  115. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/migrations/writer.py +0 -0
  116. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/options.py +0 -0
  117. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/otel.py +0 -0
  118. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/preflight/__init__.py +0 -0
  119. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/preflight/database.py +0 -0
  120. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/preflight/models.py +0 -0
  121. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/registry.py +0 -0
  122. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/schema_lock.py +0 -0
  123. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/sources.py +0 -0
  124. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/sql/__init__.py +0 -0
  125. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/sql/constants.py +0 -0
  126. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/sql/where.py +0 -0
  127. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/test/__init__.py +0 -0
  128. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/test/database.py +0 -0
  129. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/test/pytest.py +0 -0
  130. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/transaction.py +0 -0
  131. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/types.py +0 -0
  132. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/types.pyi +0 -0
  133. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/plain/postgres/utils.py +0 -0
  134. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/forms.py +0 -0
  135. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  136. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  137. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  138. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  139. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  140. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  141. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  142. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  143. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  144. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  145. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  146. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  147. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  148. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
  149. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  150. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  151. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  152. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
  153. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/migrations/__init__.py +0 -0
  154. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/__init__.py +0 -0
  155. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/constraints.py +0 -0
  156. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/defaults.py +0 -0
  157. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/delete.py +0 -0
  158. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/encrypted.py +0 -0
  159. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/forms.py +0 -0
  160. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/indexes.py +0 -0
  161. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/iteration.py +0 -0
  162. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/mixins.py +0 -0
  163. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/nullability.py +0 -0
  164. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/querysets.py +0 -0
  165. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/relationships.py +0 -0
  166. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/storage_parameters.py +0 -0
  167. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/trees.py +0 -0
  168. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/models/unregistered.py +0 -0
  169. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/urls.py +0 -0
  170. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/examples/views.py +0 -0
  171. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/settings.py +0 -0
  172. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/app/urls.py +0 -0
  173. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/conftest.py +0 -0
  174. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/conftest_convergence.py +0 -0
  175. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_apply_replan.py +0 -0
  176. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_autodetector_not_null_errors.py +0 -0
  177. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_autodetector_type_change.py +0 -0
  178. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_connection_isolation.py +0 -0
  179. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_connection_lifecycle.py +0 -0
  180. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_connection_pool.py +0 -0
  181. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_connection_self_heal.py +0 -0
  182. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_constraint_violation_error.py +0 -0
  183. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_convergence.py +0 -0
  184. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_constraints.py +0 -0
  185. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_defaults.py +0 -0
  186. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_fk.py +0 -0
  187. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_indexes.py +0 -0
  188. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_nullability.py +0 -0
  189. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
  190. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_convergence_timeouts.py +0 -0
  191. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_databases_not_on_runtime_path.py +0 -0
  192. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_db_expression_defaults.py +0 -0
  193. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_diagnose.py +0 -0
  194. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_executor_connection_hook.py +0 -0
  195. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_fk_characterization.py +0 -0
  196. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_health.py +0 -0
  197. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_introspection.py +0 -0
  198. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_literal_default_persistence.py +0 -0
  199. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_management_connection.py +0 -0
  200. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_migration_executor.py +0 -0
  201. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_no_callable_defaults.py +0 -0
  202. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_otel_metrics.py +0 -0
  203. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
  204. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
  205. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
  206. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_rollback_exc_attribution.py +0 -0
  207. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_schema_lock.py +0 -0
  208. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_schema_normalize_type.py +0 -0
  209. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/internal/test_schema_timeouts.py +0 -0
  210. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_create_update.py +0 -0
  211. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_database_url.py +0 -0
  212. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_databases.py +0 -0
  213. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_deferred_loading.py +0 -0
  214. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_delete_behaviors.py +0 -0
  215. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_encrypted_fields.py +0 -0
  216. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_exceptions.py +0 -0
  217. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_field_defaults.py +0 -0
  218. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_functions_uuid.py +0 -0
  219. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_integrity_error_mapping.py +0 -0
  220. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_iterator.py +0 -0
  221. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_m2m.py +0 -0
  222. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_manager_assignment.py +0 -0
  223. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_manual_pk.py +0 -0
  224. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_mixins.py +0 -0
  225. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_modelform_roundtrip.py +0 -0
  226. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_only_empty_defaults.py +0 -0
  227. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_order_by_expressions.py +0 -0
  228. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_queryset_repr.py +0 -0
  229. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_queryset_slicing.py +0 -0
  230. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_random_string_field.py +0 -0
  231. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_raw_query.py +0 -0
  232. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_read_only_transactions.py +0 -0
  233. {plain_postgres-0.115.0 → plain_postgres-0.117.0}/tests/public/test_related.py +0 -0
  234. {plain_postgres-0.115.0 → plain_postgres-0.117.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.117.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,51 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.117.0](https://github.com/dropseed/plain/releases/plain-postgres@0.117.0) (2026-09-04)
4
+
5
+ ### What's changed
6
+
7
+ - Fixed `QuerySet.ordered`, which raised `AttributeError: 'Meta' object has no attribute 'ordering'` for any queryset without an explicit `order_by()`. It was reading the model's default ordering off `_model_meta` instead of `model_options`, so the property only ever worked on the one branch that never reached the lookup. It now correctly reports `True` when the model declares `Meta.ordering` and `False` when it doesn't ([d9406dfc6f](https://github.com/dropseed/plain/commit/d9406dfc6f))
8
+ - Unresolved relation references are now separate attributes from resolved model classes. `rel.model` and `ManyToManyRel.through` used to hold whatever you passed to the field — a `"package.Model"` string early on, the class after `lazy_related_operation` overwrote it — so every reader had to guess which it had, and the guessing was done inconsistently (`isinstance(..., str)` in some places, `isinstance(..., ModelBase)` in others, `hasattr(..., "_model_meta")` in others still). The raw reference now lives on `rel.model_ref` and `rel.through_ref`, and `rel.model` / `rel.through` are read-only properties that return the resolved class or raise `TypeError` if it isn't resolved yet. The `TypeError` is deliberate: an `AttributeError` would be swallowed by `hasattr`/`getattr` and silently read as "no model" ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
9
+ - `ForeignKeyField.target_field` dropped its own unresolved-reference `TypeError` — the `rel.model` property raises it now, with a message naming the field ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
10
+ - `ManyToManyField._check_through_fields` reports an unresolved target by name instead of crashing on `related_model.model_options.object_name`, so a `through_fields` mistake on a not-yet-resolved relation produces the intended preflight message ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
11
+ - `ModelState` now raises `TypeError` (was `ValueError`) when a field's `to` or `through` refers to a model class instead of a string reference, and detects it by checking the reference directly rather than probing for `_model_meta` ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
12
+ - Removed `Field.concrete`. It was set to `self.column is not None` right after `column` was assigned the field's name, so it was `True` on every forward field and the checks that consulted it were dead. `bulk_create()` and `bulk_update()` now say what they actually reject — "bulk_update() cannot be used with many-to-many fields." rather than "can only be used with concrete fields." — and `Model.save(update_fields=...)` drops "non-concrete fields" from its error message. `ForeignObjectRel.concrete` and the unreachable virtual-field skip in `Model.__init__` are gone too ([af56244677](https://github.com/dropseed/plain/commit/af56244677))
13
+ - `OrderBy.asc()` and `.desc()` return the `OrderBy` instead of `None`, matching every other expression's `asc`/`desc` and making `expr.desc().asc()` and inline use work. They still take no options — passing `nulls_first` or `nulls_last` raises `TypeError` pointing you at the attributes on the `OrderBy` itself, rather than being silently ignored ([f00e707ed0](https://github.com/dropseed/plain/commit/f00e707ed0))
14
+ - Preflight expression checks no longer crash when a lookup path continues past a non-relation field — for example `Meta.ordering = ("name__nope",)` on a text field. The traversal now raises `FieldDoesNotExist` for the trailing part, which preflight already reports, instead of an `AttributeError` on `None` ([d9406dfc6f](https://github.com/dropseed/plain/commit/d9406dfc6f))
15
+ - `MigrationGraph.make_state()` raises `NodeNotFoundError` when the plan hits a placeholder node, instead of an `AttributeError` on `None` ([d9406dfc6f](https://github.com/dropseed/plain/commit/d9406dfc6f))
16
+ - The migration autodetector and `ProjectState` narrow on `RelatedField` / `ManyToManyField` / `ManyToManyRel` instead of `hasattr(field, "remote_field")` and `getattr(remote_field, "through", None)`. Rename handling only rewrites a reference when the old field is the same kind of relation, so a field swapped between a plain relation and an M2M during a rename no longer copies a mismatched reference across ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
17
+ - `ProjectState._get_related_models` reads a forward relation's target from `remote_field.model_ref` rather than `f.model` — which returned the model the field is _declared on_, not the one it points at ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
18
+ - `Meta._expire_cache` propagation to a related model no longer swallows `AttributeError` to handle the unresolved-string case; it checks `model_ref` up front, so a genuine `AttributeError` inside `_expire_cache` surfaces ([a3962f7255](https://github.com/dropseed/plain/commit/a3962f7255))
19
+
20
+ ### Upgrade instructions
21
+
22
+ - If you read `rel.model` or `rel.through` (via `field.remote_field`) in code that runs before models are fully loaded — preflight checks, `contribute_to_class`, custom fields, migration operations — switch to `rel.model_ref` / `rel.through_ref` and check `isinstance(ref, str)`. After model loading completes, `rel.model` and `rel.through` behave as before.
23
+ - If you _assigned_ to `rel.model` or `rel.through`, assign to `rel.model_ref` / `rel.through_ref` instead — the plain names are now read-only properties.
24
+ - If you referenced `Field.concrete` or `ForeignObjectRel.concrete`, remove the check. `concrete` was `True` for every forward field and `False` only on reverse relations, which `get_fields()` doesn't return unless you pass `include_reverse=True`.
25
+ - If you asserted on the exact text of the `bulk_create()`, `bulk_update()`, or `save(update_fields=...)` errors, update the expected strings.
26
+ - If you caught `ValueError` from `ModelState` for a model-class `to`/`through` reference, catch `TypeError`.
27
+ - `OrderBy.asc()`/`.desc()` returning `self` is additive — existing calls that discarded the return value are unaffected.
28
+
29
+ ## [0.116.0](https://github.com/dropseed/plain/releases/plain-postgres@0.116.0) (2026-09-04)
30
+
31
+ ### What's changed
32
+
33
+ - **`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))
34
+ - `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))
35
+ - 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))
36
+ - `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))
37
+ - `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))
38
+ - `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))
39
+ - `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))
40
+
41
+ ### Upgrade instructions
42
+
43
+ - 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.
44
+ - Replace `_model_meta.local_many_to_many` with `_model_meta.many_to_many`.
45
+ - Replace `_model_meta._non_pk_concrete_field_names` with `_model_meta._non_pk_field_names`.
46
+ - 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.
47
+ - If you called `get_joining_columns()` on a reverse relation, it is now available on `ForeignKeyRel` only.
48
+
3
49
  ## [0.115.0](https://github.com/dropseed/plain/releases/plain-postgres@0.115.0) (2026-09-02)
4
50
 
5
51
  ### 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,14 +122,7 @@ 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
- # Virtual field
132
- if field.name not in kwargs and field.column is None:
133
- continue
134
126
  if isinstance(field, RelatedField) and isinstance(
135
127
  field.remote_field, ForeignObjectRel
136
128
  ):
@@ -193,16 +185,14 @@ class Model(metaclass=ModelBase):
193
185
 
194
186
  @classmethod
195
187
  def from_db(cls, field_names: Iterable[str], values: Sequence[Any]) -> Model:
196
- if len(values) != len(cls._model_meta.concrete_fields):
188
+ if len(values) != len(cls._model_meta.fields):
197
189
  values_iter = iter(values)
198
190
  values = [
199
191
  next(values_iter) if f.name in field_names else DEFERRED
200
- for f in cls._model_meta.concrete_fields
192
+ for f in cls._model_meta.fields
201
193
  ]
202
194
  # 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
- )
195
+ field_dict = dict(zip((f.name for f in cls._model_meta.fields), values))
206
196
  new = cls(_from_db=True, **field_dict)
207
197
  new._state.adding = False
208
198
  return new
@@ -279,11 +269,7 @@ class Model(metaclass=ModelBase):
279
269
  """
280
270
  Return a set containing names of deferred fields for this instance.
281
271
  """
282
- return {
283
- f.name
284
- for f in self._model_meta.concrete_fields
285
- if f.name not in self.__dict__
286
- }
272
+ return {f.name for f in self._model_meta.fields if f.name not in self.__dict__}
287
273
 
288
274
  def refresh_from_db(self, fields: list[str] | None = None) -> None:
289
275
  """
@@ -320,15 +306,13 @@ class Model(metaclass=ModelBase):
320
306
  db_instance_qs = db_instance_qs.only(*fields)
321
307
  elif deferred_fields:
322
308
  fields = [
323
- f.name
324
- for f in self._model_meta.concrete_fields
325
- if f.name not in deferred_fields
309
+ f.name for f in self._model_meta.fields if f.name not in deferred_fields
326
310
  ]
327
311
  db_instance_qs = db_instance_qs.only(*fields)
328
312
 
329
313
  db_instance = db_instance_qs.get()
330
314
  non_loaded_fields = db_instance.get_deferred_fields()
331
- for field in self._model_meta.concrete_fields:
315
+ for field in self._model_meta.fields:
332
316
  if field.name in non_loaded_fields:
333
317
  # This field wasn't refreshed - skip ahead.
334
318
  continue
@@ -404,12 +388,12 @@ class Model(metaclass=ModelBase):
404
388
  if not fields:
405
389
  return self # explicit "update nothing" -- no-op
406
390
  fields = frozenset(fields)
407
- field_names = self._model_meta._non_pk_concrete_field_names
391
+ field_names = self._model_meta._non_pk_field_names
408
392
  non_model_fields = fields.difference(field_names)
409
393
  if non_model_fields:
410
394
  raise ValueError(
411
- "The following fields do not exist in this model, are m2m "
412
- "fields, or are non-concrete fields: "
395
+ "The following fields do not exist in this model or are m2m "
396
+ "fields: "
413
397
  f"{', '.join(sorted(non_model_fields))}"
414
398
  )
415
399
  deferred_in_update = fields & deferred_fields
@@ -420,7 +404,7 @@ class Model(metaclass=ModelBase):
420
404
  )
421
405
  elif deferred_fields:
422
406
  # Loaded via .only()/.defer() -- write just the loaded fields.
423
- loaded = self._model_meta._non_pk_concrete_field_names - deferred_fields
407
+ loaded = self._model_meta._non_pk_field_names - deferred_fields
424
408
  if loaded:
425
409
  fields = loaded
426
410
  self._prepare_related_fields_for_save(
@@ -484,7 +468,7 @@ class Model(metaclass=ModelBase):
484
468
  fields from RETURNING. Omits id from the INSERT when unset so Postgres
485
469
  generates the identity value."""
486
470
  meta = self._model_meta
487
- fields = list(meta.local_concrete_fields)
471
+ fields = list(meta.fields)
488
472
  if self.id is None:
489
473
  id_field = meta.get_forward_field("id")
490
474
  fields = [f for f in fields if f is not id_field]
@@ -494,7 +478,6 @@ class Model(metaclass=ModelBase):
494
478
  )
495
479
  if results:
496
480
  for value, field in zip(results[0], returning_fields):
497
- assert field.name is not None
498
481
  setattr(self, field.name, value)
499
482
 
500
483
  def _update_row(self, fields: Iterable[str] | None) -> None:
@@ -502,7 +485,7 @@ class Model(metaclass=ModelBase):
502
485
  row matched -- update() targets an existing row and has no INSERT
503
486
  fallback (that's create())."""
504
487
  meta = self._model_meta
505
- non_pks = [f for f in meta.local_concrete_fields if not f.primary_key]
488
+ non_pks = [f for f in meta.fields if not f.primary_key]
506
489
  if fields:
507
490
  non_pks = [f for f in non_pks if f.name in fields]
508
491
  values = [(f, f.pre_save(self, False)) for f in non_pks]
@@ -525,7 +508,7 @@ class Model(metaclass=ModelBase):
525
508
  ) -> None:
526
509
  # Ensure that a model instance without a PK hasn't been assigned to
527
510
  # 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:
511
+ for field in self._model_meta.fields:
529
512
  if field_names and field.name not in field_names:
530
513
  continue
531
514
  # If the related field isn't cached, then an instance hasn't been
@@ -578,7 +561,6 @@ class Model(metaclass=ModelBase):
578
561
  with transaction.mark_for_rollback_on_error():
579
562
  count = self._model_meta.base_queryset.filter(id=self.id)._raw_delete()
580
563
  id_field = self._model_meta.get_forward_field("id")
581
- assert id_field.name is not None
582
564
  setattr(self, id_field.name, None)
583
565
  # Only the id is cleared -- every other field value survives so callers
584
566
  # can still reference a deleted row (correlate it, log it, check it's
@@ -611,7 +593,7 @@ class Model(metaclass=ModelBase):
611
593
  meta = meta or self._model_meta
612
594
  return {
613
595
  field.name: Value(field.value_from_object(self), field)
614
- for field in meta.local_concrete_fields
596
+ for field in meta.fields
615
597
  if field.name not in exclude
616
598
  }
617
599
 
@@ -755,9 +737,9 @@ class Model(metaclass=ModelBase):
755
737
  def _check_fields(cls) -> list[PreflightResult]:
756
738
  """Perform all field checks."""
757
739
  errors: list[PreflightResult] = []
758
- for field in cls._model_meta.local_fields:
740
+ for field in cls._model_meta.fields:
759
741
  errors.extend(field.preflight(from_model=cls))
760
- for field in cls._model_meta.local_many_to_many:
742
+ for field in cls._model_meta.many_to_many:
761
743
  errors.extend(field.preflight(from_model=cls))
762
744
  return errors
763
745
 
@@ -768,13 +750,16 @@ class Model(metaclass=ModelBase):
768
750
  errors: list[PreflightResult] = []
769
751
  seen_intermediary_signatures = []
770
752
 
771
- fields = cls._model_meta.local_many_to_many
753
+ fields = cls._model_meta.many_to_many
772
754
 
773
- # Skip when the target model wasn't found.
774
- fields = (f for f in fields if isinstance(f.remote_field.model, ModelBase))
775
-
776
- # Skip when the relationship model wasn't found.
777
- fields = (f for f in fields if isinstance(f.remote_field.through, ModelBase))
755
+ # Skip when the target or relationship model wasn't found; the field's
756
+ # own preflight reports those.
757
+ fields = (
758
+ f
759
+ for f in fields
760
+ if not isinstance(f.remote_field.model_ref, str)
761
+ and not isinstance(f.remote_field.through_ref, str)
762
+ )
778
763
 
779
764
  for f in fields:
780
765
  signature = (
@@ -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
@@ -1059,7 +1033,11 @@ class Model(metaclass=ModelBase):
1059
1033
  fld = None
1060
1034
  for part in field.split(LOOKUP_SEP):
1061
1035
  try:
1062
- fld = _cls._model_meta.get_field(part) # ty: ignore[unresolved-attribute]
1036
+ if _cls is None:
1037
+ # The previous part was not a relation, so there is
1038
+ # no model left to look the next part up on.
1039
+ raise FieldDoesNotExist(part)
1040
+ fld = _cls._model_meta.get_field(part)
1063
1041
  if isinstance(fld, RelatedField):
1064
1042
  _cls = fld.path_infos[-1].to_meta.model
1065
1043
  else:
@@ -1089,9 +1067,9 @@ class Model(metaclass=ModelBase):
1089
1067
  meta = cls._model_meta
1090
1068
  valid_fields = set(
1091
1069
  chain.from_iterable(
1092
- (f.name,)
1093
- if not (f.auto_created and not f.concrete)
1094
- else (f.field.related_query_name(),)
1070
+ (f.field.related_query_name(),)
1071
+ if isinstance(f, ForeignObjectRel)
1072
+ else (f.name,)
1095
1073
  for f in chain(meta.fields, meta.related_objects)
1096
1074
  )
1097
1075
  )
@@ -1121,7 +1099,7 @@ class Model(metaclass=ModelBase):
1121
1099
  # silently truncate, so we check for names that are too long
1122
1100
  allowed_len = MAX_NAME_LENGTH
1123
1101
 
1124
- for f in cls._model_meta.local_fields:
1102
+ for f in cls._model_meta.fields:
1125
1103
  column_name = f.column
1126
1104
 
1127
1105
  # Check if column name is too long for the database.
@@ -1135,13 +1113,13 @@ class Model(metaclass=ModelBase):
1135
1113
  )
1136
1114
  )
1137
1115
 
1138
- for f in cls._model_meta.local_many_to_many:
1116
+ for f in cls._model_meta.many_to_many:
1139
1117
  # Skip nonexistent models.
1140
- if isinstance(f.remote_field.through, str):
1118
+ if isinstance(f.remote_field.through_ref, str):
1141
1119
  continue
1142
1120
 
1143
1121
  # 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:
1122
+ for m2m in f.remote_field.through_ref._model_meta.fields:
1145
1123
  rel_name = m2m.column
1146
1124
  if rel_name is not None and len(rel_name) > allowed_len:
1147
1125
  errors.append(
@@ -1235,7 +1213,7 @@ class Model(metaclass=ModelBase):
1235
1213
  id="postgres.constraint_refers_to_joined_field",
1236
1214
  )
1237
1215
  )
1238
- errors.extend(cls._check_local_fields(fields, "constraints"))
1216
+ errors.extend(cls._check_referenced_fields(fields, "constraints"))
1239
1217
  return errors
1240
1218
 
1241
1219
 
@@ -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
@@ -1698,11 +1698,23 @@ class OrderBy(Expression):
1698
1698
  self.nulls_last = None
1699
1699
  return self
1700
1700
 
1701
- def asc(self) -> None: # ty: ignore[invalid-method-override]
1701
+ def asc(self, **kwargs: Any) -> OrderBy:
1702
+ if kwargs:
1703
+ raise TypeError(
1704
+ "OrderBy.asc() takes no options; set nulls_first/nulls_last on "
1705
+ "the OrderBy itself."
1706
+ )
1702
1707
  self.descending = False
1708
+ return self
1703
1709
 
1704
- def desc(self) -> None: # ty: ignore[invalid-method-override]
1710
+ def desc(self, **kwargs: Any) -> OrderBy:
1711
+ if kwargs:
1712
+ raise TypeError(
1713
+ "OrderBy.desc() takes no options; set nulls_first/nulls_last on "
1714
+ "the OrderBy itself."
1715
+ )
1705
1716
  self.descending = True
1717
+ return self
1706
1718
 
1707
1719
 
1708
1720
  class Window(Expression):
@@ -110,11 +110,11 @@ 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
- concrete: bool
118
118
  # Set by contribute_to_class
119
119
  model: type[Model]
120
120
 
@@ -134,7 +134,7 @@ class Field[T](RegisterLookupMixin):
134
134
  non_migration_attrs: tuple[str, ...] = ()
135
135
 
136
136
  def __init__(self) -> None:
137
- self.name = None # Set by set_attributes_from_name
137
+ self.name = "" # Set by set_attributes_from_name
138
138
  self.primary_key = False
139
139
  self.auto_created = False
140
140
 
@@ -151,8 +151,8 @@ class Field[T](RegisterLookupMixin):
151
151
  def __repr__(self) -> str:
152
152
  """Display the module, class, and name of the field."""
153
153
  path = f"{self.__class__.__module__}.{self.__class__.__qualname__}"
154
- name = getattr(self, "name", None)
155
- if name is not None:
154
+ name = getattr(self, "name", "")
155
+ if name:
156
156
  return f"<{path}: {name}>"
157
157
  return f"<{path}>"
158
158
 
@@ -164,7 +164,6 @@ class Field[T](RegisterLookupMixin):
164
164
  Check if field name is valid, i.e. 1) does not end with an
165
165
  underscore, 2) does not contain "__" and 3) is not "id".
166
166
  """
167
- assert self.name is not None, "Field name must be set before checking"
168
167
  if self.name.endswith("_"):
169
168
  return [
170
169
  PreflightResult(
@@ -215,7 +214,7 @@ class Field[T](RegisterLookupMixin):
215
214
  """
216
215
  return sql, params
217
216
 
218
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
217
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
219
218
  """
220
219
  Return enough information to recreate the field as a 4-tuple:
221
220
 
@@ -259,7 +258,6 @@ class Field[T](RegisterLookupMixin):
259
258
  cls_name = self.__class__.__qualname__
260
259
  if getattr(_postgres_root, cls_name, None) is self.__class__:
261
260
  path = f"plain.postgres.{cls_name}"
262
- # Note: self.name can be None during migration state rendering when fields are cloned
263
261
  return (self.name, path, [], keywords)
264
262
 
265
263
  def clone(self) -> Self:
@@ -305,7 +303,6 @@ class Field[T](RegisterLookupMixin):
305
303
  # values - so, this is very close to normal pickle.
306
304
  state = self.__dict__.copy()
307
305
  return _empty, (self.__class__,), state
308
- assert self.name is not None
309
306
  options = model.model_options
310
307
  return _load_field, (
311
308
  options.package_label,
@@ -378,7 +375,6 @@ class Field[T](RegisterLookupMixin):
378
375
  # The database column is the field name. A foreign key overrides this
379
376
  # method only to append the "_id" suffix to `column`.
380
377
  self.column = self.name
381
- self.concrete = self.column is not None
382
378
 
383
379
  def contribute_to_class(self, cls: type[Model], name: str) -> None:
384
380
  """
@@ -394,7 +390,6 @@ class Field[T](RegisterLookupMixin):
394
390
  # Field is its own descriptor; make sure it is set on the class so
395
391
  # attribute access hits __get__/__set__.
396
392
  if self.column:
397
- assert self.name is not None
398
393
  setattr(cls, self.name, self)
399
394
 
400
395
  # Descriptor protocol implementation
@@ -422,7 +417,6 @@ class Field[T](RegisterLookupMixin):
422
417
  return self
423
418
 
424
419
  # Instance access - get value from instance dict
425
- assert self.name is not None
426
420
  data = instance.__dict__
427
421
  field_name = self.name
428
422
 
@@ -432,9 +426,7 @@ class Field[T](RegisterLookupMixin):
432
426
  # instance cheap to use beyond its primary key.
433
427
  if field_name not in data:
434
428
  missing = [
435
- f.name
436
- for f in instance._model_meta.concrete_fields
437
- if f.name not in data
429
+ f.name for f in instance._model_meta.fields if f.name not in data
438
430
  ]
439
431
  instance.refresh_from_db(fields=missing)
440
432
 
@@ -472,7 +464,6 @@ class Field[T](RegisterLookupMixin):
472
464
  stored = self.to_python(value)
473
465
 
474
466
  # Store in instance dict
475
- assert self.name is not None
476
467
  instance.__dict__[self.name] = stored
477
468
 
478
469
  def __delete__(self, instance: Model) -> None:
@@ -481,7 +472,6 @@ class Field[T](RegisterLookupMixin):
481
472
 
482
473
  Removes the value from instance.__dict__.
483
474
  """
484
- assert self.name is not None
485
475
  try:
486
476
  del instance.__dict__[self.name]
487
477
  except KeyError:
@@ -491,7 +481,6 @@ class Field[T](RegisterLookupMixin):
491
481
 
492
482
  def pre_save(self, model_instance: Model, add: bool) -> T | None:
493
483
  """Return field's value just before saving."""
494
- assert self.name is not None
495
484
  return getattr(model_instance, self.name)
496
485
 
497
486
  def get_prep_value(self, value: Any) -> Any:
@@ -544,12 +533,10 @@ class Field[T](RegisterLookupMixin):
544
533
  return self.get_db_default_expression() is not None
545
534
 
546
535
  def save_form_data(self, instance: Model, data: Any) -> None:
547
- assert self.name is not None
548
536
  setattr(instance, self.name, data)
549
537
 
550
538
  def value_from_object(self, obj: Model) -> T | None:
551
539
  """Return the value of this field in the given model instance."""
552
- assert self.name is not None
553
540
  return getattr(obj, self.name)
554
541
 
555
542
 
@@ -653,7 +640,7 @@ class ColumnField[T](Field[T]):
653
640
  return None
654
641
  return self._default_empty_value
655
642
 
656
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
643
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
657
644
  name, path, args, kwargs = super().deconstruct()
658
645
  if self.required is not True:
659
646
  kwargs["required"] = self.required
@@ -750,7 +737,7 @@ class DefaultableField[T](ColumnField[T]):
750
737
  # shared state across instances.
751
738
  return copy.deepcopy(self.default)
752
739
 
753
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
740
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
754
741
  name, path, args, kwargs = super().deconstruct()
755
742
  if self.default is not NOT_PROVIDED:
756
743
  kwargs["default"] = self.default
@@ -891,7 +878,7 @@ class ChoicesField[T](DefaultableField[T]):
891
878
  )
892
879
  super().validate(value, model_instance)
893
880
 
894
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
881
+ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
895
882
  name, path, args, kwargs = super().deconstruct()
896
883
  if self.choices is not None:
897
884
  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