From b48f4c4d4ac4e82785435badaf4c0f4088c88da5 Mon Sep 17 00:00:00 2001 From: Robert Mu Date: Wed, 15 Oct 2025 17:44:05 +0800 Subject: [PATCH] fix(relations): Ensure materialized view access method is restored On Cloudberry, Greenplum 7+, materialized views must be created with a specific access method (e.g., 'heap', 'ao_row') and associated storage options in the 'WITH' clause (e.g., 'compresstype=zstd'). The prior implementation did not back up the 'USING' clause. During restore, the database would default to another access method (e.g., 'heap') which might not support the view's storage options. This incompatibility caused the 'CREATE MATERIALIZED VIEW' command to fail validation. This commit corrects the failure by: - Updating the view query for Cloudberry, GPDB 7+ to fetch the access method. - Modifying DDL generation to include the 'USING' clause. - Updating integration tests to verify the fix. Fixes #61 --- backup/predata_relations.go | 8 +++- backup/queries_relations.go | 45 ++++++++++++++----- integration/predata_relations_create_test.go | 7 ++- integration/predata_relations_queries_test.go | 11 ++++- 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/backup/predata_relations.go b/backup/predata_relations.go index 8f42a8a2..ead394ab 100644 --- a/backup/predata_relations.go +++ b/backup/predata_relations.go @@ -444,8 +444,12 @@ func PrintCreateViewStatement(metadataFile *utils.FileWithByteCount, objToc *toc if !view.IsMaterialized { metadataFile.MustPrintf("\n\nCREATE VIEW %s%s AS %s\n", view.FQN(), view.Options, view.Definition.String) } else { - metadataFile.MustPrintf("\n\nCREATE MATERIALIZED VIEW %s%s%s AS %s\nWITH NO DATA\n%s;\n", - view.FQN(), view.Options, tablespaceClause, view.Definition.String[:len(view.Definition.String)-1], view.DistPolicy.Policy) + accessMethodClause := "" + if view.AccessMethodName != "" { + accessMethodClause = fmt.Sprintf(" USING %s", view.AccessMethodName) + } + metadataFile.MustPrintf("\n\nCREATE MATERIALIZED VIEW %s%s%s%s AS %s\nWITH NO DATA\n%s;\n", + view.FQN(), accessMethodClause, view.Options, tablespaceClause, view.Definition.String[:len(view.Definition.String)-1], view.DistPolicy.Policy) } section, entry := view.GetMetadataEntry() tier := globalTierMap[view.GetUniqueID()] diff --git a/backup/queries_relations.go b/backup/queries_relations.go index ed16db98..34652a84 100644 --- a/backup/queries_relations.go +++ b/backup/queries_relations.go @@ -344,16 +344,17 @@ func GetSequenceDefinition(connectionPool *dbconn.DBConn, seqName string) Sequen } type View struct { - Oid uint32 - Schema string - Name string - Options string - Definition sql.NullString - Tablespace string - IsMaterialized bool - DistPolicy DistPolicy - NeedsDummy bool - ColumnDefs []ColumnDefinition + Oid uint32 + Schema string + Name string + Options string + Definition sql.NullString + Tablespace string + IsMaterialized bool + DistPolicy DistPolicy + NeedsDummy bool + ColumnDefs []ColumnDefinition + AccessMethodName string } func (v View) GetMetadataEntry() (string, toc.MetadataEntry) { @@ -412,7 +413,7 @@ func GetAllViews(connectionPool *dbconn.DBConn) []View { // Materialized views were introduced in GPDB 7 and backported to GPDB 6.2. // Reloptions and tablespace added to pg_class in GPDB 6 - atLeast6Query := fmt.Sprintf(` + version6Query := fmt.Sprintf(` SELECT c.oid AS oid, quote_ident(n.nspname) AS schema, @@ -428,11 +429,31 @@ func GetAllViews(connectionPool *dbconn.DBConn) []View { AND %s AND %s`, relationAndSchemaFilterClause(), ExtensionFilterClause("c")) + atLeast7Query := fmt.Sprintf(` + SELECT + c.oid AS oid, + quote_ident(n.nspname) AS schema, + quote_ident(c.relname) AS name, + pg_get_viewdef(c.oid) AS definition, + coalesce(' WITH (' || array_to_string(c.reloptions, ', ') || ')', '') AS options, + coalesce(quote_ident(t.spcname), '') AS tablespace, + c.relkind='m' AS ismaterialized, + coalesce(quote_ident(am.amname), '') AS accessmethodname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace + LEFT JOIN pg_am am ON am.oid = c.relam + WHERE c.relkind IN ('m', 'v') + AND %s + AND %s`, relationAndSchemaFilterClause(), ExtensionFilterClause("c")) + query := "" if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("6") { query = before6Query + } else if connectionPool.Version.IsGPDB() && connectionPool.Version.Is("6") { + query = version6Query } else { - query = atLeast6Query + query = atLeast7Query } results := make([]View, 0) diff --git a/integration/predata_relations_create_test.go b/integration/predata_relations_create_test.go index 3d825f11..5a92c391 100644 --- a/integration/predata_relations_create_test.go +++ b/integration/predata_relations_create_test.go @@ -576,6 +576,9 @@ SET SUBPARTITION TEMPLATE ` + ` }) It("creates a view with privileges, owner, security label, and comment", func() { view := backup.View{Oid: 1, Schema: "public", Name: "simplemview", Definition: sql.NullString{String: " SELECT 1 AS a;", Valid: true}, IsMaterialized: true, DistPolicy: backup.DistPolicy{Policy: "DISTRIBUTED BY (a)"}} + if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() { + view.AccessMethodName = "heap" + } viewMetadata := testutils.DefaultMetadata(toc.OBJ_MATERIALIZED_VIEW, true, true, true, includeSecurityLabels) backup.PrintCreateViewStatement(backupfile, tocfile, view, viewMetadata) @@ -594,7 +597,9 @@ SET SUBPARTITION TEMPLATE ` + ` }) It("creates a materialized view with options", func() { view := backup.View{Oid: 1, Schema: "public", Name: "simplemview", Options: " WITH (fillfactor=10)", Definition: sql.NullString{String: " SELECT 1 AS a;", Valid: true}, IsMaterialized: true, DistPolicy: backup.DistPolicy{Policy: "DISTRIBUTED BY (a)"}} - + if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() { + view.AccessMethodName = "heap" + } backup.PrintCreateViewStatement(backupfile, tocfile, view, backup.ObjectMetadata{}) testhelper.AssertQueryRuns(connectionPool, buffer.String()) diff --git a/integration/predata_relations_queries_test.go b/integration/predata_relations_queries_test.go index af97c5c9..a0fc446e 100644 --- a/integration/predata_relations_queries_test.go +++ b/integration/predata_relations_queries_test.go @@ -512,6 +512,9 @@ PARTITION BY LIST (gender) results := backup.GetAllViews(connectionPool) materialView := backup.View{Oid: 1, Schema: "public", Name: "simplematerialview", Definition: sql.NullString{String: " SELECT 1 AS a;", Valid: true}, IsMaterialized: true, DistPolicy: backup.DistPolicy{Policy: "DISTRIBUTED BY (a)"}} + if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() { + materialView.AccessMethodName = "heap" + } materialView.Oid = testutils.OidFromObjectName(connectionPool, "public", "simplematerialview", backup.TYPE_RELATION) Expect(results).To(HaveLen(1)) @@ -526,7 +529,9 @@ PARTITION BY LIST (gender) results := backup.GetAllViews(connectionPool) materialView := backup.View{Oid: 1, Schema: "public", Name: "simplematerialview", Definition: sql.NullString{String: " SELECT 1 AS a;", Valid: true}, Options: " WITH (fillfactor=50, autovacuum_enabled=false)", IsMaterialized: true, DistPolicy: backup.DistPolicy{Policy: "DISTRIBUTED BY (a)"}} - + if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() { + materialView.AccessMethodName = "heap" + } materialView.Oid = testutils.OidFromObjectName(connectionPool, "public", "simplematerialview", backup.TYPE_RELATION) Expect(results).To(HaveLen(1)) structmatcher.ExpectStructsToMatchExcluding(&materialView, &results[0], "ColumnDefs", "DistPolicy.Oid") @@ -542,7 +547,9 @@ PARTITION BY LIST (gender) results := backup.GetAllViews(connectionPool) materialView := backup.View{Oid: 1, Schema: "public", Name: "simplematerialview", Definition: sql.NullString{String: " SELECT 1 AS a;", Valid: true}, Tablespace: "test_tablespace", IsMaterialized: true, DistPolicy: backup.DistPolicy{Policy: "DISTRIBUTED BY (a)"}} - + if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() { + materialView.AccessMethodName = "heap" + } Expect(results).To(HaveLen(1)) structmatcher.ExpectStructsToMatchExcluding(&materialView, &results[0], "Oid", "ColumnDefs", "DistPolicy.Oid") })