plain.models 0.46.0__py3-none-any.whl → 0.47.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.
plain/models/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # plain-models changelog
2
2
 
3
+ ## [0.47.0](https://github.com/dropseed/plain/releases/plain-models@0.47.0) (2025-09-25)
4
+
5
+ ### What's changed
6
+
7
+ - The `QuerySet.query` property has been renamed to `QuerySet.sql_query` to better distinguish it from the `Model.query` manager interface ([d250eea](https://github.com/dropseed/plain/commit/d250eeac03))
8
+
9
+ ### Upgrade instructions
10
+
11
+ - If you directly accessed the `QuerySet.query` property in your code (typically for advanced query manipulation or debugging), rename it to `QuerySet.sql_query`
12
+
13
+ ## [0.46.1](https://github.com/dropseed/plain/releases/plain-models@0.46.1) (2025-09-25)
14
+
15
+ ### What's changed
16
+
17
+ - Fixed `prefetch_related` for reverse foreign key relationships by correctly handling related managers in the prefetch query process ([2c04e80](https://github.com/dropseed/plain/commit/2c04e80dcd))
18
+
19
+ ### Upgrade instructions
20
+
21
+ - No changes required
22
+
3
23
  ## [0.46.0](https://github.com/dropseed/plain/releases/plain-models@0.46.0) (2025-09-25)
4
24
 
5
25
  ### What's changed
plain/models/deletion.py CHANGED
@@ -288,7 +288,7 @@ class Collector:
288
288
  # relationships are select_related as interactions between both
289
289
  # features are hard to get right. This should only happen in
290
290
  # the rare cases where .related_objects is overridden anyway.
291
- if not sub_objs.query.select_related:
291
+ if not sub_objs.sql_query.select_related:
292
292
  referenced_fields = set(
293
293
  chain.from_iterable(
294
294
  (rf.attname for rf in rel.field.foreign_related_fields)
@@ -17,19 +17,21 @@ from plain.models.utils import resolve_callables
17
17
 
18
18
  def _filter_prefetch_queryset(queryset, field_name, instances):
19
19
  predicate = Q(**{f"{field_name}__in": instances})
20
- if queryset.query.is_sliced:
20
+ if queryset.sql_query.is_sliced:
21
21
  if not db_connection.features.supports_over_clause:
22
22
  raise NotSupportedError(
23
23
  "Prefetching from a limited queryset is only supported on backends "
24
24
  "that support window functions."
25
25
  )
26
- low_mark, high_mark = queryset.query.low_mark, queryset.query.high_mark
27
- order_by = [expr for expr, _ in queryset.query.get_compiler().get_order_by()]
26
+ low_mark, high_mark = queryset.sql_query.low_mark, queryset.sql_query.high_mark
27
+ order_by = [
28
+ expr for expr, _ in queryset.sql_query.get_compiler().get_order_by()
29
+ ]
28
30
  window = Window(RowNumber(), partition_by=field_name, order_by=order_by)
29
31
  predicate &= GreaterThan(window, low_mark)
30
32
  if high_mark is not None:
31
33
  predicate &= LessThanOrEqual(window, high_mark)
32
- queryset.query.clear_limits()
34
+ queryset.sql_query.clear_limits()
33
35
  return queryset.filter(predicate)
34
36
 
35
37
 
plain/models/query.py CHANGED
@@ -60,7 +60,7 @@ class ModelIterable(BaseIterable):
60
60
 
61
61
  def __iter__(self):
62
62
  queryset = self.queryset
63
- compiler = queryset.query.get_compiler()
63
+ compiler = queryset.sql_query.get_compiler()
64
64
  # Execute the query. This will also fill compiler.select, klass_info,
65
65
  # and annotations.
66
66
  results = compiler.execute_sql(
@@ -117,7 +117,7 @@ class RawModelIterable(BaseIterable):
117
117
 
118
118
  def __iter__(self):
119
119
  # Cache some things for performance reasons outside the loop.
120
- query = self.queryset.query
120
+ query = self.queryset.sql_query
121
121
  compiler = db_connection.ops.compiler("SQLCompiler")(query, db_connection)
122
122
  query_iterator = iter(query)
123
123
 
@@ -159,7 +159,7 @@ class ValuesIterable(BaseIterable):
159
159
 
160
160
  def __iter__(self):
161
161
  queryset = self.queryset
162
- query = queryset.query
162
+ query = queryset.sql_query
163
163
  compiler = query.get_compiler()
164
164
 
165
165
  # extra(select=...) cols are always at the start of the row.
@@ -183,7 +183,7 @@ class ValuesListIterable(BaseIterable):
183
183
 
184
184
  def __iter__(self):
185
185
  queryset = self.queryset
186
- query = queryset.query
186
+ query = queryset.sql_query
187
187
  compiler = query.get_compiler()
188
188
 
189
189
  if queryset._fields:
@@ -225,7 +225,7 @@ class NamedValuesListIterable(ValuesListIterable):
225
225
  if queryset._fields:
226
226
  names = queryset._fields
227
227
  else:
228
- query = queryset.query
228
+ query = queryset.sql_query
229
229
  names = [
230
230
  *query.extra_select,
231
231
  *query.values_select,
@@ -245,7 +245,7 @@ class FlatValuesListIterable(BaseIterable):
245
245
 
246
246
  def __iter__(self):
247
247
  queryset = self.queryset
248
- compiler = queryset.query.get_compiler()
248
+ compiler = queryset.sql_query.get_compiler()
249
249
  for row in compiler.results_iter(
250
250
  chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size
251
251
  ):
@@ -270,15 +270,15 @@ class QuerySet:
270
270
  self._deferred_filter = None
271
271
 
272
272
  @property
273
- def query(self):
273
+ def sql_query(self):
274
274
  if self._deferred_filter:
275
275
  negate, args, kwargs = self._deferred_filter
276
276
  self._filter_or_exclude_inplace(negate, args, kwargs)
277
277
  self._deferred_filter = None
278
278
  return self._query
279
279
 
280
- @query.setter
281
- def query(self, value):
280
+ @sql_query.setter
281
+ def sql_query(self, value):
282
282
  if value.values_select:
283
283
  self._iterable_class = ValuesIterable
284
284
  self._query = value
@@ -380,11 +380,11 @@ class QuerySet:
380
380
  stop = int(k.stop)
381
381
  else:
382
382
  stop = None
383
- qs.query.set_limits(start, stop)
383
+ qs.sql_query.set_limits(start, stop)
384
384
  return list(qs)[:: k.step] if k.step else qs
385
385
 
386
386
  qs = self._chain()
387
- qs.query.set_limits(k, k + 1)
387
+ qs.sql_query.set_limits(k, k + 1)
388
388
  qs._fetch_all()
389
389
  return qs._result_cache[0]
390
390
 
@@ -400,7 +400,7 @@ class QuerySet:
400
400
  return self
401
401
  combined = self._chain()
402
402
  combined._merge_known_related_objects(other)
403
- combined.query.combine(other.query, sql.AND)
403
+ combined.sql_query.combine(other.sql_query, sql.AND)
404
404
  return combined
405
405
 
406
406
  def __or__(self, other):
@@ -412,14 +412,14 @@ class QuerySet:
412
412
  return self
413
413
  query = (
414
414
  self
415
- if self.query.can_filter()
415
+ if self.sql_query.can_filter()
416
416
  else self.model._meta.base_queryset.filter(id__in=self.values("id"))
417
417
  )
418
418
  combined = query._chain()
419
419
  combined._merge_known_related_objects(other)
420
- if not other.query.can_filter():
420
+ if not other.sql_query.can_filter():
421
421
  other = other.model._meta.base_queryset.filter(id__in=other.values("id"))
422
- combined.query.combine(other.query, sql.OR)
422
+ combined.sql_query.combine(other.sql_query, sql.OR)
423
423
  return combined
424
424
 
425
425
  def __xor__(self, other):
@@ -431,14 +431,14 @@ class QuerySet:
431
431
  return self
432
432
  query = (
433
433
  self
434
- if self.query.can_filter()
434
+ if self.sql_query.can_filter()
435
435
  else self.model._meta.base_queryset.filter(id__in=self.values("id"))
436
436
  )
437
437
  combined = query._chain()
438
438
  combined._merge_known_related_objects(other)
439
- if not other.query.can_filter():
439
+ if not other.sql_query.can_filter():
440
440
  other = other.model._meta.base_queryset.filter(id__in=other.values("id"))
441
- combined.query.combine(other.query, sql.XOR)
441
+ combined.sql_query.combine(other.sql_query, sql.XOR)
442
442
  return combined
443
443
 
444
444
  ####################################
@@ -487,7 +487,7 @@ class QuerySet:
487
487
  If args is present the expression is passed as a kwarg using
488
488
  the Aggregate object's default alias.
489
489
  """
490
- if self.query.distinct_fields:
490
+ if self.sql_query.distinct_fields:
491
491
  raise NotImplementedError("aggregate() + distinct(fields) not implemented.")
492
492
  self._validate_values_are_expressions(
493
493
  (*args, *kwargs.values()), method_name="aggregate"
@@ -502,7 +502,7 @@ class QuerySet:
502
502
  raise TypeError("Complex aggregates require an alias")
503
503
  kwargs[arg.default_alias] = arg
504
504
 
505
- return self.query.chain().get_aggregation(kwargs)
505
+ return self.sql_query.chain().get_aggregation(kwargs)
506
506
 
507
507
  def count(self):
508
508
  """
@@ -515,28 +515,30 @@ class QuerySet:
515
515
  if self._result_cache is not None:
516
516
  return len(self._result_cache)
517
517
 
518
- return self.query.get_count()
518
+ return self.sql_query.get_count()
519
519
 
520
520
  def get(self, *args, **kwargs):
521
521
  """
522
522
  Perform the query and return a single object matching the given
523
523
  keyword arguments.
524
524
  """
525
- if self.query.combinator and (args or kwargs):
525
+ if self.sql_query.combinator and (args or kwargs):
526
526
  raise NotSupportedError(
527
- f"Calling QuerySet.get(...) with filters after {self.query.combinator}() is not "
527
+ f"Calling QuerySet.get(...) with filters after {self.sql_query.combinator}() is not "
528
528
  "supported."
529
529
  )
530
- clone = self._chain() if self.query.combinator else self.filter(*args, **kwargs)
531
- if self.query.can_filter() and not self.query.distinct_fields:
530
+ clone = (
531
+ self._chain() if self.sql_query.combinator else self.filter(*args, **kwargs)
532
+ )
533
+ if self.sql_query.can_filter() and not self.sql_query.distinct_fields:
532
534
  clone = clone.order_by()
533
535
  limit = None
534
536
  if (
535
- not clone.query.select_for_update
537
+ not clone.sql_query.select_for_update
536
538
  or db_connection.features.supports_select_for_update_with_limit
537
539
  ):
538
540
  limit = MAX_GET_RESULTS
539
- clone.query.set_limits(high=limit)
541
+ clone.sql_query.set_limits(high=limit)
540
542
  num = len(clone)
541
543
  if num == 1:
542
544
  return clone._result_cache[0]
@@ -877,7 +879,7 @@ class QuerySet:
877
879
  Return a dictionary mapping each of the given IDs to the object with
878
880
  that ID. If `id_list` isn't provided, evaluate the entire QuerySet.
879
881
  """
880
- if self.query.is_sliced:
882
+ if self.sql_query.is_sliced:
881
883
  raise TypeError("Cannot use 'limit' or 'offset' with in_bulk().")
882
884
  opts = self.model._meta
883
885
  unique_fields = [
@@ -889,7 +891,7 @@ class QuerySet:
889
891
  field_name != "id"
890
892
  and not opts.get_field(field_name).primary_key
891
893
  and field_name not in unique_fields
892
- and self.query.distinct_fields != (field_name,)
894
+ and self.sql_query.distinct_fields != (field_name,)
893
895
  ):
894
896
  raise ValueError(
895
897
  f"in_bulk()'s field_name must be a unique field but {field_name!r} isn't."
@@ -916,9 +918,9 @@ class QuerySet:
916
918
  def delete(self):
917
919
  """Delete the records in the current QuerySet."""
918
920
  self._not_support_combined_queries("delete")
919
- if self.query.is_sliced:
921
+ if self.sql_query.is_sliced:
920
922
  raise TypeError("Cannot use 'limit' or 'offset' with delete().")
921
- if self.query.distinct or self.query.distinct_fields:
923
+ if self.sql_query.distinct or self.sql_query.distinct_fields:
922
924
  raise TypeError("Cannot call delete() after .distinct().")
923
925
  if self._fields is not None:
924
926
  raise TypeError("Cannot call delete() after .values() or .values_list()")
@@ -931,9 +933,9 @@ class QuerySet:
931
933
  del_query._for_write = True
932
934
 
933
935
  # Disable non-supported fields.
934
- del_query.query.select_for_update = False
935
- del_query.query.select_related = False
936
- del_query.query.clear_ordering(force=True)
936
+ del_query.sql_query.select_for_update = False
937
+ del_query.sql_query.select_related = False
938
+ del_query.sql_query.clear_ordering(force=True)
937
939
 
938
940
  from plain.models.deletion import Collector
939
941
 
@@ -950,7 +952,7 @@ class QuerySet:
950
952
  Delete objects found from the given queryset in single direct SQL
951
953
  query. No signals are sent and there is no protection for cascades.
952
954
  """
953
- query = self.query.clone()
955
+ query = self.sql_query.clone()
954
956
  query.__class__ = sql.DeleteQuery
955
957
  cursor = query.get_compiler().execute_sql(CURSOR)
956
958
  if cursor:
@@ -964,10 +966,10 @@ class QuerySet:
964
966
  fields to the appropriate values.
965
967
  """
966
968
  self._not_support_combined_queries("update")
967
- if self.query.is_sliced:
969
+ if self.sql_query.is_sliced:
968
970
  raise TypeError("Cannot update a query once a slice has been taken.")
969
971
  self._for_write = True
970
- query = self.query.chain(sql.UpdateQuery)
972
+ query = self.sql_query.chain(sql.UpdateQuery)
971
973
  query.add_update_values(kwargs)
972
974
 
973
975
  # Inline annotations in order_by(), if possible.
@@ -1004,9 +1006,9 @@ class QuerySet:
1004
1006
  code (it requires too much poking around at model internals to be
1005
1007
  useful at that level).
1006
1008
  """
1007
- if self.query.is_sliced:
1009
+ if self.sql_query.is_sliced:
1008
1010
  raise TypeError("Cannot update a query once a slice has been taken.")
1009
- query = self.query.chain(sql.UpdateQuery)
1011
+ query = self.sql_query.chain(sql.UpdateQuery)
1010
1012
  query.add_update_fields(values)
1011
1013
  # Clear any annotations so that they won't be present in subqueries.
1012
1014
  query.annotations = {}
@@ -1018,7 +1020,7 @@ class QuerySet:
1018
1020
  Return True if the QuerySet would have any results, False otherwise.
1019
1021
  """
1020
1022
  if self._result_cache is None:
1021
- return self.query.has_results()
1023
+ return self.sql_query.has_results()
1022
1024
  return bool(self._result_cache)
1023
1025
 
1024
1026
  def contains(self, obj):
@@ -1052,7 +1054,7 @@ class QuerySet:
1052
1054
  Runs an EXPLAIN on the SQL query this QuerySet would perform, and
1053
1055
  returns the results.
1054
1056
  """
1055
- return self.query.explain(format=format, **options)
1057
+ return self.sql_query.explain(format=format, **options)
1056
1058
 
1057
1059
  ##################################################
1058
1060
  # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS #
@@ -1073,7 +1075,7 @@ class QuerySet:
1073
1075
  if expressions:
1074
1076
  clone = clone.annotate(**expressions)
1075
1077
  clone._fields = fields
1076
- clone.query.set_values(fields)
1078
+ clone.sql_query.set_values(fields)
1077
1079
  return clone
1078
1080
 
1079
1081
  def values(self, *fields, **expressions):
@@ -1175,7 +1177,7 @@ class QuerySet:
1175
1177
  def none(self):
1176
1178
  """Return an empty QuerySet."""
1177
1179
  clone = self._chain()
1178
- clone.query.set_empty()
1180
+ clone.sql_query.set_empty()
1179
1181
  return clone
1180
1182
 
1181
1183
  ##################################################################
@@ -1206,7 +1208,7 @@ class QuerySet:
1206
1208
  return self._filter_or_exclude(True, args, kwargs)
1207
1209
 
1208
1210
  def _filter_or_exclude(self, negate, args, kwargs):
1209
- if (args or kwargs) and self.query.is_sliced:
1211
+ if (args or kwargs) and self.sql_query.is_sliced:
1210
1212
  raise TypeError("Cannot filter a query once a slice has been taken.")
1211
1213
  clone = self._chain()
1212
1214
  if self._defer_next_filter:
@@ -1234,7 +1236,7 @@ class QuerySet:
1234
1236
  """
1235
1237
  if isinstance(filter_obj, Q):
1236
1238
  clone = self._chain()
1237
- clone.query.add_q(filter_obj)
1239
+ clone.sql_query.add_q(filter_obj)
1238
1240
  return clone
1239
1241
  else:
1240
1242
  return self._filter_or_exclude(False, args=(), kwargs=filter_obj)
@@ -1243,13 +1245,13 @@ class QuerySet:
1243
1245
  # Clone the query to inherit the select list and everything
1244
1246
  clone = self._chain()
1245
1247
  # Clear limits and ordering so they can be reapplied
1246
- clone.query.clear_ordering(force=True)
1247
- clone.query.clear_limits()
1248
- clone.query.combined_queries = (self.query,) + tuple(
1249
- qs.query for qs in other_qs
1248
+ clone.sql_query.clear_ordering(force=True)
1249
+ clone.sql_query.clear_limits()
1250
+ clone.sql_query.combined_queries = (self.sql_query,) + tuple(
1251
+ qs.sql_query for qs in other_qs
1250
1252
  )
1251
- clone.query.combinator = combinator
1252
- clone.query.combinator_all = all
1253
+ clone.sql_query.combinator = combinator
1254
+ clone.sql_query.combinator_all = all
1253
1255
  return clone
1254
1256
 
1255
1257
  def union(self, *other_qs, all=False):
@@ -1287,11 +1289,11 @@ class QuerySet:
1287
1289
  raise ValueError("The nowait option cannot be used with skip_locked.")
1288
1290
  obj = self._chain()
1289
1291
  obj._for_write = True
1290
- obj.query.select_for_update = True
1291
- obj.query.select_for_update_nowait = nowait
1292
- obj.query.select_for_update_skip_locked = skip_locked
1293
- obj.query.select_for_update_of = of
1294
- obj.query.select_for_no_key_update = no_key
1292
+ obj.sql_query.select_for_update = True
1293
+ obj.sql_query.select_for_update_nowait = nowait
1294
+ obj.sql_query.select_for_update_skip_locked = skip_locked
1295
+ obj.sql_query.select_for_update_of = of
1296
+ obj.sql_query.select_for_no_key_update = no_key
1295
1297
  return obj
1296
1298
 
1297
1299
  def select_related(self, *fields):
@@ -1311,11 +1313,11 @@ class QuerySet:
1311
1313
 
1312
1314
  obj = self._chain()
1313
1315
  if fields == (None,):
1314
- obj.query.select_related = False
1316
+ obj.sql_query.select_related = False
1315
1317
  elif fields:
1316
- obj.query.add_select_related(fields)
1318
+ obj.sql_query.add_select_related(fields)
1317
1319
  else:
1318
- obj.query.select_related = True
1320
+ obj.sql_query.select_related = True
1319
1321
  return obj
1320
1322
 
1321
1323
  def prefetch_related(self, *lookups):
@@ -1336,7 +1338,7 @@ class QuerySet:
1336
1338
  if isinstance(lookup, Prefetch):
1337
1339
  lookup = lookup.prefetch_to
1338
1340
  lookup = lookup.split(LOOKUP_SEP, 1)[0]
1339
- if lookup in self.query._filtered_relations:
1341
+ if lookup in self.sql_query._filtered_relations:
1340
1342
  raise ValueError(
1341
1343
  "prefetch_related() is not supported with FilteredRelation."
1342
1344
  )
@@ -1394,30 +1396,30 @@ class QuerySet:
1394
1396
  f"The annotation '{alias}' conflicts with a field on the model."
1395
1397
  )
1396
1398
  if isinstance(annotation, FilteredRelation):
1397
- clone.query.add_filtered_relation(annotation, alias)
1399
+ clone.sql_query.add_filtered_relation(annotation, alias)
1398
1400
  else:
1399
- clone.query.add_annotation(
1401
+ clone.sql_query.add_annotation(
1400
1402
  annotation,
1401
1403
  alias,
1402
1404
  select=select,
1403
1405
  )
1404
- for alias, annotation in clone.query.annotations.items():
1406
+ for alias, annotation in clone.sql_query.annotations.items():
1405
1407
  if alias in annotations and annotation.contains_aggregate:
1406
1408
  if clone._fields is None:
1407
- clone.query.group_by = True
1409
+ clone.sql_query.group_by = True
1408
1410
  else:
1409
- clone.query.set_group_by()
1411
+ clone.sql_query.set_group_by()
1410
1412
  break
1411
1413
 
1412
1414
  return clone
1413
1415
 
1414
1416
  def order_by(self, *field_names):
1415
1417
  """Return a new QuerySet instance with the ordering changed."""
1416
- if self.query.is_sliced:
1418
+ if self.sql_query.is_sliced:
1417
1419
  raise TypeError("Cannot reorder a query once a slice has been taken.")
1418
1420
  obj = self._chain()
1419
- obj.query.clear_ordering(force=True, clear_default=False)
1420
- obj.query.add_ordering(*field_names)
1421
+ obj.sql_query.clear_ordering(force=True, clear_default=False)
1422
+ obj.sql_query.add_ordering(*field_names)
1421
1423
  return obj
1422
1424
 
1423
1425
  def distinct(self, *field_names):
@@ -1425,12 +1427,12 @@ class QuerySet:
1425
1427
  Return a new QuerySet instance that will select only distinct results.
1426
1428
  """
1427
1429
  self._not_support_combined_queries("distinct")
1428
- if self.query.is_sliced:
1430
+ if self.sql_query.is_sliced:
1429
1431
  raise TypeError(
1430
1432
  "Cannot create distinct fields once a slice has been taken."
1431
1433
  )
1432
1434
  obj = self._chain()
1433
- obj.query.add_distinct_fields(*field_names)
1435
+ obj.sql_query.add_distinct_fields(*field_names)
1434
1436
  return obj
1435
1437
 
1436
1438
  def extra(
@@ -1444,18 +1446,20 @@ class QuerySet:
1444
1446
  ):
1445
1447
  """Add extra SQL fragments to the query."""
1446
1448
  self._not_support_combined_queries("extra")
1447
- if self.query.is_sliced:
1449
+ if self.sql_query.is_sliced:
1448
1450
  raise TypeError("Cannot change a query once a slice has been taken.")
1449
1451
  clone = self._chain()
1450
- clone.query.add_extra(select, select_params, where, params, tables, order_by)
1452
+ clone.sql_query.add_extra(
1453
+ select, select_params, where, params, tables, order_by
1454
+ )
1451
1455
  return clone
1452
1456
 
1453
1457
  def reverse(self):
1454
1458
  """Reverse the ordering of the QuerySet."""
1455
- if self.query.is_sliced:
1459
+ if self.sql_query.is_sliced:
1456
1460
  raise TypeError("Cannot reverse a query once a slice has been taken.")
1457
1461
  clone = self._chain()
1458
- clone.query.standard_ordering = not clone.query.standard_ordering
1462
+ clone.sql_query.standard_ordering = not clone.sql_query.standard_ordering
1459
1463
  return clone
1460
1464
 
1461
1465
  def defer(self, *fields):
@@ -1470,9 +1474,9 @@ class QuerySet:
1470
1474
  raise TypeError("Cannot call defer() after .values() or .values_list()")
1471
1475
  clone = self._chain()
1472
1476
  if fields == (None,):
1473
- clone.query.clear_deferred_loading()
1477
+ clone.sql_query.clear_deferred_loading()
1474
1478
  else:
1475
- clone.query.add_deferred_loading(fields)
1479
+ clone.sql_query.add_deferred_loading(fields)
1476
1480
  return clone
1477
1481
 
1478
1482
  def only(self, *fields):
@@ -1490,10 +1494,10 @@ class QuerySet:
1490
1494
  raise TypeError("Cannot pass None as an argument to only().")
1491
1495
  for field in fields:
1492
1496
  field = field.split(LOOKUP_SEP, 1)[0]
1493
- if field in self.query._filtered_relations:
1497
+ if field in self.sql_query._filtered_relations:
1494
1498
  raise ValueError("only() is not supported with FilteredRelation.")
1495
1499
  clone = self._chain()
1496
- clone.query.add_immediate_loading(fields)
1500
+ clone.sql_query.add_immediate_loading(fields)
1497
1501
  return clone
1498
1502
 
1499
1503
  ###################################
@@ -1508,14 +1512,14 @@ class QuerySet:
1508
1512
  """
1509
1513
  if isinstance(self, EmptyQuerySet):
1510
1514
  return True
1511
- if self.query.extra_order_by or self.query.order_by:
1515
+ if self.sql_query.extra_order_by or self.sql_query.order_by:
1512
1516
  return True
1513
1517
  elif (
1514
- self.query.default_ordering
1515
- and self.query.get_meta().ordering
1518
+ self.sql_query.default_ordering
1519
+ and self.sql_query.get_meta().ordering
1516
1520
  and
1517
1521
  # A default ordering doesn't affect GROUP BY queries.
1518
- not self.query.group_by
1522
+ not self.sql_query.group_by
1519
1523
  ):
1520
1524
  return True
1521
1525
  else:
@@ -1592,7 +1596,7 @@ class QuerySet:
1592
1596
  """
1593
1597
  obj = self._clone()
1594
1598
  if obj._sticky_filter:
1595
- obj.query.filter_is_sticky = True
1599
+ obj.sql_query.filter_is_sticky = True
1596
1600
  obj._sticky_filter = False
1597
1601
  return obj
1598
1602
 
@@ -1603,7 +1607,7 @@ class QuerySet:
1603
1607
  """
1604
1608
  c = self.__class__(
1605
1609
  model=self.model,
1606
- query=self.query.chain(),
1610
+ query=self.sql_query.chain(),
1607
1611
  )
1608
1612
  c._sticky_filter = self._sticky_filter
1609
1613
  c._for_write = self._for_write
@@ -1636,9 +1640,10 @@ class QuerySet:
1636
1640
  def _merge_sanity_check(self, other):
1637
1641
  """Check that two QuerySet classes may be merged."""
1638
1642
  if self._fields is not None and (
1639
- set(self.query.values_select) != set(other.query.values_select)
1640
- or set(self.query.extra_select) != set(other.query.extra_select)
1641
- or set(self.query.annotation_select) != set(other.query.annotation_select)
1643
+ set(self.sql_query.values_select) != set(other.sql_query.values_select)
1644
+ or set(self.sql_query.extra_select) != set(other.sql_query.extra_select)
1645
+ or set(self.sql_query.annotation_select)
1646
+ != set(other.sql_query.annotation_select)
1642
1647
  ):
1643
1648
  raise TypeError(
1644
1649
  f"Merging '{self.__class__.__name__}' classes must involve the same values in each case."
@@ -1656,7 +1661,7 @@ class QuerySet:
1656
1661
  # values() queryset can only be used as nested queries
1657
1662
  # if they are set up to select only a single field.
1658
1663
  raise TypeError("Cannot use multi-field values as a filter value.")
1659
- query = self.query.resolve_expression(*args, **kwargs)
1664
+ query = self.sql_query.resolve_expression(*args, **kwargs)
1660
1665
  return query
1661
1666
 
1662
1667
  def _has_filters(self):
@@ -1665,7 +1670,7 @@ class QuerySet:
1665
1670
  equivalent with checking if all objects are present in results, for
1666
1671
  example, qs[1:]._has_filters() -> False.
1667
1672
  """
1668
- return self.query.has_filters()
1673
+ return self.sql_query.has_filters()
1669
1674
 
1670
1675
  @staticmethod
1671
1676
  def _validate_values_are_expressions(values, method_name):
@@ -1681,19 +1686,19 @@ class QuerySet:
1681
1686
  )
1682
1687
 
1683
1688
  def _not_support_combined_queries(self, operation_name):
1684
- if self.query.combinator:
1689
+ if self.sql_query.combinator:
1685
1690
  raise NotSupportedError(
1686
- f"Calling QuerySet.{operation_name}() after {self.query.combinator}() is not supported."
1691
+ f"Calling QuerySet.{operation_name}() after {self.sql_query.combinator}() is not supported."
1687
1692
  )
1688
1693
 
1689
1694
  def _check_operator_queryset(self, other, operator_):
1690
- if self.query.combinator or other.query.combinator:
1695
+ if self.sql_query.combinator or other.sql_query.combinator:
1691
1696
  raise TypeError(f"Cannot use {operator_} operator with combined queryset.")
1692
1697
 
1693
1698
 
1694
1699
  class InstanceCheckMeta(type):
1695
1700
  def __instancecheck__(self, instance):
1696
- return isinstance(instance, QuerySet) and instance.query.is_empty()
1701
+ return isinstance(instance, QuerySet) and instance.sql_query.is_empty()
1697
1702
 
1698
1703
 
1699
1704
  class EmptyQuerySet(metaclass=InstanceCheckMeta):
@@ -1722,7 +1727,7 @@ class RawQuerySet:
1722
1727
  ):
1723
1728
  self.raw_query = raw_query
1724
1729
  self.model = model
1725
- self.query = query or sql.RawQuery(sql=raw_query, params=params)
1730
+ self.sql_query = query or sql.RawQuery(sql=raw_query, params=params)
1726
1731
  self.params = params
1727
1732
  self.translations = translations or {}
1728
1733
  self._result_cache = None
@@ -1764,7 +1769,7 @@ class RawQuerySet:
1764
1769
  c = self.__class__(
1765
1770
  self.raw_query,
1766
1771
  model=self.model,
1767
- query=self.query,
1772
+ query=self.sql_query,
1768
1773
  params=self.params,
1769
1774
  translations=self.translations,
1770
1775
  )
@@ -1793,7 +1798,7 @@ class RawQuerySet:
1793
1798
  yield from RawModelIterable(self)
1794
1799
 
1795
1800
  def __repr__(self):
1796
- return f"<{self.__class__.__name__}: {self.query}>"
1801
+ return f"<{self.__class__.__name__}: {self.sql_query}>"
1797
1802
 
1798
1803
  def __getitem__(self, k):
1799
1804
  return list(self)[k]
@@ -1804,7 +1809,7 @@ class RawQuerySet:
1804
1809
  A list of model field names in the order they'll appear in the
1805
1810
  query results.
1806
1811
  """
1807
- columns = self.query.get_columns()
1812
+ columns = self.sql_query.get_columns()
1808
1813
  # Adjust any column names which don't match field names
1809
1814
  for query_name, model_name in self.translations.items():
1810
1815
  # Ignore translations for nonexistent column names
@@ -2204,7 +2209,15 @@ def prefetch_one_level(instances, prefetcher, lookup, level):
2204
2209
  if leaf and lookup.queryset is not None:
2205
2210
  qs = queryset._apply_rel_filters(lookup.queryset)
2206
2211
  else:
2207
- qs = queryset.__class__(model=queryset.model)
2212
+ # Check if queryset is a QuerySet or a related manager
2213
+ # We need a QuerySet instance to cache the prefetched values
2214
+ if isinstance(queryset, QuerySet):
2215
+ # It's already a QuerySet, create a new instance
2216
+ qs = queryset.__class__(model=queryset.model)
2217
+ else:
2218
+ # It's a related manager, get its QuerySet
2219
+ # The manager's query property returns a properly filtered QuerySet
2220
+ qs = queryset.query
2208
2221
  qs._result_cache = vals
2209
2222
  # We don't want the individual qs doing prefetch_related now,
2210
2223
  # since we have merged this into the current work.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.models
3
- Version: 0.46.0
3
+ Version: 0.47.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-File: LICENSE
@@ -1,5 +1,5 @@
1
1
  plain/models/AGENTS.md,sha256=xQQW-z-DehnCUyjiGSBfLqUjoSUdo_W1b0JmwYmWieA,209
2
- plain/models/CHANGELOG.md,sha256=sP6SfQbc4brCN0mgeyZvDQ2hEk9i2lGH49bKPJGOZlw,16770
2
+ plain/models/CHANGELOG.md,sha256=JMkQ7LwLIp63_HtRIwOMdIpveUrhHvIBJbvBDaNG9k4,17633
3
3
  plain/models/README.md,sha256=lqzWJrEIxBCHC1P8X1YoRjbsMFlu0-kG4ujP76B_ZO4,8572
4
4
  plain/models/__init__.py,sha256=aB9HhIKBh0iK3LZztInAE-rDF-yKsdfcjfMtwtN5vnI,2920
5
5
  plain/models/aggregates.py,sha256=P0mhsMl1VZt2CVHMuCHnNI8SxZ9citjDLEgioN6NOpo,7240
@@ -12,7 +12,7 @@ plain/models/constraints.py,sha256=Mm9gm5D7EKmo486dL481-hrTcxi2gxgqyUUtbGrkLjs,1
12
12
  plain/models/database_url.py,sha256=iidKVhOylf5N6t1EMPRySRQiv6LiuRjYRECB_UJ3MI8,6419
13
13
  plain/models/db.py,sha256=FpdfLYrRX2THUzDy4QdJ_OpSo9IFKLerZIEQ-T2x8zA,1348
14
14
  plain/models/default_settings.py,sha256=cDym1o_DtHySWgDRIdjgEM0YxjgYU51ZqzWVA3vpzTk,569
15
- plain/models/deletion.py,sha256=3eW3bb39NbIlIAp4ZAEtSZuiy9oH4i5e83OK_8oIaxI,17607
15
+ plain/models/deletion.py,sha256=GnjhPHLRAsrec7Aa0sczqtOEDV41AGKj6QBQdhl508A,17611
16
16
  plain/models/entrypoints.py,sha256=EC14mW19tK9dCumaNHnv4_9jQV8jomQ8jXy8Ib89VBw,191
17
17
  plain/models/enums.py,sha256=Zr-JKt2aeYsSADtAm69fDRfajS7jYwop2vWQVLJ9YYI,2726
18
18
  plain/models/exceptions.py,sha256=IqzK60-hY3TYsgOMxlWwgpVa21E7ydC-gqUG4tNvVJc,2042
@@ -23,7 +23,7 @@ plain/models/lookups.py,sha256=eCsxQXUcOoAa_U_fAAd3edcgXI1wfyFW8hPgUh8TwTo,24776
23
23
  plain/models/options.py,sha256=BOnu9NDVcgL0tJhan5gBbaK1SWNeg4NVTPNAzkKT3NE,21528
24
24
  plain/models/otel.py,sha256=36QSJS6UXv1YPJTqeSmEvdMVHRkXa_zgqqItJaXc59g,7619
25
25
  plain/models/preflight.py,sha256=_cBX7AnfQDNtZfoW0ydxH8WQM3ftCqcH0-tPhqS5q8c,8973
26
- plain/models/query.py,sha256=6t0ow7oQfVB6WiC3fQ3kLme8TBeSOscEt5xl2lu6oOQ,89703
26
+ plain/models/query.py,sha256=YxIpgRndQyXsSpNLl3uPGvu-UNQzK1CXFsf2U75XVao,90714
27
27
  plain/models/query_utils.py,sha256=zxAdfwDbOmaN_SJODl4Wl9gs-q2EzOjXbsBFTWWhh8g,14174
28
28
  plain/models/registry.py,sha256=5yxVgT_W8GlyL2bsGT2HvMQB5sKolXucP2qrhr7Wlnk,8126
29
29
  plain/models/transaction.py,sha256=KqkRDT6aqMgbPA_ch7qO8a9NyDvwY_2FaxM7FkBkcgY,9357
@@ -77,7 +77,7 @@ plain/models/fields/mixins.py,sha256=wmu3JJEOimijgepjMEFPN8u74mHpefgnsB4u5ZzVCUY
77
77
  plain/models/fields/related.py,sha256=7Ku8E5hxQxVgJy3afWZfuDlHDzrky_4mkKwPlqLpM6o,51065
78
78
  plain/models/fields/related_descriptors.py,sha256=nsVgLjpOlrha90eTfg7ad_il6_uI_YG0d4bH51LP3Os,15180
79
79
  plain/models/fields/related_lookups.py,sha256=9y6AfEcg8xRRZne2LXFP6jym9mecFlB_toYih7lD8Uw,7781
80
- plain/models/fields/related_managers.py,sha256=XiV2IvuEFLEWnej2KokCu6HFx87UyZTqqsP74v2xIHw,25011
80
+ plain/models/fields/related_managers.py,sha256=LefSf8ss8XZ_97NoKfRKHTijheb44exJ_UBLGcNgQdc,25053
81
81
  plain/models/fields/reverse_related.py,sha256=SNFytCI3BeAlB5kY6UQrv6QGqtRMo_aHWPx_-fCkfu4,10404
82
82
  plain/models/functions/__init__.py,sha256=aglCm_JtzDYk2KmxubDN_78CGG3JCfRWnfJ74Oj5YJ4,2658
83
83
  plain/models/functions/comparison.py,sha256=9uAiEuNXZiGFzJKBvktsHwx58Qpa2cPQkr6pUWsGcKo,6554
@@ -115,8 +115,8 @@ plain/models/sql/where.py,sha256=ezE9Clt2BmKo-I7ARsgqZ_aVA-1UdayCwr6ULSWZL6c,126
115
115
  plain/models/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
116
  plain/models/test/pytest.py,sha256=KD5-mxonBxOYIhUh9Ql5uJOIiC9R4t-LYfb6sjA0UdE,3486
117
117
  plain/models/test/utils.py,sha256=S3d6zf3OFWDxB_kBJr0tDvwn51bjwDVWKPumv37N-p8,467
118
- plain_models-0.46.0.dist-info/METADATA,sha256=gcrk_0dq0Aae9LjlI_uB1g5XrjfuDFQ2bU7wbAgExn8,8884
119
- plain_models-0.46.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
120
- plain_models-0.46.0.dist-info/entry_points.txt,sha256=IYJAW9MpL3PXyXFWmKmALagAGXC_5rzBn2eEGJlcV04,112
121
- plain_models-0.46.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
122
- plain_models-0.46.0.dist-info/RECORD,,
118
+ plain_models-0.47.0.dist-info/METADATA,sha256=oBEWDFezcMc1KxtVoQIMSEr15Oy7NB43aLiAGurmYfg,8884
119
+ plain_models-0.47.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
120
+ plain_models-0.47.0.dist-info/entry_points.txt,sha256=IYJAW9MpL3PXyXFWmKmALagAGXC_5rzBn2eEGJlcV04,112
121
+ plain_models-0.47.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
122
+ plain_models-0.47.0.dist-info/RECORD,,