Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions internal/diff/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,13 @@ type ddlDiff struct {
// exist when the view body is parsed (issue #414).
deferredAddedViews []*ir.View
functionsAwaitingDeferredViews []*ir.Function
// Added functions whose return/parameter type references a view being recreated
// (DROP + CREATE) by this migration. For a function whose signature changed, its
// old definition is dropped in the drop phase; creating the new one in the create
// phase (before the view's DROP) would re-pin the old view and block its RESTRICT
// drop. They are created in the modify phase, AFTER generateModifyViewsSQL
// recreates the view (issue #480).
functionsAwaitingRecreatedViews []*ir.Function
// Foreign keys that depend on a unique/PK constraint being dropped or
// recreated by this migration: existing ones are dropped before the table
// modifications (fkPreDrops) and desired-state ones are (re)created
Expand Down Expand Up @@ -1820,6 +1827,29 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto
}
}

// Functions whose return/parameter type references a view being recreated
// (DROP + CREATE in the modify phase) must be created AFTER that recreation.
// For a signature-changed function the old definition was dropped in the drop
// phase; creating the new one now (before the view's DROP) would re-pin the old
// view and block its RESTRICT drop (issue #480). Pull them out of both buckets
// and defer to the modify phase.
recreatedViewLookup := buildRecreatedViewLookup(d.modifiedViews)
if len(recreatedViewLookup) > 0 {
deferRecreated := func(fns []*ir.Function) []*ir.Function {
var keep []*ir.Function
for _, fn := range fns {
if functionReferencesNewView(fn, recreatedViewLookup) {
d.functionsAwaitingRecreatedViews = append(d.functionsAwaitingRecreatedViews, fn)
} else {
keep = append(keep, fn)
}
}
return keep
}
functionsWithoutViewDeps = deferRecreated(functionsWithoutViewDeps)
functionsWithViewDeps = deferRecreated(functionsWithViewDeps)
}

// Create functions WITHOUT view dependencies (functions may depend on tables created above)
generateCreateFunctionsSQL(functionsWithoutViewDeps, targetSchema, collector)

Expand Down Expand Up @@ -1982,6 +2012,14 @@ func (d *ddlDiff) generateModifySQL(targetSchema string, collector *diffCollecto
// Modify views - pass preDroppedViews to skip DROP for already-dropped views
generateModifyViewsSQL(d.modifiedViews, targetSchema, collector, preDroppedViews, dependentViewsCtx, recreatedViews)

// Create functions deferred from generateCreateSQL because their return/parameter
// type references a view just recreated above. Emitting them now (rather than in
// the create phase) keeps the recreated view free of dependents during its
// RESTRICT drop (issue #480).
if len(d.functionsAwaitingRecreatedViews) > 0 {
Comment thread
tianzhou marked this conversation as resolved.
generateCreateFunctionsSQL(d.functionsAwaitingRecreatedViews, targetSchema, collector)
}

// Modify functions
generateModifyFunctionsSQL(d.modifiedFunctions, targetSchema, collector)

Expand Down Expand Up @@ -2269,6 +2307,19 @@ func buildViewLookup(views []*ir.View) map[string]struct{} {
return buildSchemaNameLookup(names)
}

// buildRecreatedViewLookup returns case-insensitive lookup keys for views being
// recreated (DROP + CREATE) by this migration, i.e. modified views with
// RequiresRecreate set.
func buildRecreatedViewLookup(modifiedViews []*viewDiff) map[string]struct{} {
var names []struct{ schema, name string }
for _, vd := range modifiedViews {
if vd.RequiresRecreate {
names = append(names, struct{ schema, name string }{vd.New.Schema, vd.New.Name})
}
}
return buildSchemaNameLookup(names)
}

// functionReferencesNewView determines if a function references any newly added views
// in its return type or parameter types. This handles cases where functions use
// view composite types (e.g., RETURNS SETOF view_name or parameter of view_name type).
Expand Down
32 changes: 32 additions & 0 deletions testdata/diff/dependency/issue_480_view_function_recreate/diff.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
DROP FUNCTION IF EXISTS fn_create_user(text);

ALTER TABLE tb_users ADD COLUMN role text DEFAULT 'member' NOT NULL;

DROP VIEW IF EXISTS vw_users RESTRICT;

CREATE OR REPLACE VIEW vw_users AS
SELECT id,
email,
role,
created_at
FROM tb_users
WHERE is_deleted = false;

CREATE OR REPLACE FUNCTION fn_create_user(
email text,
role text DEFAULT 'member'
)
RETURNS vw_users
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
AS $$
DECLARE
v_result vw_users;
v_new_id UUID;
BEGIN
INSERT INTO tb_users (email, role) VALUES (email, role) RETURNING id INTO v_new_id;
SELECT id, email, role, created_at INTO v_result FROM vw_users WHERE id = v_new_id;
RETURN v_result;
END;
$$;
25 changes: 25 additions & 0 deletions testdata/diff/dependency/issue_480_view_function_recreate/new.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
CREATE TABLE tb_users (
id uuid DEFAULT gen_random_uuid(),
is_deleted boolean DEFAULT false NOT NULL,
created_at timestamptz DEFAULT now(),
email text DEFAULT 'missing@missing.com' NOT NULL,
role text DEFAULT 'member' NOT NULL,
CONSTRAINT tb_users_pkey PRIMARY KEY (id)
);

CREATE VIEW vw_users AS
SELECT id, email, role, created_at
FROM tb_users
WHERE is_deleted = FALSE;

CREATE FUNCTION fn_create_user(email TEXT, role TEXT DEFAULT 'member')
RETURNS vw_users AS $$
DECLARE
v_result vw_users;
v_new_id UUID;
BEGIN
INSERT INTO tb_users (email, role) VALUES (email, role) RETURNING id INTO v_new_id;
SELECT id, email, role, created_at INTO v_result FROM vw_users WHERE id = v_new_id;
RETURN v_result;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
24 changes: 24 additions & 0 deletions testdata/diff/dependency/issue_480_view_function_recreate/old.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE TABLE tb_users (
id uuid DEFAULT gen_random_uuid(),
is_deleted boolean DEFAULT false NOT NULL,
created_at timestamptz DEFAULT now(),
email text DEFAULT 'missing@missing.com' NOT NULL,
CONSTRAINT tb_users_pkey PRIMARY KEY (id)
);

CREATE VIEW vw_users AS
SELECT id, email, created_at
FROM tb_users
WHERE is_deleted = FALSE;

CREATE FUNCTION fn_create_user(email TEXT)
RETURNS vw_users AS $$
DECLARE
v_result vw_users;
v_new_id UUID;
BEGIN
INSERT INTO tb_users (email) VALUES (email) RETURNING id INTO v_new_id;
SELECT id, email, created_at INTO v_result FROM vw_users WHERE id = v_new_id;
RETURN v_result;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"version": "1.0.0",
"pgschema_version": "1.11.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "c7ede31c4d93f537192a343c3b52ae33b76c573082ad077bfd20a09419482c9d"
},
"groups": [
{
"steps": [
{
"sql": "DROP FUNCTION IF EXISTS fn_create_user(text);",
"type": "function",
"operation": "drop",
"path": "public.fn_create_user"
},
{
"sql": "ALTER TABLE tb_users ADD COLUMN role text DEFAULT 'member' NOT NULL;",
"type": "table.column",
"operation": "create",
"path": "public.tb_users.role"
},
{
"sql": "DROP VIEW IF EXISTS vw_users RESTRICT;",
"type": "view",
"operation": "alter",
"path": "public.vw_users"
},
{
"sql": "CREATE OR REPLACE VIEW vw_users AS\n SELECT id,\n email,\n role,\n created_at\n FROM tb_users\n WHERE is_deleted = false;",
"type": "view",
"operation": "alter",
"path": "public.vw_users"
},
{
"sql": "CREATE OR REPLACE FUNCTION fn_create_user(\n email text,\n role text DEFAULT 'member'\n)\nRETURNS vw_users\nLANGUAGE plpgsql\nVOLATILE\nSECURITY DEFINER\nAS $$\nDECLARE\n v_result vw_users;\n v_new_id UUID;\nBEGIN\n INSERT INTO tb_users (email, role) VALUES (email, role) RETURNING id INTO v_new_id;\n SELECT id, email, role, created_at INTO v_result FROM vw_users WHERE id = v_new_id;\n RETURN v_result;\nEND;\n$$;",
"type": "function",
"operation": "create",
"path": "public.fn_create_user"
}
]
}
]
}
32 changes: 32 additions & 0 deletions testdata/diff/dependency/issue_480_view_function_recreate/plan.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
DROP FUNCTION IF EXISTS fn_create_user(text);

ALTER TABLE tb_users ADD COLUMN role text DEFAULT 'member' NOT NULL;

DROP VIEW IF EXISTS vw_users RESTRICT;

CREATE OR REPLACE VIEW vw_users AS
SELECT id,
email,
role,
created_at
FROM tb_users
WHERE is_deleted = false;

CREATE OR REPLACE FUNCTION fn_create_user(
email text,
role text DEFAULT 'member'
)
RETURNS vw_users
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
AS $$
DECLARE
v_result vw_users;
v_new_id UUID;
BEGIN
INSERT INTO tb_users (email, role) VALUES (email, role) RETURNING id INTO v_new_id;
SELECT id, email, role, created_at INTO v_result FROM vw_users WHERE id = v_new_id;
RETURN v_result;
END;
$$;
53 changes: 53 additions & 0 deletions testdata/diff/dependency/issue_480_view_function_recreate/plan.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Plan: 1 to add, 2 to modify, 1 to drop.

Summary by type:
functions: 1 to add, 1 to drop
tables: 1 to modify
views: 1 to modify

Functions:
- fn_create_user
+ fn_create_user

Tables:
~ tb_users
+ role (column)

Views:
~ vw_users

DDL to be executed:
--------------------------------------------------

DROP FUNCTION IF EXISTS fn_create_user(text);

ALTER TABLE tb_users ADD COLUMN role text DEFAULT 'member' NOT NULL;

DROP VIEW IF EXISTS vw_users RESTRICT;

CREATE OR REPLACE VIEW vw_users AS
SELECT id,
email,
role,
created_at
FROM tb_users
WHERE is_deleted = false;

CREATE OR REPLACE FUNCTION fn_create_user(
email text,
role text DEFAULT 'member'
)
RETURNS vw_users
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
AS $$
DECLARE
v_result vw_users;
v_new_id UUID;
BEGIN
INSERT INTO tb_users (email, role) VALUES (email, role) RETURNING id INTO v_new_id;
SELECT id, email, role, created_at INTO v_result FROM vw_users WHERE id = v_new_id;
RETURN v_result;
END;
$$;