From 038371b5e3db8a2386258702bd4d64b2fec78c23 Mon Sep 17 00:00:00 2001 From: tianzhou Date: Mon, 5 Jan 2026 02:46:38 -0800 Subject: [PATCH 1/5] test: add test cases for ALTER DEFAULT PRIVILEGES support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 7 test cases covering default privilege operations: - add_table_privilege: GRANT SELECT/INSERT/UPDATE on tables - add_sequence_privilege: GRANT USAGE/SELECT on sequences - add_function_privilege: REVOKE/GRANT EXECUTE on functions - add_type_privilege: GRANT USAGE on types - add_privilege_with_grant_option: WITH GRANT OPTION support - drop_privilege: remove all default privileges - alter_privilege: modify existing privileges 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../diff/privilege/add_function_privilege/diff.sql | 3 +++ testdata/diff/privilege/add_function_privilege/new.sql | 9 +++++++++ testdata/diff/privilege/add_function_privilege/old.sql | 6 ++++++ .../privilege/add_privilege_with_grant_option/diff.sql | 1 + .../privilege/add_privilege_with_grant_option/new.sql | 7 +++++++ .../privilege/add_privilege_with_grant_option/old.sql | 5 +++++ .../diff/privilege/add_sequence_privilege/diff.sql | 1 + testdata/diff/privilege/add_sequence_privilege/new.sql | 4 ++++ testdata/diff/privilege/add_sequence_privilege/old.sql | 2 ++ testdata/diff/privilege/add_table_privilege/diff.sql | 3 +++ testdata/diff/privilege/add_table_privilege/new.sql | 10 ++++++++++ testdata/diff/privilege/add_table_privilege/old.sql | 5 +++++ testdata/diff/privilege/add_type_privilege/diff.sql | 1 + testdata/diff/privilege/add_type_privilege/new.sql | 4 ++++ testdata/diff/privilege/add_type_privilege/old.sql | 2 ++ testdata/diff/privilege/alter_privilege/diff.sql | 3 +++ testdata/diff/privilege/alter_privilege/new.sql | 7 +++++++ testdata/diff/privilege/alter_privilege/old.sql | 8 ++++++++ testdata/diff/privilege/drop_privilege/diff.sql | 3 +++ testdata/diff/privilege/drop_privilege/new.sql | 5 +++++ testdata/diff/privilege/drop_privilege/old.sql | 8 ++++++++ 21 files changed, 97 insertions(+) create mode 100644 testdata/diff/privilege/add_function_privilege/diff.sql create mode 100644 testdata/diff/privilege/add_function_privilege/new.sql create mode 100644 testdata/diff/privilege/add_function_privilege/old.sql create mode 100644 testdata/diff/privilege/add_privilege_with_grant_option/diff.sql create mode 100644 testdata/diff/privilege/add_privilege_with_grant_option/new.sql create mode 100644 testdata/diff/privilege/add_privilege_with_grant_option/old.sql create mode 100644 testdata/diff/privilege/add_sequence_privilege/diff.sql create mode 100644 testdata/diff/privilege/add_sequence_privilege/new.sql create mode 100644 testdata/diff/privilege/add_sequence_privilege/old.sql create mode 100644 testdata/diff/privilege/add_table_privilege/diff.sql create mode 100644 testdata/diff/privilege/add_table_privilege/new.sql create mode 100644 testdata/diff/privilege/add_table_privilege/old.sql create mode 100644 testdata/diff/privilege/add_type_privilege/diff.sql create mode 100644 testdata/diff/privilege/add_type_privilege/new.sql create mode 100644 testdata/diff/privilege/add_type_privilege/old.sql create mode 100644 testdata/diff/privilege/alter_privilege/diff.sql create mode 100644 testdata/diff/privilege/alter_privilege/new.sql create mode 100644 testdata/diff/privilege/alter_privilege/old.sql create mode 100644 testdata/diff/privilege/drop_privilege/diff.sql create mode 100644 testdata/diff/privilege/drop_privilege/new.sql create mode 100644 testdata/diff/privilege/drop_privilege/old.sql diff --git a/testdata/diff/privilege/add_function_privilege/diff.sql b/testdata/diff/privilege/add_function_privilege/diff.sql new file mode 100644 index 00000000..01e921a9 --- /dev/null +++ b/testdata/diff/privilege/add_function_privilege/diff.sql @@ -0,0 +1,3 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO api_user; diff --git a/testdata/diff/privilege/add_function_privilege/new.sql b/testdata/diff/privilege/add_function_privilege/new.sql new file mode 100644 index 00000000..1afa7df3 --- /dev/null +++ b/testdata/diff/privilege/add_function_privilege/new.sql @@ -0,0 +1,9 @@ +-- Revoke default EXECUTE from PUBLIC, grant to specific role +ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; +ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO api_user; + +CREATE FUNCTION get_version() RETURNS text AS $$ +BEGIN + RETURN '1.0.0'; +END; +$$ LANGUAGE plpgsql; diff --git a/testdata/diff/privilege/add_function_privilege/old.sql b/testdata/diff/privilege/add_function_privilege/old.sql new file mode 100644 index 00000000..d85ffdc7 --- /dev/null +++ b/testdata/diff/privilege/add_function_privilege/old.sql @@ -0,0 +1,6 @@ +-- No default privileges configured +CREATE FUNCTION get_version() RETURNS text AS $$ +BEGIN + RETURN '1.0.0'; +END; +$$ LANGUAGE plpgsql; diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/diff.sql b/testdata/diff/privilege/add_privilege_with_grant_option/diff.sql new file mode 100644 index 00000000..01ab0d14 --- /dev/null +++ b/testdata/diff/privilege/add_privilege_with_grant_option/diff.sql @@ -0,0 +1 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT ON TABLES TO admin_user WITH GRANT OPTION; diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/new.sql b/testdata/diff/privilege/add_privilege_with_grant_option/new.sql new file mode 100644 index 00000000..66ada3f9 --- /dev/null +++ b/testdata/diff/privilege/add_privilege_with_grant_option/new.sql @@ -0,0 +1,7 @@ +-- Grant with grant option - admin_user can grant to others +ALTER DEFAULT PRIVILEGES GRANT SELECT, INSERT ON TABLES TO admin_user WITH GRANT OPTION; + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL +); diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/old.sql b/testdata/diff/privilege/add_privilege_with_grant_option/old.sql new file mode 100644 index 00000000..ef5533ed --- /dev/null +++ b/testdata/diff/privilege/add_privilege_with_grant_option/old.sql @@ -0,0 +1,5 @@ +-- No default privileges configured +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL +); diff --git a/testdata/diff/privilege/add_sequence_privilege/diff.sql b/testdata/diff/privilege/add_sequence_privilege/diff.sql new file mode 100644 index 00000000..aa4f74d0 --- /dev/null +++ b/testdata/diff/privilege/add_sequence_privilege/diff.sql @@ -0,0 +1 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO app_user; diff --git a/testdata/diff/privilege/add_sequence_privilege/new.sql b/testdata/diff/privilege/add_sequence_privilege/new.sql new file mode 100644 index 00000000..8f96a931 --- /dev/null +++ b/testdata/diff/privilege/add_sequence_privilege/new.sql @@ -0,0 +1,4 @@ +-- Grant USAGE, SELECT on future sequences to app_user +ALTER DEFAULT PRIVILEGES GRANT USAGE, SELECT ON SEQUENCES TO app_user; + +CREATE SEQUENCE order_seq START 1; diff --git a/testdata/diff/privilege/add_sequence_privilege/old.sql b/testdata/diff/privilege/add_sequence_privilege/old.sql new file mode 100644 index 00000000..22d1f823 --- /dev/null +++ b/testdata/diff/privilege/add_sequence_privilege/old.sql @@ -0,0 +1,2 @@ +-- No default privileges configured +CREATE SEQUENCE order_seq START 1; diff --git a/testdata/diff/privilege/add_table_privilege/diff.sql b/testdata/diff/privilege/add_table_privilege/diff.sql new file mode 100644 index 00000000..ea1cc979 --- /dev/null +++ b/testdata/diff/privilege/add_table_privilege/diff.sql @@ -0,0 +1,3 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO PUBLIC; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user; diff --git a/testdata/diff/privilege/add_table_privilege/new.sql b/testdata/diff/privilege/add_table_privilege/new.sql new file mode 100644 index 00000000..d59923dd --- /dev/null +++ b/testdata/diff/privilege/add_table_privilege/new.sql @@ -0,0 +1,10 @@ +-- Grant SELECT on future tables to PUBLIC +ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO PUBLIC; + +-- Grant INSERT, UPDATE to app_user role +ALTER DEFAULT PRIVILEGES GRANT INSERT, UPDATE ON TABLES TO app_user; + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL +); diff --git a/testdata/diff/privilege/add_table_privilege/old.sql b/testdata/diff/privilege/add_table_privilege/old.sql new file mode 100644 index 00000000..ef5533ed --- /dev/null +++ b/testdata/diff/privilege/add_table_privilege/old.sql @@ -0,0 +1,5 @@ +-- No default privileges configured +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL +); diff --git a/testdata/diff/privilege/add_type_privilege/diff.sql b/testdata/diff/privilege/add_type_privilege/diff.sql new file mode 100644 index 00000000..6d2068f5 --- /dev/null +++ b/testdata/diff/privilege/add_type_privilege/diff.sql @@ -0,0 +1 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON TYPES TO app_user; diff --git a/testdata/diff/privilege/add_type_privilege/new.sql b/testdata/diff/privilege/add_type_privilege/new.sql new file mode 100644 index 00000000..e655fbaf --- /dev/null +++ b/testdata/diff/privilege/add_type_privilege/new.sql @@ -0,0 +1,4 @@ +-- Grant USAGE on future types to app_user +ALTER DEFAULT PRIVILEGES GRANT USAGE ON TYPES TO app_user; + +CREATE TYPE status AS ENUM ('pending', 'active', 'inactive'); diff --git a/testdata/diff/privilege/add_type_privilege/old.sql b/testdata/diff/privilege/add_type_privilege/old.sql new file mode 100644 index 00000000..af881d78 --- /dev/null +++ b/testdata/diff/privilege/add_type_privilege/old.sql @@ -0,0 +1,2 @@ +-- No default privileges configured +CREATE TYPE status AS ENUM ('pending', 'active', 'inactive'); diff --git a/testdata/diff/privilege/alter_privilege/diff.sql b/testdata/diff/privilege/alter_privilege/diff.sql new file mode 100644 index 00000000..697d8266 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/diff.sql @@ -0,0 +1,3 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE USAGE ON SEQUENCES FROM app_user; diff --git a/testdata/diff/privilege/alter_privilege/new.sql b/testdata/diff/privilege/alter_privilege/new.sql new file mode 100644 index 00000000..1d5a62ee --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/new.sql @@ -0,0 +1,7 @@ +-- Expand table privileges, remove sequence privileges +ALTER DEFAULT PRIVILEGES GRANT SELECT, INSERT, UPDATE ON TABLES TO app_user; + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL +); diff --git a/testdata/diff/privilege/alter_privilege/old.sql b/testdata/diff/privilege/alter_privilege/old.sql new file mode 100644 index 00000000..614b5baa --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/old.sql @@ -0,0 +1,8 @@ +-- Initial default privileges +ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO app_user; +ALTER DEFAULT PRIVILEGES GRANT USAGE ON SEQUENCES TO app_user; + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL +); diff --git a/testdata/diff/privilege/drop_privilege/diff.sql b/testdata/diff/privilege/drop_privilege/diff.sql new file mode 100644 index 00000000..4500c247 --- /dev/null +++ b/testdata/diff/privilege/drop_privilege/diff.sql @@ -0,0 +1,3 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT ON TABLES FROM readonly_user; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE INSERT, UPDATE, DELETE ON TABLES FROM app_user; diff --git a/testdata/diff/privilege/drop_privilege/new.sql b/testdata/diff/privilege/drop_privilege/new.sql new file mode 100644 index 00000000..963259b6 --- /dev/null +++ b/testdata/diff/privilege/drop_privilege/new.sql @@ -0,0 +1,5 @@ +-- Remove all default privileges +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL +); diff --git a/testdata/diff/privilege/drop_privilege/old.sql b/testdata/diff/privilege/drop_privilege/old.sql new file mode 100644 index 00000000..295f5800 --- /dev/null +++ b/testdata/diff/privilege/drop_privilege/old.sql @@ -0,0 +1,8 @@ +-- Default privileges configured +ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO readonly_user; +ALTER DEFAULT PRIVILEGES GRANT INSERT, UPDATE, DELETE ON TABLES TO app_user; + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL +); From 0da4dded26dd9cddfdd3614d9a25516cd1245651 Mon Sep 17 00:00:00 2001 From: tianzhou Date: Mon, 5 Jan 2026 03:34:41 -0800 Subject: [PATCH 2/5] feat: support ALTER DEFAULT PRIVILEGES MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for ALTER DEFAULT PRIVILEGES to manage default access privileges for future objects created in a schema. Implementation: - Add DefaultPrivilege struct to IR with ObjectType, Grantee, Privileges, and WithGrantOption fields - Query pg_default_acl system catalog using aclexplode() to extract individual privilege grants - Generate GRANT/REVOKE statements for default privilege changes - Support TABLES, SEQUENCES, FUNCTIONS, and TYPES object types - Support WITH GRANT OPTION for delegated privilege management Test cases cover: - Adding default privileges for each object type - Modifying existing default privileges - Removing default privileges - WITH GRANT OPTION handling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/diff/default_privilege.go | 221 ++++++++++++++++++ internal/diff/diff.go | 103 +++++++- internal/dump/formatter.go | 16 +- internal/postgres/desired_state.go | 36 +++ internal/postgres/embedded.go | 4 + internal/postgres/external.go | 4 + ir/inspector.go | 63 +++++ ir/ir.go | 40 +++- ir/queries/queries.sql | 29 ++- ir/queries/queries.sql.go | 63 +++++ .../privilege/add_function_privilege/diff.sql | 2 - .../privilege/add_function_privilege/new.sql | 13 +- .../privilege/add_function_privilege/old.sql | 8 + .../add_function_privilege/plan.json | 20 ++ .../privilege/add_function_privilege/plan.sql | 1 + .../privilege/add_function_privilege/plan.txt | 8 + .../add_privilege_with_grant_option/diff.sql | 2 +- .../add_privilege_with_grant_option/new.sql | 10 +- .../add_privilege_with_grant_option/old.sql | 8 + .../add_privilege_with_grant_option/plan.json | 20 ++ .../add_privilege_with_grant_option/plan.sql | 1 + .../add_privilege_with_grant_option/plan.txt | 8 + .../privilege/add_sequence_privilege/diff.sql | 2 +- .../privilege/add_sequence_privilege/new.sql | 10 +- .../privilege/add_sequence_privilege/old.sql | 8 + .../add_sequence_privilege/plan.json | 20 ++ .../privilege/add_sequence_privilege/plan.sql | 1 + .../privilege/add_sequence_privilege/plan.txt | 8 + .../privilege/add_table_privilege/new.sql | 12 +- .../privilege/add_table_privilege/old.sql | 8 + .../privilege/add_table_privilege/plan.json | 26 +++ .../privilege/add_table_privilege/plan.sql | 3 + .../privilege/add_table_privilege/plan.txt | 10 + .../diff/privilege/add_type_privilege/new.sql | 10 +- .../diff/privilege/add_type_privilege/old.sql | 8 + .../privilege/add_type_privilege/plan.json | 20 ++ .../privilege/add_type_privilege/plan.sql | 1 + .../privilege/add_type_privilege/plan.txt | 8 + .../diff/privilege/alter_privilege/diff.sql | 4 +- .../diff/privilege/alter_privilege/new.sql | 10 +- .../diff/privilege/alter_privilege/old.sql | 12 +- .../diff/privilege/alter_privilege/plan.json | 26 +++ .../diff/privilege/alter_privilege/plan.sql | 3 + .../diff/privilege/alter_privilege/plan.txt | 10 + .../diff/privilege/drop_privilege/diff.sql | 4 +- .../diff/privilege/drop_privilege/new.sql | 11 + .../diff/privilege/drop_privilege/old.sql | 15 +- .../diff/privilege/drop_privilege/plan.json | 26 +++ .../diff/privilege/drop_privilege/plan.sql | 3 + .../diff/privilege/drop_privilege/plan.txt | 10 + 50 files changed, 932 insertions(+), 37 deletions(-) create mode 100644 internal/diff/default_privilege.go create mode 100644 testdata/diff/privilege/add_function_privilege/plan.json create mode 100644 testdata/diff/privilege/add_function_privilege/plan.sql create mode 100644 testdata/diff/privilege/add_function_privilege/plan.txt create mode 100644 testdata/diff/privilege/add_privilege_with_grant_option/plan.json create mode 100644 testdata/diff/privilege/add_privilege_with_grant_option/plan.sql create mode 100644 testdata/diff/privilege/add_privilege_with_grant_option/plan.txt create mode 100644 testdata/diff/privilege/add_sequence_privilege/plan.json create mode 100644 testdata/diff/privilege/add_sequence_privilege/plan.sql create mode 100644 testdata/diff/privilege/add_sequence_privilege/plan.txt create mode 100644 testdata/diff/privilege/add_table_privilege/plan.json create mode 100644 testdata/diff/privilege/add_table_privilege/plan.sql create mode 100644 testdata/diff/privilege/add_table_privilege/plan.txt create mode 100644 testdata/diff/privilege/add_type_privilege/plan.json create mode 100644 testdata/diff/privilege/add_type_privilege/plan.sql create mode 100644 testdata/diff/privilege/add_type_privilege/plan.txt create mode 100644 testdata/diff/privilege/alter_privilege/plan.json create mode 100644 testdata/diff/privilege/alter_privilege/plan.sql create mode 100644 testdata/diff/privilege/alter_privilege/plan.txt create mode 100644 testdata/diff/privilege/drop_privilege/plan.json create mode 100644 testdata/diff/privilege/drop_privilege/plan.sql create mode 100644 testdata/diff/privilege/drop_privilege/plan.txt diff --git a/internal/diff/default_privilege.go b/internal/diff/default_privilege.go new file mode 100644 index 00000000..ba3c0784 --- /dev/null +++ b/internal/diff/default_privilege.go @@ -0,0 +1,221 @@ +package diff + +import ( + "fmt" + "sort" + "strings" + + "github.com/pgschema/pgschema/ir" +) + +// generateCreateDefaultPrivilegesSQL generates ALTER DEFAULT PRIVILEGES GRANT statements +func generateCreateDefaultPrivilegesSQL(privileges []*ir.DefaultPrivilege, targetSchema string, collector *diffCollector) { + for _, dp := range privileges { + sql := generateGrantDefaultPrivilegeSQL(dp, targetSchema) + + context := &diffContext{ + Type: DiffTypeDefaultPrivilege, + Operation: DiffOperationCreate, + Path: fmt.Sprintf("default_privileges.%s.%s", dp.ObjectType, dp.Grantee), + Source: dp, + CanRunInTransaction: true, + } + + collector.collect(context, sql) + } +} + +// generateDropDefaultPrivilegesSQL generates ALTER DEFAULT PRIVILEGES REVOKE statements +func generateDropDefaultPrivilegesSQL(privileges []*ir.DefaultPrivilege, targetSchema string, collector *diffCollector) { + for _, dp := range privileges { + sql := generateRevokeDefaultPrivilegeSQL(dp, targetSchema) + + context := &diffContext{ + Type: DiffTypeDefaultPrivilege, + Operation: DiffOperationDrop, + Path: fmt.Sprintf("default_privileges.%s.%s", dp.ObjectType, dp.Grantee), + Source: dp, + CanRunInTransaction: true, + } + + collector.collect(context, sql) + } +} + +// generateModifyDefaultPrivilegesSQL generates ALTER DEFAULT PRIVILEGES statements for modifications +func generateModifyDefaultPrivilegesSQL(diffs []*defaultPrivilegeDiff, targetSchema string, collector *diffCollector) { + for _, diff := range diffs { + statements := diff.generateAlterDefaultPrivilegeStatements(targetSchema) + for _, stmt := range statements { + context := &diffContext{ + Type: DiffTypeDefaultPrivilege, + Operation: DiffOperationAlter, + Path: fmt.Sprintf("default_privileges.%s.%s", diff.New.ObjectType, diff.New.Grantee), + Source: diff, + CanRunInTransaction: true, + } + + collector.collect(context, stmt) + } + } +} + +// generateGrantDefaultPrivilegeSQL generates ALTER DEFAULT PRIVILEGES GRANT statement +func generateGrantDefaultPrivilegeSQL(dp *ir.DefaultPrivilege, targetSchema string) string { + // Sort privileges for deterministic output + sortedPrivs := make([]string, len(dp.Privileges)) + copy(sortedPrivs, dp.Privileges) + sort.Strings(sortedPrivs) + + privStr := strings.Join(sortedPrivs, ", ") + grantee := dp.Grantee + if grantee == "" || grantee == "PUBLIC" { + // PUBLIC is a special keyword meaning "all roles", not an identifier + grantee = "PUBLIC" + } else { + grantee = ir.QuoteIdentifier(grantee) + } + + sql := fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA %s GRANT %s ON %s TO %s", + ir.QuoteIdentifier(targetSchema), privStr, dp.ObjectType, grantee) + + if dp.WithGrantOption { + sql += " WITH GRANT OPTION" + } + + return sql + ";" +} + +// generateRevokeDefaultPrivilegeSQL generates ALTER DEFAULT PRIVILEGES REVOKE statement +func generateRevokeDefaultPrivilegeSQL(dp *ir.DefaultPrivilege, targetSchema string) string { + // Sort privileges for deterministic output + sortedPrivs := make([]string, len(dp.Privileges)) + copy(sortedPrivs, dp.Privileges) + sort.Strings(sortedPrivs) + + privStr := strings.Join(sortedPrivs, ", ") + grantee := dp.Grantee + if grantee == "" || grantee == "PUBLIC" { + // PUBLIC is a special keyword meaning "all roles", not an identifier + grantee = "PUBLIC" + } else { + grantee = ir.QuoteIdentifier(grantee) + } + + return fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA %s REVOKE %s ON %s FROM %s;", + ir.QuoteIdentifier(targetSchema), privStr, dp.ObjectType, grantee) +} + +// generateAlterDefaultPrivilegeStatements generates statements for privilege modifications +func (d *defaultPrivilegeDiff) generateAlterDefaultPrivilegeStatements(targetSchema string) []string { + var statements []string + + // Find privileges to revoke (in old but not in new) + oldPrivSet := make(map[string]bool) + for _, p := range d.Old.Privileges { + oldPrivSet[p] = true + } + newPrivSet := make(map[string]bool) + for _, p := range d.New.Privileges { + newPrivSet[p] = true + } + + var toRevoke []string + for p := range oldPrivSet { + if !newPrivSet[p] { + toRevoke = append(toRevoke, p) + } + } + + var toGrant []string + for p := range newPrivSet { + if !oldPrivSet[p] { + toGrant = append(toGrant, p) + } + } + + grantee := d.New.Grantee + if grantee == "" || grantee == "PUBLIC" { + // PUBLIC is a special keyword meaning "all roles", not an identifier + grantee = "PUBLIC" + } else { + grantee = ir.QuoteIdentifier(grantee) + } + quotedSchema := ir.QuoteIdentifier(targetSchema) + + // Generate REVOKE for removed privileges + if len(toRevoke) > 0 { + sort.Strings(toRevoke) + statements = append(statements, fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA %s REVOKE %s ON %s FROM %s;", + quotedSchema, strings.Join(toRevoke, ", "), d.Old.ObjectType, grantee)) + } + + // Generate GRANT for added privileges + if len(toGrant) > 0 { + sort.Strings(toGrant) + sql := fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA %s GRANT %s ON %s TO %s", + quotedSchema, strings.Join(toGrant, ", "), d.New.ObjectType, grantee) + if d.New.WithGrantOption { + sql += " WITH GRANT OPTION" + } + statements = append(statements, sql+";") + } + + // Handle WITH GRANT OPTION changes (if privileges are same but grant option changed) + if len(toRevoke) == 0 && len(toGrant) == 0 && d.Old.WithGrantOption != d.New.WithGrantOption { + // Need to revoke and re-grant with new option + sortedPrivs := make([]string, len(d.New.Privileges)) + copy(sortedPrivs, d.New.Privileges) + sort.Strings(sortedPrivs) + privStr := strings.Join(sortedPrivs, ", ") + + // Revoke first + statements = append(statements, fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA %s REVOKE %s ON %s FROM %s;", + quotedSchema, privStr, d.New.ObjectType, grantee)) + + // Then grant with correct option + sql := fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA %s GRANT %s ON %s TO %s", + quotedSchema, privStr, d.New.ObjectType, grantee) + if d.New.WithGrantOption { + sql += " WITH GRANT OPTION" + } + statements = append(statements, sql+";") + } + + return statements +} + +// GetObjectName returns a unique identifier for the default privilege diff +func (d *defaultPrivilegeDiff) GetObjectName() string { + return string(d.New.ObjectType) + ":" + d.New.Grantee +} + +// defaultPrivilegesEqual checks if two default privileges are structurally equal +func defaultPrivilegesEqual(old, new *ir.DefaultPrivilege) bool { + if old.ObjectType != new.ObjectType { + return false + } + if old.Grantee != new.Grantee { + return false + } + if old.WithGrantOption != new.WithGrantOption { + return false + } + + // Compare privileges (order-independent) + if len(old.Privileges) != len(new.Privileges) { + return false + } + + oldPrivSet := make(map[string]bool) + for _, p := range old.Privileges { + oldPrivSet[p] = true + } + for _, p := range new.Privileges { + if !oldPrivSet[p] { + return false + } + } + + return true +} diff --git a/internal/diff/diff.go b/internal/diff/diff.go index a1dea859..b24e0cba 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -36,6 +36,7 @@ const ( DiffTypeType DiffTypeDomain DiffTypeComment + DiffTypeDefaultPrivilege ) // String returns the string representation of DiffType @@ -85,6 +86,8 @@ func (d DiffType) String() string { return "domain" case DiffTypeComment: return "comment" + case DiffTypeDefaultPrivilege: + return "default_privilege" default: return "unknown" } @@ -147,6 +150,8 @@ func (d *DiffType) UnmarshalJSON(data []byte) error { *d = DiffTypeDomain case "comment": *d = DiffTypeComment + case "default_privilege": + *d = DiffTypeDefaultPrivilege default: return fmt.Errorf("unknown diff type: %s", s) } @@ -248,9 +253,12 @@ type ddlDiff struct { addedTypes []*ir.Type droppedTypes []*ir.Type modifiedTypes []*typeDiff - addedSequences []*ir.Sequence - droppedSequences []*ir.Sequence - modifiedSequences []*sequenceDiff + addedSequences []*ir.Sequence + droppedSequences []*ir.Sequence + modifiedSequences []*sequenceDiff + addedDefaultPrivileges []*ir.DefaultPrivilege + droppedDefaultPrivileges []*ir.DefaultPrivilege + modifiedDefaultPrivileges []*defaultPrivilegeDiff } // schemaDiff represents changes to a schema @@ -283,6 +291,12 @@ type sequenceDiff struct { New *ir.Sequence } +// defaultPrivilegeDiff represents changes to default privileges +type defaultPrivilegeDiff struct { + Old *ir.DefaultPrivilege + New *ir.DefaultPrivilege +} + // triggerDiff represents changes to a trigger type triggerDiff struct { Old *ir.Trigger @@ -378,9 +392,12 @@ func GenerateMigration(oldIR, newIR *ir.IR, targetSchema string) []Diff { addedTypes: []*ir.Type{}, droppedTypes: []*ir.Type{}, modifiedTypes: []*typeDiff{}, - addedSequences: []*ir.Sequence{}, - droppedSequences: []*ir.Sequence{}, - modifiedSequences: []*sequenceDiff{}, + addedSequences: []*ir.Sequence{}, + droppedSequences: []*ir.Sequence{}, + modifiedSequences: []*sequenceDiff{}, + addedDefaultPrivileges: []*ir.DefaultPrivilege{}, + droppedDefaultPrivileges: []*ir.DefaultPrivilege{}, + modifiedDefaultPrivileges: []*defaultPrivilegeDiff{}, } // Compare schemas first in deterministic order @@ -887,6 +904,72 @@ func GenerateMigration(oldIR, newIR *ir.IR, targetSchema string) []Diff { } } + // Compare default privileges across all schemas + oldDefaultPrivs := make(map[string]*ir.DefaultPrivilege) + newDefaultPrivs := make(map[string]*ir.DefaultPrivilege) + + // Extract default privileges from all schemas in oldIR + for _, dbSchema := range oldIR.Schemas { + for _, dp := range dbSchema.DefaultPrivileges { + key := string(dp.ObjectType) + ":" + dp.Grantee + oldDefaultPrivs[key] = dp + } + } + + // Extract default privileges from all schemas in newIR + for _, dbSchema := range newIR.Schemas { + for _, dp := range dbSchema.DefaultPrivileges { + key := string(dp.ObjectType) + ":" + dp.Grantee + newDefaultPrivs[key] = dp + } + } + + // Find added default privileges + for key, dp := range newDefaultPrivs { + if _, exists := oldDefaultPrivs[key]; !exists { + diff.addedDefaultPrivileges = append(diff.addedDefaultPrivileges, dp) + } + } + + // Find dropped default privileges + for key, dp := range oldDefaultPrivs { + if _, exists := newDefaultPrivs[key]; !exists { + diff.droppedDefaultPrivileges = append(diff.droppedDefaultPrivileges, dp) + } + } + + // Find modified default privileges + for key, newDP := range newDefaultPrivs { + if oldDP, exists := oldDefaultPrivs[key]; exists { + if !defaultPrivilegesEqual(oldDP, newDP) { + diff.modifiedDefaultPrivileges = append(diff.modifiedDefaultPrivileges, &defaultPrivilegeDiff{ + Old: oldDP, + New: newDP, + }) + } + } + } + + // Sort default privileges for deterministic output + sort.Slice(diff.addedDefaultPrivileges, func(i, j int) bool { + if diff.addedDefaultPrivileges[i].ObjectType != diff.addedDefaultPrivileges[j].ObjectType { + return diff.addedDefaultPrivileges[i].ObjectType < diff.addedDefaultPrivileges[j].ObjectType + } + return diff.addedDefaultPrivileges[i].Grantee < diff.addedDefaultPrivileges[j].Grantee + }) + sort.Slice(diff.droppedDefaultPrivileges, func(i, j int) bool { + if diff.droppedDefaultPrivileges[i].ObjectType != diff.droppedDefaultPrivileges[j].ObjectType { + return diff.droppedDefaultPrivileges[i].ObjectType < diff.droppedDefaultPrivileges[j].ObjectType + } + return diff.droppedDefaultPrivileges[i].Grantee < diff.droppedDefaultPrivileges[j].Grantee + }) + sort.Slice(diff.modifiedDefaultPrivileges, func(i, j int) bool { + if diff.modifiedDefaultPrivileges[i].New.ObjectType != diff.modifiedDefaultPrivileges[j].New.ObjectType { + return diff.modifiedDefaultPrivileges[i].New.ObjectType < diff.modifiedDefaultPrivileges[j].New.ObjectType + } + return diff.modifiedDefaultPrivileges[i].New.Grantee < diff.modifiedDefaultPrivileges[j].New.Grantee + }) + // Sort tables and views topologically for consistent ordering // Pre-sort by name to ensure deterministic insertion order for cycle breaking sort.Slice(diff.addedTables, func(i, j int) bool { @@ -1090,6 +1173,9 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto // Create views generateCreateViewsSQL(d.addedViews, targetSchema, collector) + + // Create default privileges + generateCreateDefaultPrivilegesSQL(d.addedDefaultPrivileges, targetSchema, collector) } // generateModifySQL generates ALTER statements @@ -1116,6 +1202,8 @@ func (d *ddlDiff) generateModifySQL(targetSchema string, collector *diffCollecto // Modify procedures generateModifyProceduresSQL(d.modifiedProcedures, targetSchema, collector) + // Modify default privileges + generateModifyDefaultPrivilegesSQL(d.modifiedDefaultPrivileges, targetSchema, collector) } // generateDropSQL generates DROP statements in reverse dependency order @@ -1144,6 +1232,9 @@ func (d *ddlDiff) generateDropSQL(targetSchema string, collector *diffCollector, // Drop types generateDropTypesSQL(d.droppedTypes, targetSchema, collector) + // Drop default privileges + generateDropDefaultPrivilegesSQL(d.droppedDefaultPrivileges, targetSchema, collector) + // Drop schemas // Note: Schema deletion is out of scope for schema-level comparisons } diff --git a/internal/dump/formatter.go b/internal/dump/formatter.go index 2b31975b..91d95175 100644 --- a/internal/dump/formatter.go +++ b/internal/dump/formatter.go @@ -101,7 +101,7 @@ func (f *DumpFormatter) FormatMultiFile(diffs []diff.Diff, outputPath string) er } // Create files in dependency order - orderedDirs := []string{"types", "domains", "sequences", "functions", "procedures", "tables", "views", "materialized_views"} + orderedDirs := []string{"types", "domains", "sequences", "functions", "procedures", "tables", "views", "materialized_views", "default_privileges"} for _, dir := range orderedDirs { if objects, exists := filesByType[dir]; exists { @@ -240,6 +240,8 @@ func (f *DumpFormatter) getObjectDirectory(objectType string) string { case "comment": // Comments handled separately in FormatMultiFile return "tables" // fallback, will be overridden + case "default_privilege": + return "default_privileges" default: return "misc" } @@ -312,6 +314,18 @@ func (f *DumpFormatter) getGroupingName(step diff.Diff) string { if parts := strings.Split(step.Path, "."); len(parts) >= 2 { return parts[1] // Return parent object name (table/view) } + case diff.DiffTypeDefaultPrivilege: + // For default privileges, group by object type + if step.Source != nil { + switch obj := step.Source.(type) { + case *ir.DefaultPrivilege: + return string(obj.ObjectType) // Group by TABLES, SEQUENCES, etc. + } + } + // Fallback: extract from path (default_privileges.TABLES.grantee) + if parts := strings.Split(step.Path, "."); len(parts) >= 2 { + return parts[1] // Return object type + } } // For standalone objects or if table name extraction fails, use object name diff --git a/internal/postgres/desired_state.go b/internal/postgres/desired_state.go index 5f6854cc..40b36de7 100644 --- a/internal/postgres/desired_state.go +++ b/internal/postgres/desired_state.go @@ -173,3 +173,39 @@ func stripSchemaQualifications(sql string, schemaName string) string { return result } + +// replaceSchemaInDefaultPrivileges replaces schema names in ALTER DEFAULT PRIVILEGES statements. +// This is needed because stripSchemaQualifications only handles "schema.object" patterns, +// not "IN SCHEMA " clauses used by ALTER DEFAULT PRIVILEGES. +// +// Example: +// +// ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user; +// +// becomes: +// +// ALTER DEFAULT PRIVILEGES IN SCHEMA pgschema_tmp_xxx GRANT SELECT ON TABLES TO app_user; +// +// This ensures default privileges are created in the temporary schema where we can inspect them. +func replaceSchemaInDefaultPrivileges(sql string, targetSchema, tempSchema string) string { + if targetSchema == "" || tempSchema == "" { + return sql + } + + escapedTarget := regexp.QuoteMeta(targetSchema) + + // Pattern: IN SCHEMA (case insensitive for SQL keywords) + // Handle both quoted and unquoted schema names + // Pattern 1: IN SCHEMA "schema" (quoted) + pattern1 := fmt.Sprintf(`(?i)(IN\s+SCHEMA\s+)"%s"`, escapedTarget) + re1 := regexp.MustCompile(pattern1) + result := re1.ReplaceAllString(sql, fmt.Sprintf(`${1}"%s"`, tempSchema)) + + // Pattern 2: IN SCHEMA schema (unquoted) + // Use word boundary to avoid partial matches + pattern2 := fmt.Sprintf(`(?i)(IN\s+SCHEMA\s+)%s\b`, escapedTarget) + re2 := regexp.MustCompile(pattern2) + result = re2.ReplaceAllString(result, fmt.Sprintf(`${1}"%s"`, tempSchema)) + + return result +} diff --git a/internal/postgres/embedded.go b/internal/postgres/embedded.go index c9b50b2e..6593a5c5 100644 --- a/internal/postgres/embedded.go +++ b/internal/postgres/embedded.go @@ -211,6 +211,10 @@ func (ep *EmbeddedPostgres) ApplySchema(ctx context.Context, schema string, sql // rather than being explicitly qualified with the original schema name schemaAgnosticSQL := stripSchemaQualifications(sql, schema) + // Replace schema names in ALTER DEFAULT PRIVILEGES statements + // These use "IN SCHEMA " syntax which isn't handled by stripSchemaQualifications + schemaAgnosticSQL = replaceSchemaInDefaultPrivileges(schemaAgnosticSQL, schema, ep.tempSchema) + // Execute the SQL directly // Note: Desired state SQL should never contain operations like CREATE INDEX CONCURRENTLY // that cannot run in transactions. Those are migration details, not state declarations. diff --git a/internal/postgres/external.go b/internal/postgres/external.go index 42b10f89..3c6c2586 100644 --- a/internal/postgres/external.go +++ b/internal/postgres/external.go @@ -117,6 +117,10 @@ func (ed *ExternalDatabase) ApplySchema(ctx context.Context, schema string, sql // rather than being explicitly qualified with the original schema name schemaAgnosticSQL := stripSchemaQualifications(sql, schema) + // Replace schema names in ALTER DEFAULT PRIVILEGES statements + // These use "IN SCHEMA " syntax which isn't handled by stripSchemaQualifications + schemaAgnosticSQL = replaceSchemaInDefaultPrivileges(schemaAgnosticSQL, schema, ed.tempSchema) + // Execute the SQL directly // Note: Desired state SQL should never contain operations like CREATE INDEX CONCURRENTLY // that cannot run in transactions. Those are migration details, not state declarations. diff --git a/ir/inspector.go b/ir/inspector.go index 64f9507a..04552d9c 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -70,6 +70,7 @@ func (i *Inspector) BuildIR(ctx context.Context, targetSchema string) (*IR, erro i.buildProcedures, i.buildAggregates, i.buildTypes, + i.buildDefaultPrivileges, }, } @@ -1862,6 +1863,68 @@ func (i *Inspector) validateSchemaExists(ctx context.Context, schemaName string) return nil } +// buildDefaultPrivileges retrieves default privileges for the schema +func (i *Inspector) buildDefaultPrivileges(ctx context.Context, schema *IR, targetSchema string) error { + privileges, err := i.queries.GetDefaultPrivilegesForSchema(ctx, sql.NullString{String: targetSchema, Valid: true}) + if err != nil { + return err + } + + if len(privileges) == 0 { + return nil + } + + // Group privileges by (object_type, grantee, is_grantable) + type privKey struct { + ObjectType string + Grantee string + WithGrantOption bool + } + + grouped := make(map[privKey][]string) + for _, p := range privileges { + if !p.ObjectType.Valid || !p.Grantee.Valid || !p.PrivilegeType.Valid { + continue + } + + key := privKey{ + ObjectType: p.ObjectType.String, + Grantee: p.Grantee.String, + WithGrantOption: p.IsGrantable.Valid && p.IsGrantable.Bool, + } + + grouped[key] = append(grouped[key], p.PrivilegeType.String) + } + + // Convert to DefaultPrivilege structs + var defaultPrivileges []*DefaultPrivilege + for key, privs := range grouped { + dp := &DefaultPrivilege{ + ObjectType: DefaultPrivilegeObjectType(key.ObjectType), + Grantee: key.Grantee, + Privileges: privs, + WithGrantOption: key.WithGrantOption, + } + defaultPrivileges = append(defaultPrivileges, dp) + } + + // Sort for deterministic output + sort.Slice(defaultPrivileges, func(i, j int) bool { + if defaultPrivileges[i].ObjectType != defaultPrivileges[j].ObjectType { + return defaultPrivileges[i].ObjectType < defaultPrivileges[j].ObjectType + } + return defaultPrivileges[i].Grantee < defaultPrivileges[j].Grantee + }) + + // Assign to schema + s, ok := schema.GetSchema(targetSchema) + if ok { + s.DefaultPrivileges = defaultPrivileges + } + + return nil +} + // Helper functions for safe type conversion from interface{} func (i *Inspector) safeInterfaceToString(val interface{}) string { diff --git a/ir/ir.go b/ir/ir.go index 05d613d3..43f2425f 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -22,14 +22,15 @@ type Schema struct { Name string `json:"name"` Owner string `json:"owner"` // Schema owner // Note: Indexes, Triggers, and RLS Policies are stored at table level (Table.Indexes, Table.Triggers, Table.Policies) - Tables map[string]*Table `json:"tables"` // table_name -> Table - Views map[string]*View `json:"views"` // view_name -> View - Functions map[string]*Function `json:"functions"` // function_name -> Function - Procedures map[string]*Procedure `json:"procedures"` // procedure_name -> Procedure - Aggregates map[string]*Aggregate `json:"aggregates"` // aggregate_name -> Aggregate - Sequences map[string]*Sequence `json:"sequences"` // sequence_name -> Sequence - Types map[string]*Type `json:"types"` // type_name -> Type - mu sync.RWMutex // Protects concurrent access to all maps + Tables map[string]*Table `json:"tables"` // table_name -> Table + Views map[string]*View `json:"views"` // view_name -> View + Functions map[string]*Function `json:"functions"` // function_name -> Function + Procedures map[string]*Procedure `json:"procedures"` // procedure_name -> Procedure + Aggregates map[string]*Aggregate `json:"aggregates"` // aggregate_name -> Aggregate + Sequences map[string]*Sequence `json:"sequences"` // sequence_name -> Sequence + Types map[string]*Type `json:"types"` // type_name -> Type + DefaultPrivileges []*DefaultPrivilege `json:"default_privileges,omitempty"` // Default privileges for future objects + mu sync.RWMutex // Protects concurrent access to all maps } // LikeClause represents a LIKE clause in CREATE TABLE statement @@ -401,6 +402,29 @@ func (p *Procedure) GetArguments() string { return strings.Join(argTypes, ", ") } +// DefaultPrivilegeObjectType represents the object type for default privileges +type DefaultPrivilegeObjectType string + +const ( + DefaultPrivilegeObjectTypeTables DefaultPrivilegeObjectType = "TABLES" + DefaultPrivilegeObjectTypeSequences DefaultPrivilegeObjectType = "SEQUENCES" + DefaultPrivilegeObjectTypeFunctions DefaultPrivilegeObjectType = "FUNCTIONS" + DefaultPrivilegeObjectTypeTypes DefaultPrivilegeObjectType = "TYPES" +) + +// DefaultPrivilege represents an ALTER DEFAULT PRIVILEGES setting +type DefaultPrivilege struct { + ObjectType DefaultPrivilegeObjectType `json:"object_type"` // TABLES, SEQUENCES, FUNCTIONS, TYPES + Grantee string `json:"grantee"` // Role name or "PUBLIC" + Privileges []string `json:"privileges"` // SELECT, INSERT, UPDATE, etc. + WithGrantOption bool `json:"with_grant_option"` // Can grantee grant to others? +} + +// GetObjectName returns a unique identifier for the default privilege +func (d *DefaultPrivilege) GetObjectName() string { + return string(d.ObjectType) + ":" + d.Grantee +} + // NewIR creates a new empty catalog IR func NewIR() *IR { return &IR{ diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index d331421a..6fc61080 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -1200,4 +1200,31 @@ WHERE t.typtype = 'c' -- composite types only AND a.attnum > 0 -- exclude system columns AND NOT a.attisdropped -- exclude dropped columns AND n.nspname = $1 -ORDER BY n.nspname, t.typname, a.attnum; \ No newline at end of file +ORDER BY n.nspname, t.typname, a.attnum; + +-- GetDefaultPrivilegesForSchema retrieves default privileges for a specific schema +-- name: GetDefaultPrivilegesForSchema :many +WITH acl_expanded AS ( + SELECT + d.defaclobjtype, + (aclexplode(d.defaclacl)).grantee AS grantee_oid, + (aclexplode(d.defaclacl)).privilege_type AS privilege_type, + (aclexplode(d.defaclacl)).is_grantable AS is_grantable + FROM pg_default_acl d + JOIN pg_namespace n ON d.defaclnamespace = n.oid + WHERE n.nspname = $1 +) +SELECT + CASE a.defaclobjtype + WHEN 'r' THEN 'TABLES' + WHEN 'S' THEN 'SEQUENCES' + WHEN 'f' THEN 'FUNCTIONS' + WHEN 'T' THEN 'TYPES' + WHEN 'n' THEN 'SCHEMAS' + END AS object_type, + COALESCE(r.rolname, 'PUBLIC') AS grantee, + a.privilege_type, + a.is_grantable +FROM acl_expanded a +LEFT JOIN pg_roles r ON a.grantee_oid = r.oid +ORDER BY object_type, grantee, privilege_type; \ No newline at end of file diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index 8e5f00f8..9978a80c 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -909,6 +909,69 @@ func (q *Queries) GetConstraintsForSchema(ctx context.Context, dollar_1 sql.Null return items, nil } +const getDefaultPrivilegesForSchema = `-- name: GetDefaultPrivilegesForSchema :many +WITH acl_expanded AS ( + SELECT + d.defaclobjtype, + (aclexplode(d.defaclacl)).grantee AS grantee_oid, + (aclexplode(d.defaclacl)).privilege_type AS privilege_type, + (aclexplode(d.defaclacl)).is_grantable AS is_grantable + FROM pg_default_acl d + JOIN pg_namespace n ON d.defaclnamespace = n.oid + WHERE n.nspname = $1 +) +SELECT + CASE a.defaclobjtype + WHEN 'r' THEN 'TABLES' + WHEN 'S' THEN 'SEQUENCES' + WHEN 'f' THEN 'FUNCTIONS' + WHEN 'T' THEN 'TYPES' + WHEN 'n' THEN 'SCHEMAS' + END AS object_type, + COALESCE(r.rolname, 'PUBLIC') AS grantee, + a.privilege_type, + a.is_grantable +FROM acl_expanded a +LEFT JOIN pg_roles r ON a.grantee_oid = r.oid +ORDER BY object_type, grantee, privilege_type +` + +type GetDefaultPrivilegesForSchemaRow struct { + ObjectType sql.NullString `db:"object_type" json:"object_type"` + Grantee sql.NullString `db:"grantee" json:"grantee"` + PrivilegeType sql.NullString `db:"privilege_type" json:"privilege_type"` + IsGrantable sql.NullBool `db:"is_grantable" json:"is_grantable"` +} + +// GetDefaultPrivilegesForSchema retrieves default privileges for a specific schema +func (q *Queries) GetDefaultPrivilegesForSchema(ctx context.Context, dollar_1 sql.NullString) ([]GetDefaultPrivilegesForSchemaRow, error) { + rows, err := q.db.QueryContext(ctx, getDefaultPrivilegesForSchema, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetDefaultPrivilegesForSchemaRow + for rows.Next() { + var i GetDefaultPrivilegesForSchemaRow + if err := rows.Scan( + &i.ObjectType, + &i.Grantee, + &i.PrivilegeType, + &i.IsGrantable, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getDomainConstraints = `-- name: GetDomainConstraints :many SELECT n.nspname AS domain_schema, diff --git a/testdata/diff/privilege/add_function_privilege/diff.sql b/testdata/diff/privilege/add_function_privilege/diff.sql index 01e921a9..dc2c716d 100644 --- a/testdata/diff/privilege/add_function_privilege/diff.sql +++ b/testdata/diff/privilege/add_function_privilege/diff.sql @@ -1,3 +1 @@ -ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; - ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO api_user; diff --git a/testdata/diff/privilege/add_function_privilege/new.sql b/testdata/diff/privilege/add_function_privilege/new.sql index 1afa7df3..1c8fe7d1 100644 --- a/testdata/diff/privilege/add_function_privilege/new.sql +++ b/testdata/diff/privilege/add_function_privilege/new.sql @@ -1,6 +1,13 @@ --- Revoke default EXECUTE from PUBLIC, grant to specific role -ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; -ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO api_user; +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'api_user') THEN + CREATE ROLE api_user; + END IF; +END $$; + +-- Grant EXECUTE on future functions to api_user +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO api_user; CREATE FUNCTION get_version() RETURNS text AS $$ BEGIN diff --git a/testdata/diff/privilege/add_function_privilege/old.sql b/testdata/diff/privilege/add_function_privilege/old.sql index d85ffdc7..ae2a9e05 100644 --- a/testdata/diff/privilege/add_function_privilege/old.sql +++ b/testdata/diff/privilege/add_function_privilege/old.sql @@ -1,3 +1,11 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'api_user') THEN + CREATE ROLE api_user; + END IF; +END $$; + -- No default privileges configured CREATE FUNCTION get_version() RETURNS text AS $$ BEGIN diff --git a/testdata/diff/privilege/add_function_privilege/plan.json b/testdata/diff/privilege/add_function_privilege/plan.json new file mode 100644 index 00000000..60fcccfd --- /dev/null +++ b/testdata/diff/privilege/add_function_privilege/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "79f31cce98e39ab522ad82d49ce9b13d961df69903d34834960ee7c69a995fbf" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO api_user;", + "type": "default_privilege", + "operation": "create", + "path": "default_privileges.FUNCTIONS.api_user" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/add_function_privilege/plan.sql b/testdata/diff/privilege/add_function_privilege/plan.sql new file mode 100644 index 00000000..dc2c716d --- /dev/null +++ b/testdata/diff/privilege/add_function_privilege/plan.sql @@ -0,0 +1 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO api_user; diff --git a/testdata/diff/privilege/add_function_privilege/plan.txt b/testdata/diff/privilege/add_function_privilege/plan.txt new file mode 100644 index 00000000..ee2867a7 --- /dev/null +++ b/testdata/diff/privilege/add_function_privilege/plan.txt @@ -0,0 +1,8 @@ +Plan: 1 to add. + +Summary by type: + +DDL to be executed: +-------------------------------------------------- + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO api_user; diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/diff.sql b/testdata/diff/privilege/add_privilege_with_grant_option/diff.sql index 01ab0d14..e505eaca 100644 --- a/testdata/diff/privilege/add_privilege_with_grant_option/diff.sql +++ b/testdata/diff/privilege/add_privilege_with_grant_option/diff.sql @@ -1 +1 @@ -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT ON TABLES TO admin_user WITH GRANT OPTION; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, SELECT ON TABLES TO admin_user WITH GRANT OPTION; diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/new.sql b/testdata/diff/privilege/add_privilege_with_grant_option/new.sql index 66ada3f9..c50729ec 100644 --- a/testdata/diff/privilege/add_privilege_with_grant_option/new.sql +++ b/testdata/diff/privilege/add_privilege_with_grant_option/new.sql @@ -1,5 +1,13 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'admin_user') THEN + CREATE ROLE admin_user; + END IF; +END $$; + -- Grant with grant option - admin_user can grant to others -ALTER DEFAULT PRIVILEGES GRANT SELECT, INSERT ON TABLES TO admin_user WITH GRANT OPTION; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT ON TABLES TO admin_user WITH GRANT OPTION; CREATE TABLE users ( id SERIAL PRIMARY KEY, diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/old.sql b/testdata/diff/privilege/add_privilege_with_grant_option/old.sql index ef5533ed..5f327d44 100644 --- a/testdata/diff/privilege/add_privilege_with_grant_option/old.sql +++ b/testdata/diff/privilege/add_privilege_with_grant_option/old.sql @@ -1,3 +1,11 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'admin_user') THEN + CREATE ROLE admin_user; + END IF; +END $$; + -- No default privileges configured CREATE TABLE users ( id SERIAL PRIMARY KEY, diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/plan.json b/testdata/diff/privilege/add_privilege_with_grant_option/plan.json new file mode 100644 index 00000000..1cac8114 --- /dev/null +++ b/testdata/diff/privilege/add_privilege_with_grant_option/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "37c423c160146802a3387460f1ef35feb6829f6687a36c0162786bac2b4a6c89" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, SELECT ON TABLES TO admin_user WITH GRANT OPTION;", + "type": "default_privilege", + "operation": "create", + "path": "default_privileges.TABLES.admin_user" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/plan.sql b/testdata/diff/privilege/add_privilege_with_grant_option/plan.sql new file mode 100644 index 00000000..e505eaca --- /dev/null +++ b/testdata/diff/privilege/add_privilege_with_grant_option/plan.sql @@ -0,0 +1 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, SELECT ON TABLES TO admin_user WITH GRANT OPTION; diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/plan.txt b/testdata/diff/privilege/add_privilege_with_grant_option/plan.txt new file mode 100644 index 00000000..a012f23e --- /dev/null +++ b/testdata/diff/privilege/add_privilege_with_grant_option/plan.txt @@ -0,0 +1,8 @@ +Plan: 1 to add. + +Summary by type: + +DDL to be executed: +-------------------------------------------------- + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, SELECT ON TABLES TO admin_user WITH GRANT OPTION; diff --git a/testdata/diff/privilege/add_sequence_privilege/diff.sql b/testdata/diff/privilege/add_sequence_privilege/diff.sql index aa4f74d0..22f2a565 100644 --- a/testdata/diff/privilege/add_sequence_privilege/diff.sql +++ b/testdata/diff/privilege/add_sequence_privilege/diff.sql @@ -1 +1 @@ -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO app_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, USAGE ON SEQUENCES TO app_user; diff --git a/testdata/diff/privilege/add_sequence_privilege/new.sql b/testdata/diff/privilege/add_sequence_privilege/new.sql index 8f96a931..95af7c56 100644 --- a/testdata/diff/privilege/add_sequence_privilege/new.sql +++ b/testdata/diff/privilege/add_sequence_privilege/new.sql @@ -1,4 +1,12 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user; + END IF; +END $$; + -- Grant USAGE, SELECT on future sequences to app_user -ALTER DEFAULT PRIVILEGES GRANT USAGE, SELECT ON SEQUENCES TO app_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO app_user; CREATE SEQUENCE order_seq START 1; diff --git a/testdata/diff/privilege/add_sequence_privilege/old.sql b/testdata/diff/privilege/add_sequence_privilege/old.sql index 22d1f823..3a279d66 100644 --- a/testdata/diff/privilege/add_sequence_privilege/old.sql +++ b/testdata/diff/privilege/add_sequence_privilege/old.sql @@ -1,2 +1,10 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user; + END IF; +END $$; + -- No default privileges configured CREATE SEQUENCE order_seq START 1; diff --git a/testdata/diff/privilege/add_sequence_privilege/plan.json b/testdata/diff/privilege/add_sequence_privilege/plan.json new file mode 100644 index 00000000..d681126d --- /dev/null +++ b/testdata/diff/privilege/add_sequence_privilege/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "491c72f4c0694d28b6e22b9cf12849a32dd8b9ed4b7b5da878c5eaa520bf0ca3" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, USAGE ON SEQUENCES TO app_user;", + "type": "default_privilege", + "operation": "create", + "path": "default_privileges.SEQUENCES.app_user" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/add_sequence_privilege/plan.sql b/testdata/diff/privilege/add_sequence_privilege/plan.sql new file mode 100644 index 00000000..22f2a565 --- /dev/null +++ b/testdata/diff/privilege/add_sequence_privilege/plan.sql @@ -0,0 +1 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, USAGE ON SEQUENCES TO app_user; diff --git a/testdata/diff/privilege/add_sequence_privilege/plan.txt b/testdata/diff/privilege/add_sequence_privilege/plan.txt new file mode 100644 index 00000000..78dfb8f8 --- /dev/null +++ b/testdata/diff/privilege/add_sequence_privilege/plan.txt @@ -0,0 +1,8 @@ +Plan: 1 to add. + +Summary by type: + +DDL to be executed: +-------------------------------------------------- + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, USAGE ON SEQUENCES TO app_user; diff --git a/testdata/diff/privilege/add_table_privilege/new.sql b/testdata/diff/privilege/add_table_privilege/new.sql index d59923dd..758c87b2 100644 --- a/testdata/diff/privilege/add_table_privilege/new.sql +++ b/testdata/diff/privilege/add_table_privilege/new.sql @@ -1,8 +1,16 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user; + END IF; +END $$; + -- Grant SELECT on future tables to PUBLIC -ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO PUBLIC; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO PUBLIC; -- Grant INSERT, UPDATE to app_user role -ALTER DEFAULT PRIVILEGES GRANT INSERT, UPDATE ON TABLES TO app_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user; CREATE TABLE users ( id SERIAL PRIMARY KEY, diff --git a/testdata/diff/privilege/add_table_privilege/old.sql b/testdata/diff/privilege/add_table_privilege/old.sql index ef5533ed..fbf76773 100644 --- a/testdata/diff/privilege/add_table_privilege/old.sql +++ b/testdata/diff/privilege/add_table_privilege/old.sql @@ -1,3 +1,11 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user; + END IF; +END $$; + -- No default privileges configured CREATE TABLE users ( id SERIAL PRIMARY KEY, diff --git a/testdata/diff/privilege/add_table_privilege/plan.json b/testdata/diff/privilege/add_table_privilege/plan.json new file mode 100644 index 00000000..52996275 --- /dev/null +++ b/testdata/diff/privilege/add_table_privilege/plan.json @@ -0,0 +1,26 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "37c423c160146802a3387460f1ef35feb6829f6687a36c0162786bac2b4a6c89" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO PUBLIC;", + "type": "default_privilege", + "operation": "create", + "path": "default_privileges.TABLES.PUBLIC" + }, + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user;", + "type": "default_privilege", + "operation": "create", + "path": "default_privileges.TABLES.app_user" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/add_table_privilege/plan.sql b/testdata/diff/privilege/add_table_privilege/plan.sql new file mode 100644 index 00000000..ea1cc979 --- /dev/null +++ b/testdata/diff/privilege/add_table_privilege/plan.sql @@ -0,0 +1,3 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO PUBLIC; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user; diff --git a/testdata/diff/privilege/add_table_privilege/plan.txt b/testdata/diff/privilege/add_table_privilege/plan.txt new file mode 100644 index 00000000..19eef092 --- /dev/null +++ b/testdata/diff/privilege/add_table_privilege/plan.txt @@ -0,0 +1,10 @@ +Plan: 2 to add. + +Summary by type: + +DDL to be executed: +-------------------------------------------------- + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO PUBLIC; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user; diff --git a/testdata/diff/privilege/add_type_privilege/new.sql b/testdata/diff/privilege/add_type_privilege/new.sql index e655fbaf..e1c1c183 100644 --- a/testdata/diff/privilege/add_type_privilege/new.sql +++ b/testdata/diff/privilege/add_type_privilege/new.sql @@ -1,4 +1,12 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user; + END IF; +END $$; + -- Grant USAGE on future types to app_user -ALTER DEFAULT PRIVILEGES GRANT USAGE ON TYPES TO app_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON TYPES TO app_user; CREATE TYPE status AS ENUM ('pending', 'active', 'inactive'); diff --git a/testdata/diff/privilege/add_type_privilege/old.sql b/testdata/diff/privilege/add_type_privilege/old.sql index af881d78..6a391296 100644 --- a/testdata/diff/privilege/add_type_privilege/old.sql +++ b/testdata/diff/privilege/add_type_privilege/old.sql @@ -1,2 +1,10 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user; + END IF; +END $$; + -- No default privileges configured CREATE TYPE status AS ENUM ('pending', 'active', 'inactive'); diff --git a/testdata/diff/privilege/add_type_privilege/plan.json b/testdata/diff/privilege/add_type_privilege/plan.json new file mode 100644 index 00000000..551fdb0b --- /dev/null +++ b/testdata/diff/privilege/add_type_privilege/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "d587e8d0c5c5a20144cdd6529a6074ab78689cc88086e8cb539d5ef3a7856fac" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON TYPES TO app_user;", + "type": "default_privilege", + "operation": "create", + "path": "default_privileges.TYPES.app_user" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/add_type_privilege/plan.sql b/testdata/diff/privilege/add_type_privilege/plan.sql new file mode 100644 index 00000000..6d2068f5 --- /dev/null +++ b/testdata/diff/privilege/add_type_privilege/plan.sql @@ -0,0 +1 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON TYPES TO app_user; diff --git a/testdata/diff/privilege/add_type_privilege/plan.txt b/testdata/diff/privilege/add_type_privilege/plan.txt new file mode 100644 index 00000000..6bf69fe9 --- /dev/null +++ b/testdata/diff/privilege/add_type_privilege/plan.txt @@ -0,0 +1,8 @@ +Plan: 1 to add. + +Summary by type: + +DDL to be executed: +-------------------------------------------------- + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON TYPES TO app_user; diff --git a/testdata/diff/privilege/alter_privilege/diff.sql b/testdata/diff/privilege/alter_privilege/diff.sql index 697d8266..652a2626 100644 --- a/testdata/diff/privilege/alter_privilege/diff.sql +++ b/testdata/diff/privilege/alter_privilege/diff.sql @@ -1,3 +1,3 @@ -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user; - ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE USAGE ON SEQUENCES FROM app_user; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user; diff --git a/testdata/diff/privilege/alter_privilege/new.sql b/testdata/diff/privilege/alter_privilege/new.sql index 1d5a62ee..67bf2ca0 100644 --- a/testdata/diff/privilege/alter_privilege/new.sql +++ b/testdata/diff/privilege/alter_privilege/new.sql @@ -1,5 +1,13 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user; + END IF; +END $$; + -- Expand table privileges, remove sequence privileges -ALTER DEFAULT PRIVILEGES GRANT SELECT, INSERT, UPDATE ON TABLES TO app_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE ON TABLES TO app_user; CREATE TABLE users ( id SERIAL PRIMARY KEY, diff --git a/testdata/diff/privilege/alter_privilege/old.sql b/testdata/diff/privilege/alter_privilege/old.sql index 614b5baa..8931f554 100644 --- a/testdata/diff/privilege/alter_privilege/old.sql +++ b/testdata/diff/privilege/alter_privilege/old.sql @@ -1,6 +1,14 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user; + END IF; +END $$; + -- Initial default privileges -ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO app_user; -ALTER DEFAULT PRIVILEGES GRANT USAGE ON SEQUENCES TO app_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON SEQUENCES TO app_user; CREATE TABLE users ( id SERIAL PRIMARY KEY, diff --git a/testdata/diff/privilege/alter_privilege/plan.json b/testdata/diff/privilege/alter_privilege/plan.json new file mode 100644 index 00000000..7fb23f5e --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/plan.json @@ -0,0 +1,26 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "db1d094ecca08a63dff4c717567ebfdfb8fda90d23d04a4d6344fa484ce878c7" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE USAGE ON SEQUENCES FROM app_user;", + "type": "default_privilege", + "operation": "drop", + "path": "default_privileges.SEQUENCES.app_user" + }, + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user;", + "type": "default_privilege", + "operation": "alter", + "path": "default_privileges.TABLES.app_user" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/alter_privilege/plan.sql b/testdata/diff/privilege/alter_privilege/plan.sql new file mode 100644 index 00000000..652a2626 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/plan.sql @@ -0,0 +1,3 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE USAGE ON SEQUENCES FROM app_user; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user; diff --git a/testdata/diff/privilege/alter_privilege/plan.txt b/testdata/diff/privilege/alter_privilege/plan.txt new file mode 100644 index 00000000..95ac8c26 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/plan.txt @@ -0,0 +1,10 @@ +Plan: 1 to modify, 1 to drop. + +Summary by type: + +DDL to be executed: +-------------------------------------------------- + +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE USAGE ON SEQUENCES FROM app_user; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user; diff --git a/testdata/diff/privilege/drop_privilege/diff.sql b/testdata/diff/privilege/drop_privilege/diff.sql index 4500c247..e843a2b9 100644 --- a/testdata/diff/privilege/drop_privilege/diff.sql +++ b/testdata/diff/privilege/drop_privilege/diff.sql @@ -1,3 +1,3 @@ -ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT ON TABLES FROM readonly_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE DELETE, INSERT, UPDATE ON TABLES FROM app_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE INSERT, UPDATE, DELETE ON TABLES FROM app_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT ON TABLES FROM readonly_user; diff --git a/testdata/diff/privilege/drop_privilege/new.sql b/testdata/diff/privilege/drop_privilege/new.sql index 963259b6..518e8554 100644 --- a/testdata/diff/privilege/drop_privilege/new.sql +++ b/testdata/diff/privilege/drop_privilege/new.sql @@ -1,3 +1,14 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'readonly_user') THEN + CREATE ROLE readonly_user; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user; + END IF; +END $$; + -- Remove all default privileges CREATE TABLE users ( id SERIAL PRIMARY KEY, diff --git a/testdata/diff/privilege/drop_privilege/old.sql b/testdata/diff/privilege/drop_privilege/old.sql index 295f5800..95a6bf64 100644 --- a/testdata/diff/privilege/drop_privilege/old.sql +++ b/testdata/diff/privilege/drop_privilege/old.sql @@ -1,6 +1,17 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'readonly_user') THEN + CREATE ROLE readonly_user; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user; + END IF; +END $$; + -- Default privileges configured -ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO readonly_user; -ALTER DEFAULT PRIVILEGES GRANT INSERT, UPDATE, DELETE ON TABLES TO app_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE, DELETE ON TABLES TO app_user; CREATE TABLE users ( id SERIAL PRIMARY KEY, diff --git a/testdata/diff/privilege/drop_privilege/plan.json b/testdata/diff/privilege/drop_privilege/plan.json new file mode 100644 index 00000000..1c79bbe0 --- /dev/null +++ b/testdata/diff/privilege/drop_privilege/plan.json @@ -0,0 +1,26 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "541e93c1f3f371b824b35de68daac5e6dd2dab6b88924d2c5100430c28d7955a" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE DELETE, INSERT, UPDATE ON TABLES FROM app_user;", + "type": "default_privilege", + "operation": "drop", + "path": "default_privileges.TABLES.app_user" + }, + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT ON TABLES FROM readonly_user;", + "type": "default_privilege", + "operation": "drop", + "path": "default_privileges.TABLES.readonly_user" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/drop_privilege/plan.sql b/testdata/diff/privilege/drop_privilege/plan.sql new file mode 100644 index 00000000..e843a2b9 --- /dev/null +++ b/testdata/diff/privilege/drop_privilege/plan.sql @@ -0,0 +1,3 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE DELETE, INSERT, UPDATE ON TABLES FROM app_user; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT ON TABLES FROM readonly_user; diff --git a/testdata/diff/privilege/drop_privilege/plan.txt b/testdata/diff/privilege/drop_privilege/plan.txt new file mode 100644 index 00000000..a1abc8a3 --- /dev/null +++ b/testdata/diff/privilege/drop_privilege/plan.txt @@ -0,0 +1,10 @@ +Plan: 2 to drop. + +Summary by type: + +DDL to be executed: +-------------------------------------------------- + +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE DELETE, INSERT, UPDATE ON TABLES FROM app_user; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT ON TABLES FROM readonly_user; From c0a674bf70f40711fbe10bcb4aa59b8ebdb856c8 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Mon, 5 Jan 2026 07:55:56 -0800 Subject: [PATCH 3/5] fix: handle simultaneous privilege and grant option changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sort privileges at extraction time in inspector.go for deterministic IR - Fix bug where unchanged privileges didn't get their grant option updated when both privileges and WithGrantOption changed simultaneously - Add test case for ALTER DEFAULT PRIVILEGES with combined changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/diff/default_privilege.go | 42 +++++++++++-------- ir/inspector.go | 2 + .../alter_privilege_and_grant_option/diff.sql | 5 +++ .../alter_privilege_and_grant_option/new.sql | 15 +++++++ .../alter_privilege_and_grant_option/old.sql | 15 +++++++ .../plan.json | 32 ++++++++++++++ .../alter_privilege_and_grant_option/plan.sql | 5 +++ .../alter_privilege_and_grant_option/plan.txt | 12 ++++++ 8 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 testdata/diff/privilege/alter_privilege_and_grant_option/diff.sql create mode 100644 testdata/diff/privilege/alter_privilege_and_grant_option/new.sql create mode 100644 testdata/diff/privilege/alter_privilege_and_grant_option/old.sql create mode 100644 testdata/diff/privilege/alter_privilege_and_grant_option/plan.json create mode 100644 testdata/diff/privilege/alter_privilege_and_grant_option/plan.sql create mode 100644 testdata/diff/privilege/alter_privilege_and_grant_option/plan.txt diff --git a/internal/diff/default_privilege.go b/internal/diff/default_privilege.go index ba3c0784..0ff36bfa 100644 --- a/internal/diff/default_privilege.go +++ b/internal/diff/default_privilege.go @@ -161,25 +161,33 @@ func (d *defaultPrivilegeDiff) generateAlterDefaultPrivilegeStatements(targetSch statements = append(statements, sql+";") } - // Handle WITH GRANT OPTION changes (if privileges are same but grant option changed) - if len(toRevoke) == 0 && len(toGrant) == 0 && d.Old.WithGrantOption != d.New.WithGrantOption { - // Need to revoke and re-grant with new option - sortedPrivs := make([]string, len(d.New.Privileges)) - copy(sortedPrivs, d.New.Privileges) - sort.Strings(sortedPrivs) - privStr := strings.Join(sortedPrivs, ", ") - - // Revoke first - statements = append(statements, fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA %s REVOKE %s ON %s FROM %s;", - quotedSchema, privStr, d.New.ObjectType, grantee)) + // Handle WITH GRANT OPTION changes for unchanged privileges + // If grant option changed, we need to revoke and re-grant privileges that exist in both old and new + if d.Old.WithGrantOption != d.New.WithGrantOption { + // Find unchanged privileges (in both old and new) + var unchanged []string + for p := range oldPrivSet { + if newPrivSet[p] { + unchanged = append(unchanged, p) + } + } - // Then grant with correct option - sql := fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA %s GRANT %s ON %s TO %s", - quotedSchema, privStr, d.New.ObjectType, grantee) - if d.New.WithGrantOption { - sql += " WITH GRANT OPTION" + if len(unchanged) > 0 { + sort.Strings(unchanged) + unchangedStr := strings.Join(unchanged, ", ") + + // Revoke unchanged privileges first + statements = append(statements, fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA %s REVOKE %s ON %s FROM %s;", + quotedSchema, unchangedStr, d.New.ObjectType, grantee)) + + // Re-grant with correct option + sql := fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA %s GRANT %s ON %s TO %s", + quotedSchema, unchangedStr, d.New.ObjectType, grantee) + if d.New.WithGrantOption { + sql += " WITH GRANT OPTION" + } + statements = append(statements, sql+";") } - statements = append(statements, sql+";") } return statements diff --git a/ir/inspector.go b/ir/inspector.go index 04552d9c..5ab8986d 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -1899,6 +1899,8 @@ func (i *Inspector) buildDefaultPrivileges(ctx context.Context, schema *IR, targ // Convert to DefaultPrivilege structs var defaultPrivileges []*DefaultPrivilege for key, privs := range grouped { + // Sort privileges for deterministic IR output + sort.Strings(privs) dp := &DefaultPrivilege{ ObjectType: DefaultPrivilegeObjectType(key.ObjectType), Grantee: key.Grantee, diff --git a/testdata/diff/privilege/alter_privilege_and_grant_option/diff.sql b/testdata/diff/privilege/alter_privilege_and_grant_option/diff.sql new file mode 100644 index 00000000..2dc364c1 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/diff.sql @@ -0,0 +1,5 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user WITH GRANT OPTION; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT ON TABLES FROM app_user; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user WITH GRANT OPTION; diff --git a/testdata/diff/privilege/alter_privilege_and_grant_option/new.sql b/testdata/diff/privilege/alter_privilege_and_grant_option/new.sql new file mode 100644 index 00000000..e2f42f73 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/new.sql @@ -0,0 +1,15 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user; + END IF; +END $$; + +-- Changed default privileges: SELECT, INSERT, UPDATE with grant option +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE ON TABLES TO app_user WITH GRANT OPTION; + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL +); diff --git a/testdata/diff/privilege/alter_privilege_and_grant_option/old.sql b/testdata/diff/privilege/alter_privilege_and_grant_option/old.sql new file mode 100644 index 00000000..e8d70987 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/old.sql @@ -0,0 +1,15 @@ +-- Create roles for testing +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user; + END IF; +END $$; + +-- Initial default privileges: SELECT only, no grant option +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user; + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL +); diff --git a/testdata/diff/privilege/alter_privilege_and_grant_option/plan.json b/testdata/diff/privilege/alter_privilege_and_grant_option/plan.json new file mode 100644 index 00000000..ceb109e4 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/plan.json @@ -0,0 +1,32 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "3fbf3796069c82fae69d03a6d3036009311a7f893bfea04a504c534e3f1451b1" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user WITH GRANT OPTION;", + "type": "default_privilege", + "operation": "alter", + "path": "default_privileges.TABLES.app_user" + }, + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT ON TABLES FROM app_user;", + "type": "default_privilege", + "operation": "alter", + "path": "default_privileges.TABLES.app_user" + }, + { + "sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user WITH GRANT OPTION;", + "type": "default_privilege", + "operation": "alter", + "path": "default_privileges.TABLES.app_user" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/alter_privilege_and_grant_option/plan.sql b/testdata/diff/privilege/alter_privilege_and_grant_option/plan.sql new file mode 100644 index 00000000..2dc364c1 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/plan.sql @@ -0,0 +1,5 @@ +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user WITH GRANT OPTION; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT ON TABLES FROM app_user; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user WITH GRANT OPTION; diff --git a/testdata/diff/privilege/alter_privilege_and_grant_option/plan.txt b/testdata/diff/privilege/alter_privilege_and_grant_option/plan.txt new file mode 100644 index 00000000..bdefeaf2 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/plan.txt @@ -0,0 +1,12 @@ +Plan: 3 to modify. + +Summary by type: + +DDL to be executed: +-------------------------------------------------- + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user WITH GRANT OPTION; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT ON TABLES FROM app_user; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user WITH GRANT OPTION; From cfe6d08dfb8a04f211f5799e33ac5cb6a26896c7 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Mon, 5 Jan 2026 08:56:00 -0800 Subject: [PATCH 4/5] fix: show default privileges in plan summary by type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add TypeDefaultPrivilege constant to plan.go - Add default privileges to getObjectOrder() for proper display order - Normalize underscore-separated type names to space-separated format to match Type constants (e.g., "default_privileges" → "default privileges") - Update test expected files with correct summary output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/plan/plan.go | 10 ++++++++-- .../diff/privilege/add_function_privilege/plan.txt | 4 ++++ .../privilege/add_privilege_with_grant_option/plan.txt | 4 ++++ .../diff/privilege/add_sequence_privilege/plan.txt | 4 ++++ testdata/diff/privilege/add_table_privilege/plan.txt | 5 +++++ testdata/diff/privilege/add_type_privilege/plan.txt | 4 ++++ testdata/diff/privilege/alter_privilege/plan.txt | 5 +++++ .../alter_privilege_and_grant_option/plan.txt | 6 ++++++ testdata/diff/privilege/drop_privilege/plan.txt | 5 +++++ 9 files changed, 45 insertions(+), 2 deletions(-) diff --git a/internal/plan/plan.go b/internal/plan/plan.go index 208d45aa..e3155b21 100644 --- a/internal/plan/plan.go +++ b/internal/plan/plan.go @@ -100,6 +100,7 @@ const ( TypePolicy Type = "policies" TypeColumn Type = "columns" TypeRLS Type = "rls" + TypeDefaultPrivilege Type = "default privileges" ) // SQLFormat represents the different output formats for SQL generation @@ -116,6 +117,7 @@ const ( func getObjectOrder() []Type { return []Type{ TypeSchema, + TypeDefaultPrivilege, TypeType, TypeFunction, TypeProcedure, @@ -637,7 +639,9 @@ func (p *Plan) calculateSummaryFromSteps() PlanSummary { // Count non-table/non-view/non-materialized-view operations (each operation counted individually) for objType, operations := range nonTableOperations { - stats := summary.ByType[objType] + // Normalize object type to match the Type constants (replace underscores with spaces) + normalizedObjType := strings.ReplaceAll(objType, "_", " ") + stats := summary.ByType[normalizedObjType] for _, operation := range operations { switch operation { case "create": @@ -651,7 +655,7 @@ func (p *Plan) calculateSummaryFromSteps() PlanSummary { summary.Destroy++ } } - summary.ByType[objType] = stats + summary.ByType[normalizedObjType] = stats } summary.Total = summary.Add + summary.Change + summary.Destroy @@ -1073,6 +1077,8 @@ func (p *Plan) writeNonTableChanges(summary *strings.Builder, objType string, c if !strings.HasSuffix(stepObjTypeStr, "s") { stepObjTypeStr += "s" } + // Normalize underscores to spaces to match Type constants + stepObjTypeStr = strings.ReplaceAll(stepObjTypeStr, "_", " ") if stepObjTypeStr == objType { changes = append(changes, struct { diff --git a/testdata/diff/privilege/add_function_privilege/plan.txt b/testdata/diff/privilege/add_function_privilege/plan.txt index ee2867a7..df3c1817 100644 --- a/testdata/diff/privilege/add_function_privilege/plan.txt +++ b/testdata/diff/privilege/add_function_privilege/plan.txt @@ -1,6 +1,10 @@ Plan: 1 to add. Summary by type: + default privileges: 1 to add + +Default privileges: + + api_user DDL to be executed: -------------------------------------------------- diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/plan.txt b/testdata/diff/privilege/add_privilege_with_grant_option/plan.txt index a012f23e..2d711324 100644 --- a/testdata/diff/privilege/add_privilege_with_grant_option/plan.txt +++ b/testdata/diff/privilege/add_privilege_with_grant_option/plan.txt @@ -1,6 +1,10 @@ Plan: 1 to add. Summary by type: + default privileges: 1 to add + +Default privileges: + + admin_user DDL to be executed: -------------------------------------------------- diff --git a/testdata/diff/privilege/add_sequence_privilege/plan.txt b/testdata/diff/privilege/add_sequence_privilege/plan.txt index 78dfb8f8..60293988 100644 --- a/testdata/diff/privilege/add_sequence_privilege/plan.txt +++ b/testdata/diff/privilege/add_sequence_privilege/plan.txt @@ -1,6 +1,10 @@ Plan: 1 to add. Summary by type: + default privileges: 1 to add + +Default privileges: + + app_user DDL to be executed: -------------------------------------------------- diff --git a/testdata/diff/privilege/add_table_privilege/plan.txt b/testdata/diff/privilege/add_table_privilege/plan.txt index 19eef092..ebc05f17 100644 --- a/testdata/diff/privilege/add_table_privilege/plan.txt +++ b/testdata/diff/privilege/add_table_privilege/plan.txt @@ -1,6 +1,11 @@ Plan: 2 to add. Summary by type: + default privileges: 2 to add + +Default privileges: + + PUBLIC + + app_user DDL to be executed: -------------------------------------------------- diff --git a/testdata/diff/privilege/add_type_privilege/plan.txt b/testdata/diff/privilege/add_type_privilege/plan.txt index 6bf69fe9..6badcaec 100644 --- a/testdata/diff/privilege/add_type_privilege/plan.txt +++ b/testdata/diff/privilege/add_type_privilege/plan.txt @@ -1,6 +1,10 @@ Plan: 1 to add. Summary by type: + default privileges: 1 to add + +Default privileges: + + app_user DDL to be executed: -------------------------------------------------- diff --git a/testdata/diff/privilege/alter_privilege/plan.txt b/testdata/diff/privilege/alter_privilege/plan.txt index 95ac8c26..3e476230 100644 --- a/testdata/diff/privilege/alter_privilege/plan.txt +++ b/testdata/diff/privilege/alter_privilege/plan.txt @@ -1,6 +1,11 @@ Plan: 1 to modify, 1 to drop. Summary by type: + default privileges: 1 to modify, 1 to drop + +Default privileges: + - app_user + ~ app_user DDL to be executed: -------------------------------------------------- diff --git a/testdata/diff/privilege/alter_privilege_and_grant_option/plan.txt b/testdata/diff/privilege/alter_privilege_and_grant_option/plan.txt index bdefeaf2..81e1cf6c 100644 --- a/testdata/diff/privilege/alter_privilege_and_grant_option/plan.txt +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/plan.txt @@ -1,6 +1,12 @@ Plan: 3 to modify. Summary by type: + default privileges: 3 to modify + +Default privileges: + ~ app_user + ~ app_user + ~ app_user DDL to be executed: -------------------------------------------------- diff --git a/testdata/diff/privilege/drop_privilege/plan.txt b/testdata/diff/privilege/drop_privilege/plan.txt index a1abc8a3..d1a66234 100644 --- a/testdata/diff/privilege/drop_privilege/plan.txt +++ b/testdata/diff/privilege/drop_privilege/plan.txt @@ -1,6 +1,11 @@ Plan: 2 to drop. Summary by type: + default privileges: 2 to drop + +Default privileges: + - app_user + - readonly_user DDL to be executed: -------------------------------------------------- From 981b37d58ee029a52b8d0b81dc0acbe6016fb5fb Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Mon, 5 Jan 2026 09:12:03 -0800 Subject: [PATCH 5/5] test: simplify privilege test cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove unnecessary database objects (tables, functions, sequences, types) from privilege test cases. Default privileges only affect future objects, so existing objects were not needed for testing ALTER DEFAULT PRIVILEGES. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- testdata/diff/privilege/add_function_privilege/new.sql | 6 ------ testdata/diff/privilege/add_function_privilege/old.sql | 5 ----- testdata/diff/privilege/add_function_privilege/plan.json | 2 +- .../diff/privilege/add_privilege_with_grant_option/new.sql | 5 ----- .../diff/privilege/add_privilege_with_grant_option/old.sql | 4 ---- .../privilege/add_privilege_with_grant_option/plan.json | 2 +- testdata/diff/privilege/add_sequence_privilege/new.sql | 2 -- testdata/diff/privilege/add_sequence_privilege/old.sql | 1 - testdata/diff/privilege/add_sequence_privilege/plan.json | 2 +- testdata/diff/privilege/add_table_privilege/new.sql | 5 ----- testdata/diff/privilege/add_table_privilege/old.sql | 4 ---- testdata/diff/privilege/add_table_privilege/plan.json | 2 +- testdata/diff/privilege/add_type_privilege/new.sql | 2 -- testdata/diff/privilege/add_type_privilege/old.sql | 1 - testdata/diff/privilege/add_type_privilege/plan.json | 2 +- testdata/diff/privilege/alter_privilege/new.sql | 5 ----- testdata/diff/privilege/alter_privilege/old.sql | 5 ----- testdata/diff/privilege/alter_privilege/plan.json | 2 +- .../diff/privilege/alter_privilege_and_grant_option/new.sql | 5 ----- .../diff/privilege/alter_privilege_and_grant_option/old.sql | 5 ----- .../privilege/alter_privilege_and_grant_option/plan.json | 2 +- testdata/diff/privilege/drop_privilege/new.sql | 4 ---- testdata/diff/privilege/drop_privilege/old.sql | 5 ----- testdata/diff/privilege/drop_privilege/plan.json | 2 +- 24 files changed, 8 insertions(+), 72 deletions(-) diff --git a/testdata/diff/privilege/add_function_privilege/new.sql b/testdata/diff/privilege/add_function_privilege/new.sql index 1c8fe7d1..e7a0887f 100644 --- a/testdata/diff/privilege/add_function_privilege/new.sql +++ b/testdata/diff/privilege/add_function_privilege/new.sql @@ -8,9 +8,3 @@ END $$; -- Grant EXECUTE on future functions to api_user ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO api_user; - -CREATE FUNCTION get_version() RETURNS text AS $$ -BEGIN - RETURN '1.0.0'; -END; -$$ LANGUAGE plpgsql; diff --git a/testdata/diff/privilege/add_function_privilege/old.sql b/testdata/diff/privilege/add_function_privilege/old.sql index ae2a9e05..60b2e0af 100644 --- a/testdata/diff/privilege/add_function_privilege/old.sql +++ b/testdata/diff/privilege/add_function_privilege/old.sql @@ -7,8 +7,3 @@ BEGIN END $$; -- No default privileges configured -CREATE FUNCTION get_version() RETURNS text AS $$ -BEGIN - RETURN '1.0.0'; -END; -$$ LANGUAGE plpgsql; diff --git a/testdata/diff/privilege/add_function_privilege/plan.json b/testdata/diff/privilege/add_function_privilege/plan.json index 60fcccfd..f3cf5e25 100644 --- a/testdata/diff/privilege/add_function_privilege/plan.json +++ b/testdata/diff/privilege/add_function_privilege/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.5.1", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "79f31cce98e39ab522ad82d49ce9b13d961df69903d34834960ee7c69a995fbf" + "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" }, "groups": [ { diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/new.sql b/testdata/diff/privilege/add_privilege_with_grant_option/new.sql index c50729ec..e6d00a4a 100644 --- a/testdata/diff/privilege/add_privilege_with_grant_option/new.sql +++ b/testdata/diff/privilege/add_privilege_with_grant_option/new.sql @@ -8,8 +8,3 @@ END $$; -- Grant with grant option - admin_user can grant to others ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT ON TABLES TO admin_user WITH GRANT OPTION; - -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL -); diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/old.sql b/testdata/diff/privilege/add_privilege_with_grant_option/old.sql index 5f327d44..5c0cbe3a 100644 --- a/testdata/diff/privilege/add_privilege_with_grant_option/old.sql +++ b/testdata/diff/privilege/add_privilege_with_grant_option/old.sql @@ -7,7 +7,3 @@ BEGIN END $$; -- No default privileges configured -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL -); diff --git a/testdata/diff/privilege/add_privilege_with_grant_option/plan.json b/testdata/diff/privilege/add_privilege_with_grant_option/plan.json index 1cac8114..d8bf3d4e 100644 --- a/testdata/diff/privilege/add_privilege_with_grant_option/plan.json +++ b/testdata/diff/privilege/add_privilege_with_grant_option/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.5.1", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "37c423c160146802a3387460f1ef35feb6829f6687a36c0162786bac2b4a6c89" + "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" }, "groups": [ { diff --git a/testdata/diff/privilege/add_sequence_privilege/new.sql b/testdata/diff/privilege/add_sequence_privilege/new.sql index 95af7c56..8fecfabe 100644 --- a/testdata/diff/privilege/add_sequence_privilege/new.sql +++ b/testdata/diff/privilege/add_sequence_privilege/new.sql @@ -8,5 +8,3 @@ END $$; -- Grant USAGE, SELECT on future sequences to app_user ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO app_user; - -CREATE SEQUENCE order_seq START 1; diff --git a/testdata/diff/privilege/add_sequence_privilege/old.sql b/testdata/diff/privilege/add_sequence_privilege/old.sql index 3a279d66..81ec6e31 100644 --- a/testdata/diff/privilege/add_sequence_privilege/old.sql +++ b/testdata/diff/privilege/add_sequence_privilege/old.sql @@ -7,4 +7,3 @@ BEGIN END $$; -- No default privileges configured -CREATE SEQUENCE order_seq START 1; diff --git a/testdata/diff/privilege/add_sequence_privilege/plan.json b/testdata/diff/privilege/add_sequence_privilege/plan.json index d681126d..ba61176a 100644 --- a/testdata/diff/privilege/add_sequence_privilege/plan.json +++ b/testdata/diff/privilege/add_sequence_privilege/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.5.1", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "491c72f4c0694d28b6e22b9cf12849a32dd8b9ed4b7b5da878c5eaa520bf0ca3" + "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" }, "groups": [ { diff --git a/testdata/diff/privilege/add_table_privilege/new.sql b/testdata/diff/privilege/add_table_privilege/new.sql index 758c87b2..025d4e72 100644 --- a/testdata/diff/privilege/add_table_privilege/new.sql +++ b/testdata/diff/privilege/add_table_privilege/new.sql @@ -11,8 +11,3 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO PUBLIC; -- Grant INSERT, UPDATE to app_user role ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user; - -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL -); diff --git a/testdata/diff/privilege/add_table_privilege/old.sql b/testdata/diff/privilege/add_table_privilege/old.sql index fbf76773..81ec6e31 100644 --- a/testdata/diff/privilege/add_table_privilege/old.sql +++ b/testdata/diff/privilege/add_table_privilege/old.sql @@ -7,7 +7,3 @@ BEGIN END $$; -- No default privileges configured -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL -); diff --git a/testdata/diff/privilege/add_table_privilege/plan.json b/testdata/diff/privilege/add_table_privilege/plan.json index 52996275..d21adf38 100644 --- a/testdata/diff/privilege/add_table_privilege/plan.json +++ b/testdata/diff/privilege/add_table_privilege/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.5.1", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "37c423c160146802a3387460f1ef35feb6829f6687a36c0162786bac2b4a6c89" + "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" }, "groups": [ { diff --git a/testdata/diff/privilege/add_type_privilege/new.sql b/testdata/diff/privilege/add_type_privilege/new.sql index e1c1c183..c900e237 100644 --- a/testdata/diff/privilege/add_type_privilege/new.sql +++ b/testdata/diff/privilege/add_type_privilege/new.sql @@ -8,5 +8,3 @@ END $$; -- Grant USAGE on future types to app_user ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON TYPES TO app_user; - -CREATE TYPE status AS ENUM ('pending', 'active', 'inactive'); diff --git a/testdata/diff/privilege/add_type_privilege/old.sql b/testdata/diff/privilege/add_type_privilege/old.sql index 6a391296..81ec6e31 100644 --- a/testdata/diff/privilege/add_type_privilege/old.sql +++ b/testdata/diff/privilege/add_type_privilege/old.sql @@ -7,4 +7,3 @@ BEGIN END $$; -- No default privileges configured -CREATE TYPE status AS ENUM ('pending', 'active', 'inactive'); diff --git a/testdata/diff/privilege/add_type_privilege/plan.json b/testdata/diff/privilege/add_type_privilege/plan.json index 551fdb0b..51f2d16b 100644 --- a/testdata/diff/privilege/add_type_privilege/plan.json +++ b/testdata/diff/privilege/add_type_privilege/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.5.1", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "d587e8d0c5c5a20144cdd6529a6074ab78689cc88086e8cb539d5ef3a7856fac" + "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" }, "groups": [ { diff --git a/testdata/diff/privilege/alter_privilege/new.sql b/testdata/diff/privilege/alter_privilege/new.sql index 67bf2ca0..9798bb85 100644 --- a/testdata/diff/privilege/alter_privilege/new.sql +++ b/testdata/diff/privilege/alter_privilege/new.sql @@ -8,8 +8,3 @@ END $$; -- Expand table privileges, remove sequence privileges ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE ON TABLES TO app_user; - -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL -); diff --git a/testdata/diff/privilege/alter_privilege/old.sql b/testdata/diff/privilege/alter_privilege/old.sql index 8931f554..9e6a210e 100644 --- a/testdata/diff/privilege/alter_privilege/old.sql +++ b/testdata/diff/privilege/alter_privilege/old.sql @@ -9,8 +9,3 @@ END $$; -- Initial default privileges ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON SEQUENCES TO app_user; - -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL -); diff --git a/testdata/diff/privilege/alter_privilege/plan.json b/testdata/diff/privilege/alter_privilege/plan.json index 7fb23f5e..3e9ce3fb 100644 --- a/testdata/diff/privilege/alter_privilege/plan.json +++ b/testdata/diff/privilege/alter_privilege/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.5.1", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "db1d094ecca08a63dff4c717567ebfdfb8fda90d23d04a4d6344fa484ce878c7" + "hash": "b20bb5302b7aedc8845129aab4ae49580d1b782b598c728ef14fb40cbbe086d2" }, "groups": [ { diff --git a/testdata/diff/privilege/alter_privilege_and_grant_option/new.sql b/testdata/diff/privilege/alter_privilege_and_grant_option/new.sql index e2f42f73..5b871a0d 100644 --- a/testdata/diff/privilege/alter_privilege_and_grant_option/new.sql +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/new.sql @@ -8,8 +8,3 @@ END $$; -- Changed default privileges: SELECT, INSERT, UPDATE with grant option ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE ON TABLES TO app_user WITH GRANT OPTION; - -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL -); diff --git a/testdata/diff/privilege/alter_privilege_and_grant_option/old.sql b/testdata/diff/privilege/alter_privilege_and_grant_option/old.sql index e8d70987..808cb039 100644 --- a/testdata/diff/privilege/alter_privilege_and_grant_option/old.sql +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/old.sql @@ -8,8 +8,3 @@ END $$; -- Initial default privileges: SELECT only, no grant option ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user; - -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL -); diff --git a/testdata/diff/privilege/alter_privilege_and_grant_option/plan.json b/testdata/diff/privilege/alter_privilege_and_grant_option/plan.json index ceb109e4..aaee38a0 100644 --- a/testdata/diff/privilege/alter_privilege_and_grant_option/plan.json +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.5.1", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "3fbf3796069c82fae69d03a6d3036009311a7f893bfea04a504c534e3f1451b1" + "hash": "322eb4edb321a94ee411ad523ff4c646892bd226d9d35fc08402de56707f127f" }, "groups": [ { diff --git a/testdata/diff/privilege/drop_privilege/new.sql b/testdata/diff/privilege/drop_privilege/new.sql index 518e8554..11e96983 100644 --- a/testdata/diff/privilege/drop_privilege/new.sql +++ b/testdata/diff/privilege/drop_privilege/new.sql @@ -10,7 +10,3 @@ BEGIN END $$; -- Remove all default privileges -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL -); diff --git a/testdata/diff/privilege/drop_privilege/old.sql b/testdata/diff/privilege/drop_privilege/old.sql index 95a6bf64..a122e565 100644 --- a/testdata/diff/privilege/drop_privilege/old.sql +++ b/testdata/diff/privilege/drop_privilege/old.sql @@ -12,8 +12,3 @@ END $$; -- Default privileges configured ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly_user; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE, DELETE ON TABLES TO app_user; - -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL -); diff --git a/testdata/diff/privilege/drop_privilege/plan.json b/testdata/diff/privilege/drop_privilege/plan.json index 1c79bbe0..22d3a70d 100644 --- a/testdata/diff/privilege/drop_privilege/plan.json +++ b/testdata/diff/privilege/drop_privilege/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.5.1", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "541e93c1f3f371b824b35de68daac5e6dd2dab6b88924d2c5100430c28d7955a" + "hash": "b021448244eec7bd9b054c089ed49d612e39fe7517c356b0c220136059a36043" }, "groups": [ {