fix(iris): avoid quoted table preview identifiers

Closes #7604
This commit is contained in:
t8y2 committed 2026-09-01 16:15:07 +00:00
1 parent 48f4fac89e
commit bd7ed63468
5 files changed
+63 -27

No files matched your search

@@ -154,6 +154,24 @@ pub fn quote_table_identifier(database_type: Option<DatabaseType>, 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();
@@ -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()
+18 -8
View File
@@ -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!(