diff --git a/internal/diff/default_privilege.go b/internal/diff/default_privilege.go new file mode 100644 index 00000000..0ff36bfa --- /dev/null +++ b/internal/diff/default_privilege.go @@ -0,0 +1,229 @@ +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 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) + } + } + + 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+";") + } + } + + 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/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/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..5ab8986d 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,70 @@ 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 { + // Sort privileges for deterministic IR output + sort.Strings(privs) + 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 new file mode 100644 index 00000000..dc2c716d --- /dev/null +++ b/testdata/diff/privilege/add_function_privilege/diff.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/new.sql b/testdata/diff/privilege/add_function_privilege/new.sql new file mode 100644 index 00000000..e7a0887f --- /dev/null +++ b/testdata/diff/privilege/add_function_privilege/new.sql @@ -0,0 +1,10 @@ +-- 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; 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..60b2e0af --- /dev/null +++ b/testdata/diff/privilege/add_function_privilege/old.sql @@ -0,0 +1,9 @@ +-- 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 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..f3cf5e25 --- /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": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" + }, + "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..df3c1817 --- /dev/null +++ b/testdata/diff/privilege/add_function_privilege/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to add. + +Summary by type: + default privileges: 1 to add + +Default privileges: + + api_user + +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 new file mode 100644 index 00000000..e505eaca --- /dev/null +++ b/testdata/diff/privilege/add_privilege_with_grant_option/diff.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/new.sql b/testdata/diff/privilege/add_privilege_with_grant_option/new.sql new file mode 100644 index 00000000..e6d00a4a --- /dev/null +++ b/testdata/diff/privilege/add_privilege_with_grant_option/new.sql @@ -0,0 +1,10 @@ +-- 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 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/old.sql b/testdata/diff/privilege/add_privilege_with_grant_option/old.sql new file mode 100644 index 00000000..5c0cbe3a --- /dev/null +++ b/testdata/diff/privilege/add_privilege_with_grant_option/old.sql @@ -0,0 +1,9 @@ +-- 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 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..d8bf3d4e --- /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": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" + }, + "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..2d711324 --- /dev/null +++ b/testdata/diff/privilege/add_privilege_with_grant_option/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to add. + +Summary by type: + default privileges: 1 to add + +Default privileges: + + admin_user + +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 new file mode 100644 index 00000000..22f2a565 --- /dev/null +++ b/testdata/diff/privilege/add_sequence_privilege/diff.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/new.sql b/testdata/diff/privilege/add_sequence_privilege/new.sql new file mode 100644 index 00000000..8fecfabe --- /dev/null +++ b/testdata/diff/privilege/add_sequence_privilege/new.sql @@ -0,0 +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 $$; + +-- Grant USAGE, SELECT on future sequences to app_user +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO app_user; 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..81ec6e31 --- /dev/null +++ b/testdata/diff/privilege/add_sequence_privilege/old.sql @@ -0,0 +1,9 @@ +-- 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 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..ba61176a --- /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": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" + }, + "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..60293988 --- /dev/null +++ b/testdata/diff/privilege/add_sequence_privilege/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to add. + +Summary by type: + default privileges: 1 to add + +Default privileges: + + app_user + +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/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..025d4e72 --- /dev/null +++ b/testdata/diff/privilege/add_table_privilege/new.sql @@ -0,0 +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 $$; + +-- Grant SELECT on future 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 IN SCHEMA public GRANT INSERT, UPDATE ON TABLES TO app_user; 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..81ec6e31 --- /dev/null +++ b/testdata/diff/privilege/add_table_privilege/old.sql @@ -0,0 +1,9 @@ +-- 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 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..d21adf38 --- /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": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" + }, + "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..ebc05f17 --- /dev/null +++ b/testdata/diff/privilege/add_table_privilege/plan.txt @@ -0,0 +1,15 @@ +Plan: 2 to add. + +Summary by type: + default privileges: 2 to add + +Default privileges: + + PUBLIC + + app_user + +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/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..c900e237 --- /dev/null +++ b/testdata/diff/privilege/add_type_privilege/new.sql @@ -0,0 +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 $$; + +-- Grant USAGE on future types to app_user +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON TYPES TO app_user; 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..81ec6e31 --- /dev/null +++ b/testdata/diff/privilege/add_type_privilege/old.sql @@ -0,0 +1,9 @@ +-- 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 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..51f2d16b --- /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": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" + }, + "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..6badcaec --- /dev/null +++ b/testdata/diff/privilege/add_type_privilege/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to add. + +Summary by type: + default privileges: 1 to add + +Default privileges: + + app_user + +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 new file mode 100644 index 00000000..652a2626 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/diff.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/new.sql b/testdata/diff/privilege/alter_privilege/new.sql new file mode 100644 index 00000000..9798bb85 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/new.sql @@ -0,0 +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 $$; + +-- Expand table privileges, remove sequence privileges +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE ON TABLES TO app_user; diff --git a/testdata/diff/privilege/alter_privilege/old.sql b/testdata/diff/privilege/alter_privilege/old.sql new file mode 100644 index 00000000..9e6a210e --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/old.sql @@ -0,0 +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 $$; + +-- 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; diff --git a/testdata/diff/privilege/alter_privilege/plan.json b/testdata/diff/privilege/alter_privilege/plan.json new file mode 100644 index 00000000..3e9ce3fb --- /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": "b20bb5302b7aedc8845129aab4ae49580d1b782b598c728ef14fb40cbbe086d2" + }, + "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..3e476230 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/plan.txt @@ -0,0 +1,15 @@ +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: +-------------------------------------------------- + +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_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..5b871a0d --- /dev/null +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/new.sql @@ -0,0 +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 $$; + +-- 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; 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..808cb039 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/old.sql @@ -0,0 +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 $$; + +-- Initial default privileges: SELECT only, no grant option +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user; 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..aaee38a0 --- /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": "322eb4edb321a94ee411ad523ff4c646892bd226d9d35fc08402de56707f127f" + }, + "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..81e1cf6c --- /dev/null +++ b/testdata/diff/privilege/alter_privilege_and_grant_option/plan.txt @@ -0,0 +1,18 @@ +Plan: 3 to modify. + +Summary by type: + default privileges: 3 to modify + +Default privileges: + ~ app_user + ~ app_user + ~ app_user + +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; diff --git a/testdata/diff/privilege/drop_privilege/diff.sql b/testdata/diff/privilege/drop_privilege/diff.sql new file mode 100644 index 00000000..e843a2b9 --- /dev/null +++ b/testdata/diff/privilege/drop_privilege/diff.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/new.sql b/testdata/diff/privilege/drop_privilege/new.sql new file mode 100644 index 00000000..11e96983 --- /dev/null +++ b/testdata/diff/privilege/drop_privilege/new.sql @@ -0,0 +1,12 @@ +-- 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 diff --git a/testdata/diff/privilege/drop_privilege/old.sql b/testdata/diff/privilege/drop_privilege/old.sql new file mode 100644 index 00000000..a122e565 --- /dev/null +++ b/testdata/diff/privilege/drop_privilege/old.sql @@ -0,0 +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 $$; + +-- 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; diff --git a/testdata/diff/privilege/drop_privilege/plan.json b/testdata/diff/privilege/drop_privilege/plan.json new file mode 100644 index 00000000..22d3a70d --- /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": "b021448244eec7bd9b054c089ed49d612e39fe7517c356b0c220136059a36043" + }, + "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..d1a66234 --- /dev/null +++ b/testdata/diff/privilege/drop_privilege/plan.txt @@ -0,0 +1,15 @@ +Plan: 2 to drop. + +Summary by type: + default privileges: 2 to drop + +Default privileges: + - app_user + - readonly_user + +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;