| 550 | 550 | Perform a COUNT() query using the current filter constraints. |
| 551 | 551 | """ |
| 552 | 552 | obj = self.clone() |
| 553 | ||
| 554 | # Strip unused annotations for count() queries | |
| 555 | # Only strip annotations that are not referenced in filters, ordering, or other annotations | |
| 556 | if obj.annotations: | |
| 557 | used_annotations = self._get_used_annotations_for_count(obj) | |
| 558 | if used_annotations is not None and used_annotations != set(obj.annotations.keys()): | |
| 559 | # Only keep annotations that are actually used | |
| 560 | obj.annotations = { | |
| 561 | alias: annotation for alias, annotation in obj.annotations.items() | |
| 562 | if alias in used_annotations | |
| 563 | } | |
| 564 | # Clear the annotation mask cache | |
| 565 | obj._annotation_select_cache = None | |
| 566 | ||
| 553 | 567 | obj.add_annotation(Count("*"), alias="__count", is_summary=True) |
| 554 | 568 | return obj.get_aggregation(using, ["__count"])["__count"] |
| 555 | 569 | |
| 570 | def _get_used_annotations_for_count(self, query): | |
| 571 | """ | |
| 572 | Determine which annotations are actually used in the query. | |
| 573 | Returns a set of annotation aliases that are referenced in: | |
| 574 | - WHERE clauses (filters) | |
| 575 | - ORDER BY clauses | |
| 576 | - Other annotations | |
| 577 | Returns None if all annotations should be kept (conservative approach). | |
| 578 | """ | |
| 579 | if not query.annotations: | |
| 580 | return set() | |
| 581 | ||
| 582 | used_annotations = set() | |
| 583 | ||
| 584 | # Check if annotations are referenced in WHERE clause | |
| 585 | if query.where: | |
| 586 | used_annotations.update(self._extract_annotations_from_node(query.where, query.annotations)) | |
| 587 | ||
| 588 | # Check if annotations are referenced in ORDER BY clause | |
| 589 | if query.order_by: | |
| 590 | for order_expr in query.order_by: | |
| 591 | if isinstance(order_expr, str) and order_expr in query.annotations: | |
| 592 | used_annotations.add(order_expr) | |
| 593 | ||
| 594 | # Check if annotations are referenced in other annotations | |
| 595 | for alias, annotation in query.annotations.items(): | |
| 596 | # Skip if this annotation is already marked as used | |
| 597 | if alias in used_annotations: | |
| 598 | continue | |
| 599 | # Check if this annotation references other annotations | |
| 600 | referenced = self._extract_annotations_from_expression(annotation, query.annotations) | |
| 601 | if referenced: | |
| 602 | used_annotations.update(referenced) | |
| 603 | ||
| 604 | # Ultra-conservative approach: only strip non-Count annotations | |
| 605 | # Keep all Count annotations to avoid breaking existing behavior | |
| 606 | if not used_annotations: | |
| 607 | # Only strip annotations that are not Count functions | |
| 608 | annotations_to_strip = set() | |
| 609 | for alias, annotation in query.annotations.items(): | |
| 610 | # Only strip annotations that are not Count functions | |
| 611 | if not (hasattr(annotation, 'function') and annotation.function == "COUNT"): | |
| 612 | annotations_to_strip.add(alias) | |
| 613 | ||
| 614 | return annotations_to_strip | |
| 615 | ||
| 616 | return used_annotations | |
| 617 | ||
| 618 | def _extract_annotations_from_node(self, node, annotations): | |
| 619 | """Extract annotation references from a WHERE clause node.""" | |
| 620 | used = set() | |
| 621 | if hasattr(node, 'children'): | |
| 622 | for child in node.children: | |
| 623 | used.update(self._extract_annotations_from_node(child, annotations)) | |
| 624 | elif hasattr(node, 'lhs') and hasattr(node, 'rhs'): | |
| 625 | # Binary condition | |
| 626 | used.update(self._extract_annotations_from_expression(node.lhs, annotations)) | |
| 627 | used.update(self._extract_annotations_from_expression(node.rhs, annotations)) | |
| 628 | elif hasattr(node, 'lookup_parts'): | |
| 629 | # Simple lookup | |
| 630 | lookup_str = LOOKUP_SEP.join(node.lookup_parts) | |
| 631 | for annotation_alias in annotations: | |
| 632 | if lookup_str.startswith(annotation_alias + LOOKUP_SEP) or lookup_str == annotation_alias: | |
| 633 | used.add(annotation_alias) | |
| 634 | return used | |
| 635 | ||
| 636 | def _is_safe_to_strip_count_annotation(self, annotation, query): | |
| 637 | """ | |
| 638 | Determine if a Count annotation can be safely stripped from a count query. | |
| 639 | Be very conservative - only strip COUNT(*) or very simple cases. | |
| 640 | """ | |
| 641 | # Get the source expressions of the Count annotation | |
| 642 | source_expressions = annotation.get_source_expressions() | |
| 643 | ||
| 644 | # COUNT(*) is always safe to strip as it doesn't create joins | |
| 645 | for expr in source_expressions: | |
| 646 | if hasattr(expr, '__class__') and expr.__class__.__name__ == 'Star': | |
| 647 | return True | |
| 648 | ||
| 649 | # For other Count annotations, be very conservative | |
| 650 | # Only consider stripping if it's a simple field reference that doesn't create joins | |
| 651 | if len(source_expressions) == 1: | |
| 652 | expr = source_expressions[0] | |
| 653 | if hasattr(expr, 'name'): | |
| 654 | try: | |
| 655 | # Check if it's a simple field on the same model (no joins) | |
| 656 | field_list = expr.name.split(LOOKUP_SEP) | |
| 657 | if len(field_list) == 1: | |
| 658 | # Single field reference - check if it's a local field (not a relation) | |
| 659 | opts = query.get_meta() | |
| 660 | try: | |
| 661 | field = opts.get_field(field_list[0]) | |
| 662 | # Only safe to strip if it's not a relation field | |
| 663 | return not field.is_relation | |
| 664 | except: | |
| 665 | # Field not found, assume it's not safe | |
| 666 | return False | |
| 667 | except Exception: | |
| 668 | # If we can't resolve it, assume it's not safe | |
| 669 | return False | |
| 670 | ||
| 671 | # Default to not safe for any complex cases | |
| 672 | return False | |
| 673 | ||
| 674 | def _count_annotation_creates_join(self, annotation, query): | |
| 675 | """ | |
| 676 | Check if a Count annotation creates a join that affects row count. | |
| 677 | Returns True if the Count annotation references a related field that would create a join. | |
| 678 | """ | |
| 679 | # Get the source expressions of the Count annotation | |
| 680 | source_expressions = annotation.get_source_expressions() | |
| 681 | ||
| 682 | for expr in source_expressions: | |
| 683 | # Skip Star expressions (COUNT(*)) as they don't create joins | |
| 684 | if hasattr(expr, '__class__') and expr.__class__.__name__ == 'Star': | |
| 685 | continue | |
| 686 | ||
| 687 | # Check if this is a field reference that might create a join | |
| 688 | if hasattr(expr, 'name'): | |
| 689 | # This looks like a field reference, check if it's a related field | |
| 690 | try: | |
| 691 | # Try to resolve the field name to see if it creates a join | |
| 692 | field_list = expr.name.split(LOOKUP_SEP) | |
| 693 | if len(field_list) > 1: | |
| 694 | # Multi-part lookup (e.g., 'authors__name') likely creates a join | |
| 695 | return True | |
| 696 | ||
| 697 | # Check if it's a related field on the model | |
| 698 | opts = query.get_meta() | |
| 699 | try: | |
| 700 | field = opts.get_field(field_list[0]) | |
| 701 | # If it's a relation field (ForeignKey, ManyToManyField, etc.), it creates a join | |
| 702 | if field.is_relation: | |
| 703 | return True | |
| 704 | except: | |
| 705 | # Field not found, might be an annotation or something else | |
| 706 | pass | |
| 707 | ||
| 708 | except Exception: | |
| 709 | # If we can't resolve it, assume it might create a join to be safe | |
| 710 | return True | |
| 711 | ||
| 712 | # If we get here, no join-creating expressions were found | |
| 713 | return False | |
| 714 | ||
| 715 | def _extract_annotations_from_expression(self, expression, annotations): | |
| 716 | """Extract annotation references from an expression.""" | |
| 717 | used = set() | |
| 718 | if hasattr(expression, 'source_expressions'): | |
| 719 | for source_expr in expression.source_expressions: | |
| 720 | used.update(self._extract_annotations_from_expression(source_expr, annotations)) | |
| 721 | elif hasattr(expression, 'name') and expression.name in annotations: | |
| 722 | used.add(expression.name) | |
| 723 | elif hasattr(expression, 'lookup_parts'): | |
| 724 | lookup_str = LOOKUP_SEP.join(expression.lookup_parts) | |
| 725 | for annotation_alias in annotations: | |
| 726 | if lookup_str.startswith(annotation_alias + LOOKUP_SEP) or lookup_str == annotation_alias: | |
| 727 | used.add(annotation_alias) | |
| 728 | return used | |
| 729 | ||
| 556 | 730 | def has_filters(self): |
| 557 | 731 | return self.where |
| 558 | 732 |
| Test Name | Status |
|---|---|
test_non_aggregate_annotation_pruned (aggregation.tests.AggregateAnnotationPruningTests) | Fail |
test_unreferenced_aggregate_annotation_pruned (aggregation.tests.AggregateAnnotationPruningTests) | Fail |
test_unused_aliased_aggregate_pruned (aggregation.tests.AggregateAnnotationPruningTests) | Pass |
test_referenced_aggregate_annotation_kept (aggregation.tests.AggregateAnnotationPruningTests) | Pass |
test_add_implementation (aggregation.tests.AggregateTestCase) | Pass |
test_aggregate_alias (aggregation.tests.AggregateTestCase) | Pass |
test_aggregate_annotation (aggregation.tests.AggregateTestCase) | Pass |
test_aggregate_in_order_by (aggregation.tests.AggregateTestCase) | Pass |
test_aggregate_join_transform (aggregation.tests.AggregateTestCase) | Pass |
test_aggregate_multi_join (aggregation.tests.AggregateTestCase) | Pass |
test_aggregate_over_aggregate (aggregation.tests.AggregateTestCase) | Pass |
test_aggregate_over_complex_annotation (aggregation.tests.AggregateTestCase) | Pass |
test_aggregate_transform (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_after_annotation (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_compound_expression (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_expression (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_group_by (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_integer (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_not_in_aggregate (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_passed_another_aggregate (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_unset (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_unsupported_by_count (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_using_date_from_database (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_using_date_from_python (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_using_datetime_from_database (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_using_datetime_from_python (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_using_decimal_from_database (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_using_decimal_from_python (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_using_duration_from_database (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_using_duration_from_python (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_using_time_from_database (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_using_time_from_python (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_default_zero (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_exists_annotation (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_exists_multivalued_outeref (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_expressions (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_filter_exists (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_nested_subquery_outerref (aggregation.tests.AggregateTestCase) | Pass |
test_aggregation_order_by_not_selected_annotation_values (aggregation.tests.AggregateTestCase) | Pass |
Random() is not included in the GROUP BY when used for ordering. | Pass |
Subquery annotations are excluded from the GROUP BY if they are | Pass |
test_aggregation_subquery_annotation_exists (aggregation.tests.AggregateTestCase) | Pass |
Subquery annotations must be included in the GROUP BY if they use | Pass |
test_aggregation_subquery_annotation_related_field (aggregation.tests.AggregateTestCase) | Pass |
Subquery annotations and external aliases are excluded from the GROUP | Pass |
test_aggregation_subquery_annotation_values_collision (aggregation.tests.AggregateTestCase) | Pass |
test_alias_sql_injection (aggregation.tests.AggregateTestCase) | Pass |
test_annotate_basic (aggregation.tests.AggregateTestCase) | Pass |
test_annotate_defer (aggregation.tests.AggregateTestCase) | Pass |
test_annotate_defer_select_related (aggregation.tests.AggregateTestCase) | Pass |
test_annotate_m2m (aggregation.tests.AggregateTestCase) | Pass |
test_annotate_ordering (aggregation.tests.AggregateTestCase) | Pass |
test_annotate_over_annotate (aggregation.tests.AggregateTestCase) | Pass |
test_annotate_values (aggregation.tests.AggregateTestCase) | Pass |
test_annotate_values_aggregate (aggregation.tests.AggregateTestCase) | Pass |
test_annotate_values_list (aggregation.tests.AggregateTestCase) | Pass |
test_annotated_aggregate_over_annotated_aggregate (aggregation.tests.AggregateTestCase) | Pass |
test_annotation (aggregation.tests.AggregateTestCase) | Pass |
test_annotation_expressions (aggregation.tests.AggregateTestCase) | Pass |
test_arguments_must_be_expressions (aggregation.tests.AggregateTestCase) | Pass |
test_avg_decimal_field (aggregation.tests.AggregateTestCase) | Pass |
test_avg_duration_field (aggregation.tests.AggregateTestCase) | Pass |
test_backwards_m2m_annotate (aggregation.tests.AggregateTestCase) | Pass |
test_coalesced_empty_result_set (aggregation.tests.AggregateTestCase) | Pass |
test_combine_different_types (aggregation.tests.AggregateTestCase) | Pass |
test_complex_aggregations_require_kwarg (aggregation.tests.AggregateTestCase) | Pass |
test_complex_values_aggregation (aggregation.tests.AggregateTestCase) | Pass |
test_count (aggregation.tests.AggregateTestCase) | Pass |
test_count_distinct_expression (aggregation.tests.AggregateTestCase) | Pass |
test_count_star (aggregation.tests.AggregateTestCase) | Pass |
.dates() returns a distinct set of dates when applied to a | Pass |
test_decimal_max_digits_has_no_effect (aggregation.tests.AggregateTestCase) | Pass |
test_distinct_on_aggregate (aggregation.tests.AggregateTestCase) | Pass |
test_empty_aggregate (aggregation.tests.AggregateTestCase) | Pass |
test_empty_result_optimization (aggregation.tests.AggregateTestCase) | Pass |
test_even_more_aggregate (aggregation.tests.AggregateTestCase) | Pass |
test_exists_extra_where_with_aggregate (aggregation.tests.AggregateTestCase) | Pass |
test_exists_none_with_aggregate (aggregation.tests.AggregateTestCase) | Pass |
test_expression_on_aggregation (aggregation.tests.AggregateTestCase) | Pass |
test_filter_aggregate (aggregation.tests.AggregateTestCase) | Pass |
Filtering against an aggregate requires the usage of the HAVING clause. | Pass |
test_filtering (aggregation.tests.AggregateTestCase) | Pass |
test_fkey_aggregate (aggregation.tests.AggregateTestCase) | Pass |
Exists annotations are included in the GROUP BY if they are | Pass |
Subquery annotations are included in the GROUP BY if they are | Pass |
An annotation included in values() before an aggregate should be | Pass |
test_more_aggregation (aggregation.tests.AggregateTestCase) | Pass |
test_multi_arg_aggregate (aggregation.tests.AggregateTestCase) | Pass |
test_multiple_aggregates (aggregation.tests.AggregateTestCase) | Pass |
An annotation not included in values() before an aggregate should be | Pass |
test_nonaggregate_aggregation_throws (aggregation.tests.AggregateTestCase) | Pass |
test_nonfield_annotation (aggregation.tests.AggregateTestCase) | Pass |
test_order_of_precedence (aggregation.tests.AggregateTestCase) | Pass |
test_related_aggregate (aggregation.tests.AggregateTestCase) | Pass |
test_reverse_fkey_annotate (aggregation.tests.AggregateTestCase) | Pass |
test_single_aggregate (aggregation.tests.AggregateTestCase) | Pass |
Sum on a distinct() QuerySet should aggregate only the distinct items. | Pass |
test_sum_duration_field (aggregation.tests.AggregateTestCase) | Pass |
Subqueries do not needlessly contain ORDER BY, SELECT FOR UPDATE or | Pass |
Aggregation over sliced queryset works correctly. | Pass |
Doing exclude() on a foreign model after annotate() doesn't crash. | Pass |
test_values_aggregation (aggregation.tests.AggregateTestCase) | Pass |
test_values_annotation_with_expression (aggregation.tests.AggregateTestCase) | Pass |
Loading...
Ridges.AI© 2025 Ridges AI. Building the future of decentralized AI development.
