-
Notifications
You must be signed in to change notification settings - Fork 51
Add support for extensions in the IR (#436) #443
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,7 @@ const ( | |
| DiffTypePrivilege | ||
| DiffTypeRevokedDefaultPrivilege | ||
| DiffTypeColumnPrivilege | ||
| DiffTypeExtension | ||
| ) | ||
|
|
||
| // String returns the string representation of DiffType | ||
|
|
@@ -103,6 +104,8 @@ func (d DiffType) String() string { | |
| return "revoked_default_privilege" | ||
| case DiffTypeColumnPrivilege: | ||
| return "column_privilege" | ||
| case DiffTypeExtension: | ||
| return "extension" | ||
| default: | ||
| return "unknown" | ||
| } | ||
|
|
@@ -177,6 +180,8 @@ func (d *DiffType) UnmarshalJSON(data []byte) error { | |
| *d = DiffTypeRevokedDefaultPrivilege | ||
| case "column_privilege": | ||
| *d = DiffTypeColumnPrivilege | ||
| case "extension": | ||
| *d = DiffTypeExtension | ||
| default: | ||
| return fmt.Errorf("unknown diff type: %s", s) | ||
| } | ||
|
|
@@ -296,6 +301,9 @@ type ddlDiff struct { | |
| addedColumnPrivileges []*ir.ColumnPrivilege | ||
| droppedColumnPrivileges []*ir.ColumnPrivilege | ||
| modifiedColumnPrivileges []*columnPrivilegeDiff | ||
| // Cluster-level extensions | ||
| addedExtensions []*ir.Extension | ||
| droppedExtensions []*ir.Extension | ||
| } | ||
|
|
||
| // schemaDiff represents changes to a schema | ||
|
|
@@ -460,6 +468,27 @@ func GenerateMigration(oldIR, newIR *ir.IR, targetSchema string) []Diff { | |
| addedColumnPrivileges: []*ir.ColumnPrivilege{}, | ||
| droppedColumnPrivileges: []*ir.ColumnPrivilege{}, | ||
| modifiedColumnPrivileges: []*columnPrivilegeDiff{}, | ||
| addedExtensions: []*ir.Extension{}, | ||
| droppedExtensions: []*ir.Extension{}, | ||
| } | ||
|
|
||
| // Compute extension diffs (cluster-level, so no schema filtering). | ||
| // Modifications (version bumps) are out of scope for this initial PR; only | ||
| // added/dropped are tracked. See #436 for the broader extension story. | ||
| { | ||
| extNames := sortedKeys(newIR.Extensions) | ||
| for _, name := range extNames { | ||
| newExt := newIR.Extensions[name] | ||
| if _, exists := oldIR.Extensions[name]; !exists { | ||
| diff.addedExtensions = append(diff.addedExtensions, newExt) | ||
| } | ||
| } | ||
| oldExtNames := sortedKeys(oldIR.Extensions) | ||
| for _, name := range oldExtNames { | ||
| if _, exists := newIR.Extensions[name]; !exists { | ||
| diff.droppedExtensions = append(diff.droppedExtensions, oldIR.Extensions[name]) | ||
| } | ||
|
Comment on lines
+478
to
+490
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extension diffing only checks whether the name exists in both IRs. The IR now carries Context Used: CLAUDE.md (source) |
||
| } | ||
| } | ||
|
|
||
| // Compare schemas first in deterministic order | ||
|
|
@@ -1499,6 +1528,10 @@ func (d *ddlDiff) generatePreDropMaterializedViewsSQL(targetSchema string, colle | |
| func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollector) { | ||
| // Note: Schema creation is out of scope for schema-level comparisons | ||
|
|
||
| // Extensions first: they provide operator classes, types, and functions that | ||
| // downstream schema objects (e.g., a GIST index on UUID via btree_gist) depend on. | ||
| generateCreateExtensionsSQL(d.addedExtensions, collector) | ||
|
|
||
| // Build function lookup early - needed for both domain and table dependency checks | ||
| newFunctionLookup := buildFunctionLookup(d.addedFunctions) | ||
|
|
||
|
|
@@ -1721,6 +1754,10 @@ func (d *ddlDiff) generateDropSQL(targetSchema string, collector *diffCollector, | |
| // Drop types | ||
| generateDropTypesSQL(d.droppedTypes, targetSchema, collector) | ||
|
|
||
| // Drop extensions last: any schema object that depended on the extension | ||
| // must already be gone before we try to drop the extension itself. | ||
| generateDropExtensionsSQL(d.droppedExtensions, collector) | ||
|
|
||
| // Drop schemas | ||
| // Note: Schema deletion is out of scope for schema-level comparisons | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,60 @@ | ||||||||||||||
| package diff | ||||||||||||||
|
|
||||||||||||||
| import ( | ||||||||||||||
| "fmt" | ||||||||||||||
|
|
||||||||||||||
| "github.com/pgplex/pgschema/ir" | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| // generateCreateExtensionsSQL generates `CREATE EXTENSION IF NOT EXISTS` statements | ||||||||||||||
| // for newly added extensions. Emitted before any schema-level objects because | ||||||||||||||
| // extensions can provide operator classes, types, and functions that those | ||||||||||||||
| // objects depend on (e.g., a GIST index using btree_gist's UUID operator class). | ||||||||||||||
| func generateCreateExtensionsSQL(extensions []*ir.Extension, collector *diffCollector) { | ||||||||||||||
| for _, ext := range extensions { | ||||||||||||||
| sql := generateExtensionSQL(ext) | ||||||||||||||
| context := &diffContext{ | ||||||||||||||
| Type: DiffTypeExtension, | ||||||||||||||
| Operation: DiffOperationCreate, | ||||||||||||||
| Path: extensionPath(ext), | ||||||||||||||
| Source: ext, | ||||||||||||||
| CanRunInTransaction: true, | ||||||||||||||
| } | ||||||||||||||
| collector.collect(context, sql) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // generateDropExtensionsSQL generates `DROP EXTENSION IF EXISTS` statements | ||||||||||||||
| // for extensions removed from the target. Emitted after all schema-level drops | ||||||||||||||
| // to avoid dependency conflicts. | ||||||||||||||
| func generateDropExtensionsSQL(extensions []*ir.Extension, collector *diffCollector) { | ||||||||||||||
| for _, ext := range extensions { | ||||||||||||||
| context := &diffContext{ | ||||||||||||||
| Type: DiffTypeExtension, | ||||||||||||||
| Operation: DiffOperationDrop, | ||||||||||||||
| Path: extensionPath(ext), | ||||||||||||||
| Source: ext, | ||||||||||||||
| CanRunInTransaction: true, | ||||||||||||||
| } | ||||||||||||||
| collector.collect(context, fmt.Sprintf("DROP EXTENSION IF EXISTS %s;", ir.QuoteIdentifier(ext.Name))) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // extensionPath returns the identifier used in the diff Path field. Extensions | ||||||||||||||
| // are cluster-level so no schema qualifier is included; doing so would leak | ||||||||||||||
| // the plan command's temporary schema into the recorded plan and break | ||||||||||||||
| // golden-output stability across runs. | ||||||||||||||
| func extensionPath(ext *ir.Extension) string { | ||||||||||||||
| return ext.Name | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // generateExtensionSQL renders a single CREATE EXTENSION statement. | ||||||||||||||
| // Extensions are cluster-level; the installed schema is intentionally not | ||||||||||||||
| // emitted here. Honoring it would require either pinning it to the user's | ||||||||||||||
| // declared value (which we cannot recover from pg_extension alone — the plan | ||||||||||||||
| // command's temporary schema becomes the install schema when no WITH SCHEMA | ||||||||||||||
| // is given) or filtering out transient schemas. Preserving the user-declared | ||||||||||||||
| // install schema is tracked as a follow-up to #436. | ||||||||||||||
| func generateExtensionSQL(ext *ir.Extension) string { | ||||||||||||||
| return fmt.Sprintf("CREATE EXTENSION IF NOT EXISTS %s;", ir.QuoteIdentifier(ext.Name)) | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+58
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Context Used: CLAUDE.md (source) |
||||||||||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| CREATE EXTENSION IF NOT EXISTS btree_gist; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| CREATE EXTENSION IF NOT EXISTS btree_gist; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| -- Empty schema (no extensions declared) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This compares all database extensions and drops any extension missing from the desired IR, even when the command is scoped to one target schema. If the current database has an extension used by another schema or application and the desired SQL for
publicdoes not declare it, a schema-scoped plan can emitDROP EXTENSIONfor unrelated database state.Context Used: CLAUDE.md (source)