plain.models 0.34.2__tar.gz → 0.34.4__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 (131) hide show
  1. {plain_models-0.34.2 → plain_models-0.34.4}/PKG-INFO +1 -1
  2. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/CHANGELOG.md +21 -0
  3. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/base/creation.py +3 -20
  4. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/base/features.py +0 -3
  5. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/base/schema.py +14 -29
  6. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/sqlite3/creation.py +1 -3
  7. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/base.py +1 -4
  8. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/constraints.py +1 -5
  9. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/deletion.py +8 -8
  10. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/fields/__init__.py +2 -10
  11. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/fields/related.py +1 -7
  12. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/fields/related_descriptors.py +1 -4
  13. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/forms.py +1 -7
  14. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/sql/query.py +2 -21
  15. {plain_models-0.34.2 → plain_models-0.34.4}/pyproject.toml +1 -1
  16. plain_models-0.34.4/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +101 -0
  17. plain_models-0.34.4/tests/app/examples/models.py +59 -0
  18. plain_models-0.34.4/tests/test_delete_behaviors.py +76 -0
  19. plain_models-0.34.2/tests/app/examples/models.py +0 -16
  20. {plain_models-0.34.2 → plain_models-0.34.4}/.gitignore +0 -0
  21. {plain_models-0.34.2 → plain_models-0.34.4}/LICENSE +0 -0
  22. {plain_models-0.34.2 → plain_models-0.34.4}/README.md +0 -0
  23. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/README.md +0 -0
  24. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/__init__.py +0 -0
  25. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/aggregates.py +0 -0
  26. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/__init__.py +0 -0
  27. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/base/__init__.py +0 -0
  28. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/base/base.py +0 -0
  29. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/base/client.py +0 -0
  30. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/base/introspection.py +0 -0
  31. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/base/operations.py +0 -0
  32. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/base/validation.py +0 -0
  33. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/ddl_references.py +0 -0
  34. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/mysql/__init__.py +0 -0
  35. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/mysql/base.py +0 -0
  36. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/mysql/client.py +0 -0
  37. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/mysql/compiler.py +0 -0
  38. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/mysql/creation.py +0 -0
  39. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/mysql/features.py +0 -0
  40. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/mysql/introspection.py +0 -0
  41. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/mysql/operations.py +0 -0
  42. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/mysql/schema.py +0 -0
  43. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/mysql/validation.py +0 -0
  44. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/postgresql/__init__.py +0 -0
  45. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/postgresql/base.py +0 -0
  46. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/postgresql/client.py +0 -0
  47. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/postgresql/creation.py +0 -0
  48. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/postgresql/features.py +0 -0
  49. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/postgresql/introspection.py +0 -0
  50. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/postgresql/operations.py +0 -0
  51. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/postgresql/schema.py +0 -0
  52. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/sqlite3/__init__.py +0 -0
  53. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/sqlite3/_functions.py +0 -0
  54. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/sqlite3/base.py +0 -0
  55. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/sqlite3/client.py +0 -0
  56. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/sqlite3/features.py +0 -0
  57. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/sqlite3/introspection.py +0 -0
  58. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/sqlite3/operations.py +0 -0
  59. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/sqlite3/schema.py +0 -0
  60. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backends/utils.py +0 -0
  61. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backups/__init__.py +0 -0
  62. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backups/cli.py +0 -0
  63. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backups/clients.py +0 -0
  64. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/backups/core.py +0 -0
  65. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/cli.py +0 -0
  66. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/config.py +0 -0
  67. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/connections.py +0 -0
  68. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/constants.py +0 -0
  69. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/database_url.py +0 -0
  70. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/db.py +0 -0
  71. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/default_settings.py +0 -0
  72. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/entrypoints.py +0 -0
  73. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/enums.py +0 -0
  74. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/exceptions.py +0 -0
  75. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/expressions.py +0 -0
  76. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/fields/json.py +0 -0
  77. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/fields/mixins.py +0 -0
  78. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/fields/related_lookups.py +0 -0
  79. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/fields/reverse_related.py +0 -0
  80. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/functions/__init__.py +0 -0
  81. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/functions/comparison.py +0 -0
  82. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/functions/datetime.py +0 -0
  83. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/functions/math.py +0 -0
  84. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/functions/mixins.py +0 -0
  85. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/functions/text.py +0 -0
  86. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/functions/window.py +0 -0
  87. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/indexes.py +0 -0
  88. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/lookups.py +0 -0
  89. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/manager.py +0 -0
  90. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/__init__.py +0 -0
  91. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/autodetector.py +0 -0
  92. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/exceptions.py +0 -0
  93. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/executor.py +0 -0
  94. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/graph.py +0 -0
  95. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/loader.py +0 -0
  96. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/migration.py +0 -0
  97. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/operations/__init__.py +0 -0
  98. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/operations/base.py +0 -0
  99. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/operations/fields.py +0 -0
  100. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/operations/models.py +0 -0
  101. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/operations/special.py +0 -0
  102. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/optimizer.py +0 -0
  103. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/questioner.py +0 -0
  104. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/recorder.py +0 -0
  105. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/serializer.py +0 -0
  106. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/state.py +0 -0
  107. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/utils.py +0 -0
  108. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/migrations/writer.py +0 -0
  109. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/options.py +0 -0
  110. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/preflight.py +0 -0
  111. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/query.py +0 -0
  112. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/query_utils.py +0 -0
  113. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/registry.py +0 -0
  114. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/sql/__init__.py +0 -0
  115. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/sql/compiler.py +0 -0
  116. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/sql/constants.py +0 -0
  117. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/sql/datastructures.py +0 -0
  118. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/sql/subqueries.py +0 -0
  119. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/sql/where.py +0 -0
  120. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/test/__init__.py +0 -0
  121. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/test/pytest.py +0 -0
  122. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/test/utils.py +0 -0
  123. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/transaction.py +0 -0
  124. {plain_models-0.34.2 → plain_models-0.34.4}/plain/models/utils.py +0 -0
  125. {plain_models-0.34.2 → plain_models-0.34.4}/tests/app/examples/migrations/0001_initial.py +0 -0
  126. {plain_models-0.34.2 → plain_models-0.34.4}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  127. {plain_models-0.34.2 → plain_models-0.34.4}/tests/app/examples/migrations/__init__.py +0 -0
  128. {plain_models-0.34.2 → plain_models-0.34.4}/tests/app/settings.py +0 -0
  129. {plain_models-0.34.2 → plain_models-0.34.4}/tests/app/urls.py +0 -0
  130. {plain_models-0.34.2 → plain_models-0.34.4}/tests/test_database_url.py +0 -0
  131. {plain_models-0.34.2 → plain_models-0.34.4}/tests/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.models
3
- Version: 0.34.2
3
+ Version: 0.34.4
4
4
  Summary: Database models for Plain.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -1,5 +1,26 @@
1
1
  # plain-models changelog
2
2
 
3
+ ## [0.34.4](https://github.com/dropseed/plain/releases/plain-models@0.34.4) (2025-07-02)
4
+
5
+ ### What's changed
6
+
7
+ - The built-in `on_delete` behaviors (`CASCADE`, `PROTECT`, `RESTRICT`, `SET_NULL`, `SET_DEFAULT`, and the callables returned by `SET(...)`) no longer receive the legacy `using` argument. Their signatures are now `(collector, field, sub_objs)` ([20325a1](https://github.com/dropseed/plain/commit/20325a1)).
8
+ - Removed the unused `interprets_empty_strings_as_nulls` backend feature flag and the related fallback logic ([285378c](https://github.com/dropseed/plain/commit/285378c)).
9
+
10
+ ### Upgrade instructions
11
+
12
+ - No changes required
13
+
14
+ ## [0.34.3](https://github.com/dropseed/plain/releases/plain-models@0.34.3) (2025-06-29)
15
+
16
+ ### What's changed
17
+
18
+ - Simplified log output when creating or destroying test databases during test setup. The messages now display the test database name directly and no longer reference the deprecated "alias" terminology ([a543706](https://github.com/dropseed/plain/commit/a543706)).
19
+
20
+ ### Upgrade instructions
21
+
22
+ - No changes required
23
+
3
24
  ## [0.34.2](https://github.com/dropseed/plain/releases/plain-models@0.34.2) (2025-06-27)
4
25
 
5
26
  ### What's changed
@@ -36,9 +36,7 @@ class BaseDatabaseCreation:
36
36
  test_database_name = self._get_test_db_name(prefix)
37
37
 
38
38
  if verbosity >= 1:
39
- self.log(
40
- f"Creating test database for alias {self._get_database_display_str(verbosity, test_database_name)}..."
41
- )
39
+ self.log(f"Creating test database '{test_database_name}'...")
42
40
 
43
41
  self._create_test_db(
44
42
  test_database_name=test_database_name, verbosity=verbosity, autoclobber=True
@@ -127,15 +125,6 @@ class BaseDatabaseCreation:
127
125
  # # because constraint checks were disabled.
128
126
  # self.connection.check_constraints(table_names=table_names)
129
127
 
130
- def _get_database_display_str(self, verbosity, database_name):
131
- """
132
- Return display string for a database for use in various actions.
133
- """
134
- return "'{}'{}".format(
135
- self.connection.alias,
136
- (f" ('{database_name}')") if verbosity >= 2 else "",
137
- )
138
-
139
128
  def _get_test_db_name(self, prefix=""):
140
129
  """
141
130
  Internal implementation - return the name of the test DB that will be
@@ -182,11 +171,7 @@ class BaseDatabaseCreation:
182
171
  try:
183
172
  if verbosity >= 1:
184
173
  self.log(
185
- "Destroying old test database for alias {}...".format(
186
- self._get_database_display_str(
187
- verbosity, test_database_name
188
- ),
189
- )
174
+ f"Destroying old test database '{test_database_name}'..."
190
175
  )
191
176
  cursor.execute(
192
177
  "DROP DATABASE {dbname}".format(**test_db_params)
@@ -211,9 +196,7 @@ class BaseDatabaseCreation:
211
196
  test_database_name = self.connection.settings_dict["NAME"]
212
197
 
213
198
  if verbosity >= 1:
214
- self.log(
215
- f"Destroying test database for alias {self._get_database_display_str(verbosity, test_database_name)}..."
216
- )
199
+ self.log(f"Destroying test database '{test_database_name}'...")
217
200
  self._destroy_test_db(test_database_name, verbosity)
218
201
 
219
202
  # Restore the original database name
@@ -9,9 +9,6 @@ class BaseDatabaseFeatures:
9
9
  empty_fetchmany_value = []
10
10
  update_can_self_select = True
11
11
 
12
- # Does the backend distinguish between '' and None?
13
- interprets_empty_strings_as_nulls = False
14
-
15
12
  # Does the backend support initially deferrable unique constraints?
16
13
  supports_deferrable_unique_constraints = False
17
14
 
@@ -297,14 +297,6 @@ class BaseDatabaseSchemaEditor:
297
297
  else:
298
298
  yield column_default
299
299
  params.append(default_value)
300
- # Oracle treats the empty string ('') as null, so coerce the null
301
- # option whenever '' is a possible value.
302
- if (
303
- field.empty_strings_allowed
304
- and not field.primary_key
305
- and self.connection.features.interprets_empty_strings_as_nulls
306
- ):
307
- null = True
308
300
 
309
301
  if not null:
310
302
  yield "NOT NULL"
@@ -1042,27 +1034,20 @@ class BaseDatabaseSchemaEditor:
1042
1034
  Return a (sql, params) fragment to set a column to null or non-null
1043
1035
  as required by new_field, or None if no changes are required.
1044
1036
  """
1045
- if (
1046
- self.connection.features.interprets_empty_strings_as_nulls
1047
- and new_field.empty_strings_allowed
1048
- ):
1049
- # The field is nullable in the database anyway, leave it alone.
1050
- return
1051
- else:
1052
- new_db_params = new_field.db_parameters(connection=self.connection)
1053
- sql = (
1054
- self.sql_alter_column_null
1055
- if new_field.allow_null
1056
- else self.sql_alter_column_not_null
1057
- )
1058
- return (
1059
- sql
1060
- % {
1061
- "column": self.quote_name(new_field.column),
1062
- "type": new_db_params["type"],
1063
- },
1064
- [],
1065
- )
1037
+ new_db_params = new_field.db_parameters(connection=self.connection)
1038
+ sql = (
1039
+ self.sql_alter_column_null
1040
+ if new_field.allow_null
1041
+ else self.sql_alter_column_not_null
1042
+ )
1043
+ return (
1044
+ sql
1045
+ % {
1046
+ "column": self.quote_name(new_field.column),
1047
+ "type": new_db_params["type"],
1048
+ },
1049
+ [],
1050
+ )
1066
1051
 
1067
1052
  def _alter_column_default_sql(self, model, old_field, new_field, drop=False):
1068
1053
  """
@@ -32,9 +32,7 @@ class DatabaseCreation(BaseDatabaseCreation):
32
32
  if not self.is_in_memory_db(test_database_name):
33
33
  # Erase the old test database file.
34
34
  if verbosity >= 1:
35
- self.log(
36
- f"Destroying old test database for alias {self._get_database_display_str(verbosity, test_database_name)}..."
37
- )
35
+ self.log(f"Destroying old test database '{test_database_name}'...")
38
36
  if os.access(test_database_name, os.F_OK):
39
37
  if not autoclobber:
40
38
  confirm = input(
@@ -833,10 +833,7 @@ class Model(metaclass=ModelBase):
833
833
  f = self._meta.get_field(field_name)
834
834
  lookup_value = getattr(self, f.attname)
835
835
  # TODO: Handle multiple backends with different feature flags.
836
- if lookup_value is None or (
837
- lookup_value == ""
838
- and db_connection.features.interprets_empty_strings_as_nulls
839
- ):
836
+ if lookup_value is None:
840
837
  # no value, skip the lookup
841
838
  continue
842
839
  if f.primary_key and not self._state.adding:
@@ -2,7 +2,6 @@ from enum import Enum
2
2
  from types import NoneType
3
3
 
4
4
  from plain.exceptions import FieldError, ValidationError
5
- from plain.models.db import db_connection
6
5
  from plain.models.expressions import Exists, ExpressionList, F, OrderBy
7
6
  from plain.models.indexes import IndexExpression
8
7
  from plain.models.lookups import Exact
@@ -356,10 +355,7 @@ class UniqueConstraint(BaseConstraint):
356
355
  return
357
356
  field = model._meta.get_field(field_name)
358
357
  lookup_value = getattr(instance, field.attname)
359
- if lookup_value is None or (
360
- lookup_value == ""
361
- and db_connection.features.interprets_empty_strings_as_nulls
362
- ):
358
+ if lookup_value is None:
363
359
  # A composite constraint containing NULL value cannot cause
364
360
  # a violation since NULL != NULL in SQL.
365
361
  return
@@ -24,7 +24,7 @@ class RestrictedError(IntegrityError):
24
24
  super().__init__(msg, restricted_objects)
25
25
 
26
26
 
27
- def CASCADE(collector, field, sub_objs, using):
27
+ def CASCADE(collector, field, sub_objs):
28
28
  collector.collect(
29
29
  sub_objs,
30
30
  source=field.remote_field.model,
@@ -35,7 +35,7 @@ def CASCADE(collector, field, sub_objs, using):
35
35
  collector.add_field_update(field, None, sub_objs)
36
36
 
37
37
 
38
- def PROTECT(collector, field, sub_objs, using):
38
+ def PROTECT(collector, field, sub_objs):
39
39
  raise ProtectedError(
40
40
  f"Cannot delete some instances of model '{field.remote_field.model.__name__}' because they are "
41
41
  f"referenced through a protected foreign key: '{sub_objs[0].__class__.__name__}.{field.name}'",
@@ -43,7 +43,7 @@ def PROTECT(collector, field, sub_objs, using):
43
43
  )
44
44
 
45
45
 
46
- def RESTRICT(collector, field, sub_objs, using):
46
+ def RESTRICT(collector, field, sub_objs):
47
47
  collector.add_restricted_objects(field, sub_objs)
48
48
  collector.add_dependency(field.remote_field.model, field.model)
49
49
 
@@ -51,12 +51,12 @@ def RESTRICT(collector, field, sub_objs, using):
51
51
  def SET(value):
52
52
  if callable(value):
53
53
 
54
- def set_on_delete(collector, field, sub_objs, using):
54
+ def set_on_delete(collector, field, sub_objs):
55
55
  collector.add_field_update(field, value(), sub_objs)
56
56
 
57
57
  else:
58
58
 
59
- def set_on_delete(collector, field, sub_objs, using):
59
+ def set_on_delete(collector, field, sub_objs):
60
60
  collector.add_field_update(field, value, sub_objs)
61
61
 
62
62
  set_on_delete.deconstruct = lambda: ("plain.models.SET", (value,), {})
@@ -64,21 +64,21 @@ def SET(value):
64
64
  return set_on_delete
65
65
 
66
66
 
67
- def SET_NULL(collector, field, sub_objs, using):
67
+ def SET_NULL(collector, field, sub_objs):
68
68
  collector.add_field_update(field, None, sub_objs)
69
69
 
70
70
 
71
71
  SET_NULL.lazy_sub_objs = True
72
72
 
73
73
 
74
- def SET_DEFAULT(collector, field, sub_objs, using):
74
+ def SET_DEFAULT(collector, field, sub_objs):
75
75
  collector.add_field_update(field, field.get_default(), sub_objs)
76
76
 
77
77
 
78
78
  SET_DEFAULT.lazy_sub_objs = True
79
79
 
80
80
 
81
- def DO_NOTHING(collector, field, sub_objs, using):
81
+ def DO_NOTHING(collector, field, sub_objs):
82
82
  pass
83
83
 
84
84
 
@@ -365,11 +365,7 @@ class Field(RegisterLookupMixin):
365
365
  return errors
366
366
 
367
367
  def _check_null_allowed_for_primary_keys(self):
368
- if (
369
- self.primary_key
370
- and self.allow_null
371
- and not db_connection.features.interprets_empty_strings_as_nulls
372
- ):
368
+ if self.primary_key and self.allow_null:
373
369
  # We cannot reliably check this for backends like Oracle which
374
370
  # consider NULL and '' to be equal (and thus set up
375
371
  # character-based fields a little differently).
@@ -885,11 +881,7 @@ class Field(RegisterLookupMixin):
885
881
  return self.default
886
882
  return lambda: self.default
887
883
 
888
- if (
889
- not self.empty_strings_allowed
890
- or self.allow_null
891
- and not db_connection.features.interprets_empty_strings_as_nulls
892
- ):
884
+ if not self.empty_strings_allowed or self.allow_null:
893
885
  return return_None
894
886
  return str # return empty string
895
887
 
@@ -974,11 +974,7 @@ class ForeignKey(ForeignObject):
974
974
 
975
975
  def get_db_prep_save(self, value, connection):
976
976
  if value is None or (
977
- value == ""
978
- and (
979
- not self.target_field.empty_strings_allowed
980
- or connection.features.interprets_empty_strings_as_nulls
981
- )
977
+ value == "" and not self.target_field.empty_strings_allowed
982
978
  ):
983
979
  return None
984
980
  else:
@@ -1019,8 +1015,6 @@ class ForeignKey(ForeignObject):
1019
1015
 
1020
1016
  def get_db_converters(self, connection):
1021
1017
  converters = super().get_db_converters(connection)
1022
- if connection.features.interprets_empty_strings_as_nulls:
1023
- converters += [self.convert_empty_strings]
1024
1018
  return converters
1025
1019
 
1026
1020
  def get_col(self, alias, output_field=None):
@@ -369,15 +369,12 @@ def create_reverse_many_to_one_manager(superclass, rel):
369
369
  """
370
370
  Filter the queryset for the instance this manager is bound to.
371
371
  """
372
- empty_strings_as_null = (
373
- db_connection.features.interprets_empty_strings_as_nulls
374
- )
375
372
  queryset._add_hints(instance=self.instance)
376
373
  queryset._defer_next_filter = True
377
374
  queryset = queryset.filter(**self.core_filters)
378
375
  for field in self.field.foreign_related_fields:
379
376
  val = getattr(self.instance, field.attname)
380
- if val is None or (val == "" and empty_strings_as_null):
377
+ if val is None:
381
378
  return queryset.none()
382
379
  if self.field.many_to_one:
383
380
  # Guard against field-like objects such as GenericRelation
@@ -769,13 +769,7 @@ def modelfield_to_formfield(
769
769
  # Passing max_length to forms.CharField means that the value's length
770
770
  # will be validated twice. This is considered acceptable since we want
771
771
  # the value in the form field (to pass into widget for example).
772
- # TODO: Handle multiple backends with different feature flags.
773
- from plain.models.db import db_connection
774
-
775
- if (
776
- modelfield.allow_null
777
- and not db_connection.features.interprets_empty_strings_as_nulls
778
- ):
772
+ if modelfield.allow_null:
779
773
  defaults["empty_value"] = None
780
774
  return fields.CharField(
781
775
  max_length=modelfield.max_length,
@@ -1233,16 +1233,6 @@ class Query(BaseExpression):
1233
1233
  raise ValueError("Cannot use None as a query value")
1234
1234
  return lhs.get_lookup("isnull")(lhs, True)
1235
1235
 
1236
- # For Oracle '' is equivalent to null. The check must be done at this
1237
- # stage because join promotion can't be done in the compiler. A similar
1238
- # thing is done in is_nullable(), too.
1239
- if (
1240
- lookup_name == "exact"
1241
- and lookup.rhs == ""
1242
- and db_connection.features.interprets_empty_strings_as_nulls
1243
- ):
1244
- return lhs.get_lookup("isnull")(lhs, True)
1245
-
1246
1236
  return lookup
1247
1237
 
1248
1238
  def try_transform(self, lhs, name):
@@ -2466,20 +2456,11 @@ class Query(BaseExpression):
2466
2456
  return trimmed_prefix, contains_louter
2467
2457
 
2468
2458
  def is_nullable(self, field):
2469
- """
2470
- Check if the given field should be treated as nullable.
2471
-
2472
- Some backends treat '' as null and Plain treats such fields as
2473
- nullable for those backends. In such situations field.allow_null can be
2474
- False even if we should treat the field as nullable.
2475
- """
2459
+ """Check if the given field should be treated as nullable."""
2476
2460
  # QuerySet does not have knowledge of which connection is going to be
2477
2461
  # used. For the single-database setup we always reference the default
2478
2462
  # connection here.
2479
- return field.allow_null or (
2480
- field.empty_strings_allowed
2481
- and db_connection.features.interprets_empty_strings_as_nulls
2482
- )
2463
+ return field.allow_null
2483
2464
 
2484
2465
 
2485
2466
  def get_order_dir(field, default="ASC"):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.models"
3
- version = "0.34.2"
3
+ version = "0.34.4"
4
4
  description = "Database models for Plain."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  readme = "README.md"
@@ -0,0 +1,101 @@
1
+ # Generated by Plain 0.52.2 on 2025-07-02 18:16
2
+
3
+ import plain.models.deletion
4
+ from plain import models
5
+ from plain.models import migrations
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ dependencies = [
10
+ ("examples", "0002_test_field_removed"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name="DeleteParent",
16
+ fields=[
17
+ ("id", models.BigAutoField(auto_created=True, primary_key=True)),
18
+ ("name", models.CharField(max_length=100)),
19
+ ],
20
+ ),
21
+ migrations.CreateModel(
22
+ name="ChildSetNull",
23
+ fields=[
24
+ ("id", models.BigAutoField(auto_created=True, primary_key=True)),
25
+ (
26
+ "parent",
27
+ models.ForeignKey(
28
+ allow_null=True,
29
+ on_delete=plain.models.deletion.SET_NULL,
30
+ to="examples.deleteparent",
31
+ ),
32
+ ),
33
+ ],
34
+ ),
35
+ migrations.CreateModel(
36
+ name="ChildSetDefault",
37
+ fields=[
38
+ ("id", models.BigAutoField(auto_created=True, primary_key=True)),
39
+ (
40
+ "parent",
41
+ models.ForeignKey(
42
+ default=1,
43
+ on_delete=plain.models.deletion.SET_DEFAULT,
44
+ to="examples.deleteparent",
45
+ ),
46
+ ),
47
+ ],
48
+ ),
49
+ migrations.CreateModel(
50
+ name="ChildDoNothing",
51
+ fields=[
52
+ ("id", models.BigAutoField(auto_created=True, primary_key=True)),
53
+ (
54
+ "parent",
55
+ models.ForeignKey(
56
+ on_delete=plain.models.deletion.DO_NOTHING,
57
+ to="examples.deleteparent",
58
+ ),
59
+ ),
60
+ ],
61
+ ),
62
+ migrations.CreateModel(
63
+ name="ChildCascade",
64
+ fields=[
65
+ ("id", models.BigAutoField(auto_created=True, primary_key=True)),
66
+ (
67
+ "parent",
68
+ models.ForeignKey(
69
+ on_delete=plain.models.deletion.CASCADE,
70
+ to="examples.deleteparent",
71
+ ),
72
+ ),
73
+ ],
74
+ ),
75
+ migrations.CreateModel(
76
+ name="ChildRestrict",
77
+ fields=[
78
+ ("id", models.BigAutoField(auto_created=True, primary_key=True)),
79
+ (
80
+ "parent",
81
+ models.ForeignKey(
82
+ on_delete=plain.models.deletion.RESTRICT,
83
+ to="examples.deleteparent",
84
+ ),
85
+ ),
86
+ ],
87
+ ),
88
+ migrations.CreateModel(
89
+ name="ChildProtect",
90
+ fields=[
91
+ ("id", models.BigAutoField(auto_created=True, primary_key=True)),
92
+ (
93
+ "parent",
94
+ models.ForeignKey(
95
+ on_delete=plain.models.deletion.PROTECT,
96
+ to="examples.deleteparent",
97
+ ),
98
+ ),
99
+ ],
100
+ ),
101
+ ]
@@ -0,0 +1,59 @@
1
+ from plain import models
2
+
3
+
4
+ @models.register_model
5
+ class Car(models.Model):
6
+ make = models.CharField(max_length=100)
7
+ model = models.CharField(max_length=100)
8
+
9
+ class Meta:
10
+ constraints = [
11
+ models.UniqueConstraint(fields=["make", "model"], name="unique_make_model"),
12
+ ]
13
+
14
+
15
+ class UnregisteredModel(models.Model):
16
+ pass
17
+
18
+
19
+ @models.register_model
20
+ class DeleteParent(models.Model):
21
+ name = models.CharField(max_length=100)
22
+
23
+
24
+ @models.register_model
25
+ class ChildCascade(models.Model):
26
+ parent = models.ForeignKey(DeleteParent, on_delete=models.CASCADE)
27
+
28
+
29
+ @models.register_model
30
+ class ChildProtect(models.Model):
31
+ parent = models.ForeignKey(DeleteParent, on_delete=models.PROTECT)
32
+
33
+
34
+ @models.register_model
35
+ class ChildRestrict(models.Model):
36
+ parent = models.ForeignKey(DeleteParent, on_delete=models.RESTRICT)
37
+
38
+
39
+ @models.register_model
40
+ class ChildSetNull(models.Model):
41
+ parent = models.ForeignKey(
42
+ DeleteParent,
43
+ on_delete=models.SET_NULL,
44
+ allow_null=True,
45
+ )
46
+
47
+
48
+ @models.register_model
49
+ class ChildSetDefault(models.Model):
50
+ parent = models.ForeignKey(
51
+ DeleteParent,
52
+ on_delete=models.SET_DEFAULT,
53
+ default=1,
54
+ )
55
+
56
+
57
+ @models.register_model
58
+ class ChildDoNothing(models.Model):
59
+ parent = models.ForeignKey(DeleteParent, on_delete=models.DO_NOTHING)
@@ -0,0 +1,76 @@
1
+ import pytest
2
+ from app.examples.models import (
3
+ ChildCascade,
4
+ ChildDoNothing,
5
+ ChildProtect,
6
+ ChildRestrict,
7
+ ChildSetDefault,
8
+ ChildSetNull,
9
+ DeleteParent,
10
+ )
11
+
12
+ from plain.models import (
13
+ IntegrityError,
14
+ ProtectedError,
15
+ RestrictedError,
16
+ db_connection,
17
+ )
18
+
19
+
20
+ def _create_parents():
21
+ default_parent = DeleteParent.objects.create(name="default")
22
+ parent = DeleteParent.objects.create(name="parent")
23
+ return default_parent, parent
24
+
25
+
26
+ def test_cascade_delete(db):
27
+ _create_parents()
28
+ parent = DeleteParent.objects.get(name="parent")
29
+ ChildCascade.objects.create(parent=parent)
30
+ parent.delete()
31
+ assert ChildCascade.objects.count() == 0
32
+
33
+
34
+ def test_protect_delete(db):
35
+ _create_parents()
36
+ parent = DeleteParent.objects.get(name="parent")
37
+ ChildProtect.objects.create(parent=parent)
38
+ with pytest.raises(ProtectedError):
39
+ parent.delete()
40
+ assert DeleteParent.objects.filter(pk=parent.pk).exists()
41
+
42
+
43
+ def test_restrict_delete(db):
44
+ _create_parents()
45
+ parent = DeleteParent.objects.get(name="parent")
46
+ ChildRestrict.objects.create(parent=parent)
47
+ with pytest.raises(RestrictedError):
48
+ parent.delete()
49
+ assert DeleteParent.objects.filter(pk=parent.pk).exists()
50
+
51
+
52
+ def test_set_null_delete(db):
53
+ _create_parents()
54
+ parent = DeleteParent.objects.get(name="parent")
55
+ child = ChildSetNull.objects.create(parent=parent)
56
+ parent.delete()
57
+ child.refresh_from_db()
58
+ assert child.parent_id is None
59
+
60
+
61
+ def test_set_default_delete(db):
62
+ default_parent, parent = _create_parents()
63
+ child = ChildSetDefault.objects.create(parent=parent)
64
+ parent.delete()
65
+ child.refresh_from_db()
66
+ assert child.parent_id == default_parent.pk
67
+
68
+
69
+ def test_do_nothing_delete(db):
70
+ default_parent, parent = _create_parents()
71
+ child = ChildDoNothing.objects.create(parent=parent)
72
+ parent.delete()
73
+ with pytest.raises(IntegrityError):
74
+ db_connection.check_constraints()
75
+ child.parent = default_parent
76
+ child.save(clean_and_validate=False)
@@ -1,16 +0,0 @@
1
- from plain import models
2
-
3
-
4
- @models.register_model
5
- class Car(models.Model):
6
- make = models.CharField(max_length=100)
7
- model = models.CharField(max_length=100)
8
-
9
- class Meta:
10
- constraints = [
11
- models.UniqueConstraint(fields=["make", "model"], name="unique_make_model"),
12
- ]
13
-
14
-
15
- class UnregisteredModel(models.Model):
16
- pass
File without changes
File without changes
File without changes