-
Notifications
You must be signed in to change notification settings - Fork 42
fix: order CREATE VIEW after ALTER TABLE ADD COLUMN within a plan (#414) #417
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
base: main
Are you sure you want to change the base?
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 |
|---|---|---|
|
|
@@ -291,6 +291,11 @@ type ddlDiff struct { | |
| addedColumnPrivileges []*ir.ColumnPrivilege | ||
| droppedColumnPrivileges []*ir.ColumnPrivilege | ||
| modifiedColumnPrivileges []*columnPrivilegeDiff | ||
| // Newly-added views that reference newly-added columns on modified tables. | ||
| // Created in the modify phase, AFTER generateModifyTablesSQL, so the columns | ||
| // exist when the view body is parsed (issue #414). | ||
| deferredAddedViews []*ir.View | ||
| functionsAwaitingDeferredViews []*ir.Function | ||
| } | ||
|
|
||
| // schemaDiff represents changes to a schema | ||
|
|
@@ -1609,8 +1614,61 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto | |
| // Note: We need to create triggers for ALL tables, not just the original d.addedTables | ||
| generateCreateTriggersFromTables(d.addedTables, targetSchema, collector) | ||
|
|
||
| // Create views | ||
| generateCreateViewsSQL(d.addedViews, targetSchema, collector) | ||
| // Create views, deferring any whose body references a newly-added column on a | ||
| // modified table. Those columns are emitted by generateModifyTablesSQL during | ||
| // the modify phase, so deferred views are created there (issue #414) | ||
| addedColLookup := buildModifiedTableAddedColumnLookup(d.modifiedTables) | ||
| viewsToCreateNow := d.addedViews | ||
| if len(addedColLookup) > 0 { | ||
| viewsToCreateNow = nil | ||
| for _, v := range d.addedViews { | ||
| if viewReferencesAddedColumn(v, addedColLookup) { | ||
| d.deferredAddedViews = append(d.deferredAddedViews, v) | ||
| } else { | ||
| viewsToCreateNow = append(viewsToCreateNow, v) | ||
| } | ||
| } | ||
|
|
||
| // Transitive closure: also defer any view whose body references a view | ||
| // already in deferredAddedViews. Iterate to fixpoint so chains of any | ||
| // length (V3 -> V2 -> V1 -> added column) move together. Walking | ||
| // viewsToCreateNow in order preserves topological ordering on each pass. | ||
| // Each iteration reads d.deferredAddedViews fresh, so a view appended | ||
| // during this pass is visible to the very next sibling examined — that | ||
| // is what lets a topo-sorted chain drain in a single pass. | ||
| for { | ||
| var stillNow []*ir.View | ||
| added := false | ||
| for _, v := range viewsToCreateNow { | ||
| if viewReferencesAnyDeferredView(v, d.deferredAddedViews) { | ||
| d.deferredAddedViews = append(d.deferredAddedViews, v) | ||
| added = true | ||
| } else { | ||
| stillNow = append(stillNow, v) | ||
| } | ||
| } | ||
| viewsToCreateNow = stillNow | ||
| if !added { | ||
| break | ||
| } | ||
| } | ||
| } | ||
| generateCreateViewsSQL(viewsToCreateNow, targetSchema, collector) | ||
|
|
||
| // If any views were deferred, also defer functions whose view dependency is | ||
| // on those deferred views — they must be created after the views exist. | ||
| if len(d.deferredAddedViews) > 0 { | ||
| deferredViewLookup := buildViewLookup(d.deferredAddedViews) | ||
| var keepNow []*ir.Function | ||
| for _, fn := range functionsWithViewDeps { | ||
| if functionReferencesNewView(fn, deferredViewLookup) { | ||
| d.functionsAwaitingDeferredViews = append(d.functionsAwaitingDeferredViews, fn) | ||
| } else { | ||
| keepNow = append(keepNow, fn) | ||
| } | ||
| } | ||
| functionsWithViewDeps = keepNow | ||
| } | ||
|
Comment on lines
+1617
to
+1671
|
||
|
|
||
| // Create functions WITH view dependencies (now that views exist) | ||
| // These functions reference views in their return type or parameter types (issue #300) | ||
|
|
@@ -1646,6 +1704,16 @@ func (d *ddlDiff) generateModifySQL(targetSchema string, collector *diffCollecto | |
| // Modify tables | ||
| generateModifyTablesSQL(d.modifiedTables, d.droppedTables, targetSchema, collector) | ||
|
|
||
| // Create views deferred from generateCreateSQL — their bodies reference | ||
| // columns just added by ALTER TABLE above (issue #414). Likewise, emit | ||
| // any functions whose view dependency was on those deferred views. | ||
| if len(d.deferredAddedViews) > 0 { | ||
| generateCreateViewsSQL(d.deferredAddedViews, targetSchema, collector) | ||
| } | ||
| if len(d.functionsAwaitingDeferredViews) > 0 { | ||
| generateCreateFunctionsSQL(d.functionsAwaitingDeferredViews, targetSchema, collector) | ||
| } | ||
|
|
||
| // Find views that depend on views being recreated (issue #268, #308) | ||
| // Handles both materialized views and regular views with RequiresRecreate | ||
| // Exclude newly added views - they will be created in CREATE phase after recreated views | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -769,6 +769,67 @@ func viewDependsOnTable(view *ir.View, tableSchema, tableName string) bool { | |
| return false | ||
| } | ||
|
|
||
| // buildModifiedTableAddedColumnLookup returns a map of lowercased schema.tableName | ||
| // to a set of lowercased column names being added by ALTER TABLE on that table. | ||
| func buildModifiedTableAddedColumnLookup(modifiedTables []*tableDiff) map[string]map[string]struct{} { | ||
| lookup := make(map[string]map[string]struct{}) | ||
| for _, td := range modifiedTables { | ||
| if len(td.AddedColumns) == 0 { | ||
| continue | ||
| } | ||
| key := strings.ToLower(td.Table.Schema + "." + td.Table.Name) | ||
| cols := make(map[string]struct{}, len(td.AddedColumns)) | ||
| for _, c := range td.AddedColumns { | ||
| cols[strings.ToLower(c.Name)] = struct{}{} | ||
| } | ||
| lookup[key] = cols | ||
| } | ||
| return lookup | ||
| } | ||
|
|
||
| // viewReferencesAnyDeferredView reports whether the view's body references any | ||
| // of the provided deferred views by name. Used for transitive deferral so that | ||
| // view chains (V2 -> V1 -> added column) move together to the modify phase. | ||
| func viewReferencesAnyDeferredView(view *ir.View, deferred []*ir.View) bool { | ||
| if view == nil || view.Definition == "" || len(deferred) == 0 { | ||
| return false | ||
| } | ||
| for _, dv := range deferred { | ||
| if viewDependsOnView(view, dv.Name) { | ||
| return true | ||
| } | ||
| if dv.Schema != "" && viewDependsOnView(view, dv.Schema+"."+dv.Name) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| // viewReferencesAddedColumn reports whether the view's definition references | ||
| // any modified table AND at least one of the columns being added to that table. | ||
| // Both checks are required to avoid deferring views that simply happen to | ||
| // mention a column name being added to an unrelated table. | ||
| func viewReferencesAddedColumn(view *ir.View, addedCols map[string]map[string]struct{}) bool { | ||
| if view == nil || view.Definition == "" || len(addedCols) == 0 { | ||
| return false | ||
| } | ||
| for tableKey, cols := range addedCols { | ||
| parts := strings.SplitN(tableKey, ".", 2) | ||
| if len(parts) != 2 { | ||
| continue | ||
| } | ||
| if !viewDependsOnTable(view, parts[0], parts[1]) { | ||
| continue | ||
| } | ||
| for col := range cols { | ||
| if containsIdentifier(view.Definition, col) { | ||
| return true | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+812
to
+829
|
||
| return false | ||
| } | ||
|
|
||
| // dependentViewsContext tracks views that depend on views being recreated | ||
| type dependentViewsContext struct { | ||
| // dependents maps view key (schema.name) to list of dependent views | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| ALTER TABLE foo ADD COLUMN run_id uuid; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_base AS | ||
| SELECT id, | ||
| run_id | ||
| FROM foo | ||
| WHERE run_id IS NOT NULL; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_summary AS | ||
| SELECT id | ||
| FROM foo_base; | ||
|
|
||
| CREATE OR REPLACE FUNCTION get_foo_summary() | ||
| RETURNS SETOF foo_summary | ||
| LANGUAGE sql | ||
| STABLE | ||
| AS $$ SELECT * FROM foo_summary | ||
| $$; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| CREATE TABLE foo ( | ||
| id bigint PRIMARY KEY, | ||
| run_id uuid | ||
| ); | ||
|
|
||
| CREATE OR REPLACE VIEW foo_base AS | ||
| SELECT id, run_id FROM foo WHERE run_id IS NOT NULL; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_summary AS | ||
| SELECT id FROM foo_base; | ||
|
|
||
| CREATE OR REPLACE FUNCTION get_foo_summary() | ||
| RETURNS SETOF foo_summary | ||
| LANGUAGE sql STABLE | ||
| AS $$ SELECT * FROM foo_summary $$; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| CREATE TABLE foo ( | ||
| id bigint PRIMARY KEY | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| { | ||
| "version": "1.0.0", | ||
| "pgschema_version": "1.9.0", | ||
| "created_at": "1970-01-01T00:00:00Z", | ||
| "source_fingerprint": { | ||
| "hash": "e1fb0e7b8fda0362df6ecdbc88f6910f1faaa4d896de95796aa412b913e18858" | ||
| }, | ||
| "groups": [ | ||
| { | ||
| "steps": [ | ||
| { | ||
| "sql": "ALTER TABLE foo ADD COLUMN run_id uuid;", | ||
| "type": "table.column", | ||
| "operation": "create", | ||
| "path": "public.foo.run_id" | ||
| }, | ||
| { | ||
| "sql": "CREATE OR REPLACE VIEW foo_base AS\n SELECT id,\n run_id\n FROM foo\n WHERE run_id IS NOT NULL;", | ||
| "type": "view", | ||
| "operation": "create", | ||
| "path": "public.foo_base" | ||
| }, | ||
| { | ||
| "sql": "CREATE OR REPLACE VIEW foo_summary AS\n SELECT id\n FROM foo_base;", | ||
| "type": "view", | ||
| "operation": "create", | ||
| "path": "public.foo_summary" | ||
| }, | ||
| { | ||
| "sql": "CREATE OR REPLACE FUNCTION get_foo_summary()\nRETURNS SETOF foo_summary\nLANGUAGE sql\nSTABLE\nAS $$ SELECT * FROM foo_summary\n$$;", | ||
| "type": "function", | ||
| "operation": "create", | ||
| "path": "public.get_foo_summary" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| ALTER TABLE foo ADD COLUMN run_id uuid; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_base AS | ||
| SELECT id, | ||
| run_id | ||
| FROM foo | ||
| WHERE run_id IS NOT NULL; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_summary AS | ||
| SELECT id | ||
| FROM foo_base; | ||
|
|
||
| CREATE OR REPLACE FUNCTION get_foo_summary() | ||
| RETURNS SETOF foo_summary | ||
| LANGUAGE sql | ||
| STABLE | ||
| AS $$ SELECT * FROM foo_summary | ||
| $$; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| Plan: 3 to add, 1 to modify. | ||
|
|
||
| Summary by type: | ||
| functions: 1 to add | ||
| tables: 1 to modify | ||
| views: 2 to add | ||
|
|
||
| Functions: | ||
| + get_foo_summary | ||
|
|
||
| Tables: | ||
| ~ foo | ||
| + run_id (column) | ||
|
|
||
| Views: | ||
| + foo_base | ||
| + foo_summary | ||
|
|
||
| DDL to be executed: | ||
| -------------------------------------------------- | ||
|
|
||
| ALTER TABLE foo ADD COLUMN run_id uuid; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_base AS | ||
| SELECT id, | ||
| run_id | ||
| FROM foo | ||
| WHERE run_id IS NOT NULL; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_summary AS | ||
| SELECT id | ||
| FROM foo_base; | ||
|
|
||
| CREATE OR REPLACE FUNCTION get_foo_summary() | ||
| RETURNS SETOF foo_summary | ||
| LANGUAGE sql | ||
| STABLE | ||
| AS $$ SELECT * FROM foo_summary | ||
| $$; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| ALTER TABLE foo ADD COLUMN run_id uuid; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_base AS | ||
| SELECT id, | ||
| run_id | ||
| FROM foo | ||
| WHERE run_id IS NOT NULL; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_summary AS | ||
| SELECT id | ||
| FROM foo_base; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| CREATE TABLE foo ( | ||
| id bigint PRIMARY KEY, | ||
| run_id uuid | ||
| ); | ||
|
|
||
| CREATE OR REPLACE VIEW foo_base AS | ||
| SELECT id, run_id FROM foo WHERE run_id IS NOT NULL; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_summary AS | ||
| SELECT id FROM foo_base; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| CREATE TABLE foo ( | ||
| id bigint PRIMARY KEY | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| { | ||
| "version": "1.0.0", | ||
| "pgschema_version": "1.9.0", | ||
| "created_at": "1970-01-01T00:00:00Z", | ||
| "source_fingerprint": { | ||
| "hash": "e1fb0e7b8fda0362df6ecdbc88f6910f1faaa4d896de95796aa412b913e18858" | ||
| }, | ||
| "groups": [ | ||
| { | ||
| "steps": [ | ||
| { | ||
| "sql": "ALTER TABLE foo ADD COLUMN run_id uuid;", | ||
| "type": "table.column", | ||
| "operation": "create", | ||
| "path": "public.foo.run_id" | ||
| }, | ||
| { | ||
| "sql": "CREATE OR REPLACE VIEW foo_base AS\n SELECT id,\n run_id\n FROM foo\n WHERE run_id IS NOT NULL;", | ||
| "type": "view", | ||
| "operation": "create", | ||
| "path": "public.foo_base" | ||
| }, | ||
| { | ||
| "sql": "CREATE OR REPLACE VIEW foo_summary AS\n SELECT id\n FROM foo_base;", | ||
| "type": "view", | ||
| "operation": "create", | ||
| "path": "public.foo_summary" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| ALTER TABLE foo ADD COLUMN run_id uuid; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_base AS | ||
| SELECT id, | ||
| run_id | ||
| FROM foo | ||
| WHERE run_id IS NOT NULL; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_summary AS | ||
| SELECT id | ||
| FROM foo_base; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| Plan: 2 to add, 1 to modify. | ||
|
|
||
| Summary by type: | ||
| tables: 1 to modify | ||
| views: 2 to add | ||
|
|
||
| Tables: | ||
| ~ foo | ||
| + run_id (column) | ||
|
|
||
| Views: | ||
| + foo_base | ||
| + foo_summary | ||
|
|
||
| DDL to be executed: | ||
| -------------------------------------------------- | ||
|
|
||
| ALTER TABLE foo ADD COLUMN run_id uuid; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_base AS | ||
| SELECT id, | ||
| run_id | ||
| FROM foo | ||
| WHERE run_id IS NOT NULL; | ||
|
|
||
| CREATE OR REPLACE VIEW foo_summary AS | ||
| SELECT id | ||
| FROM foo_base; |
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.
If a newly-added view V2 depends on a newly-added view V1, and V1 is deferred (because V1's body references an added column), V2 is checked only against
addedColLookup— it won't reference the modified table directly, so it stays inviewsToCreateNow. PostgreSQL then tries toCREATE VIEW v2during the create phase while V1 doesn't yet exist, producing "ERROR: relation 'v1' does not exist".A complete fix would also defer any view in
viewsToCreateNowwhose body references a deferred view (i.e., a name that appears inbuildViewLookup(d.deferredAddedViews)). Without that transitive step, multi-level view chains still produce a different error than the one fixed here.