Skip to content

Commit 956ed18

Browse files
authoredMar 29, 2025··
Merge pull request #612 from supabase/fix/inner-join-null-typing
fix(types): inner join type on nullable relationships
2 parents 8ba8745 + 29b7ec4 commit 956ed18

File tree

5 files changed

+152
-2
lines changed

5 files changed

+152
-2
lines changed
 

‎src/select-query-parser/result.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,9 @@ type ProcessEmbeddedResourceResult<
372372
TablesAndViews<Schema>[CurrentTableOrView],
373373
Resolved['relation']
374374
> extends true
375-
? ProcessedChildren | null
375+
? Field extends { innerJoin: true }
376+
? ProcessedChildren
377+
: ProcessedChildren | null
376378
: ProcessedChildren
377379
}
378380
: {

‎test/db/01-dummy-data.sql

+34-1
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,37 @@ INSERT INTO public.cornercase (id, array_column)
8686
VALUES
8787
(1, ARRAY['test', 'one']),
8888
(2, ARRAY['another']),
89-
(3, ARRAY['test2']);
89+
(3, ARRAY['test2']);
90+
91+
create table
92+
hotel (
93+
id bigint generated by default as identity primary key,
94+
name text null
95+
);
96+
97+
create table
98+
booking (
99+
id bigint generated by default as identity primary key,
100+
hotel_id bigint, -- nullable foreign key is needed to reproduce !inner supabase-js error
101+
foreign key (hotel_id) references hotel (id)
102+
);
103+
104+
-- Insert sample hotels
105+
INSERT INTO hotel (id, name)
106+
VALUES
107+
(1, 'Sunset Resort'),
108+
(2, 'Mountain View Hotel'),
109+
(3, 'Beachfront Inn'),
110+
(4, NULL);
111+
112+
-- Insert bookings with various relationship scenarios
113+
INSERT INTO booking (id, hotel_id)
114+
VALUES
115+
(1, 1), -- Valid booking for Sunset Resort
116+
(2, 1), -- Another booking for Sunset Resort (duplicate reference)
117+
(3, 2), -- Booking for Mountain View Hotel
118+
(4, NULL), -- Booking with no hotel (null reference)
119+
(5, 3), -- Booking for Beachfront Inn
120+
(6, 1), -- Third booking for Sunset Resort
121+
(7, NULL), -- Another booking with no hotel
122+
(8, 4); -- Booking for hotel with null name

‎test/relationships.ts

+63
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ export const selectParams = {
190190
select:
191191
'msgs:messages(id, ...message_details(created_at, channel!inner(id, slug, owner:users(*))))',
192192
},
193+
innerJoinOnNullableRelationship: {
194+
from: 'booking',
195+
select: 'id, hotel!inner(id, name)',
196+
},
193197
} as const
194198

195199
export const selectQueries = {
@@ -371,6 +375,9 @@ export const selectQueries = {
371375
nestedQueryWithSelectiveFieldsAndInnerJoin: postgrest
372376
.from(selectParams.nestedQueryWithSelectiveFieldsAndInnerJoin.from)
373377
.select(selectParams.nestedQueryWithSelectiveFieldsAndInnerJoin.select),
378+
innerJoinOnNullableRelationship: postgrest
379+
.from(selectParams.innerJoinOnNullableRelationship.from)
380+
.select(selectParams.innerJoinOnNullableRelationship.select),
374381
} as const
375382

376383
test('nested query with selective fields', async () => {
@@ -518,6 +525,62 @@ test('!inner relationship', async () => {
518525
`)
519526
})
520527

528+
test('!inner relationship on nullable relation', async () => {
529+
const res = await selectQueries.innerJoinOnNullableRelationship
530+
expect(res).toMatchInlineSnapshot(`
531+
Object {
532+
"count": null,
533+
"data": Array [
534+
Object {
535+
"hotel": Object {
536+
"id": 1,
537+
"name": "Sunset Resort",
538+
},
539+
"id": 1,
540+
},
541+
Object {
542+
"hotel": Object {
543+
"id": 1,
544+
"name": "Sunset Resort",
545+
},
546+
"id": 2,
547+
},
548+
Object {
549+
"hotel": Object {
550+
"id": 2,
551+
"name": "Mountain View Hotel",
552+
},
553+
"id": 3,
554+
},
555+
Object {
556+
"hotel": Object {
557+
"id": 3,
558+
"name": "Beachfront Inn",
559+
},
560+
"id": 5,
561+
},
562+
Object {
563+
"hotel": Object {
564+
"id": 1,
565+
"name": "Sunset Resort",
566+
},
567+
"id": 6,
568+
},
569+
Object {
570+
"hotel": Object {
571+
"id": 4,
572+
"name": null,
573+
},
574+
"id": 8,
575+
},
576+
],
577+
"error": null,
578+
"status": 200,
579+
"statusText": "OK",
580+
}
581+
`)
582+
})
583+
521584
test('one-to-many relationship', async () => {
522585
const res = await selectQueries.oneToMany.limit(1).single()
523586
expect(res).toMatchInlineSnapshot(`

‎test/select-query-parser/select.test-d.ts

+14
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@ type Schema = Database['public']
8585
expectType<TypeEqual<typeof result, typeof expected>>(true)
8686
}
8787

88+
// !inner relationship on nullable relation
89+
{
90+
const { data } = await selectQueries.innerJoinOnNullableRelationship
91+
let result: Exclude<typeof data, null>
92+
let expected: Array<{
93+
id: number
94+
hotel: {
95+
id: number
96+
name: string | null
97+
}
98+
}>
99+
expectType<TypeEqual<typeof result, typeof expected>>(true)
100+
}
101+
88102
// one-to-many relationship
89103
{
90104
const { data } = await selectQueries.oneToMany.limit(1).single()

‎test/types.ts

+38
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,44 @@ export type Database = {
496496
}
497497
]
498498
}
499+
booking: {
500+
Row: {
501+
hotel_id: number | null
502+
id: number
503+
}
504+
Insert: {
505+
hotel_id?: number | null
506+
id?: number
507+
}
508+
Update: {
509+
hotel_id?: number | null
510+
id?: number
511+
}
512+
Relationships: [
513+
{
514+
foreignKeyName: 'booking_hotel_id_fkey'
515+
columns: ['hotel_id']
516+
isOneToOne: false
517+
referencedRelation: 'hotel'
518+
referencedColumns: ['id']
519+
}
520+
]
521+
}
522+
hotel: {
523+
Row: {
524+
id: number
525+
name: string | null
526+
}
527+
Insert: {
528+
id?: number
529+
name?: string | null
530+
}
531+
Update: {
532+
id?: number
533+
name?: string | null
534+
}
535+
Relationships: []
536+
}
499537
}
500538
Views: {
501539
non_updatable_view: {

0 commit comments

Comments
 (0)
Please sign in to comment.