plain.models 0.33.1__py3-none-any.whl → 0.34.0__py3-none-any.whl

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 (56) hide show
  1. plain/models/CHANGELOG.md +17 -0
  2. plain/models/README.md +8 -10
  3. plain/models/__init__.py +2 -6
  4. plain/models/backends/base/base.py +10 -18
  5. plain/models/backends/base/creation.py +3 -4
  6. plain/models/backends/base/introspection.py +2 -3
  7. plain/models/backends/base/schema.py +3 -9
  8. plain/models/backends/mysql/validation.py +1 -1
  9. plain/models/backends/postgresql/base.py +15 -23
  10. plain/models/backends/postgresql/schema.py +0 -2
  11. plain/models/backends/sqlite3/base.py +1 -1
  12. plain/models/backends/sqlite3/creation.py +2 -2
  13. plain/models/backends/sqlite3/features.py +1 -1
  14. plain/models/backends/sqlite3/schema.py +1 -1
  15. plain/models/backends/utils.py +2 -6
  16. plain/models/backups/core.py +15 -22
  17. plain/models/base.py +179 -225
  18. plain/models/cli.py +25 -62
  19. plain/models/connections.py +48 -165
  20. plain/models/constraints.py +10 -10
  21. plain/models/db.py +7 -15
  22. plain/models/default_settings.py +13 -20
  23. plain/models/deletion.py +14 -16
  24. plain/models/expressions.py +7 -10
  25. plain/models/fields/__init__.py +56 -76
  26. plain/models/fields/json.py +9 -12
  27. plain/models/fields/related.py +5 -17
  28. plain/models/fields/related_descriptors.py +43 -95
  29. plain/models/forms.py +2 -4
  30. plain/models/indexes.py +2 -3
  31. plain/models/lookups.py +0 -7
  32. plain/models/manager.py +1 -14
  33. plain/models/migrations/executor.py +0 -16
  34. plain/models/migrations/loader.py +1 -1
  35. plain/models/migrations/migration.py +1 -1
  36. plain/models/migrations/operations/base.py +4 -11
  37. plain/models/migrations/operations/fields.py +4 -4
  38. plain/models/migrations/operations/models.py +10 -10
  39. plain/models/migrations/operations/special.py +6 -14
  40. plain/models/migrations/recorder.py +1 -1
  41. plain/models/options.py +4 -7
  42. plain/models/preflight.py +25 -44
  43. plain/models/query.py +47 -102
  44. plain/models/query_utils.py +4 -4
  45. plain/models/sql/compiler.py +7 -11
  46. plain/models/sql/query.py +32 -42
  47. plain/models/sql/subqueries.py +6 -8
  48. plain/models/sql/where.py +1 -1
  49. plain/models/test/pytest.py +21 -32
  50. plain/models/test/utils.py +7 -143
  51. plain/models/transaction.py +66 -164
  52. {plain_models-0.33.1.dist-info → plain_models-0.34.0.dist-info}/METADATA +9 -11
  53. {plain_models-0.33.1.dist-info → plain_models-0.34.0.dist-info}/RECORD +56 -55
  54. {plain_models-0.33.1.dist-info → plain_models-0.34.0.dist-info}/WHEEL +0 -0
  55. {plain_models-0.33.1.dist-info → plain_models-0.34.0.dist-info}/entry_points.txt +0 -0
  56. {plain_models-0.33.1.dist-info → plain_models-0.34.0.dist-info}/licenses/LICENSE +0 -0
plain/models/sql/query.py CHANGED
@@ -20,7 +20,7 @@ from string import ascii_uppercase
20
20
  from plain.exceptions import FieldDoesNotExist, FieldError
21
21
  from plain.models.aggregates import Count
22
22
  from plain.models.constants import LOOKUP_SEP
23
- from plain.models.db import DEFAULT_DB_ALIAS, NotSupportedError, connections
23
+ from plain.models.db import NotSupportedError, db_connection
24
24
  from plain.models.expressions import (
25
25
  BaseExpression,
26
26
  Col,
@@ -83,10 +83,9 @@ JoinInfo = namedtuple(
83
83
  class RawQuery:
84
84
  """A single raw SQL query."""
85
85
 
86
- def __init__(self, sql, using, params=()):
86
+ def __init__(self, sql, params=()):
87
87
  self.params = params
88
88
  self.sql = sql
89
- self.using = using
90
89
  self.cursor = None
91
90
 
92
91
  # Mirror some properties of a normal query so that
@@ -95,23 +94,23 @@ class RawQuery:
95
94
  self.extra_select = {}
96
95
  self.annotation_select = {}
97
96
 
98
- def chain(self, using):
99
- return self.clone(using)
97
+ def chain(self):
98
+ return self.clone()
100
99
 
101
- def clone(self, using):
102
- return RawQuery(self.sql, using, params=self.params)
100
+ def clone(self):
101
+ return RawQuery(self.sql, params=self.params)
103
102
 
104
103
  def get_columns(self):
105
104
  if self.cursor is None:
106
105
  self._execute_query()
107
- converter = connections[self.using].introspection.identifier_converter
106
+ converter = db_connection.introspection.identifier_converter
108
107
  return [converter(column_meta[0]) for column_meta in self.cursor.description]
109
108
 
110
109
  def __iter__(self):
111
110
  # Always execute a new query for a new iterator.
112
111
  # This could be optimized with a cache at the expense of RAM.
113
112
  self._execute_query()
114
- if not connections[self.using].features.can_use_chunked_reads:
113
+ if not db_connection.features.can_use_chunked_reads:
115
114
  # If the database can't use chunked reads we need to make sure we
116
115
  # evaluate the entire query up front.
117
116
  result = list(self.cursor)
@@ -134,12 +133,10 @@ class RawQuery:
134
133
  return self.sql % self.params_type(self.params)
135
134
 
136
135
  def _execute_query(self):
137
- connection = connections[self.using]
138
-
139
136
  # Adapt parameters to the database, as much as possible considering
140
137
  # that the target type isn't known. See #17755.
141
138
  params_type = self.params_type
142
- adapter = connection.ops.adapt_unknown_value
139
+ adapter = db_connection.ops.adapt_unknown_value
143
140
  if params_type is tuple:
144
141
  params = tuple(adapter(val) for val in self.params)
145
142
  elif params_type is dict:
@@ -149,7 +146,7 @@ class RawQuery:
149
146
  else:
150
147
  raise RuntimeError(f"Unexpected params type: {params_type}")
151
148
 
152
- self.cursor = connection.cursor()
149
+ self.cursor = db_connection.cursor()
153
150
  self.cursor.execute(self.sql, params)
154
151
 
155
152
 
@@ -286,7 +283,7 @@ class Query(BaseExpression):
286
283
  Return the query as an SQL string and the parameters that will be
287
284
  substituted into the query.
288
285
  """
289
- return self.get_compiler(DEFAULT_DB_ALIAS).as_sql()
286
+ return self.get_compiler().as_sql()
290
287
 
291
288
  def __deepcopy__(self, memo):
292
289
  """Limit the amount of work when a Query is deepcopied."""
@@ -294,13 +291,9 @@ class Query(BaseExpression):
294
291
  memo[id(self)] = result
295
292
  return result
296
293
 
297
- def get_compiler(self, using=None, connection=None, elide_empty=True):
298
- if using is None and connection is None:
299
- raise ValueError("Need either using or connection")
300
- if using:
301
- connection = connections[using]
302
- return connection.ops.compiler(self.compiler)(
303
- self, connection, using, elide_empty
294
+ def get_compiler(self, *, elide_empty=True):
295
+ return db_connection.ops.compiler(self.compiler)(
296
+ self, db_connection, elide_empty
304
297
  )
305
298
 
306
299
  def get_meta(self):
@@ -382,7 +375,7 @@ class Query(BaseExpression):
382
375
  alias = None
383
376
  return target.get_col(alias, field)
384
377
 
385
- def get_aggregation(self, using, aggregate_exprs):
378
+ def get_aggregation(self, aggregate_exprs):
386
379
  """
387
380
  Return the dictionary with the values of the existing aggregations.
388
381
  """
@@ -519,7 +512,7 @@ class Query(BaseExpression):
519
512
  outer_query.clear_limits()
520
513
  outer_query.select_for_update = False
521
514
  outer_query.select_related = False
522
- compiler = outer_query.get_compiler(using, elide_empty=elide_empty)
515
+ compiler = outer_query.get_compiler(elide_empty=elide_empty)
523
516
  result = compiler.execute_sql(SINGLE)
524
517
  if result is None:
525
518
  result = empty_set_result
@@ -529,12 +522,12 @@ class Query(BaseExpression):
529
522
 
530
523
  return dict(zip(outer_query.annotation_select, result))
531
524
 
532
- def get_count(self, using):
525
+ def get_count(self):
533
526
  """
534
527
  Perform a COUNT() query using the current filter constraints.
535
528
  """
536
529
  obj = self.clone()
537
- return obj.get_aggregation(using, {"__count": Count("*")})["__count"]
530
+ return obj.get_aggregation({"__count": Count("*")})["__count"]
538
531
 
539
532
  def has_filters(self):
540
533
  return self.where
@@ -561,12 +554,12 @@ class Query(BaseExpression):
561
554
  q.add_annotation(Value(1), "a")
562
555
  return q
563
556
 
564
- def has_results(self, using):
565
- q = self.exists(using)
566
- compiler = q.get_compiler(using=using)
557
+ def has_results(self):
558
+ q = self.exists()
559
+ compiler = q.get_compiler()
567
560
  return compiler.has_results()
568
561
 
569
- def explain(self, using, format=None, **options):
562
+ def explain(self, format=None, **options):
570
563
  q = self.clone()
571
564
  for option_name in options:
572
565
  if (
@@ -575,7 +568,7 @@ class Query(BaseExpression):
575
568
  ):
576
569
  raise ValueError(f"Invalid option name: {option_name!r}.")
577
570
  q.explain_info = ExplainInfo(format, options)
578
- compiler = q.get_compiler(using=using)
571
+ compiler = q.get_compiler()
579
572
  return "\n".join(compiler.explain_query())
580
573
 
581
574
  def combine(self, rhs, connector):
@@ -1110,12 +1103,12 @@ class Query(BaseExpression):
1110
1103
  # unnecessary ORDER BY clause.
1111
1104
  if (
1112
1105
  self.subquery
1113
- and not connection.features.ignores_unnecessary_order_by_in_subqueries
1106
+ and not db_connection.features.ignores_unnecessary_order_by_in_subqueries
1114
1107
  ):
1115
1108
  self.clear_ordering(force=False)
1116
1109
  for query in self.combined_queries:
1117
1110
  query.clear_ordering(force=False)
1118
- sql, params = self.get_compiler(connection=connection).as_sql()
1111
+ sql, params = self.get_compiler().as_sql()
1119
1112
  if self.subquery:
1120
1113
  sql = f"({sql})"
1121
1114
  return sql, params
@@ -1241,13 +1234,12 @@ class Query(BaseExpression):
1241
1234
  return lhs.get_lookup("isnull")(lhs, True)
1242
1235
 
1243
1236
  # For Oracle '' is equivalent to null. The check must be done at this
1244
- # stage because join promotion can't be done in the compiler. Using
1245
- # DEFAULT_DB_ALIAS isn't nice but it's the best that can be done here.
1246
- # A similar thing is done in is_nullable(), too.
1237
+ # stage because join promotion can't be done in the compiler. A similar
1238
+ # thing is done in is_nullable(), too.
1247
1239
  if (
1248
1240
  lookup_name == "exact"
1249
1241
  and lookup.rhs == ""
1250
- and connections[DEFAULT_DB_ALIAS].features.interprets_empty_strings_as_nulls
1242
+ and db_connection.features.interprets_empty_strings_as_nulls
1251
1243
  ):
1252
1244
  return lhs.get_lookup("isnull")(lhs, True)
1253
1245
 
@@ -2481,14 +2473,12 @@ class Query(BaseExpression):
2481
2473
  nullable for those backends. In such situations field.allow_null can be
2482
2474
  False even if we should treat the field as nullable.
2483
2475
  """
2484
- # We need to use DEFAULT_DB_ALIAS here, as QuerySet does not have
2485
- # (nor should it have) knowledge of which connection is going to be
2486
- # used. The proper fix would be to defer all decisions where
2487
- # is_nullable() is needed to the compiler stage, but that is not easy
2488
- # to do currently.
2476
+ # QuerySet does not have knowledge of which connection is going to be
2477
+ # used. For the single-database setup we always reference the default
2478
+ # connection here.
2489
2479
  return field.allow_null or (
2490
2480
  field.empty_strings_allowed
2491
- and connections[DEFAULT_DB_ALIAS].features.interprets_empty_strings_as_nulls
2481
+ and db_connection.features.interprets_empty_strings_as_nulls
2492
2482
  )
2493
2483
 
2494
2484
 
@@ -14,16 +14,16 @@ class DeleteQuery(Query):
14
14
 
15
15
  compiler = "SQLDeleteCompiler"
16
16
 
17
- def do_query(self, table, where, using):
17
+ def do_query(self, table, where):
18
18
  self.alias_map = {table: self.alias_map[table]}
19
19
  self.where = where
20
- cursor = self.get_compiler(using).execute_sql(CURSOR)
20
+ cursor = self.get_compiler().execute_sql(CURSOR)
21
21
  if cursor:
22
22
  with cursor:
23
23
  return cursor.rowcount
24
24
  return 0
25
25
 
26
- def delete_batch(self, pk_list, using):
26
+ def delete_batch(self, pk_list):
27
27
  """
28
28
  Set up and execute delete queries for all the objects in pk_list.
29
29
 
@@ -39,9 +39,7 @@ class DeleteQuery(Query):
39
39
  f"{field.attname}__in",
40
40
  pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE],
41
41
  )
42
- num_deleted += self.do_query(
43
- self.get_meta().db_table, self.where, using=using
44
- )
42
+ num_deleted += self.do_query(self.get_meta().db_table, self.where)
45
43
  return num_deleted
46
44
 
47
45
 
@@ -68,14 +66,14 @@ class UpdateQuery(Query):
68
66
  obj.related_updates = self.related_updates.copy()
69
67
  return obj
70
68
 
71
- def update_batch(self, pk_list, values, using):
69
+ def update_batch(self, pk_list, values):
72
70
  self.add_update_values(values)
73
71
  for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
74
72
  self.clear_where()
75
73
  self.add_filter(
76
74
  "pk__in", pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]
77
75
  )
78
- self.get_compiler(using).execute_sql(NO_RESULTS)
76
+ self.get_compiler().execute_sql(NO_RESULTS)
79
77
 
80
78
  def add_update_values(self, values):
81
79
  """
plain/models/sql/where.py CHANGED
@@ -351,5 +351,5 @@ class SubqueryConstraint:
351
351
  def as_sql(self, compiler, connection):
352
352
  query = self.query_object
353
353
  query.set_values(self.targets)
354
- query_compiler = query.get_compiler(connection=connection)
354
+ query_compiler = query.get_compiler()
355
355
  return query_compiler.as_subquery_condition(self.alias, self.columns, compiler)
@@ -6,10 +6,10 @@ from plain.signals import request_finished, request_started
6
6
 
7
7
  from .. import transaction
8
8
  from ..backends.base.base import BaseDatabaseWrapper
9
- from ..db import close_old_connections, connections
9
+ from ..db import close_old_connections, db_connection
10
10
  from .utils import (
11
- setup_databases,
12
- teardown_databases,
11
+ setup_database,
12
+ teardown_database,
13
13
  )
14
14
 
15
15
 
@@ -40,7 +40,7 @@ def setup_db(request):
40
40
  verbosity = request.config.option.verbose
41
41
 
42
42
  # Set up the test db across the entire session
43
- _old_db_config = setup_databases(verbosity=verbosity)
43
+ _old_db_name = setup_database(verbosity=verbosity)
44
44
 
45
45
  # Keep connections open during request client / testing
46
46
  request_started.disconnect(close_old_connections)
@@ -53,7 +53,7 @@ def setup_db(request):
53
53
  request_finished.connect(close_old_connections)
54
54
 
55
55
  # When the test session is done, tear down the test db
56
- teardown_databases(_old_db_config, verbosity=verbosity)
56
+ teardown_database(_old_db_name, verbosity=verbosity)
57
57
 
58
58
 
59
59
  @pytest.fixture
@@ -63,37 +63,26 @@ def db(setup_db, request):
63
63
  # Set .cursor() back to the original implementation to unblock it
64
64
  BaseDatabaseWrapper.cursor = BaseDatabaseWrapper._enabled_cursor
65
65
 
66
- # Keep track of the atomic blocks so we can roll them back
67
- atomics = {}
66
+ if not db_connection.features.supports_transactions:
67
+ pytest.fail("Database does not support transactions")
68
68
 
69
- for connection in connections.all():
70
- # By default we use transactions to rollback changes,
71
- # so we need to ensure the database supports transactions
72
- if not connection.features.supports_transactions:
73
- pytest.fail("Database does not support transactions")
74
-
75
- # Clear the queries log before each test?
76
- # connection.queries_log.clear()
77
-
78
- atomic = transaction.atomic(using=connection.alias)
79
- atomic._from_testcase = True # TODO remove this somehow?
80
- atomic.__enter__()
81
- atomics[connection] = atomic
69
+ atomic = transaction.atomic()
70
+ atomic._from_testcase = True # TODO remove this somehow?
71
+ atomic.__enter__()
82
72
 
83
73
  yield
84
74
 
85
- for connection, atomic in atomics.items():
86
- if (
87
- connection.features.can_defer_constraint_checks
88
- and not connection.needs_rollback
89
- and connection.is_usable()
90
- ):
91
- connection.check_constraints()
75
+ if (
76
+ db_connection.features.can_defer_constraint_checks
77
+ and not db_connection.needs_rollback
78
+ and db_connection.is_usable()
79
+ ):
80
+ db_connection.check_constraints()
92
81
 
93
- transaction.set_rollback(True, using=connection.alias)
94
- atomic.__exit__(None, None, None)
82
+ db_connection.set_rollback(True)
83
+ atomic.__exit__(None, None, None)
95
84
 
96
- connection.close()
85
+ db_connection.close()
97
86
 
98
87
 
99
88
  @pytest.fixture
@@ -115,9 +104,9 @@ def isolated_db(request):
115
104
  prefix = re.sub(r"[^0-9A-Za-z_]+", "_", raw_name)
116
105
 
117
106
  # Set up a fresh test database for this test, using the prefix
118
- _old_db_config = setup_databases(verbosity=verbosity, prefix=prefix)
107
+ _old_db_name = setup_database(verbosity=verbosity, prefix=prefix)
119
108
 
120
109
  yield
121
110
 
122
111
  # Tear down the test database created for this test
123
- teardown_databases(_old_db_config, verbosity=verbosity)
112
+ teardown_database(_old_db_name, verbosity=verbosity)
@@ -1,147 +1,11 @@
1
- from plain.exceptions import ImproperlyConfigured
2
- from plain.models import DEFAULT_DB_ALIAS, connections
1
+ from plain.models import db_connection
3
2
 
4
3
 
5
- def setup_databases(*, verbosity, prefix=""):
6
- """
7
- Create the test databases.
4
+ def setup_database(*, verbosity, prefix=""):
5
+ old_name = db_connection.settings_dict["NAME"]
6
+ db_connection.creation.create_test_db(verbosity=verbosity, prefix=prefix)
7
+ return old_name
8
8
 
9
- If prefix is provided, each test database name will be prefixed with
10
- "<prefix>_" to isolate it from the default test database.
11
- """
12
9
 
13
- test_databases, mirrored_aliases = get_unique_databases_and_mirrors(prefix=prefix)
14
-
15
- old_names = []
16
-
17
- for db_name, aliases in test_databases.values():
18
- first_alias = None
19
- for alias in aliases:
20
- connection = connections[alias]
21
- old_names.append((connection, db_name, first_alias is None))
22
-
23
- # Actually create the database for the first connection
24
- if first_alias is None:
25
- first_alias = alias
26
- connection.creation.create_test_db(
27
- verbosity=verbosity,
28
- prefix=prefix,
29
- )
30
- # Configure all other connections as mirrors of the first one
31
- else:
32
- connections[alias].creation.set_as_test_mirror(
33
- connections[first_alias].settings_dict
34
- )
35
-
36
- # Configure the test mirrors.
37
- for alias, mirror_alias in mirrored_aliases.items():
38
- connections[alias].creation.set_as_test_mirror(
39
- connections[mirror_alias].settings_dict
40
- )
41
-
42
- return old_names
43
-
44
-
45
- def get_unique_databases_and_mirrors(prefix=""):
46
- """
47
- Figure out which databases actually need to be created.
48
-
49
- Deduplicate entries in DATABASES that correspond the same database or are
50
- configured as test mirrors.
51
-
52
- Return two values:
53
- - test_databases: ordered mapping of signatures to (name, list of aliases)
54
- where all aliases share the same underlying database.
55
- - mirrored_aliases: mapping of mirror aliases to original aliases.
56
- """
57
-
58
- aliases = connections
59
- mirrored_aliases = {}
60
- test_databases = {}
61
- dependencies = {}
62
- default_sig = connections[DEFAULT_DB_ALIAS].creation.test_db_signature(prefix)
63
-
64
- for alias in connections:
65
- connection = connections[alias]
66
- test_settings = connection.settings_dict["TEST"]
67
-
68
- if test_settings["MIRROR"]:
69
- # If the database is marked as a test mirror, save the alias.
70
- mirrored_aliases[alias] = test_settings["MIRROR"]
71
- elif alias in aliases:
72
- # Store a tuple with DB parameters that uniquely identify it.
73
- # If we have two aliases with the same values for that tuple,
74
- # we only need to create the test database once.
75
- item = test_databases.setdefault(
76
- connection.creation.test_db_signature(prefix),
77
- (connection.settings_dict["NAME"], []),
78
- )
79
- # The default database must be the first because data migrations
80
- # use the default alias by default.
81
- if alias == DEFAULT_DB_ALIAS:
82
- item[1].insert(0, alias)
83
- else:
84
- item[1].append(alias)
85
-
86
- if "DEPENDENCIES" in test_settings:
87
- dependencies[alias] = test_settings["DEPENDENCIES"]
88
- else:
89
- if (
90
- alias != DEFAULT_DB_ALIAS
91
- and connection.creation.test_db_signature(prefix) != default_sig
92
- ):
93
- dependencies[alias] = test_settings.get(
94
- "DEPENDENCIES", [DEFAULT_DB_ALIAS]
95
- )
96
-
97
- test_databases = dict(dependency_ordered(test_databases.items(), dependencies))
98
- return test_databases, mirrored_aliases
99
-
100
-
101
- def teardown_databases(old_config, verbosity):
102
- """Destroy all the non-mirror databases."""
103
- for connection, old_name, destroy in old_config:
104
- if destroy:
105
- connection.creation.destroy_test_db(old_name, verbosity)
106
-
107
-
108
- def dependency_ordered(test_databases, dependencies):
109
- """
110
- Reorder test_databases into an order that honors the dependencies
111
- described in TEST[DEPENDENCIES].
112
- """
113
- ordered_test_databases = []
114
- resolved_databases = set()
115
-
116
- # Maps db signature to dependencies of all its aliases
117
- dependencies_map = {}
118
-
119
- # Check that no database depends on its own alias
120
- for sig, (_, aliases) in test_databases:
121
- all_deps = set()
122
- for alias in aliases:
123
- all_deps.update(dependencies.get(alias, []))
124
- if not all_deps.isdisjoint(aliases):
125
- raise ImproperlyConfigured(
126
- f"Circular dependency: databases {aliases!r} depend on each other, "
127
- "but are aliases."
128
- )
129
- dependencies_map[sig] = all_deps
130
-
131
- while test_databases:
132
- changed = False
133
- deferred = []
134
-
135
- # Try to find a DB that has all its dependencies met
136
- for signature, (db_name, aliases) in test_databases:
137
- if dependencies_map[signature].issubset(resolved_databases):
138
- resolved_databases.update(aliases)
139
- ordered_test_databases.append((signature, (db_name, aliases)))
140
- changed = True
141
- else:
142
- deferred.append((signature, (db_name, aliases)))
143
-
144
- if not changed:
145
- raise ImproperlyConfigured("Circular dependency in TEST[DEPENDENCIES]")
146
- test_databases = deferred
147
- return ordered_test_databases
10
+ def teardown_database(old_name, verbosity):
11
+ db_connection.creation.destroy_test_db(old_name, verbosity)