@@ -6862,6 +6862,182 @@ def test_exists_compound_predicate(self):
68626862 self .assertEqual ([u .username for u in query ], ['u1' , 'u2' ])
68636863
68646864
6865+ class OJOrg (TestModel ):
6866+ name = TextField ()
6867+
6868+
6869+ class OJUser (TestModel ):
6870+ org = ForeignKeyField (OJOrg , null = True )
6871+ name = TextField ()
6872+
6873+
6874+ class OJNote (TestModel ):
6875+ author = ForeignKeyField (OJUser , null = True )
6876+ content = TextField ()
6877+
6878+
6879+ class OJNoteNoFK (TestModel ):
6880+ # Same table as OJNote, but the author column is a plain integer, so the
6881+ # join attribute is not a field descriptor.
6882+ author_id = IntegerField (null = True )
6883+ content = TextField ()
6884+ class Meta :
6885+ table_name = 'oj_note'
6886+
6887+
6888+ class TestOuterJoinHydratesNone (ModelTestCase ):
6889+ requires = [OJOrg , OJUser , OJNote ]
6890+
6891+ def setUp (self ):
6892+ super (TestOuterJoinHydratesNone , self ).setUp ()
6893+ acme = OJOrg .create (name = 'acme' )
6894+ huey = OJUser .create (org = acme , name = 'huey' )
6895+ zaizee = OJUser .create (org = None , name = 'zaizee' ) # No org.
6896+ OJUser .create (org = acme , name = 'mickey' ) # No notes.
6897+ OJNote .create (author = huey , content = 'meow' )
6898+ OJNote .create (author = None , content = '???' ) # No author.
6899+ OJNote .create (author = zaizee , content = 'zzz' )
6900+
6901+ def assertNotes (self , query , expected , attr = 'author' , model = OJNote ):
6902+ # Populate the relation as None.
6903+ with self .assertQueryCount (1 ):
6904+ accum = []
6905+ for note in query .order_by (model .id ):
6906+ rel = getattr (note , attr )
6907+ accum .append ((note .content ,
6908+ rel .name if rel is not None else None ))
6909+ self .assertEqual (accum , expected )
6910+
6911+ def test_join_fk (self ):
6912+ expected = [('meow' , 'huey' ), ('???' , None ), ('zzz' , 'zaizee' )]
6913+ self .assertNotes (
6914+ OJNote .select (OJNote , OJUser ).join (OJUser , JOIN .LEFT_OUTER ),
6915+ expected )
6916+
6917+ UA = OJUser .alias ()
6918+ query = OJNote .select (OJNote , UA ).join (UA , JOIN .LEFT_OUTER )
6919+ self .assertNotes (query , expected )
6920+
6921+ # The subquery holds only huey, so zaizee's note misses as well.
6922+ subq = OJUser .select ().where (OJUser .name == 'huey' )
6923+ query = (OJNote .select (OJNote , subq .c .id , subq .c .name )
6924+ .join (subq , JOIN .LEFT_OUTER , on = OJNote .author == subq .c .id ))
6925+ self .assertNotes (
6926+ query ,
6927+ [('meow' , 'huey' ), ('???' , None ), ('zzz' , None )])
6928+
6929+ def test_join_custom_attr (self ):
6930+ # attr= is a plain attribute, not a field descriptor.
6931+ expected = [('meow' , 'huey' ), ('???' , None ), ('zzz' , 'zaizee' )]
6932+
6933+ query = (OJNote .select (OJNote , OJUser )
6934+ .join (OJUser , JOIN .LEFT_OUTER , attr = 'u' ))
6935+ self .assertNotes (query , expected , attr = 'u' )
6936+
6937+ UA = OJUser .alias ()
6938+ query = OJNote .select (OJNote , UA ).join (UA , JOIN .LEFT_OUTER , attr = 'u' )
6939+ self .assertNotes (query , expected , attr = 'u' )
6940+
6941+ subq = OJUser .select ().where (OJUser .name == 'huey' )
6942+ query = (OJNote .select (OJNote , subq .c .id , subq .c .name )
6943+ .join (subq , JOIN .LEFT_OUTER , on = OJNote .author == subq .c .id ,
6944+ attr = 'u' ))
6945+ self .assertNotes (
6946+ query ,
6947+ [('meow' , 'huey' ), ('???' , None ), ('zzz' , None )],
6948+ attr = 'u' )
6949+
6950+ def test_join_without_fk (self ):
6951+ expected = [('meow' , 'huey' ), ('???' , None ), ('zzz' , 'zaizee' )]
6952+
6953+ query = (OJNoteNoFK .select (OJNoteNoFK , OJUser )
6954+ .join (OJUser , JOIN .LEFT_OUTER ,
6955+ on = OJNoteNoFK .author_id == OJUser .id , attr = 'author' ))
6956+ self .assertNotes (query , expected , model = OJNoteNoFK )
6957+
6958+ UA = OJUser .alias ()
6959+ query = (OJNoteNoFK .select (OJNoteNoFK , UA )
6960+ .join (UA , JOIN .LEFT_OUTER , on = OJNoteNoFK .author_id == UA .id ,
6961+ attr = 'author' ))
6962+ self .assertNotes (query , expected , model = OJNoteNoFK )
6963+
6964+ subq = OJUser .select ().where (OJUser .name == 'huey' )
6965+ query = (OJNoteNoFK .select (OJNoteNoFK , subq .c .id , subq .c .name )
6966+ .join (subq , JOIN .LEFT_OUTER ,
6967+ on = OJNoteNoFK .author_id == subq .c .id , attr = 'author' ))
6968+ self .assertNotes (
6969+ query ,
6970+ [('meow' , 'huey' ), ('???' , None ), ('zzz' , None )],
6971+ model = OJNoteNoFK )
6972+
6973+ def assertUsers (self , query , attr = 'ojnote' ):
6974+ with self .assertQueryCount (1 ):
6975+ accum = []
6976+ for user in query .order_by (OJUser .id ):
6977+ note = getattr (user , attr )
6978+ accum .append ((user .name ,
6979+ note .content if note is not None else None ))
6980+ self .assertEqual (accum , [('huey' , 'meow' ), ('zaizee' , 'zzz' ),
6981+ ('mickey' , None )])
6982+
6983+ def test_join_backref (self ):
6984+ # The foreign key is on the right-hand model, so the attribute
6985+ # defaults to the model name and is not a descriptor on OJUser.
6986+ self .assertUsers (
6987+ OJUser .select (OJUser , OJNote ).join (OJNote , JOIN .LEFT_OUTER ))
6988+
6989+ NA = OJNote .alias ()
6990+ self .assertUsers (
6991+ OJUser .select (OJUser , NA ).join (NA , JOIN .LEFT_OUTER ))
6992+
6993+ query = (OJUser .select (OJUser , OJNote )
6994+ .join (OJNote , JOIN .LEFT_OUTER , attr = 'n' ))
6995+ self .assertUsers (query , attr = 'n' )
6996+
6997+ def assertOrgs (self , query , expected , model = OJNote ):
6998+ with self .assertQueryCount (1 ):
6999+ accum = []
7000+ for note in query .order_by (model .id ):
7001+ author = note .author
7002+ org = author .org if author is not None else None
7003+ accum .append ((
7004+ note .content ,
7005+ author .name if author is not None else None ,
7006+ org .name if org is not None else None ))
7007+ self .assertEqual (accum , expected )
7008+
7009+ def test_multi_hop (self ):
7010+ # "???" has no author at all (the intervening model is absent), and
7011+ # "zzz" has an author with no org (the leaf is absent).
7012+ expected = [('meow' , 'huey' , 'acme' ), ('???' , None , None ),
7013+ ('zzz' , 'zaizee' , None )]
7014+ self .assertOrgs (
7015+ (OJNote .select (OJNote , OJUser , OJOrg )
7016+ .join (OJUser , JOIN .LEFT_OUTER )
7017+ .join (OJOrg , JOIN .LEFT_OUTER )), expected )
7018+
7019+ UA = OJUser .alias ()
7020+ self .assertOrgs (
7021+ (OJNote .select (OJNote , UA , OJOrg )
7022+ .join (UA , JOIN .LEFT_OUTER )
7023+ .join (OJOrg , JOIN .LEFT_OUTER )), expected )
7024+
7025+ u_subq = OJUser .select ().where (OJUser .name == 'huey' )
7026+ o_subq = OJOrg .select ()
7027+ query = (OJNoteNoFK
7028+ .select (OJNoteNoFK , u_subq .c .id , u_subq .c .name ,
7029+ u_subq .c .org_id , o_subq .c .id , o_subq .c .name )
7030+ .join (u_subq , JOIN .LEFT_OUTER ,
7031+ on = OJNoteNoFK .author_id == u_subq .c .id , attr = 'author' )
7032+ .join (o_subq , JOIN .LEFT_OUTER ,
7033+ on = u_subq .c .org_id == o_subq .c .id , attr = 'org' ))
7034+ self .assertOrgs (
7035+ query ,
7036+ [('meow' , 'huey' , 'acme' ), ('???' , None , None ),
7037+ ('zzz' , None , None )],
7038+ model = OJNoteNoFK )
7039+
7040+
68657041class TestLikeColumnValue (ModelTestCase ):
68667042 requires = [User , Tweet ]
68677043
0 commit comments