Skip to content

Commit e55441c

Browse files
committed
feat: add embeded function introspection
1 parent c9fbb77 commit e55441c

File tree

3 files changed

+239
-51
lines changed

3 files changed

+239
-51
lines changed

src/lib/sql/functions.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ select
5454
else false
5555
end as returns_set_of_table,
5656
case
57-
when f.proretset and rt.typrelid != 0 then
57+
when rt.typrelid != 0 then
5858
(select relname from pg_class where oid = rt.typrelid)
5959
else null
6060
end as return_table_name,

src/server/templates/typescript.ts

Lines changed: 222 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import type {
88
PostgresView,
99
} from '../../lib/index.js'
1010
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'
1216

1317
export const apply = async ({
1418
schemas,
@@ -43,16 +47,45 @@ export const apply = async ({
4347
)
4448

4549
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+
4687
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}` : ''}`
5689
}
5790

5891
const getFunctionReturnType = (schema: PostgresSchema, fn: PostgresFunction): string => {
@@ -370,42 +403,53 @@ export type Database = {
370403
return '[_ in never]: never'
371404
}
372405
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+
})
409453
.reduce(
410454
(acc, curr) => {
411455
acc[curr.name] ??= []
@@ -415,12 +459,140 @@ export type Database = {
415459
{} as Record<string, PostgresFunction[]>
416460
)
417461
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+
419466
const functionSignatures = fns.map((fn) => {
420467
const inArgs = fn.args.filter(({ mode }) => VALID_FUNCTION_ARGS_MODE.has(mode))
421468
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) {
424596
const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => {
425597
const type = types.find(({ id }) => id === type_id)
426598
let tsType = 'unknown'
@@ -437,7 +609,7 @@ export type Database = {
437609
argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }`
438610
}
439611
440-
return `{ Args: ${argsType}; Returns: ${getFunctionTsReturnType(fn, getFunctionReturnType(schema, fn))} }`
612+
return `{ Args: ${argsType}; Returns: ${getFunctionTsReturnType(fn, returnType)} }`
441613
})
442614
443615
return `${JSON.stringify(fnName)}:\n${functionSignatures.map((sig) => `| ${sig}`).join('\n')}`

test/db/00-init.sql

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,3 +398,19 @@ LANGUAGE SQL STABLE
398398
AS $$
399399
SELECT ROW(ARRAY[$1.name])::composite_type_with_array_attribute;
400400
$$;
401+
402+
-- Function that returns a single element
403+
CREATE OR REPLACE FUNCTION public.function_using_table_returns(user_row users)
404+
RETURNS todos
405+
LANGUAGE SQL STABLE
406+
AS $$
407+
SELECT * FROM public.todos WHERE user_id = user_row.id LIMIT 1;
408+
$$;
409+
410+
CREATE OR REPLACE FUNCTION public.function_using_setof_rows_one(user_row users)
411+
RETURNS SETOF todos
412+
LANGUAGE SQL STABLE
413+
ROWS 1
414+
AS $$
415+
SELECT * FROM public.todos WHERE user_id = user_row.idLIMIT 1;
416+
$$;

0 commit comments

Comments
 (0)