Skip to content

fix: generate Insert/Update types for views with INSTEAD OF triggers#1062

Open
nancysangani wants to merge 2 commits intosupabase:masterfrom
nancysangani:fix/view-insert-update-types-with-instead-of-triggers
Open

fix: generate Insert/Update types for views with INSTEAD OF triggers#1062
nancysangani wants to merge 2 commits intosupabase:masterfrom
nancysangani:fix/view-insert-update-types-with-instead-of-triggers

Conversation

@nancysangani
Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Bug fix

What is the current behavior?

Views that use INSTEAD OF INSERT OR UPDATE triggers do not get Insert/Update type definitions generated by supabase gen types. Only simple auto-updatable views produce these types.

Fixes #850

What is the new behavior?

Views with INSTEAD OF INSERT or INSTEAD OF UPDATE triggers now correctly get Insert and Update type definitions generated. The two are handled independently, so a view with only an INSERT trigger gets only an Insert type, and vice versa.

Additional context

Root cause: the generator was gating Insert/Update emission solely on pg_relation_is_updatable(), which returns false for any complex view (joins, expressions) regardless of whether INSTEAD OF triggers make it writable.

Fix: extended the views SQL query to also check pg_trigger for INSTEAD OF triggers, exposing is_insert_enabled and is_update_enabled fields separately from is_updatable.

@nancysangani nancysangani requested review from a team, avallete and soedirgo as code owners April 11, 2026 13:26
Copilot AI review requested due to automatic review settings April 11, 2026 13:26
@nancysangani
Copy link
Copy Markdown
Contributor Author

/cc @avallete @soedirgo

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes supabase gen types view writability detection by extending view introspection to account for INSTEAD OF INSERT / INSTEAD OF UPDATE triggers, so Insert/Update type blocks can be generated for writable (trigger-backed) views, not just auto-updatable ones.

Changes:

  • Extend the views introspection SQL to expose is_insert_enabled and is_update_enabled separately from is_updatable.
  • Update the TypeScript type template to gate Views.*.Insert / Views.*.Update emission on the new flags.
  • Add test schema objects (tables/view/trigger) and update snapshots across typegen + meta view/trigger tests.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/lib/sql/views.sql.ts Adds trigger-aware is_insert_enabled/is_update_enabled fields to view introspection SQL.
src/lib/types.ts Extends PostgresView schema with is_insert_enabled/is_update_enabled.
src/server/templates/typescript.ts Generates view Insert and Update blocks independently based on new view flags.
test/db/00-init.sql Adds profile, profile_type, profile_view, and an INSTEAD OF trigger for regression coverage.
test/lib/views.ts Updates view metadata snapshots to include the new view fields.
test/lib/triggers.ts Updates trigger list snapshots to include the new INSTEAD OF trigger.
test/server/typegen.ts Updates expected generated types snapshots for the new tables/view.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +14 to +18
-- A view supports INSERT if it is auto-updatable OR has an INSTEAD OF INSERT trigger
(
(pg_relation_is_updatable(c.oid, false) & 8) = 8
OR EXISTS (
SELECT 1 FROM pg_trigger t
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VIEWS_SQL defines is_insert_enabled/is_update_enabled twice in the same SELECT (first at lines 14-35, then again at lines 36-57). This yields duplicate output column names and the second definition can override the first in result mapping, which can make is_insert_enabled/is_update_enabled incorrect (e.g. preventing view Insert type generation). Remove the duplicated block and keep a single, consistent trigger-bit check for INSTEAD OF INSERT/UPDATE.

Copilot uses AI. Check for mistakes.
Comment on lines 229 to 248
const triggers = res.data?.map(({ id, table_id, ...trigger }) => trigger)
expect(triggers).toMatchInlineSnapshot(`
[
{
"activation": "INSTEAD OF",
"condition": null,
"enabled_mode": "ORIGIN",
"events": [
"INSERT",
"UPDATE",
],
"function_args": [],
"function_name": "profile_view_instead_of_trigger",
"function_schema": "public",
"name": "profile_view_instead_of_trigger",
"orientation": "ROW",
"schema": "public",
"table": "profile_view",
},
{
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This snapshot now includes the global profile_view_instead_of_trigger from the shared test DB init, so the test is no longer isolated to the triggers created inside this test case. Consider filtering res.data down to just the tr triggers (or schema s1/s2) before snapshotting, to avoid unrelated init triggers making the test brittle.

Suggested change
const triggers = res.data?.map(({ id, table_id, ...trigger }) => trigger)
expect(triggers).toMatchInlineSnapshot(`
[
{
"activation": "INSTEAD OF",
"condition": null,
"enabled_mode": "ORIGIN",
"events": [
"INSERT",
"UPDATE",
],
"function_args": [],
"function_name": "profile_view_instead_of_trigger",
"function_schema": "public",
"name": "profile_view_instead_of_trigger",
"orientation": "ROW",
"schema": "public",
"table": "profile_view",
},
{
const triggers = res.data
?.filter(({ name, schema }) => name === 'tr' && (schema === 's1' || schema === 's2'))
.map(({ id, table_id, ...trigger }) => trigger)
expect(triggers).toMatchInlineSnapshot(`
[
{

Copilot uses AI. Check for mistakes.
Comment on lines 308 to 327
const triggers = res.data?.map(({ id, table_id, ...trigger }) => trigger)
expect(triggers).toMatchInlineSnapshot(`
[
{
"activation": "INSTEAD OF",
"condition": null,
"enabled_mode": "ORIGIN",
"events": [
"INSERT",
"UPDATE",
],
"function_args": [],
"function_name": "profile_view_instead_of_trigger",
"function_schema": "public",
"name": "profile_view_instead_of_trigger",
"orientation": "ROW",
"schema": "public",
"table": "profile_view",
},
{
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test snapshots the entire output of pgMeta.triggers.list(), which now includes unrelated triggers from the shared test DB init (e.g. profile_view_instead_of_trigger). To keep this test focused and stable, filter the returned triggers to the schema/table/name created in this test (e.g. schema MySchema) before snapshotting.

Suggested change
const triggers = res.data?.map(({ id, table_id, ...trigger }) => trigger)
expect(triggers).toMatchInlineSnapshot(`
[
{
"activation": "INSTEAD OF",
"condition": null,
"enabled_mode": "ORIGIN",
"events": [
"INSERT",
"UPDATE",
],
"function_args": [],
"function_name": "profile_view_instead_of_trigger",
"function_schema": "public",
"name": "profile_view_instead_of_trigger",
"orientation": "ROW",
"schema": "public",
"table": "profile_view",
},
{
const triggers = res.data
?.filter(
({ schema, table, name }) =>
schema === 'MySchema' && table === 'MyTable' && name === 'my_trigger'
)
.map(({ id, table_id, ...trigger }) => trigger)
expect(triggers).toMatchInlineSnapshot(`
[
{

Copilot uses AI. Check for mistakes.
Comment on lines +430 to +441
profile_view: {
Row: {
id: string | null
profileType: string | null
username: string | null
}

Update: {
id?: never
profileType?: never
username?: never
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says views with INSTEAD OF INSERT triggers should get an Insert type generated independently of Update. In the test DB init, profile_view is created with an INSTEAD OF INSERT OR UPDATE trigger, but this expected generated TypeScript output for profile_view only includes Update (no Insert). Either the generator logic is still not detecting INSERT triggers for views, or the snapshot needs to include the Insert section to match the intended behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +1701 to +1712
profile_view: {
Row: {
id: string | null
profileType: string | null
username: string | null
}

Update: {
id?: never
profileType?: never
username?: never
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as earlier profile_view expectation: the test DB init creates an INSTEAD OF INSERT OR UPDATE trigger on profile_view, but this expected generated output only includes Update and omits Insert. This seems inconsistent with the PR’s stated behavior (Insert/Update handled independently).

Copilot uses AI. Check for mistakes.
Comment on lines +2984 to +2995
profile_view: {
Row: {
id: string | null
profileType: string | null
username: string | null
}

Update: {
id?: never
profileType?: never
username?: never
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue for this profile_view snapshot: despite an INSTEAD OF INSERT OR UPDATE trigger in the test DB init, the expected generated types only include Update and omit Insert. Either INSERT triggers aren’t being detected or the snapshot should be updated to assert Insert generation.

Copilot uses AI. Check for mistakes.
Comment on lines +4272 to +4283
profile_view: {
Row: {
id: string | null
profileType: string | null
username: string | null
}

Update: {
id?: never
profileType?: never
username?: never
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same profile_view snapshot issue here: the view has an INSTEAD OF INSERT OR UPDATE trigger in test setup, but the expected generated output omits the Insert type. This conflicts with the PR description that INSERT and UPDATE trigger support should be reflected independently.

Copilot uses AI. Check for mistakes.
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.

[supabase gen] complex Views with triggers missing Insert/Update definitions

2 participants