Skip to content

feat(graphile-function-source): create/updateFunctionFromSource — write functions from SQL text - #1918

Merged
pyramation merged 4 commits into
mainfrom
feat/create-function-from-source
Oct 19, 2026
Merged

pyramation merged 4 commits into
mainfrom
feat/create-function-from-source

Conversation

@pyramation

@pyramation pyramation commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Lets a client create and replace a function from CREATE FUNCTION text through GraphQL, without pre-parsing it into body_ast. The server parses the text and writes the same row the generated createFunction / updateFunction would. The database decides whether a definition is allowed; this plugin only parses and writes.

It ships as a new standalone package, graphile-function-source (graphile/graphile-function-source, laid out like graphile-upload-plugin). It owns parseFunctionSource, FunctionFromSourcePlugin/FunctionFromSourcePreset, the error codes and the FunctionDefinition type, and depends on plpgsql-parser. graphile-settings depends on it (workspace:^), and ConstructivePreset extends FunctionFromSourcePreset the same way it extends UploadPreset. graphile-settings doesn't re-export it and doesn't depend on plpgsql-parser. The package's tests run in the pg-graphql CI job.

createFunctionFromSource(input: { databaseId, schemaId, source, apiExposed = false, clientMutationId })
  : CreateFunctionPayload   # same insert + payload as createFunction; added when the table is insertable
updateFunctionFromSource(input: { id, source, clientMutationId })
  : UpdateFunctionPayload   # same update + payload as updateFunction; added when the table is updatable by id
# delete needs no source: the generated deleteFunction(input: { id })
  • One text format for both: a single plain CREATE FUNCTION statement. CREATE OR REPLACE is refused by both with FUNCTION_SOURCE_UNSUPPORTED. The message points to updateFunctionFromSource with the function's id, because replacing is chosen by the mutation, not by the text.
  • Parse in the server (parseFunctionSource, plpgsql-parser@^18.5.14). The statement maps onto the existing metaschema_public.function columns: name, kind (plpgsql / sql / trigger when RETURNS trigger), arguments [{name,type:FieldType,mode}], returns ({type,setof} or {table}), volatility, is_strict, security_invoker, body_ast.
    • PL/pgSQL: body_ast is the hydrated PLpgSQL_function, datums intact.
    • SQL: body_ast is the parsed body ({stmts}).
  • Same writes as the generated mutations: pgInsertSingle(resource, {database_id, schema_id, api_exposed, ...definition}) and pgUpdateSingle(resource, { id }, definition), sharing one definitionColumns helper, on the request's own pg client. Update writes only the 8 definition columns; database_id, schema_id and api_exposed are not touched. There's no role switch and no hand-written SQL.
  • Errors are never dropped:
    • Parse failures come back as a GraphQL error with extensions: { code: 'FUNCTION_SOURCE_PARSE_ERROR', position }. position is the parser's 0-based offset into the submitted source; for SQL-body errors it is mapped back to the source.
    • Anything the columns can't carry is refused with FUNCTION_SOURCE_UNSUPPORTED instead of being silently dropped. That covers qualified names, OR REPLACE, argument defaults, COST/SET/…, other languages, and multiple statements.
    • SECURITY DEFINER is passed through as security_invoker = false, so the database can refuse it.
  • Limitation: libpg_query gives no offset for PL/pgSQL body syntax errors. Those come back with the parser's message (e.g. syntax error at or near "RETRN") but no position.

Tests

These cover the plugin's mechanics only. Authorization belongs to the database and is tested with the database.

  • __tests__/function-from-source.test.ts: mapping for plpgsql / sql / trigger / RETURNS TABLE / typmods / variadic, parse-error positions, and the unsupported refusals (OR REPLACE included; one parser serves both mutations).
  • __tests__/function-from-source.integration.test.ts (graphile-test + FunctionFromSourcePreset over metaschema_public, with one database and schema seeded):
    • create writes the mapped columns, datums intact;
    • a create parse error returns the position and writes nothing;
    • update writes only the definition columns (rename + new signature + new language), keeping id, database, schema and exposure;
    • update refuses OR REPLACE and changes nothing;
    • an update parse error changes nothing.

Before / after

Current tests against the plugin before updateFunctionFromSource (0a33ca0):

PASS __tests__/function-from-source.test.ts
FAIL __tests__/function-from-source.integration.test.ts
  ● updateFunctionFromSource › writes only the definition columns, keeping database, schema and exposure
  ● updateFunctionFromSource › refuses OR REPLACE, the same text format as create, and changes nothing
  ● updateFunctionFromSource › returns a parse error and changes nothing
Tests:       3 failed, 16 passed, 19 total

After:

PASS __tests__/function-from-source.test.ts
PASS __tests__/function-from-source.integration.test.ts
Tests:       19 passed, 19 total

The create tests were checked the same way when they were added: with the plugin removed they failed, and with it they passed.

Publish

graphile-function-source (new), then graphile-settings and whatever lerna bumps with it (constructive-graphql-test, @constructive-io/graphql-test, …).

Link to Devin session: https://app.devin.ai/sessions/b8a3bb3787ae4afb991d6498d1c51f7d
Open in Devin Desktop: https://app.devin.ai/desktop/session/b8a3bb3787ae4afb991d6498d1c51f7d?variant=devin
Requested by: @pyramation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@tenki-reviewer

tenki-reviewer Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Review complete. No blocking issues — approved ✅; 1 nitpick below.

🧹 Nitpicks (1) — 🟢 1 low
  • 🟢 RLS policy errors when jwt.claims.database_id GUC is missing (function-from-source-seed.sql:16) — The policy expression current_setting('jwt.claims.database_id')::uuid omits missing_ok, so any query touching metaschema_public.function as authenticated without the GUC set raises unrecognized configuration parameter instead of filtering rows (graphile/graphile-settings/sql/function-from-source-seed.sql:16-19).

This PR introduces a new graphile-settings plugin that exposes PostgreSQL function mutations built directly from SQL source text, including libpg_query-based parsing, body extraction, and error reporting.

Files Change
graphile/graphile-settings/src/plugins/function-from-source.ts, src/plugins/index.ts New plugin that parses CREATE FUNCTION sources into AST-backed definitions and wires them into the mutation layer.
graphile/graphile-settings/src/presets/constructive-preset.ts Includes the new plugin/preset unconditionally in the Constructive preset.
graphile/graphile-settings/sql/function-from-source-seed.sql Seed schema for metaschema_public.function with an RLS policy keyed on a JWT claim.
graphile/graphile-settings/__tests__/*.test.ts, package.json Unit and integration tests plus test-script wiring for the new plugin.

Reviewed commit: 690aa0e

@devin-ai-integration devin-ai-integration Bot changed the title feat(graphile-settings): createFunctionFromSource — author customer functions from SQL text feat(graphile-function-source): create/updateFunctionFromSource — author and replace customer functions from SQL text Oct 19, 2026
@devin-ai-integration devin-ai-integration Bot changed the title feat(graphile-function-source): create/updateFunctionFromSource — author and replace customer functions from SQL text feat(graphile-function-source): create/updateFunctionFromSource — write functions from SQL text Oct 19, 2026
@pyramation
pyramation merged commit ce483f2 into main Oct 19, 2026
19 checks passed
@pyramation
pyramation deleted the feat/create-function-from-source branch October 19, 2026 05:45
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.

1 participant