Skip to content

Commit 547351d

Browse files
committed
fix(postgrest): fix scalar computed column type inference for isNotNullable and SETOF scalar
1 parent f185703 commit 547351d

4 files changed

Lines changed: 121 additions & 32 deletions

File tree

packages/core/postgrest-js/src/select-query-parser/result.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,11 @@ type ProcessFieldNode<
291291
Relationships extends GenericRelationship[],
292292
Field extends Ast.FieldNode,
293293
> = Field['children'] extends []
294-
? {}
294+
? // Empty `()` — could be a scalar computed column (e.g. `user_count()`).
295+
// Route through ProcessEmbeddedResource so the scalar path returns the correct
296+
// primitive type. For non-scalar embedded resources with empty selection,
297+
// ProcessNodes([]) returns {} which preserves the same prior behavior.
298+
ProcessEmbeddedResource<ClientOptions, Schema, Relationships, Field, RelationName>
295299
: IsNonEmptyArray<Field['children']> extends true // Has embedded resource?
296300
? ProcessEmbeddedResource<ClientOptions, Schema, Relationships, Field, RelationName>
297301
: ProcessSimpleField<Row, RelationName, Field>
@@ -366,13 +370,25 @@ export type ProcessEmbeddedResource<
366370
> =
367371
ResolveRelationship<Schema, Relationships, Field, CurrentTableOrView> extends infer Resolved
368372
? Resolved extends {
369-
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
370-
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' | 'func' }
371-
direction: string
373+
scalarType: infer ScalarType
374+
relation: { isSetofReturn?: boolean; isNotNullable?: boolean }
372375
}
373-
? ProcessEmbeddedResourceResult<ClientOptions, Schema, Resolved, Field, CurrentTableOrView>
374-
: // Otherwise the Resolved is a SelectQueryError return it
375-
{ [K in GetFieldNodeResultName<Field>]: Resolved }
376+
? // Scalar computed column: bypass ProcessNodes and return the primitive type directly.
377+
{
378+
[K in GetFieldNodeResultName<Field>]: Resolved['relation']['isSetofReturn'] extends true
379+
? ScalarType
380+
: Resolved['relation']['isNotNullable'] extends true
381+
? ScalarType
382+
: ScalarType | null
383+
}
384+
: Resolved extends {
385+
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
386+
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' | 'func' }
387+
direction: string
388+
}
389+
? ProcessEmbeddedResourceResult<ClientOptions, Schema, Resolved, Field, CurrentTableOrView>
390+
: // Otherwise the Resolved is a SelectQueryError return it
391+
{ [K in GetFieldNodeResultName<Field>]: Resolved }
376392
: {
377393
[K in GetFieldNodeResultName<Field>]: SelectQueryError<'Failed to resolve relationship.'> &
378394
string

packages/core/postgrest-js/src/select-query-parser/utils.ts

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -464,30 +464,58 @@ export type ResolveForwardRelationship<
464464
CurrentTableOrView,
465465
Field['name']
466466
> extends infer FoundEmbededFunctionJoinTableRelation
467-
? FoundEmbededFunctionJoinTableRelation extends GenericSetofOption
468-
? {
469-
referencedTable: TablesAndViews<Schema>[FoundEmbededFunctionJoinTableRelation['to']]
470-
relation: {
471-
foreignKeyName: `${Field['name']}_${CurrentTableOrView}_${FoundEmbededFunctionJoinTableRelation['to']}_forward`
472-
columns: []
473-
isOneToOne: FoundEmbededFunctionJoinTableRelation['isOneToOne'] extends true
474-
? true
475-
: false
476-
referencedColumns: []
477-
referencedRelation: FoundEmbededFunctionJoinTableRelation['to']
478-
} & {
479-
match: 'func'
480-
isNotNullable: FoundEmbededFunctionJoinTableRelation['isNotNullable'] extends true
481-
? true
482-
: FoundEmbededFunctionJoinTableRelation['isSetofReturn'] extends true
483-
? false
484-
: true
485-
isSetofReturn: FoundEmbededFunctionJoinTableRelation['isSetofReturn']
486-
}
487-
direction: 'forward'
488-
from: CurrentTableOrView
489-
type: 'found-by-embeded-function'
490-
}
467+
? FoundEmbededFunctionJoinTableRelation extends GenericFunction
468+
? FoundEmbededFunctionJoinTableRelation['SetofOptions'] extends GenericSetofOption
469+
? FoundEmbededFunctionJoinTableRelation['SetofOptions']['to'] extends ''
470+
? // Scalar computed column: function returns a primitive (not a table row).
471+
// `to` is '' because there is no target table — the value is returned directly.
472+
{
473+
referencedTable: { Row: Record<string, never>; Relationships: [] }
474+
relation: {
475+
foreignKeyName: `${Field['name']}_${CurrentTableOrView}_scalar_forward`
476+
columns: []
477+
isOneToOne: false
478+
referencedColumns: []
479+
referencedRelation: ''
480+
} & {
481+
match: 'func'
482+
isNotNullable: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isNotNullable'] extends true
483+
? true
484+
: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isSetofReturn'] extends true
485+
? false
486+
: true
487+
isSetofReturn: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isSetofReturn']
488+
}
489+
scalarType: FoundEmbededFunctionJoinTableRelation['Returns']
490+
direction: 'forward'
491+
from: CurrentTableOrView
492+
type: 'found-by-embeded-scalar-function'
493+
}
494+
: // Table-valued function: `to` names the target table/view.
495+
{
496+
referencedTable: TablesAndViews<Schema>[FoundEmbededFunctionJoinTableRelation['SetofOptions']['to']]
497+
relation: {
498+
foreignKeyName: `${Field['name']}_${CurrentTableOrView}_${FoundEmbededFunctionJoinTableRelation['SetofOptions']['to']}_forward`
499+
columns: []
500+
isOneToOne: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isOneToOne'] extends true
501+
? true
502+
: false
503+
referencedColumns: []
504+
referencedRelation: FoundEmbededFunctionJoinTableRelation['SetofOptions']['to']
505+
} & {
506+
match: 'func'
507+
isNotNullable: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isNotNullable'] extends true
508+
? true
509+
: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isSetofReturn'] extends true
510+
? false
511+
: true
512+
isSetofReturn: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isSetofReturn']
513+
}
514+
direction: 'forward'
515+
from: CurrentTableOrView
516+
type: 'found-by-embeded-function'
517+
}
518+
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
491519
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
492520
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
493521
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
@@ -542,7 +570,7 @@ type ResolveEmbededFunctionJoinTableRelationship<
542570
CurrentTableOrView
543571
> extends infer Fn
544572
? Fn extends GenericFunction
545-
? Fn['SetofOptions']
573+
? Fn
546574
: false
547575
: false
548576

packages/core/postgrest-js/test/embeded_functions_join.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,4 +1433,30 @@ describe('embeded functions select', () => {
14331433
>
14341434
>(true)
14351435
})
1436+
1437+
test('scalar_computed_count - non-SETOF non-nullable scalar returns number (not null)', async () => {
1438+
const res = await postgrest.from('users').select('username, scalar_computed_count()')
1439+
let result: Exclude<typeof res.data, null>
1440+
const ExpectedSchema = z.array(
1441+
z.object({
1442+
username: z.string(),
1443+
scalar_computed_count: z.number(),
1444+
})
1445+
)
1446+
let expected: z.infer<typeof ExpectedSchema>
1447+
expectType<TypeEqual<typeof result, typeof expected>>(true)
1448+
})
1449+
1450+
test('scalar_computed_ids - SETOF scalar returns string[] (not null)', async () => {
1451+
const res = await postgrest.from('users').select('username, scalar_computed_ids()')
1452+
let result: Exclude<typeof res.data, null>
1453+
const ExpectedSchema = z.array(
1454+
z.object({
1455+
username: z.string(),
1456+
scalar_computed_ids: z.array(z.string()),
1457+
})
1458+
)
1459+
let expected: z.infer<typeof ExpectedSchema>
1460+
expectType<TypeEqual<typeof result, typeof expected>>(true)
1461+
})
14361462
})

packages/core/postgrest-js/test/types.generated.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,25 @@ export type Database = {
716716
isSetofReturn: true
717717
}
718718
}
719+
scalar_computed_count: {
720+
Args: { '': Database['public']['Tables']['users']['Row'] }
721+
Returns: number
722+
SetofOptions: {
723+
from: 'users'
724+
to: ''
725+
isSetofReturn: false
726+
isNotNullable: true
727+
}
728+
}
729+
scalar_computed_ids: {
730+
Args: { '': Database['public']['Tables']['users']['Row'] }
731+
Returns: string[]
732+
SetofOptions: {
733+
from: 'users'
734+
to: ''
735+
isSetofReturn: true
736+
}
737+
}
719738
function_returning_single_row: {
720739
Args: { messages: Database['public']['Tables']['messages']['Row'] }
721740
Returns: {

0 commit comments

Comments
 (0)