From 64a1afc7f78998847bbe485b5135b89a4c6d22bd Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 15 Jul 2025 18:18:26 +0200 Subject: [PATCH 01/26] feat(typegen): add setof function type introspection - Introspect the setof function fields for functions - Restore functions as unions of args + returns --- Dockerfile | 2 +- package.json | 3 +- src/lib/PostgresMetaTypes.ts | 2 +- src/lib/sql/functions.sql | 75 +- src/lib/types.ts | 4 + src/server/app.ts | 9 +- src/server/constants.ts | 9 + src/server/templates/swift.ts | 2 + src/server/templates/typescript.ts | 433 +++-- test/db/00-init.sql | 236 ++- test/lib/functions.ts | 159 ++ test/lib/tables.ts | 18 + test/server/query.ts | 2 + test/server/typegen.ts | 2713 ++++++++++++++++++++++++---- 14 files changed, 3198 insertions(+), 469 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8756b7ac..df79412a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20 as build +FROM node:20 AS build WORKDIR /usr/src/app # Do `npm ci` separately so we can cache `node_modules` # https://nodejs.org/en/docs/guides/nodejs-docker-webapp/ diff --git a/package.json b/package.json index 62315e9f..324e75a0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "gen:types:go": "PG_META_GENERATE_TYPES=go node --loader ts-node/esm src/server/server.ts", "gen:types:swift": "PG_META_GENERATE_TYPES=swift node --loader ts-node/esm src/server/server.ts", "start": "node dist/server/server.js", - "dev": "trap 'npm run db:clean' INT && run-s db:clean db:run && nodemon --exec node --loader ts-node/esm src/server/server.ts | pino-pretty --colorize", + "dev": "trap 'npm run db:clean' INT && run-s db:clean db:run && run-s dev:code", + "dev:code": "nodemon --exec node --loader ts-node/esm src/server/server.ts | pino-pretty --colorize", "test": "run-s db:clean db:run test:run db:clean", "db:clean": "cd test/db && docker compose down", "db:run": "cd test/db && docker compose up --detach --wait", diff --git a/src/lib/PostgresMetaTypes.ts b/src/lib/PostgresMetaTypes.ts index 35371d55..a3d73fd6 100644 --- a/src/lib/PostgresMetaTypes.ts +++ b/src/lib/PostgresMetaTypes.ts @@ -33,7 +33,7 @@ export default class PostgresMetaTypes { t.typrelid = 0 or ( select - c.relkind ${includeTableTypes ? `in ('c', 'r')` : `= 'c'`} + c.relkind ${includeTableTypes ? `in ('c', 'r', 'v')` : `= 'c'`} from pg_class c where diff --git a/src/lib/sql/functions.sql b/src/lib/sql/functions.sql index d2258402..80ca4177 100644 --- a/src/lib/sql/functions.sql +++ b/src/lib/sql/functions.sql @@ -44,6 +44,25 @@ select pg_get_function_result(f.oid) as return_type, nullif(rt.typrelid::int8, 0) as return_type_relation_id, f.proretset as is_set_returning_function, + case + when f.proretset and rt.typrelid != 0 and exists ( + select 1 from pg_class c + where c.oid = rt.typrelid + -- exclude custom types relation from what is considered a set of table + and c.relkind in ('r', 'p', 'v', 'm', 'f') + ) then true + else false + end as returns_set_of_table, + case + when rt.typrelid != 0 then + (select relname from pg_class where oid = rt.typrelid) + else null + end as return_table_name, + case + when f.proretset then + coalesce(f.prorows, 0) > 1 + else false + end as returns_multiple_rows, case when f.provolatile = 'i' then 'IMMUTABLE' when f.provolatile = 's' then 'STABLE' @@ -76,32 +95,48 @@ from select oid, jsonb_agg(jsonb_build_object( - 'mode', t2.mode, + 'mode', mode, 'name', name, 'type_id', type_id, - 'has_default', has_default + 'has_default', has_default, + 'table_name', table_name )) as args from ( select - oid, - unnest(arg_modes) as mode, - unnest(arg_names) as name, - unnest(arg_types)::int8 as type_id, - unnest(arg_has_defaults) as has_default - from - functions - ) as t1, - lateral ( - select + t1.oid, + t2.mode, + t1.name, + t1.type_id, + t1.has_default, case - when t1.mode = 'i' then 'in' - when t1.mode = 'o' then 'out' - when t1.mode = 'b' then 'inout' - when t1.mode = 'v' then 'variadic' - else 'table' - end as mode - ) as t2 + when pt.typrelid != 0 then pc.relname + else null + end as table_name + from + ( + select + oid, + unnest(arg_modes) as mode, + unnest(arg_names) as name, + unnest(arg_types)::int8 as type_id, + unnest(arg_has_defaults) as has_default + from + functions + ) as t1 + cross join lateral ( + select + case + when t1.mode = 'i' then 'in' + when t1.mode = 'o' then 'out' + when t1.mode = 'b' then 'inout' + when t1.mode = 'v' then 'variadic' + else 'table' + end as mode + ) as t2 + left join pg_type pt on pt.oid = t1.type_id + left join pg_class pc on pc.oid = pt.typrelid + ) sub group by - t1.oid + oid ) f_args on f_args.oid = f.oid diff --git a/src/lib/types.ts b/src/lib/types.ts index bfd60250..4d4c2889 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -148,6 +148,7 @@ const postgresFunctionSchema = Type.Object({ name: Type.String(), type_id: Type.Number(), has_default: Type.Boolean(), + table_name: Type.Union([Type.String(), Type.Null()]), }) ), argument_types: Type.String(), @@ -156,6 +157,9 @@ const postgresFunctionSchema = Type.Object({ return_type: Type.String(), return_type_relation_id: Type.Union([Type.Integer(), Type.Null()]), is_set_returning_function: Type.Boolean(), + returns_set_of_table: Type.Boolean(), + return_table_name: Type.Union([Type.String(), Type.Null()]), + returns_multiple_rows: Type.Boolean(), behavior: Type.Union([ Type.Literal('IMMUTABLE'), Type.Literal('STABLE'), diff --git a/src/server/app.ts b/src/server/app.ts index 8efa733c..9df05341 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -3,14 +3,19 @@ import * as Sentry from '@sentry/node' import cors from '@fastify/cors' import swagger from '@fastify/swagger' import { fastify, FastifyInstance, FastifyServerOptions } from 'fastify' -import { PG_META_REQ_HEADER } from './constants.js' +import { PG_META_REQ_HEADER, MAX_BODY_LIMIT } from './constants.js' import routes from './routes/index.js' import { extractRequestForLogging } from './utils.js' // Pseudo package declared only for this module import pkg from '#package.json' with { type: 'json' } export const build = (opts: FastifyServerOptions = {}): FastifyInstance => { - const app = fastify({ disableRequestLogging: true, requestIdHeader: PG_META_REQ_HEADER, ...opts }) + const app = fastify({ + disableRequestLogging: true, + requestIdHeader: PG_META_REQ_HEADER, + bodyLimit: MAX_BODY_LIMIT, + ...opts, + }) Sentry.setupFastifyErrorHandler(app) app.setErrorHandler((error, request, reply) => { diff --git a/src/server/constants.ts b/src/server/constants.ts index 8bf66417..c64b45e6 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -51,12 +51,21 @@ export const GENERATE_TYPES_SWIFT_ACCESS_CONTROL = process.env ? (process.env.PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL as AccessControl) : 'internal' +// json/jsonb/text types +export const VALID_UNNAMED_FUNCTION_ARG_TYPES = new Set([114, 3802, 25]) +export const VALID_FUNCTION_ARGS_MODE = new Set(['in', 'inout', 'variadic']) + export const PG_META_MAX_RESULT_SIZE = process.env.PG_META_MAX_RESULT_SIZE_MB ? // Node-postgres get a maximum size in bytes make the conversion from the env variable // from MB to Bytes parseInt(process.env.PG_META_MAX_RESULT_SIZE_MB, 10) * 1024 * 1024 : 2 * 1024 * 1024 * 1024 // default to 2GB max query size result +export const MAX_BODY_LIMIT = process.env.PG_META_MAX_BODY_LIMIT_MB + ? // Fastify server max body size allowed, is in bytes, convert from MB to Bytes + parseInt(process.env.PG_META_MAX_BODY_LIMIT_MB, 10) * 1024 * 1024 + : 3 * 1024 * 1024 + export const DEFAULT_POOL_CONFIG: PoolConfig = { max: 1, connectionTimeoutMillis: PG_CONN_TIMEOUT_SECS * 1000, diff --git a/src/server/templates/swift.ts b/src/server/templates/swift.ts index e596610e..7bb41207 100644 --- a/src/server/templates/swift.ts +++ b/src/server/templates/swift.ts @@ -309,6 +309,8 @@ const pgTypeToSwiftType = ( swiftType = 'Float' } else if (pgType === 'float8') { swiftType = 'Double' + } else if (['numeric', 'decimal'].includes(pgType)) { + swiftType = 'Decimal' } else if (pgType === 'uuid') { swiftType = 'UUID' } else if ( diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 29c34c1a..cd837bf2 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -8,7 +8,11 @@ import type { PostgresView, } from '../../lib/index.js' import type { GeneratorMetadata } from '../../lib/generators.js' -import { GENERATE_TYPES_DEFAULT_SCHEMA } from '../constants.js' +import { + GENERATE_TYPES_DEFAULT_SCHEMA, + VALID_FUNCTION_ARGS_MODE, + VALID_UNNAMED_FUNCTION_ARG_TYPES, +} from '../constants.js' export const apply = async ({ schemas, @@ -33,9 +37,108 @@ export const apply = async ({ .filter((c) => c.table_id in columnsByTableId) .sort(({ name: a }, { name: b }) => a.localeCompare(b)) .forEach((c) => columnsByTableId[c.table_id].push(c)) + // group types by id for quicker lookup + const typesById = types.reduce( + (acc, type) => { + acc[type.id] = type + return acc + }, + {} as Record + ) + + const getFunctionTsReturnType = (fn: PostgresFunction, returnType: string) => { + // Determine if this function should have SetofOptions + let setofOptionsInfo = '' + + // Only add SetofOptions for functions with table arguments (embedded functions) + // or specific functions that need RETURNS table-name introspection fixes + if (fn.args.length === 1 && fn.args[0].table_name) { + // Case 1: Standard embedded function with proper setof detection + if (fn.returns_set_of_table && fn.return_table_name) { + setofOptionsInfo = `SetofOptions: { + from: ${JSON.stringify(typesById[fn.args[0].type_id].format)} + to: ${JSON.stringify(fn.return_table_name)} + isOneToOne: ${fn.returns_multiple_rows ? false : true} + isSetofReturn: true + }` + } + // Case 2: Handle RETURNS table-name those are always a one to one relationship + else if (fn.return_table_name && !fn.returns_set_of_table) { + const sourceTable = typesById[fn.args[0].type_id].format + let targetTable = fn.return_table_name + setofOptionsInfo = `SetofOptions: { + from: ${JSON.stringify(sourceTable)} + to: ${JSON.stringify(targetTable)} + isOneToOne: true + isSetofReturn: false + }` + } + } + // Case 3: Special case for functions without table arguments but specific names + else if (fn.return_table_name) { + setofOptionsInfo = `SetofOptions: { + from: "*" + to: ${JSON.stringify(fn.return_table_name)} + isOneToOne: ${fn.returns_multiple_rows ? false : true} + isSetofReturn: ${fn.is_set_returning_function} + }` + } + + return `${returnType}${fn.is_set_returning_function && fn.returns_multiple_rows ? '[]' : ''} + ${setofOptionsInfo ? `${setofOptionsInfo}` : ''}` + } + + const getFunctionReturnType = (schema: PostgresSchema, fn: PostgresFunction): string => { + // Case 1: `returns table`. + const tableArgs = fn.args.filter(({ mode }) => mode === 'table') + if (tableArgs.length > 0) { + const argsNameAndType = tableArgs + .map(({ name, type_id }) => { + const type = typesById[type_id] + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(schema, type.name, { types, schemas, tables, views }) + } + return { name, type: tsType } + }) + .toSorted((a, b) => a.name.localeCompare(b.name)) + + return `{ + ${argsNameAndType.map(({ name, type }) => `${JSON.stringify(name)}: ${type}`)} + }` + } + + // Case 2: returns a relation's row type. + const relation = [...tables, ...views].find(({ id }) => id === fn.return_type_relation_id) + if (relation) { + return `{ + ${columnsByTableId[relation.id] + .toSorted((a, b) => a.name.localeCompare(b.name)) + .map( + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType(schema, column.format, { + types, + schemas, + tables, + views, + })} ${column.is_nullable ? '| null' : ''}` + ) + .sort() + .join(',\n')} + }` + } + + // Case 3: returns base/array/composite/enum type. + const type = typesById[fn.return_type_id] + if (type) { + return pgTypeToTsType(schema, type.name, { types, schemas, tables, views }) + } + + return 'unknown' + } const internal_supabase_schema = postgrestVersion - ? `// Allows to automatically instanciate createClient with right options + ? `// Allows to automatically instantiate createClient with right options // instead of createClient(URL, KEY) __InternalSupabase: { PostgrestVersion: '${postgrestVersion}' @@ -104,19 +207,10 @@ export type Database = { ), ...schemaFunctions .filter((fn) => fn.argument_types === table.name) - .map((fn) => { - const type = types.find(({ id }) => id === fn.return_type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return `${JSON.stringify(fn.name)}: ${tsType} | null` - }), + .map( + (fn) => + `${JSON.stringify(fn.name)}: ${getFunctionReturnType(schema, fn)} | null` + ), ]} } Insert: { @@ -213,15 +307,23 @@ export type Database = { : schemaViews.map( (view) => `${JSON.stringify(view.name)}: { Row: { - ${columnsByTableId[view.id].map( - (column) => - `${JSON.stringify(column.name)}: ${pgTypeToTsType(schema, column.format, { - types, - schemas, - tables, - views, - })} ${column.is_nullable ? '| null' : ''}` - )} + ${[ + ...columnsByTableId[view.id].map( + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType(schema, column.format, { + types, + schemas, + tables, + views, + })} ${column.is_nullable ? '| null' : ''}` + ), + ...schemaFunctions + .filter((fn) => fn.argument_types === view.name) + .map( + (fn) => + `${JSON.stringify(fn.name)}: ${getFunctionReturnType(schema, fn)} | null` + ), + ]} } ${ 'is_updatable' in view && view.is_updatable @@ -301,107 +403,218 @@ export type Database = { if (schemaFunctions.length === 0) { return '[_ in never]: never' } + const schemaFunctionsGroupedByName = schemaFunctions + .filter((func) => { + // Get all input args (in, inout, variadic modes) + const inArgs = func.args + .toSorted((a, b) => a.name.localeCompare(b.name)) + .filter(({ mode }) => VALID_FUNCTION_ARGS_MODE.has(mode)) + // Case 1: Function has no parameters + if (inArgs.length === 0) { + return true + } - const schemaFunctionsGroupedByName = schemaFunctions.reduce( - (acc, curr) => { - acc[curr.name] ??= [] - acc[curr.name].push(curr) - return acc - }, - {} as Record - ) - - return Object.entries(schemaFunctionsGroupedByName).map( - ([fnName, fns]) => - `${JSON.stringify(fnName)}: { - Args: ${fns - .map(({ args }) => { - const inArgs = args.filter(({ mode }) => mode === 'in') - - if (inArgs.length === 0) { - return 'Record' - } + // Case 2: All input args are named + if (!inArgs.some(({ name }) => name === '')) { + return true + } + + // Case 3: All unnamed args have default values AND are valid types + if ( + inArgs.every((arg) => { + if (arg.name === '') { + return arg.has_default && VALID_UNNAMED_FUNCTION_ARG_TYPES.has(arg.type_id) + } + return true + }) + ) { + return true + } + + // Case 4: Single unnamed parameter of valid type (json, jsonb, text) + // Exclude all functions definitions that have only one single argument unnamed argument that isn't + // a json/jsonb/text as it won't be considered by PostgREST + if ( + inArgs.length === 1 && + inArgs[0].name === '' && + (VALID_UNNAMED_FUNCTION_ARG_TYPES.has(inArgs[0].type_id) || + // OR if the function have a single unnamed args which is another table (embeded function) + (inArgs[0].table_name && func.return_table_name) || + // OR if the function takes a table row but doesn't qualify as embedded (for error reporting) + (inArgs[0].table_name && !func.return_table_name)) + ) { + return true + } + + // NOTE: Functions with named table arguments are generally excluded + // as they're not supported by PostgREST in the expected way + + return false + }) + .reduce( + (acc, curr) => { + acc[curr.name] ??= [] + acc[curr.name].push(curr) + return acc + }, + {} as Record + ) + + return Object.entries(schemaFunctionsGroupedByName).map(([fnName, _fns]) => { + // Check for function overload conflicts + const fns = _fns.toSorted((a, b) => b.definition.localeCompare(a.definition)) + + const functionSignatures = fns.map((fn) => { + const inArgs = fn.args.filter(({ mode }) => VALID_FUNCTION_ARGS_MODE.has(mode)) + + // Special error case for functions that take table row but don't qualify as embedded functions + const hasTableRowError = (fn: PostgresFunction) => { + if ( + inArgs.length === 1 && + inArgs[0].name === '' && + inArgs[0].table_name && + !fn.return_table_name + ) { + return true + } + return false + } + + // Check for generic conflict cases that need error reporting + const getConflictError = (fn: PostgresFunction) => { + const sameFunctions = fns.filter((f) => f.name === fn.name) + if (sameFunctions.length <= 1) return null + + // Generic conflict detection patterns - const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = types.find(({ id }) => id === type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return { name, type: tsType, has_default } + // Pattern 1: No-args vs default-args conflicts + if (inArgs.length === 0) { + const conflictingFns = sameFunctions.filter((otherFn) => { + if (otherFn === fn) return false + const otherInArgs = otherFn.args.filter(({ mode }) => + VALID_FUNCTION_ARGS_MODE.has(mode) + ) + return ( + otherInArgs.length === 1 && + otherInArgs[0].name === '' && + otherInArgs[0].has_default + ) + }) + + if (conflictingFns.length > 0) { + const conflictingFn = conflictingFns[0] + const returnTypeName = + types.find((t) => t.id === conflictingFn.return_type_id)?.name || + 'unknown' + 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` + } + } + + // Pattern 2: Same parameter name but different types (unresolvable overloads) + if (inArgs.length === 1 && inArgs[0].name !== '') { + const conflictingFns = sameFunctions.filter((otherFn) => { + if (otherFn === fn) return false + const otherInArgs = otherFn.args.filter(({ mode }) => + VALID_FUNCTION_ARGS_MODE.has(mode) + ) + return ( + otherInArgs.length === 1 && + otherInArgs[0].name === inArgs[0].name && + otherInArgs[0].type_id !== inArgs[0].type_id + ) + }) + + if (conflictingFns.length > 0) { + const allConflictingFunctions = [fn, ...conflictingFns] + const conflictList = allConflictingFunctions + .sort((a, b) => { + const aArgs = a.args.filter(({ mode }) => + VALID_FUNCTION_ARGS_MODE.has(mode) + ) + const bArgs = b.args.filter(({ mode }) => + VALID_FUNCTION_ARGS_MODE.has(mode) + ) + return (aArgs[0]?.type_id || 0) - (bArgs[0]?.type_id || 0) }) - return `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` - }) - .toSorted() - // A function can have multiples definitions with differents args, but will always return the same type - .join(' | ')} - Returns: ${(() => { - // Case 1: `returns table`. - const tableArgs = fns[0].args.filter(({ mode }) => mode === 'table') - if (tableArgs.length > 0) { - const argsNameAndType = tableArgs.map(({ name, type_id }) => { - const type = types.find(({ id }) => id === type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return { name, type: tsType } + .map((f) => { + const args = f.args.filter(({ mode }) => + VALID_FUNCTION_ARGS_MODE.has(mode) + ) + return `${schema.name}.${fn.name}(${args.map((a) => `${a.name || ''} => ${types.find((t) => t.id === a.type_id)?.name || 'unknown'}`).join(', ')})` }) + .join(', ') - return `{ - ${argsNameAndType.map( - ({ name, type }) => `${JSON.stringify(name)}: ${type}` - )} - }` - } + 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` + } + } - // Case 2: returns a relation's row type. - const relation = [...tables, ...views].find( - ({ id }) => id === fns[0].return_type_relation_id - ) - if (relation) { - return `{ - ${columnsByTableId[relation.id].map( - (column) => - `${JSON.stringify(column.name)}: ${pgTypeToTsType( - schema, - column.format, - { - types, - schemas, - tables, - views, - } - )} ${column.is_nullable ? '| null' : ''}` - )} - }` - } + return null + } + + let argsType = 'never' + let returnType = getFunctionReturnType(schema, fn) - // Case 3: returns base/array/composite/enum type. - const type = types.find(({ id }) => id === fns[0].return_type_id) + // Check for specific error cases + const conflictError = getConflictError(fn) + if (conflictError) { + if (inArgs.length > 0) { + const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { + const type = types.find(({ id }) => id === type_id) + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(schema, type.name, { + types, + schemas, + tables, + views, + }) + } + return { name, type: tsType, has_default } + }) + argsType = `{ ${argsNameAndType.toSorted((a, b) => a.name.localeCompare(b.name)).map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` + } + returnType = `{ error: true } & ${JSON.stringify(conflictError)}` + } else if (hasTableRowError(fn)) { + // Special case for computed fields returning scalars functions + if (inArgs.length > 0) { + const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { + const type = types.find(({ id }) => id === type_id) + let tsType = 'unknown' if (type) { - return pgTypeToTsType(schema, type.name, { + tsType = pgTypeToTsType(schema, type.name, { types, schemas, tables, views, }) } + return { name, type: tsType, has_default } + }) + argsType = `{ ${argsNameAndType.toSorted((a, b) => a.name.localeCompare(b.name)).map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` + } + 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`)}` + } else if (inArgs.length > 0) { + const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { + const type = types.find(({ id }) => id === type_id) + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(schema, type.name, { + types, + schemas, + tables, + views, + }) + } + return { name, type: tsType, has_default } + }) + argsType = `{ ${argsNameAndType.toSorted((a, b) => a.name.localeCompare(b.name)).map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` + } + + return `{ Args: ${argsType}; Returns: ${getFunctionTsReturnType(fn, returnType)} }` + }) - return 'unknown' - })()}${fns[0].is_set_returning_function ? '[]' : ''} - }` - ) + return `${JSON.stringify(fnName)}:\n${functionSignatures.map((sig) => `| ${sig}`).join('\n')}` + }) })()} } Enums: { diff --git a/test/db/00-init.sql b/test/db/00-init.sql index 00c6a472..ae5b8200 100644 --- a/test/db/00-init.sql +++ b/test/db/00-init.sql @@ -8,7 +8,8 @@ CREATE TYPE composite_type_with_array_attribute AS (my_text_array text[]); CREATE TABLE public.users ( id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text, - status user_status DEFAULT 'ACTIVE' + status user_status DEFAULT 'ACTIVE', + decimal numeric ); INSERT INTO public.users (name) @@ -55,6 +56,17 @@ $$ language plpgsql; CREATE VIEW todos_view AS SELECT * FROM public.todos; -- For testing typegen on view-to-view relationships create view users_view as select * from public.users; +-- Create a more complex view for testing +CREATE VIEW user_todos_summary_view AS +SELECT + u.id as user_id, + u.name as user_name, + u.status as user_status, + COUNT(t.id) as todo_count, + array_agg(t.details) FILTER (WHERE t.details IS NOT NULL) as todo_details +FROM public.users u +LEFT JOIN public.todos t ON t."user-id" = u.id +GROUP BY u.id, u.name, u.status; create materialized view todos_matview as select * from public.todos; @@ -68,6 +80,11 @@ $$ select substring($1.details, 1, 3); $$ language sql stable; +create function public.blurb_varchar(public.todos_view) returns character varying as +$$ +select substring($1.details, 1, 3); +$$ language sql stable; + create function public.details_length(public.todos) returns integer as $$ select length($1.details); @@ -100,6 +117,15 @@ as $$ select * from public.users limit 1; $$; +create or replace function public.function_returning_single_row(todos public.todos) +returns public.users +language sql +stable +as $$ + select * from public.users limit 1; +$$; + + create or replace function public.function_returning_set_of_rows() returns setof public.users language sql @@ -116,6 +142,15 @@ as $$ select id, name from public.users; $$; +create or replace function public.function_returning_table_with_args(user_id int) +returns table (id int, name text) +language sql +stable +as $$ + select id, name from public.users WHERE id = user_id; +$$; + + create or replace function public.polymorphic_function(text) returns void language sql as ''; create or replace function public.polymorphic_function(bool) returns void language sql as ''; @@ -181,3 +216,202 @@ LANGUAGE SQL STABLE AS $$ SELECT * FROM public.todos WHERE "user-id" = todo_row."user-id"; $$; + +-- SETOF composite_type - Returns multiple rows of a custom composite type +CREATE OR REPLACE FUNCTION public.get_composite_type_data() +RETURNS SETOF composite_type_with_array_attribute +LANGUAGE SQL STABLE +AS $$ + SELECT ROW(ARRAY['hello', 'world']::text[])::composite_type_with_array_attribute + UNION ALL + SELECT ROW(ARRAY['foo', 'bar']::text[])::composite_type_with_array_attribute; +$$; + +-- SETOF record - Returns multiple rows with structure defined in the function +CREATE OR REPLACE FUNCTION public.get_user_summary() +RETURNS SETOF record +LANGUAGE SQL STABLE +AS $$ + SELECT u.id, name, count(t.id) as todo_count + FROM public.users u + LEFT JOIN public.todos t ON t."user-id" = u.id + GROUP BY u.id, u.name; +$$; + +-- SETOF scalar_type - Returns multiple values of a basic type +CREATE OR REPLACE FUNCTION public.get_user_ids() +RETURNS SETOF bigint +LANGUAGE SQL STABLE +AS $$ + SELECT id FROM public.users; +$$; + + +-- Function returning view using scalar as input +CREATE OR REPLACE FUNCTION public.get_single_user_summary_from_view(search_user_id bigint) +RETURNS SETOF user_todos_summary_view +LANGUAGE SQL STABLE +ROWS 1 +AS $$ + SELECT * FROM user_todos_summary_view WHERE user_id = search_user_id; +$$; +-- Function returning view using table row as input +CREATE OR REPLACE FUNCTION public.get_single_user_summary_from_view(user_row users) +RETURNS SETOF user_todos_summary_view +LANGUAGE SQL STABLE +ROWS 1 +AS $$ + SELECT * FROM user_todos_summary_view WHERE user_id = user_row.id; +$$; +-- Function returning view using another view row as input +CREATE OR REPLACE FUNCTION public.get_single_user_summary_from_view(userview_row users_view) +RETURNS SETOF user_todos_summary_view +LANGUAGE SQL STABLE +ROWS 1 +AS $$ + SELECT * FROM user_todos_summary_view WHERE user_id = userview_row.id; +$$; + + +-- Function returning view using scalar as input +CREATE OR REPLACE FUNCTION public.get_todos_from_user(search_user_id bigint) +RETURNS SETOF todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM todos WHERE "user-id" = search_user_id; +$$; +-- Function returning view using table row as input +CREATE OR REPLACE FUNCTION public.get_todos_from_user(user_row users) +RETURNS SETOF todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM todos WHERE "user-id" = user_row.id; +$$; +-- Function returning view using another view row as input +CREATE OR REPLACE FUNCTION public.get_todos_from_user(userview_row users_view) +RETURNS SETOF todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM todos WHERE "user-id" = userview_row.id; +$$; + +-- Valid postgresql function override but that produce an unresolvable postgrest function call +create function postgrest_unresolvable_function() returns void language sql as ''; +create function postgrest_unresolvable_function(a text) returns int language sql as 'select 1'; +create function postgrest_unresolvable_function(a int) returns text language sql as $$ + SELECT 'toto' +$$; +-- Valid postgresql function override with differents returns types depending of different arguments +create function postgrest_resolvable_with_override_function() returns void language sql as ''; +create function postgrest_resolvable_with_override_function(a text) returns int language sql as 'select 1'; +create function postgrest_resolvable_with_override_function(b int) returns text language sql as $$ + SELECT 'toto' +$$; +-- Function overrides returning setof tables +create function postgrest_resolvable_with_override_function(user_id bigint) returns setof users language sql stable as $$ + SELECT * FROM users WHERE id = user_id; +$$; +create function postgrest_resolvable_with_override_function(todo_id bigint, completed boolean) returns setof todos language sql stable as $$ + SELECT * FROM todos WHERE id = todo_id AND completed = completed; +$$; +-- Function override taking a table as argument and returning a setof +create function postgrest_resolvable_with_override_function(user_row users) returns setof todos language sql stable as $$ + SELECT * FROM todos WHERE "user-id" = user_row.id; +$$; + +create or replace function public.polymorphic_function_with_different_return(bool) returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_different_return(int) returns int language sql as 'SELECT 2'; +create or replace function public.polymorphic_function_with_different_return(text) returns text language sql as $$ SELECT 'foo' $$; + +create or replace function public.polymorphic_function_with_no_params_or_unnamed() returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_no_params_or_unnamed(bool) returns int language sql as 'SELECT 2'; +create or replace function public.polymorphic_function_with_no_params_or_unnamed(text) returns text language sql as $$ SELECT 'foo' $$; +-- 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 +create or replace function public.polymorphic_function_with_unnamed_integer(int) returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_json(json) returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_jsonb(jsonb) returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_text(text) returns int language sql as 'SELECT 1'; + +-- Functions with unnamed parameters that have default values +create or replace function public.polymorphic_function_with_unnamed_default() returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_default(int default 42) returns int language sql as 'SELECT 2'; +create or replace function public.polymorphic_function_with_unnamed_default(text default 'default') returns text language sql as $$ SELECT 'foo' $$; + +-- Functions with unnamed parameters that have default values and multiple overloads +create or replace function public.polymorphic_function_with_unnamed_default_overload() returns int language sql as 'SELECT 1'; +create or replace function public.polymorphic_function_with_unnamed_default_overload(int default 42) returns int language sql as 'SELECT 2'; +create or replace function public.polymorphic_function_with_unnamed_default_overload(text default 'default') returns text language sql as $$ SELECT 'foo' $$; +create or replace function public.polymorphic_function_with_unnamed_default_overload(bool default true) returns int language sql as 'SELECT 3'; + +-- Test function with unnamed row parameter returning setof +CREATE OR REPLACE FUNCTION public.test_unnamed_row_setof(todos) +RETURNS SETOF todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.todos WHERE "user-id" = $1."user-id"; +$$; + +CREATE OR REPLACE FUNCTION public.test_unnamed_row_setof(users) +RETURNS SETOF todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.todos WHERE "user-id" = $1."id"; +$$; + + +CREATE OR REPLACE FUNCTION public.test_unnamed_row_setof(user_id bigint) +RETURNS SETOF todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.todos WHERE "user-id" = user_id; +$$; + +-- Test function with unnamed row parameter returning scalar +CREATE OR REPLACE FUNCTION public.test_unnamed_row_scalar(todos) +RETURNS integer +LANGUAGE SQL STABLE +AS $$ + SELECT COUNT(*) FROM public.todos WHERE "user-id" = $1."user-id"; +$$; + +-- Test function with unnamed view row parameter +CREATE OR REPLACE FUNCTION public.test_unnamed_view_row(todos_view) +RETURNS SETOF todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.todos WHERE "user-id" = $1."user-id"; +$$; + +-- Test function with multiple unnamed row parameters +CREATE OR REPLACE FUNCTION public.test_unnamed_multiple_rows(users, todos) +RETURNS SETOF todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.todos + WHERE "user-id" = $1.id + AND id = $2.id; +$$; + +-- Test function with unnamed row parameter returning composite +CREATE OR REPLACE FUNCTION public.test_unnamed_row_composite(users) +RETURNS composite_type_with_array_attribute +LANGUAGE SQL STABLE +AS $$ + SELECT ROW(ARRAY[$1.name])::composite_type_with_array_attribute; +$$; + +-- Function that returns a single element +CREATE OR REPLACE FUNCTION public.function_using_table_returns(user_row users) +RETURNS todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.todos WHERE todos."user-id" = user_row.id LIMIT 1; +$$; + +CREATE OR REPLACE FUNCTION public.function_using_setof_rows_one(user_row users) +RETURNS SETOF todos +LANGUAGE SQL STABLE +ROWS 1 +AS $$ + SELECT * FROM public.todos WHERE todos."user-id" = user_row.id LIMIT 1; +$$; diff --git a/test/lib/functions.ts b/test/lib/functions.ts index 05de3244..084f2b8f 100644 --- a/test/lib/functions.ts +++ b/test/lib/functions.ts @@ -12,12 +12,14 @@ test('list', async () => { "has_default": false, "mode": "in", "name": "", + "table_name": null, "type_id": 23, }, { "has_default": false, "mode": "in", "name": "", + "table_name": null, "type_id": 23, }, ], @@ -36,9 +38,12 @@ test('list', async () => { "is_set_returning_function": false, "language": "sql", "name": "add", + "return_table_name": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, + "returns_multiple_rows": false, + "returns_set_of_table": false, "schema": "public", "security_definer": false, } @@ -46,6 +51,137 @@ test('list', async () => { ) }) +test('list set-returning function with single object limit', async () => { + const res = await pgMeta.functions.list() + expect(res.data?.filter(({ name }) => name === 'get_user_audit_setof_single_row')) + .toMatchInlineSnapshot(` + [ + { + "args": [ + { + "has_default": false, + "mode": "in", + "name": "user_row", + "table_name": "users", + "type_id": 16395, + }, + ], + "argument_types": "user_row users", + "behavior": "STABLE", + "complete_statement": "CREATE OR REPLACE FUNCTION public.get_user_audit_setof_single_row(user_row users) + RETURNS SETOF users_audit + LANGUAGE sql + STABLE ROWS 1 + AS $function$ + SELECT * FROM public.users_audit WHERE user_id = user_row.id; + $function$ + ", + "config_params": null, + "definition": " + SELECT * FROM public.users_audit WHERE user_id = user_row.id; + ", + "id": 16506, + "identity_argument_types": "user_row users", + "is_set_returning_function": true, + "language": "sql", + "name": "get_user_audit_setof_single_row", + "return_table_name": "users_audit", + "return_type": "SETOF users_audit", + "return_type_id": 16418, + "return_type_relation_id": 16416, + "returns_multiple_rows": false, + "returns_set_of_table": true, + "schema": "public", + "security_definer": false, + }, + ] + `) +}) + +test('list set-returning function with multiples definitions', async () => { + const res = await pgMeta.functions.list() + expect(res.data?.filter(({ name }) => name === 'get_todos_setof_rows')).toMatchInlineSnapshot(` + [ + { + "args": [ + { + "has_default": false, + "mode": "in", + "name": "user_row", + "table_name": "users", + "type_id": 16395, + }, + ], + "argument_types": "user_row users", + "behavior": "STABLE", + "complete_statement": "CREATE OR REPLACE FUNCTION public.get_todos_setof_rows(user_row users) + RETURNS SETOF todos + LANGUAGE sql + STABLE + AS $function$ + SELECT * FROM public.todos WHERE "user-id" = user_row.id; + $function$ + ", + "config_params": null, + "definition": " + SELECT * FROM public.todos WHERE "user-id" = user_row.id; + ", + "id": 16507, + "identity_argument_types": "user_row users", + "is_set_returning_function": true, + "language": "sql", + "name": "get_todos_setof_rows", + "return_table_name": "todos", + "return_type": "SETOF todos", + "return_type_id": 16404, + "return_type_relation_id": 16402, + "returns_multiple_rows": true, + "returns_set_of_table": true, + "schema": "public", + "security_definer": false, + }, + { + "args": [ + { + "has_default": false, + "mode": "in", + "name": "todo_row", + "table_name": "todos", + "type_id": 16404, + }, + ], + "argument_types": "todo_row todos", + "behavior": "STABLE", + "complete_statement": "CREATE OR REPLACE FUNCTION public.get_todos_setof_rows(todo_row todos) + RETURNS SETOF todos + LANGUAGE sql + STABLE + AS $function$ + SELECT * FROM public.todos WHERE "user-id" = todo_row."user-id"; + $function$ + ", + "config_params": null, + "definition": " + SELECT * FROM public.todos WHERE "user-id" = todo_row."user-id"; + ", + "id": 16508, + "identity_argument_types": "todo_row todos", + "is_set_returning_function": true, + "language": "sql", + "name": "get_todos_setof_rows", + "return_table_name": "todos", + "return_type": "SETOF todos", + "return_type_id": 16404, + "return_type_relation_id": 16402, + "returns_multiple_rows": true, + "returns_set_of_table": true, + "schema": "public", + "security_definer": false, + }, + ] + `) +}) + test('list functions with included schemas', async () => { let res = await pgMeta.functions.list({ includedSchemas: ['public'], @@ -107,12 +243,14 @@ test('retrieve, create, update, delete', async () => { "has_default": false, "mode": "in", "name": "a", + "table_name": null, "type_id": 21, }, { "has_default": false, "mode": "in", "name": "b", + "table_name": null, "type_id": 21, }, ], @@ -136,9 +274,12 @@ test('retrieve, create, update, delete', async () => { "is_set_returning_function": false, "language": "sql", "name": "test_func", + "return_table_name": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, + "returns_multiple_rows": false, + "returns_set_of_table": false, "schema": "public", "security_definer": true, }, @@ -157,12 +298,14 @@ test('retrieve, create, update, delete', async () => { "has_default": false, "mode": "in", "name": "a", + "table_name": null, "type_id": 21, }, { "has_default": false, "mode": "in", "name": "b", + "table_name": null, "type_id": 21, }, ], @@ -186,9 +329,12 @@ test('retrieve, create, update, delete', async () => { "is_set_returning_function": false, "language": "sql", "name": "test_func", + "return_table_name": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, + "returns_multiple_rows": false, + "returns_set_of_table": false, "schema": "public", "security_definer": true, }, @@ -211,12 +357,14 @@ test('retrieve, create, update, delete', async () => { "has_default": false, "mode": "in", "name": "a", + "table_name": null, "type_id": 21, }, { "has_default": false, "mode": "in", "name": "b", + "table_name": null, "type_id": 21, }, ], @@ -240,9 +388,12 @@ test('retrieve, create, update, delete', async () => { "is_set_returning_function": false, "language": "sql", "name": "test_func_renamed", + "return_table_name": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, + "returns_multiple_rows": false, + "returns_set_of_table": false, "schema": "test_schema", "security_definer": true, }, @@ -261,12 +412,14 @@ test('retrieve, create, update, delete', async () => { "has_default": false, "mode": "in", "name": "a", + "table_name": null, "type_id": 21, }, { "has_default": false, "mode": "in", "name": "b", + "table_name": null, "type_id": 21, }, ], @@ -290,9 +443,12 @@ test('retrieve, create, update, delete', async () => { "is_set_returning_function": false, "language": "sql", "name": "test_func_renamed", + "return_table_name": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, + "returns_multiple_rows": false, + "returns_set_of_table": false, "schema": "test_schema", "security_definer": true, }, @@ -345,9 +501,12 @@ test('retrieve set-returning function', async () => { "is_set_returning_function": true, "language": "sql", "name": "function_returning_set_of_rows", + "return_table_name": "users", "return_type": "SETOF users", "return_type_id": Any, "return_type_relation_id": Any, + "returns_multiple_rows": true, + "returns_set_of_table": true, "schema": "public", "security_definer": false, } diff --git a/test/lib/tables.ts b/test/lib/tables.ts index c4c934e7..c35546b8 100644 --- a/test/lib/tables.ts +++ b/test/lib/tables.ts @@ -78,6 +78,24 @@ test('list', async () => { "schema": "public", "table": "users", }, + { + "check": null, + "comment": null, + "data_type": "numeric", + "default_value": null, + "enums": [], + "format": "numeric", + "identity_generation": null, + "is_generated": false, + "is_identity": false, + "is_nullable": true, + "is_unique": false, + "is_updatable": true, + "name": "decimal", + "ordinal_position": 4, + "schema": "public", + "table": "users", + }, { "check": null, "comment": null, diff --git a/test/server/query.ts b/test/server/query.ts index 2b4bc2ba..8a9d6076 100644 --- a/test/server/query.ts +++ b/test/server/query.ts @@ -10,11 +10,13 @@ test('query', async () => { expect(res.json()).toMatchInlineSnapshot(` [ { + "decimal": null, "id": 1, "name": "Joe Bloggs", "status": "ACTIVE", }, { + "decimal": null, "id": 2, "name": "Jane Doe", "status": "ACTIVE", diff --git a/test/server/typegen.ts b/test/server/typegen.ts index fa47cbec..bdcddb39 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -129,6 +129,12 @@ test('typegen: typescript', async () => { details_is_long: boolean | null details_length: number | null details_words: string[] | null + test_unnamed_row_scalar: number | null + test_unnamed_row_setof: { + details: string | null + id: number + "user-id": number + } | null } Insert: { details?: string | null @@ -147,6 +153,12 @@ test('typegen: typescript', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -193,6 +205,12 @@ test('typegen: typescript', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "user_details_user_id_fkey" + columns: ["user_id"] + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "user_details_user_id_fkey" columns: ["user_id"] @@ -221,16 +239,27 @@ test('typegen: typescript', async () => { } users: { Row: { + decimal: number | null id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + test_unnamed_row_composite: + | Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"] + | null + test_unnamed_row_setof: { + details: string | null + id: number + "user-id": number + } | null } Insert: { + decimal?: number | null id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null } Update: { + decimal?: number | null id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null @@ -285,6 +314,12 @@ test('typegen: typescript', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -316,6 +351,12 @@ test('typegen: typescript', async () => { details: string | null id: number | null "user-id": number | null + blurb_varchar: string | null + test_unnamed_view_row: { + details: string | null + id: number + "user-id": number + } | null } Insert: { details?: string | null @@ -334,6 +375,12 @@ test('typegen: typescript', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -360,18 +407,31 @@ test('typegen: typescript', async () => { }, ] } + user_todos_summary_view: { + Row: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + Relationships: [] + } users_view: { Row: { + decimal: number | null id: number | null name: string | null status: Database["public"]["Enums"]["user_status"] | null } Insert: { + decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null } Update: { + decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null @@ -391,57 +451,263 @@ test('typegen: typescript', async () => { Functions: { blurb: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: string - } - blurb_varchar: { - Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: string - } + Returns: { + error: true + } & "the function public.blurb with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } + blurb_varchar: + | { + Args: { "": Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + error: true + } & "the function public.blurb_varchar with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } + | { + Args: { "": Database["public"]["Views"]["todos_view"]["Row"] } + Returns: { + error: true + } & "the function public.blurb_varchar with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } details_is_long: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: boolean + Returns: { + error: true + } & "the function public.details_is_long with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } details_length: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: number + Returns: { + error: true + } & "the function public.details_length with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } details_words: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: string[] + Returns: { + error: true + } & "the function public.details_words with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } function_returning_row: { - Args: Record + Args: never Returns: { + decimal: number | null id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null } + SetofOptions: { + from: "*" + to: "users" + isOneToOne: true + isSetofReturn: false + } } function_returning_set_of_rows: { - Args: Record + Args: never Returns: { + decimal: number | null id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + isSetofReturn: true + } + } + function_returning_single_row: { + Args: { todos: Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + decimal: number | null + id: number + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "todos" + to: "users" + isOneToOne: true + isSetofReturn: false + } } function_returning_table: { - Args: Record + Args: never Returns: { id: number name: string }[] } - get_todos_setof_rows: { - Args: - | { todo_row: Database["public"]["Tables"]["todos"]["Row"] } - | { user_row: Database["public"]["Tables"]["users"]["Row"] } + function_returning_table_with_args: { + Args: { user_id: number } + Returns: { + id: number + name: string + }[] + } + function_using_setof_rows_one: { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } Returns: { details: string | null id: number "user-id": number - }[] + } + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: true + isSetofReturn: true + } } + function_using_table_returns: { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + } + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: true + isSetofReturn: false + } + } + get_composite_type_data: { + Args: never + Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"][] + SetofOptions: { + from: "*" + to: "composite_type_with_array_attribute" + isOneToOne: false + isSetofReturn: true + } + } + get_single_user_summary_from_view: + | { + Args: { + userview_row: Database["public"]["Views"]["users_view"]["Row"] + } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "users_view" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } + } + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "users" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } + } + | { + Args: { search_user_id: number } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "*" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } + } + get_todos_from_user: + | { + Args: { + userview_row: Database["public"]["Views"]["users_view"]["Row"] + } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users_view" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { search_user_id: number } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + get_todos_setof_rows: + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { todo_row: Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "todos" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } get_user_audit_setof_single_row: { Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } Returns: { @@ -449,31 +715,193 @@ test('typegen: typescript', async () => { id: number previous_value: Json | null user_id: number | null - }[] - } - polymorphic_function: { - Args: { "": boolean } | { "": string } - Returns: undefined + } + SetofOptions: { + from: "users" + to: "users_audit" + isOneToOne: true + isSetofReturn: true + } } - postgres_fdw_disconnect: { + get_user_ids: { Args: never; Returns: number[] } + get_user_summary: { Args: never; Returns: Record[] } + polymorphic_function: { Args: { "": string }; Returns: undefined } + polymorphic_function_with_different_return: { Args: { "": string } - Returns: boolean + Returns: string + } + polymorphic_function_with_no_params_or_unnamed: + | { Args: never; Returns: number } + | { Args: { "": string }; Returns: string } + polymorphic_function_with_unnamed_default: + | { + Args: never + Returns: { + error: true + } & "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default(), public.polymorphic_function_with_unnamed_default( => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { Args: { ""?: string }; Returns: string } + polymorphic_function_with_unnamed_default_overload: + | { + Args: never + Returns: { + error: true + } & "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default_overload(), public.polymorphic_function_with_unnamed_default_overload( => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { Args: { ""?: string }; Returns: string } + polymorphic_function_with_unnamed_json: { + Args: { "": Json } + Returns: number } - postgres_fdw_disconnect_all: { - Args: Record - Returns: boolean + polymorphic_function_with_unnamed_jsonb: { + Args: { "": Json } + Returns: number } + polymorphic_function_with_unnamed_text: { + Args: { "": string } + Returns: number + } + postgres_fdw_disconnect: { Args: { "": string }; Returns: boolean } + postgres_fdw_disconnect_all: { Args: never; Returns: boolean } postgres_fdw_get_connections: { - Args: Record + Args: never Returns: Record[] } - postgres_fdw_handler: { - Args: Record - Returns: unknown - } - test_internal_query: { - Args: Record - Returns: undefined + postgres_fdw_handler: { Args: never; Returns: unknown } + postgrest_resolvable_with_override_function: + | { Args: { a: string }; Returns: number } + | { Args: { b: number }; Returns: string } + | { + Args: { user_id: number } + Returns: { + decimal: number | null + id: number + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { completed: boolean; todo_id: number } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { Args: never; Returns: undefined } + postgrest_unresolvable_function: + | { + Args: { a: string } + Returns: { + error: true + } & "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { + Args: { a: number } + Returns: { + error: true + } & "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { Args: never; Returns: undefined } + test_internal_query: { Args: never; Returns: undefined } + test_unnamed_row_composite: { + Args: { "": Database["public"]["Tables"]["users"]["Row"] } + Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"] + SetofOptions: { + from: "users" + to: "composite_type_with_array_attribute" + isOneToOne: true + isSetofReturn: false + } + } + test_unnamed_row_scalar: { + Args: { "": Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + error: true + } & "the function public.test_unnamed_row_scalar with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } + test_unnamed_row_setof: + | { + Args: { user_id: number } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { "": Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "todos" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { "": Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + test_unnamed_view_row: { + Args: { "": Database["public"]["Views"]["todos_view"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "todos_view" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } } Enums: { @@ -754,6 +1182,12 @@ test('typegen w/ one-to-one relationships', async () => { details_is_long: boolean | null details_length: number | null details_words: string[] | null + test_unnamed_row_scalar: number | null + test_unnamed_row_setof: { + details: string | null + id: number + "user-id": number + } | null } Insert: { details?: string | null @@ -773,6 +1207,13 @@ test('typegen w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -824,6 +1265,13 @@ test('typegen w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "user_details_user_id_fkey" + columns: ["user_id"] + isOneToOne: true + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "user_details_user_id_fkey" columns: ["user_id"] @@ -856,16 +1304,27 @@ test('typegen w/ one-to-one relationships', async () => { } users: { Row: { + decimal: number | null id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + test_unnamed_row_composite: + | Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"] + | null + test_unnamed_row_setof: { + details: string | null + id: number + "user-id": number + } | null } Insert: { + decimal?: number | null id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null } Update: { + decimal?: number | null id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null @@ -921,6 +1380,13 @@ test('typegen w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -956,6 +1422,12 @@ test('typegen w/ one-to-one relationships', async () => { details: string | null id: number | null "user-id": number | null + blurb_varchar: string | null + test_unnamed_view_row: { + details: string | null + id: number + "user-id": number + } | null } Insert: { details?: string | null @@ -975,6 +1447,13 @@ test('typegen w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -1005,18 +1484,31 @@ test('typegen w/ one-to-one relationships', async () => { }, ] } + user_todos_summary_view: { + Row: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + Relationships: [] + } users_view: { Row: { + decimal: number | null id: number | null name: string | null status: Database["public"]["Enums"]["user_status"] | null } Insert: { + decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null } Update: { + decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null @@ -1036,57 +1528,263 @@ test('typegen w/ one-to-one relationships', async () => { Functions: { blurb: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: string - } - blurb_varchar: { - Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: string - } + Returns: { + error: true + } & "the function public.blurb with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } + blurb_varchar: + | { + Args: { "": Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + error: true + } & "the function public.blurb_varchar with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } + | { + Args: { "": Database["public"]["Views"]["todos_view"]["Row"] } + Returns: { + error: true + } & "the function public.blurb_varchar with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } details_is_long: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: boolean + Returns: { + error: true + } & "the function public.details_is_long with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } details_length: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: number + Returns: { + error: true + } & "the function public.details_length with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } details_words: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: string[] + Returns: { + error: true + } & "the function public.details_words with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } function_returning_row: { - Args: Record + Args: never Returns: { + decimal: number | null id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null } + SetofOptions: { + from: "*" + to: "users" + isOneToOne: true + isSetofReturn: false + } } function_returning_set_of_rows: { - Args: Record + Args: never Returns: { + decimal: number | null id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + isSetofReturn: true + } + } + function_returning_single_row: { + Args: { todos: Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + decimal: number | null + id: number + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "todos" + to: "users" + isOneToOne: true + isSetofReturn: false + } } function_returning_table: { - Args: Record + Args: never Returns: { id: number name: string }[] } - get_todos_setof_rows: { - Args: - | { todo_row: Database["public"]["Tables"]["todos"]["Row"] } - | { user_row: Database["public"]["Tables"]["users"]["Row"] } + function_returning_table_with_args: { + Args: { user_id: number } + Returns: { + id: number + name: string + }[] + } + function_using_setof_rows_one: { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } Returns: { details: string | null id: number "user-id": number - }[] + } + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: true + isSetofReturn: true + } } + function_using_table_returns: { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + } + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: true + isSetofReturn: false + } + } + get_composite_type_data: { + Args: never + Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"][] + SetofOptions: { + from: "*" + to: "composite_type_with_array_attribute" + isOneToOne: false + isSetofReturn: true + } + } + get_single_user_summary_from_view: + | { + Args: { + userview_row: Database["public"]["Views"]["users_view"]["Row"] + } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "users_view" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } + } + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "users" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } + } + | { + Args: { search_user_id: number } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "*" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } + } + get_todos_from_user: + | { + Args: { + userview_row: Database["public"]["Views"]["users_view"]["Row"] + } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users_view" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { search_user_id: number } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + get_todos_setof_rows: + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { todo_row: Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "todos" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } get_user_audit_setof_single_row: { Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } Returns: { @@ -1094,31 +1792,193 @@ test('typegen w/ one-to-one relationships', async () => { id: number previous_value: Json | null user_id: number | null - }[] - } - polymorphic_function: { - Args: { "": boolean } | { "": string } - Returns: undefined + } + SetofOptions: { + from: "users" + to: "users_audit" + isOneToOne: true + isSetofReturn: true + } } - postgres_fdw_disconnect: { + get_user_ids: { Args: never; Returns: number[] } + get_user_summary: { Args: never; Returns: Record[] } + polymorphic_function: { Args: { "": string }; Returns: undefined } + polymorphic_function_with_different_return: { Args: { "": string } - Returns: boolean + Returns: string } - postgres_fdw_disconnect_all: { - Args: Record - Returns: boolean + polymorphic_function_with_no_params_or_unnamed: + | { Args: never; Returns: number } + | { Args: { "": string }; Returns: string } + polymorphic_function_with_unnamed_default: + | { + Args: never + Returns: { + error: true + } & "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default(), public.polymorphic_function_with_unnamed_default( => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { Args: { ""?: string }; Returns: string } + polymorphic_function_with_unnamed_default_overload: + | { + Args: never + Returns: { + error: true + } & "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default_overload(), public.polymorphic_function_with_unnamed_default_overload( => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { Args: { ""?: string }; Returns: string } + polymorphic_function_with_unnamed_json: { + Args: { "": Json } + Returns: number + } + polymorphic_function_with_unnamed_jsonb: { + Args: { "": Json } + Returns: number } + polymorphic_function_with_unnamed_text: { + Args: { "": string } + Returns: number + } + postgres_fdw_disconnect: { Args: { "": string }; Returns: boolean } + postgres_fdw_disconnect_all: { Args: never; Returns: boolean } postgres_fdw_get_connections: { - Args: Record + Args: never Returns: Record[] } - postgres_fdw_handler: { - Args: Record - Returns: unknown - } - test_internal_query: { - Args: Record - Returns: undefined + postgres_fdw_handler: { Args: never; Returns: unknown } + postgrest_resolvable_with_override_function: + | { Args: { a: string }; Returns: number } + | { Args: { b: number }; Returns: string } + | { + Args: { user_id: number } + Returns: { + decimal: number | null + id: number + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { completed: boolean; todo_id: number } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { Args: never; Returns: undefined } + postgrest_unresolvable_function: + | { + Args: { a: string } + Returns: { + error: true + } & "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { + Args: { a: number } + Returns: { + error: true + } & "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { Args: never; Returns: undefined } + test_internal_query: { Args: never; Returns: undefined } + test_unnamed_row_composite: { + Args: { "": Database["public"]["Tables"]["users"]["Row"] } + Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"] + SetofOptions: { + from: "users" + to: "composite_type_with_array_attribute" + isOneToOne: true + isSetofReturn: false + } + } + test_unnamed_row_scalar: { + Args: { "": Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + error: true + } & "the function public.test_unnamed_row_scalar with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } + test_unnamed_row_setof: + | { + Args: { user_id: number } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { "": Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "todos" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { "": Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + test_unnamed_view_row: { + Args: { "": Database["public"]["Views"]["todos_view"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "todos_view" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } } Enums: { @@ -1399,6 +2259,12 @@ test('typegen: typescript w/ one-to-one relationships', async () => { details_is_long: boolean | null details_length: number | null details_words: string[] | null + test_unnamed_row_scalar: number | null + test_unnamed_row_setof: { + details: string | null + id: number + "user-id": number + } | null } Insert: { details?: string | null @@ -1418,6 +2284,13 @@ test('typegen: typescript w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -1469,6 +2342,13 @@ test('typegen: typescript w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "user_details_user_id_fkey" + columns: ["user_id"] + isOneToOne: true + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "user_details_user_id_fkey" columns: ["user_id"] @@ -1501,16 +2381,27 @@ test('typegen: typescript w/ one-to-one relationships', async () => { } users: { Row: { + decimal: number | null id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + test_unnamed_row_composite: + | Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"] + | null + test_unnamed_row_setof: { + details: string | null + id: number + "user-id": number + } | null } Insert: { + decimal?: number | null id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null } Update: { + decimal?: number | null id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null @@ -1566,6 +2457,13 @@ test('typegen: typescript w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -1601,6 +2499,12 @@ test('typegen: typescript w/ one-to-one relationships', async () => { details: string | null id: number | null "user-id": number | null + blurb_varchar: string | null + test_unnamed_view_row: { + details: string | null + id: number + "user-id": number + } | null } Insert: { details?: string | null @@ -1620,6 +2524,13 @@ test('typegen: typescript w/ one-to-one relationships', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -1650,18 +2561,31 @@ test('typegen: typescript w/ one-to-one relationships', async () => { }, ] } + user_todos_summary_view: { + Row: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + Relationships: [] + } users_view: { Row: { + decimal: number | null id: number | null name: string | null status: Database["public"]["Enums"]["user_status"] | null } Insert: { + decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null } Update: { + decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null @@ -1681,57 +2605,263 @@ test('typegen: typescript w/ one-to-one relationships', async () => { Functions: { blurb: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: string - } - blurb_varchar: { - Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: string - } + Returns: { + error: true + } & "the function public.blurb with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } + blurb_varchar: + | { + Args: { "": Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + error: true + } & "the function public.blurb_varchar with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } + | { + Args: { "": Database["public"]["Views"]["todos_view"]["Row"] } + Returns: { + error: true + } & "the function public.blurb_varchar with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } details_is_long: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: boolean + Returns: { + error: true + } & "the function public.details_is_long with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } details_length: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: number + Returns: { + error: true + } & "the function public.details_length with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } details_words: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: string[] + Returns: { + error: true + } & "the function public.details_words with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } function_returning_row: { - Args: Record + Args: never Returns: { + decimal: number | null id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null } + SetofOptions: { + from: "*" + to: "users" + isOneToOne: true + isSetofReturn: false + } } function_returning_set_of_rows: { - Args: Record + Args: never Returns: { + decimal: number | null id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + isSetofReturn: true + } + } + function_returning_single_row: { + Args: { todos: Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + decimal: number | null + id: number + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "todos" + to: "users" + isOneToOne: true + isSetofReturn: false + } } function_returning_table: { - Args: Record + Args: never Returns: { id: number name: string }[] } - get_todos_setof_rows: { - Args: - | { todo_row: Database["public"]["Tables"]["todos"]["Row"] } - | { user_row: Database["public"]["Tables"]["users"]["Row"] } + function_returning_table_with_args: { + Args: { user_id: number } + Returns: { + id: number + name: string + }[] + } + function_using_setof_rows_one: { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } Returns: { details: string | null id: number "user-id": number - }[] + } + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: true + isSetofReturn: true + } } + function_using_table_returns: { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + } + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: true + isSetofReturn: false + } + } + get_composite_type_data: { + Args: never + Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"][] + SetofOptions: { + from: "*" + to: "composite_type_with_array_attribute" + isOneToOne: false + isSetofReturn: true + } + } + get_single_user_summary_from_view: + | { + Args: { + userview_row: Database["public"]["Views"]["users_view"]["Row"] + } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "users_view" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } + } + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "users" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } + } + | { + Args: { search_user_id: number } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "*" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } + } + get_todos_from_user: + | { + Args: { + userview_row: Database["public"]["Views"]["users_view"]["Row"] + } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users_view" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { search_user_id: number } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + get_todos_setof_rows: + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { todo_row: Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "todos" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } get_user_audit_setof_single_row: { Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } Returns: { @@ -1739,31 +2869,193 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number previous_value: Json | null user_id: number | null - }[] - } - polymorphic_function: { - Args: { "": boolean } | { "": string } - Returns: undefined + } + SetofOptions: { + from: "users" + to: "users_audit" + isOneToOne: true + isSetofReturn: true + } } - postgres_fdw_disconnect: { + get_user_ids: { Args: never; Returns: number[] } + get_user_summary: { Args: never; Returns: Record[] } + polymorphic_function: { Args: { "": string }; Returns: undefined } + polymorphic_function_with_different_return: { Args: { "": string } - Returns: boolean + Returns: string + } + polymorphic_function_with_no_params_or_unnamed: + | { Args: never; Returns: number } + | { Args: { "": string }; Returns: string } + polymorphic_function_with_unnamed_default: + | { + Args: never + Returns: { + error: true + } & "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default(), public.polymorphic_function_with_unnamed_default( => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { Args: { ""?: string }; Returns: string } + polymorphic_function_with_unnamed_default_overload: + | { + Args: never + Returns: { + error: true + } & "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default_overload(), public.polymorphic_function_with_unnamed_default_overload( => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { Args: { ""?: string }; Returns: string } + polymorphic_function_with_unnamed_json: { + Args: { "": Json } + Returns: number } - postgres_fdw_disconnect_all: { - Args: Record - Returns: boolean + polymorphic_function_with_unnamed_jsonb: { + Args: { "": Json } + Returns: number } + polymorphic_function_with_unnamed_text: { + Args: { "": string } + Returns: number + } + postgres_fdw_disconnect: { Args: { "": string }; Returns: boolean } + postgres_fdw_disconnect_all: { Args: never; Returns: boolean } postgres_fdw_get_connections: { - Args: Record + Args: never Returns: Record[] } - postgres_fdw_handler: { - Args: Record - Returns: unknown - } - test_internal_query: { - Args: Record - Returns: undefined + postgres_fdw_handler: { Args: never; Returns: unknown } + postgrest_resolvable_with_override_function: + | { Args: { a: string }; Returns: number } + | { Args: { b: number }; Returns: string } + | { + Args: { user_id: number } + Returns: { + decimal: number | null + id: number + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { completed: boolean; todo_id: number } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { Args: never; Returns: undefined } + postgrest_unresolvable_function: + | { + Args: { a: string } + Returns: { + error: true + } & "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { + Args: { a: number } + Returns: { + error: true + } & "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { Args: never; Returns: undefined } + test_internal_query: { Args: never; Returns: undefined } + test_unnamed_row_composite: { + Args: { "": Database["public"]["Tables"]["users"]["Row"] } + Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"] + SetofOptions: { + from: "users" + to: "composite_type_with_array_attribute" + isOneToOne: true + isSetofReturn: false + } + } + test_unnamed_row_scalar: { + Args: { "": Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + error: true + } & "the function public.test_unnamed_row_scalar with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } + test_unnamed_row_setof: + | { + Args: { user_id: number } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { "": Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "todos" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { "": Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + test_unnamed_view_row: { + Args: { "": Database["public"]["Views"]["todos_view"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "todos_view" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } } Enums: { @@ -1928,7 +3220,7 @@ test('typegen: typescript w/ postgrestVersion', async () => { | Json[] export type Database = { - // Allows to automatically instanciate createClient with right options + // Allows to automatically instantiate createClient with right options // instead of createClient(URL, KEY) __InternalSupabase: { PostgrestVersion: "13" @@ -2049,6 +3341,12 @@ test('typegen: typescript w/ postgrestVersion', async () => { details_is_long: boolean | null details_length: number | null details_words: string[] | null + test_unnamed_row_scalar: number | null + test_unnamed_row_setof: { + details: string | null + id: number + "user-id": number + } | null } Insert: { details?: string | null @@ -2068,6 +3366,13 @@ test('typegen: typescript w/ postgrestVersion', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -2119,6 +3424,13 @@ test('typegen: typescript w/ postgrestVersion', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "user_details_user_id_fkey" + columns: ["user_id"] + isOneToOne: true + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "user_details_user_id_fkey" columns: ["user_id"] @@ -2151,16 +3463,27 @@ test('typegen: typescript w/ postgrestVersion', async () => { } users: { Row: { + decimal: number | null id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + test_unnamed_row_composite: + | Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"] + | null + test_unnamed_row_setof: { + details: string | null + id: number + "user-id": number + } | null } Insert: { + decimal?: number | null id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null } Update: { + decimal?: number | null id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null @@ -2216,6 +3539,13 @@ test('typegen: typescript w/ postgrestVersion', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -2251,6 +3581,12 @@ test('typegen: typescript w/ postgrestVersion', async () => { details: string | null id: number | null "user-id": number | null + blurb_varchar: string | null + test_unnamed_view_row: { + details: string | null + id: number + "user-id": number + } | null } Insert: { details?: string | null @@ -2270,6 +3606,13 @@ test('typegen: typescript w/ postgrestVersion', async () => { referencedRelation: "a_view" referencedColumns: ["id"] }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "user_todos_summary_view" + referencedColumns: ["user_id"] + }, { foreignKeyName: "todos_user-id_fkey" columns: ["user-id"] @@ -2300,18 +3643,31 @@ test('typegen: typescript w/ postgrestVersion', async () => { }, ] } + user_todos_summary_view: { + Row: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + Relationships: [] + } users_view: { Row: { + decimal: number | null id: number | null name: string | null status: Database["public"]["Enums"]["user_status"] | null } Insert: { + decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null } Update: { + decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null @@ -2331,57 +3687,263 @@ test('typegen: typescript w/ postgrestVersion', async () => { Functions: { blurb: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: string - } - blurb_varchar: { - Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: string - } + Returns: { + error: true + } & "the function public.blurb with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } + blurb_varchar: + | { + Args: { "": Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + error: true + } & "the function public.blurb_varchar with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } + | { + Args: { "": Database["public"]["Views"]["todos_view"]["Row"] } + Returns: { + error: true + } & "the function public.blurb_varchar with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } details_is_long: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: boolean + Returns: { + error: true + } & "the function public.details_is_long with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } details_length: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: number + Returns: { + error: true + } & "the function public.details_length with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } details_words: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } - Returns: string[] + Returns: { + error: true + } & "the function public.details_words with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } function_returning_row: { - Args: Record + Args: never Returns: { + decimal: number | null id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null } + SetofOptions: { + from: "*" + to: "users" + isOneToOne: true + isSetofReturn: false + } } function_returning_set_of_rows: { - Args: Record + Args: never Returns: { + decimal: number | null id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + isSetofReturn: true + } + } + function_returning_single_row: { + Args: { todos: Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + decimal: number | null + id: number + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "todos" + to: "users" + isOneToOne: true + isSetofReturn: false + } } function_returning_table: { - Args: Record + Args: never Returns: { id: number name: string }[] } - get_todos_setof_rows: { - Args: - | { todo_row: Database["public"]["Tables"]["todos"]["Row"] } - | { user_row: Database["public"]["Tables"]["users"]["Row"] } + function_returning_table_with_args: { + Args: { user_id: number } + Returns: { + id: number + name: string + }[] + } + function_using_setof_rows_one: { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } Returns: { details: string | null id: number "user-id": number - }[] + } + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: true + isSetofReturn: true + } } + function_using_table_returns: { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + } + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: true + isSetofReturn: false + } + } + get_composite_type_data: { + Args: never + Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"][] + SetofOptions: { + from: "*" + to: "composite_type_with_array_attribute" + isOneToOne: false + isSetofReturn: true + } + } + get_single_user_summary_from_view: + | { + Args: { + userview_row: Database["public"]["Views"]["users_view"]["Row"] + } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "users_view" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } + } + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "users" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } + } + | { + Args: { search_user_id: number } + Returns: { + todo_count: number | null + todo_details: string[] | null + user_id: number | null + user_name: string | null + user_status: Database["public"]["Enums"]["user_status"] | null + } + SetofOptions: { + from: "*" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } + } + get_todos_from_user: + | { + Args: { + userview_row: Database["public"]["Views"]["users_view"]["Row"] + } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users_view" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { search_user_id: number } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + get_todos_setof_rows: + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { todo_row: Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "todos" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } get_user_audit_setof_single_row: { Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } Returns: { @@ -2389,31 +3951,193 @@ test('typegen: typescript w/ postgrestVersion', async () => { id: number previous_value: Json | null user_id: number | null - }[] - } - polymorphic_function: { - Args: { "": boolean } | { "": string } - Returns: undefined + } + SetofOptions: { + from: "users" + to: "users_audit" + isOneToOne: true + isSetofReturn: true + } } - postgres_fdw_disconnect: { + get_user_ids: { Args: never; Returns: number[] } + get_user_summary: { Args: never; Returns: Record[] } + polymorphic_function: { Args: { "": string }; Returns: undefined } + polymorphic_function_with_different_return: { Args: { "": string } - Returns: boolean + Returns: string } - postgres_fdw_disconnect_all: { - Args: Record - Returns: boolean + polymorphic_function_with_no_params_or_unnamed: + | { Args: never; Returns: number } + | { Args: { "": string }; Returns: string } + polymorphic_function_with_unnamed_default: + | { + Args: never + Returns: { + error: true + } & "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default(), public.polymorphic_function_with_unnamed_default( => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { Args: { ""?: string }; Returns: string } + polymorphic_function_with_unnamed_default_overload: + | { + Args: never + Returns: { + error: true + } & "Could not choose the best candidate function between: public.polymorphic_function_with_unnamed_default_overload(), public.polymorphic_function_with_unnamed_default_overload( => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { Args: { ""?: string }; Returns: string } + polymorphic_function_with_unnamed_json: { + Args: { "": Json } + Returns: number } + polymorphic_function_with_unnamed_jsonb: { + Args: { "": Json } + Returns: number + } + polymorphic_function_with_unnamed_text: { + Args: { "": string } + Returns: number + } + postgres_fdw_disconnect: { Args: { "": string }; Returns: boolean } + postgres_fdw_disconnect_all: { Args: never; Returns: boolean } postgres_fdw_get_connections: { - Args: Record + Args: never Returns: Record[] } - postgres_fdw_handler: { - Args: Record - Returns: unknown - } - test_internal_query: { - Args: Record - Returns: undefined + postgres_fdw_handler: { Args: never; Returns: unknown } + postgrest_resolvable_with_override_function: + | { Args: { a: string }; Returns: number } + | { Args: { b: number }; Returns: string } + | { + Args: { user_id: number } + Returns: { + decimal: number | null + id: number + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { completed: boolean; todo_id: number } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { user_row: Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { Args: never; Returns: undefined } + postgrest_unresolvable_function: + | { + Args: { a: string } + Returns: { + error: true + } & "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { + Args: { a: number } + Returns: { + error: true + } & "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" + } + | { Args: never; Returns: undefined } + test_internal_query: { Args: never; Returns: undefined } + test_unnamed_row_composite: { + Args: { "": Database["public"]["Tables"]["users"]["Row"] } + Returns: Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"] + SetofOptions: { + from: "users" + to: "composite_type_with_array_attribute" + isOneToOne: true + isSetofReturn: false + } + } + test_unnamed_row_scalar: { + Args: { "": Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + error: true + } & "the function public.test_unnamed_row_scalar with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" + } + test_unnamed_row_setof: + | { + Args: { user_id: number } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { "": Database["public"]["Tables"]["todos"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "todos" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + | { + Args: { "": Database["public"]["Tables"]["users"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "users" + to: "todos" + isOneToOne: false + isSetofReturn: true + } + } + test_unnamed_view_row: { + Args: { "": Database["public"]["Views"]["todos_view"]["Row"] } + Returns: { + details: string | null + id: number + "user-id": number + }[] + SetofOptions: { + from: "todos_view" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } } Enums: { @@ -2561,203 +4285,482 @@ test('typegen: typescript w/ postgrestVersion', async () => { ) }) +test('typegen: typescript consistent types definitions orders', async () => { + // Helper function to clean up test entities + const cleanupTestEntities = async () => { + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + -- Drop materialized views first (depend on views/tables) + DROP MATERIALIZED VIEW IF EXISTS test_matview_alpha CASCADE; + DROP MATERIALIZED VIEW IF EXISTS test_matview_beta CASCADE; + DROP MATERIALIZED VIEW IF EXISTS test_matview_gamma CASCADE; + + -- Drop views (may depend on tables) + DROP VIEW IF EXISTS test_view_alpha CASCADE; + DROP VIEW IF EXISTS test_view_beta CASCADE; + DROP VIEW IF EXISTS test_view_gamma CASCADE; + + -- Drop functions + DROP FUNCTION IF EXISTS test_func_alpha(integer, text, boolean) CASCADE; + DROP FUNCTION IF EXISTS test_func_beta(integer, text, boolean) CASCADE; + DROP FUNCTION IF EXISTS test_func_gamma(integer, text, boolean) CASCADE; + + -- Alternative signatures for functions (different parameter orders) + DROP FUNCTION IF EXISTS test_func_alpha(text, boolean, integer) CASCADE; + DROP FUNCTION IF EXISTS test_func_beta(boolean, integer, text) CASCADE; + DROP FUNCTION IF EXISTS test_func_gamma(boolean, text, integer) CASCADE; + + -- Drop tables + DROP TABLE IF EXISTS test_table_alpha CASCADE; + DROP TABLE IF EXISTS test_table_beta CASCADE; + DROP TABLE IF EXISTS test_table_gamma CASCADE; + + -- Drop types + DROP TYPE IF EXISTS test_enum_alpha CASCADE; + DROP TYPE IF EXISTS test_enum_beta CASCADE; + `, + }, + }) + } + + // Clean up any existing test entities + await cleanupTestEntities() + + // === FIRST ROUND: Create entities in order A->B->G with property order 1 === + + // Create custom types first + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + CREATE TYPE test_enum_alpha AS ENUM ('active', 'inactive', 'pending'); + CREATE TYPE test_enum_beta AS ENUM ('high', 'medium', 'low'); + `, + }, + }) + + // Create tables in order: alpha, beta, gamma with specific column orders + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + CREATE TABLE test_table_alpha ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + status test_enum_alpha DEFAULT 'active', + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE test_table_beta ( + id SERIAL PRIMARY KEY, + priority test_enum_beta DEFAULT 'medium', + description TEXT, + alpha_id INTEGER REFERENCES test_table_alpha(id) + ); + + CREATE TABLE test_table_gamma ( + id SERIAL PRIMARY KEY, + beta_id INTEGER REFERENCES test_table_beta(id), + value NUMERIC(10,2), + is_active BOOLEAN DEFAULT true + ); + `, + }, + }) + + // Create functions in order: alpha, beta, gamma with specific parameter orders + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + CREATE FUNCTION test_func_alpha(param_a integer, param_b text, param_c boolean) + RETURNS integer AS 'SELECT param_a + 1' LANGUAGE sql IMMUTABLE; + + CREATE FUNCTION test_func_beta(param_a integer, param_b text, param_c boolean) + RETURNS text AS 'SELECT param_b || ''_processed''' LANGUAGE sql IMMUTABLE; + + CREATE FUNCTION test_func_gamma(param_a integer, param_b text, param_c boolean) + RETURNS boolean AS 'SELECT NOT param_c' LANGUAGE sql IMMUTABLE; + `, + }, + }) + + // Create views in order: alpha, beta, gamma + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + CREATE VIEW test_view_alpha AS + SELECT id, name, status, created_at FROM test_table_alpha; + + CREATE VIEW test_view_beta AS + SELECT id, priority, description, alpha_id FROM test_table_beta; + + CREATE VIEW test_view_gamma AS + SELECT id, beta_id, value, is_active FROM test_table_gamma; + `, + }, + }) + + // Create materialized views in order: alpha, beta, gamma + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + CREATE MATERIALIZED VIEW test_matview_alpha AS + SELECT id, name, status FROM test_table_alpha; + + CREATE MATERIALIZED VIEW test_matview_beta AS + SELECT id, priority, description FROM test_table_beta; + + CREATE MATERIALIZED VIEW test_matview_gamma AS + SELECT id, value, is_active FROM test_table_gamma; + `, + }, + }) + + // Generate types for first configuration + const { body: firstCall } = await app.inject({ + method: 'GET', + path: '/generators/typescript', + query: { detect_one_to_one_relationships: 'true', postgrest_version: '13' }, + }) + + // === SECOND ROUND: Drop and recreate in reverse order G->B->A with different property orders === + + // Clean up all test entities + await cleanupTestEntities() + + // Create custom types in reverse order but keep the enum internal ordering (typegen is rightfully dependent on the enum order) + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + CREATE TYPE test_enum_beta AS ENUM ('high', 'medium', 'low'); + CREATE TYPE test_enum_alpha AS ENUM ('active', 'inactive', 'pending'); + `, + }, + }) + + // Create tables in reverse order: gamma, beta, alpha with different column orders + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + CREATE TABLE test_table_alpha ( + created_at TIMESTAMP DEFAULT NOW(), + status test_enum_alpha DEFAULT 'active', + name TEXT NOT NULL, + id SERIAL PRIMARY KEY + ); + + CREATE TABLE test_table_beta ( + alpha_id INTEGER REFERENCES test_table_alpha(id), + description TEXT, + priority test_enum_beta DEFAULT 'medium', + id SERIAL PRIMARY KEY + ); + + CREATE TABLE test_table_gamma ( + is_active BOOLEAN DEFAULT true, + value NUMERIC(10,2), + beta_id INTEGER REFERENCES test_table_beta(id), + id SERIAL PRIMARY KEY + ); + `, + }, + }) + + // Create functions in reverse order: gamma, beta, alpha with different parameter orders + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + CREATE FUNCTION test_func_gamma(param_c boolean, param_a integer, param_b text) + RETURNS boolean AS 'SELECT NOT param_c' LANGUAGE sql IMMUTABLE; + + CREATE FUNCTION test_func_beta(param_b text, param_c boolean, param_a integer) + RETURNS text AS 'SELECT param_b || ''_processed''' LANGUAGE sql IMMUTABLE; + + CREATE FUNCTION test_func_alpha(param_c boolean, param_b text, param_a integer) + RETURNS integer AS 'SELECT param_a + 1' LANGUAGE sql IMMUTABLE; + `, + }, + }) + + // Create views in reverse order: gamma, beta, alpha + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + CREATE VIEW test_view_gamma AS + SELECT is_active, value, beta_id, id FROM test_table_gamma; + + CREATE VIEW test_view_beta AS + SELECT alpha_id, description, priority, id FROM test_table_beta; + + CREATE VIEW test_view_alpha AS + SELECT created_at, status, name, id FROM test_table_alpha; + `, + }, + }) + + // Create materialized views in reverse order: gamma, beta, alpha + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + CREATE MATERIALIZED VIEW test_matview_gamma AS + SELECT is_active, value, id FROM test_table_gamma; + + CREATE MATERIALIZED VIEW test_matview_beta AS + SELECT description, priority, id FROM test_table_beta; + + CREATE MATERIALIZED VIEW test_matview_alpha AS + SELECT status, name, id FROM test_table_alpha; + `, + }, + }) + + // Generate types for second configuration + const { body: secondCall } = await app.inject({ + method: 'GET', + path: '/generators/typescript', + query: { detect_one_to_one_relationships: 'true', postgrest_version: '13' }, + }) + + // Clean up test entities + await cleanupTestEntities() + + // The generated types should be identical regardless of: + // 1. Entity creation order (alpha->beta->gamma vs gamma->beta->alpha) + // 2. Property declaration order (columns, function parameters) + // 3. Enum value order + expect(firstCall).toEqual(secondCall) +}) + test('typegen: go', async () => { const { body } = await app.inject({ method: 'GET', path: '/generators/go' }) expect(body).toMatchInlineSnapshot(` "package database -type PublicUsersSelect struct { - Id int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicUsersInsert struct { - Id *int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicUsersUpdate struct { - Id *int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicTodosSelect struct { - Details *string \`json:"details"\` - Id int64 \`json:"id"\` - UserId int64 \`json:"user-id"\` -} - -type PublicTodosInsert struct { - Details *string \`json:"details"\` - Id *int64 \`json:"id"\` - UserId int64 \`json:"user-id"\` -} - -type PublicTodosUpdate struct { - Details *string \`json:"details"\` - Id *int64 \`json:"id"\` - UserId *int64 \`json:"user-id"\` -} - -type PublicUsersAuditSelect struct { - CreatedAt *string \`json:"created_at"\` - Id int64 \`json:"id"\` - PreviousValue interface{} \`json:"previous_value"\` - UserId *int64 \`json:"user_id"\` -} - -type PublicUsersAuditInsert struct { - CreatedAt *string \`json:"created_at"\` - Id *int64 \`json:"id"\` - PreviousValue interface{} \`json:"previous_value"\` - UserId *int64 \`json:"user_id"\` -} - -type PublicUsersAuditUpdate struct { - CreatedAt *string \`json:"created_at"\` - Id *int64 \`json:"id"\` - PreviousValue interface{} \`json:"previous_value"\` - UserId *int64 \`json:"user_id"\` -} - -type PublicUserDetailsSelect struct { - Details *string \`json:"details"\` - UserId int64 \`json:"user_id"\` -} - -type PublicUserDetailsInsert struct { - Details *string \`json:"details"\` - UserId int64 \`json:"user_id"\` -} - -type PublicUserDetailsUpdate struct { - Details *string \`json:"details"\` - UserId *int64 \`json:"user_id"\` -} - -type PublicEmptySelect struct { - -} - -type PublicEmptyInsert struct { - -} - -type PublicEmptyUpdate struct { - -} - -type PublicTableWithOtherTablesRowTypeSelect struct { - Col1 interface{} \`json:"col1"\` - Col2 interface{} \`json:"col2"\` -} - -type PublicTableWithOtherTablesRowTypeInsert struct { - Col1 interface{} \`json:"col1"\` - Col2 interface{} \`json:"col2"\` -} - -type PublicTableWithOtherTablesRowTypeUpdate struct { - Col1 interface{} \`json:"col1"\` - Col2 interface{} \`json:"col2"\` -} - -type PublicTableWithPrimaryKeyOtherThanIdSelect struct { - Name *string \`json:"name"\` - OtherId int64 \`json:"other_id"\` -} - -type PublicTableWithPrimaryKeyOtherThanIdInsert struct { - Name *string \`json:"name"\` - OtherId *int64 \`json:"other_id"\` -} - -type PublicTableWithPrimaryKeyOtherThanIdUpdate struct { - Name *string \`json:"name"\` - OtherId *int64 \`json:"other_id"\` -} - -type PublicCategorySelect struct { - Id int32 \`json:"id"\` - Name string \`json:"name"\` -} - -type PublicCategoryInsert struct { - Id *int32 \`json:"id"\` - Name string \`json:"name"\` -} - -type PublicCategoryUpdate struct { - Id *int32 \`json:"id"\` - Name *string \`json:"name"\` -} - -type PublicMemesSelect struct { - Category *int32 \`json:"category"\` - CreatedAt string \`json:"created_at"\` - Id int32 \`json:"id"\` - Metadata interface{} \`json:"metadata"\` - Name string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicMemesInsert struct { - Category *int32 \`json:"category"\` - CreatedAt string \`json:"created_at"\` - Id *int32 \`json:"id"\` - Metadata interface{} \`json:"metadata"\` - Name string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicMemesUpdate struct { - Category *int32 \`json:"category"\` - CreatedAt *string \`json:"created_at"\` - Id *int32 \`json:"id"\` - Metadata interface{} \`json:"metadata"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicTodosViewSelect struct { - Details *string \`json:"details"\` - Id *int64 \`json:"id"\` - UserId *int64 \`json:"user-id"\` -} - -type PublicUsersViewSelect struct { - Id *int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` -} - -type PublicAViewSelect struct { - Id *int64 \`json:"id"\` -} - -type PublicUsersViewWithMultipleRefsToUsersSelect struct { - InitialId *int64 \`json:"initial_id"\` - InitialName *string \`json:"initial_name"\` - SecondId *int64 \`json:"second_id"\` - SecondName *string \`json:"second_name"\` -} - -type PublicTodosMatviewSelect struct { - Details *string \`json:"details"\` - Id *int64 \`json:"id"\` - UserId *int64 \`json:"user-id"\` -} - -type PublicCompositeTypeWithArrayAttribute struct { - MyTextArray interface{} \`json:"my_text_array"\` -} - -type PublicCompositeTypeWithRecordAttribute struct { - Todo interface{} \`json:"todo"\` -}" + type PublicUsersSelect struct { + Decimal *float64 \`json:"decimal"\` + Id int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicUsersInsert struct { + Decimal *float64 \`json:"decimal"\` + Id *int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicUsersUpdate struct { + Decimal *float64 \`json:"decimal"\` + Id *int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicTodosSelect struct { + Details *string \`json:"details"\` + Id int64 \`json:"id"\` + UserId int64 \`json:"user-id"\` + } + + type PublicTodosInsert struct { + Details *string \`json:"details"\` + Id *int64 \`json:"id"\` + UserId int64 \`json:"user-id"\` + } + + type PublicTodosUpdate struct { + Details *string \`json:"details"\` + Id *int64 \`json:"id"\` + UserId *int64 \`json:"user-id"\` + } + + type PublicUsersAuditSelect struct { + CreatedAt *string \`json:"created_at"\` + Id int64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId *int64 \`json:"user_id"\` + } + + type PublicUsersAuditInsert struct { + CreatedAt *string \`json:"created_at"\` + Id *int64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId *int64 \`json:"user_id"\` + } + + type PublicUsersAuditUpdate struct { + CreatedAt *string \`json:"created_at"\` + Id *int64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId *int64 \`json:"user_id"\` + } + + type PublicUserDetailsSelect struct { + Details *string \`json:"details"\` + UserId int64 \`json:"user_id"\` + } + + type PublicUserDetailsInsert struct { + Details *string \`json:"details"\` + UserId int64 \`json:"user_id"\` + } + + type PublicUserDetailsUpdate struct { + Details *string \`json:"details"\` + UserId *int64 \`json:"user_id"\` + } + + type PublicEmptySelect struct { + + } + + type PublicEmptyInsert struct { + + } + + type PublicEmptyUpdate struct { + + } + + type PublicTableWithOtherTablesRowTypeSelect struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` + } + + type PublicTableWithOtherTablesRowTypeInsert struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` + } + + type PublicTableWithOtherTablesRowTypeUpdate struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` + } + + type PublicTableWithPrimaryKeyOtherThanIdSelect struct { + Name *string \`json:"name"\` + OtherId int64 \`json:"other_id"\` + } + + type PublicTableWithPrimaryKeyOtherThanIdInsert struct { + Name *string \`json:"name"\` + OtherId *int64 \`json:"other_id"\` + } + + type PublicTableWithPrimaryKeyOtherThanIdUpdate struct { + Name *string \`json:"name"\` + OtherId *int64 \`json:"other_id"\` + } + + type PublicCategorySelect struct { + Id int32 \`json:"id"\` + Name string \`json:"name"\` + } + + type PublicCategoryInsert struct { + Id *int32 \`json:"id"\` + Name string \`json:"name"\` + } + + type PublicCategoryUpdate struct { + Id *int32 \`json:"id"\` + Name *string \`json:"name"\` + } + + type PublicMemesSelect struct { + Category *int32 \`json:"category"\` + CreatedAt string \`json:"created_at"\` + Id int32 \`json:"id"\` + Metadata interface{} \`json:"metadata"\` + Name string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicMemesInsert struct { + Category *int32 \`json:"category"\` + CreatedAt string \`json:"created_at"\` + Id *int32 \`json:"id"\` + Metadata interface{} \`json:"metadata"\` + Name string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicMemesUpdate struct { + Category *int32 \`json:"category"\` + CreatedAt *string \`json:"created_at"\` + Id *int32 \`json:"id"\` + Metadata interface{} \`json:"metadata"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicAViewSelect struct { + Id *int64 \`json:"id"\` + } + + type PublicTodosViewSelect struct { + Details *string \`json:"details"\` + Id *int64 \`json:"id"\` + UserId *int64 \`json:"user-id"\` + } + + type PublicUsersViewSelect struct { + Decimal *float64 \`json:"decimal"\` + Id *int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + } + + type PublicUserTodosSummaryViewSelect struct { + TodoCount *int64 \`json:"todo_count"\` + TodoDetails []*string \`json:"todo_details"\` + UserId *int64 \`json:"user_id"\` + UserName *string \`json:"user_name"\` + UserStatus *string \`json:"user_status"\` + } + + type PublicUsersViewWithMultipleRefsToUsersSelect struct { + InitialId *int64 \`json:"initial_id"\` + InitialName *string \`json:"initial_name"\` + SecondId *int64 \`json:"second_id"\` + SecondName *string \`json:"second_name"\` + } + + type PublicTodosMatviewSelect struct { + Details *string \`json:"details"\` + Id *int64 \`json:"id"\` + UserId *int64 \`json:"user-id"\` + } + + type PublicCompositeTypeWithArrayAttribute struct { + MyTextArray interface{} \`json:"my_text_array"\` + } + + type PublicCompositeTypeWithRecordAttribute struct { + Todo interface{} \`json:"todo"\` + }" `) }) @@ -2991,30 +4994,36 @@ test('typegen: swift', async () => { } } internal struct UsersSelect: Codable, Hashable, Sendable, Identifiable { + internal let decimal: Decimal? internal let id: Int64 internal let name: String? internal let status: UserStatus? internal enum CodingKeys: String, CodingKey { + case decimal = "decimal" case id = "id" case name = "name" case status = "status" } } internal struct UsersInsert: Codable, Hashable, Sendable, Identifiable { + internal let decimal: Decimal? internal let id: Int64? internal let name: String? internal let status: UserStatus? internal enum CodingKeys: String, CodingKey { + case decimal = "decimal" case id = "id" case name = "name" case status = "status" } } internal struct UsersUpdate: Codable, Hashable, Sendable, Identifiable { + internal let decimal: Decimal? internal let id: Int64? internal let name: String? internal let status: UserStatus? internal enum CodingKeys: String, CodingKey { + case decimal = "decimal" case id = "id" case name = "name" case status = "status" @@ -3082,11 +5091,27 @@ test('typegen: swift', async () => { case userId = "user-id" } } + internal struct UserTodosSummaryViewSelect: Codable, Hashable, Sendable { + internal let todoCount: Int64? + internal let todoDetails: [String]? + internal let userId: Int64? + internal let userName: String? + internal let userStatus: UserStatus? + internal enum CodingKeys: String, CodingKey { + case todoCount = "todo_count" + case todoDetails = "todo_details" + case userId = "user_id" + case userName = "user_name" + case userStatus = "user_status" + } + } internal struct UsersViewSelect: Codable, Hashable, Sendable { + internal let decimal: Decimal? internal let id: Int64? internal let name: String? internal let status: UserStatus? internal enum CodingKeys: String, CodingKey { + case decimal = "decimal" case id = "id" case name = "name" case status = "status" @@ -3354,30 +5379,36 @@ test('typegen: swift w/ public access control', async () => { } } public struct UsersSelect: Codable, Hashable, Sendable, Identifiable { + public let decimal: Decimal? public let id: Int64 public let name: String? public let status: UserStatus? public enum CodingKeys: String, CodingKey { + case decimal = "decimal" case id = "id" case name = "name" case status = "status" } } public struct UsersInsert: Codable, Hashable, Sendable, Identifiable { + public let decimal: Decimal? public let id: Int64? public let name: String? public let status: UserStatus? public enum CodingKeys: String, CodingKey { + case decimal = "decimal" case id = "id" case name = "name" case status = "status" } } public struct UsersUpdate: Codable, Hashable, Sendable, Identifiable { + public let decimal: Decimal? public let id: Int64? public let name: String? public let status: UserStatus? public enum CodingKeys: String, CodingKey { + case decimal = "decimal" case id = "id" case name = "name" case status = "status" @@ -3445,11 +5476,27 @@ test('typegen: swift w/ public access control', async () => { case userId = "user-id" } } + public struct UserTodosSummaryViewSelect: Codable, Hashable, Sendable { + public let todoCount: Int64? + public let todoDetails: [String]? + public let userId: Int64? + public let userName: String? + public let userStatus: UserStatus? + public enum CodingKeys: String, CodingKey { + case todoCount = "todo_count" + case todoDetails = "todo_details" + case userId = "user_id" + case userName = "user_name" + case userStatus = "user_status" + } + } public struct UsersViewSelect: Codable, Hashable, Sendable { + public let decimal: Decimal? public let id: Int64? public let name: String? public let status: UserStatus? public enum CodingKeys: String, CodingKey { + case decimal = "decimal" case id = "id" case name = "name" case status = "status" From 3bb1b45f57cf532037e5f93479fdecc70d6dadf2 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 6 Aug 2025 16:06:11 +0200 Subject: [PATCH 02/26] chore: update snapshots --- test/server/typegen.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/server/typegen.ts b/test/server/typegen.ts index bdcddb39..03f93b45 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -770,7 +770,6 @@ test('typegen: typescript', async () => { postgres_fdw_handler: { Args: never; Returns: unknown } postgrest_resolvable_with_override_function: | { Args: { a: string }; Returns: number } - | { Args: { b: number }; Returns: string } | { Args: { user_id: number } Returns: { @@ -814,6 +813,7 @@ test('typegen: typescript', async () => { isSetofReturn: true } } + | { Args: { b: number }; Returns: string } | { Args: never; Returns: undefined } postgrest_unresolvable_function: | { @@ -1847,7 +1847,6 @@ test('typegen w/ one-to-one relationships', async () => { postgres_fdw_handler: { Args: never; Returns: unknown } postgrest_resolvable_with_override_function: | { Args: { a: string }; Returns: number } - | { Args: { b: number }; Returns: string } | { Args: { user_id: number } Returns: { @@ -1891,6 +1890,7 @@ test('typegen w/ one-to-one relationships', async () => { isSetofReturn: true } } + | { Args: { b: number }; Returns: string } | { Args: never; Returns: undefined } postgrest_unresolvable_function: | { @@ -2924,7 +2924,6 @@ test('typegen: typescript w/ one-to-one relationships', async () => { postgres_fdw_handler: { Args: never; Returns: unknown } postgrest_resolvable_with_override_function: | { Args: { a: string }; Returns: number } - | { Args: { b: number }; Returns: string } | { Args: { user_id: number } Returns: { @@ -2968,6 +2967,7 @@ test('typegen: typescript w/ one-to-one relationships', async () => { isSetofReturn: true } } + | { Args: { b: number }; Returns: string } | { Args: never; Returns: undefined } postgrest_unresolvable_function: | { @@ -4006,7 +4006,6 @@ test('typegen: typescript w/ postgrestVersion', async () => { postgres_fdw_handler: { Args: never; Returns: unknown } postgrest_resolvable_with_override_function: | { Args: { a: string }; Returns: number } - | { Args: { b: number }; Returns: string } | { Args: { user_id: number } Returns: { @@ -4050,6 +4049,7 @@ test('typegen: typescript w/ postgrestVersion', async () => { isSetofReturn: true } } + | { Args: { b: number }; Returns: string } | { Args: never; Returns: undefined } postgrest_unresolvable_function: | { From 6b8c75320f136e78230df83f5f5ee1a63bed53fe Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 6 Aug 2025 18:20:25 +0200 Subject: [PATCH 03/26] chore: unify sort and dedup loops --- src/server/templates/typescript.ts | 568 +++++++++++++++-------------- 1 file changed, 288 insertions(+), 280 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index cd837bf2..e9254fa6 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -30,13 +30,110 @@ export const apply = async ({ detectOneToOneRelationships: boolean postgrestVersion?: string }): Promise => { + schemas.sort((a, b) => a.name.localeCompare(b.name)) + const columnsByTableId = Object.fromEntries( [...tables, ...foreignTables, ...views, ...materializedViews].map((t) => [t.id, []]) ) - columns - .filter((c) => c.table_id in columnsByTableId) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - .forEach((c) => columnsByTableId[c.table_id].push(c)) + for (const column of columns) { + if (column.table_id in columnsByTableId) { + columnsByTableId[column.table_id].push(column) + } + } + for (const tableId in columnsByTableId) { + columnsByTableId[tableId].sort((a, b) => a.name.localeCompare(b.name)) + } + + const introspectionBySchema = Object.fromEntries<{ + tables: Pick[] + views: PostgresView[] + functions: { fn: PostgresFunction; inArgs: PostgresFunction['args'] }[] + enums: PostgresType[] + compositeTypes: PostgresType[] + }>( + schemas.map((s) => [ + s.name, + { tables: [], views: [], functions: [], enums: [], compositeTypes: [] }, + ]) + ) + for (const table of tables) { + if (table.schema in introspectionBySchema) { + introspectionBySchema[table.schema].tables.push(table) + } + } + for (const table of foreignTables) { + if (table.schema in introspectionBySchema) { + introspectionBySchema[table.schema].tables.push(table) + } + } + for (const view of views) { + if (view.schema in introspectionBySchema) { + introspectionBySchema[view.schema].views.push(view) + } + } + for (const materializedView of materializedViews) { + if (materializedView.schema in introspectionBySchema) { + introspectionBySchema[materializedView.schema].views.push({ + ...materializedView, + is_updatable: false, + }) + } + } + for (const func of functions) { + if (func.schema in introspectionBySchema) { + func.args.sort((a, b) => a.name.localeCompare(b.name)) + // Get all input args (in, inout, variadic modes) + const inArgs = func.args.filter(({ mode }) => VALID_FUNCTION_ARGS_MODE.has(mode)) + + if ( + // Case 1: Function has no parameters + inArgs.length === 0 || + // Case 2: All input args are named + !inArgs.some(({ name }) => name === '') || + // Case 3: All unnamed args have default values AND are valid types + inArgs.every((arg) => { + if (arg.name === '') { + return arg.has_default && VALID_UNNAMED_FUNCTION_ARG_TYPES.has(arg.type_id) + } + return true + }) || + // Case 4: Single unnamed parameter of valid type (json, jsonb, text) + // Exclude all functions definitions that have only one single argument unnamed argument that isn't + // a json/jsonb/text as it won't be considered by PostgREST + (inArgs.length === 1 && + inArgs[0].name === '' && + (VALID_UNNAMED_FUNCTION_ARG_TYPES.has(inArgs[0].type_id) || + // OR if the function have a single unnamed args which is another table (embeded function) + (inArgs[0].table_name && func.return_table_name) || + // OR if the function takes a table row but doesn't qualify as embedded (for error reporting) + (inArgs[0].table_name && !func.return_table_name))) + ) { + func.args.sort((a, b) => a.name.localeCompare(b.name)) + introspectionBySchema[func.schema].functions.push({ fn: func, inArgs }) + } + + // NOTE: Functions with named table arguments are generally excluded + // as they're not supported by PostgREST in the expected way + } + } + for (const type of types) { + if (type.schema in introspectionBySchema) { + if (type.enums.length > 0) { + introspectionBySchema[type.schema].enums.push(type) + } + if (type.attributes.length > 0) { + introspectionBySchema[type.schema].compositeTypes.push(type) + } + } + } + for (const schema in introspectionBySchema) { + introspectionBySchema[schema].tables.sort((a, b) => a.name.localeCompare(b.name)) + introspectionBySchema[schema].views.sort((a, b) => a.name.localeCompare(b.name)) + introspectionBySchema[schema].functions.sort((a, b) => a.fn.name.localeCompare(b.fn.name)) + introspectionBySchema[schema].enums.sort((a, b) => a.name.localeCompare(b.name)) + introspectionBySchema[schema].compositeTypes.sort((a, b) => a.name.localeCompare(b.name)) + } + // group types by id for quicker lookup const typesById = types.reduce( (acc, type) => { @@ -51,7 +148,7 @@ export const apply = async ({ let setofOptionsInfo = '' // Only add SetofOptions for functions with table arguments (embedded functions) - // or specific functions that need RETURNS table-name introspection fixes + // or specific functions that RETURNS table-name if (fn.args.length === 1 && fn.args[0].table_name) { // Case 1: Standard embedded function with proper setof detection if (fn.returns_set_of_table && fn.return_table_name) { @@ -74,7 +171,8 @@ export const apply = async ({ }` } } - // Case 3: Special case for functions without table arguments but specific names + // Case 3: Special case for functions without table arguments still returning a table + // Those can be used in rpc to select sub fields of a table else if (fn.return_table_name) { setofOptionsInfo = `SetofOptions: { from: "*" @@ -92,16 +190,14 @@ export const apply = async ({ // Case 1: `returns table`. const tableArgs = fn.args.filter(({ mode }) => mode === 'table') if (tableArgs.length > 0) { - const argsNameAndType = tableArgs - .map(({ name, type_id }) => { - const type = typesById[type_id] - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { types, schemas, tables, views }) - } - return { name, type: tsType } - }) - .toSorted((a, b) => a.name.localeCompare(b.name)) + const argsNameAndType = tableArgs.map(({ name, type_id }) => { + const type = typesById[type_id] + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(schema, type.name, { types, schemas, tables, views }) + } + return { name, type: tsType } + }) return `{ ${argsNameAndType.map(({ name, type }) => `${JSON.stringify(name)}: ${type}`)} @@ -109,11 +205,14 @@ export const apply = async ({ } // Case 2: returns a relation's row type. - const relation = [...tables, ...views].find(({ id }) => id === fn.return_type_relation_id) + const relation = + introspectionBySchema[schema.name]?.tables.find( + ({ id }) => id === fn.return_type_relation_id + ) || + introspectionBySchema[schema.name]?.views.find(({ id }) => id === fn.return_type_relation_id) if (relation) { return `{ ${columnsByTableId[relation.id] - .toSorted((a, b) => a.name.localeCompare(b.name)) .map( (column) => `${JSON.stringify(column.name)}: ${pgTypeToTsType(schema, column.format, { @@ -123,7 +222,6 @@ export const apply = async ({ views, })} ${column.is_nullable ? '| null' : ''}` ) - .sort() .join(',\n')} }` } @@ -136,6 +234,143 @@ export const apply = async ({ return 'unknown' } + // Special error case for functions that take table row but don't qualify as embedded functions + const hasTableRowError = (fn: PostgresFunction, inArgs: PostgresFunction['args']) => { + if ( + inArgs.length === 1 && + inArgs[0].name === '' && + inArgs[0].table_name && + !fn.return_table_name + ) { + return true + } + return false + } + + // Check for generic conflict cases that need error reporting + const getConflictError = ( + schema: PostgresSchema, + fns: Array<{ fn: PostgresFunction; inArgs: PostgresFunction['args'] }>, + fn: PostgresFunction, + inArgs: PostgresFunction['args'] + ) => { + // If there is a single function definition, there is no conflict + if (fns.length <= 1) return null + + // Generic conflict detection patterns + // Pattern 1: No-args vs default-args conflicts + if (inArgs.length === 0) { + const conflictingFns = fns.filter(({ fn: otherFn, inArgs: otherInArgs }) => { + if (otherFn === fn) return false + return otherInArgs.length === 1 && otherInArgs[0].name === '' && otherInArgs[0].has_default + }) + + if (conflictingFns.length > 0) { + const conflictingFn = conflictingFns[0] + const returnTypeName = typesById[conflictingFn.fn.return_type_id]?.name || 'unknown' + 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` + } + } + + // Pattern 2: Same parameter name but different types (unresolvable overloads) + if (inArgs.length === 1 && inArgs[0].name !== '') { + const conflictingFns = fns.filter(({ fn: otherFn, inArgs: otherInArgs }) => { + if (otherFn === fn) return false + return ( + otherInArgs.length === 1 && + otherInArgs[0].name === inArgs[0].name && + otherInArgs[0].type_id !== inArgs[0].type_id + ) + }) + + if (conflictingFns.length > 0) { + const allConflictingFunctions = [{ fn, inArgs }, ...conflictingFns] + const conflictList = allConflictingFunctions + .sort((a, b) => { + const aArgs = a.inArgs + const bArgs = b.inArgs + return (aArgs[0]?.type_id || 0) - (bArgs[0]?.type_id || 0) + }) + .map((f) => { + const args = f.inArgs + return `${schema.name}.${fn.name}(${args.map((a) => `${a.name || ''} => ${typesById[a.type_id]?.name || 'unknown'}`).join(', ')})` + }) + .join(', ') + + 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` + } + } + + return null + } + + const getFunctionSignatures = ( + schema: PostgresSchema, + fns: Array<{ fn: PostgresFunction; inArgs: PostgresFunction['args'] }> + ) => { + return fns.map(({ fn, inArgs }) => { + let argsType = 'never' + let returnType = getFunctionReturnType(schema, fn) + + // Check for specific error cases + const conflictError = getConflictError(schema, fns, fn, inArgs) + if (conflictError) { + if (inArgs.length > 0) { + const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { + const type = typesById[type_id] + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(schema, type.name, { + types, + schemas, + tables, + views, + }) + } + return { name, type: tsType, has_default } + }) + argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` + } + returnType = `{ error: true } & ${JSON.stringify(conflictError)}` + } else if (hasTableRowError(fn, inArgs)) { + // Special case for computed fields returning scalars functions + if (inArgs.length > 0) { + const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { + const type = typesById[type_id] + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(schema, type.name, { + types, + schemas, + tables, + views, + }) + } + return { name, type: tsType, has_default } + }) + argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` + } + 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`)}` + } else if (inArgs.length > 0) { + const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { + const type = typesById[type_id] + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(schema, type.name, { + types, + schemas, + tables, + views, + }) + } + return { name, type: tsType, has_default } + }) + argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` + } + + return `{ Args: ${argsType}; Returns: ${getFunctionTsReturnType(fn, returnType)} }` + }) + } const internal_supabase_schema = postgrestVersion ? `// Allows to automatically instantiate createClient with right options @@ -150,44 +385,15 @@ export type Json = string | number | boolean | null | { [key: string]: Json | un export type Database = { ${internal_supabase_schema} - ${schemas - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - .map((schema) => { - const schemaTables = [...tables, ...foreignTables] - .filter((table) => table.schema === schema.name) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaViews = [...views, ...materializedViews] - .filter((view) => view.schema === schema.name) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaFunctions = functions - .filter((func) => { - if (func.schema !== schema.name) { - return false - } - - // Either: - // 1. All input args are be named, or - // 2. There is only one input arg which is unnamed - const inArgs = func.args.filter(({ mode }) => ['in', 'inout', 'variadic'].includes(mode)) - - if (!inArgs.some(({ name }) => name === '')) { - return true - } - - if (inArgs.length === 1) { - return true - } - - return false - }) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaEnums = types - .filter((type) => type.schema === schema.name && type.enums.length > 0) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaCompositeTypes = types - .filter((type) => type.schema === schema.name && type.attributes.length > 0) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - return `${JSON.stringify(schema.name)}: { + ${schemas.map((schema) => { + const { + tables: schemaTables, + views: schemaViews, + functions: schemaFunctions, + enums: schemaEnums, + compositeTypes: schemaCompositeTypes, + } = introspectionBySchema[schema.name] + return `${JSON.stringify(schema.name)}: { Tables: { ${ schemaTables.length === 0 @@ -206,9 +412,9 @@ export type Database = { })} ${column.is_nullable ? '| null' : ''}` ), ...schemaFunctions - .filter((fn) => fn.argument_types === table.name) + .filter(({ fn }) => fn.argument_types === table.name) .map( - (fn) => + ({ fn }) => `${JSON.stringify(fn.name)}: ${getFunctionReturnType(schema, fn)} | null` ), ]} @@ -318,15 +524,15 @@ export type Database = { })} ${column.is_nullable ? '| null' : ''}` ), ...schemaFunctions - .filter((fn) => fn.argument_types === view.name) + .filter(({ fn }) => fn.argument_types === view.name) .map( - (fn) => + ({ fn }) => `${JSON.stringify(fn.name)}: ${getFunctionReturnType(schema, fn)} | null` ), ]} } ${ - 'is_updatable' in view && view.is_updatable + view.is_updatable ? `Insert: { ${columnsByTableId[view.id].map((column) => { let output = JSON.stringify(column.name) @@ -403,216 +609,22 @@ export type Database = { if (schemaFunctions.length === 0) { return '[_ in never]: never' } - const schemaFunctionsGroupedByName = schemaFunctions - .filter((func) => { - // Get all input args (in, inout, variadic modes) - const inArgs = func.args - .toSorted((a, b) => a.name.localeCompare(b.name)) - .filter(({ mode }) => VALID_FUNCTION_ARGS_MODE.has(mode)) - // Case 1: Function has no parameters - if (inArgs.length === 0) { - return true - } - - // Case 2: All input args are named - if (!inArgs.some(({ name }) => name === '')) { - return true - } - - // Case 3: All unnamed args have default values AND are valid types - if ( - inArgs.every((arg) => { - if (arg.name === '') { - return arg.has_default && VALID_UNNAMED_FUNCTION_ARG_TYPES.has(arg.type_id) - } - return true - }) - ) { - return true - } - - // Case 4: Single unnamed parameter of valid type (json, jsonb, text) - // Exclude all functions definitions that have only one single argument unnamed argument that isn't - // a json/jsonb/text as it won't be considered by PostgREST - if ( - inArgs.length === 1 && - inArgs[0].name === '' && - (VALID_UNNAMED_FUNCTION_ARG_TYPES.has(inArgs[0].type_id) || - // OR if the function have a single unnamed args which is another table (embeded function) - (inArgs[0].table_name && func.return_table_name) || - // OR if the function takes a table row but doesn't qualify as embedded (for error reporting) - (inArgs[0].table_name && !func.return_table_name)) - ) { - return true - } - - // NOTE: Functions with named table arguments are generally excluded - // as they're not supported by PostgREST in the expected way - - return false - }) - .reduce( - (acc, curr) => { - acc[curr.name] ??= [] - acc[curr.name].push(curr) - return acc - }, - {} as Record + const schemaFunctionsGroupedByName = schemaFunctions.reduce( + (acc, curr) => { + acc[curr.fn.name] ??= [] + acc[curr.fn.name].push(curr) + return acc + }, + {} as Record + ) + for (const fnName in schemaFunctionsGroupedByName) { + schemaFunctionsGroupedByName[fnName].sort((a, b) => + b.fn.definition.localeCompare(a.fn.definition) ) + } - return Object.entries(schemaFunctionsGroupedByName).map(([fnName, _fns]) => { - // Check for function overload conflicts - const fns = _fns.toSorted((a, b) => b.definition.localeCompare(a.definition)) - - const functionSignatures = fns.map((fn) => { - const inArgs = fn.args.filter(({ mode }) => VALID_FUNCTION_ARGS_MODE.has(mode)) - - // Special error case for functions that take table row but don't qualify as embedded functions - const hasTableRowError = (fn: PostgresFunction) => { - if ( - inArgs.length === 1 && - inArgs[0].name === '' && - inArgs[0].table_name && - !fn.return_table_name - ) { - return true - } - return false - } - - // Check for generic conflict cases that need error reporting - const getConflictError = (fn: PostgresFunction) => { - const sameFunctions = fns.filter((f) => f.name === fn.name) - if (sameFunctions.length <= 1) return null - - // Generic conflict detection patterns - - // Pattern 1: No-args vs default-args conflicts - if (inArgs.length === 0) { - const conflictingFns = sameFunctions.filter((otherFn) => { - if (otherFn === fn) return false - const otherInArgs = otherFn.args.filter(({ mode }) => - VALID_FUNCTION_ARGS_MODE.has(mode) - ) - return ( - otherInArgs.length === 1 && - otherInArgs[0].name === '' && - otherInArgs[0].has_default - ) - }) - - if (conflictingFns.length > 0) { - const conflictingFn = conflictingFns[0] - const returnTypeName = - types.find((t) => t.id === conflictingFn.return_type_id)?.name || - 'unknown' - 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` - } - } - - // Pattern 2: Same parameter name but different types (unresolvable overloads) - if (inArgs.length === 1 && inArgs[0].name !== '') { - const conflictingFns = sameFunctions.filter((otherFn) => { - if (otherFn === fn) return false - const otherInArgs = otherFn.args.filter(({ mode }) => - VALID_FUNCTION_ARGS_MODE.has(mode) - ) - return ( - otherInArgs.length === 1 && - otherInArgs[0].name === inArgs[0].name && - otherInArgs[0].type_id !== inArgs[0].type_id - ) - }) - - if (conflictingFns.length > 0) { - const allConflictingFunctions = [fn, ...conflictingFns] - const conflictList = allConflictingFunctions - .sort((a, b) => { - const aArgs = a.args.filter(({ mode }) => - VALID_FUNCTION_ARGS_MODE.has(mode) - ) - const bArgs = b.args.filter(({ mode }) => - VALID_FUNCTION_ARGS_MODE.has(mode) - ) - return (aArgs[0]?.type_id || 0) - (bArgs[0]?.type_id || 0) - }) - .map((f) => { - const args = f.args.filter(({ mode }) => - VALID_FUNCTION_ARGS_MODE.has(mode) - ) - return `${schema.name}.${fn.name}(${args.map((a) => `${a.name || ''} => ${types.find((t) => t.id === a.type_id)?.name || 'unknown'}`).join(', ')})` - }) - .join(', ') - - 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` - } - } - - return null - } - - let argsType = 'never' - let returnType = getFunctionReturnType(schema, fn) - - // Check for specific error cases - const conflictError = getConflictError(fn) - if (conflictError) { - if (inArgs.length > 0) { - const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = types.find(({ id }) => id === type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return { name, type: tsType, has_default } - }) - argsType = `{ ${argsNameAndType.toSorted((a, b) => a.name.localeCompare(b.name)).map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` - } - returnType = `{ error: true } & ${JSON.stringify(conflictError)}` - } else if (hasTableRowError(fn)) { - // Special case for computed fields returning scalars functions - if (inArgs.length > 0) { - const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = types.find(({ id }) => id === type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return { name, type: tsType, has_default } - }) - argsType = `{ ${argsNameAndType.toSorted((a, b) => a.name.localeCompare(b.name)).map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` - } - 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`)}` - } else if (inArgs.length > 0) { - const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = types.find(({ id }) => id === type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return { name, type: tsType, has_default } - }) - argsType = `{ ${argsNameAndType.toSorted((a, b) => a.name.localeCompare(b.name)).map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` - } - - return `{ Args: ${argsType}; Returns: ${getFunctionTsReturnType(fn, returnType)} }` - }) - + return Object.entries(schemaFunctionsGroupedByName).map(([fnName, fns]) => { + const functionSignatures = getFunctionSignatures(schema, fns) return `${JSON.stringify(fnName)}:\n${functionSignatures.map((sig) => `| ${sig}`).join('\n')}` }) })()} @@ -637,7 +649,7 @@ export type Database = { ({ name, attributes }) => `${JSON.stringify(name)}: { ${attributes.map(({ name, type_id }) => { - const type = types.find(({ id }) => id === type_id) + const type = typesById[type_id] let tsType = 'unknown' if (type) { tsType = `${pgTypeToTsType(schema, type.name, { @@ -654,7 +666,7 @@ export type Database = { } } }` - })} + })} } type DatabaseWithoutInternals = Omit @@ -763,13 +775,9 @@ export type CompositeTypes< : never export const Constants = { - ${schemas - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - .map((schema) => { - const schemaEnums = types - .filter((type) => type.schema === schema.name && type.enums.length > 0) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - return `${JSON.stringify(schema.name)}: { + ${schemas.map((schema) => { + const schemaEnums = introspectionBySchema[schema.name]?.enums ?? [] + return `${JSON.stringify(schema.name)}: { Enums: { ${schemaEnums.map( (enum_) => @@ -779,7 +787,7 @@ export const Constants = { )} } }` - })} + })} } as const ` From c22c83065da99c157a8d7e5e9456af75f4c54631 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 6 Aug 2025 18:27:30 +0200 Subject: [PATCH 04/26] chore: remove duplicate sort --- src/server/templates/typescript.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index e9254fa6..c375f45f 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -108,7 +108,6 @@ export const apply = async ({ // OR if the function takes a table row but doesn't qualify as embedded (for error reporting) (inArgs[0].table_name && !func.return_table_name))) ) { - func.args.sort((a, b) => a.name.localeCompare(b.name)) introspectionBySchema[func.schema].functions.push({ fn: func, inArgs }) } From fb11ed8cb3cac3ae7e500ac7afa90c1a54296024 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 16 Sep 2025 15:35:24 +0200 Subject: [PATCH 05/26] chore: include view in type --- src/lib/sql/types.sql.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/sql/types.sql.ts b/src/lib/sql/types.sql.ts index 990fa22f..d707ebb5 100644 --- a/src/lib/sql/types.sql.ts +++ b/src/lib/sql/types.sql.ts @@ -46,7 +46,7 @@ from t.typrelid = 0 or ( select - c.relkind ${props.includeTableTypes ? `in ('c', 'r')` : `= 'c'`} + c.relkind ${props.includeTableTypes ? `in ('c', 'r', 'v')` : `= 'c'`} from pg_class c where From d6626b4068c29954cfb81e53d660bd6523c26bca Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 16 Sep 2025 16:39:20 +0200 Subject: [PATCH 06/26] fix: isOneToOne --- src/server/templates/typescript.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 8ba87cf9..741dbca3 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -154,7 +154,7 @@ export const apply = async ({ setofOptionsInfo = `SetofOptions: { from: ${JSON.stringify(typesById[fn.args[0].type_id].format)} to: ${JSON.stringify(fn.return_table_name)} - isOneToOne: ${Boolean(fn.returns_multiple_rows)} + isOneToOne: ${Boolean(!fn.returns_multiple_rows)} isSetofReturn: true }` } @@ -176,7 +176,7 @@ export const apply = async ({ setofOptionsInfo = `SetofOptions: { from: "*" to: ${JSON.stringify(fn.return_table_name)} - isOneToOne: ${Boolean(fn.returns_multiple_rows)} + isOneToOne: ${Boolean(!fn.returns_multiple_rows)} isSetofReturn: ${fn.is_set_returning_function} }` } From 6b251d6edafcb30d3b69ea18638a57307b35930f Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 16 Sep 2025 17:52:03 +0200 Subject: [PATCH 07/26] fix: tests --- src/lib/sql/functions.sql.ts | 1 + test/server/typegen.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/lib/sql/functions.sql.ts b/src/lib/sql/functions.sql.ts index cf5ecc45..930fc4cb 100644 --- a/src/lib/sql/functions.sql.ts +++ b/src/lib/sql/functions.sql.ts @@ -177,6 +177,7 @@ from ) as t2 left join pg_type pt on pt.oid = t1.type_id left join pg_class pc on pc.oid = pt.typrelid + order by t1.name asc ) sub group by oid diff --git a/test/server/typegen.ts b/test/server/typegen.ts index 03f93b45..6b8f7fc9 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -4309,9 +4309,9 @@ test('typegen: typescript consistent types definitions orders', async () => { DROP FUNCTION IF EXISTS test_func_gamma(integer, text, boolean) CASCADE; -- Alternative signatures for functions (different parameter orders) - DROP FUNCTION IF EXISTS test_func_alpha(text, boolean, integer) CASCADE; - DROP FUNCTION IF EXISTS test_func_beta(boolean, integer, text) CASCADE; - DROP FUNCTION IF EXISTS test_func_gamma(boolean, text, integer) CASCADE; + DROP FUNCTION IF EXISTS test_func_alpha_2(boolean, text, integer) CASCADE; + DROP FUNCTION IF EXISTS test_func_beta_2(text, boolean, integer) CASCADE; + DROP FUNCTION IF EXISTS test_func_gamma_2(boolean, integer, text) CASCADE; -- Drop tables DROP TABLE IF EXISTS test_table_alpha CASCADE; @@ -4481,19 +4481,19 @@ test('typegen: typescript consistent types definitions orders', async () => { }, }) - // Create functions in reverse order: gamma, beta, alpha with different parameter orders + // Create functions in reverse order: gamma, beta, alpha with same parameter orders await app.inject({ method: 'POST', path: '/query', payload: { query: ` - CREATE FUNCTION test_func_gamma(param_c boolean, param_a integer, param_b text) + CREATE FUNCTION test_func_gamma(param_a integer, param_b text, param_c boolean) RETURNS boolean AS 'SELECT NOT param_c' LANGUAGE sql IMMUTABLE; - CREATE FUNCTION test_func_beta(param_b text, param_c boolean, param_a integer) + CREATE FUNCTION test_func_beta(param_a integer, param_b text, param_c boolean) RETURNS text AS 'SELECT param_b || ''_processed''' LANGUAGE sql IMMUTABLE; - CREATE FUNCTION test_func_alpha(param_c boolean, param_b text, param_a integer) + CREATE FUNCTION test_func_alpha(param_a integer, param_b text, param_c boolean) RETURNS integer AS 'SELECT param_a + 1' LANGUAGE sql IMMUTABLE; `, }, From da46bf62f7045ba979016b9604f34f1863d29d4d Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Sep 2025 11:56:11 +0200 Subject: [PATCH 08/26] chore: dedup typescript typegen logic --- src/server/templates/typescript.ts | 516 +++++++++++++++-------------- 1 file changed, 259 insertions(+), 257 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 1c262fae..9e9a9fef 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -1,4 +1,5 @@ import prettier from 'prettier' +import type { GeneratorMetadata } from '../../lib/generators.js' import type { PostgresColumn, PostgresFunction, @@ -7,9 +8,13 @@ import type { PostgresType, PostgresView, } from '../../lib/index.js' -import type { GeneratorMetadata } from '../../lib/generators.js' import { GENERATE_TYPES_DEFAULT_SCHEMA, VALID_FUNCTION_ARGS_MODE } from '../constants.js' +type TsRelationship = Pick< + GeneratorMetadata['relationships'][number], + 'foreign_key_name' | 'columns' | 'is_one_to_one' | 'referenced_relation' | 'referenced_columns' +> + export const apply = async ({ schemas, tables, @@ -41,8 +46,14 @@ export const apply = async ({ } const introspectionBySchema = Object.fromEntries<{ - tables: Pick[] - views: PostgresView[] + tables: { + table: Pick + relationships: TsRelationship[] + }[] + views: { + view: PostgresView + relationships: TsRelationship[] + }[] functions: { fn: PostgresFunction; inArgs: PostgresFunction['args'] }[] enums: PostgresType[] compositeTypes: PostgresType[] @@ -52,26 +63,70 @@ export const apply = async ({ { tables: [], views: [], functions: [], enums: [], compositeTypes: [] }, ]) ) + + function getRelationships( + object: { schema: string; name: string }, + relationships: GeneratorMetadata['relationships'] + ): Pick< + GeneratorMetadata['relationships'][number], + 'foreign_key_name' | 'columns' | 'is_one_to_one' | 'referenced_relation' | 'referenced_columns' + >[] { + return relationships + .filter( + (relationship) => + relationship.schema === object.schema && + relationship.referenced_schema === object.schema && + relationship.relation === object.name + ) + .toSorted( + (a, b) => + a.foreign_key_name.localeCompare(b.foreign_key_name) || + a.referenced_relation.localeCompare(b.referenced_relation) || + JSON.stringify(a.referenced_columns).localeCompare(JSON.stringify(b.referenced_columns)) + ) + } + + function generateRelationshiptTsDefinition(relationship: TsRelationship): string { + return `{ + foreignKeyName: ${JSON.stringify(relationship.foreign_key_name)} + columns: ${JSON.stringify(relationship.columns)}${detectOneToOneRelationships ? `\nisOneToOne: ${relationship.is_one_to_one}` : ''} + referencedRelation: ${JSON.stringify(relationship.referenced_relation)} + referencedColumns: ${JSON.stringify(relationship.referenced_columns)} + }` + } + for (const table of tables) { if (table.schema in introspectionBySchema) { - introspectionBySchema[table.schema].tables.push(table) + introspectionBySchema[table.schema].tables.push({ + table, + relationships: getRelationships(table, relationships), + }) } } for (const table of foreignTables) { if (table.schema in introspectionBySchema) { - introspectionBySchema[table.schema].tables.push(table) + introspectionBySchema[table.schema].tables.push({ + table, + relationships: getRelationships(table, relationships), + }) } } for (const view of views) { if (view.schema in introspectionBySchema) { - introspectionBySchema[view.schema].views.push(view) + introspectionBySchema[view.schema].views.push({ + view, + relationships: getRelationships(view, relationships), + }) } } for (const materializedView of materializedViews) { if (materializedView.schema in introspectionBySchema) { introspectionBySchema[materializedView.schema].views.push({ - ...materializedView, - is_updatable: false, + view: { + ...materializedView, + is_updatable: false, + }, + relationships: getRelationships(materializedView, relationships), }) } } @@ -104,8 +159,8 @@ export const apply = async ({ } } for (const schema in introspectionBySchema) { - introspectionBySchema[schema].tables.sort((a, b) => a.name.localeCompare(b.name)) - introspectionBySchema[schema].views.sort((a, b) => a.name.localeCompare(b.name)) + introspectionBySchema[schema].tables.sort((a, b) => a.table.name.localeCompare(b.table.name)) + introspectionBySchema[schema].views.sort((a, b) => a.view.name.localeCompare(b.view.name)) introspectionBySchema[schema].functions.sort((a, b) => a.fn.name.localeCompare(b.fn.name)) introspectionBySchema[schema].enums.sort((a, b) => a.name.localeCompare(b.name)) introspectionBySchema[schema].compositeTypes.sort((a, b) => a.name.localeCompare(b.name)) @@ -120,6 +175,98 @@ export const apply = async ({ {} as Record ) + const getFunctionTsReturnType = (fn: PostgresFunction, returnType: string) => { + return `${returnType}${fn.is_set_returning_function ? '[]' : ''}` + } + + const getFunctionReturnType = (schema: PostgresSchema, fn: PostgresFunction): string => { + const tableArgs = fn.args.filter(({ mode }) => mode === 'table') + if (tableArgs.length > 0) { + const argsNameAndType = tableArgs.map(({ name, type_id }) => { + const type = typesById[type_id] + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(schema, type.name, { + types, + schemas, + tables, + views, + }) + } + return { name, type: tsType } + }) + + return `{ + ${argsNameAndType.map(({ name, type }) => `${JSON.stringify(name)}: ${type}`)} + }` + } + + // Case 2: returns a relation's row type. + const relation = + introspectionBySchema[schema.name]?.tables.find( + ({ table: { id } }) => id === fn.return_type_relation_id + )?.table || + introspectionBySchema[schema.name]?.views.find( + ({ view: { id } }) => id === fn.return_type_relation_id + )?.view + if (relation) { + return `{ + ${columnsByTableId[relation.id].map( + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType(schema, column.format, { + types, + schemas, + tables, + views, + })} ${column.is_nullable ? '| null' : ''}` + )} + }` + } + + // Case 3: returns base/array/composite/enum type. + const type = typesById[fn.return_type_id] + if (type) { + return pgTypeToTsType(schema, type.name, { + types, + schemas, + tables, + views, + }) + } + + return 'unknown' + } + + const getFunctionSignatures = ( + schema: PostgresSchema, + fns: Array<{ fn: PostgresFunction; inArgs: PostgresFunction['args'] }> + ) => { + const args = fns + .map(({ inArgs }) => { + if (inArgs.length === 0) { + return 'Record' + } + const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { + const type = typesById[type_id] + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(schema, type.name, { + types, + schemas, + tables, + views, + }) + } + return { name, type: tsType, has_default } + }) + return `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` + }) + .toSorted() + // A function can have multiples definitions with differents args, but will always return the same type + .join(' | ') + return `{\nArgs: ${args}\n Returns: ${getFunctionTsReturnType(fns[0].fn, getFunctionReturnType(schema, fns[0].fn))}\n}` + } + const internal_supabase_schema = postgrestVersion ? `// Allows to automatically instantiate createClient with right options // instead of createClient(URL, KEY) @@ -128,6 +275,24 @@ export const apply = async ({ }` : '' + function generateColumnTsDefinition( + schema: PostgresSchema, + column: { + name: string + format: string + is_nullable: boolean + is_optional: boolean + }, + context: { + types: PostgresType[] + schemas: PostgresSchema[] + tables: PostgresTable[] + views: PostgresView[] + } + ) { + return `${JSON.stringify(column.name)}${column.is_optional ? '?' : ''}: ${pgTypeToTsType(schema, column.format, context)} ${column.is_nullable ? '| null' : ''}` + } + let output = ` export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] @@ -147,117 +312,68 @@ export type Database = { schemaTables.length === 0 ? '[_ in never]: never' : schemaTables.map( - (table) => `${JSON.stringify(table.name)}: { + ({ table, relationships }) => `${JSON.stringify(table.name)}: { Row: { ${[ - ...columnsByTableId[table.id].map( - (column) => - `${JSON.stringify(column.name)}: ${pgTypeToTsType(schema, column.format, { - types, - schemas, - tables, - views, - })} ${column.is_nullable ? '| null' : ''}` + ...columnsByTableId[table.id].map((column) => + generateColumnTsDefinition( + schema, + { + name: column.name, + format: column.format, + is_nullable: column.is_nullable, + is_optional: false, + }, + { types, schemas, tables, views } + ) ), ...schemaFunctions .filter(({ fn }) => fn.argument_types === table.name) .map(({ fn }) => { - const type = typesById[fn.return_type_id] - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return `${JSON.stringify(fn.name)}: ${tsType} | null` + return `${JSON.stringify(fn.name)}: ${getFunctionReturnType(schema, fn)} | null` }), ]} } Insert: { ${columnsByTableId[table.id].map((column) => { - let output = JSON.stringify(column.name) - if (column.identity_generation === 'ALWAYS') { - return `${output}?: never` - } - - if ( - column.is_nullable || - column.is_identity || - column.default_value !== null - ) { - output += '?:' - } else { - output += ':' + return `${JSON.stringify(column.name)}?: never` } - - output += pgTypeToTsType(schema, column.format, { - types, - schemas, - tables, - views, - }) - - if (column.is_nullable) { - output += '| null' - } - - return output + return generateColumnTsDefinition( + schema, + { + name: column.name, + format: column.format, + is_nullable: column.is_nullable, + is_optional: + column.is_nullable || + column.is_identity || + column.default_value !== null, + }, + { types, schemas, tables, views } + ) })} } Update: { ${columnsByTableId[table.id].map((column) => { - let output = JSON.stringify(column.name) - if (column.identity_generation === 'ALWAYS') { - return `${output}?: never` + return `${JSON.stringify(column.name)}?: never` } - output += `?: ${pgTypeToTsType(schema, column.format, { - types, - schemas, - tables, - views, - })}` - - if (column.is_nullable) { - output += '| null' - } - - return output + return generateColumnTsDefinition( + schema, + { + name: column.name, + format: column.format, + is_nullable: column.is_nullable, + is_optional: true, + }, + { types, schemas, tables, views } + ) })} } Relationships: [ - ${relationships - .filter( - (relationship) => - relationship.schema === table.schema && - relationship.referenced_schema === table.schema && - relationship.relation === table.name - ) - .sort( - (a, b) => - a.foreign_key_name.localeCompare(b.foreign_key_name) || - a.referenced_relation.localeCompare(b.referenced_relation) || - JSON.stringify(a.referenced_columns).localeCompare( - JSON.stringify(b.referenced_columns) - ) - ) - .map( - (relationship) => `{ - foreignKeyName: ${JSON.stringify(relationship.foreign_key_name)} - columns: ${JSON.stringify(relationship.columns)} - ${ - detectOneToOneRelationships - ? `isOneToOne: ${relationship.is_one_to_one};` - : '' - }referencedRelation: ${JSON.stringify(relationship.referenced_relation)} - referencedColumns: ${JSON.stringify(relationship.referenced_columns)} - }` - )} + ${relationships.map(generateRelationshiptTsDefinition)} ] }` ) @@ -268,86 +384,61 @@ export type Database = { schemaViews.length === 0 ? '[_ in never]: never' : schemaViews.map( - (view) => `${JSON.stringify(view.name)}: { + ({ view, relationships }) => `${JSON.stringify(view.name)}: { Row: { - ${columnsByTableId[view.id].map( - (column) => - `${JSON.stringify(column.name)}: ${pgTypeToTsType(schema, column.format, { - types, - schemas, - tables, - views, - })} ${column.is_nullable ? '| null' : ''}` + ${columnsByTableId[view.id].map((column) => + generateColumnTsDefinition( + schema, + { + name: column.name, + format: column.format, + is_nullable: column.is_nullable, + is_optional: false, + }, + { types, schemas, tables, views } + ) )} } ${ view.is_updatable ? `Insert: { ${columnsByTableId[view.id].map((column) => { - let output = JSON.stringify(column.name) - if (!column.is_updatable) { - return `${output}?: never` + return `${JSON.stringify(column.name)}?: never` } - - output += `?: ${pgTypeToTsType(schema, column.format, { - types, - schemas, - tables, - views, - })} | null` - - return output + return generateColumnTsDefinition( + schema, + { + name: column.name, + format: column.format, + is_nullable: true, + is_optional: true, + }, + { types, schemas, tables, views } + ) })} } Update: { ${columnsByTableId[view.id].map((column) => { - let output = JSON.stringify(column.name) - if (!column.is_updatable) { - return `${output}?: never` + return `${JSON.stringify(column.name)}?: never` } - - output += `?: ${pgTypeToTsType(schema, column.format, { - types, - schemas, - tables, - views, - })} | null` - - return output + return generateColumnTsDefinition( + schema, + { + name: column.name, + format: column.format, + is_nullable: true, + is_optional: true, + }, + { types, schemas, tables, views } + ) })} } ` : '' }Relationships: [ - ${relationships - .filter( - (relationship) => - relationship.schema === view.schema && - relationship.referenced_schema === view.schema && - relationship.relation === view.name - ) - .sort( - (a, b) => - a.foreign_key_name.localeCompare(b.foreign_key_name) || - a.referenced_relation.localeCompare(b.referenced_relation) || - JSON.stringify(a.referenced_columns).localeCompare( - JSON.stringify(b.referenced_columns) - ) - ) - .map( - (relationship) => `{ - foreignKeyName: ${JSON.stringify(relationship.foreign_key_name)} - columns: ${JSON.stringify(relationship.columns)} - ${ - detectOneToOneRelationships - ? `isOneToOne: ${relationship.is_one_to_one};` - : '' - }referencedRelation: ${JSON.stringify(relationship.referenced_relation)} - referencedColumns: ${JSON.stringify(relationship.referenced_columns)} - }` - )} + ${relationships.map(generateRelationshiptTsDefinition)} ] }` ) @@ -373,97 +464,12 @@ export type Database = { ) } - return Object.entries(schemaFunctionsGroupedByName).map( - ([fnName, fns]) => - `${JSON.stringify(fnName)}: { - Args: ${fns - .map(({ inArgs }) => { - if (inArgs.length === 0) { - return 'Record' - } - - const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = typesById[type_id] - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return { name, type: tsType, has_default } - }) - return `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` - }) - .toSorted() - // A function can have multiples definitions with differents args, but will always return the same type - .join(' | ')} - Returns: ${(() => { - // Case 1: `returns table`. - const tableArgs = fns[0].fn.args.filter(({ mode }) => mode === 'table') - if (tableArgs.length > 0) { - const argsNameAndType = tableArgs.map(({ name, type_id }) => { - const type = typesById[type_id] - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return { name, type: tsType } - }) - - return `{ - ${argsNameAndType - .toSorted((a, b) => a.name.localeCompare(b.name)) - .map(({ name, type }) => `${JSON.stringify(name)}: ${type}`)} - }` - } - - // Case 2: returns a relation's row type. - const relation = [...tables, ...views].find( - ({ id }) => id === fns[0].fn.return_type_relation_id - ) - if (relation) { - return `{ - ${columnsByTableId[relation.id] - .toSorted((a, b) => a.name.localeCompare(b.name)) - .map( - (column) => - `${JSON.stringify(column.name)}: ${pgTypeToTsType( - schema, - column.format, - { - types, - schemas, - tables, - views, - } - )} ${column.is_nullable ? '| null' : ''}` - )} - }` - } - - // Case 3: returns base/array/composite/enum type. - const type = typesById[fns[0].fn.return_type_id] - if (type) { - return pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - - return 'unknown' - })()}${fns[0].fn.is_set_returning_function ? '[]' : ''} - }` - ) + return Object.entries(schemaFunctionsGroupedByName) + .map(([fnName, fns]) => { + const functionSignatures = getFunctionSignatures(schema, fns) + return `${JSON.stringify(fnName)}:\n${functionSignatures}` + }) + .join(',\n') })()} } Enums: { @@ -612,13 +618,9 @@ export type CompositeTypes< : never export const Constants = { - ${schemas - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - .map((schema) => { - const schemaEnums = types - .filter((type) => type.schema === schema.name && type.enums.length > 0) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - return `${JSON.stringify(schema.name)}: { + ${schemas.map((schema) => { + const schemaEnums = introspectionBySchema[schema.name].enums + return `${JSON.stringify(schema.name)}: { Enums: { ${schemaEnums.map( (enum_) => @@ -628,7 +630,7 @@ export const Constants = { )} } }` - })} + })} } as const ` From f8bbfcabec0cbf893d06d91cebd68e5a0360a73b Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Sep 2025 17:22:12 +0200 Subject: [PATCH 09/26] chore: re-use generateColumn --- src/server/templates/typescript.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index a6353441..79674506 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -271,14 +271,22 @@ export const apply = async ({ if (relation) { return `{ ${columnsByTableId[relation.id] - .map( - (column) => - `${JSON.stringify(column.name)}: ${pgTypeToTsType(schema, column.format, { + .map((column) => + generateColumnTsDefinition( + schema, + { + name: column.name, + format: column.format, + is_nullable: column.is_nullable, + is_optional: false, + }, + { types, schemas, tables, views, - })} ${column.is_nullable ? '| null' : ''}` + } + ) ) .join(',\n')} }` From 445f1ffb71352c815f3da5d6d4793a82eb63b3bf Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Sep 2025 18:04:20 +0200 Subject: [PATCH 10/26] fix: retrieve prorows only --- src/lib/sql/functions.sql.ts | 19 +--------- src/lib/types.ts | 4 +- test/lib/functions.ts | 72 +++++++++++------------------------- 3 files changed, 24 insertions(+), 71 deletions(-) diff --git a/src/lib/sql/functions.sql.ts b/src/lib/sql/functions.sql.ts index 930fc4cb..fb171b71 100644 --- a/src/lib/sql/functions.sql.ts +++ b/src/lib/sql/functions.sql.ts @@ -86,24 +86,9 @@ select nullif(rt.typrelid::int8, 0) as return_type_relation_id, f.proretset as is_set_returning_function, case - when f.proretset and rt.typrelid != 0 and exists ( - select 1 from pg_class c - where c.oid = rt.typrelid - -- exclude custom types relation from what is considered a set of table - and c.relkind in ('r', 'p', 'v', 'm', 'f') - ) then true - else false - end as returns_set_of_table, - case - when rt.typrelid != 0 then - (select relname from pg_class where oid = rt.typrelid) + when f.proretset then nullif(f.prorows, 0) else null - end as return_table_name, - case - when f.proretset then - coalesce(f.prorows, 0) > 1 - else false - end as returns_multiple_rows, + end as prorows, case when f.provolatile = 'i' then 'IMMUTABLE' when f.provolatile = 's' then 'STABLE' diff --git a/src/lib/types.ts b/src/lib/types.ts index 4d4c2889..8f9d5e4d 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -157,9 +157,7 @@ const postgresFunctionSchema = Type.Object({ return_type: Type.String(), return_type_relation_id: Type.Union([Type.Integer(), Type.Null()]), is_set_returning_function: Type.Boolean(), - returns_set_of_table: Type.Boolean(), - return_table_name: Type.Union([Type.String(), Type.Null()]), - returns_multiple_rows: Type.Boolean(), + prorows: Type.Union([Type.Number(), Type.Null()]), behavior: Type.Union([ Type.Literal('IMMUTABLE'), Type.Literal('STABLE'), diff --git a/test/lib/functions.ts b/test/lib/functions.ts index 3b76a02f..8ab4c10a 100644 --- a/test/lib/functions.ts +++ b/test/lib/functions.ts @@ -4,8 +4,7 @@ import { pgMeta } from './utils' test('list', async () => { const res = await pgMeta.functions.list() expect(res.data?.find(({ name }) => name === 'add')).toMatchInlineSnapshot( - { id: expect.any(Number) }, - ` + { id: expect.any(Number) }, ` { "args": [ { @@ -38,17 +37,14 @@ test('list', async () => { "is_set_returning_function": false, "language": "sql", "name": "add", - "return_table_name": null, + "prorows": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, - "returns_multiple_rows": false, - "returns_set_of_table": false, "schema": "public", "security_definer": false, } - ` - ) + `) }) test('list set-returning function with single object limit', async () => { @@ -85,12 +81,10 @@ test('list set-returning function with single object limit', async () => { "is_set_returning_function": true, "language": "sql", "name": "get_user_audit_setof_single_row", - "return_table_name": "users_audit", + "prorows": 1, "return_type": "SETOF users_audit", "return_type_id": 16418, "return_type_relation_id": 16416, - "returns_multiple_rows": false, - "returns_set_of_table": true, "schema": "public", "security_definer": false, }, @@ -131,12 +125,10 @@ test('list set-returning function with multiples definitions', async () => { "is_set_returning_function": true, "language": "sql", "name": "get_todos_setof_rows", - "return_table_name": "todos", + "prorows": 1000, "return_type": "SETOF todos", "return_type_id": 16404, "return_type_relation_id": 16402, - "returns_multiple_rows": true, - "returns_set_of_table": true, "schema": "public", "security_definer": false, }, @@ -169,12 +161,10 @@ test('list set-returning function with multiples definitions', async () => { "is_set_returning_function": true, "language": "sql", "name": "get_todos_setof_rows", - "return_table_name": "todos", + "prorows": 1000, "return_type": "SETOF todos", "return_type_id": 16404, "return_type_relation_id": 16402, - "returns_multiple_rows": true, - "returns_set_of_table": true, "schema": "public", "security_definer": false, }, @@ -234,8 +224,7 @@ test('retrieve, create, update, delete', async () => { config_params: { search_path: 'hooks, auth', role: 'postgres' }, }) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, - ` + { data: { id: expect.any(Number) } }, ` { "data": { "args": [ @@ -274,23 +263,19 @@ test('retrieve, create, update, delete', async () => { "is_set_returning_function": false, "language": "sql", "name": "test_func", - "return_table_name": null, + "prorows": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, - "returns_multiple_rows": false, - "returns_set_of_table": false, "schema": "public", "security_definer": true, }, "error": null, } - ` - ) + `) res = await pgMeta.functions.retrieve({ id: res.data!.id }) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, - ` + { data: { id: expect.any(Number) } }, ` { "data": { "args": [ @@ -329,27 +314,23 @@ test('retrieve, create, update, delete', async () => { "is_set_returning_function": false, "language": "sql", "name": "test_func", - "return_table_name": null, + "prorows": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, - "returns_multiple_rows": false, - "returns_set_of_table": false, "schema": "public", "security_definer": true, }, "error": null, } - ` - ) + `) res = await pgMeta.functions.update(res.data!.id, { name: 'test_func_renamed', schema: 'test_schema', definition: 'select b - a', }) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, - ` + { data: { id: expect.any(Number) } }, ` { "data": { "args": [ @@ -388,23 +369,19 @@ test('retrieve, create, update, delete', async () => { "is_set_returning_function": false, "language": "sql", "name": "test_func_renamed", - "return_table_name": null, + "prorows": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, - "returns_multiple_rows": false, - "returns_set_of_table": false, "schema": "test_schema", "security_definer": true, }, "error": null, } - ` - ) + `) res = await pgMeta.functions.remove(res.data!.id) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, - ` + { data: { id: expect.any(Number) } }, ` { "data": { "args": [ @@ -443,19 +420,16 @@ test('retrieve, create, update, delete', async () => { "is_set_returning_function": false, "language": "sql", "name": "test_func_renamed", - "return_table_name": null, + "prorows": null, "return_type": "integer", "return_type_id": 23, "return_type_relation_id": null, - "returns_multiple_rows": false, - "returns_set_of_table": false, "schema": "test_schema", "security_definer": true, }, "error": null, } - ` - ) + `) res = await pgMeta.functions.retrieve({ id: res.data!.id }) expect(res).toMatchObject({ data: null, @@ -478,8 +452,7 @@ test('retrieve set-returning function', async () => { id: expect.any(Number), return_type_id: expect.any(Number), return_type_relation_id: expect.any(Number), - }, - ` + }, ` { "args": [], "argument_types": "", @@ -501,17 +474,14 @@ test('retrieve set-returning function', async () => { "is_set_returning_function": true, "language": "sql", "name": "function_returning_set_of_rows", - "return_table_name": "users", + "prorows": 1000, "return_type": "SETOF users", "return_type_id": Any, "return_type_relation_id": Any, - "returns_multiple_rows": true, - "returns_set_of_table": true, "schema": "public", "security_definer": false, } - ` - ) + `) }) test('retrieve function by args filter - polymorphic function with text argument', async () => { From 5d002dfbbbebf009c404cc51e14708ef3cb0aff7 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Sep 2025 18:48:32 +0200 Subject: [PATCH 11/26] chore: refactor typegen for prorows only --- src/server/templates/typescript.ts | 93 +++++++++++++++++++----------- 1 file changed, 60 insertions(+), 33 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 79674506..a38b16da 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -37,9 +37,22 @@ export const apply = async ({ }): Promise => { schemas.sort((a, b) => a.name.localeCompare(b.name)) - const columnsByTableId = Object.fromEntries( - [...tables, ...foreignTables, ...views, ...materializedViews].map((t) => [t.id, []]) + const columnsByTableId: Record = {} + const tablesNamesByTableId: Record = {} + // group types by id for quicker lookup + const typesById = types.reduce( + (acc, type) => { + acc[type.id] = type + return acc + }, + {} as Record ) + const tablesLike = [...tables, ...foreignTables, ...views, ...materializedViews] + + for (const tableLike of tablesLike) { + columnsByTableId[tableLike.id] = [] + tablesNamesByTableId[tableLike.id] = tableLike.name + } for (const column of columns) { if (column.table_id in columnsByTableId) { columnsByTableId[column.table_id].push(column) @@ -134,6 +147,30 @@ export const apply = async ({ }) } } + for (const type of types) { + if (type.schema in introspectionBySchema) { + if (type.enums.length > 0) { + introspectionBySchema[type.schema].enums.push(type) + } + if (type.attributes.length > 0) { + introspectionBySchema[type.schema].compositeTypes.push(type) + } + } + } + // Helper function to get table/view name from relation id + const getTableNameFromRelationId = ( + relationId: number | null, + returnTypeId: number | null + ): string | null => { + if (!relationId) return null + + if (tablesNamesByTableId[relationId]) return tablesNamesByTableId[relationId] + // if it's a composite type we use the type name as relation name to allow sub-selecting fields of the composite type + if (returnTypeId && typesById[returnTypeId] && typesById[returnTypeId].attributes.length > 0) + return typesById[returnTypeId].name + return null + } + for (const func of functions) { if (func.schema in introspectionBySchema) { func.args.sort((a, b) => a.name.localeCompare(b.name)) @@ -159,24 +196,16 @@ export const apply = async ({ inArgs[0].name === '' && (VALID_UNNAMED_FUNCTION_ARG_TYPES.has(inArgs[0].type_id) || // OR if the function have a single unnamed args which is another table (embeded function) - (inArgs[0].table_name && func.return_table_name) || + (inArgs[0].table_name && + getTableNameFromRelationId(func.return_type_relation_id, func.return_type_id)) || // OR if the function takes a table row but doesn't qualify as embedded (for error reporting) - (inArgs[0].table_name && !func.return_table_name))) + (inArgs[0].table_name && + !getTableNameFromRelationId(func.return_type_relation_id, func.return_type_id)))) ) { introspectionBySchema[func.schema].functions.push({ fn: func, inArgs }) } } } - for (const type of types) { - if (type.schema in introspectionBySchema) { - if (type.enums.length > 0) { - introspectionBySchema[type.schema].enums.push(type) - } - if (type.attributes.length > 0) { - introspectionBySchema[type.schema].compositeTypes.push(type) - } - } - } for (const schema in introspectionBySchema) { introspectionBySchema[schema].tables.sort((a, b) => a.table.name.localeCompare(b.table.name)) introspectionBySchema[schema].views.sort((a, b) => a.view.name.localeCompare(b.view.name)) @@ -185,35 +214,33 @@ export const apply = async ({ introspectionBySchema[schema].compositeTypes.sort((a, b) => a.name.localeCompare(b.name)) } - // group types by id for quicker lookup - const typesById = types.reduce( - (acc, type) => { - acc[type.id] = type - return acc - }, - {} as Record - ) - const getFunctionTsReturnType = (fn: PostgresFunction, returnType: string) => { // Determine if this function should have SetofOptions let setofOptionsInfo = '' + const returnTableName = getTableNameFromRelationId( + fn.return_type_relation_id, + fn.return_type_id + ) + const returnsSetOfTable = fn.is_set_returning_function && fn.return_type_relation_id !== null + const returnsMultipleRows = fn.prorows !== null && fn.prorows > 1 + // Only add SetofOptions for functions with table arguments (embedded functions) // or specific functions that RETURNS table-name if (fn.args.length === 1 && fn.args[0].table_name) { // Case 1: Standard embedded function with proper setof detection - if (fn.returns_set_of_table && fn.return_table_name) { + if (returnsSetOfTable && returnTableName) { setofOptionsInfo = `SetofOptions: { from: ${JSON.stringify(typesById[fn.args[0].type_id].format)} - to: ${JSON.stringify(fn.return_table_name)} - isOneToOne: ${Boolean(!fn.returns_multiple_rows)} + to: ${JSON.stringify(returnTableName)} + isOneToOne: ${Boolean(!returnsMultipleRows)} isSetofReturn: true }` } // Case 2: Handle RETURNS table-name those are always a one to one relationship - else if (fn.return_table_name && !fn.returns_set_of_table) { + else if (returnTableName && !returnsSetOfTable) { const sourceTable = typesById[fn.args[0].type_id].format - const targetTable = fn.return_table_name + const targetTable = returnTableName setofOptionsInfo = `SetofOptions: { from: ${JSON.stringify(sourceTable)} to: ${JSON.stringify(targetTable)} @@ -224,16 +251,16 @@ export const apply = async ({ } // Case 3: Special case for functions without table arguments still returning a table // Those can be used in rpc to select sub fields of a table - else if (fn.return_table_name) { + else if (returnTableName) { setofOptionsInfo = `SetofOptions: { from: "*" - to: ${JSON.stringify(fn.return_table_name)} - isOneToOne: ${Boolean(!fn.returns_multiple_rows)} + to: ${JSON.stringify(returnTableName)} + isOneToOne: ${Boolean(!returnsMultipleRows)} isSetofReturn: ${fn.is_set_returning_function} }` } - return `${returnType}${fn.is_set_returning_function && fn.returns_multiple_rows ? '[]' : ''} + return `${returnType}${fn.is_set_returning_function && returnsMultipleRows ? '[]' : ''} ${setofOptionsInfo ? `${setofOptionsInfo}` : ''}` } @@ -311,7 +338,7 @@ export const apply = async ({ inArgs.length === 1 && inArgs[0].name === '' && inArgs[0].table_name && - !fn.return_table_name + !getTableNameFromRelationId(fn.return_type_relation_id, fn.return_type_id) ) { return true } From a5ea005d4bbab12e64282b2d83561bf46d18f3da Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Sep 2025 19:54:38 +0200 Subject: [PATCH 12/26] fix: only get reltype in types --- src/lib/sql/functions.sql.ts | 10 ++---- src/lib/sql/types.sql.ts | 3 +- src/lib/types.ts | 2 +- src/server/templates/typescript.ts | 13 +++++--- test/lib/functions.ts | 49 +++++++++++++++--------------- test/lib/types.ts | 3 ++ 6 files changed, 41 insertions(+), 39 deletions(-) diff --git a/src/lib/sql/functions.sql.ts b/src/lib/sql/functions.sql.ts index fb171b71..87112f19 100644 --- a/src/lib/sql/functions.sql.ts +++ b/src/lib/sql/functions.sql.ts @@ -124,8 +124,7 @@ from 'mode', mode, 'name', name, 'type_id', type_id, - 'has_default', has_default, - 'table_name', table_name + 'has_default', has_default )) as args from ( @@ -134,11 +133,7 @@ from t2.mode, t1.name, t1.type_id, - t1.has_default, - case - when pt.typrelid != 0 then pc.relname - else null - end as table_name + t1.has_default from ( select @@ -161,7 +156,6 @@ from end as mode ) as t2 left join pg_type pt on pt.oid = t1.type_id - left join pg_class pc on pc.oid = pt.typrelid order by t1.name asc ) sub group by diff --git a/src/lib/sql/types.sql.ts b/src/lib/sql/types.sql.ts index d707ebb5..3d2744b0 100644 --- a/src/lib/sql/types.sql.ts +++ b/src/lib/sql/types.sql.ts @@ -13,7 +13,8 @@ select format_type (t.oid, null) as format, coalesce(t_enums.enums, '[]') as enums, coalesce(t_attributes.attributes, '[]') as attributes, - obj_description (t.oid, 'pg_type') as comment + obj_description (t.oid, 'pg_type') as comment, + nullif(t.typrelid::int8, 0) as type_relation_id from pg_type t left join pg_namespace n on n.oid = t.typnamespace diff --git a/src/lib/types.ts b/src/lib/types.ts index 8f9d5e4d..26b3bc78 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -148,7 +148,6 @@ const postgresFunctionSchema = Type.Object({ name: Type.String(), type_id: Type.Number(), has_default: Type.Boolean(), - table_name: Type.Union([Type.String(), Type.Null()]), }) ), argument_types: Type.String(), @@ -444,6 +443,7 @@ export const postgresTypeSchema = Type.Object({ enums: Type.Array(Type.String()), attributes: Type.Array(Type.Object({ name: Type.String(), type_id: Type.Integer() })), comment: Type.Union([Type.String(), Type.Null()]), + type_relation_id: Type.Union([Type.Integer(), Type.Null()]), }) export type PostgresType = Static diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index a38b16da..11336cbc 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -39,6 +39,7 @@ export const apply = async ({ const columnsByTableId: Record = {} const tablesNamesByTableId: Record = {} + const relationTypeByIds = new Map() // group types by id for quicker lookup const typesById = types.reduce( (acc, type) => { @@ -148,6 +149,10 @@ export const apply = async ({ } } for (const type of types) { + // Save all the types that are relation types for quicker lookup + if (type.type_relation_id) { + relationTypeByIds.set(type.id, type) + } if (type.schema in introspectionBySchema) { if (type.enums.length > 0) { introspectionBySchema[type.schema].enums.push(type) @@ -196,10 +201,10 @@ export const apply = async ({ inArgs[0].name === '' && (VALID_UNNAMED_FUNCTION_ARG_TYPES.has(inArgs[0].type_id) || // OR if the function have a single unnamed args which is another table (embeded function) - (inArgs[0].table_name && + (relationTypeByIds.get(inArgs[0].type_id) && getTableNameFromRelationId(func.return_type_relation_id, func.return_type_id)) || // OR if the function takes a table row but doesn't qualify as embedded (for error reporting) - (inArgs[0].table_name && + (relationTypeByIds.get(inArgs[0].type_id) && !getTableNameFromRelationId(func.return_type_relation_id, func.return_type_id)))) ) { introspectionBySchema[func.schema].functions.push({ fn: func, inArgs }) @@ -227,7 +232,7 @@ export const apply = async ({ // Only add SetofOptions for functions with table arguments (embedded functions) // or specific functions that RETURNS table-name - if (fn.args.length === 1 && fn.args[0].table_name) { + if (fn.args.length === 1 && relationTypeByIds.get(fn.args[0].type_id)) { // Case 1: Standard embedded function with proper setof detection if (returnsSetOfTable && returnTableName) { setofOptionsInfo = `SetofOptions: { @@ -337,7 +342,7 @@ export const apply = async ({ if ( inArgs.length === 1 && inArgs[0].name === '' && - inArgs[0].table_name && + relationTypeByIds.get(inArgs[0].type_id) && !getTableNameFromRelationId(fn.return_type_relation_id, fn.return_type_id) ) { return true diff --git a/test/lib/functions.ts b/test/lib/functions.ts index 8ab4c10a..5c08e146 100644 --- a/test/lib/functions.ts +++ b/test/lib/functions.ts @@ -4,21 +4,20 @@ import { pgMeta } from './utils' test('list', async () => { const res = await pgMeta.functions.list() expect(res.data?.find(({ name }) => name === 'add')).toMatchInlineSnapshot( - { id: expect.any(Number) }, ` + { id: expect.any(Number) }, + ` { "args": [ { "has_default": false, "mode": "in", "name": "", - "table_name": null, "type_id": 23, }, { "has_default": false, "mode": "in", "name": "", - "table_name": null, "type_id": 23, }, ], @@ -44,7 +43,8 @@ test('list', async () => { "schema": "public", "security_definer": false, } - `) + ` + ) }) test('list set-returning function with single object limit', async () => { @@ -58,7 +58,6 @@ test('list set-returning function with single object limit', async () => { "has_default": false, "mode": "in", "name": "user_row", - "table_name": "users", "type_id": 16395, }, ], @@ -102,7 +101,6 @@ test('list set-returning function with multiples definitions', async () => { "has_default": false, "mode": "in", "name": "user_row", - "table_name": "users", "type_id": 16395, }, ], @@ -138,7 +136,6 @@ test('list set-returning function with multiples definitions', async () => { "has_default": false, "mode": "in", "name": "todo_row", - "table_name": "todos", "type_id": 16404, }, ], @@ -224,7 +221,8 @@ test('retrieve, create, update, delete', async () => { config_params: { search_path: 'hooks, auth', role: 'postgres' }, }) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, ` + { data: { id: expect.any(Number) } }, + ` { "data": { "args": [ @@ -232,14 +230,12 @@ test('retrieve, create, update, delete', async () => { "has_default": false, "mode": "in", "name": "a", - "table_name": null, "type_id": 21, }, { "has_default": false, "mode": "in", "name": "b", - "table_name": null, "type_id": 21, }, ], @@ -272,10 +268,12 @@ test('retrieve, create, update, delete', async () => { }, "error": null, } - `) + ` + ) res = await pgMeta.functions.retrieve({ id: res.data!.id }) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, ` + { data: { id: expect.any(Number) } }, + ` { "data": { "args": [ @@ -283,14 +281,12 @@ test('retrieve, create, update, delete', async () => { "has_default": false, "mode": "in", "name": "a", - "table_name": null, "type_id": 21, }, { "has_default": false, "mode": "in", "name": "b", - "table_name": null, "type_id": 21, }, ], @@ -323,14 +319,16 @@ test('retrieve, create, update, delete', async () => { }, "error": null, } - `) + ` + ) res = await pgMeta.functions.update(res.data!.id, { name: 'test_func_renamed', schema: 'test_schema', definition: 'select b - a', }) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, ` + { data: { id: expect.any(Number) } }, + ` { "data": { "args": [ @@ -338,14 +336,12 @@ test('retrieve, create, update, delete', async () => { "has_default": false, "mode": "in", "name": "a", - "table_name": null, "type_id": 21, }, { "has_default": false, "mode": "in", "name": "b", - "table_name": null, "type_id": 21, }, ], @@ -378,10 +374,12 @@ test('retrieve, create, update, delete', async () => { }, "error": null, } - `) + ` + ) res = await pgMeta.functions.remove(res.data!.id) expect(res).toMatchInlineSnapshot( - { data: { id: expect.any(Number) } }, ` + { data: { id: expect.any(Number) } }, + ` { "data": { "args": [ @@ -389,14 +387,12 @@ test('retrieve, create, update, delete', async () => { "has_default": false, "mode": "in", "name": "a", - "table_name": null, "type_id": 21, }, { "has_default": false, "mode": "in", "name": "b", - "table_name": null, "type_id": 21, }, ], @@ -429,7 +425,8 @@ test('retrieve, create, update, delete', async () => { }, "error": null, } - `) + ` + ) res = await pgMeta.functions.retrieve({ id: res.data!.id }) expect(res).toMatchObject({ data: null, @@ -452,7 +449,8 @@ test('retrieve set-returning function', async () => { id: expect.any(Number), return_type_id: expect.any(Number), return_type_relation_id: expect.any(Number), - }, ` + }, + ` { "args": [], "argument_types": "", @@ -481,7 +479,8 @@ test('retrieve set-returning function', async () => { "schema": "public", "security_definer": false, } - `) + ` + ) }) test('retrieve function by args filter - polymorphic function with text argument', async () => { diff --git a/test/lib/types.ts b/test/lib/types.ts index fb8c8f30..1c21f38f 100644 --- a/test/lib/types.ts +++ b/test/lib/types.ts @@ -17,6 +17,7 @@ test('list', async () => { "id": Any, "name": "user_status", "schema": "public", + "type_relation_id": null, } ` ) @@ -73,6 +74,7 @@ test('list types with include Table Types', async () => { "id": Any, "name": "todos", "schema": "public", + "type_relation_id": 16402, } ` ) @@ -112,6 +114,7 @@ test('composite type attributes', async () => { "id": Any, "name": "test_composite", "schema": "public", + "type_relation_id": 16966, } ` ) From 2ba4a18c4a943deb4c54278458f7bf4936284665 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Sep 2025 19:57:16 +0200 Subject: [PATCH 13/26] chore: reuse relationTypeByIds --- src/server/templates/typescript.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 11336cbc..52a7514b 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -171,9 +171,8 @@ export const apply = async ({ if (tablesNamesByTableId[relationId]) return tablesNamesByTableId[relationId] // if it's a composite type we use the type name as relation name to allow sub-selecting fields of the composite type - if (returnTypeId && typesById[returnTypeId] && typesById[returnTypeId].attributes.length > 0) - return typesById[returnTypeId].name - return null + const reltype = returnTypeId ? relationTypeByIds.get(returnTypeId) : null + return reltype ? reltype.name : null } for (const func of functions) { From 96c8ac7717ae783b4d631696fc4791277e86fc4c Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Sep 2025 20:00:07 +0200 Subject: [PATCH 14/26] chore: reduce functions changes to minimum --- src/lib/sql/functions.sql.ts | 49 ++++++++++++++---------------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/src/lib/sql/functions.sql.ts b/src/lib/sql/functions.sql.ts index 87112f19..97dad2f3 100644 --- a/src/lib/sql/functions.sql.ts +++ b/src/lib/sql/functions.sql.ts @@ -121,7 +121,7 @@ from select oid, jsonb_agg(jsonb_build_object( - 'mode', mode, + 'mode', t2.mode, 'name', name, 'type_id', type_id, 'has_default', has_default @@ -129,37 +129,26 @@ from from ( select - t1.oid, - t2.mode, - t1.name, - t1.type_id, - t1.has_default + oid, + unnest(arg_modes) as mode, + unnest(arg_names) as name, + unnest(arg_types)::int8 as type_id, + unnest(arg_has_defaults) as has_default from - ( - select - oid, - unnest(arg_modes) as mode, - unnest(arg_names) as name, - unnest(arg_types)::int8 as type_id, - unnest(arg_has_defaults) as has_default - from - functions - ) as t1 - cross join lateral ( - select - case - when t1.mode = 'i' then 'in' - when t1.mode = 'o' then 'out' - when t1.mode = 'b' then 'inout' - when t1.mode = 'v' then 'variadic' - else 'table' - end as mode - ) as t2 - left join pg_type pt on pt.oid = t1.type_id - order by t1.name asc - ) sub + functions + ) as t1, + lateral ( + select + case + when t1.mode = 'i' then 'in' + when t1.mode = 'o' then 'out' + when t1.mode = 'b' then 'inout' + when t1.mode = 'v' then 'variadic' + else 'table' + end as mode + ) as t2 group by - oid + t1.oid ) f_args on f_args.oid = f.oid ${props.limit ? `limit ${props.limit}` : ''} ${props.offset ? `offset ${props.offset}` : ''} From 89b5561bf00a9da811d64a91c8aff3d91836538f Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Sep 2025 20:14:51 +0200 Subject: [PATCH 15/26] chore: only single loop for types --- src/server/templates/typescript.ts | 96 ++++++++++++++---------------- 1 file changed, 45 insertions(+), 51 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 52a7514b..4266cee6 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -36,33 +36,6 @@ export const apply = async ({ postgrestVersion?: string }): Promise => { schemas.sort((a, b) => a.name.localeCompare(b.name)) - - const columnsByTableId: Record = {} - const tablesNamesByTableId: Record = {} - const relationTypeByIds = new Map() - // group types by id for quicker lookup - const typesById = types.reduce( - (acc, type) => { - acc[type.id] = type - return acc - }, - {} as Record - ) - const tablesLike = [...tables, ...foreignTables, ...views, ...materializedViews] - - for (const tableLike of tablesLike) { - columnsByTableId[tableLike.id] = [] - tablesNamesByTableId[tableLike.id] = tableLike.name - } - for (const column of columns) { - if (column.table_id in columnsByTableId) { - columnsByTableId[column.table_id].push(column) - } - } - for (const tableId in columnsByTableId) { - columnsByTableId[tableId].sort((a, b) => a.name.localeCompare(b.name)) - } - const introspectionBySchema = Object.fromEntries<{ tables: { table: Pick @@ -81,6 +54,41 @@ export const apply = async ({ { tables: [], views: [], functions: [], enums: [], compositeTypes: [] }, ]) ) + const columnsByTableId: Record = {} + const tablesNamesByTableId: Record = {} + const relationTypeByIds = new Map() + // group types by id for quicker lookup + const typesById = new Map() + const tablesLike = [...tables, ...foreignTables, ...views, ...materializedViews] + + for (const tableLike of tablesLike) { + columnsByTableId[tableLike.id] = [] + tablesNamesByTableId[tableLike.id] = tableLike.name + } + for (const column of columns) { + if (column.table_id in columnsByTableId) { + columnsByTableId[column.table_id].push(column) + } + } + for (const tableId in columnsByTableId) { + columnsByTableId[tableId].sort((a, b) => a.name.localeCompare(b.name)) + } + + for (const type of types) { + typesById.set(type.id, type) + // Save all the types that are relation types for quicker lookup + if (type.type_relation_id) { + relationTypeByIds.set(type.id, type) + } + if (type.schema in introspectionBySchema) { + if (type.enums.length > 0) { + introspectionBySchema[type.schema].enums.push(type) + } + if (type.attributes.length > 0) { + introspectionBySchema[type.schema].compositeTypes.push(type) + } + } + } function getRelationships( object: { schema: string; name: string }, @@ -148,20 +156,6 @@ export const apply = async ({ }) } } - for (const type of types) { - // Save all the types that are relation types for quicker lookup - if (type.type_relation_id) { - relationTypeByIds.set(type.id, type) - } - if (type.schema in introspectionBySchema) { - if (type.enums.length > 0) { - introspectionBySchema[type.schema].enums.push(type) - } - if (type.attributes.length > 0) { - introspectionBySchema[type.schema].compositeTypes.push(type) - } - } - } // Helper function to get table/view name from relation id const getTableNameFromRelationId = ( relationId: number | null, @@ -235,7 +229,7 @@ export const apply = async ({ // Case 1: Standard embedded function with proper setof detection if (returnsSetOfTable && returnTableName) { setofOptionsInfo = `SetofOptions: { - from: ${JSON.stringify(typesById[fn.args[0].type_id].format)} + from: ${JSON.stringify(typesById.get(fn.args[0].type_id)?.format)} to: ${JSON.stringify(returnTableName)} isOneToOne: ${Boolean(!returnsMultipleRows)} isSetofReturn: true @@ -243,7 +237,7 @@ export const apply = async ({ } // Case 2: Handle RETURNS table-name those are always a one to one relationship else if (returnTableName && !returnsSetOfTable) { - const sourceTable = typesById[fn.args[0].type_id].format + const sourceTable = typesById.get(fn.args[0].type_id)?.format const targetTable = returnTableName setofOptionsInfo = `SetofOptions: { from: ${JSON.stringify(sourceTable)} @@ -273,7 +267,7 @@ export const apply = async ({ const tableArgs = fn.args.filter(({ mode }) => mode === 'table') if (tableArgs.length > 0) { const argsNameAndType = tableArgs.map(({ name, type_id }) => { - const type = typesById[type_id] + const type = typesById.get(type_id) let tsType = 'unknown' if (type) { tsType = pgTypeToTsType(schema, type.name, { @@ -324,7 +318,7 @@ export const apply = async ({ } // Case 3: returns base/array/composite/enum type. - const type = typesById[fn.return_type_id] + const type = typesById.get(fn.return_type_id) if (type) { return pgTypeToTsType(schema, type.name, { types, @@ -369,7 +363,7 @@ export const apply = async ({ if (conflictingFns.length > 0) { const conflictingFn = conflictingFns[0] - const returnTypeName = typesById[conflictingFn.fn.return_type_id]?.name || 'unknown' + const returnTypeName = typesById.get(conflictingFn.fn.return_type_id)?.name || 'unknown' 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` } } @@ -395,7 +389,7 @@ export const apply = async ({ }) .map((f) => { const args = f.inArgs - return `${schema.name}.${fn.name}(${args.map((a) => `${a.name || ''} => ${typesById[a.type_id]?.name || 'unknown'}`).join(', ')})` + return `${schema.name}.${fn.name}(${args.map((a) => `${a.name || ''} => ${typesById.get(a.type_id)?.name || 'unknown'}`).join(', ')})` }) .join(', ') @@ -420,7 +414,7 @@ export const apply = async ({ if (conflictError) { if (inArgs.length > 0) { const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = typesById[type_id] + const type = typesById.get(type_id) let tsType = 'unknown' if (type) { tsType = pgTypeToTsType(schema, type.name, { @@ -439,7 +433,7 @@ export const apply = async ({ // Special case for computed fields returning scalars functions if (inArgs.length > 0) { const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = typesById[type_id] + const type = typesById.get(type_id) let tsType = 'unknown' if (type) { tsType = pgTypeToTsType(schema, type.name, { @@ -456,7 +450,7 @@ export const apply = async ({ 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`)}` } else if (inArgs.length > 0) { const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = typesById[type_id] + const type = typesById.get(type_id) let tsType = 'unknown' if (type) { tsType = pgTypeToTsType(schema, type.name, { @@ -708,7 +702,7 @@ export type Database = { ({ name, attributes }) => `${JSON.stringify(name)}: { ${attributes.map(({ name, type_id }) => { - const type = typesById[type_id] + const type = typesById.get(type_id) let tsType = 'unknown' if (type) { tsType = `${pgTypeToTsType(schema, type.name, { From e73b3b9dab9642ef5249c1b02d9de85fdd7f50ea Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Sep 2025 20:17:17 +0200 Subject: [PATCH 16/26] chore: single sort for relationships --- src/server/templates/typescript.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 4266cee6..bf87d21b 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -36,6 +36,12 @@ export const apply = async ({ postgrestVersion?: string }): Promise => { schemas.sort((a, b) => a.name.localeCompare(b.name)) + relationships.sort( + (a, b) => + a.foreign_key_name.localeCompare(b.foreign_key_name) || + a.referenced_relation.localeCompare(b.referenced_relation) || + JSON.stringify(a.referenced_columns).localeCompare(JSON.stringify(b.referenced_columns)) + ) const introspectionBySchema = Object.fromEntries<{ tables: { table: Pick @@ -97,19 +103,12 @@ export const apply = async ({ GeneratorMetadata['relationships'][number], 'foreign_key_name' | 'columns' | 'is_one_to_one' | 'referenced_relation' | 'referenced_columns' >[] { - return relationships - .filter( - (relationship) => - relationship.schema === object.schema && - relationship.referenced_schema === object.schema && - relationship.relation === object.name - ) - .toSorted( - (a, b) => - a.foreign_key_name.localeCompare(b.foreign_key_name) || - a.referenced_relation.localeCompare(b.referenced_relation) || - JSON.stringify(a.referenced_columns).localeCompare(JSON.stringify(b.referenced_columns)) - ) + return relationships.filter( + (relationship) => + relationship.schema === object.schema && + relationship.referenced_schema === object.schema && + relationship.relation === object.name + ) } function generateRelationshiptTsDefinition(relationship: TsRelationship): string { From 59efa8c64d00abba15549e1681599bda82b9e140 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Sep 2025 20:26:18 +0200 Subject: [PATCH 17/26] chore: reduce loops --- src/server/templates/typescript.ts | 90 ++++++++++++++---------------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 9e9a9fef..f0079874 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -32,19 +32,12 @@ export const apply = async ({ postgrestVersion?: string }): Promise => { schemas.sort((a, b) => a.name.localeCompare(b.name)) - - const columnsByTableId = Object.fromEntries( - [...tables, ...foreignTables, ...views, ...materializedViews].map((t) => [t.id, []]) + relationships.sort( + (a, b) => + a.foreign_key_name.localeCompare(b.foreign_key_name) || + a.referenced_relation.localeCompare(b.referenced_relation) || + JSON.stringify(a.referenced_columns).localeCompare(JSON.stringify(b.referenced_columns)) ) - for (const column of columns) { - if (column.table_id in columnsByTableId) { - columnsByTableId[column.table_id].push(column) - } - } - for (const tableId in columnsByTableId) { - columnsByTableId[tableId].sort((a, b) => a.name.localeCompare(b.name)) - } - const introspectionBySchema = Object.fromEntries<{ tables: { table: Pick @@ -64,6 +57,33 @@ export const apply = async ({ ]) ) + const columnsByTableId = Object.fromEntries( + [...tables, ...foreignTables, ...views, ...materializedViews].map((t) => [t.id, []]) + ) + // group types by id for quicker lookup + const typesById = new Map() + + for (const column of columns) { + if (column.table_id in columnsByTableId) { + columnsByTableId[column.table_id].push(column) + } + } + for (const tableId in columnsByTableId) { + columnsByTableId[tableId].sort((a, b) => a.name.localeCompare(b.name)) + } + + for (const type of types) { + typesById.set(type.id, type) + if (type.schema in introspectionBySchema) { + if (type.enums.length > 0) { + introspectionBySchema[type.schema].enums.push(type) + } + if (type.attributes.length > 0) { + introspectionBySchema[type.schema].compositeTypes.push(type) + } + } + } + function getRelationships( object: { schema: string; name: string }, relationships: GeneratorMetadata['relationships'] @@ -71,19 +91,12 @@ export const apply = async ({ GeneratorMetadata['relationships'][number], 'foreign_key_name' | 'columns' | 'is_one_to_one' | 'referenced_relation' | 'referenced_columns' >[] { - return relationships - .filter( - (relationship) => - relationship.schema === object.schema && - relationship.referenced_schema === object.schema && - relationship.relation === object.name - ) - .toSorted( - (a, b) => - a.foreign_key_name.localeCompare(b.foreign_key_name) || - a.referenced_relation.localeCompare(b.referenced_relation) || - JSON.stringify(a.referenced_columns).localeCompare(JSON.stringify(b.referenced_columns)) - ) + return relationships.filter( + (relationship) => + relationship.schema === object.schema && + relationship.referenced_schema === object.schema && + relationship.relation === object.name + ) } function generateRelationshiptTsDefinition(relationship: TsRelationship): string { @@ -148,16 +161,6 @@ export const apply = async ({ } } } - for (const type of types) { - if (type.schema in introspectionBySchema) { - if (type.enums.length > 0) { - introspectionBySchema[type.schema].enums.push(type) - } - if (type.attributes.length > 0) { - introspectionBySchema[type.schema].compositeTypes.push(type) - } - } - } for (const schema in introspectionBySchema) { introspectionBySchema[schema].tables.sort((a, b) => a.table.name.localeCompare(b.table.name)) introspectionBySchema[schema].views.sort((a, b) => a.view.name.localeCompare(b.view.name)) @@ -166,15 +169,6 @@ export const apply = async ({ introspectionBySchema[schema].compositeTypes.sort((a, b) => a.name.localeCompare(b.name)) } - // group types by id for quicker lookup - const typesById = types.reduce( - (acc, type) => { - acc[type.id] = type - return acc - }, - {} as Record - ) - const getFunctionTsReturnType = (fn: PostgresFunction, returnType: string) => { return `${returnType}${fn.is_set_returning_function ? '[]' : ''}` } @@ -183,7 +177,7 @@ export const apply = async ({ const tableArgs = fn.args.filter(({ mode }) => mode === 'table') if (tableArgs.length > 0) { const argsNameAndType = tableArgs.map(({ name, type_id }) => { - const type = typesById[type_id] + const type = typesById.get(type_id) let tsType = 'unknown' if (type) { tsType = pgTypeToTsType(schema, type.name, { @@ -224,7 +218,7 @@ export const apply = async ({ } // Case 3: returns base/array/composite/enum type. - const type = typesById[fn.return_type_id] + const type = typesById.get(fn.return_type_id) if (type) { return pgTypeToTsType(schema, type.name, { types, @@ -247,7 +241,7 @@ export const apply = async ({ return 'Record' } const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = typesById[type_id] + const type = typesById.get(type_id) let tsType = 'unknown' if (type) { tsType = pgTypeToTsType(schema, type.name, { @@ -492,7 +486,7 @@ export type Database = { ({ name, attributes }) => `${JSON.stringify(name)}: { ${attributes.map(({ name, type_id }) => { - const type = typesById[type_id] + const type = typesById.get(type_id) let tsType = 'unknown' if (type) { tsType = `${pgTypeToTsType(schema, type.name, { From d1e3ad9e49a3ac0483c2b43442ae77c66bcbb2df Mon Sep 17 00:00:00 2001 From: avallete Date: Sat, 27 Sep 2025 11:56:08 +0200 Subject: [PATCH 18/26] fix: relationtype setof functions generation --- src/server/templates/typescript.ts | 31 +++++----- test/lib/types.ts | 8 +-- test/server/typegen.ts | 96 ------------------------------ 3 files changed, 20 insertions(+), 115 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index bf87d21b..98563dad 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -221,29 +221,32 @@ export const apply = async ({ ) const returnsSetOfTable = fn.is_set_returning_function && fn.return_type_relation_id !== null const returnsMultipleRows = fn.prorows !== null && fn.prorows > 1 - - // Only add SetofOptions for functions with table arguments (embedded functions) - // or specific functions that RETURNS table-name - if (fn.args.length === 1 && relationTypeByIds.get(fn.args[0].type_id)) { - // Case 1: Standard embedded function with proper setof detection - if (returnsSetOfTable && returnTableName) { - setofOptionsInfo = `SetofOptions: { - from: ${JSON.stringify(typesById.get(fn.args[0].type_id)?.format)} + if (fn.args.length === 1) { + const relationType = relationTypeByIds.get(fn.args[0].type_id) + + // Only add SetofOptions for functions with table arguments (embedded functions) + // or specific functions that RETURNS table-name + if (relationType) { + const sourceTable = relationType.format + // Case 1: Standard embedded function with proper setof detection + if (returnsSetOfTable && returnTableName) { + setofOptionsInfo = `SetofOptions: { + from: ${JSON.stringify(sourceTable)} to: ${JSON.stringify(returnTableName)} isOneToOne: ${Boolean(!returnsMultipleRows)} isSetofReturn: true }` - } - // Case 2: Handle RETURNS table-name those are always a one to one relationship - else if (returnTableName && !returnsSetOfTable) { - const sourceTable = typesById.get(fn.args[0].type_id)?.format - const targetTable = returnTableName - setofOptionsInfo = `SetofOptions: { + } + // Case 2: Handle RETURNS table-name those are always a one to one relationship + else if (returnTableName && !returnsSetOfTable) { + const targetTable = returnTableName + setofOptionsInfo = `SetofOptions: { from: ${JSON.stringify(sourceTable)} to: ${JSON.stringify(targetTable)} isOneToOne: true isSetofReturn: false }` + } } } // Case 3: Special case for functions without table arguments still returning a table diff --git a/test/lib/types.ts b/test/lib/types.ts index 1c21f38f..71560ee1 100644 --- a/test/lib/types.ts +++ b/test/lib/types.ts @@ -95,8 +95,7 @@ test('composite type attributes', async () => { const res = await pgMeta.types.list() expect(res.data?.find(({ name }) => name === 'test_composite')).toMatchInlineSnapshot( - { id: expect.any(Number) }, - ` + { id: expect.any(Number) }, ` { "attributes": [ { @@ -114,10 +113,9 @@ test('composite type attributes', async () => { "id": Any, "name": "test_composite", "schema": "public", - "type_relation_id": 16966, + "type_relation_id": 16964, } - ` - ) + `) await pgMeta.query(`drop type test_composite;`) }) diff --git a/test/server/typegen.ts b/test/server/typegen.ts index 6b8f7fc9..5b515a95 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -627,12 +627,6 @@ test('typegen: typescript', async () => { user_name: string | null user_status: Database["public"]["Enums"]["user_status"] | null } - SetofOptions: { - from: "*" - to: "user_todos_summary_view" - isOneToOne: true - isSetofReturn: true - } } get_todos_from_user: | { @@ -672,12 +666,6 @@ test('typegen: typescript', async () => { id: number "user-id": number }[] - SetofOptions: { - from: "*" - to: "todos" - isOneToOne: false - isSetofReturn: true - } } get_todos_setof_rows: | { @@ -778,12 +766,6 @@ test('typegen: typescript', async () => { name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] - SetofOptions: { - from: "*" - to: "users" - isOneToOne: false - isSetofReturn: true - } } | { Args: { completed: boolean; todo_id: number } @@ -854,12 +836,6 @@ test('typegen: typescript', async () => { id: number "user-id": number }[] - SetofOptions: { - from: "*" - to: "todos" - isOneToOne: false - isSetofReturn: true - } } | { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } @@ -1704,12 +1680,6 @@ test('typegen w/ one-to-one relationships', async () => { user_name: string | null user_status: Database["public"]["Enums"]["user_status"] | null } - SetofOptions: { - from: "*" - to: "user_todos_summary_view" - isOneToOne: true - isSetofReturn: true - } } get_todos_from_user: | { @@ -1749,12 +1719,6 @@ test('typegen w/ one-to-one relationships', async () => { id: number "user-id": number }[] - SetofOptions: { - from: "*" - to: "todos" - isOneToOne: false - isSetofReturn: true - } } get_todos_setof_rows: | { @@ -1855,12 +1819,6 @@ test('typegen w/ one-to-one relationships', async () => { name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] - SetofOptions: { - from: "*" - to: "users" - isOneToOne: false - isSetofReturn: true - } } | { Args: { completed: boolean; todo_id: number } @@ -1931,12 +1889,6 @@ test('typegen w/ one-to-one relationships', async () => { id: number "user-id": number }[] - SetofOptions: { - from: "*" - to: "todos" - isOneToOne: false - isSetofReturn: true - } } | { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } @@ -2781,12 +2733,6 @@ test('typegen: typescript w/ one-to-one relationships', async () => { user_name: string | null user_status: Database["public"]["Enums"]["user_status"] | null } - SetofOptions: { - from: "*" - to: "user_todos_summary_view" - isOneToOne: true - isSetofReturn: true - } } get_todos_from_user: | { @@ -2826,12 +2772,6 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number "user-id": number }[] - SetofOptions: { - from: "*" - to: "todos" - isOneToOne: false - isSetofReturn: true - } } get_todos_setof_rows: | { @@ -2932,12 +2872,6 @@ test('typegen: typescript w/ one-to-one relationships', async () => { name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] - SetofOptions: { - from: "*" - to: "users" - isOneToOne: false - isSetofReturn: true - } } | { Args: { completed: boolean; todo_id: number } @@ -3008,12 +2942,6 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number "user-id": number }[] - SetofOptions: { - from: "*" - to: "todos" - isOneToOne: false - isSetofReturn: true - } } | { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } @@ -3863,12 +3791,6 @@ test('typegen: typescript w/ postgrestVersion', async () => { user_name: string | null user_status: Database["public"]["Enums"]["user_status"] | null } - SetofOptions: { - from: "*" - to: "user_todos_summary_view" - isOneToOne: true - isSetofReturn: true - } } get_todos_from_user: | { @@ -3908,12 +3830,6 @@ test('typegen: typescript w/ postgrestVersion', async () => { id: number "user-id": number }[] - SetofOptions: { - from: "*" - to: "todos" - isOneToOne: false - isSetofReturn: true - } } get_todos_setof_rows: | { @@ -4014,12 +3930,6 @@ test('typegen: typescript w/ postgrestVersion', async () => { name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] - SetofOptions: { - from: "*" - to: "users" - isOneToOne: false - isSetofReturn: true - } } | { Args: { completed: boolean; todo_id: number } @@ -4090,12 +4000,6 @@ test('typegen: typescript w/ postgrestVersion', async () => { id: number "user-id": number }[] - SetofOptions: { - from: "*" - to: "todos" - isOneToOne: false - isSetofReturn: true - } } | { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } From 32e789fda97bf4e1aaf978612f0c49171f25c610 Mon Sep 17 00:00:00 2001 From: avallete Date: Sat, 27 Sep 2025 11:57:51 +0200 Subject: [PATCH 19/26] chore: fix prettier --- test/lib/types.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/lib/types.ts b/test/lib/types.ts index 71560ee1..b1d3bfe4 100644 --- a/test/lib/types.ts +++ b/test/lib/types.ts @@ -95,7 +95,8 @@ test('composite type attributes', async () => { const res = await pgMeta.types.list() expect(res.data?.find(({ name }) => name === 'test_composite')).toMatchInlineSnapshot( - { id: expect.any(Number) }, ` + { id: expect.any(Number) }, + ` { "attributes": [ { @@ -115,7 +116,8 @@ test('composite type attributes', async () => { "schema": "public", "type_relation_id": 16964, } - `) + ` + ) await pgMeta.query(`drop type test_composite;`) }) From cfb405f4b881a1c5a0915a77c4c633e5162d30dd Mon Sep 17 00:00:00 2001 From: avallete Date: Sat, 27 Sep 2025 12:04:22 +0200 Subject: [PATCH 20/26] chore: update snapshots --- test/lib/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/types.ts b/test/lib/types.ts index b1d3bfe4..1c21f38f 100644 --- a/test/lib/types.ts +++ b/test/lib/types.ts @@ -114,7 +114,7 @@ test('composite type attributes', async () => { "id": Any, "name": "test_composite", "schema": "public", - "type_relation_id": 16964, + "type_relation_id": 16966, } ` ) From 8824a5aa854354dab4fb6950f257ad495badf358 Mon Sep 17 00:00:00 2001 From: avallete Date: Sat, 27 Sep 2025 12:59:12 +0200 Subject: [PATCH 21/26] chore: fix types test --- test/lib/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/types.ts b/test/lib/types.ts index 1c21f38f..4956c116 100644 --- a/test/lib/types.ts +++ b/test/lib/types.ts @@ -114,7 +114,7 @@ test('composite type attributes', async () => { "id": Any, "name": "test_composite", "schema": "public", - "type_relation_id": 16966, + "type_relation_id": Any, } ` ) From f8321c67c5944123397537a0a3956101e6ec0e3a Mon Sep 17 00:00:00 2001 From: avallete Date: Sat, 27 Sep 2025 13:09:39 +0200 Subject: [PATCH 22/26] fix: test types --- test/lib/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/types.ts b/test/lib/types.ts index 4956c116..349a1b80 100644 --- a/test/lib/types.ts +++ b/test/lib/types.ts @@ -95,7 +95,7 @@ test('composite type attributes', async () => { const res = await pgMeta.types.list() expect(res.data?.find(({ name }) => name === 'test_composite')).toMatchInlineSnapshot( - { id: expect.any(Number) }, + { id: expect.any(Number), type_relation_id: expect.any(Number) }, ` { "attributes": [ From 0262ebc9fd3914f7fdc2627f0b7c6e37e14f5e8e Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 29 Sep 2025 11:45:15 +0200 Subject: [PATCH 23/26] fix: include materializedView types --- src/lib/sql/types.sql.ts | 2 +- test/db/00-init.sql | 7 ++++ test/lib/functions.ts | 4 +-- test/server/typegen.ts | 76 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/lib/sql/types.sql.ts b/src/lib/sql/types.sql.ts index 3d2744b0..c230f23f 100644 --- a/src/lib/sql/types.sql.ts +++ b/src/lib/sql/types.sql.ts @@ -47,7 +47,7 @@ from t.typrelid = 0 or ( select - c.relkind ${props.includeTableTypes ? `in ('c', 'r', 'v')` : `= 'c'`} + c.relkind ${props.includeTableTypes ? `in ('c', 'r', 'v', 'm')` : `= 'c'`} from pg_class c where diff --git a/test/db/00-init.sql b/test/db/00-init.sql index ae5b8200..67fc2d91 100644 --- a/test/db/00-init.sql +++ b/test/db/00-init.sql @@ -203,6 +203,13 @@ AS $$ SELECT * FROM public.users_audit WHERE user_id = user_row.id; $$; +CREATE OR REPLACE FUNCTION public.get_todos_by_matview(todos_matview) +RETURNS SETOF todos ROWS 1 +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.todos LIMIT 1; +$$; + CREATE OR REPLACE FUNCTION public.get_todos_setof_rows(user_row users) RETURNS SETOF todos LANGUAGE SQL STABLE diff --git a/test/lib/functions.ts b/test/lib/functions.ts index 5c08e146..ef540b4b 100644 --- a/test/lib/functions.ts +++ b/test/lib/functions.ts @@ -118,7 +118,7 @@ test('list set-returning function with multiples definitions', async () => { "definition": " SELECT * FROM public.todos WHERE "user-id" = user_row.id; ", - "id": 16507, + "id": 16508, "identity_argument_types": "user_row users", "is_set_returning_function": true, "language": "sql", @@ -153,7 +153,7 @@ test('list set-returning function with multiples definitions', async () => { "definition": " SELECT * FROM public.todos WHERE "user-id" = todo_row."user-id"; ", - "id": 16508, + "id": 16509, "identity_argument_types": "todo_row todos", "is_set_returning_function": true, "language": "sql", diff --git a/test/server/typegen.ts b/test/server/typegen.ts index 5b515a95..5eb617de 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -306,6 +306,11 @@ test('typegen: typescript', async () => { details: string | null id: number | null "user-id": number | null + get_todos_by_matview: { + details: string | null + id: number + "user-id": number + } | null } Relationships: [ { @@ -628,6 +633,20 @@ test('typegen: typescript', async () => { user_status: Database["public"]["Enums"]["user_status"] | null } } + get_todos_by_matview: { + Args: { "": unknown } + Returns: { + details: string | null + id: number + "user-id": number + } + SetofOptions: { + from: "todos_matview" + to: "todos" + isOneToOne: true + isSetofReturn: true + } + } get_todos_from_user: | { Args: { @@ -1347,6 +1366,11 @@ test('typegen w/ one-to-one relationships', async () => { details: string | null id: number | null "user-id": number | null + get_todos_by_matview: { + details: string | null + id: number + "user-id": number + } | null } Relationships: [ { @@ -1681,6 +1705,20 @@ test('typegen w/ one-to-one relationships', async () => { user_status: Database["public"]["Enums"]["user_status"] | null } } + get_todos_by_matview: { + Args: { "": unknown } + Returns: { + details: string | null + id: number + "user-id": number + } + SetofOptions: { + from: "todos_matview" + to: "todos" + isOneToOne: true + isSetofReturn: true + } + } get_todos_from_user: | { Args: { @@ -2400,6 +2438,11 @@ test('typegen: typescript w/ one-to-one relationships', async () => { details: string | null id: number | null "user-id": number | null + get_todos_by_matview: { + details: string | null + id: number + "user-id": number + } | null } Relationships: [ { @@ -2734,6 +2777,20 @@ test('typegen: typescript w/ one-to-one relationships', async () => { user_status: Database["public"]["Enums"]["user_status"] | null } } + get_todos_by_matview: { + Args: { "": unknown } + Returns: { + details: string | null + id: number + "user-id": number + } + SetofOptions: { + from: "todos_matview" + to: "todos" + isOneToOne: true + isSetofReturn: true + } + } get_todos_from_user: | { Args: { @@ -3458,6 +3515,11 @@ test('typegen: typescript w/ postgrestVersion', async () => { details: string | null id: number | null "user-id": number | null + get_todos_by_matview: { + details: string | null + id: number + "user-id": number + } | null } Relationships: [ { @@ -3792,6 +3854,20 @@ test('typegen: typescript w/ postgrestVersion', async () => { user_status: Database["public"]["Enums"]["user_status"] | null } } + get_todos_by_matview: { + Args: { "": unknown } + Returns: { + details: string | null + id: number + "user-id": number + } + SetofOptions: { + from: "todos_matview" + to: "todos" + isOneToOne: true + isSetofReturn: true + } + } get_todos_from_user: | { Args: { From bc81acd9967f496adf9e7d1334ada95e8428f8d4 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 30 Sep 2025 14:01:58 +0200 Subject: [PATCH 24/26] test: add search_todos_by_details function --- test/db/00-init.sql | 7 +++++++ test/lib/functions.ts | 4 ++-- test/server/typegen.ts | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/test/db/00-init.sql b/test/db/00-init.sql index 67fc2d91..64107713 100644 --- a/test/db/00-init.sql +++ b/test/db/00-init.sql @@ -210,6 +210,13 @@ AS $$ SELECT * FROM public.todos LIMIT 1; $$; +CREATE OR REPLACE FUNCTION public.search_todos_by_details(search_details text) +RETURNS SETOF todos +LANGUAGE SQL STABLE +AS $$ + SELECT * FROM public.todos WHERE details ilike search_details; +$$; + CREATE OR REPLACE FUNCTION public.get_todos_setof_rows(user_row users) RETURNS SETOF todos LANGUAGE SQL STABLE diff --git a/test/lib/functions.ts b/test/lib/functions.ts index ef540b4b..9d6088b6 100644 --- a/test/lib/functions.ts +++ b/test/lib/functions.ts @@ -118,7 +118,7 @@ test('list set-returning function with multiples definitions', async () => { "definition": " SELECT * FROM public.todos WHERE "user-id" = user_row.id; ", - "id": 16508, + "id": 16509, "identity_argument_types": "user_row users", "is_set_returning_function": true, "language": "sql", @@ -153,7 +153,7 @@ test('list set-returning function with multiples definitions', async () => { "definition": " SELECT * FROM public.todos WHERE "user-id" = todo_row."user-id"; ", - "id": 16509, + "id": 16510, "identity_argument_types": "todo_row todos", "is_set_returning_function": true, "language": "sql", diff --git a/test/server/typegen.ts b/test/server/typegen.ts index 5eb617de..a695e62a 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -830,6 +830,14 @@ test('typegen: typescript', async () => { } & "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" } | { Args: never; Returns: undefined } + search_todos_by_details: { + Args: { search_details: string } + Returns: { + details: string | null + id: number + "user-id": number + }[] + } test_internal_query: { Args: never; Returns: undefined } test_unnamed_row_composite: { Args: { "": Database["public"]["Tables"]["users"]["Row"] } @@ -1902,6 +1910,14 @@ test('typegen w/ one-to-one relationships', async () => { } & "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" } | { Args: never; Returns: undefined } + search_todos_by_details: { + Args: { search_details: string } + Returns: { + details: string | null + id: number + "user-id": number + }[] + } test_internal_query: { Args: never; Returns: undefined } test_unnamed_row_composite: { Args: { "": Database["public"]["Tables"]["users"]["Row"] } @@ -2974,6 +2990,14 @@ test('typegen: typescript w/ one-to-one relationships', async () => { } & "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" } | { Args: never; Returns: undefined } + search_todos_by_details: { + Args: { search_details: string } + Returns: { + details: string | null + id: number + "user-id": number + }[] + } test_internal_query: { Args: never; Returns: undefined } test_unnamed_row_composite: { Args: { "": Database["public"]["Tables"]["users"]["Row"] } @@ -4051,6 +4075,14 @@ test('typegen: typescript w/ postgrestVersion', async () => { } & "Could not choose the best candidate function between: public.postgrest_unresolvable_function(a => int4), public.postgrest_unresolvable_function(a => text). Try renaming the parameters or the function itself in the database so function overloading can be resolved" } | { Args: never; Returns: undefined } + search_todos_by_details: { + Args: { search_details: string } + Returns: { + details: string | null + id: number + "user-id": number + }[] + } test_internal_query: { Args: never; Returns: undefined } test_unnamed_row_composite: { Args: { "": Database["public"]["Tables"]["users"]["Row"] } From 40c06b8e011f91c1ae4d2f3d65f2a7c07ad78af8 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 30 Sep 2025 14:13:58 +0200 Subject: [PATCH 25/26] fix: add setof from * for all relation functions --- src/server/templates/typescript.ts | 22 +++--- test/server/typegen.ts | 120 +++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 10 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 98563dad..0c91976d 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -221,6 +221,18 @@ export const apply = async ({ ) const returnsSetOfTable = fn.is_set_returning_function && fn.return_type_relation_id !== null const returnsMultipleRows = fn.prorows !== null && fn.prorows > 1 + // Case 1: if the function returns a table, we need to add SetofOptions to allow selecting sub fields of the table + // Those can be used in rpc to select sub fields of a table + if (returnTableName) { + setofOptionsInfo = `SetofOptions: { + from: "*" + to: ${JSON.stringify(returnTableName)} + isOneToOne: ${Boolean(!returnsMultipleRows)} + isSetofReturn: ${fn.is_set_returning_function} + }` + } + // Case 2: if the function has a single table argument, we need to add SetofOptions to allow selecting sub fields of the table + // and set the right "from" and "to" values to allow selecting from a table row if (fn.args.length === 1) { const relationType = relationTypeByIds.get(fn.args[0].type_id) @@ -249,16 +261,6 @@ export const apply = async ({ } } } - // Case 3: Special case for functions without table arguments still returning a table - // Those can be used in rpc to select sub fields of a table - else if (returnTableName) { - setofOptionsInfo = `SetofOptions: { - from: "*" - to: ${JSON.stringify(returnTableName)} - isOneToOne: ${Boolean(!returnsMultipleRows)} - isSetofReturn: ${fn.is_set_returning_function} - }` - } return `${returnType}${fn.is_set_returning_function && returnsMultipleRows ? '[]' : ''} ${setofOptionsInfo ? `${setofOptionsInfo}` : ''}` diff --git a/test/server/typegen.ts b/test/server/typegen.ts index a695e62a..f101adde 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -632,6 +632,12 @@ test('typegen: typescript', async () => { user_name: string | null user_status: Database["public"]["Enums"]["user_status"] | null } + SetofOptions: { + from: "*" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } } get_todos_by_matview: { Args: { "": unknown } @@ -685,6 +691,12 @@ test('typegen: typescript', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } get_todos_setof_rows: | { @@ -785,6 +797,12 @@ test('typegen: typescript', async () => { name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + isSetofReturn: true + } } | { Args: { completed: boolean; todo_id: number } @@ -837,6 +855,12 @@ test('typegen: typescript', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } test_internal_query: { Args: never; Returns: undefined } test_unnamed_row_composite: { @@ -863,6 +887,12 @@ test('typegen: typescript', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } | { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } @@ -1712,6 +1742,12 @@ test('typegen w/ one-to-one relationships', async () => { user_name: string | null user_status: Database["public"]["Enums"]["user_status"] | null } + SetofOptions: { + from: "*" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } } get_todos_by_matview: { Args: { "": unknown } @@ -1765,6 +1801,12 @@ test('typegen w/ one-to-one relationships', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } get_todos_setof_rows: | { @@ -1865,6 +1907,12 @@ test('typegen w/ one-to-one relationships', async () => { name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + isSetofReturn: true + } } | { Args: { completed: boolean; todo_id: number } @@ -1917,6 +1965,12 @@ test('typegen w/ one-to-one relationships', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } test_internal_query: { Args: never; Returns: undefined } test_unnamed_row_composite: { @@ -1943,6 +1997,12 @@ test('typegen w/ one-to-one relationships', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } | { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } @@ -2792,6 +2852,12 @@ test('typegen: typescript w/ one-to-one relationships', async () => { user_name: string | null user_status: Database["public"]["Enums"]["user_status"] | null } + SetofOptions: { + from: "*" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } } get_todos_by_matview: { Args: { "": unknown } @@ -2845,6 +2911,12 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } get_todos_setof_rows: | { @@ -2945,6 +3017,12 @@ test('typegen: typescript w/ one-to-one relationships', async () => { name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + isSetofReturn: true + } } | { Args: { completed: boolean; todo_id: number } @@ -2997,6 +3075,12 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } test_internal_query: { Args: never; Returns: undefined } test_unnamed_row_composite: { @@ -3023,6 +3107,12 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } | { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } @@ -3877,6 +3967,12 @@ test('typegen: typescript w/ postgrestVersion', async () => { user_name: string | null user_status: Database["public"]["Enums"]["user_status"] | null } + SetofOptions: { + from: "*" + to: "user_todos_summary_view" + isOneToOne: true + isSetofReturn: true + } } get_todos_by_matview: { Args: { "": unknown } @@ -3930,6 +4026,12 @@ test('typegen: typescript w/ postgrestVersion', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } get_todos_setof_rows: | { @@ -4030,6 +4132,12 @@ test('typegen: typescript w/ postgrestVersion', async () => { name: string | null status: Database["public"]["Enums"]["user_status"] | null }[] + SetofOptions: { + from: "*" + to: "users" + isOneToOne: false + isSetofReturn: true + } } | { Args: { completed: boolean; todo_id: number } @@ -4082,6 +4190,12 @@ test('typegen: typescript w/ postgrestVersion', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } test_internal_query: { Args: never; Returns: undefined } test_unnamed_row_composite: { @@ -4108,6 +4222,12 @@ test('typegen: typescript w/ postgrestVersion', async () => { id: number "user-id": number }[] + SetofOptions: { + from: "*" + to: "todos" + isOneToOne: false + isSetofReturn: true + } } | { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } From 8b65b3904cbc41cd26ea88876ff77d239c70219e Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Tue, 7 Oct 2025 21:20:24 +0200 Subject: [PATCH 26/26] fix(typescript): union unknown null (#995) * fix(typescript): unknown is already nullable Fixes: https://github.com/supabase/cli/issues/4234 https://github.com/supabase/cli/issues/577 * fix: also exclude any from null union --- src/server/templates/typescript.ts | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 0c91976d..1b527686 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -482,6 +482,14 @@ export const apply = async ({ }` : '' + function generateNullableUnionTsType(tsType: string, isNullable: boolean) { + // Only add the null union if the type is not unknown as unknown already includes null + if (tsType === 'unknown' || tsType === 'any' || !isNullable) { + return tsType + } + return `${tsType} | null` + } + function generateColumnTsDefinition( schema: PostgresSchema, column: { @@ -497,7 +505,7 @@ export const apply = async ({ views: PostgresView[] } ) { - return `${JSON.stringify(column.name)}${column.is_optional ? '?' : ''}: ${pgTypeToTsType(schema, column.format, context)} ${column.is_nullable ? '| null' : ''}` + return `${JSON.stringify(column.name)}${column.is_optional ? '?' : ''}: ${generateNullableUnionTsType(pgTypeToTsType(schema, column.format, context), column.is_nullable)}` } let output = ` @@ -537,7 +545,7 @@ export type Database = { ...schemaFunctions .filter(({ fn }) => fn.argument_types === table.name) .map(({ fn }) => { - return `${JSON.stringify(fn.name)}: ${getFunctionReturnType(schema, fn)} | null` + return `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}` }), ]} } @@ -610,7 +618,7 @@ export type Database = { .filter(({ fn }) => fn.argument_types === view.name) .map( ({ fn }) => - `${JSON.stringify(fn.name)}: ${getFunctionReturnType(schema, fn)} | null` + `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}` ), ]} } @@ -709,12 +717,15 @@ export type Database = { const type = typesById.get(type_id) let tsType = 'unknown' if (type) { - tsType = `${pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - })} | null` + tsType = `${generateNullableUnionTsType( + pgTypeToTsType(schema, type.name, { + types, + schemas, + tables, + views, + }), + true + )}` } return `${JSON.stringify(name)}: ${tsType}` })}