From bd7ed6346814e2a8aff2945359dea559b814cb4a Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 1 Sep 2026 16:15:07 +0000 Subject: [PATCH] fix(iris): avoid quoted table preview identifiers Closes #7604 --- crates/dbx-core/src/data_grid_sql.rs | 19 +++----------- crates/dbx-core/src/sql_dialect.rs | 3 ++- .../dbx-core/src/sql_dialect/identifiers.rs | 18 +++++++++++++ .../dbx-core/src/sql_dialect/table_select.rs | 24 +++++++++++++++-- crates/dbx-core/src/sql_dialect/tests.rs | 26 +++++++++++++------ 5 files changed, 63 insertions(+), 27 deletions(-) diff --git a/crates/dbx-core/src/data_grid_sql.rs b/crates/dbx-core/src/data_grid_sql.rs index 92f78c289c..1b33e3d206 100644 --- a/crates/dbx-core/src/data_grid_sql.rs +++ b/crates/dbx-core/src/data_grid_sql.rs @@ -3123,22 +3123,9 @@ pub(crate) fn qualified_table_name( crate::sql_dialect::qualified_table_name(database_type, schema, table_name) } -fn iris_data_grid_identifier(name: &str, identifier_quote: Option<&str>) -> String { - let mut chars = name.chars(); - let ordinary = chars.next().is_some_and(|first| first == '_' || first == '%' || first.is_alphabetic()) - && chars.all(|ch| ch == '_' || ch.is_alphanumeric()); - if ordinary { - // Caché metadata names are case-insensitive. Quoting every ordinary - // name breaks Caché 2016 when delimited identifiers are disabled. - return name.to_string(); - } - let quote = identifier_quote.map(str::trim).filter(|quote| !quote.is_empty()).unwrap_or("\""); - format!("{quote}{}{quote}", name.replace(quote, &format!("{quote}{quote}"))) -} - fn data_grid_identifier(database_type: Option, name: &str, identifier_quote: Option<&str>) -> String { if database_type == Some(DatabaseType::Iris) { - return iris_data_grid_identifier(name, identifier_quote); + return crate::sql_dialect::quote_iris_identifier(name, identifier_quote); } crate::sql_dialect::quote_table_data_identifier(database_type, name, identifier_quote) } @@ -3152,11 +3139,11 @@ pub(crate) fn data_grid_qualified_table_name( identifier_quote: Option<&str>, ) -> String { if database_type == Some(DatabaseType::Iris) { - let table = iris_data_grid_identifier(table_name, identifier_quote); + let table = crate::sql_dialect::quote_iris_identifier(table_name, identifier_quote); return schema .map(str::trim) .filter(|schema| !schema.is_empty()) - .map(|schema| format!("{}.{table}", iris_data_grid_identifier(schema, identifier_quote))) + .map(|schema| format!("{}.{table}", crate::sql_dialect::quote_iris_identifier(schema, identifier_quote))) .unwrap_or(table); } if crate::sql_dialect::uses_connection_identifier_quote(database_type, identifier_quote) { diff --git a/crates/dbx-core/src/sql_dialect.rs b/crates/dbx-core/src/sql_dialect.rs index 048c937b87..3e63c93dc0 100644 --- a/crates/dbx-core/src/sql_dialect.rs +++ b/crates/dbx-core/src/sql_dialect.rs @@ -38,7 +38,8 @@ pub use identifiers::{ normalize_where_input, qualified_table_name, qualified_table_name_with_catalog, quote_table_identifier, }; pub(crate) use identifiers::{ - parse_sqlserver_linked_schema_ref, qualified_transfer_table, quote_transfer_identifier, transfer_column_identifier, + parse_sqlserver_linked_schema_ref, qualified_transfer_table, quote_iris_identifier, quote_transfer_identifier, + transfer_column_identifier, }; pub use table_select::{ build_count_table_sql, build_table_data_select_sql, build_table_data_select_sql_with_database, diff --git a/crates/dbx-core/src/sql_dialect/identifiers.rs b/crates/dbx-core/src/sql_dialect/identifiers.rs index 651f581978..3277ea8a53 100644 --- a/crates/dbx-core/src/sql_dialect/identifiers.rs +++ b/crates/dbx-core/src/sql_dialect/identifiers.rs @@ -154,6 +154,24 @@ pub fn quote_table_identifier(database_type: Option, name: &str) - } } +/// Quote an IRIS identifier only when its spelling requires a delimited name. +/// +/// Caché/IRIS installations may disable delimited identifiers. The IRIS JDBC +/// preparser turns quoted names into `:%qpar(...)` parameters in that mode, +/// which is not valid in table references. Ordinary metadata names are +/// case-insensitive and can be sent unquoted; names containing separators or +/// other punctuation still need the identifier quote character. +pub(crate) fn quote_iris_identifier(name: &str, identifier_quote: Option<&str>) -> String { + let mut chars = name.chars(); + let ordinary = chars.next().is_some_and(|first| first == '_' || first == '%' || first.is_alphabetic()) + && chars.all(|ch| ch == '_' || ch.is_alphanumeric()); + if ordinary { + return name.to_string(); + } + let quote = identifier_quote.map(str::trim).filter(|quote| !quote.is_empty()).unwrap_or("\""); + format!("{quote}{}{quote}", name.replace(quote, &format!("{quote}{quote}"))) +} + pub(crate) fn quote_gaussdb_jdbc_identifier(name: &str, identifier_quote: &str) -> String { if is_explicitly_quoted_identifier(name) { return name.to_string(); diff --git a/crates/dbx-core/src/sql_dialect/table_select.rs b/crates/dbx-core/src/sql_dialect/table_select.rs index c2343dda20..732cab5e7a 100644 --- a/crates/dbx-core/src/sql_dialect/table_select.rs +++ b/crates/dbx-core/src/sql_dialect/table_select.rs @@ -5,7 +5,7 @@ use super::capabilities::{ }; use super::identifiers::{ normalize_where_input, qualified_table_name, qualified_table_name_with_catalog, quote_gaussdb_jdbc_identifier, - quote_table_identifier, + quote_iris_identifier, quote_table_identifier, }; use super::types::{ TableDataSelectSqlOptions, TableSelectSqlOptions, DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN, @@ -209,6 +209,8 @@ pub fn build_table_data_select_sql_with_database( let table = if let Some(database) = jdbc_tdengine_database { qualified_table_name(Some(DatabaseType::Tdengine), Some(database), &options.table_name) // Doris / StarRocks multi-catalog: prefix the catalog for external-catalog tables. + } else if database_type == Some(DatabaseType::Iris) { + table_data_qualified_table_name(database_type, schema, &options.table_name, options.identifier_quote.as_deref()) } else if uses_connection_identifier_quote(database_type, options.identifier_quote.as_deref()) { table_data_qualified_table_name(database_type, schema, &options.table_name, options.identifier_quote.as_deref()) } else if include_database_name { @@ -416,6 +418,14 @@ pub(crate) fn table_data_qualified_table_name( table_name: &str, identifier_quote: Option<&str>, ) -> String { + if database_type == Some(DatabaseType::Iris) { + let table = quote_iris_identifier(table_name, identifier_quote); + return schema + .map(str::trim) + .filter(|schema| !schema.is_empty()) + .map(|schema| format!("{}.{table}", quote_iris_identifier(schema, identifier_quote))) + .unwrap_or(table); + } if !uses_connection_identifier_quote(database_type, identifier_quote) { return qualified_table_name(database_type, schema, table_name); } @@ -475,7 +485,17 @@ pub fn build_table_select_sql(options: TableSelectSqlOptions<'_>) -> String { if database_type == Some(DatabaseType::VictoriaMetrics) { return format!("{}[1h]", victoriametrics_metric_selector(options.table_name)); } - let table = qualified_table_name(database_type, options.schema, options.table_name); + let table = if database_type == Some(DatabaseType::Iris) { + let table = quote_iris_identifier(options.table_name, None); + options + .schema + .map(str::trim) + .filter(|schema| !schema.is_empty()) + .map(|schema| format!("{}.{table}", quote_iris_identifier(schema, None))) + .unwrap_or(table) + } else { + qualified_table_name(database_type, options.schema, options.table_name) + }; let select_columns = quoted_table_columns_or_star(database_type, options.columns); let order_by = if options.order_columns.is_empty() { String::new() diff --git a/crates/dbx-core/src/sql_dialect/tests.rs b/crates/dbx-core/src/sql_dialect/tests.rs index 43b9bf7091..22fd4a12d7 100644 --- a/crates/dbx-core/src/sql_dialect/tests.rs +++ b/crates/dbx-core/src/sql_dialect/tests.rs @@ -321,7 +321,7 @@ fn builds_select_sql_with_limit_syntax_for_database_type() { order_columns: &[], limit: 100, }), - "SELECT TOP 100 * FROM \"Ens\".\"AlarmResponse\"" + "SELECT TOP 100 * FROM Ens.AlarmResponse" ); assert_eq!( build_table_select_sql(TableSelectSqlOptions { @@ -778,7 +778,7 @@ fn builds_table_data_where_and_schema_queries() { include_row_id: false, ..Default::default() }), - "SELECT TOP 100 * FROM \"Ens\".\"AlarmResponse\"" + "SELECT TOP 100 * FROM Ens.AlarmResponse" ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { @@ -797,7 +797,7 @@ fn builds_table_data_where_and_schema_queries() { include_row_id: false, ..Default::default() }), - "SELECT * FROM \"Ens\".\"AlarmResponse\" WHERE (Status = 'Open') ORDER BY \"ID\" ASC" + "SELECT * FROM Ens.AlarmResponse WHERE (Status = 'Open') ORDER BY \"ID\" ASC" ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { @@ -1078,7 +1078,7 @@ fn explicit_table_data_order_is_preserved() { } #[test] -fn builds_iris_table_data_sql_with_literal_top_and_quoted_object() { +fn builds_iris_table_data_sql_with_literal_top_and_ordinary_object() { let sql = build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Iris), schema: Some("Ens".to_string()), @@ -1095,15 +1095,25 @@ fn builds_iris_table_data_sql_with_literal_top_and_quoted_object() { ..Default::default() }); - assert_eq!( - sql, - "SELECT TOP 25 * FROM \"Ens\".\"AlarmResponse\" WHERE (\"Status\" = 'Open') ORDER BY \"Status\" DESC" - ); + assert_eq!(sql, "SELECT TOP 25 * FROM Ens.AlarmResponse WHERE (\"Status\" = 'Open') ORDER BY \"Status\" DESC"); assert!(!sql.contains("?")); assert!(!sql.contains(":%qpar")); assert!(!sql.contains(" LIMIT ")); } +#[test] +fn iris_table_data_sql_quotes_only_delimited_object_names() { + let sql = build_table_data_select_sql(TableDataSelectSqlOptions { + database_type: Some(DatabaseType::Iris), + schema: Some("App Schema".to_string()), + table_name: "Patient Record".to_string(), + limit: Some(25), + ..Default::default() + }); + + assert_eq!(sql, "SELECT TOP 25 * FROM \"App Schema\".\"Patient Record\""); +} + #[test] fn builds_table_data_special_column_queries() { assert_eq!(