@@ -8,7 +8,11 @@ import type {
8
8
PostgresView ,
9
9
} from '../../lib/index.js'
10
10
import type { GeneratorMetadata } from '../../lib/generators.js'
11
- import { GENERATE_TYPES_DEFAULT_SCHEMA , VALID_FUNCTION_ARGS_MODE } from '../constants.js'
11
+ import {
12
+ GENERATE_TYPES_DEFAULT_SCHEMA ,
13
+ VALID_FUNCTION_ARGS_MODE ,
14
+ VALID_UNNAMED_FUNCTION_ARG_TYPES ,
15
+ } from '../constants.js'
12
16
13
17
export const apply = async ( {
14
18
schemas,
@@ -43,16 +47,45 @@ export const apply = async ({
43
47
)
44
48
45
49
const getFunctionTsReturnType = ( fn : PostgresFunction , returnType : string ) => {
50
+ // Determine if this function should have SetofOptions
51
+ let setofOptionsInfo = ''
52
+
53
+ // Only add SetofOptions for functions with table arguments (embedded functions)
54
+ // or specific functions that need RETURNS table-name introspection fixes
55
+ if ( fn . args . length === 1 && fn . args [ 0 ] . table_name ) {
56
+ // Case 1: Standard embedded function with proper setof detection
57
+ if ( fn . returns_set_of_table && fn . return_table_name ) {
58
+ setofOptionsInfo = `SetofOptions: {
59
+ from: ${ JSON . stringify ( typesById [ fn . args [ 0 ] . type_id ] . format ) }
60
+ to: ${ JSON . stringify ( fn . return_table_name ) }
61
+ isOneToOne: ${ fn . returns_multiple_rows ? false : true }
62
+ isSetofReturn: true
63
+ }`
64
+ }
65
+ // Case 2: Handle RETURNS table-name those are always a one to one relationship
66
+ else if ( fn . return_table_name && ! fn . returns_set_of_table ) {
67
+ const sourceTable = typesById [ fn . args [ 0 ] . type_id ] . format
68
+ let targetTable = fn . return_table_name
69
+ setofOptionsInfo = `SetofOptions: {
70
+ from: ${ JSON . stringify ( sourceTable ) }
71
+ to: ${ JSON . stringify ( targetTable ) }
72
+ isOneToOne: true
73
+ isSetofReturn: false
74
+ }`
75
+ }
76
+ }
77
+ // Case 3: Special case for functions without table arguments but specific names
78
+ else if ( fn . return_table_name ) {
79
+ setofOptionsInfo = `SetofOptions: {
80
+ from: "*"
81
+ to: ${ JSON . stringify ( fn . return_table_name ) }
82
+ isOneToOne: ${ fn . returns_multiple_rows ? false : true }
83
+ isSetofReturn: ${ fn . is_set_returning_function }
84
+ }`
85
+ }
86
+
46
87
return `${ returnType } ${ fn . is_set_returning_function && fn . returns_multiple_rows ? '[]' : '' }
47
- ${
48
- fn . returns_set_of_table && fn . args . length === 1 && fn . args [ 0 ] . table_name
49
- ? `SetofOptions: {
50
- from: ${ JSON . stringify ( typesById [ fn . args [ 0 ] . type_id ] . format ) }
51
- to: ${ JSON . stringify ( fn . return_table_name ) }
52
- isOneToOne: ${ fn . returns_multiple_rows ? false : true }
53
- }`
54
- : ''
55
- } `
88
+ ${ setofOptionsInfo ? `${ setofOptionsInfo } ` : '' } `
56
89
}
57
90
58
91
const getFunctionReturnType = ( schema : PostgresSchema , fn : PostgresFunction ) : string => {
@@ -370,42 +403,53 @@ export type Database = {
370
403
return '[_ in never]: never'
371
404
}
372
405
const schemaFunctionsGroupedByName = schemaFunctions
373
- // .filter((func) => {
374
- // // Get all input args (in, inout, variadic modes)
375
- // const inArgs = func.args.filter(({ mode }) => VALID_FUNCTION_ARGS_MODE.has(mode))
376
- // // Case 1: Function has no parameters
377
- // if (inArgs.length === 0) {
378
- // return true
379
- // }
380
-
381
- // // Case 2: All input args are named
382
- // if (!inArgs.some(({ name }) => name === '')) {
383
- // return true
384
- // }
385
-
386
- // // Case 3: All unnamed args have default values
387
- // if (inArgs.every((arg) => (arg.name === '' ? arg.has_default : true))) {
388
- // return true
389
- // }
390
-
391
- // // Case 4: Single unnamed parameter of valid type (json, jsonb, text)
392
- // // Exclude all functions definitions that have only one single argument unnamed argument that isn't
393
- // // a json/jsonb/text as it won't be considered by PostgREST
394
- // if (
395
- // (inArgs.length === 1 &&
396
- // inArgs[0].name === '' &&
397
- // VALID_UNNAMED_FUNCTION_ARG_TYPES.has(inArgs[0].type_id)) ||
398
- // // OR if the function have a single unnamed args which is another table (embeded function)
399
- // (inArgs.length === 1 &&
400
- // inArgs[0].name === '' &&
401
- // inArgs[0].table_name &&
402
- // func.return_table_name)
403
- // ) {
404
- // return true
405
- // }
406
-
407
- // return false
408
- // })
406
+ . filter ( ( func ) => {
407
+ // Get all input args (in, inout, variadic modes)
408
+ const inArgs = func . args
409
+ . toSorted ( ( a , b ) => a . name . localeCompare ( b . name ) )
410
+ . filter ( ( { mode } ) => VALID_FUNCTION_ARGS_MODE . has ( mode ) )
411
+ // Case 1: Function has no parameters
412
+ if ( inArgs . length === 0 ) {
413
+ return true
414
+ }
415
+
416
+ // Case 2: All input args are named
417
+ if ( ! inArgs . some ( ( { name } ) => name === '' ) ) {
418
+ return true
419
+ }
420
+
421
+ // Case 3: All unnamed args have default values AND are valid types
422
+ if (
423
+ inArgs . every ( ( arg ) => {
424
+ if ( arg . name === '' ) {
425
+ return arg . has_default && VALID_UNNAMED_FUNCTION_ARG_TYPES . has ( arg . type_id )
426
+ }
427
+ return true
428
+ } )
429
+ ) {
430
+ return true
431
+ }
432
+
433
+ // Case 4: Single unnamed parameter of valid type (json, jsonb, text)
434
+ // Exclude all functions definitions that have only one single argument unnamed argument that isn't
435
+ // a json/jsonb/text as it won't be considered by PostgREST
436
+ if (
437
+ inArgs . length === 1 &&
438
+ inArgs [ 0 ] . name === '' &&
439
+ ( VALID_UNNAMED_FUNCTION_ARG_TYPES . has ( inArgs [ 0 ] . type_id ) ||
440
+ // OR if the function have a single unnamed args which is another table (embeded function)
441
+ ( inArgs [ 0 ] . table_name && func . return_table_name ) ||
442
+ // OR if the function takes a table row but doesn't qualify as embedded (for error reporting)
443
+ ( inArgs [ 0 ] . table_name && ! func . return_table_name ) )
444
+ ) {
445
+ return true
446
+ }
447
+
448
+ // NOTE: Functions with named table arguments are generally excluded
449
+ // as they're not supported by PostgREST in the expected way
450
+
451
+ return false
452
+ } )
409
453
. reduce (
410
454
( acc , curr ) => {
411
455
acc [ curr . name ] ??= [ ]
@@ -415,12 +459,140 @@ export type Database = {
415
459
{ } as Record < string , PostgresFunction [ ] >
416
460
)
417
461
418
- return Object . entries ( schemaFunctionsGroupedByName ) . map ( ( [ fnName , fns ] ) => {
462
+ return Object . entries ( schemaFunctionsGroupedByName ) . map ( ( [ fnName , _fns ] ) => {
463
+ // Check for function overload conflicts
464
+ const fns = _fns . toSorted ( ( a , b ) => b . definition . localeCompare ( a . definition ) )
465
+
419
466
const functionSignatures = fns . map ( ( fn ) => {
420
467
const inArgs = fn . args . filter ( ( { mode } ) => VALID_FUNCTION_ARGS_MODE . has ( mode ) )
421
468
422
- let argsType = 'Record<PropertyKey, never>'
423
- if ( inArgs . length > 0 ) {
469
+ // Special error case for functions that take table row but don't qualify as embedded functions
470
+ const hasTableRowError = ( fn : PostgresFunction ) => {
471
+ if (
472
+ inArgs . length === 1 &&
473
+ inArgs [ 0 ] . name === '' &&
474
+ inArgs [ 0 ] . table_name &&
475
+ ! fn . return_table_name
476
+ ) {
477
+ return true
478
+ }
479
+ return false
480
+ }
481
+
482
+ // Check for generic conflict cases that need error reporting
483
+ const getConflictError = ( fn : PostgresFunction ) => {
484
+ const sameFunctions = fns . filter ( ( f ) => f . name === fn . name )
485
+ if ( sameFunctions . length <= 1 ) return null
486
+
487
+ // Generic conflict detection patterns
488
+
489
+ // Pattern 1: No-args vs default-args conflicts
490
+ if ( inArgs . length === 0 ) {
491
+ const conflictingFns = sameFunctions . filter ( ( otherFn ) => {
492
+ if ( otherFn === fn ) return false
493
+ const otherInArgs = otherFn . args . filter ( ( { mode } ) =>
494
+ VALID_FUNCTION_ARGS_MODE . has ( mode )
495
+ )
496
+ return (
497
+ otherInArgs . length === 1 &&
498
+ otherInArgs [ 0 ] . name === '' &&
499
+ otherInArgs [ 0 ] . has_default
500
+ )
501
+ } )
502
+
503
+ if ( conflictingFns . length > 0 ) {
504
+ const conflictingFn = conflictingFns [ 0 ]
505
+ const returnTypeName =
506
+ types . find ( ( t ) => t . id === conflictingFn . return_type_id ) ?. name ||
507
+ 'unknown'
508
+ return `Could not choose the best candidate function between: ${ schema . name } .${ fn . name } (), ${ schema . name } .${ fn . name } ( => ${ returnTypeName } ). Try renaming the parameters or the function itself in the database so function overloading can be resolved`
509
+ }
510
+ }
511
+
512
+ // Pattern 2: Same parameter name but different types (unresolvable overloads)
513
+ if ( inArgs . length === 1 && inArgs [ 0 ] . name !== '' ) {
514
+ const conflictingFns = sameFunctions . filter ( ( otherFn ) => {
515
+ if ( otherFn === fn ) return false
516
+ const otherInArgs = otherFn . args . filter ( ( { mode } ) =>
517
+ VALID_FUNCTION_ARGS_MODE . has ( mode )
518
+ )
519
+ return (
520
+ otherInArgs . length === 1 &&
521
+ otherInArgs [ 0 ] . name === inArgs [ 0 ] . name &&
522
+ otherInArgs [ 0 ] . type_id !== inArgs [ 0 ] . type_id
523
+ )
524
+ } )
525
+
526
+ if ( conflictingFns . length > 0 ) {
527
+ const allConflictingFunctions = [ fn , ...conflictingFns ]
528
+ const conflictList = allConflictingFunctions
529
+ . sort ( ( a , b ) => {
530
+ const aArgs = a . args . filter ( ( { mode } ) =>
531
+ VALID_FUNCTION_ARGS_MODE . has ( mode )
532
+ )
533
+ const bArgs = b . args . filter ( ( { mode } ) =>
534
+ VALID_FUNCTION_ARGS_MODE . has ( mode )
535
+ )
536
+ return ( aArgs [ 0 ] ?. type_id || 0 ) - ( bArgs [ 0 ] ?. type_id || 0 )
537
+ } )
538
+ . map ( ( f ) => {
539
+ const args = f . args . filter ( ( { mode } ) =>
540
+ VALID_FUNCTION_ARGS_MODE . has ( mode )
541
+ )
542
+ return `${ schema . name } .${ fn . name } (${ args . map ( ( a ) => `${ a . name || '' } => ${ types . find ( ( t ) => t . id === a . type_id ) ?. name || 'unknown' } ` ) . join ( ', ' ) } )`
543
+ } )
544
+ . join ( ', ' )
545
+
546
+ return `Could not choose the best candidate function between: ${ conflictList } . Try renaming the parameters or the function itself in the database so function overloading can be resolved`
547
+ }
548
+ }
549
+
550
+ return null
551
+ }
552
+
553
+ let argsType = 'never'
554
+ let returnType = getFunctionReturnType ( schema , fn )
555
+
556
+ // Check for specific error cases
557
+ const conflictError = getConflictError ( fn )
558
+ if ( conflictError ) {
559
+ if ( inArgs . length > 0 ) {
560
+ const argsNameAndType = inArgs . map ( ( { name, type_id, has_default } ) => {
561
+ const type = types . find ( ( { id } ) => id === type_id )
562
+ let tsType = 'unknown'
563
+ if ( type ) {
564
+ tsType = pgTypeToTsType ( schema , type . name , {
565
+ types,
566
+ schemas,
567
+ tables,
568
+ views,
569
+ } )
570
+ }
571
+ return { name, type : tsType , has_default }
572
+ } )
573
+ argsType = `{ ${ argsNameAndType . map ( ( { name, type, has_default } ) => `${ JSON . stringify ( name ) } ${ has_default ? '?' : '' } : ${ type } ` ) } }`
574
+ }
575
+ returnType = `{ error: true } & ${ JSON . stringify ( conflictError ) } `
576
+ } else if ( hasTableRowError ( fn ) ) {
577
+ // Special case for computed fields returning scalars functions
578
+ if ( inArgs . length > 0 ) {
579
+ const argsNameAndType = inArgs . map ( ( { name, type_id, has_default } ) => {
580
+ const type = types . find ( ( { id } ) => id === type_id )
581
+ let tsType = 'unknown'
582
+ if ( type ) {
583
+ tsType = pgTypeToTsType ( schema , type . name , {
584
+ types,
585
+ schemas,
586
+ tables,
587
+ views,
588
+ } )
589
+ }
590
+ return { name, type : tsType , has_default }
591
+ } )
592
+ argsType = `{ ${ argsNameAndType . map ( ( { name, type, has_default } ) => `${ JSON . stringify ( name ) } ${ has_default ? '?' : '' } : ${ type } ` ) } }`
593
+ }
594
+ returnType = `{ error: true } & ${ JSON . stringify ( `the function ${ schema . name } .${ fn . name } with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache` ) } `
595
+ } else if ( inArgs . length > 0 ) {
424
596
const argsNameAndType = inArgs . map ( ( { name, type_id, has_default } ) => {
425
597
const type = types . find ( ( { id } ) => id === type_id )
426
598
let tsType = 'unknown'
@@ -437,7 +609,7 @@ export type Database = {
437
609
argsType = `{ ${ argsNameAndType . map ( ( { name, type, has_default } ) => `${ JSON . stringify ( name ) } ${ has_default ? '?' : '' } : ${ type } ` ) } }`
438
610
}
439
611
440
- return `{ Args: ${ argsType } ; Returns: ${ getFunctionTsReturnType ( fn , getFunctionReturnType ( schema , fn ) ) } }`
612
+ return `{ Args: ${ argsType } ; Returns: ${ getFunctionTsReturnType ( fn , returnType ) } }`
441
613
} )
442
614
443
615
return `${ JSON . stringify ( fnName ) } :\n${ functionSignatures . map ( ( sig ) => `| ${ sig } ` ) . join ( '\n' ) } `
0 commit comments