From 151172f81a79aab5cd90cc35f582d5e1eccaca9c Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 25 Aug 2026 10:41:37 +0000 Subject: [PATCH] fix(postgres): qualify view source relation references Closes #7137 --- crates/dbx-core/src/schema.rs | 140 ++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 30 deletions(-) diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index b2fcdf829a..3a57f15516 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -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 { @@ -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 { 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" ); }