Skip to content

[Pg] Add PostGreSQL function support - #6467

Draft
FirstTimeGaming wants to merge 32 commits into
drizzle-team:betafrom
FirstTimeGaming:pg-functions-v1
Draft

FirstTimeGaming wants to merge 32 commits into
drizzle-team:betafrom
FirstTimeGaming:pg-functions-v1

Conversation

@FirstTimeGaming

Copy link
Copy Markdown

Note: This is a draft to get feedback, full drizzle-kit implementation is not finished yet, future implementation will support full migration compatibility

Description

Adds first-class PostgreSQL function support to Drizzle ORM

Functions can be declared using pgFunction() or through a PostgreSQL schema using pgSchema(...).function().

ORM

  • Adds PgFunction and PgFunctionBuilder
  • Adds typed function arguments and return types
  • Supports scalar, table, SETOF, void, record, and trigger returns
  • Supports language, volatility, security, null-input, parallel, search path, and configuration options
  • Adds typed function-body callbacks
  • Adds schema-aware functions
  • Adds argument, return, and schema inference helpers

Example:

const getUser = pgFunction('get_user', {
	args: {
		id: uuid(),
	},
	returns: users,
}).as((f) => sql`
	SELECT *
	FROM ${users}
	WHERE ${users.id} = ${f.args.id}
`);

Tests

  • Adds type tests for argument, return, and schema inference
  • Adds runtime tests for function builders and argument references

Scope

This PR introduces the ORM PostgreSQL function api intended for code-first schema generation through Drizzle Kit.

Included:

  • drizzle-orm declaration API
  • Schema-aware functions
  • Function arguments and return types
  • Function configuration and body declaration

Not included:

  • Triggers
    • This would be a relatively simple addition, only needing a PgTable, PgFunction, and behavior constructor
  • Procedures
  • OUT, INOUT, or VARIADIC arguments
    • This can be added later if there is demand for it; it adds some complexity on the generation side outside of the default mode for function arguments

Scaffolds PgFunction and builds out the helper classes such as PgFunctionBuilder and pgFunction
Add function type generics so we pass through specific types instead of widened types
Add infer helperrs for pg funtion returns and args
Add setof, tableReturn, void, record, trigger, helpers, udate type inferencing to be more explicit
Add type testing for all basic and custom cases for function arguments and return types
Update to beta stream, use AnyPgColumnBuilder, ResolvePgColumnConfig, InferSelectModel over previous alpha versions
Add functions to index.ts, clean up unused variables and typos
Add callback function inside the builder to allow users to reference the defined function parameters, used exclusively for arguments for now
Add schema support to pgFunctions and add PgSchema.function() to mimic table style declarations
Mark pgFunctionWithSchema helper as internal with reference to public functions
Move function imports to the barrel import for functions add in a39b057
Explicitly omit parenthesis in PgFunctionArgument
Add runutime tests to confirm:
* configuration
* as(body) parsing
* argument references
@rutmus

rutmus commented Oct 11, 2026

Copy link
Copy Markdown

Thanks for picking this up! We run Postgres functions in production on Drizzle today through a userland workaround: TS definitions that a script turns into generate --custom migrations. We'd love to see first-class support land.

I wrote up an RFC that pulls the existing work together (this PR, #4753, #4844, #5373 and the v1 prototype from #5891) into one design for the v1 kit. It covers overload-aware identity, which changes need OR REPLACE and which need drop + create, ordering, and pull/introspection: #6477

Happy to contribute test cases from our production use, or help with the drizzle-kit side on top of this PR, whichever is most useful to the maintainers.

@FirstTimeGaming

Copy link
Copy Markdown
Author

This information is amazing.

My design choices align with the document you sent, and I'll be sure to follow that so that contributors stay aligned.

My biggest issue right now is the edge cases I may not be thinking about. If there's any specific issues you have ran into regarding functions and trigger definition behavior as DDL that would be great information when writing test cases for the kit side.

If you have any more questions feel free to comment on this PR or send me an email

Add fromExports entry for functions, define DDL schema for functions
Normalize config values from string, giving api a small subset of types casted to strings
Map orm PgFunction state to serializable kit Function state
Remove unnecessary whitespace in `functionFrom`
Sort configuration so that we keep a consistent ordering to make diffs consistent even when users might change order
Use inline params in body so that we don't get parameterized values since we handle that conversion in kit
Create a consistent helper function for the SQL name of a function arg
Use argument name helper in kit from orm so naming stays consistent across changes
Get proper sql types through helpers so kit doesn't lose information on conversion
Use function table helper to get fully qualified escaped function name
Unwrap column to capture schema for an enum before returning it to builder
@rutmus

rutmus commented Oct 13, 2026

Copy link
Copy Markdown

Thanks, great to hear the designs line up! Here are the edge cases that shaped our setup, roughly in the order I'd turn them into kit tests:

  1. Overload identity. Changing an argument type doesn't replace a function; Postgres creates a new overload and the old one keeps existing. We key everything on the normalized signature ('schema.name(argtypes)'::regprocedure), and duplicate signatures in the schema should be rejected up front. Test: change f(text) to f(varchar) and assert the old overload is dropped (or a rename hint is shown), not left behind.

  2. What CREATE OR REPLACE can't do. Return type changes, input parameter renames, and removing a parameter default are rejected by OR REPLACE and need DROP + CREATE, which fails if something depends on the function and loses grants. Our userland generator only does create/replace, so these hit us as manual migrations. Worth a test per case.

  3. Attribute drift, not just body drift. We assert installed pg_proc attributes match the declaration: provolatile, proparallel, prosecdef, proisstrict, and language. A function can keep an identical body while its volatility or PARALLEL setting differs, and that matters for indexes, generated columns, and query plans. The kit should diff each attribute and emit OR REPLACE for it.

  4. Idempotency. Rerunning migrations must leave pg_get_functiondef(...) byte-identical for every function, and a second generate with no schema change must produce nothing. That's our most valuable regression test.

  5. Bodies with nested dollar quotes. We use $function$ … $function$ because bodies can contain $$. Body comparison and codegen for pull should keep the tag and the exact text, since prosrc is stored verbatim.

  6. Multiple functions in one migration. Generating several functions together, then changing only one, should emit only the changed one, with statement breakpoints between them.

  7. Adopting existing functions. Older trigger functions in our project were written by hand in earlier migrations, outside any registry. On adoption, pull and the first generate should recognize them as existing, without re-creating or dropping them. The same applies to extension-owned functions, which should be ignored entirely.

  8. Dependent objects on change. Generated columns, expression indexes, and views that depend on an expression are fragile when the kit regenerates them. In a related migration we had to replace Drizzle's DROP/ADD with Postgres 17 ALTER COLUMN … SET EXPRESSION to keep dependent views and indexes. Functions referenced from those places need the same care around ordering and drops.

Happy to answer more here, or review the kit side when it's up.

Serialize function arg defaults from AnyPgColumnBuilder
@FirstTimeGaming

FirstTimeGaming commented Oct 13, 2026 •

Copy link
Copy Markdown
Author

Hello,

I have two design questions that weren't answered by the RFC

1. Argument naming and prefixing

My design prefixes arguments with a_ as to avoid name collision.
Should kit and orm

  1. Enforce that functions own this namespace by preventing users from creating column names, tables, or other objects manually with the a_ prefix
  2. Use something closer to foreign keys where they garble the name or another similar naming convention that's less likely to collide,
  3. Or allow users to use the a_ namespace, with a warning on migration generation or similar that this may cause collisions in functions

2. Declare conventions

PostGreSQL requires you to define variables you're going to use in a DECLARE block before the BEGIN ... END section.
Example

CREATE OR REPLACE FUNCTION public.get_active_users()
RETURNS uuid[]
LANGUAGE plpgsql
SECURITY INVOKER
AS $function$
DECLARE
  users_list uuid[];
  active_cutoff timestamptz := NOW() - INTERVAL '365 days';
BEGIN
  SELECT COALESCE(ARRAY_AGG(user_id), ARRAY[]::uuid[])
  INTO users_list
  FROM public.users
  WHERE last_activity > active_cutoff;

  RETURN users_list;
END;
$function$;

In the current API users would have to define this declare block manually in the as section

Example

const getActiveUsers = pgFunction('getActiveUsers', {
	args: {},
	returns: uuid().array(),
}).as(sql`
DECLARE
  users_list uuid[];
  active_cutoff timestamptz := NOW() - INTERVAL '365 days';
BEGIN
  SELECT COALESCE(ARRAY_AGG(${users.userId}), ARRAY[]::uuid[])
  INTO users_list
  FROM ${users}
  WHERE ${users.lastActivity} > active_cutoff;

  RETURN users_list;
END;
`);

But if we allowed users to define their variables in the declare block we could do two things

  1. Simplify the as block for the user
  2. Give type hints in the as block

Example

const getActiveUsers = pgFunction('getActiveUsers', {
	args: {},
	returns: uuid().array(),
	declare: {
		usersList: uuid().array(),
		activeCutoff: date().default(sql`NOW() - INTERVAL '365 days'`)
	}

}).as((f) => {
return sql`
	SELECT COALESCE(ARRAY_AGG(${users.userId}), ARRAY[]::uuid[])
	INTO ${f.declare.userList}
	FROM ${users}
	WHERE ${users.lastActivity} > ${f.declare.activeCutoff};

	RETURN ${f.declare.userList}${f.declare.userList};
`
});

Now this would come with the same issue as 1. where we may need to restrict this namespace with a prefix such as v_ but I think this provides the user a much nicer experience without having to worry about language boilerplate that we are supposed to handle kit side with the language config value.

Please let me know if you have any questions.

@rutmus

rutmus commented Oct 15, 2026

Copy link
Copy Markdown

Thanks! On 1, I'd skip the a_ prefix and definitely not reserve it on tables/columns. It leaks into the public signature (named-arg calls, introspection) and makes pull awkward for existing functions without it. Postgres already avoids collisions if refs are qualified with the function name (my_fn.arg, or $n in SQL functions), so the typed refs in .as() could render that way and keep argument names exactly as declared.

On 2, a typed declare sounds good to me, as long as raw bodies stay a full escape hatch.

Convert functions from InterimSchema to DDL using arg overloads and marking duplicate signatures
Override the diff checker `getCompositeKey` when `entityType === 'functions'` so we can properly compare without losing overloads
Discover functions in diff, convert to json, push to json in order
Add support for `RETURNS NULL ON NULL INPUT` null input type
Add declare handler, tests, and kit discovery
Add basic function to sql convertors, ignoring arg and return type cases on funtion alterations
Allow arg and return type changes by dropping the function before creating it
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants