From fe03f740b7621d1e85ae592ae930fa0de50396d1 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 26 Jun 2026 19:39:08 +0800 Subject: [PATCH] fix(postgres): handle legacy materialized view source --- crates/dbx-core/src/schema.rs | 91 +++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 5 deletions(-) diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index bbf2a7322..2e2bc94ca 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -3036,16 +3036,42 @@ pub fn sqlserver_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSo } pub fn postgres_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSourceKind) -> String { + postgres_object_source_sql_inner(schema, name, kind, true) +} + +fn postgres_object_source_sql_without_relispopulated(schema: &str, name: &str, kind: &db::ObjectSourceKind) -> String { + postgres_object_source_sql_inner(schema, name, kind, false) +} + +fn postgres_object_source_sql_inner( + schema: &str, + name: &str, + kind: &db::ObjectSourceKind, + include_relispopulated: bool, +) -> String { match kind { db::ObjectSourceKind::View | db::ObjectSourceKind::MaterializedView => { + let materialized_populated_clause = if include_relispopulated { + " || CASE WHEN c.relispopulated THEN ' WITH DATA' ELSE ' WITH NO DATA' END" + } else { + "" + }; + let materialized_viewdef = "regexp_replace(pg_get_viewdef(c.oid, 0), ';[[:space:]]*$', '')"; + let materialized_source_expr = format!( + "CASE WHEN {materialized_viewdef} ~* '^[[:space:]]*CREATE[[:space:]]+(OR[[:space:]]+REPLACE[[:space:]]+)?MATERIALIZED[[:space:]]+VIEW[[:space:]]+' \ + THEN {materialized_viewdef} \ + ELSE format('CREATE MATERIALIZED VIEW %I.%I AS ', n.nspname, c.relname) || {materialized_viewdef}{materialized_populated_clause} \ + END" + ); format!( - "SELECT CASE WHEN c.relkind = 'm' THEN format('CREATE MATERIALIZED VIEW %I.%I AS ', n.nspname, c.relname) || regexp_replace(pg_get_viewdef(c.oid, 0), ';[[:space:]]*$', '') || CASE WHEN c.relispopulated THEN ' WITH DATA' ELSE ' WITH NO DATA' END \ + "SELECT CASE WHEN c.relkind = 'm' THEN {} \ ELSE format('CREATE OR REPLACE VIEW %I.%I AS ', n.nspname, c.relname) || pg_get_viewdef(c.oid, 0) \ END \ FROM pg_catalog.pg_class c \ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \ WHERE n.nspname = {} AND c.relname = {} AND c.relkind IN ('v','m') \ ORDER BY c.oid LIMIT 1", + materialized_source_expr, sql_string(schema), sql_string(name) ) @@ -3372,6 +3398,16 @@ async fn postgres_object_source( let sql = postgres_object_source_sql(schema, name, object_type); match db::postgres::execute_query(pool, &sql).await.and_then(first_string_cell) { Ok(source) => Ok(source), + Err(primary_err) + if postgres_missing_relispopulated_error(&primary_err) + && matches!(object_type, db::ObjectSourceKind::View | db::ObjectSourceKind::MaterializedView) => + { + let fallback_sql = postgres_object_source_sql_without_relispopulated(schema, name, object_type); + db::postgres::execute_query(pool, &fallback_sql) + .await + .and_then(first_string_cell) + .map_err(|fallback_err| format!("{primary_err}; relispopulated fallback failed: {fallback_err}")) + } Err(primary_err) if matches!(object_type, db::ObjectSourceKind::View) => { let fallback_sql = postgres_view_source_fallback_sql(schema, name); db::postgres::execute_query(pool, &fallback_sql) @@ -3383,6 +3419,14 @@ async fn postgres_object_source( } } +fn postgres_missing_relispopulated_error(err: &str) -> bool { + let lower = err.to_ascii_lowercase(); + lower.contains("does not exist") + && (lower.contains("column c.relispopulated") + || lower.contains("column \"c\".\"relispopulated\"") + || lower.contains("column \"relispopulated\"")) +} + #[cfg(test)] mod object_source_tests { use super::*; @@ -3398,16 +3442,53 @@ mod object_source_tests { #[test] fn builds_postgres_object_source_sql_for_views_and_functions() { - assert_eq!( - postgres_object_source_sql("public", "active_users", &ObjectSourceKind::View), - "SELECT CASE WHEN c.relkind = 'm' THEN format('CREATE MATERIALIZED VIEW %I.%I AS ', n.nspname, c.relname) || regexp_replace(pg_get_viewdef(c.oid, 0), ';[[:space:]]*$', '') || CASE WHEN c.relispopulated THEN ' WITH DATA' ELSE ' WITH NO DATA' END ELSE format('CREATE OR REPLACE VIEW %I.%I AS ', n.nspname, c.relname) || pg_get_viewdef(c.oid, 0) END FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND c.relname = 'active_users' AND c.relkind IN ('v','m') ORDER BY c.oid LIMIT 1" - ); + let view_sql = postgres_object_source_sql("public", "active_users", &ObjectSourceKind::View); + + assert!(view_sql.contains("CREATE MATERIALIZED VIEW")); + assert!(view_sql.contains("CREATE OR REPLACE VIEW")); + assert!(view_sql.contains("CASE WHEN c.relispopulated THEN ' WITH DATA' ELSE ' WITH NO DATA' END")); + assert!(view_sql.contains("n.nspname = 'public'")); + assert!(view_sql.contains("c.relname = 'active_users'")); + assert_eq!( postgres_object_source_sql("public", "recalc_score", &ObjectSourceKind::Function), "SELECT pg_get_functiondef(p.oid) FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = 'public' AND p.proname = 'recalc_score' AND p.prokind = 'f' ORDER BY p.oid LIMIT 1" ); } + #[test] + fn builds_postgres_object_source_sql_without_relispopulated_for_legacy_catalogs() { + let sql = postgres_object_source_sql_without_relispopulated( + "public", + "active_users", + &ObjectSourceKind::MaterializedView, + ); + + assert!(sql.contains("CREATE MATERIALIZED VIEW")); + assert!(sql.contains("pg_get_viewdef(c.oid, 0)")); + assert!(!sql.contains("relispopulated")); + } + + #[test] + fn keeps_legacy_materialized_viewdef_when_it_already_contains_create_statement() { + let sql = postgres_object_source_sql("public", "active_users", &ObjectSourceKind::MaterializedView); + + assert!( + sql.contains( + "~* '^[[:space:]]*CREATE[[:space:]]+(OR[[:space:]]+REPLACE[[:space:]]+)?MATERIALIZED[[:space:]]+VIEW[[:space:]]+'" + ) + ); + assert!(sql.contains( + "THEN regexp_replace(pg_get_viewdef(c.oid, 0), ';[[:space:]]*$', '') ELSE format('CREATE MATERIALIZED VIEW" + )); + } + + #[test] + fn detects_legacy_postgres_relispopulated_errors() { + assert!(postgres_missing_relispopulated_error("ERROR: column c.relispopulated does not exist")); + assert!(!postgres_missing_relispopulated_error("ERROR: relation public.relispopulated does not exist")); + } + #[test] fn builds_postgres_view_source_sql_without_regclass_cast() { let sql = postgres_object_source_sql("tenant's schema", "active users", &ObjectSourceKind::View);