From b30275a4b8614d82b8676093ae99f5aa4b27533a Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Sat, 21 Feb 2026 20:47:00 -0800 Subject: [PATCH 1/2] fix: use DROP+CREATE for views with column changes instead of CREATE OR REPLACE (#308) When a table gains a new column and a dependent view uses SELECT *, the view's column positions shift. PostgreSQL's CREATE OR REPLACE VIEW rejects this because it cannot rename or reorder existing columns. This fix: - Adds column tracking (via pg_attribute) to the View IR - Detects when old columns are NOT a prefix of new columns - Uses DROP VIEW + CREATE VIEW instead of CREATE OR REPLACE VIEW - Handles dependent view cascading (drop/recreate in dependency order) - Generalizes the existing materialized view recreation logic to also handle regular views Co-Authored-By: Claude Opus 4.6 --- internal/diff/diff.go | 13 +- internal/diff/view.go | 144 +++++++++++------- ir/inspector.go | 38 +++++ ir/ir.go | 1 + .../diff/comment/add_index_comment/plan.json | 2 +- .../diff/comment/add_view_comment/plan.json | 2 +- .../diff/create_index/drop_index/plan.json | 2 +- .../alter_materialized_view/plan.json | 2 +- .../drop_materialized_view/plan.json | 2 +- .../diff/create_trigger/add_trigger/plan.json | 2 +- .../diff/create_view/alter_view/plan.json | 2 +- testdata/diff/create_view/drop_view/plan.json | 2 +- .../plan.json | 2 +- .../issue_300_view_depends_on_view/plan.json | 2 +- .../diff.sql | 12 ++ .../new.sql | 16 ++ .../old.sql | 15 ++ .../plan.json | 32 ++++ .../plan.sql | 12 ++ .../plan.txt | 28 ++++ .../table_to_materialized_view/plan.json | 2 +- .../diff/dependency/table_to_view/plan.json | 2 +- testdata/diff/migrate/v5/plan.json | 2 +- .../add_materialized_view_index/plan.json | 2 +- .../alter_materialized_view_index/plan.json | 2 +- 25 files changed, 270 insertions(+), 71 deletions(-) create mode 100644 testdata/diff/dependency/issue_308_view_select_star_column_reorder/diff.sql create mode 100644 testdata/diff/dependency/issue_308_view_select_star_column_reorder/new.sql create mode 100644 testdata/diff/dependency/issue_308_view_select_star_column_reorder/old.sql create mode 100644 testdata/diff/dependency/issue_308_view_select_star_column_reorder/plan.json create mode 100644 testdata/diff/dependency/issue_308_view_select_star_column_reorder/plan.sql create mode 100644 testdata/diff/dependency/issue_308_view_select_star_column_reorder/plan.txt diff --git a/internal/diff/diff.go b/internal/diff/diff.go index bb872b79..080845b0 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -827,7 +827,11 @@ func GenerateMigration(oldIR, newIR *ir.IR, targetSchema string) []Diff { if structurallyDifferent || commentChanged || indexesChanged || triggersChanged { // For materialized views with structural changes, mark for recreation - if newView.Materialized && structurallyDifferent { + // For regular views with column changes incompatible with CREATE OR REPLACE VIEW, + // also mark for recreation (issue #308) + needsRecreate := structurallyDifferent && (newView.Materialized || viewColumnsRequireRecreate(oldView, newView)) + + if needsRecreate { diff.modifiedViews = append(diff.modifiedViews, &viewDiff{ Old: oldView, New: newView, @@ -1624,9 +1628,10 @@ func (d *ddlDiff) generateModifySQL(targetSchema string, collector *diffCollecto // Modify tables generateModifyTablesSQL(d.modifiedTables, targetSchema, collector) - // Find views that depend on materialized views being recreated (issue #268) - // Exclude newly added views - they will be created in CREATE phase after mat views - dependentViewsCtx := findDependentViewsForMatViews(d.allNewViews, d.modifiedViews, d.addedViews) + // 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 + dependentViewsCtx := findDependentViewsForRecreatedViews(d.allNewViews, d.modifiedViews, d.addedViews) // Track views recreated as dependencies to avoid duplicate processing recreatedViews := make(map[string]bool) diff --git a/internal/diff/view.go b/internal/diff/view.go index 2919d79a..7bcef5a8 100644 --- a/internal/diff/view.go +++ b/internal/diff/view.go @@ -106,14 +106,22 @@ func generateModifyViewsSQL(diffs []*viewDiff, targetSchema string, collector *d var allDependentViewsToRecreate []*ir.View seenDependentViews := make(map[string]bool) - // Phase 1: Drop all dependent views and drop/recreate all materialized views + // Phase 1: Drop all dependent views and drop/recreate views that require recreation for _, diff := range diffs { - // Handle materialized views that require recreation (DROP + CREATE) + // Handle views that require recreation (DROP + CREATE) + // This applies to materialized views with structural changes and + // regular views with column changes incompatible with CREATE OR REPLACE (issue #308) if diff.RequiresRecreate { viewKey := diff.New.Schema + "." + diff.New.Name viewName := qualifyEntityName(diff.New.Schema, diff.New.Name, targetSchema) - // Get dependent views for this materialized view + // Determine types based on whether view is materialized + diffType := DiffTypeView + if diff.New.Materialized { + diffType = DiffTypeMaterializedView + } + + // Get dependent views for this view var dependentViews []*ir.View if dependentViewsCtx != nil { dependentViews = dependentViewsCtx.GetDependents(viewKey) @@ -123,12 +131,12 @@ func generateModifyViewsSQL(diffs []*viewDiff, targetSchema string, collector *d // We use RESTRICT (not CASCADE) to fail safely if there are transitive // dependencies that we haven't tracked. This prevents silently dropping // views that wouldn't be recreated. - // Skip views that have already been dropped (when a view depends on multiple mat views). + // Skip views that have already been dropped (when a view depends on multiple views). for i := len(dependentViews) - 1; i >= 0; i-- { depView := dependentViews[i] depViewKey := depView.Schema + "." + depView.Name - // Skip if already dropped (view depends on multiple mat views being recreated) + // Skip if already dropped (view depends on multiple views being recreated) if droppedDependentViews[depViewKey] { continue } @@ -162,7 +170,7 @@ func generateModifyViewsSQL(diffs []*viewDiff, targetSchema string, collector *d createSQL := generateViewSQL(diff.New, targetSchema) context := &diffContext{ - Type: DiffTypeMaterializedView, + Type: diffType, Operation: DiffOperationCreate, Path: fmt.Sprintf("%s.%s", diff.New.Schema, diff.New.Name), Source: diff, @@ -170,8 +178,13 @@ func generateModifyViewsSQL(diffs []*viewDiff, targetSchema string, collector *d } collector.collect(context, createSQL) } else { - // DROP the old materialized view - dropSQL := fmt.Sprintf("DROP MATERIALIZED VIEW %s RESTRICT;", viewName) + // DROP the old view + var dropSQL string + if diff.New.Materialized { + dropSQL = fmt.Sprintf("DROP MATERIALIZED VIEW %s RESTRICT;", viewName) + } else { + dropSQL = fmt.Sprintf("DROP VIEW IF EXISTS %s RESTRICT;", viewName) + } createSQL := generateViewSQL(diff.New, targetSchema) statements := []SQLStatement{ @@ -187,7 +200,7 @@ func generateModifyViewsSQL(diffs []*viewDiff, targetSchema string, collector *d // Use DiffOperationAlter to categorize as a modification context := &diffContext{ - Type: DiffTypeMaterializedView, + Type: diffType, Operation: DiffOperationAlter, Path: fmt.Sprintf("%s.%s", diff.New.Schema, diff.New.Name), Source: diff, @@ -198,15 +211,23 @@ func generateModifyViewsSQL(diffs []*viewDiff, targetSchema string, collector *d // Add view comment if present if diff.New.Comment != "" { - sql := fmt.Sprintf("COMMENT ON MATERIALIZED VIEW %s IS %s;", viewName, quoteString(diff.New.Comment)) + var commentSQL string + var commentType DiffType + if diff.New.Materialized { + commentSQL = fmt.Sprintf("COMMENT ON MATERIALIZED VIEW %s IS %s;", viewName, quoteString(diff.New.Comment)) + commentType = DiffTypeMaterializedViewComment + } else { + commentSQL = fmt.Sprintf("COMMENT ON VIEW %s IS %s;", viewName, quoteString(diff.New.Comment)) + commentType = DiffTypeViewComment + } commentContext := &diffContext{ - Type: DiffTypeMaterializedViewComment, + Type: commentType, Operation: DiffOperationCreate, Path: fmt.Sprintf("%s.%s", diff.New.Schema, diff.New.Name), Source: diff.New, CanRunInTransaction: true, } - collector.collect(commentContext, sql) + collector.collect(commentContext, commentSQL) } // Recreate indexes for materialized views @@ -555,6 +576,39 @@ func viewsEqual(old, new *ir.View) bool { return old.Definition == new.Definition } +// viewColumnsRequireRecreate checks whether the view's column set has changed +// in a way that requires DROP + CREATE instead of CREATE OR REPLACE. +// PostgreSQL's CREATE OR REPLACE VIEW only allows adding new columns at the end; +// it rejects any changes to existing column names or positions. +func viewColumnsRequireRecreate(old, new *ir.View) bool { + oldCols := old.Columns + newCols := new.Columns + + // If column info is not available, fall back to safe behavior (no recreate needed; + // CREATE OR REPLACE will fail at apply time if columns are incompatible) + if len(oldCols) == 0 || len(newCols) == 0 { + return false + } + + // If old columns are a prefix of new columns, CREATE OR REPLACE VIEW is safe + // (only new columns added at end) + if len(newCols) >= len(oldCols) { + prefixMatch := true + for i, col := range oldCols { + if newCols[i] != col { + prefixMatch = false + break + } + } + if prefixMatch { + return false + } + } + + // Columns were reordered, renamed, or removed — requires DROP + CREATE + return true +} + // viewDependsOnView checks if viewA depends on viewB func viewDependsOnView(viewA *ir.View, viewBName string) bool { if viewA == nil || viewA.Definition == "" { @@ -617,27 +671,7 @@ func viewDependsOnTable(view *ir.View, tableSchema, tableName string) bool { return false } -// viewDependsOnMaterializedView checks if a regular view depends on a materialized view -func viewDependsOnMaterializedView(view *ir.View, matViewSchema, matViewName string) bool { - if view == nil || view.Definition == "" || view.Materialized { - return false - } - - // Check for unqualified name using word boundary matching - if containsIdentifier(view.Definition, matViewName) { - return true - } - - // Check for qualified name (schema.matview) - qualifiedName := matViewSchema + "." + matViewName - if containsIdentifier(view.Definition, qualifiedName) { - return true - } - - return false -} - -// dependentViewsContext tracks views that depend on materialized views being recreated +// dependentViewsContext tracks views that depend on views being recreated type dependentViewsContext struct { // dependents maps materialized view key (schema.name) to list of dependent regular views dependents map[string][]*ir.View @@ -650,21 +684,22 @@ func newDependentViewsContext() *dependentViewsContext { } } -// GetDependents returns the list of views that depend on the given materialized view -func (ctx *dependentViewsContext) GetDependents(matViewKey string) []*ir.View { +// GetDependents returns the list of views that depend on the given view being recreated +func (ctx *dependentViewsContext) GetDependents(viewKey string) []*ir.View { if ctx == nil || ctx.dependents == nil { return nil } - return ctx.dependents[matViewKey] + return ctx.dependents[viewKey] } -// findDependentViewsForMatViews finds all regular views that depend on materialized views being recreated. -// This includes transitive dependencies (views that depend on views that depend on the mat view). +// findDependentViewsForRecreatedViews finds all views that depend on views being recreated. +// This includes transitive dependencies (views that depend on views that depend on the recreated view). +// Handles both materialized views and regular views with RequiresRecreate (issue #308). // allViews contains all views from the new state (used for dependency analysis and recreation) -// modifiedViews contains the materialized views being recreated +// modifiedViews contains the views being recreated // addedViews contains views that are newly added (not in old schema) - these should be excluded -// because they will be created in the CREATE phase after mat views are recreated -func findDependentViewsForMatViews(allViews map[string]*ir.View, modifiedViews []*viewDiff, addedViews []*ir.View) *dependentViewsContext { +// because they will be created in the CREATE phase after recreated views +func findDependentViewsForRecreatedViews(allViews map[string]*ir.View, modifiedViews []*viewDiff, addedViews []*ir.View) *dependentViewsContext { ctx := newDependentViewsContext() // Build a set of added view keys to exclude from dependents @@ -674,13 +709,13 @@ func findDependentViewsForMatViews(allViews map[string]*ir.View, modifiedViews [ } for _, viewDiff := range modifiedViews { - if !viewDiff.RequiresRecreate || !viewDiff.New.Materialized { + if !viewDiff.RequiresRecreate { continue } - matViewKey := viewDiff.New.Schema + "." + viewDiff.New.Name + recreatedViewKey := viewDiff.New.Schema + "." + viewDiff.New.Name - // Find all regular views that directly depend on this materialized view + // Find all views that directly depend on this view being recreated // Exclude newly added views - they will be created in CREATE phase directDependents := make([]*ir.View, 0) for _, view := range allViews { @@ -688,7 +723,12 @@ func findDependentViewsForMatViews(allViews map[string]*ir.View, modifiedViews [ if addedViewKeys[viewKey] { continue // Skip newly added views } - if viewDependsOnMaterializedView(view, viewDiff.New.Schema, viewDiff.New.Name) { + // Skip the view being recreated itself + if viewKey == recreatedViewKey { + continue + } + if viewDependsOnView(view, viewDiff.New.Name) || + viewDependsOnView(view, viewDiff.New.Schema+"."+viewDiff.New.Name) { directDependents = append(directDependents, view) } } @@ -700,7 +740,7 @@ func findDependentViewsForMatViews(allViews map[string]*ir.View, modifiedViews [ // Topologically sort the dependents so they can be dropped/recreated in correct order sortedDependents := topologicallySortViews(allDependents) - ctx.dependents[matViewKey] = sortedDependents + ctx.dependents[recreatedViewKey] = sortedDependents } return ctx @@ -760,19 +800,19 @@ func findTransitiveDependents(initialViews []*ir.View, allViews map[string]*ir.V return result } -// sortModifiedViewsForProcessing sorts modifiedViews to ensure materialized views +// sortModifiedViewsForProcessing sorts modifiedViews to ensure views // with RequiresRecreate are processed first. This ensures dependent views are // added to recreatedViews before their own modifications would be processed. func sortModifiedViewsForProcessing(views []*viewDiff) { sort.SliceStable(views, func(i, j int) bool { - // Materialized views with RequiresRecreate should come first - iMatRecreate := views[i].RequiresRecreate && views[i].New.Materialized - jMatRecreate := views[j].RequiresRecreate && views[j].New.Materialized + // Views with RequiresRecreate should come first + iRecreate := views[i].RequiresRecreate + jRecreate := views[j].RequiresRecreate - if iMatRecreate && !jMatRecreate { + if iRecreate && !jRecreate { return true } - if !iMatRecreate && jMatRecreate { + if !iRecreate && jRecreate { return false } diff --git a/ir/inspector.go b/ir/inspector.go index d8e412dd..2990eea5 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -1357,10 +1357,17 @@ func (i *Inspector) buildViews(ctx context.Context, schema *IR, targetSchema str definition = strings.TrimSuffix(definition, ";") } + // Fetch view column names from pg_attribute (ordered by attnum) + columns, err := i.getViewColumns(ctx, schemaName, viewName) + if err != nil { + return fmt.Errorf("failed to get columns for view %s.%s: %w", schemaName, viewName, err) + } + v := &View{ Schema: schemaName, Name: viewName, Definition: definition, + Columns: columns, Comment: comment, Materialized: view.IsMaterialized.Valid && view.IsMaterialized.Bool, } @@ -1371,6 +1378,37 @@ func (i *Inspector) buildViews(ctx context.Context, schema *IR, targetSchema str return nil } +// getViewColumns returns the ordered list of column names for a view or materialized view. +// Uses pg_attribute to get column names ordered by their position (attnum). +func (i *Inspector) getViewColumns(ctx context.Context, schemaName, viewName string) ([]string, error) { + query := ` + SELECT a.attname + FROM pg_attribute a + JOIN pg_class c ON a.attrelid = c.oid + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE n.nspname = $1 + AND c.relname = $2 + AND a.attnum > 0 + AND NOT a.attisdropped + ORDER BY a.attnum` + + rows, err := i.db.QueryContext(ctx, query, schemaName, viewName) + if err != nil { + return nil, err + } + defer rows.Close() + + var columns []string + for rows.Next() { + var colName string + if err := rows.Scan(&colName); err != nil { + return nil, err + } + columns = append(columns, colName) + } + return columns, rows.Err() +} + // extractWhenClauseFromTriggerDef extracts the WHEN clause from a trigger definition // returned by pg_get_triggerdef(). The format is: // "CREATE TRIGGER name ... WHEN (condition) EXECUTE FUNCTION ..." diff --git a/ir/ir.go b/ir/ir.go index f3c18f65..82976e45 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -122,6 +122,7 @@ type View struct { Schema string `json:"schema"` Name string `json:"name"` Definition string `json:"definition"` + Columns []string `json:"columns,omitempty"` // Ordered list of output column names Comment string `json:"comment,omitempty"` Materialized bool `json:"materialized,omitempty"` Indexes map[string]*Index `json:"indexes,omitempty"` // For materialized views only diff --git a/testdata/diff/comment/add_index_comment/plan.json b/testdata/diff/comment/add_index_comment/plan.json index 11a92a63..82f76064 100644 --- a/testdata/diff/comment/add_index_comment/plan.json +++ b/testdata/diff/comment/add_index_comment/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "4f1ff7accdf71ea376888abac067affdc0307d15f9b6fc44b17603c20f63a39b" + "hash": "d5424ad774c4a220725ab96071453230e0817f6ed77df1c3d427be4c28d1d3e5" }, "groups": [ { diff --git a/testdata/diff/comment/add_view_comment/plan.json b/testdata/diff/comment/add_view_comment/plan.json index 17ec7b55..f347b7e0 100644 --- a/testdata/diff/comment/add_view_comment/plan.json +++ b/testdata/diff/comment/add_view_comment/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "c90334f48d6ac1978ac04366373e4611a43d8027dca3e849d191772d87f0373d" + "hash": "5f72cd96b40f589c3c326b1677cfe598fd18d335c67824a10a730f7179a7e79d" }, "groups": [ { diff --git a/testdata/diff/create_index/drop_index/plan.json b/testdata/diff/create_index/drop_index/plan.json index cb1d4a09..f9fe2e23 100644 --- a/testdata/diff/create_index/drop_index/plan.json +++ b/testdata/diff/create_index/drop_index/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "1001a06e3e83694102fd12e599753c2cdc6f10fe057fb695e9a4feb5f8e280d2" + "hash": "f2873843c4de053af739e6d0641037ede53c6c78a5c6b7887f30826a4f6dfe34" }, "groups": [ { diff --git a/testdata/diff/create_materialized_view/alter_materialized_view/plan.json b/testdata/diff/create_materialized_view/alter_materialized_view/plan.json index b8261133..5f7e0e9d 100644 --- a/testdata/diff/create_materialized_view/alter_materialized_view/plan.json +++ b/testdata/diff/create_materialized_view/alter_materialized_view/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "731eb3ecfccfda8ea697e2def6e8a6ef1e3a5abd0707741f23aa416f83db54ab" + "hash": "f0ac3bb964fd207775142c2f9119442b29f04e10c74de4162cf6e468335e5835" }, "groups": [ { diff --git a/testdata/diff/create_materialized_view/drop_materialized_view/plan.json b/testdata/diff/create_materialized_view/drop_materialized_view/plan.json index 83cbd519..be123bd4 100644 --- a/testdata/diff/create_materialized_view/drop_materialized_view/plan.json +++ b/testdata/diff/create_materialized_view/drop_materialized_view/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "11bf597bede0110c90a6788ecf4c5c0a9191c5b762d76812e543b0eb55ee0daf" + "hash": "6ab6529bbf7c84fb6b639938bdaa3ca5af18b75b3de3637421b43469e4342865" }, "groups": [ { diff --git a/testdata/diff/create_trigger/add_trigger/plan.json b/testdata/diff/create_trigger/add_trigger/plan.json index 1987f690..0d6e9d69 100644 --- a/testdata/diff/create_trigger/add_trigger/plan.json +++ b/testdata/diff/create_trigger/add_trigger/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "b640a56ddb8d22697eeec4c5a6db0ebf41b14d2d11089187b3fa71b95e8e2d87" + "hash": "f709d6c4a0ded1f2bbfaa619c74df2f415695c7caecbaab0760a81d802cae7d0" }, "groups": [ { diff --git a/testdata/diff/create_view/alter_view/plan.json b/testdata/diff/create_view/alter_view/plan.json index 92f76e26..7c2149a0 100644 --- a/testdata/diff/create_view/alter_view/plan.json +++ b/testdata/diff/create_view/alter_view/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "4d5048d66c43dc04dca3edf4522e89ec3580dd8258012114edda545c0d345841" + "hash": "c19a9770ce221783099768139e373f972b1e180103d51e663961f38bb6e92194" }, "groups": [ { diff --git a/testdata/diff/create_view/drop_view/plan.json b/testdata/diff/create_view/drop_view/plan.json index 6684948c..1e68a68a 100644 --- a/testdata/diff/create_view/drop_view/plan.json +++ b/testdata/diff/create_view/drop_view/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "9ef269447191864604c91aca5dd54e7b5c2a7569d517434dcfdb9f59df0a468f" + "hash": "51c0c66c5b170a5edf5aa05d24de36669e89212b0f178a1b611ead263e15448d" }, "groups": [ { diff --git a/testdata/diff/dependency/issue_300_function_table_composite_type/plan.json b/testdata/diff/dependency/issue_300_function_table_composite_type/plan.json index d7c550db..cf6debbf 100644 --- a/testdata/diff/dependency/issue_300_function_table_composite_type/plan.json +++ b/testdata/diff/dependency/issue_300_function_table_composite_type/plan.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "pgschema_version": "1.7.1", + "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" diff --git a/testdata/diff/dependency/issue_300_view_depends_on_view/plan.json b/testdata/diff/dependency/issue_300_view_depends_on_view/plan.json index af8ce879..45d6e09d 100644 --- a/testdata/diff/dependency/issue_300_view_depends_on_view/plan.json +++ b/testdata/diff/dependency/issue_300_view_depends_on_view/plan.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "pgschema_version": "1.7.1", + "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" diff --git a/testdata/diff/dependency/issue_308_view_select_star_column_reorder/diff.sql b/testdata/diff/dependency/issue_308_view_select_star_column_reorder/diff.sql new file mode 100644 index 00000000..fe9b65c9 --- /dev/null +++ b/testdata/diff/dependency/issue_308_view_select_star_column_reorder/diff.sql @@ -0,0 +1,12 @@ +ALTER TABLE item ADD COLUMN new_col text; + +DROP VIEW IF EXISTS item_extended RESTRICT; + +CREATE OR REPLACE VIEW item_extended AS + SELECT i.id, + i.title, + i.status, + i.new_col, + c.name AS category_name + FROM item i + JOIN category c ON c.id = i.id; diff --git a/testdata/diff/dependency/issue_308_view_select_star_column_reorder/new.sql b/testdata/diff/dependency/issue_308_view_select_star_column_reorder/new.sql new file mode 100644 index 00000000..979cd9f0 --- /dev/null +++ b/testdata/diff/dependency/issue_308_view_select_star_column_reorder/new.sql @@ -0,0 +1,16 @@ +CREATE TABLE item ( + id uuid PRIMARY KEY, + title text, + status text, + new_col text +); + +CREATE TABLE category ( + id uuid PRIMARY KEY, + name text +); + +CREATE VIEW item_extended AS +SELECT i.*, c.name AS category_name +FROM item i +JOIN category c ON c.id = i.id; diff --git a/testdata/diff/dependency/issue_308_view_select_star_column_reorder/old.sql b/testdata/diff/dependency/issue_308_view_select_star_column_reorder/old.sql new file mode 100644 index 00000000..c8717e4f --- /dev/null +++ b/testdata/diff/dependency/issue_308_view_select_star_column_reorder/old.sql @@ -0,0 +1,15 @@ +CREATE TABLE item ( + id uuid PRIMARY KEY, + title text, + status text +); + +CREATE TABLE category ( + id uuid PRIMARY KEY, + name text +); + +CREATE VIEW item_extended AS +SELECT i.*, c.name AS category_name +FROM item i +JOIN category c ON c.id = i.id; diff --git a/testdata/diff/dependency/issue_308_view_select_star_column_reorder/plan.json b/testdata/diff/dependency/issue_308_view_select_star_column_reorder/plan.json new file mode 100644 index 00000000..10ad2805 --- /dev/null +++ b/testdata/diff/dependency/issue_308_view_select_star_column_reorder/plan.json @@ -0,0 +1,32 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.7.2", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "5286641907331c5566ca08272365dcd0be588ae72a7c17176a7c73b8cc1c1a23" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER TABLE item ADD COLUMN new_col text;", + "type": "table.column", + "operation": "create", + "path": "public.item.new_col" + }, + { + "sql": "DROP VIEW IF EXISTS item_extended RESTRICT;", + "type": "view", + "operation": "alter", + "path": "public.item_extended" + }, + { + "sql": "CREATE OR REPLACE VIEW item_extended AS\n SELECT i.id,\n i.title,\n i.status,\n i.new_col,\n c.name AS category_name\n FROM item i\n JOIN category c ON c.id = i.id;", + "type": "view", + "operation": "alter", + "path": "public.item_extended" + } + ] + } + ] +} diff --git a/testdata/diff/dependency/issue_308_view_select_star_column_reorder/plan.sql b/testdata/diff/dependency/issue_308_view_select_star_column_reorder/plan.sql new file mode 100644 index 00000000..fe9b65c9 --- /dev/null +++ b/testdata/diff/dependency/issue_308_view_select_star_column_reorder/plan.sql @@ -0,0 +1,12 @@ +ALTER TABLE item ADD COLUMN new_col text; + +DROP VIEW IF EXISTS item_extended RESTRICT; + +CREATE OR REPLACE VIEW item_extended AS + SELECT i.id, + i.title, + i.status, + i.new_col, + c.name AS category_name + FROM item i + JOIN category c ON c.id = i.id; diff --git a/testdata/diff/dependency/issue_308_view_select_star_column_reorder/plan.txt b/testdata/diff/dependency/issue_308_view_select_star_column_reorder/plan.txt new file mode 100644 index 00000000..a0883a19 --- /dev/null +++ b/testdata/diff/dependency/issue_308_view_select_star_column_reorder/plan.txt @@ -0,0 +1,28 @@ +Plan: 2 to modify. + +Summary by type: + tables: 1 to modify + views: 1 to modify + +Tables: + ~ item + + new_col (column) + +Views: + ~ item_extended + +DDL to be executed: +-------------------------------------------------- + +ALTER TABLE item ADD COLUMN new_col text; + +DROP VIEW IF EXISTS item_extended RESTRICT; + +CREATE OR REPLACE VIEW item_extended AS + SELECT i.id, + i.title, + i.status, + i.new_col, + c.name AS category_name + FROM item i + JOIN category c ON c.id = i.id; diff --git a/testdata/diff/dependency/table_to_materialized_view/plan.json b/testdata/diff/dependency/table_to_materialized_view/plan.json index abaee90c..1b5ffc1a 100644 --- a/testdata/diff/dependency/table_to_materialized_view/plan.json +++ b/testdata/diff/dependency/table_to_materialized_view/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "a08aef91b00c2d7e8a5340d796b0dfff1405fa6251557833e8ba6c4aa8b490ab" + "hash": "71e22b38d379c8253b2d30e24ea3001d3dd852bc24a363cb9251721eae354e0e" }, "groups": [ { diff --git a/testdata/diff/dependency/table_to_view/plan.json b/testdata/diff/dependency/table_to_view/plan.json index 46399dc9..87ed136c 100644 --- a/testdata/diff/dependency/table_to_view/plan.json +++ b/testdata/diff/dependency/table_to_view/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "599d8a0663cf5915775e7a6a6a643b1b7ceca262a1a066139d90b0a69371d78d" + "hash": "2bbf94e4c07fcfaf9eb9b9dedf281aef14449bcc388f5875cb6e7e1cd5594c36" }, "groups": [ { diff --git a/testdata/diff/migrate/v5/plan.json b/testdata/diff/migrate/v5/plan.json index b12938ef..bd686510 100644 --- a/testdata/diff/migrate/v5/plan.json +++ b/testdata/diff/migrate/v5/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "610b9cb87c1e796cc5716ceaace23fe2820425474054cf4ce10a2f2e2bae879e" + "hash": "d35c347ef96d6e64cb3745559d649fda2b4f2a4b16ffc3bc6a40d10d216c823a" }, "groups": [ { diff --git a/testdata/diff/online/add_materialized_view_index/plan.json b/testdata/diff/online/add_materialized_view_index/plan.json index b25d89ae..addf08d3 100644 --- a/testdata/diff/online/add_materialized_view_index/plan.json +++ b/testdata/diff/online/add_materialized_view_index/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "d406efe9d31c922a6f588c87b0724a50cb6f2d88cc57170855e59dc201315f2b" + "hash": "dfe0bcd1267c9c0da4dd48acf8336d699ccec6529b706ed8786d76fcf2c8f43a" }, "groups": [ { diff --git a/testdata/diff/online/alter_materialized_view_index/plan.json b/testdata/diff/online/alter_materialized_view_index/plan.json index 4255d21f..96a94c3a 100644 --- a/testdata/diff/online/alter_materialized_view_index/plan.json +++ b/testdata/diff/online/alter_materialized_view_index/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.7.2", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "a789dab87de027305ad9ecd75cb4c0080b8e1d4229a82e22ae9bb7e4ec62aa84" + "hash": "bc770ddaf5f2a6240a64435aded0de44672a1e62b8f2d4702ea2833d2074c825" }, "groups": [ { From 8c9297c0956dbe5c1104835db2fc9a3a2aef476c Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Sat, 21 Feb 2026 20:55:11 -0800 Subject: [PATCH 2/2] fix: update outdated field comment in dependentViewsContext Co-Authored-By: Claude Opus 4.6 --- internal/diff/view.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/diff/view.go b/internal/diff/view.go index 7bcef5a8..becf49a0 100644 --- a/internal/diff/view.go +++ b/internal/diff/view.go @@ -673,7 +673,7 @@ func viewDependsOnTable(view *ir.View, tableSchema, tableName string) bool { // dependentViewsContext tracks views that depend on views being recreated type dependentViewsContext struct { - // dependents maps materialized view key (schema.name) to list of dependent regular views + // dependents maps view key (schema.name) to list of dependent views dependents map[string][]*ir.View }