Skip to content

fix: reconstruct materialized views WITH NO DATA during validation - #305

Merged
jtayal-stripe merged 1 commit into
mainfrom
fix/matview-with-no-data
Aug 6, 2026
Merged

fix: reconstruct materialized views WITH NO DATA during validation#305
jtayal-stripe merged 1 commit into
mainfrom
fix/matview-with-no-data

Conversation

@jtayal-stripe

@jtayal-stripe jtayal-stripe commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fix a bug where materialized views were reconstructed WITH DATA during plan validation, causing their stored queries to execute unnecessarily.

Root cause: materializedViewSQLGenerator.Add() emitted CREATE MATERIALIZED VIEW ... AS <query> without WITH NO DATA. Postgres defaults to WITH DATA, which populates the view by executing the stored query. This is both unnecessary (the temp database is discarded after validation) and potentially unsafe.

Fix:

  • Append WITH NO DATA to all CREATE MATERIALIZED VIEW DDL generated during schema reconstruction
  • Strip trailing semicolons from pg_get_viewdef() output before appending WITH NO DATA to prevent syntax errors

Changes

pkg/diff/materialized_view_sql_generator.go

  • Add() now emits CREATE MATERIALIZED VIEW ... AS <query> WITH NO DATA instead of CREATE MATERIALIZED VIEW ... AS <query>
  • Trailing semicolons/whitespace from pg_get_viewdef() are trimmed via strings.TrimRight so that WITH NO DATA is part of the same statement

pkg/diff/schema_migration_plan_test.go

Three new unit test cases:

  1. Add materialized view generates WITH NO DATA -- verifies new matviews include WITH NO DATA
  2. Alter materialized view (recreation) generates WITH NO DATA -- verifies the DROP + re-CREATE path includes WITH NO DATA
  3. Add materialized view with options generates WITH NO DATA -- verifies matviews with storage options (e.g. fillfactor) include WITH NO DATA

Test results

Without fix (tests FAIL) -- proving the bug exists

Tests were run against the codebase before the fix was applied (fix stashed), confirming the bug:

=== RUN   TestSchemaMigrationPlanTest/Add_materialized_view_generates_WITH_NO_DATA
    schema_migration_plan_test.go:535:
        Error Trace:    /Users/jtayal/stripe/pg-schema-diff/pkg/diff/schema_migration_plan_test.go:535
        Error:          Not equal:
                        expected: "CREATE MATERIALIZED VIEW \"public\".\"test_mv\" AS\n SELECT 1 AS x\nWITH NO DATA"
                        actual  : "CREATE MATERIALIZED VIEW \"public\".\"test_mv\" AS\n SELECT 1 AS x"

                        Diff:
                        --- Expected
                        +++ Actual
                        @@ -2,3 +2,2 @@
                          SELECT 1 AS x
                        -WITH NO DATA
--- FAIL: TestSchemaMigrationPlanTest/Add_materialized_view_generates_WITH_NO_DATA (0.00s)

=== RUN   TestSchemaMigrationPlanTest/Alter_materialized_view_(recreation)_generates_WITH_NO_DATA
    schema_migration_plan_test.go:535:
        Error Trace:    /Users/jtayal/stripe/pg-schema-diff/pkg/diff/schema_migration_plan_test.go:535
        Error:          Not equal:
                        expected: "CREATE MATERIALIZED VIEW \"public\".\"test_mv\" AS\n SELECT 2 AS x\nWITH NO DATA"
                        actual  : "CREATE MATERIALIZED VIEW \"public\".\"test_mv\" AS\n SELECT 2 AS x"

                        Diff:
                        --- Expected
                        +++ Actual
                        @@ -2,3 +2,2 @@
                          SELECT 2 AS x
                        -WITH NO DATA
--- FAIL: TestSchemaMigrationPlanTest/Alter_materialized_view_(recreation)_generates_WITH_NO_DATA (0.00s)

=== RUN   TestSchemaMigrationPlanTest/Add_materialized_view_with_options_generates_WITH_NO_DATA
    schema_migration_plan_test.go:535:
        Error Trace:    /Users/jtayal/stripe/pg-schema-diff/pkg/diff/schema_migration_plan_test.go:535
        Error:          Not equal:
                        expected: "CREATE MATERIALIZED VIEW \"public\".\"test_mv\" WITH (fillfactor=70) AS\n SELECT 1 AS x\nWITH NO DATA"
                        actual  : "CREATE MATERIALIZED VIEW \"public\".\"test_mv\" WITH (fillfactor=70) AS\n SELECT 1 AS x"

                        Diff:
                        --- Expected
                        +++ Actual
                        @@ -2,3 +2,2 @@
                          SELECT 1 AS x
                        -WITH NO DATA
--- FAIL: TestSchemaMigrationPlanTest/Add_materialized_view_with_options_generates_WITH_NO_DATA (0.00s)

FAIL
FAIL    github.com/stripe/pg-schema-diff/pkg/diff       0.431s

With fix (tests PASS)

--- PASS: TestSchemaMigrationPlanTest/Add_materialized_view_generates_WITH_NO_DATA (0.00s)
--- PASS: TestSchemaMigrationPlanTest/Alter_materialized_view_(recreation)_generates_WITH_NO_DATA (0.00s)
--- PASS: TestSchemaMigrationPlanTest/Add_materialized_view_with_options_generates_WITH_NO_DATA (0.00s)
PASS
ok      github.com/stripe/pg-schema-diff/pkg/diff       0.428s

Full test suite (no regressions)

ok   github.com/stripe/pg-schema-diff/cmd/pg-schema-diff                   38.836s
ok   github.com/stripe/pg-schema-diff/internal/concurrent                   2.571s
ok   github.com/stripe/pg-schema-diff/internal/graph                        1.219s
ok   github.com/stripe/pg-schema-diff/internal/migration_acceptance_tests  188.950s
ok   github.com/stripe/pg-schema-diff/internal/pgdump                       6.073s
ok   github.com/stripe/pg-schema-diff/internal/pgengine                     3.109s
ok   github.com/stripe/pg-schema-diff/internal/pgidentifier                 1.040s
ok   github.com/stripe/pg-schema-diff/internal/schema                      28.604s
ok   github.com/stripe/pg-schema-diff/internal/set                          2.608s
ok   github.com/stripe/pg-schema-diff/internal/util                         2.189s
ok   github.com/stripe/pg-schema-diff/pkg/diff                             15.355s
ok   github.com/stripe/pg-schema-diff/pkg/schema                            7.946s
ok   github.com/stripe/pg-schema-diff/pkg/tempdb                           12.722s

All 14 packages pass, 0 failures.

Test plan

  • Unit test: Add() generates DDL with WITH NO DATA
  • Unit test: matview recreation (alter path) includes WITH NO DATA
  • Unit test: matview with options includes WITH NO DATA
  • Full test suite (14 packages, 25 matview acceptance tests) passes with no regressions
  • Verified tests FAIL without the fix (proving the bug exists on main)

@rajsuvariya-stripe rajsuvariya-stripe left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

During plan validation, setSchemaForEmptyDatabase reconstructs the source schema in a temporary database. For materialized views, the Add() method emitted CREATE MATERIALIZED VIEW ... AS <query> without WITH NO DATA, causing Postgres to execute the view's stored query with the operator's privileges.

A low-privileged user could plant a materialized view WITH NO DATA whose body calls a SECURITY INVOKER function running COPY ... TO PROGRAM. When a privileged operator later runs pg-schema-diff plan, the view would be reconstructed WITH DATA, executing the malicious function as the operator.

The fix appends WITH NO DATA to all CREATE MATERIALIZED VIEW statements. pg-schema-diff generates migration plans and should never implicitly execute user-defined queries during schema reconstruction. Also strips trailing semicolons from pg_get_viewdef() output to prevent syntax errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
Committed-By-Agent: cursor
Co-authored-by: Cursor <cursoragent@cursor.com>
Committed-By-Agent: cursor
Co-authored-by: Cursor <cursoragent@cursor.com>
Committed-By-Agent: cursor
@jtayal-stripe
jtayal-stripe force-pushed the fix/matview-with-no-data branch from 6c43b7f to 775e2a1 Compare August 6, 2026 09:18
@jtayal-stripe
jtayal-stripe merged commit 9ada471 into main Aug 6, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants