fix(postgres): qualify view source relation references

Closes #7137
This commit is contained in:
t8y2 committed 2026-08-25 10:44:25 +00:00
1 parent 51f7b1d8b5
commit 151172f81a
1 file changed
+110 -30
+110 -30
View File
@@ -8023,7 +8023,7 @@ pub fn postgres_object_source_sql(
kind: &db::ObjectSourceKind,
signature: Option<&str>,
) -> String {
postgres_object_source_sql_inner(schema, name, kind, signature, true, false)
postgres_object_source_sql_inner(schema, name, kind, signature, true, false, true)
}
fn postgres_trigger_object_source_sql(schema: &str, name: &str, relation_name: Option<&str>) -> String {
@@ -8050,7 +8050,7 @@ fn opengauss_object_source_sql(
kind: &db::ObjectSourceKind,
signature: Option<&str>,
) -> String {
postgres_object_source_sql_inner(schema, name, kind, signature, true, true)
postgres_object_source_sql_inner(schema, name, kind, signature, true, true, false)
}
fn opengauss_sequence_object_source_sql(schema: &str, name: &str, include_cache: bool) -> String {
@@ -8093,15 +8093,6 @@ fn opengauss_sequence_object_source_sql(schema: &str, name: &str, include_cache:
)
}
fn postgres_object_source_sql_without_relispopulated(
schema: &str,
name: &str,
kind: &db::ObjectSourceKind,
signature: Option<&str>,
) -> String {
postgres_object_source_sql_inner(schema, name, kind, signature, false, false)
}
fn postgres_function_object_source_sql_without_prokind(
schema: &str,
name: &str,
@@ -8155,6 +8146,7 @@ fn postgres_object_source_sql_inner(
signature: Option<&str>,
include_relispopulated: bool,
unwrap_opengauss_record: bool,
isolate_view_search_path: bool,
) -> String {
match kind {
db::ObjectSourceKind::View | db::ObjectSourceKind::MaterializedView => {
@@ -8163,20 +8155,35 @@ fn postgres_object_source_sql_inner(
} else {
""
};
let materialized_viewdef = "regexp_replace(pg_get_viewdef(c.oid, 0), ';[[:space:]]*$', '')";
let viewdef = if isolate_view_search_path {
"pg_catalog.pg_get_viewdef(c.oid, 0)"
} else {
"pg_get_viewdef(c.oid, 0)"
};
let regexp_replace = if isolate_view_search_path { "pg_catalog.regexp_replace" } else { "regexp_replace" };
let format_fn = if isolate_view_search_path { "pg_catalog.format" } else { "format" };
let materialized_viewdef = format!("{regexp_replace}({viewdef}, ';[[: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} \
ELSE {format_fn}('CREATE MATERIALIZED VIEW %I.%I AS ', n.nspname, c.relname) || {materialized_viewdef}{materialized_populated_clause} \
END"
);
let search_path_cte = if isolate_view_search_path {
"WITH dbx_search_path AS (SELECT pg_catalog.set_config('search_path', '', true) AS applied) "
} else {
""
};
let search_path_join = if isolate_view_search_path { " CROSS JOIN dbx_search_path path_guard" } else { "" };
let search_path_guard = if isolate_view_search_path { " AND path_guard.applied IS NOT NULL" } else { "" };
format!(
"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) \
"{search_path_cte}SELECT CASE WHEN c.relkind = 'm' THEN {} \
ELSE {format_fn}('CREATE OR REPLACE VIEW %I.%I AS ', n.nspname, c.relname) || {viewdef} \
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') \
{search_path_join} \
WHERE n.nspname = {} AND c.relname = {} AND c.relkind IN ('v','m'){search_path_guard} \
ORDER BY c.oid LIMIT 1",
materialized_source_expr,
sql_string(schema),
@@ -8353,15 +8360,32 @@ pub(crate) fn mysql_object_source_ddl_column_index(kind: &db::ObjectSourceKind)
}
}
fn postgres_view_source_fallback_sql_inner(schema: &str, name: &str, isolate_search_path: bool) -> String {
if isolate_search_path {
format!(
"WITH dbx_search_path AS (SELECT pg_catalog.set_config('search_path', '', true) AS applied) \
SELECT v.definition \
FROM pg_catalog.pg_views v \
CROSS JOIN dbx_search_path path_guard \
WHERE v.schemaname = {} AND v.viewname = {} AND path_guard.applied IS NOT NULL \
LIMIT 1",
sql_string(schema),
sql_string(name)
)
} else {
format!(
"SELECT definition \
FROM pg_catalog.pg_views \
WHERE schemaname = {} AND viewname = {} \
LIMIT 1",
sql_string(schema),
sql_string(name)
)
}
}
pub fn postgres_view_source_fallback_sql(schema: &str, name: &str) -> String {
format!(
"SELECT definition \
FROM pg_catalog.pg_views \
WHERE schemaname = {} AND viewname = {} \
LIMIT 1",
sql_string(schema),
sql_string(name)
)
postgres_view_source_fallback_sql_inner(schema, name, true)
}
fn first_string_cell(result: db::QueryResult) -> Result<String, String> {
@@ -8740,6 +8764,9 @@ async fn get_object_source_once(
}
PoolKind::Postgres(pool) => {
let unwrap_opengauss_record = db_config.as_ref().is_some_and(is_opengauss_family_config);
let isolate_view_search_path = postgres_view_source_uses_isolated_search_path(
db_config.as_ref().map(|config| &config.db_type),
);
postgres_object_source(
pool,
schema,
@@ -8748,6 +8775,7 @@ async fn get_object_source_once(
signature,
relation_name,
unwrap_opengauss_record,
isolate_view_search_path,
)
.await?
}
@@ -9113,6 +9141,10 @@ async fn db2_column_comments(
comments
}
fn postgres_view_source_uses_isolated_search_path(database_type: Option<&DatabaseType>) -> bool {
database_type == Some(&DatabaseType::Postgres)
}
async fn postgres_object_source(
pool: &deadpool_postgres::Pool,
schema: &str,
@@ -9121,13 +9153,16 @@ async fn postgres_object_source(
signature: Option<&str>,
relation_name: Option<&str>,
unwrap_opengauss_record: bool,
isolate_view_search_path: bool,
) -> Result<String, String> {
let sql = if matches!(object_type, db::ObjectSourceKind::Trigger) {
postgres_trigger_object_source_sql(schema, name, relation_name)
} else if unwrap_opengauss_record {
opengauss_object_source_sql(schema, name, object_type, signature)
} else {
} else if isolate_view_search_path {
postgres_object_source_sql(schema, name, object_type, signature)
} else {
postgres_object_source_sql_inner(schema, name, object_type, signature, true, false, false)
};
match db::postgres::execute_query(pool, &sql).await.and_then(first_string_cell) {
Ok(source) => Ok(source),
@@ -9135,7 +9170,15 @@ async fn postgres_object_source(
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, signature);
let fallback_sql = postgres_object_source_sql_inner(
schema,
name,
object_type,
signature,
false,
false,
isolate_view_search_path,
);
db::postgres::execute_query(pool, &fallback_sql)
.await
.and_then(first_string_cell)
@@ -9178,7 +9221,7 @@ async fn postgres_object_source(
.map_err(|fallback_err| format!("{primary_err}; prokind fallback failed: {fallback_err}"))
}
Err(primary_err) if matches!(object_type, db::ObjectSourceKind::View) => {
let fallback_sql = postgres_view_source_fallback_sql(schema, name);
let fallback_sql = postgres_view_source_fallback_sql_inner(schema, name, isolate_view_search_path);
db::postgres::execute_query(pool, &fallback_sql)
.await
.and_then(first_string_cell)
@@ -9253,6 +9296,11 @@ mod object_source_tests {
fn builds_postgres_object_source_sql_for_views_and_functions() {
let view_sql = postgres_object_source_sql("public", "active_users", &ObjectSourceKind::View, None);
assert!(view_sql
.starts_with("WITH dbx_search_path AS (SELECT pg_catalog.set_config('search_path', '', true) AS applied)"));
assert!(view_sql.contains("pg_catalog.pg_get_viewdef(c.oid, 0)"));
assert!(view_sql.contains("CROSS JOIN dbx_search_path path_guard"));
assert!(view_sql.contains("path_guard.applied IS NOT NULL"));
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"));
@@ -9268,6 +9316,29 @@ mod object_source_tests {
postgres_object_source_sql("public", "recalc_score", &ObjectSourceKind::Function, Some("integer, integer")),
"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' AND pg_get_function_identity_arguments(p.oid) = 'integer, integer' ORDER BY p.oid LIMIT 1"
);
let opengauss_view_sql = opengauss_object_source_sql("public", "active_users", &ObjectSourceKind::View, None);
assert!(!opengauss_view_sql.contains("set_config('search_path'"));
let compatible_view_sql = postgres_object_source_sql_inner(
"public",
"active_users",
&ObjectSourceKind::View,
None,
true,
false,
false,
);
assert!(!compatible_view_sql.contains("set_config('search_path'"));
}
#[test]
fn isolates_view_search_path_only_for_native_postgres() {
assert!(postgres_view_source_uses_isolated_search_path(Some(&DatabaseType::Postgres)));
assert!(!postgres_view_source_uses_isolated_search_path(Some(&DatabaseType::Redshift)));
assert!(!postgres_view_source_uses_isolated_search_path(Some(&DatabaseType::OpenGauss)));
assert!(!postgres_view_source_uses_isolated_search_path(Some(&DatabaseType::Kingbase)));
assert!(!postgres_view_source_uses_isolated_search_path(None));
}
#[test]
@@ -9283,15 +9354,20 @@ mod object_source_tests {
#[test]
fn builds_postgres_object_source_sql_without_relispopulated_for_legacy_catalogs() {
let sql = postgres_object_source_sql_without_relispopulated(
let sql = postgres_object_source_sql_inner(
"public",
"active_users",
&ObjectSourceKind::MaterializedView,
None,
false,
false,
true,
);
assert!(sql.contains("CREATE MATERIALIZED VIEW"));
assert!(sql.contains("pg_get_viewdef(c.oid, 0)"));
assert!(sql.contains("pg_catalog.pg_get_viewdef(c.oid, 0)"));
assert!(sql.contains("pg_catalog.set_config('search_path', '', true)"));
assert!(sql.contains("path_guard.applied IS NOT NULL"));
assert!(!sql.contains("relispopulated"));
}
@@ -9428,7 +9504,7 @@ mod object_source_tests {
)
);
assert!(sql.contains(
"THEN regexp_replace(pg_get_viewdef(c.oid, 0), ';[[:space:]]*$', '') ELSE format('CREATE MATERIALIZED VIEW"
"THEN pg_catalog.regexp_replace(pg_catalog.pg_get_viewdef(c.oid, 0), ';[[:space:]]*$', '') ELSE pg_catalog.format('CREATE MATERIALIZED VIEW"
));
}
@@ -9461,6 +9537,10 @@ mod object_source_tests {
fn builds_postgres_view_source_fallback_sql_from_pg_views() {
assert_eq!(
postgres_view_source_fallback_sql("tenant's schema", "active users"),
"WITH dbx_search_path AS (SELECT pg_catalog.set_config('search_path', '', true) AS applied) SELECT v.definition FROM pg_catalog.pg_views v CROSS JOIN dbx_search_path path_guard WHERE v.schemaname = 'tenant''s schema' AND v.viewname = 'active users' AND path_guard.applied IS NOT NULL LIMIT 1"
);
assert_eq!(
postgres_view_source_fallback_sql_inner("tenant's schema", "active users", false),
"SELECT definition FROM pg_catalog.pg_views WHERE schemaname = 'tenant''s schema' AND viewname = 'active users' LIMIT 1"
);
}