fix(postgres): handle legacy materialized view source

This commit is contained in:
t8y2 2026-06-26 19:39:08 +08:00
parent fbcf3591d0
commit fe03f740b7
1 changed files with 86 additions and 5 deletions

View File

@ -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);