|
8 | 8 |
|
9 | 9 | from core.models import DataSource, ObjectType |
10 | 10 | from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup |
11 | | -from extras.models import ConfigContext, ConfigContextProfile, ConfigTemplate, ImageAttachment, Tag |
| 11 | +from extras.models import ConfigContext, ConfigContextProfile, ConfigTemplate, ImageAttachment, Tag, TaggedItem |
12 | 12 | from tenancy.models import Tenant, TenantGroup |
13 | 13 | from utilities.exceptions import AbortRequest |
14 | 14 | from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine |
@@ -536,6 +536,34 @@ def test_virtualmachine_site_context(self): |
536 | 536 | vms[1].get_config_context() |
537 | 537 | ) |
538 | 538 |
|
| 539 | + def test_valid_local_context_data(self): |
| 540 | + device = Device.objects.first() |
| 541 | + device.local_context_data = None |
| 542 | + device.clean() |
| 543 | + |
| 544 | + device.local_context_data = {"foo": "bar"} |
| 545 | + device.clean() |
| 546 | + |
| 547 | + def test_invalid_local_context_data(self): |
| 548 | + device = Device.objects.first() |
| 549 | + |
| 550 | + device.local_context_data = "" |
| 551 | + with self.assertRaises(ValidationError): |
| 552 | + device.clean() |
| 553 | + |
| 554 | + device.local_context_data = 0 |
| 555 | + with self.assertRaises(ValidationError): |
| 556 | + device.clean() |
| 557 | + |
| 558 | + device.local_context_data = False |
| 559 | + with self.assertRaises(ValidationError): |
| 560 | + device.clean() |
| 561 | + |
| 562 | + device.local_context_data = 'foo' |
| 563 | + with self.assertRaises(ValidationError): |
| 564 | + device.clean() |
| 565 | + |
| 566 | + @tag('regression') |
539 | 567 | def test_multiple_tags_return_distinct_objects(self): |
540 | 568 | """ |
541 | 569 | Tagged items use a generic relationship, which results in duplicate rows being returned when queried. |
@@ -573,6 +601,7 @@ def test_multiple_tags_return_distinct_objects(self): |
573 | 601 | self.assertEqual(ConfigContext.objects.get_for_object(device).count(), 1) |
574 | 602 | self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context()) |
575 | 603 |
|
| 604 | + @tag('regression') |
576 | 605 | def test_multiple_tags_return_distinct_objects_with_separate_config_contexts(self): |
577 | 606 | """ |
578 | 607 | Tagged items use a generic relationship, which results in duplicate rows being returned when queried. |
@@ -621,32 +650,47 @@ def test_multiple_tags_return_distinct_objects_with_separate_config_contexts(sel |
621 | 650 | self.assertEqual(ConfigContext.objects.get_for_object(device).count(), 2) |
622 | 651 | self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context()) |
623 | 652 |
|
624 | | - def test_valid_local_context_data(self): |
625 | | - device = Device.objects.first() |
626 | | - device.local_context_data = None |
627 | | - device.clean() |
628 | | - |
629 | | - device.local_context_data = {"foo": "bar"} |
630 | | - device.clean() |
| 653 | + @tag('performance', 'regression') |
| 654 | + def test_config_context_annotation_query_optimization(self): |
| 655 | + """ |
| 656 | + Regression test for issue #20327: Ensure config context annotation |
| 657 | + doesn't use expensive DISTINCT on main query. |
631 | 658 |
|
632 | | - def test_invalid_local_context_data(self): |
| 659 | + Verifies that DISTINCT is only used in tag subquery where needed, |
| 660 | + not on the main device query which is expensive for large datasets. |
| 661 | + """ |
633 | 662 | device = Device.objects.first() |
634 | | - |
635 | | - device.local_context_data = "" |
636 | | - with self.assertRaises(ValidationError): |
637 | | - device.clean() |
638 | | - |
639 | | - device.local_context_data = 0 |
640 | | - with self.assertRaises(ValidationError): |
641 | | - device.clean() |
642 | | - |
643 | | - device.local_context_data = False |
644 | | - with self.assertRaises(ValidationError): |
645 | | - device.clean() |
646 | | - |
647 | | - device.local_context_data = 'foo' |
648 | | - with self.assertRaises(ValidationError): |
649 | | - device.clean() |
| 663 | + queryset = Device.objects.filter(pk=device.pk).annotate_config_context_data() |
| 664 | + |
| 665 | + # Main device query should NOT use DISTINCT |
| 666 | + self.assertFalse(queryset.query.distinct) |
| 667 | + |
| 668 | + # Check that tag subqueries DO use DISTINCT by inspecting the annotation |
| 669 | + config_annotation = queryset.query.annotations.get('config_context_data') |
| 670 | + self.assertIsNotNone(config_annotation) |
| 671 | + |
| 672 | + def find_tag_subqueries(where_node): |
| 673 | + """Find subqueries in WHERE clause that relate to tag filtering""" |
| 674 | + subqueries = [] |
| 675 | + |
| 676 | + def traverse(node): |
| 677 | + if hasattr(node, 'children'): |
| 678 | + for child in node.children: |
| 679 | + try: |
| 680 | + if child.rhs.query.model is TaggedItem: |
| 681 | + subqueries.append(child.rhs.query) |
| 682 | + except AttributeError: |
| 683 | + traverse(child) |
| 684 | + traverse(where_node) |
| 685 | + return subqueries |
| 686 | + |
| 687 | + # Find subqueries in the WHERE clause that should have DISTINCT |
| 688 | + tag_subqueries = find_tag_subqueries(config_annotation.query.where) |
| 689 | + distinct_subqueries = [sq for sq in tag_subqueries if sq.distinct] |
| 690 | + |
| 691 | + # Verify we found at least one DISTINCT subquery for tags |
| 692 | + self.assertEqual(len(distinct_subqueries), 1) |
| 693 | + self.assertTrue(distinct_subqueries[0].distinct) |
650 | 694 |
|
651 | 695 |
|
652 | 696 | class ConfigTemplateTest(TestCase): |
|
0 commit comments