@@ -567,3 +567,333 @@ def test_or_with_mixed_pushable_and_non_pushable_fields(self):
567567 {"$match" : {"$or" : [{"queries__reader.name" : "Alice" }, {"name" : "Central" }]}},
568568 ],
569569 )
570+
571+ def test_double_negation_pushdown (self ):
572+ a1 = Author .objects .create (name = "Alice" )
573+ a2 = Author .objects .create (name = "Bob" )
574+ b1 = Book .objects .create (title = "Book1" , author = a1 , isbn = "111" )
575+ Book .objects .create (title = "Book2" , author = a2 , isbn = "222" )
576+ b3 = Book .objects .create (title = "Book3" , author = a1 , isbn = "333" )
577+ expected = [b1 , b3 ]
578+ with self .assertNumQueries (1 ) as ctx :
579+ self .assertSequenceEqual (
580+ Book .objects .filter (~ (~ models .Q (author__name = "Alice" ) | models .Q (title = "Book4" ))),
581+ expected ,
582+ )
583+ self .assertAggregateQuery (
584+ ctx .captured_queries [0 ]["sql" ],
585+ "queries__book" ,
586+ [
587+ {
588+ "$lookup" : {
589+ "from" : "queries__author" ,
590+ "let" : {"parent__field__0" : "$author_id" },
591+ "pipeline" : [
592+ {
593+ "$match" : {
594+ "$and" : [
595+ {
596+ "$expr" : {
597+ "$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]
598+ }
599+ },
600+ {"name" : "Alice" },
601+ ]
602+ }
603+ }
604+ ],
605+ "as" : "queries__author" ,
606+ }
607+ },
608+ {"$unwind" : "$queries__author" },
609+ {
610+ "$match" : {
611+ "$nor" : [
612+ {
613+ "$or" : [
614+ {"$nor" : [{"queries__author.name" : "Alice" }]},
615+ {"title" : "Book4" },
616+ ]
617+ }
618+ ]
619+ }
620+ },
621+ ],
622+ )
623+
624+ def test_partial_or_pushdown (self ):
625+ a1 = Author .objects .create (name = "Alice" )
626+ a2 = Author .objects .create (name = "Bob" )
627+ a3 = Author .objects .create (name = "Charlie" )
628+ b1 = Book .objects .create (title = "B1" , author = a1 , isbn = "111" )
629+ b2 = Book .objects .create (title = "B2" , author = a2 , isbn = "111" )
630+ Book .objects .create (title = "B3" , author = a3 , isbn = "222" )
631+ condition = models .Q (author__name = "Alice" ) | (
632+ models .Q (author__name = "Bob" ) & models .Q (isbn = "111" )
633+ )
634+ expected = [b1 , b2 ]
635+ with self .assertNumQueries (1 ) as ctx :
636+ self .assertSequenceEqual (list (Book .objects .filter (condition )), expected )
637+ self .assertAggregateQuery (
638+ ctx .captured_queries [0 ]["sql" ],
639+ "queries__book" ,
640+ [
641+ {
642+ "$lookup" : {
643+ "as" : "queries__author" ,
644+ "from" : "queries__author" ,
645+ "let" : {"parent__field__0" : "$author_id" },
646+ "pipeline" : [
647+ {
648+ "$match" : {
649+ "$and" : [
650+ {
651+ "$expr" : {
652+ "$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]
653+ }
654+ },
655+ {"$or" : [{"name" : "Alice" }, {"name" : "Bob" }]},
656+ ]
657+ }
658+ }
659+ ],
660+ }
661+ },
662+ {"$unwind" : "$queries__author" },
663+ {
664+ "$match" : {
665+ "$or" : [
666+ {"queries__author.name" : "Alice" },
667+ {"$and" : [{"queries__author.name" : "Bob" }, {"isbn" : "111" }]},
668+ ]
669+ }
670+ },
671+ ],
672+ )
673+
674+ def test_multiple_ors_with_partial_pushdown (self ):
675+ a1 = Author .objects .create (name = "Alice" )
676+ a2 = Author .objects .create (name = "Bob" )
677+ a3 = Author .objects .create (name = "Charlie" )
678+ a4 = Author .objects .create (name = "David" )
679+ b1 = Book .objects .create (title = "B1" , author = a1 , isbn = "111" )
680+ b2 = Book .objects .create (title = "B2" , author = a1 , isbn = "222" )
681+ b3 = Book .objects .create (title = "B3" , author = a2 , isbn = "333" )
682+ b4 = Book .objects .create (title = "B4" , author = a3 , isbn = "333" )
683+ Book .objects .create (title = "B5" , author = a4 , isbn = "444" )
684+
685+ left = models .Q (author__name = "Alice" ) & (models .Q (isbn = "111" ) | models .Q (isbn = "222" ))
686+ right = (models .Q (author__name = "Bob" ) | models .Q (author__name = "Charlie" )) & models .Q (
687+ isbn = "333"
688+ )
689+ condition = left | right
690+
691+ expected = [b1 , b2 , b3 , b4 ]
692+ with self .assertNumQueries (1 ) as ctx :
693+ self .assertSequenceEqual (list (Book .objects .filter (condition )), expected )
694+
695+ self .assertAggregateQuery (
696+ ctx .captured_queries [0 ]["sql" ],
697+ "queries__book" ,
698+ [
699+ {
700+ "$lookup" : {
701+ "as" : "queries__author" ,
702+ "from" : "queries__author" ,
703+ "let" : {"parent__field__0" : "$author_id" },
704+ "pipeline" : [
705+ {
706+ "$match" : {
707+ "$and" : [
708+ {
709+ "$expr" : {
710+ "$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]
711+ }
712+ },
713+ {
714+ "$or" : [
715+ {"name" : "Alice" },
716+ {"$or" : [{"name" : "Bob" }, {"name" : "Charlie" }]},
717+ ]
718+ },
719+ ]
720+ }
721+ }
722+ ],
723+ }
724+ },
725+ {"$unwind" : "$queries__author" },
726+ {
727+ "$match" : {
728+ "$or" : [
729+ {
730+ "$and" : [
731+ {"queries__author.name" : "Alice" },
732+ {"$or" : [{"isbn" : "111" }, {"isbn" : "222" }]},
733+ ]
734+ },
735+ {
736+ "$and" : [
737+ {
738+ "$or" : [
739+ {"queries__author.name" : "Bob" },
740+ {"queries__author.name" : "Charlie" },
741+ ]
742+ },
743+ {"isbn" : "333" },
744+ ]
745+ },
746+ ]
747+ }
748+ },
749+ ],
750+ )
751+
752+ def test_self_join_tag_three_levels_none_pushable (self ):
753+ t1 = Tag .objects .create (name = "T1" )
754+ t2 = Tag .objects .create (name = "T2" , parent = t1 )
755+ t3 = Tag .objects .create (name = "T3" , parent = t2 )
756+ Tag .objects .create (name = "T4" , parent = t3 )
757+ Tag .objects .create (name = "T5" , parent = t1 )
758+ t6 = Tag .objects .create (name = "T6" , parent = t2 )
759+ cond = (
760+ models .Q (name = "T1" ) | models .Q (parent__name = "T2" ) | models .Q (parent__parent__name = "T3" )
761+ )
762+ expected = [t1 , t3 , t6 ]
763+ with self .assertNumQueries (1 ) as ctx :
764+ self .assertSequenceEqual (list (Tag .objects .filter (cond )), expected )
765+ self .assertAggregateQuery (
766+ ctx .captured_queries [0 ]["sql" ],
767+ "queries__tag" ,
768+ # Django translate this kind of queries into left outer join
769+ [
770+ {
771+ "$lookup" : {
772+ "as" : "T2" ,
773+ "from" : "queries__tag" ,
774+ "let" : {"parent__field__0" : "$parent_id" },
775+ "pipeline" : [
776+ {
777+ "$match" : {
778+ "$expr" : {"$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]}
779+ }
780+ }
781+ ],
782+ }
783+ },
784+ {
785+ "$set" : {
786+ "T2" : {
787+ "$cond" : {
788+ "else" : "$T2" ,
789+ "if" : {
790+ "$or" : [
791+ {"$eq" : [{"$type" : "$T2" }, "missing" ]},
792+ {"$eq" : [{"$size" : "$T2" }, 0 ]},
793+ ]
794+ },
795+ "then" : [{}],
796+ }
797+ }
798+ }
799+ },
800+ {"$unwind" : "$T2" },
801+ {
802+ "$lookup" : {
803+ "as" : "T3" ,
804+ "from" : "queries__tag" ,
805+ "let" : {"parent__field__0" : "$T2.parent_id" },
806+ "pipeline" : [
807+ {
808+ "$match" : {
809+ "$expr" : {"$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]}
810+ }
811+ }
812+ ],
813+ }
814+ },
815+ {
816+ "$set" : {
817+ "T3" : {
818+ "$cond" : {
819+ "else" : "$T3" ,
820+ "if" : {
821+ "$or" : [
822+ {"$eq" : [{"$type" : "$T3" }, "missing" ]},
823+ {"$eq" : [{"$size" : "$T3" }, 0 ]},
824+ ]
825+ },
826+ "then" : [{}],
827+ }
828+ }
829+ }
830+ },
831+ {"$unwind" : "$T3" },
832+ {"$match" : {"$or" : [{"name" : "T1" }, {"T2.name" : "T2" }, {"T3.name" : "T3" }]}},
833+ ],
834+ )
835+
836+ def test_self_join_tag_three_levels_pushable (self ):
837+ t1 = Tag .objects .create (name = "T1" )
838+ t2 = Tag .objects .create (name = "T2" , parent = t1 )
839+ t3 = Tag .objects .create (name = "T3" , parent = t2 )
840+ Tag .objects .create (name = "T4" , parent = t3 )
841+ Tag .objects .create (name = "T5" , parent = t1 )
842+ Tag .objects .create (name = "T6" , parent = t2 )
843+ with self .assertNumQueries (1 ) as ctx :
844+ self .assertSequenceEqual (
845+ list (Tag .objects .filter (name = "T1" , parent__name = "T2" , parent__parent__name = "T3" )),
846+ [],
847+ )
848+
849+ self .assertAggregateQuery (
850+ ctx .captured_queries [0 ]["sql" ],
851+ "queries__tag" ,
852+ [
853+ {
854+ "$lookup" : {
855+ "as" : "T2" ,
856+ "from" : "queries__tag" ,
857+ "let" : {"parent__field__0" : "$parent_id" },
858+ "pipeline" : [
859+ {
860+ "$match" : {
861+ "$and" : [
862+ {
863+ "$expr" : {
864+ "$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]
865+ }
866+ },
867+ {"name" : "T2" },
868+ ]
869+ }
870+ }
871+ ],
872+ }
873+ },
874+ {"$unwind" : "$T2" },
875+ {
876+ "$lookup" : {
877+ "as" : "T3" ,
878+ "from" : "queries__tag" ,
879+ "let" : {"parent__field__0" : "$T2.parent_id" },
880+ "pipeline" : [
881+ {
882+ "$match" : {
883+ "$and" : [
884+ {
885+ "$expr" : {
886+ "$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]
887+ }
888+ },
889+ {"name" : "T3" },
890+ ]
891+ }
892+ }
893+ ],
894+ }
895+ },
896+ {"$unwind" : "$T3" },
897+ {"$match" : {"$and" : [{"name" : "T1" }, {"T2.name" : "T2" }, {"T3.name" : "T3" }]}},
898+ ],
899+ )
0 commit comments