fix(postgres): support pg10 routine metadata without prokind

This commit is contained in:
Guoyu Su 2026-06-28 20:16:25 +08:00 committed by GitHub
parent 11fac631c7
commit 7b32c71b1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 156 additions and 29 deletions

View File

@ -1513,7 +1513,7 @@ fn like_fuzzy_pattern(value: &str) -> String {
})
}
fn list_objects_sql(include_timestamps: bool) -> &'static str {
fn list_object_relations_sql(include_timestamps: bool) -> &'static str {
if include_timestamps {
return "SELECT c.relname AS object_name, \
CASE c.relkind \
@ -1540,21 +1540,7 @@ fn list_objects_sql(include_timestamps: bool) -> &'static str {
LEFT JOIN LATERAL pg_stat_file( \
CASE WHEN c.relkind IN ('r','m','f','p') THEN pg_relation_filepath(c.oid) END, true \
) stat ON true \
WHERE n.nspname = $1 AND c.relkind IN ('r','v','m','f','p','S') \
UNION ALL \
SELECT p.proname AS object_name, \
CASE p.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS object_type, \
obj_description(p.oid) AS object_comment, \
NULL::text AS created_at, \
CASE WHEN current_setting('track_commit_timestamp', true) = 'on' \
THEN pg_xact_commit_timestamp(p.xmin)::text END AS updated_at, \
NULL::text AS parent_schema, \
NULL::text AS parent_name, \
CASE p.prokind WHEN 'p' THEN 2 ELSE 3 END AS sort_order \
FROM pg_catalog.pg_proc p \
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace \
WHERE n.nspname = $1 AND p.prokind IN ('p','f') \
ORDER BY sort_order, object_name";
WHERE n.nspname = $1 AND c.relkind IN ('r','v','m','f','p','S')";
}
"SELECT c.relname AS object_name, \
@ -1575,9 +1561,27 @@ fn list_objects_sql(include_timestamps: bool) -> &'static str {
LEFT JOIN pg_catalog.pg_inherits i ON i.inhrelid = c.oid \
LEFT JOIN pg_catalog.pg_class pc ON pc.oid = i.inhparent \
LEFT JOIN pg_catalog.pg_namespace pn ON pn.oid = pc.relnamespace \
WHERE n.nspname = $1 AND c.relkind IN ('r','v','m','f','p','S') \
UNION ALL \
SELECT p.proname AS object_name, \
WHERE n.nspname = $1 AND c.relkind IN ('r','v','m','f','p','S')"
}
fn list_object_routines_sql(include_timestamps: bool, has_proc_prokind: bool) -> &'static str {
if has_proc_prokind {
if include_timestamps {
return "SELECT p.proname AS object_name, \
CASE p.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS object_type, \
obj_description(p.oid) AS object_comment, \
NULL::text AS created_at, \
CASE WHEN current_setting('track_commit_timestamp', true) = 'on' \
THEN pg_xact_commit_timestamp(p.xmin)::text END AS updated_at, \
NULL::text AS parent_schema, \
NULL::text AS parent_name, \
CASE p.prokind WHEN 'p' THEN 2 ELSE 3 END AS sort_order \
FROM pg_catalog.pg_proc p \
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace \
WHERE n.nspname = $1 AND p.prokind IN ('p','f')";
}
return "SELECT p.proname AS object_name, \
CASE p.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS object_type, \
obj_description(p.oid) AS object_comment, \
NULL::text AS created_at, \
@ -1587,17 +1591,71 @@ fn list_objects_sql(include_timestamps: bool) -> &'static str {
CASE p.prokind WHEN 'p' THEN 2 ELSE 3 END AS sort_order \
FROM pg_catalog.pg_proc p \
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace \
WHERE n.nspname = $1 AND p.prokind IN ('p','f') \
ORDER BY sort_order, object_name"
WHERE n.nspname = $1 AND p.prokind IN ('p','f')";
}
if include_timestamps {
return "SELECT p.proname AS object_name, \
'FUNCTION' AS object_type, \
obj_description(p.oid) AS object_comment, \
NULL::text AS created_at, \
CASE WHEN current_setting('track_commit_timestamp', true) = 'on' \
THEN pg_xact_commit_timestamp(p.xmin)::text END AS updated_at, \
NULL::text AS parent_schema, \
NULL::text AS parent_name, \
3 AS sort_order \
FROM pg_catalog.pg_proc p \
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace \
WHERE n.nspname = $1 AND NOT p.proisagg AND NOT p.proiswindow";
}
"SELECT p.proname AS object_name, \
'FUNCTION' AS object_type, \
obj_description(p.oid) AS object_comment, \
NULL::text AS created_at, \
NULL::text AS updated_at, \
NULL::text AS parent_schema, \
NULL::text AS parent_name, \
3 AS sort_order \
FROM pg_catalog.pg_proc p \
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace \
WHERE n.nspname = $1 AND NOT p.proisagg AND NOT p.proiswindow"
}
fn list_objects_sql(include_timestamps: bool, has_proc_prokind: bool) -> String {
format!(
"{} UNION ALL {} ORDER BY sort_order, object_name",
list_object_relations_sql(include_timestamps),
list_object_routines_sql(include_timestamps, has_proc_prokind)
)
}
fn postgres_proc_has_prokind_sql() -> &'static str {
"SELECT EXISTS ( \
SELECT 1 \
FROM pg_catalog.pg_attribute \
WHERE attrelid = 'pg_catalog.pg_proc'::regclass \
AND attname = 'prokind' \
AND NOT attisdropped \
)"
}
async fn postgres_proc_has_prokind(client: &deadpool_postgres::Client) -> Result<bool, String> {
let stmt = client.prepare_cached(postgres_proc_has_prokind_sql()).await.map_err(|e| e.to_string())?;
let row = client.query_one(&stmt, &[]).await.map_err(|e| e.to_string())?;
Ok(row.get(0))
}
pub async fn list_objects(pool: &Pool, schema: &str) -> Result<Vec<ObjectInfo>, String> {
let client = checkout_postgres_client(pool, None, super::connection_timeout()).await?;
let stmt = client.prepare_cached(list_objects_sql(true)).await.map_err(|e| e.to_string())?;
let has_proc_prokind = postgres_proc_has_prokind(&client).await?;
let sql = list_objects_sql(true, has_proc_prokind);
let stmt = client.prepare_cached(&sql).await.map_err(|e| e.to_string())?;
let rows = match client.query(&stmt, &[&schema]).await {
Ok(rows) => rows,
Err(_) => {
let stmt = client.prepare_cached(list_objects_sql(false)).await.map_err(|e| e.to_string())?;
let fallback_sql = list_objects_sql(false, has_proc_prokind);
let stmt = client.prepare_cached(&fallback_sql).await.map_err(|e| e.to_string())?;
client.query(&stmt, &[&schema]).await.map_err(|e| e.to_string())?
}
};
@ -3154,7 +3212,7 @@ mod tests {
#[test]
fn list_objects_sql_includes_routines() {
let sql = list_objects_sql(true);
let sql = list_objects_sql(true, true);
assert!(sql.contains("pg_catalog.pg_class"));
assert!(sql.contains("pg_catalog.pg_proc"));
assert!(sql.contains("pg_catalog.pg_inherits"));
@ -3169,7 +3227,7 @@ mod tests {
#[test]
fn list_objects_sql_without_timestamps_omits_stat_file() {
let sql = list_objects_sql(false);
let sql = list_objects_sql(false, true);
assert!(!sql.contains("pg_stat_file"));
assert!(sql.contains("NULL::text AS created_at"));
assert!(sql.contains("NULL::text AS updated_at"));
@ -3177,14 +3235,36 @@ mod tests {
#[test]
fn both_list_objects_sql_variants_use_parameter() {
assert!(list_objects_sql(true).contains("$1"));
assert!(list_objects_sql(false).contains("$1"));
assert!(list_objects_sql(true, true).contains("$1"));
assert!(list_objects_sql(false, true).contains("$1"));
assert!(list_objects_sql(true, false).contains("$1"));
assert!(list_objects_sql(false, false).contains("$1"));
}
#[test]
fn both_list_objects_sql_variants_include_pg_proc() {
assert!(list_objects_sql(true).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(true, true).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(false, true).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(true, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(false, false).contains("pg_catalog.pg_proc"));
}
#[test]
fn legacy_list_objects_sql_avoids_pg11_proc_kind_column() {
let sql = list_objects_sql(true, false);
assert!(!sql.contains("p.prokind"));
assert!(sql.contains("NOT p.proisagg"));
assert!(sql.contains("NOT p.proiswindow"));
assert!(sql.contains("'FUNCTION' AS object_type"));
assert!(!sql.contains("'PROCEDURE'"));
}
#[test]
fn postgres_proc_has_prokind_sql_checks_catalog_attribute() {
let sql = postgres_proc_has_prokind_sql();
assert!(sql.contains("pg_catalog.pg_attribute"));
assert!(sql.contains("'pg_catalog.pg_proc'::regclass"));
assert!(sql.contains("attname = 'prokind'"));
}
#[test]

View File

@ -3062,6 +3062,18 @@ fn postgres_object_source_sql_without_relispopulated(schema: &str, name: &str, k
postgres_object_source_sql_inner(schema, name, kind, false)
}
fn postgres_function_object_source_sql_without_prokind(schema: &str, name: &str) -> String {
format!(
"SELECT pg_get_functiondef(p.oid) \
FROM pg_proc p \
JOIN pg_namespace n ON n.oid = p.pronamespace \
WHERE n.nspname = {} AND p.proname = {} AND NOT p.proisagg AND NOT p.proiswindow \
ORDER BY p.oid LIMIT 1",
sql_string(schema),
sql_string(name)
)
}
fn postgres_object_source_sql_inner(
schema: &str,
name: &str,
@ -3427,6 +3439,16 @@ async fn postgres_object_source(
.and_then(first_string_cell)
.map_err(|fallback_err| format!("{primary_err}; relispopulated fallback failed: {fallback_err}"))
}
Err(primary_err)
if postgres_missing_prokind_error(&primary_err)
&& matches!(object_type, db::ObjectSourceKind::Function) =>
{
let fallback_sql = postgres_function_object_source_sql_without_prokind(schema, name);
db::postgres::execute_query(pool, &fallback_sql)
.await
.and_then(first_string_cell)
.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);
db::postgres::execute_query(pool, &fallback_sql)
@ -3438,6 +3460,14 @@ async fn postgres_object_source(
}
}
fn postgres_missing_prokind_error(err: &str) -> bool {
let lower = err.to_ascii_lowercase();
lower.contains("does not exist")
&& (lower.contains("column p.prokind")
|| lower.contains("column \"p\".\"prokind\"")
|| lower.contains("column \"prokind\""))
}
fn postgres_missing_relispopulated_error(err: &str) -> bool {
let lower = err.to_ascii_lowercase();
lower.contains("does not exist")
@ -3488,6 +3518,16 @@ mod object_source_tests {
assert!(!sql.contains("relispopulated"));
}
#[test]
fn builds_postgres_function_source_sql_without_prokind_for_legacy_catalogs() {
let sql = postgres_function_object_source_sql_without_prokind("public", "recalc_score");
assert_eq!(
sql,
"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 NOT p.proisagg AND NOT p.proiswindow ORDER BY p.oid LIMIT 1"
);
}
#[test]
fn keeps_legacy_materialized_viewdef_when_it_already_contains_create_statement() {
let sql = postgres_object_source_sql("public", "active_users", &ObjectSourceKind::MaterializedView);
@ -3508,6 +3548,13 @@ mod object_source_tests {
assert!(!postgres_missing_relispopulated_error("ERROR: relation public.relispopulated does not exist"));
}
#[test]
fn detects_legacy_postgres_prokind_errors() {
assert!(postgres_missing_prokind_error("ERROR: column p.prokind does not exist"));
assert!(postgres_missing_prokind_error("ERROR: column \"p\".\"prokind\" does not exist"));
assert!(!postgres_missing_prokind_error("ERROR: relation public.prokind 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);