Skip to content

Commit 29c52c4

Browse files
committed
wip: unnamed need to prefer text/json/jsonb and no-params
1 parent 9fb399f commit 29c52c4

File tree

2 files changed

+286
-70
lines changed

2 files changed

+286
-70
lines changed

src/server/templates/typescript.ts

Lines changed: 252 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -284,101 +284,284 @@ export type Database = {
284284
{} as Record<string, PostgresFunction[]>
285285
)
286286
287-
return Object.entries(schemaFunctionsGroupedByName).map(
288-
([fnName, fns]) =>
289-
`${JSON.stringify(fnName)}: {
290-
Args: ${fns
291-
.map(({ args }) => {
292-
const inArgs = args.filter(({ mode }) => mode === 'in')
293-
294-
if (inArgs.length === 0) {
295-
return 'Record<PropertyKey, never>'
296-
}
297-
const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => {
298-
const type = typesById[type_id]
287+
return Object.entries(schemaFunctionsGroupedByName).map(([fnName, fns]) => {
288+
// Group functions by their argument names signature to detect conflicts
289+
const fnsByArgNames = new Map<string, PostgresFunction[]>()
290+
291+
fns.forEach((fn) => {
292+
const namedInArgs = fn.args
293+
.filter(
294+
({ mode, name }) => ['in', 'inout', 'variadic'].includes(mode) && name !== ''
295+
)
296+
.map((arg) => arg.name)
297+
.sort()
298+
.join(',')
299+
300+
if (!fnsByArgNames.has(namedInArgs)) {
301+
fnsByArgNames.set(namedInArgs, [])
302+
}
303+
fnsByArgNames.get(namedInArgs)!.push(fn)
304+
})
305+
306+
// For each group of functions sharing the same argument names, check if they have conflicting types
307+
const conflictingSignatures = new Set<string>()
308+
fnsByArgNames.forEach((groupedFns, argNames) => {
309+
if (groupedFns.length > 1) {
310+
// Check if any args have different types within this group
311+
const firstFn = groupedFns[0]
312+
const firstFnArgTypes = new Map(
313+
firstFn.args
314+
.filter(
315+
({ mode, name }) =>
316+
['in', 'inout', 'variadic'].includes(mode) && name !== ''
317+
)
318+
.map((arg) => [arg.name, String(arg.type_id)])
319+
)
320+
321+
const hasConflict = groupedFns.some((fn) => {
322+
const fnArgTypes = new Map(
323+
fn.args
324+
.filter(
325+
({ mode, name }) =>
326+
['in', 'inout', 'variadic'].includes(mode) && name !== ''
327+
)
328+
.map((arg) => [arg.name, String(arg.type_id)])
329+
)
330+
331+
return [...firstFnArgTypes.entries()].some(
332+
([name, typeId]) => fnArgTypes.get(name) !== typeId
333+
)
334+
})
335+
336+
if (hasConflict) {
337+
conflictingSignatures.add(argNames)
338+
}
339+
}
340+
})
341+
342+
// Generate all possible function signatures as a union
343+
const signatures = (() => {
344+
// Special case: if any function has a single unnamed parameter
345+
const unnamedFns = fns.filter((fn) => fn.args.some(({ name }) => name === ''))
346+
if (unnamedFns.length > 0) {
347+
// Take only the first function with unnamed parameters
348+
const firstUnnamedFn = unnamedFns[0]
349+
const firstArgType = typesById[firstUnnamedFn.args[0].type_id]
350+
const tsType = firstArgType
351+
? pgTypeToTsType(firstArgType.name, { types, schemas, tables, views })
352+
: 'unknown'
353+
354+
const returnType = (() => {
355+
// Case 1: `returns table`.
356+
const tableArgs = firstUnnamedFn.args.filter(({ mode }) => mode === 'table')
357+
if (tableArgs.length > 0) {
358+
const argsNameAndType = tableArgs
359+
.map(({ name, type_id }) => {
360+
const type = types.find(({ id }) => id === type_id)
299361
let tsType = 'unknown'
300362
if (type) {
301363
tsType = pgTypeToTsType(type.name, { types, schemas, tables, views })
302364
}
303-
return { name, type: tsType, has_default }
365+
return { name, type: tsType }
304366
})
305-
return `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }`
367+
.sort((a, b) => a.name.localeCompare(b.name))
368+
369+
return `{
370+
${argsNameAndType.map(
371+
({ name, type }) => `${JSON.stringify(name)}: ${type}`
372+
)}
373+
}`
374+
}
375+
376+
// Case 2: returns a relation's row type.
377+
const relation = [...tables, ...views].find(
378+
({ id }) => id === firstUnnamedFn.return_type_relation_id
379+
)
380+
if (relation) {
381+
return `{
382+
${columnsByTableId[relation.id]
383+
.map(
384+
(column) =>
385+
`${JSON.stringify(column.name)}: ${pgTypeToTsType(column.format, {
386+
types,
387+
schemas,
388+
tables,
389+
views,
390+
})} ${column.is_nullable ? '| null' : ''}`
391+
)
392+
.sort()
393+
.join(',\n')}
394+
}`
395+
}
396+
397+
// Case 3: returns base/array/composite/enum type.
398+
const returnType = types.find(
399+
({ id }) => id === firstUnnamedFn.return_type_id
400+
)
401+
if (returnType) {
402+
return pgTypeToTsType(returnType.name, { types, schemas, tables, views })
403+
}
404+
405+
return 'unknown'
406+
})()
407+
408+
return [
409+
`{
410+
Args: { "": ${tsType} },
411+
Returns: ${returnType}${firstUnnamedFn.is_set_returning_function && firstUnnamedFn.returns_multiple_rows ? '[]' : ''}
412+
${
413+
firstUnnamedFn.returns_set_of_table
414+
? `,
415+
SetofOptions: {
416+
from: ${
417+
firstUnnamedFn.args.length > 0 && firstUnnamedFn.args[0].table_name
418+
? JSON.stringify(typesById[firstUnnamedFn.args[0].type_id].format)
419+
: '"*"'
420+
},
421+
to: ${JSON.stringify(firstUnnamedFn.return_table_name)},
422+
isOneToOne: ${firstUnnamedFn.returns_multiple_rows ? false : true}
423+
}`
424+
: ''
425+
}
426+
}`,
427+
]
428+
}
429+
430+
// For functions with named parameters, generate all signatures
431+
const namedFns = fns.filter((fn) => !fn.args.some(({ name }) => name === ''))
432+
return namedFns.map((fn) => {
433+
const inArgs = fn.args.filter(({ mode }) => mode === 'in')
434+
const namedInArgs = inArgs
435+
.filter((arg) => arg.name !== '')
436+
.map((arg) => arg.name)
437+
.sort()
438+
.join(',')
439+
440+
// If this argument combination would cause a conflict, return an error type signature
441+
if (conflictingSignatures.has(namedInArgs)) {
442+
const conflictingFns = fnsByArgNames.get(namedInArgs)!
443+
const conflictDesc = conflictingFns
444+
.map((cfn) => {
445+
const argsStr = cfn.args
446+
.filter(({ mode }) => mode === 'in')
447+
.map((arg) => {
448+
const type = typesById[arg.type_id]
449+
return `${arg.name} => ${type?.name || 'unknown'}`
450+
})
451+
.sort()
452+
.join(', ')
453+
return `${fnName}(${argsStr})`
306454
})
307-
.toSorted()
308-
// A function can have multiples definitions with differents args, but will always return the same type
309-
.join(' | ')}
310-
Returns: ${(() => {
311-
// Case 1: `returns table`.
312-
const tableArgs = fns[0].args.filter(({ mode }) => mode === 'table')
313-
if (tableArgs.length > 0) {
314-
const argsNameAndType = tableArgs.map(({ name, type_id }) => {
455+
.sort()
456+
.join(', ')
457+
458+
return `{
459+
Args: { ${inArgs
460+
.map((arg) => `${JSON.stringify(arg.name)}: unknown`)
461+
.sort()
462+
.join(', ')} },
463+
Returns: { error: true } & "Could not choose the best candidate function between: ${conflictDesc}. Try renaming the parameters or the function itself in the database so function overloading can be resolved"
464+
}`
465+
}
466+
467+
// Generate normal function signature
468+
const returnType = (() => {
469+
// Case 1: `returns table`.
470+
const tableArgs = fn.args.filter(({ mode }) => mode === 'table')
471+
if (tableArgs.length > 0) {
472+
const argsNameAndType = tableArgs
473+
.map(({ name, type_id }) => {
315474
const type = types.find(({ id }) => id === type_id)
316475
let tsType = 'unknown'
317476
if (type) {
318477
tsType = pgTypeToTsType(type.name, { types, schemas, tables, views })
319478
}
320479
return { name, type: tsType }
321480
})
481+
.sort((a, b) => a.name.localeCompare(b.name))
322482
323-
return `{
324-
${argsNameAndType.map(
325-
({ name, type }) => `${JSON.stringify(name)}: ${type}`
326-
)}
327-
}`
328-
}
483+
return `{
484+
${argsNameAndType.map(
485+
({ name, type }) => `${JSON.stringify(name)}: ${type}`
486+
)}
487+
}`
488+
}
329489
330-
// Case 2: returns a relation's row type.
331-
const relation = [...tables, ...views].find(
332-
({ id }) => id === fns[0].return_type_relation_id
333-
)
334-
if (relation) {
335-
return `{
336-
${columnsByTableId[relation.id].map(
490+
// Case 2: returns a relation's row type.
491+
const relation = [...tables, ...views].find(
492+
({ id }) => id === fn.return_type_relation_id
493+
)
494+
if (relation) {
495+
return `{
496+
${columnsByTableId[relation.id]
497+
.map(
337498
(column) =>
338499
`${JSON.stringify(column.name)}: ${pgTypeToTsType(column.format, {
339500
types,
340501
schemas,
341502
tables,
342503
views,
343504
})} ${column.is_nullable ? '| null' : ''}`
344-
)}
345-
}`
346-
}
347-
348-
// Case 3: returns base/array/composite/enum type.
349-
const type = types.find(({ id }) => id === fns[0].return_type_id)
350-
if (type) {
351-
return pgTypeToTsType(type.name, { types, schemas, tables, views })
352-
}
353-
354-
return 'unknown'
355-
})()}${fns[0].is_set_returning_function && fns[0].returns_multiple_rows ? '[]' : ''}
356-
${
357-
// if the function return a set of a table and some definition take in parameter another table
358-
fns[0].returns_set_of_table
359-
? `SetofOptions: {
360-
from: ${fns
361-
.map((fnd) => {
362-
if (fnd.args.length > 0 && fnd.args[0].table_name) {
363-
const tableType = typesById[fnd.args[0].type_id]
364-
return JSON.stringify(tableType.format)
365-
} else {
366-
// If the function can be called with scalars or without any arguments, then add a * matching everything
367-
return '"*"'
368-
}
369-
})
370-
// Dedup before join
371-
.filter((value, index, self) => self.indexOf(value) === index)
372-
.toSorted()
373-
.join(' | ')}
374-
to: ${JSON.stringify(fns[0].return_table_name)}
375-
isOneToOne: ${fns[0].returns_multiple_rows ? false : true}
505+
)
506+
.sort()
507+
.join(',\n')}
508+
}`
376509
}
377-
`
510+
511+
// Case 3: returns base/array/composite/enum type.
512+
const type = types.find(({ id }) => id === fn.return_type_id)
513+
if (type) {
514+
return pgTypeToTsType(type.name, { types, schemas, tables, views })
515+
}
516+
517+
return 'unknown'
518+
})()
519+
520+
return `{
521+
Args: ${
522+
inArgs.length === 0
523+
? 'Record<PropertyKey, never>'
524+
: `{ ${inArgs
525+
.map(({ name, type_id, has_default }) => {
526+
const type = typesById[type_id]
527+
let tsType = 'unknown'
528+
if (type) {
529+
tsType = pgTypeToTsType(type.name, {
530+
types,
531+
schemas,
532+
tables,
533+
views,
534+
})
535+
}
536+
return `${JSON.stringify(name)}${has_default ? '?' : ''}: ${tsType}`
537+
})
538+
.sort()
539+
.join(', ')} }`
540+
},
541+
Returns: ${returnType}${fn.is_set_returning_function && fn.returns_multiple_rows ? '[]' : ''}
542+
${
543+
fn.returns_set_of_table
544+
? `,
545+
SetofOptions: {
546+
from: ${
547+
fn.args.length > 0 && fn.args[0].table_name
548+
? JSON.stringify(typesById[fn.args[0].type_id].format)
549+
: '"*"'
550+
},
551+
to: ${JSON.stringify(fn.return_table_name)},
552+
isOneToOne: ${fn.returns_multiple_rows ? false : true}
553+
}`
378554
: ''
379555
}
380556
}`
381-
)
557+
})
558+
})()
559+
560+
// Remove duplicates, sort, and join with |
561+
return `${JSON.stringify(fnName)}: ${Array.from(new Set(signatures))
562+
.sort()
563+
.join('\n | ')}`
564+
})
382565
})()}
383566
}
384567
Enums: {

test/db/00-init.sql

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,37 @@ RETURNS SETOF todos
269269
LANGUAGE SQL STABLE
270270
AS $$
271271
SELECT * FROM todos WHERE "user-id" = userview_row.id;
272-
$$;
272+
$$;
273+
274+
-- Valid postgresql function override but that produce an unresolvable postgrest function call
275+
create function postgrest_unresolvable_function() returns void language sql as '';
276+
create function postgrest_unresolvable_function(a text) returns int language sql as 'select 1';
277+
create function postgrest_unresolvable_function(a int) returns text language sql as $$
278+
SELECT 'toto'
279+
$$;
280+
-- Valid postgresql function override with differents returns types depending of different arguments
281+
create function postgrest_resolvable_with_override_function() returns void language sql as '';
282+
create function postgrest_resolvable_with_override_function(a text) returns int language sql as 'select 1';
283+
create function postgrest_resolvable_with_override_function(b int) returns text language sql as $$
284+
SELECT 'toto'
285+
$$;
286+
-- Function overrides returning setof tables
287+
create function postgrest_resolvable_with_override_function(user_id bigint) returns setof users language sql stable as $$
288+
SELECT * FROM users WHERE id = user_id;
289+
$$;
290+
create function postgrest_resolvable_with_override_function(todo_id bigint, completed boolean) returns setof todos language sql stable as $$
291+
SELECT * FROM todos WHERE id = todo_id AND completed = completed;
292+
$$;
293+
-- Function override taking a table as argument and returning a setof
294+
create function postgrest_resolvable_with_override_function(user_row users) returns setof todos language sql stable as $$
295+
SELECT * FROM todos WHERE "user-id" = user_row.id;
296+
$$;
297+
298+
create or replace function public.polymorphic_function_with_different_return(text) returns void language sql as '';
299+
create or replace function public.polymorphic_function_with_different_return(bool) returns int language sql as 'SELECT 1';
300+
301+
-- Function with a single unnamed params that isn't a json/jsonb/text should never appears in the type gen as it won't be in postgrest schema
302+
create or replace function public.polymorphic_function_with_unnamed_integer(int) returns int language sql as 'SELECT 1';
303+
create or replace function public.polymorphic_function_with_unnamed_json(json) returns int language sql as 'SELECT 1';
304+
create or replace function public.polymorphic_function_with_unnamed_jsonb(jsonb) returns int language sql as 'SELECT 1';
305+
create or replace function public.polymorphic_function_with_unnamed_text(text) returns int language sql as 'SELECT 1';

0 commit comments

Comments
 (0)