@@ -749,3 +749,333 @@ def test_or_with_mixed_pushable_and_non_pushable_fields(self):
749749 {"$match" : {"$or" : [{"queries__reader.name" : "Alice" }, {"name" : "Central" }]}},
750750 ],
751751 )
752+
753+ def test_double_negation_pushdown (self ):
754+ a1 = Author .objects .create (name = "Alice" )
755+ a2 = Author .objects .create (name = "Bob" )
756+ b1 = Book .objects .create (title = "Book1" , author = a1 , isbn = "111" )
757+ Book .objects .create (title = "Book2" , author = a2 , isbn = "222" )
758+ b3 = Book .objects .create (title = "Book3" , author = a1 , isbn = "333" )
759+ expected = [b1 , b3 ]
760+ with self .assertNumQueries (1 ) as ctx :
761+ self .assertSequenceEqual (
762+ Book .objects .filter (~ (~ models .Q (author__name = "Alice" ) | models .Q (title = "Book4" ))),
763+ expected ,
764+ )
765+ self .assertAggregateQuery (
766+ ctx .captured_queries [0 ]["sql" ],
767+ "queries__book" ,
768+ [
769+ {
770+ "$lookup" : {
771+ "from" : "queries__author" ,
772+ "let" : {"parent__field__0" : "$author_id" },
773+ "pipeline" : [
774+ {
775+ "$match" : {
776+ "$and" : [
777+ {
778+ "$expr" : {
779+ "$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]
780+ }
781+ },
782+ {"name" : "Alice" },
783+ ]
784+ }
785+ }
786+ ],
787+ "as" : "queries__author" ,
788+ }
789+ },
790+ {"$unwind" : "$queries__author" },
791+ {
792+ "$match" : {
793+ "$nor" : [
794+ {
795+ "$or" : [
796+ {"$nor" : [{"queries__author.name" : "Alice" }]},
797+ {"title" : "Book4" },
798+ ]
799+ }
800+ ]
801+ }
802+ },
803+ ],
804+ )
805+
806+ def test_partial_or_pushdown (self ):
807+ a1 = Author .objects .create (name = "Alice" )
808+ a2 = Author .objects .create (name = "Bob" )
809+ a3 = Author .objects .create (name = "Charlie" )
810+ b1 = Book .objects .create (title = "B1" , author = a1 , isbn = "111" )
811+ b2 = Book .objects .create (title = "B2" , author = a2 , isbn = "111" )
812+ Book .objects .create (title = "B3" , author = a3 , isbn = "222" )
813+ condition = models .Q (author__name = "Alice" ) | (
814+ models .Q (author__name = "Bob" ) & models .Q (isbn = "111" )
815+ )
816+ expected = [b1 , b2 ]
817+ with self .assertNumQueries (1 ) as ctx :
818+ self .assertSequenceEqual (list (Book .objects .filter (condition )), expected )
819+ self .assertAggregateQuery (
820+ ctx .captured_queries [0 ]["sql" ],
821+ "queries__book" ,
822+ [
823+ {
824+ "$lookup" : {
825+ "as" : "queries__author" ,
826+ "from" : "queries__author" ,
827+ "let" : {"parent__field__0" : "$author_id" },
828+ "pipeline" : [
829+ {
830+ "$match" : {
831+ "$and" : [
832+ {
833+ "$expr" : {
834+ "$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]
835+ }
836+ },
837+ {"$or" : [{"name" : "Alice" }, {"name" : "Bob" }]},
838+ ]
839+ }
840+ }
841+ ],
842+ }
843+ },
844+ {"$unwind" : "$queries__author" },
845+ {
846+ "$match" : {
847+ "$or" : [
848+ {"queries__author.name" : "Alice" },
849+ {"$and" : [{"queries__author.name" : "Bob" }, {"isbn" : "111" }]},
850+ ]
851+ }
852+ },
853+ ],
854+ )
855+
856+ def test_multiple_ors_with_partial_pushdown (self ):
857+ a1 = Author .objects .create (name = "Alice" )
858+ a2 = Author .objects .create (name = "Bob" )
859+ a3 = Author .objects .create (name = "Charlie" )
860+ a4 = Author .objects .create (name = "David" )
861+ b1 = Book .objects .create (title = "B1" , author = a1 , isbn = "111" )
862+ b2 = Book .objects .create (title = "B2" , author = a1 , isbn = "222" )
863+ b3 = Book .objects .create (title = "B3" , author = a2 , isbn = "333" )
864+ b4 = Book .objects .create (title = "B4" , author = a3 , isbn = "333" )
865+ Book .objects .create (title = "B5" , author = a4 , isbn = "444" )
866+
867+ left = models .Q (author__name = "Alice" ) & (models .Q (isbn = "111" ) | models .Q (isbn = "222" ))
868+ right = (models .Q (author__name = "Bob" ) | models .Q (author__name = "Charlie" )) & models .Q (
869+ isbn = "333"
870+ )
871+ condition = left | right
872+
873+ expected = [b1 , b2 , b3 , b4 ]
874+ with self .assertNumQueries (1 ) as ctx :
875+ self .assertSequenceEqual (list (Book .objects .filter (condition )), expected )
876+
877+ self .assertAggregateQuery (
878+ ctx .captured_queries [0 ]["sql" ],
879+ "queries__book" ,
880+ [
881+ {
882+ "$lookup" : {
883+ "as" : "queries__author" ,
884+ "from" : "queries__author" ,
885+ "let" : {"parent__field__0" : "$author_id" },
886+ "pipeline" : [
887+ {
888+ "$match" : {
889+ "$and" : [
890+ {
891+ "$expr" : {
892+ "$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]
893+ }
894+ },
895+ {
896+ "$or" : [
897+ {"name" : "Alice" },
898+ {"$or" : [{"name" : "Bob" }, {"name" : "Charlie" }]},
899+ ]
900+ },
901+ ]
902+ }
903+ }
904+ ],
905+ }
906+ },
907+ {"$unwind" : "$queries__author" },
908+ {
909+ "$match" : {
910+ "$or" : [
911+ {
912+ "$and" : [
913+ {"queries__author.name" : "Alice" },
914+ {"$or" : [{"isbn" : "111" }, {"isbn" : "222" }]},
915+ ]
916+ },
917+ {
918+ "$and" : [
919+ {
920+ "$or" : [
921+ {"queries__author.name" : "Bob" },
922+ {"queries__author.name" : "Charlie" },
923+ ]
924+ },
925+ {"isbn" : "333" },
926+ ]
927+ },
928+ ]
929+ }
930+ },
931+ ],
932+ )
933+
934+ def test_self_join_tag_three_levels_none_pushable (self ):
935+ t1 = Tag .objects .create (name = "T1" )
936+ t2 = Tag .objects .create (name = "T2" , parent = t1 )
937+ t3 = Tag .objects .create (name = "T3" , parent = t2 )
938+ Tag .objects .create (name = "T4" , parent = t3 )
939+ Tag .objects .create (name = "T5" , parent = t1 )
940+ t6 = Tag .objects .create (name = "T6" , parent = t2 )
941+ cond = (
942+ models .Q (name = "T1" ) | models .Q (parent__name = "T2" ) | models .Q (parent__parent__name = "T3" )
943+ )
944+ expected = [t1 , t3 , t6 ]
945+ with self .assertNumQueries (1 ) as ctx :
946+ self .assertSequenceEqual (list (Tag .objects .filter (cond )), expected )
947+ self .assertAggregateQuery (
948+ ctx .captured_queries [0 ]["sql" ],
949+ "queries__tag" ,
950+ # Django translate this kind of queries into left outer join
951+ [
952+ {
953+ "$lookup" : {
954+ "as" : "T2" ,
955+ "from" : "queries__tag" ,
956+ "let" : {"parent__field__0" : "$parent_id" },
957+ "pipeline" : [
958+ {
959+ "$match" : {
960+ "$expr" : {"$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]}
961+ }
962+ }
963+ ],
964+ }
965+ },
966+ {
967+ "$set" : {
968+ "T2" : {
969+ "$cond" : {
970+ "else" : "$T2" ,
971+ "if" : {
972+ "$or" : [
973+ {"$eq" : [{"$type" : "$T2" }, "missing" ]},
974+ {"$eq" : [{"$size" : "$T2" }, 0 ]},
975+ ]
976+ },
977+ "then" : [{}],
978+ }
979+ }
980+ }
981+ },
982+ {"$unwind" : "$T2" },
983+ {
984+ "$lookup" : {
985+ "as" : "T3" ,
986+ "from" : "queries__tag" ,
987+ "let" : {"parent__field__0" : "$T2.parent_id" },
988+ "pipeline" : [
989+ {
990+ "$match" : {
991+ "$expr" : {"$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]}
992+ }
993+ }
994+ ],
995+ }
996+ },
997+ {
998+ "$set" : {
999+ "T3" : {
1000+ "$cond" : {
1001+ "else" : "$T3" ,
1002+ "if" : {
1003+ "$or" : [
1004+ {"$eq" : [{"$type" : "$T3" }, "missing" ]},
1005+ {"$eq" : [{"$size" : "$T3" }, 0 ]},
1006+ ]
1007+ },
1008+ "then" : [{}],
1009+ }
1010+ }
1011+ }
1012+ },
1013+ {"$unwind" : "$T3" },
1014+ {"$match" : {"$or" : [{"name" : "T1" }, {"T2.name" : "T2" }, {"T3.name" : "T3" }]}},
1015+ ],
1016+ )
1017+
1018+ def test_self_join_tag_three_levels_pushable (self ):
1019+ t1 = Tag .objects .create (name = "T1" )
1020+ t2 = Tag .objects .create (name = "T2" , parent = t1 )
1021+ t3 = Tag .objects .create (name = "T3" , parent = t2 )
1022+ Tag .objects .create (name = "T4" , parent = t3 )
1023+ Tag .objects .create (name = "T5" , parent = t1 )
1024+ Tag .objects .create (name = "T6" , parent = t2 )
1025+ with self .assertNumQueries (1 ) as ctx :
1026+ self .assertSequenceEqual (
1027+ list (Tag .objects .filter (name = "T1" , parent__name = "T2" , parent__parent__name = "T3" )),
1028+ [],
1029+ )
1030+
1031+ self .assertAggregateQuery (
1032+ ctx .captured_queries [0 ]["sql" ],
1033+ "queries__tag" ,
1034+ [
1035+ {
1036+ "$lookup" : {
1037+ "as" : "T2" ,
1038+ "from" : "queries__tag" ,
1039+ "let" : {"parent__field__0" : "$parent_id" },
1040+ "pipeline" : [
1041+ {
1042+ "$match" : {
1043+ "$and" : [
1044+ {
1045+ "$expr" : {
1046+ "$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]
1047+ }
1048+ },
1049+ {"name" : "T2" },
1050+ ]
1051+ }
1052+ }
1053+ ],
1054+ }
1055+ },
1056+ {"$unwind" : "$T2" },
1057+ {
1058+ "$lookup" : {
1059+ "as" : "T3" ,
1060+ "from" : "queries__tag" ,
1061+ "let" : {"parent__field__0" : "$T2.parent_id" },
1062+ "pipeline" : [
1063+ {
1064+ "$match" : {
1065+ "$and" : [
1066+ {
1067+ "$expr" : {
1068+ "$and" : [{"$eq" : ["$$parent__field__0" , "$_id" ]}]
1069+ }
1070+ },
1071+ {"name" : "T3" },
1072+ ]
1073+ }
1074+ }
1075+ ],
1076+ }
1077+ },
1078+ {"$unwind" : "$T3" },
1079+ {"$match" : {"$and" : [{"name" : "T1" }, {"T2.name" : "T2" }, {"T3.name" : "T3" }]}},
1080+ ],
1081+ )
0 commit comments