diff --git a/crates/dbx-core/src/db/postgres.rs b/crates/dbx-core/src/db/postgres.rs index 617cb5ebc..5e00e6156 100644 --- a/crates/dbx-core/src/db/postgres.rs +++ b/crates/dbx-core/src/db/postgres.rs @@ -930,6 +930,32 @@ const POSTGRES_COLUMNS_COMPAT_SQL: &str = "SELECT a.attname AS column_name, \ AND a.attnum > 0 AND NOT a.attisdropped \ ORDER BY a.attnum"; +const POSTGRES_COLUMNS_INFORMATION_SCHEMA_SQL: &str = "SELECT c.column_name, \ + CASE WHEN c.data_type = 'USER-DEFINED' THEN c.udt_name ELSE c.data_type END AS full_type, \ + c.is_nullable = 'YES' AS is_nullable, \ + c.column_default, \ + EXISTS ( \ + SELECT 1 FROM information_schema.table_constraints tc \ + JOIN information_schema.key_column_usage kcu \ + ON kcu.constraint_catalog = tc.constraint_catalog \ + AND kcu.constraint_schema = tc.constraint_schema \ + AND kcu.constraint_name = tc.constraint_name \ + AND kcu.table_schema = tc.table_schema \ + AND kcu.table_name = tc.table_name \ + WHERE tc.constraint_type = 'PRIMARY KEY' \ + AND tc.table_schema = c.table_schema \ + AND tc.table_name = c.table_name \ + AND kcu.column_name = c.column_name \ + ) AS is_pk, \ + NULL::text AS column_comment, \ + NULL::text AS column_extra, \ + CAST(c.numeric_precision AS int) AS numeric_precision, \ + CAST(c.numeric_scale AS int) AS numeric_scale, \ + CAST(c.character_maximum_length AS int) AS character_maximum_length \ + FROM information_schema.columns c \ + WHERE c.table_schema = $1 AND c.table_name = $2 \ + ORDER BY c.ordinal_position"; + fn column_info_from_row(row: &Row) -> ColumnInfo { let full_type = row.try_get::<_, Option>(1).ok().flatten().unwrap_or_default(); ColumnInfo { @@ -967,12 +993,19 @@ pub async fn get_columns(pool: &Pool, schema: &str, table: &str) -> Result { let primary_message = pg_error_to_string(primary_error); let fallback_message = pg_error_to_string(fallback_error); - log::debug!( - "[postgres][get_columns:compat-failed] primary_error={} fallback_error={}", - primary_message, - fallback_message - ); - Err(fallback_message) + match get_columns_with_sql(&client, POSTGRES_COLUMNS_INFORMATION_SCHEMA_SQL, schema, table).await { + Ok(columns) => Ok(columns), + Err(information_schema_error) => { + let information_schema_message = pg_error_to_string(information_schema_error); + log::debug!( + "[postgres][get_columns:compat-failed] primary_error={} fallback_error={} information_schema_error={}", + primary_message, + fallback_message, + information_schema_message + ); + Err(information_schema_message) + } + } } }, } @@ -1558,6 +1591,15 @@ mod tests { assert!(POSTGRES_COLUMNS_COMPAT_SQL.contains("col_description")); } + #[test] + fn postgres_column_metadata_has_information_schema_fallback() { + assert!(POSTGRES_COLUMNS_INFORMATION_SCHEMA_SQL.contains("information_schema.columns")); + assert!(POSTGRES_COLUMNS_INFORMATION_SCHEMA_SQL.contains("information_schema.table_constraints")); + assert!(POSTGRES_COLUMNS_INFORMATION_SCHEMA_SQL.contains("information_schema.key_column_usage")); + assert!(!POSTGRES_COLUMNS_INFORMATION_SCHEMA_SQL.contains("pg_attribute")); + assert!(!POSTGRES_COLUMNS_INFORMATION_SCHEMA_SQL.contains("regclass")); + } + #[test] fn postgres_index_metadata_has_legacy_catalog_fallback() { assert!(POSTGRES_INDEXES_SQL.contains("ix.indnkeyatts")); diff --git a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java index be7d8bb77..08a1a5998 100644 --- a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java +++ b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java @@ -32,6 +32,7 @@ import java.sql.Timestamp; import java.sql.Types; import java.time.temporal.TemporalAccessor; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Properties; @@ -488,13 +489,13 @@ public final class DbxJdbcPlugin { if (driverQuirks(connection).useOracleMetadata()) { return oracleGetColumns(conn, oracleEffectiveSchema(conn, schema), table); } - DatabaseMetaData meta = conn.getMetaData(); - Set primaryKeys = primaryKeys(meta, database, schema, table); - appendColumns(result, meta, emptyToNull(database), emptyToNull(schema), table, primaryKeys); - if (result.isEmpty() && database != null) { - primaryKeys = primaryKeys(meta, null, schema, table); - appendColumns(result, meta, null, emptyToNull(schema), table, primaryKeys); - } + DatabaseMetaData meta = conn.getMetaData(); + Set primaryKeys = safePrimaryKeys(meta, database, schema, table); + appendColumns(result, meta, emptyToNull(database), emptyToNull(schema), table, primaryKeys); + if (result.isEmpty() && database != null) { + primaryKeys = safePrimaryKeys(meta, null, schema, table); + appendColumns(result, meta, null, emptyToNull(schema), table, primaryKeys); + } return result; } @@ -603,6 +604,14 @@ public final class DbxJdbcPlugin { return primaryKeys; } + private static Set safePrimaryKeys(DatabaseMetaData meta, String database, String schema, String table) { + try { + return primaryKeys(meta, database, schema, table); + } catch (SQLException ignored) { + return Collections.emptySet(); + } + } + // --- Oracle-specific metadata methods --- private static boolean isOracleUrl(String url) {