plain.postgres 0.112.0__tar.gz → 0.113.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 (231) hide show
  1. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/PKG-INFO +4 -3
  2. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/CHANGELOG.md +19 -1
  3. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/README.md +3 -2
  4. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +1 -1
  5. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/cli/sync.py +1 -1
  6. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/expressions.py +3 -5
  7. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/base.py +53 -18
  8. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/binary.py +5 -4
  9. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/encrypted.py +56 -104
  10. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/json.py +12 -5
  11. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/related.py +8 -30
  12. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/related_managers.py +8 -4
  13. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/reverse_related.py +3 -9
  14. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/forms.py +4 -11
  15. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/lookups.py +2 -2
  16. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/autodetector.py +26 -16
  17. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/query.py +4 -87
  18. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/query_utils.py +6 -50
  19. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/sql/__init__.py +1 -2
  20. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/sql/compiler.py +15 -133
  21. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/sql/datastructures.py +1 -36
  22. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/sql/query.py +26 -284
  23. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/sql/where.py +4 -42
  24. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/types.pyi +36 -0
  25. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/pyproject.toml +1 -1
  26. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/encrypted.py +1 -1
  27. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_autodetector_not_null_errors.py +86 -42
  28. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_literal_default_persistence.py +42 -4
  29. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_schema_normalize_type.py +7 -0
  30. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_encrypted_fields.py +13 -3
  31. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_m2m.py +37 -0
  32. plain_postgres-0.113.0/tests/public/test_only_empty_defaults.py +76 -0
  33. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/.gitignore +0 -0
  34. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/CLAUDE.md +0 -0
  35. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/LICENSE +0 -0
  36. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/README.md +0 -0
  37. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/__init__.py +0 -0
  38. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/adapters.py +0 -0
  39. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  40. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/aggregates.py +0 -0
  41. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/base.py +0 -0
  42. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/cli/__init__.py +0 -0
  43. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/cli/converge.py +0 -0
  44. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/cli/core.py +0 -0
  45. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/cli/decorators.py +0 -0
  46. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/cli/diagnose.py +0 -0
  47. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/cli/migrations.py +0 -0
  48. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/cli/schema.py +0 -0
  49. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/config.py +0 -0
  50. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/connection.py +0 -0
  51. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/constants.py +0 -0
  52. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/constraints.py +0 -0
  53. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/convergence/__init__.py +0 -0
  54. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/convergence/analysis.py +0 -0
  55. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/convergence/corrections.py +0 -0
  56. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/convergence/planning.py +0 -0
  57. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/database_url.py +0 -0
  58. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/databases.py +0 -0
  59. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/db.py +0 -0
  60. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/ddl.py +0 -0
  61. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/default_settings.py +0 -0
  62. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/deletion.py +0 -0
  63. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/dialect.py +0 -0
  64. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/entrypoints.py +0 -0
  65. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/enums.py +0 -0
  66. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/exceptions.py +0 -0
  67. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/__init__.py +0 -0
  68. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/boolean.py +0 -0
  69. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/duration.py +0 -0
  70. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/mixins.py +0 -0
  71. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/network.py +0 -0
  72. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/numeric.py +0 -0
  73. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/primary_key.py +0 -0
  74. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/related_descriptors.py +0 -0
  75. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/related_lookups.py +0 -0
  76. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
  77. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/temporal.py +0 -0
  78. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/text.py +0 -0
  79. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/timezones.py +0 -0
  80. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/fields/uuid.py +0 -0
  81. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/functions/__init__.py +0 -0
  82. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/functions/comparison.py +0 -0
  83. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/functions/datetime.py +0 -0
  84. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/functions/math.py +0 -0
  85. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/functions/mixins.py +0 -0
  86. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/functions/random.py +0 -0
  87. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/functions/text.py +0 -0
  88. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/functions/uuid.py +0 -0
  89. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/functions/window.py +0 -0
  90. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/indexes.py +0 -0
  91. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/introspection/__init__.py +0 -0
  92. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/introspection/health/__init__.py +0 -0
  93. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
  94. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  95. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
  96. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/introspection/health/context.py +0 -0
  97. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/introspection/health/helpers.py +0 -0
  98. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/introspection/health/ownership.py +0 -0
  99. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/introspection/health/runner.py +0 -0
  100. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/introspection/health/types.py +0 -0
  101. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/introspection/schema.py +0 -0
  102. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/meta.py +0 -0
  103. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/middleware.py +0 -0
  104. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/__init__.py +0 -0
  105. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/exceptions.py +0 -0
  106. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/executor.py +0 -0
  107. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/graph.py +0 -0
  108. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/loader.py +0 -0
  109. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/migration.py +0 -0
  110. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/operations/__init__.py +0 -0
  111. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/operations/base.py +0 -0
  112. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/operations/fields.py +0 -0
  113. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/operations/models.py +0 -0
  114. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/operations/special.py +0 -0
  115. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/optimizer.py +0 -0
  116. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/questioner.py +0 -0
  117. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/recorder.py +0 -0
  118. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/serializer.py +0 -0
  119. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/state.py +0 -0
  120. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/utils.py +0 -0
  121. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/migrations/writer.py +0 -0
  122. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/options.py +0 -0
  123. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/otel.py +0 -0
  124. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/preflight/__init__.py +0 -0
  125. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/preflight/database.py +0 -0
  126. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/preflight/indexes.py +0 -0
  127. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/preflight/models.py +0 -0
  128. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/registry.py +0 -0
  129. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/schema.py +0 -0
  130. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/schema_lock.py +0 -0
  131. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/sources.py +0 -0
  132. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/sql/constants.py +0 -0
  133. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/test/__init__.py +0 -0
  134. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/test/database.py +0 -0
  135. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/test/pytest.py +0 -0
  136. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/transaction.py +0 -0
  137. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/types.py +0 -0
  138. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/plain/postgres/utils.py +0 -0
  139. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/forms.py +0 -0
  140. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  141. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  142. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  143. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  144. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  145. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  146. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  147. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  148. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  149. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  150. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  151. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  152. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  153. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
  154. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  155. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  156. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  157. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
  158. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/migrations/__init__.py +0 -0
  159. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/__init__.py +0 -0
  160. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/constraints.py +0 -0
  161. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/defaults.py +0 -0
  162. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/delete.py +0 -0
  163. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/forms.py +0 -0
  164. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/indexes.py +0 -0
  165. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/iteration.py +0 -0
  166. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/mixins.py +0 -0
  167. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/nullability.py +0 -0
  168. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/querysets.py +0 -0
  169. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/relationships.py +0 -0
  170. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/storage_parameters.py +0 -0
  171. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/trees.py +0 -0
  172. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/models/unregistered.py +0 -0
  173. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/urls.py +0 -0
  174. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/examples/views.py +0 -0
  175. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/settings.py +0 -0
  176. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/app/urls.py +0 -0
  177. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/conftest.py +0 -0
  178. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/conftest_convergence.py +0 -0
  179. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_apply_replan.py +0 -0
  180. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_autodetector_type_change.py +0 -0
  181. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_connection_isolation.py +0 -0
  182. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_connection_lifecycle.py +0 -0
  183. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_connection_pool.py +0 -0
  184. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_connection_self_heal.py +0 -0
  185. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_constraint_violation_error.py +0 -0
  186. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_convergence.py +0 -0
  187. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_convergence_constraints.py +0 -0
  188. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_convergence_defaults.py +0 -0
  189. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_convergence_fk.py +0 -0
  190. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_convergence_indexes.py +0 -0
  191. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_convergence_nullability.py +0 -0
  192. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
  193. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_convergence_timeouts.py +0 -0
  194. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_databases_not_on_runtime_path.py +0 -0
  195. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_db_expression_defaults.py +0 -0
  196. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_diagnose.py +0 -0
  197. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_executor_connection_hook.py +0 -0
  198. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_fk_characterization.py +0 -0
  199. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_health.py +0 -0
  200. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_introspection.py +0 -0
  201. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_management_connection.py +0 -0
  202. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_migration_executor.py +0 -0
  203. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_no_callable_defaults.py +0 -0
  204. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_otel_metrics.py +0 -0
  205. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
  206. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
  207. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
  208. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_rollback_exc_attribution.py +0 -0
  209. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_schema_lock.py +0 -0
  210. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/internal/test_schema_timeouts.py +0 -0
  211. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_create_update.py +0 -0
  212. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_database_url.py +0 -0
  213. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_databases.py +0 -0
  214. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_deferred_loading.py +0 -0
  215. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_delete_behaviors.py +0 -0
  216. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_exceptions.py +0 -0
  217. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_field_defaults.py +0 -0
  218. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_functions_uuid.py +0 -0
  219. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_integrity_error_mapping.py +0 -0
  220. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_iterator.py +0 -0
  221. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_manager_assignment.py +0 -0
  222. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_manual_pk.py +0 -0
  223. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_mixins.py +0 -0
  224. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_modelform_roundtrip.py +0 -0
  225. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_queryset_repr.py +0 -0
  226. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_queryset_slicing.py +0 -0
  227. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_random_string_field.py +0 -0
  228. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_raw_query.py +0 -0
  229. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_read_only_transactions.py +0 -0
  230. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_related.py +0 -0
  231. {plain_postgres-0.112.0 → plain_postgres-0.113.0}/tests/public/test_related_instance_filter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.postgres
3
- Version: 0.112.0
3
+ Version: 0.113.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
@@ -941,6 +941,7 @@ Values are encrypted using Fernet (AES-128-CBC + HMAC-SHA256) with a key derived
941
941
 
942
942
  - **No lookups** — encrypted values are non-deterministic (same plaintext produces different ciphertext each time), so filtering on encrypted fields doesn't work. Only `isnull` lookups are supported.
943
943
  - **No indexes or constraints** — encrypted fields cannot be used in indexes or unique constraints. Preflight checks will catch this.
944
+ - **Only `default=""`** — on `EncryptedTextField` (paired with `required=False`), the empty string is stored as plaintext `''`, so it's the one value expressible as a column `DEFAULT` (declare it to add the field to a populated table). Any other default would need ciphertext, which is non-deterministic. `EncryptedJSONField` accepts no default at all — even `{}` serializes to text that would need ciphertext; use `allow_null=True`.
944
945
 
945
946
  **Key rotation:**
946
947
 
@@ -1492,9 +1493,9 @@ See [`default_settings.py`](./default_settings.py) for more details.
1492
1493
 
1493
1494
  Add the field to your model class, then run `plain migrations create` to create a migration.
1494
1495
 
1495
- If the field is required (no `default=` and not `allow_null=True`), the autodetector refuses to generate the migration, since there's no value to seed existing rows with. You have two options:
1496
+ If the field has no `default=` and isn't `allow_null=True`, the autodetector refuses to generate the migration, since there's no value to seed existing rows with — `required=False` alone is not enough (it only affects Python-side validation, not the column). You have two options:
1496
1497
 
1497
- 1. Declare a `default=` on the field so the new column has a value for existing rows.
1498
+ 1. Declare a `default=` on the field so the new column has a value for existing rows. For an optional string field the idiom is `required=False, default=""` (for `BinaryField`, `default=b""`).
1498
1499
  2. Add the field with `allow_null=True`, scaffold a data migration with `plain migrations create --empty --name backfill_<field>` to populate existing rows, then remove `allow_null=True` from the field — convergence applies `NOT NULL` on the next `postgres sync`.
1499
1500
 
1500
1501
  #### How do I make an existing column `NOT NULL`?
@@ -1,6 +1,24 @@
1
1
  # plain-postgres changelog
2
2
 
3
- ## [0.112.0](https://github.com/dropseed/plain/releases/plain-postgres@0.112.0) (2026-07-21)
3
+ ## [0.113.0](https://github.com/dropseed/plain/releases/plain-postgres@0.113.0) (2026-08-02)
4
+
5
+ ### What's changed
6
+
7
+ - **`QuerySet.extra()` has been removed**, along with all of its supporting machinery (`extra_select`, `extra_tables`, `extra_order_by`, `ExtraWhere`). Use `annotate()` with `RawSQL` or other expressions instead. ([3fed0b32f1](https://github.com/dropseed/plain/commit/3fed0b32f1))
8
+ - **`FilteredRelation` has been removed** — it's no longer importable from `plain.postgres` or usable in `annotate()`. Express conditional joins with `Q` filters or subqueries instead. ([434f57852f](https://github.com/dropseed/plain/commit/434f57852f))
9
+ - **`QuerySet.alias()` and logical XOR have been removed** — `Q(...) ^ Q(...)` and `queryset ^ queryset` are no longer supported. Use `annotate()` in place of `alias()`, and compose AND/OR/NOT in place of XOR. ([c12cf81af4](https://github.com/dropseed/plain/commit/c12cf81af4))
10
+ - **Encrypted and binary fields now accept their empty value as a declared `default=`.** `EncryptedTextField` (now a `TextField` subclass) accepts exactly `default=""` and `BinaryField` accepts exactly `default=b""`, each paired with `required=False` — the empty value is stored as plaintext so it's the one default expressible as a column `DEFAULT`, which is what lets the field be added to a populated table. `default=None` requires `allow_null=True` + `required=False`. The `types.pyi` stub overloads encode the same coupling. ([2a86968e5a](https://github.com/dropseed/plain/commit/2a86968e5a))
11
+ - **The migration autodetector no longer waves through `required=False` fields with no declared default.** Adding a non-nullable field without a `default=` now refuses at `plain migrations create` time with a field-accurate remedy (previously these could generate an `AddField` that failed at apply time on any populated table). The optional-string idiom is `required=False, default=""`. ([2a86968e5a](https://github.com/dropseed/plain/commit/2a86968e5a))
12
+ - **`EncryptedJSONField` is now a `JSONField` subclass** and `EncryptedTextField` a `TextField` subclass, instead of parallel `ColumnField` implementations — behavior like max_length validation and JSON encoding now comes from the real base classes. The encrypted lookup surface is still just `exact`/`isnull`, and JSON key transforms are blocked (they'd operate on ciphertext). `EncryptedJSONField` accepts no `default=` at all — even `{}` would need ciphertext; use `allow_null=True`. ([5153ceb106](https://github.com/dropseed/plain/commit/5153ceb106), [2a86968e5a](https://github.com/dropseed/plain/commit/2a86968e5a))
13
+ - Model forms no longer prefill a `BinaryField`'s `b""` default as a text input's initial value (it rendered as the literal `b''`), and encrypted fields now map to form fields through the regular `TextField`/`JSONField` branches. ([2a86968e5a](https://github.com/dropseed/plain/commit/2a86968e5a))
14
+ - M2M prefetches now expose the through-table join column via `annotate(RawSQL(...))` instead of the removed `extra()`, and internal path-info plumbing lost its `filtered_relation` threading. ([3fed0b32f1](https://github.com/dropseed/plain/commit/3fed0b32f1), [434f57852f](https://github.com/dropseed/plain/commit/434f57852f))
15
+
16
+ ### Upgrade instructions
17
+
18
+ - Replace `QuerySet.extra()` calls: `extra(select=...)` becomes `annotate(name=RawSQL("...", []))`, `extra(where=...)` becomes `filter()` with expressions or `RawSQL`, and `extra(order_by=...)` becomes `order_by()` on an annotation.
19
+ - Replace `FilteredRelation` annotations with filtered subqueries or `Q` conditions, and `QuerySet.alias()` with `annotate()`.
20
+ - Replace `Q(a) ^ Q(b)` / `qs1 ^ qs2` with explicit AND/OR logic: `(Q(a) | Q(b)) & ~(Q(a) & Q(b))`.
21
+ - If `plain migrations create` now refuses a field it previously accepted, follow the error's remedy: declare `required=False, default=""` (or `default=b""` / an appropriate literal) so existing rows get a value, or add it with `allow_null=True` plus a backfill data migration.(https://github.com/dropseed/plain/releases/plain-postgres@0.112.0) (2026-07-21)
4
22
 
5
23
  ### What's changed
6
24
 
@@ -928,6 +928,7 @@ Values are encrypted using Fernet (AES-128-CBC + HMAC-SHA256) with a key derived
928
928
 
929
929
  - **No lookups** — encrypted values are non-deterministic (same plaintext produces different ciphertext each time), so filtering on encrypted fields doesn't work. Only `isnull` lookups are supported.
930
930
  - **No indexes or constraints** — encrypted fields cannot be used in indexes or unique constraints. Preflight checks will catch this.
931
+ - **Only `default=""`** — on `EncryptedTextField` (paired with `required=False`), the empty string is stored as plaintext `''`, so it's the one value expressible as a column `DEFAULT` (declare it to add the field to a populated table). Any other default would need ciphertext, which is non-deterministic. `EncryptedJSONField` accepts no default at all — even `{}` serializes to text that would need ciphertext; use `allow_null=True`.
931
932
 
932
933
  **Key rotation:**
933
934
 
@@ -1479,9 +1480,9 @@ See [`default_settings.py`](./default_settings.py) for more details.
1479
1480
 
1480
1481
  Add the field to your model class, then run `plain migrations create` to create a migration.
1481
1482
 
1482
- If the field is required (no `default=` and not `allow_null=True`), the autodetector refuses to generate the migration, since there's no value to seed existing rows with. You have two options:
1483
+ If the field has no `default=` and isn't `allow_null=True`, the autodetector refuses to generate the migration, since there's no value to seed existing rows with — `required=False` alone is not enough (it only affects Python-side validation, not the column). You have two options:
1483
1484
 
1484
- 1. Declare a `default=` on the field so the new column has a value for existing rows.
1485
+ 1. Declare a `default=` on the field so the new column has a value for existing rows. For an optional string field the idiom is `required=False, default=""` (for `BinaryField`, `default=b""`).
1485
1486
  2. Add the field with `allow_null=True`, scaffold a data migration with `plain migrations create --empty --name backfill_<field>` to populate existing rows, then remove `allow_null=True` from the field — convergence applies `NOT NULL` on the next `postgres sync`.
1486
1487
 
1487
1488
  #### How do I make an existing column `NOT NULL`?
@@ -79,7 +79,7 @@ Run `uv run plain docs postgres` for full patterns with code examples.
79
79
  - Indexes: `{table}_{column(s)}_idx`
80
80
  - Constraints: `{table}_{column(s)}_{type}` (e.g., `_unique`, `_check`)
81
81
  - Choose `on_delete` deliberately: CASCADE for owned children, RESTRICT for referenced data, SET_NULL for optional references
82
- - No `allow_null` on string fields — use `default=""`
82
+ - No `allow_null` on string fields — use `default=""`. Optional string fields are `required=False, default=""` — the declared default is what lets the column be added to a populated table (`required=False` alone only affects Python-side validation)
83
83
 
84
84
  Run `uv run plain docs postgres` for full patterns with code examples.
85
85
 
@@ -7,6 +7,7 @@ import click
7
7
  from plain.runtime import settings
8
8
 
9
9
  from ..convergence import execute_plan, plan_convergence
10
+ from ..db import get_connection
10
11
  from .decorators import cli_schema_lock, database_management_command
11
12
 
12
13
 
@@ -112,7 +113,6 @@ def _create_migrations() -> None:
112
113
 
113
114
 
114
115
  def _migrate() -> None:
115
- from ..db import get_connection
116
116
  from ..migrations.executor import MigrationExecutor
117
117
 
118
118
  click.secho("Applying migrations...", bold=True)
@@ -163,9 +163,7 @@ class Combinable:
163
163
  def bitrightshift(self, other: Any) -> CombinedExpression:
164
164
  return self._combine(other, self.BITRIGHTSHIFT, False)
165
165
 
166
- def __xor__(self, other: Any) -> Q:
167
- if getattr(self, "conditional", False) and getattr(other, "conditional", False):
168
- return Q(self) ^ Q(other)
166
+ def __xor__(self, other: Any) -> None:
169
167
  raise NotImplementedError(
170
168
  "Use .bitand(), .bitor(), and .bitxor() for bitwise logical operations."
171
169
  )
@@ -431,8 +429,8 @@ class BaseExpression:
431
429
  def get_lookup(self, lookup: str) -> type[Lookup] | None:
432
430
  return self.output_field.get_lookup(lookup)
433
431
 
434
- def get_transform(self, name: str) -> type[Transform] | None:
435
- return self.output_field.get_transform(name) # ty: ignore[invalid-return-type]
432
+ def get_transform(self, name: str) -> Callable[..., Transform] | None:
433
+ return self.output_field.get_transform(name)
436
434
 
437
435
  def relabeled_clone(self, change_map: dict[str, str]) -> Self:
438
436
  clone = self.copy()
@@ -516,8 +516,8 @@ class Field[T](RegisterLookupMixin):
516
516
  return self.get_db_prep_value(value, connection=connection, prepared=False)
517
517
 
518
518
  # Empty-value fallback used by ColumnField.get_default for the
519
- # not-null + not-required + empty_strings_allowed case (Python-side
520
- # Model() construction). BinaryField overrides with b"".
519
+ # not-null + empty_strings_allowed case (Python-side Model()
520
+ # construction). BinaryField overrides with b"".
521
521
  _default_empty_value: Any = ""
522
522
 
523
523
  def has_default(self) -> bool:
@@ -666,6 +666,15 @@ class DefaultableField[T](ColumnField[T]):
666
666
 
667
667
  non_migration_attrs = (*ColumnField.non_migration_attrs, "default")
668
668
 
669
+ # Subclasses whose __init__ deliberately doesn't take default=
670
+ # (EncryptedJSONField) set this False so callers like the autodetector's
671
+ # error guidance don't suggest a kwarg that would raise.
672
+ accepts_default = True
673
+
674
+ # Subclasses set this when their only expressible column DEFAULT is the
675
+ # class's `_default_empty_value` ("" / b"").
676
+ only_empty_default = False
677
+
669
678
  def __init__(
670
679
  self,
671
680
  *,
@@ -674,22 +683,48 @@ class DefaultableField[T](ColumnField[T]):
674
683
  allow_null: bool = False,
675
684
  validators: Sequence[Callable[..., Any]] = (),
676
685
  ):
677
- if default is not NOT_PROVIDED and callable(default):
678
- raise TypeError(
679
- f"{type(self).__name__}(default=...) must be a static literal. "
680
- f"For empty collections pass default={{}} or default=[]; for "
681
- f"per-row generation use a DB-side expression "
682
- f"(create_now=True, generate=True, RandomStringField)."
683
- )
684
- if default is not NOT_PROVIDED and isinstance(default, str) and "\\" in default:
685
- # psycopg quotes backslash-bearing strings with `E'...'` escape
686
- # syntax, but pg_get_expr returns the stored DEFAULT as a standard
687
- # `'...'` literal — the two forms don't compare lexically, so
688
- # convergence would flag spurious drift on every sync. Reject at
689
- # declaration time rather than ship a sync that never converges.
690
- raise ValueError(
691
- f"{type(self).__name__}(default=...) must not contain a backslash."
692
- )
686
+ if default is not NOT_PROVIDED:
687
+ if callable(default):
688
+ raise TypeError(
689
+ f"{type(self).__name__}(default=...) must be a static literal. "
690
+ f"For empty collections pass default={{}} or default=[]; for "
691
+ f"per-row generation use a DB-side expression "
692
+ f"(create_now=True, generate=True, RandomStringField)."
693
+ )
694
+ if isinstance(default, str) and "\\" in default:
695
+ # psycopg quotes backslash-bearing strings with `E'...'` escape
696
+ # syntax, but pg_get_expr returns the stored DEFAULT as a standard
697
+ # `'...'` literal — the two forms don't compare lexically, so
698
+ # convergence would flag spurious drift on every sync. Reject at
699
+ # declaration time rather than ship a sync that never converges.
700
+ raise ValueError(
701
+ f"{type(self).__name__}(default=...) must not contain a backslash."
702
+ )
703
+ if self.only_empty_default:
704
+ empty = self._default_empty_value
705
+ if default is None:
706
+ if not allow_null or required:
707
+ raise ValueError(
708
+ f"{type(self).__name__} with default=None must also "
709
+ f"set allow_null=True and required=False — the "
710
+ f"column must accept NULL, and required=True would "
711
+ f"reject the defaulted None on every save."
712
+ )
713
+ # `type()` (not isinstance/==) so bytearray()/memoryview(b"")
714
+ # can't slip through — they compare equal to b"" but don't
715
+ # deepcopy or serialize like it.
716
+ elif type(default) is not type(empty) or default != empty:
717
+ raise ValueError(
718
+ f"{type(self).__name__} only supports default={empty!r} — "
719
+ f"the empty value, used to backfill existing rows when "
720
+ f"the field is added to a populated table."
721
+ )
722
+ elif required:
723
+ raise ValueError(
724
+ f"{type(self).__name__} with default={empty!r} must also "
725
+ f"set required=False — the default fills the field with "
726
+ f"an empty value that required=True then rejects."
727
+ )
693
728
  self.default = default
694
729
  super().__init__(
695
730
  required=required,
@@ -8,7 +8,7 @@ import psycopg
8
8
 
9
9
  from plain.validators import MaxLengthValidator
10
10
 
11
- from .base import ColumnField
11
+ from .base import NOT_PROVIDED, DefaultableField
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from plain.postgres.connection import DatabaseConnection
@@ -17,10 +17,11 @@ if TYPE_CHECKING:
17
17
 
18
18
  class BinaryField[
19
19
  T: (bytes | memoryview, bytes | memoryview | None) = bytes | memoryview
20
- ](ColumnField[T]):
20
+ ](DefaultableField[T]):
21
21
  db_type_sql = "bytea"
22
22
  empty_values = [None, b""]
23
23
  _default_empty_value = b""
24
+ only_empty_default = True
24
25
 
25
26
  def __init__(
26
27
  self,
@@ -28,14 +29,14 @@ class BinaryField[
28
29
  max_length: int | None = None,
29
30
  required: bool = True,
30
31
  allow_null: bool = False,
32
+ default: Any = NOT_PROVIDED,
31
33
  validators: Sequence[Callable[..., Any]] = (),
32
34
  ):
33
- # `default` is intentionally not accepted: a str default on a bytes
34
- # field is a type mismatch.
35
35
  self.max_length = max_length
36
36
  super().__init__(
37
37
  required=required,
38
38
  allow_null=allow_null,
39
+ default=default,
39
40
  validators=validators,
40
41
  )
41
42
  if self.max_length is not None:
@@ -16,11 +16,14 @@ except ImportError:
16
16
  hashes = None # ty: ignore[invalid-assignment]
17
17
  PBKDF2HMAC = None # ty: ignore[invalid-assignment]
18
18
 
19
- from plain import exceptions, preflight
19
+ from plain import preflight
20
+ from plain.postgres.lookups import Exact, IsNull
20
21
  from plain.runtime import settings
21
22
  from plain.utils.encoding import force_bytes
22
23
 
23
- from .base import ColumnField
24
+ from .base import NOT_PROVIDED
25
+ from .json import JSONField
26
+ from .text import TextField
24
27
 
25
28
  if TYPE_CHECKING:
26
29
  from collections.abc import Callable, Sequence
@@ -104,19 +107,12 @@ def _decrypt(value: str) -> str:
104
107
  )
105
108
 
106
109
 
107
- # isnull is obviously needed. exact is required so that `filter(field=None)`
108
- # works — the ORM resolves "exact" first and then rewrites None to isnull.
109
- # Exact lookups on non-None values will silently return no results (since
110
- # ciphertext is non-deterministic), but blocking exact entirely would break
111
- # the None/isnull path.
112
- _ALLOWED_LOOKUPS = {"isnull", "exact"}
113
-
114
-
115
110
  class EncryptedFieldMixin:
116
111
  """Shared behavior for all encrypted fields.
117
112
 
118
- Blocks lookups (except isnull and exact) since encrypted values are non-deterministic.
119
- Errors at preflight if the field is used in indexes or unique constraints.
113
+ Owns the lookup surface (isnull and exact only — ciphertext is
114
+ non-deterministic) and the preflight that blocks indexes and unique
115
+ constraints.
120
116
 
121
117
  Must be used with Field as a co-base class.
122
118
  """
@@ -125,17 +121,32 @@ class EncryptedFieldMixin:
125
121
  name: str
126
122
  model: Any
127
123
 
128
- def get_lookup(self, lookup_name: str) -> type[Lookup] | None:
129
- if lookup_name not in _ALLOWED_LOOKUPS:
130
- return None
131
- get_lookup = getattr(super(), "get_lookup")
132
- return get_lookup(lookup_name)
133
-
134
- def get_transform(
135
- self, lookup_name: str
136
- ) -> type[Transform] | Callable[..., Any] | None:
124
+ # The complete lookup surface, replacing the base field's registry.
125
+ # isnull is obviously needed. exact is required so that `filter(field=None)`
126
+ # works — the ORM resolves "exact" first and then rewrites None to isnull.
127
+ # Exact lookups on non-None values will silently return no results (since
128
+ # ciphertext is non-deterministic), but blocking exact entirely would break
129
+ # the None/isnull path. The base classes are named directly — inheriting
130
+ # the concrete field's registrations would leak specialized lookups like
131
+ # JSONField's JSONExact, which compares against the jsonb 'null' literal
132
+ # and defeats the None→isnull rewrite. get_lookup()/get_transform() and
133
+ # registry consumers (e.g. unsupported-lookup error suggestions) all
134
+ # resolve through this one dict. A classmethod so both class-level and
135
+ # instance-level callers work.
136
+ @classmethod
137
+ def get_lookups(cls) -> dict[str, type[Lookup | Transform]]:
138
+ return {"exact": Exact, "isnull": IsNull}
139
+
140
+ def get_transform(self, name: str) -> Callable[..., Transform] | None:
141
+ # JSONField's get_transform falls back to KeyTransformFactory for any
142
+ # name — key transforms would operate on ciphertext, so block them.
137
143
  return None
138
144
 
145
+ def preflight(self, **kwargs: Any) -> list[PreflightResult]:
146
+ errors: list[PreflightResult] = super().preflight(**kwargs) # ty: ignore[unresolved-attribute]
147
+ errors.extend(self._check_encrypted_constraints())
148
+ return errors
149
+
139
150
  def _check_encrypted_constraints(self) -> list[PreflightResult]:
140
151
  errors: list[PreflightResult] = []
141
152
  if not hasattr(self, "model"):
@@ -178,20 +189,21 @@ class EncryptedFieldMixin:
178
189
  return errors
179
190
 
180
191
 
181
- class EncryptedTextField[T: (str, str | None) = str](
182
- EncryptedFieldMixin, ColumnField[T]
183
- ):
184
- """A text field that encrypts its value before storing in the database.
192
+ class EncryptedTextField[T: (str, str | None) = str](EncryptedFieldMixin, TextField[T]):
193
+ """A TextField that encrypts its value before storing in the database.
185
194
 
186
195
  Values are encrypted using Fernet (AES-128-CBC + HMAC-SHA256) with a key
187
196
  derived from SECRET_KEY. The database column is always ``text`` regardless
188
197
  of max_length, since ciphertext length is unpredictable.
189
198
 
190
199
  max_length is enforced on the plaintext value (validation), not on the
191
- ciphertext stored in the database.
200
+ ciphertext stored in the database. Only ``default=""`` (with
201
+ ``required=False``) is accepted — empty strings are stored as plaintext
202
+ ``''``, so the empty value is the one default expressible as a column
203
+ DEFAULT; anything else would need ciphertext, which is non-deterministic.
192
204
  """
193
205
 
194
- db_type_sql = "text"
206
+ only_empty_default = True
195
207
 
196
208
  def __init__(
197
209
  self,
@@ -199,40 +211,20 @@ class EncryptedTextField[T: (str, str | None) = str](
199
211
  max_length: int | None = None,
200
212
  required: bool = True,
201
213
  allow_null: bool = False,
214
+ default: Any = NOT_PROVIDED,
202
215
  validators: Sequence[Callable[..., Any]] = (),
203
216
  ):
204
- # `default` is intentionally not accepted: Fernet encryption is
205
- # non-deterministic, so a literal column DEFAULT cannot be expressed.
206
- self.max_length = max_length
217
+ # Deliberately narrower than TextField: no `choices` — exact lookups
218
+ # on ciphertext are non-deterministic, so choice-based filtering would
219
+ # silently match nothing.
207
220
  super().__init__(
221
+ max_length=max_length,
208
222
  required=required,
209
223
  allow_null=allow_null,
224
+ default=default,
210
225
  validators=validators,
211
226
  )
212
227
 
213
- def to_python(self, value: Any) -> str | None:
214
- if isinstance(value, str) or value is None:
215
- return value
216
- return str(value)
217
-
218
- def validate(self, value: Any, model_instance: Any) -> None:
219
- super().validate(value, model_instance)
220
- if (
221
- self.max_length is not None
222
- and value is not None
223
- and len(value) > self.max_length
224
- ):
225
- raise exceptions.ValidationError(
226
- f"Ensure this value has at most {self.max_length} characters (it has {len(value)}).",
227
- code="max_length",
228
- )
229
-
230
- def get_prep_value(self, value: Any) -> Any:
231
- value = super().get_prep_value(value)
232
- if value is None:
233
- return value
234
- return self.to_python(value)
235
-
236
228
  def get_db_prep_value(
237
229
  self, value: Any, connection: DatabaseConnection, prepared: bool = False
238
230
  ) -> Any:
@@ -248,19 +240,8 @@ class EncryptedTextField[T: (str, str | None) = str](
248
240
  return value
249
241
  return _decrypt(value)
250
242
 
251
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
252
- name, path, args, kwargs = super().deconstruct()
253
- if self.max_length is not None:
254
- kwargs["max_length"] = self.max_length
255
- return name, path, args, kwargs
256
243
 
257
- def preflight(self, **kwargs: Any) -> list[PreflightResult]:
258
- errors = super().preflight(**kwargs)
259
- errors.extend(self._check_encrypted_constraints())
260
- return errors
261
-
262
-
263
- class EncryptedJSONField(EncryptedFieldMixin, ColumnField):
244
+ class EncryptedJSONField(EncryptedFieldMixin, JSONField):
264
245
  """A JSONField that encrypts its serialized value before storing in the database.
265
246
 
266
247
  The JSON value is serialized to a string, encrypted, and stored as text.
@@ -268,7 +249,7 @@ class EncryptedJSONField(EncryptedFieldMixin, ColumnField):
268
249
  """
269
250
 
270
251
  db_type_sql = "text"
271
- empty_strings_allowed = False
252
+ accepts_default = False
272
253
 
273
254
  def __init__(
274
255
  self,
@@ -279,47 +260,23 @@ class EncryptedJSONField(EncryptedFieldMixin, ColumnField):
279
260
  allow_null: bool = False,
280
261
  validators: Sequence[Callable[..., Any]] = (),
281
262
  ):
282
- # `default` is intentionally not accepted: Fernet encryption is
283
- # non-deterministic, so a literal column DEFAULT cannot be expressed.
284
- if encoder and not callable(encoder):
285
- raise ValueError("The encoder parameter must be a callable object.")
286
- if decoder and not callable(decoder):
287
- raise ValueError("The decoder parameter must be a callable object.")
288
- self.encoder = encoder
289
- self.decoder = decoder
263
+ # Deliberately narrower than JSONField: no `default` — there is no
264
+ # empty plaintext value (even {} serializes to text that would need
265
+ # ciphertext, which is non-deterministic), so no literal column
266
+ # DEFAULT can be expressed.
290
267
  super().__init__(
268
+ encoder=encoder,
269
+ decoder=decoder,
291
270
  required=required,
292
271
  allow_null=allow_null,
293
272
  validators=validators,
294
273
  )
295
274
 
296
- def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
297
- name, path, args, kwargs = super().deconstruct()
298
- if self.encoder is not None:
299
- kwargs["encoder"] = self.encoder
300
- if self.decoder is not None:
301
- kwargs["decoder"] = self.decoder
302
- return name, path, args, kwargs
303
-
304
- def validate(self, value: Any, model_instance: Any) -> None:
305
- super().validate(value, model_instance)
306
- try:
307
- json.dumps(value, cls=self.encoder)
308
- except TypeError:
309
- raise exceptions.ValidationError(
310
- "Value must be valid JSON.",
311
- code="invalid",
312
- params={"value": value},
313
- )
314
-
315
- def get_db_prep_value(
316
- self, value: Any, connection: DatabaseConnection, prepared: bool = False
317
- ) -> Any:
318
- value = super().get_db_prep_value(value, connection, prepared)
275
+ def adapt_json_db_value(self, value: Any) -> Any:
276
+ # jsonb adaptation would emit jsonb — this column stores ciphertext.
319
277
  if value is None:
320
278
  return value
321
- json_str = json.dumps(value, cls=self.encoder)
322
- return _encrypt(json_str)
279
+ return _encrypt(json.dumps(value, cls=self.encoder))
323
280
 
324
281
  def from_db_value(
325
282
  self, value: Any, expression: Any, connection: DatabaseConnection
@@ -334,8 +291,3 @@ class EncryptedJSONField(EncryptedFieldMixin, ColumnField):
334
291
  "Encrypted field contains data that is not valid JSON. "
335
292
  "The stored value may be corrupt."
336
293
  )
337
-
338
- def preflight(self, **kwargs: Any) -> list[PreflightResult]:
339
- errors = super().preflight(**kwargs)
340
- errors.extend(self._check_encrypted_constraints())
341
- return errors
@@ -83,6 +83,15 @@ class JSONField(DefaultableField):
83
83
  value = value.value
84
84
  elif hasattr(value, "as_sql"):
85
85
  return value
86
+ return self.adapt_json_db_value(value)
87
+
88
+ def adapt_json_db_value(self, value: Any) -> Any:
89
+ """Adapt a plain JSON value for this field's column — the one step
90
+ subclasses with a different column type (EncryptedJSONField) replace.
91
+
92
+ May receive None (get_db_prep_save short-circuits it, but other
93
+ get_db_prep_value callers don't): here it adapts to jsonb null;
94
+ EncryptedJSONField maps it to SQL NULL."""
86
95
  return adapt_json_value(value, self.encoder)
87
96
 
88
97
  def get_db_prep_save(self, value: Any, connection: DatabaseConnection) -> Any:
@@ -90,14 +99,12 @@ class JSONField(DefaultableField):
90
99
  return value
91
100
  return self.get_db_prep_value(value, connection)
92
101
 
93
- def get_transform(
94
- self, lookup_name: str
95
- ) -> type[Transform] | Callable[..., Any] | None:
102
+ def get_transform(self, name: str) -> Callable[..., Transform] | None:
96
103
  # Always returns a transform (never None in practice)
97
- transform = super().get_transform(lookup_name)
104
+ transform = super().get_transform(name)
98
105
  if transform:
99
106
  return transform
100
- return KeyTransformFactory(lookup_name)
107
+ return KeyTransformFactory(name)
101
108
 
102
109
  def validate(self, value: Any, model_instance: Any) -> None:
103
110
  super().validate(value, model_instance)
@@ -390,7 +390,8 @@ class ForeignKeyField(ColumnField, RelatedField):
390
390
  return (self.target_field.column, self.column)
391
391
  return (self.column, self.target_field.column)
392
392
 
393
- def get_path_info(self, filtered_relation: Any = None) -> list[PathInfo]:
393
+ @cached_property
394
+ def path_infos(self) -> list[PathInfo]:
394
395
  """Get path from this field to the related model."""
395
396
  meta = self.remote_field.model._model_meta
396
397
  from_meta = self.model._model_meta
@@ -402,15 +403,11 @@ class ForeignKeyField(ColumnField, RelatedField):
402
403
  join_field=self,
403
404
  m2m=False,
404
405
  direct=True,
405
- filtered_relation=filtered_relation,
406
406
  )
407
407
  ]
408
408
 
409
409
  @cached_property
410
- def path_infos(self) -> list[PathInfo]:
411
- return self.get_path_info()
412
-
413
- def get_reverse_path_info(self, filtered_relation: Any = None) -> list[PathInfo]:
410
+ def reverse_path_infos(self) -> list[PathInfo]:
414
411
  """Get path from the related model to this field's model."""
415
412
  meta = self.model._model_meta
416
413
  from_meta = self.remote_field.model._model_meta
@@ -423,14 +420,9 @@ class ForeignKeyField(ColumnField, RelatedField):
423
420
  # The reverse of a foreign key always fans out to many rows.
424
421
  m2m=True,
425
422
  direct=False,
426
- filtered_relation=filtered_relation,
427
423
  )
428
424
  ]
429
425
 
430
- @cached_property
431
- def reverse_path_infos(self) -> list[PathInfo]:
432
- return self.get_reverse_path_info()
433
-
434
426
  def contribute_to_class(self, cls: type[Model], name: str) -> None:
435
427
  super().contribute_to_class(cls, name)
436
428
  setattr(cls, name, ForwardForeignKeyDescriptor(self))
@@ -870,9 +862,7 @@ class ManyToManyField(RelatedField):
870
862
 
871
863
  return name, path, args, kwargs
872
864
 
873
- def _get_path_info(
874
- self, direct: bool = False, filtered_relation: Any = None
875
- ) -> list[PathInfo]:
865
+ def _get_path_info(self, direct: bool = False) -> list[PathInfo]:
876
866
  """Called by both direct and indirect m2m traversal."""
877
867
  int_model = self.remote_field.through
878
868
  # M2M through model fields are always ForeignKey
@@ -886,32 +876,20 @@ class ManyToManyField(RelatedField):
886
876
  )
887
877
  if direct:
888
878
  join1infos = linkfield1.reverse_path_infos
889
- if filtered_relation:
890
- join2infos = linkfield2.get_path_info(filtered_relation)
891
- else:
892
- join2infos = linkfield2.path_infos
879
+ join2infos = linkfield2.path_infos
893
880
  else:
894
881
  join1infos = linkfield2.reverse_path_infos
895
- if filtered_relation:
896
- join2infos = linkfield1.get_path_info(filtered_relation)
897
- else:
898
- join2infos = linkfield1.path_infos
882
+ join2infos = linkfield1.path_infos
899
883
 
900
884
  return [*join1infos, *join2infos]
901
885
 
902
- def get_path_info(self, filtered_relation: Any = None) -> list[PathInfo]:
903
- return self._get_path_info(direct=True, filtered_relation=filtered_relation)
904
-
905
886
  @cached_property
906
887
  def path_infos(self) -> list[PathInfo]:
907
- return self.get_path_info()
908
-
909
- def get_reverse_path_info(self, filtered_relation: Any = None) -> list[PathInfo]:
910
- return self._get_path_info(direct=False, filtered_relation=filtered_relation)
888
+ return self._get_path_info(direct=True)
911
889
 
912
890
  @cached_property
913
891
  def reverse_path_infos(self) -> list[PathInfo]:
914
- return self.get_reverse_path_info()
892
+ return self._get_path_info(direct=False)
915
893
 
916
894
  def _get_m2m_db_table(self) -> str:
917
895
  """