fix(iris): keep ordinary schema names unquoted in qualified queries

This commit is contained in:
t8y2 authored and GitHub committed 2026-09-17 01:37:59 +08:00
1 parent e4d7113a18
commit 2dfd4e0058
1 file changed
+50 -38
+50 -38
View File
@@ -25,7 +25,7 @@ use crate::db::agent_driver::{AgentCallError, AgentErrorStage, AgentOperationOut
use crate::models::connection::{ConnectionConfig, DatabaseType}; use crate::models::connection::{ConnectionConfig, DatabaseType};
use crate::query_execution_sql::{is_oracle_proven_read_only_statement, is_write_sql, strip_sql_comments_and_literals}; use crate::query_execution_sql::{is_oracle_proven_read_only_statement, is_write_sql, strip_sql_comments_and_literals};
use crate::sql::{split_sql_batches, split_sql_statements, starts_with_executable_sql_keyword_for_database}; use crate::sql::{split_sql_batches, split_sql_statements, starts_with_executable_sql_keyword_for_database};
use crate::sql_dialect::{resolve_for_db, CAP_TRANSACTIONAL_DDL}; use crate::sql_dialect::{quote_iris_identifier, resolve_for_db, CAP_TRANSACTIONAL_DDL};
use crate::sql_risk::{classify_sql_risk_for_database, SqlRisk}; use crate::sql_risk::{classify_sql_risk_for_database, SqlRisk};
pub const QUERY_TIMEOUT: Duration = Duration::from_secs(30); pub const QUERY_TIMEOUT: Duration = Duration::from_secs(30);
@@ -907,7 +907,9 @@ fn sql_for_execution_context_with_identifier_quote(
return sql.to_string(); return sql.to_string();
}; };
match db_type { match db_type {
Some(DatabaseType::Iris) => qualify_iris_unqualified_dml(sql, schema).unwrap_or_else(|| sql.to_string()), Some(DatabaseType::Iris) => {
qualify_iris_unqualified_dml(sql, schema, identifier_quote).unwrap_or_else(|| sql.to_string())
}
Some(DatabaseType::SqlServer) => { Some(DatabaseType::SqlServer) => {
qualify_sqlserver_unqualified_dml(sql, schema).unwrap_or_else(|| sql.to_string()) qualify_sqlserver_unqualified_dml(sql, schema).unwrap_or_else(|| sql.to_string())
} }
@@ -918,13 +920,18 @@ fn sql_for_execution_context_with_identifier_quote(
} }
} }
fn qualify_iris_unqualified_dml(sql: &str, schema: &str) -> Option<String> { fn qualify_iris_unqualified_dml(sql: &str, schema: &str, identifier_quote: Option<&str>) -> Option<String> {
let dialect = GenericDialect {}; let dialect = GenericDialect {};
let mut statements = Parser::parse_sql(&dialect, sql).ok()?; let mut statements = Parser::parse_sql(&dialect, sql).ok()?;
if statements.is_empty() { if statements.is_empty() {
return None; return None;
} }
// Caché/IRIS installations may run with delimited identifiers disabled. The
// JDBC preparser then turns a double-quoted name into a `:%qpar` parameter,
// and the statement fails at prepare with "IDENTIFIER expected". Ordinary
// schema names are case-insensitive there, so they must stay unquoted.
let schema_identifier = Ident::new(quote_iris_identifier(schema, identifier_quote));
let mut changed = false; let mut changed = false;
for statement in &mut statements { for statement in &mut statements {
if !statement_uses_schema_context(statement) { if !statement_uses_schema_context(statement) {
@@ -933,7 +940,7 @@ fn qualify_iris_unqualified_dml(sql: &str, schema: &str) -> Option<String> {
let cte_names = statement_cte_names(statement); let cte_names = statement_cte_names(statement);
let table_aliases = statement_table_aliases(statement); let table_aliases = statement_table_aliases(statement);
let _ = visit_relations_mut(statement, |name| { let _ = visit_relations_mut(statement, |name| {
if qualify_unqualified_relation_name(name, schema, &cte_names, &table_aliases) { if qualify_unqualified_relation_name(name, &schema_identifier, &cte_names, &table_aliases) {
changed = true; changed = true;
} }
ControlFlow::<()>::Continue(()) ControlFlow::<()>::Continue(())
@@ -958,8 +965,7 @@ fn qualify_sqlserver_unqualified_dml(sql: &str, schema: &str) -> Option<String>
let cte_names = statement_cte_names(statement); let cte_names = statement_cte_names(statement);
let table_aliases = statement_table_aliases(statement); let table_aliases = statement_table_aliases(statement);
let mut qualifier = SchemaRelationQualifier { let mut qualifier = SchemaRelationQualifier {
schema, schema_identifier: Ident::with_quote('[', schema),
identifier_quote: '[',
cte_names: &cte_names, cte_names: &cte_names,
table_aliases: &table_aliases, table_aliases: &table_aliases,
parameterized_table_depth: 0, parameterized_table_depth: 0,
@@ -987,8 +993,7 @@ fn qualify_kingbase_unqualified_relations(sql: &str, schema: &str, identifier_qu
let cte_names = statement_cte_names(statement); let cte_names = statement_cte_names(statement);
let table_aliases = statement_table_aliases(statement); let table_aliases = statement_table_aliases(statement);
let mut qualifier = SchemaRelationQualifier { let mut qualifier = SchemaRelationQualifier {
schema, schema_identifier: Ident::with_quote(identifier_quote_char(identifier_quote), schema),
identifier_quote: identifier_quote_char(identifier_quote),
cte_names: &cte_names, cte_names: &cte_names,
table_aliases: &table_aliases, table_aliases: &table_aliases,
parameterized_table_depth: 0, parameterized_table_depth: 0,
@@ -1002,8 +1007,7 @@ fn qualify_kingbase_unqualified_relations(sql: &str, schema: &str, identifier_qu
} }
struct SchemaRelationQualifier<'a> { struct SchemaRelationQualifier<'a> {
schema: &'a str, schema_identifier: Ident,
identifier_quote: char,
cte_names: &'a HashSet<String>, cte_names: &'a HashSet<String>,
table_aliases: &'a HashSet<String>, table_aliases: &'a HashSet<String>,
parameterized_table_depth: usize, parameterized_table_depth: usize,
@@ -1029,13 +1033,7 @@ impl VisitorMut for SchemaRelationQualifier<'_> {
fn post_visit_relation(&mut self, relation: &mut ObjectName) -> ControlFlow<Self::Break> { fn post_visit_relation(&mut self, relation: &mut ObjectName) -> ControlFlow<Self::Break> {
if self.parameterized_table_depth == 0 if self.parameterized_table_depth == 0
&& qualify_unqualified_relation_name_with_quote( && qualify_unqualified_relation_name(relation, &self.schema_identifier, self.cte_names, self.table_aliases)
relation,
self.schema,
self.cte_names,
self.table_aliases,
self.identifier_quote,
)
{ {
self.changed = true; self.changed = true;
} }
@@ -1054,21 +1052,14 @@ fn statement_uses_schema_context(statement: &Statement) -> bool {
) )
} }
/// Qualify a single-part relation with `schema_identifier`, which is already
/// rendered in the dialect's own spelling (quoted where the dialect needs it,
/// unquoted where quoting would break parsing).
fn qualify_unqualified_relation_name( fn qualify_unqualified_relation_name(
name: &mut ObjectName, name: &mut ObjectName,
schema: &str, schema_identifier: &Ident,
cte_names: &HashSet<String>, cte_names: &HashSet<String>,
table_aliases: &HashSet<String>, table_aliases: &HashSet<String>,
) -> bool {
qualify_unqualified_relation_name_with_quote(name, schema, cte_names, table_aliases, '"')
}
fn qualify_unqualified_relation_name_with_quote(
name: &mut ObjectName,
schema: &str,
cte_names: &HashSet<String>,
table_aliases: &HashSet<String>,
identifier_quote: char,
) -> bool { ) -> bool {
let [ObjectNamePart::Identifier(table)] = name.0.as_slice() else { let [ObjectNamePart::Identifier(table)] = name.0.as_slice() else {
return false; return false;
@@ -1082,10 +1073,7 @@ fn qualify_unqualified_relation_name_with_quote(
} }
let table = table.clone(); let table = table.clone();
name.0 = vec![ name.0 = vec![ObjectNamePart::Identifier(schema_identifier.clone()), ObjectNamePart::Identifier(table)];
ObjectNamePart::Identifier(Ident::with_quote(identifier_quote, schema)),
ObjectNamePart::Identifier(table),
];
true true
} }
@@ -10199,15 +10187,15 @@ for line in sys.stdin:
fn iris_execution_context_qualifies_unqualified_dml_tables() { fn iris_execution_context_qualifies_unqualified_dml_tables() {
assert_eq!( assert_eq!(
sql_for_execution_context(Some(DatabaseType::Iris), "SELECT * FROM TABLES", Some("INFORMATION_SCHEMA")), sql_for_execution_context(Some(DatabaseType::Iris), "SELECT * FROM TABLES", Some("INFORMATION_SCHEMA")),
"SELECT * FROM \"INFORMATION_SCHEMA\".TABLES" "SELECT * FROM INFORMATION_SCHEMA.TABLES"
); );
let qualified_join = sql_for_execution_context( let qualified_join = sql_for_execution_context(
Some(DatabaseType::Iris), Some(DatabaseType::Iris),
"SELECT * FROM orders o JOIN customers c ON c.id = o.customer_id", "SELECT * FROM orders o JOIN customers c ON c.id = o.customer_id",
Some("Sales"), Some("Sales"),
); );
assert!(qualified_join.contains("FROM \"Sales\".orders")); assert!(qualified_join.contains("FROM Sales.orders"));
assert!(qualified_join.contains("JOIN \"Sales\".customers")); assert!(qualified_join.contains("JOIN Sales.customers"));
assert!(qualified_join.contains("c.id = o.customer_id")); assert!(qualified_join.contains("c.id = o.customer_id"));
assert_eq!( assert_eq!(
sql_for_execution_context(Some(DatabaseType::Iris), "SELECT * FROM INFORMATION_SCHEMA.TABLES", Some("APP")), sql_for_execution_context(Some(DatabaseType::Iris), "SELECT * FROM INFORMATION_SCHEMA.TABLES", Some("APP")),
@@ -10223,7 +10211,7 @@ for line in sys.stdin:
"WITH recent AS (SELECT * FROM events) SELECT * FROM recent WHERE EXISTS (SELECT 1 FROM audits)", "WITH recent AS (SELECT * FROM events) SELECT * FROM recent WHERE EXISTS (SELECT 1 FROM audits)",
Some("APP") Some("APP")
), ),
"WITH recent AS (SELECT * FROM \"APP\".events) SELECT * FROM recent WHERE EXISTS (SELECT 1 FROM \"APP\".audits)" "WITH recent AS (SELECT * FROM APP.events) SELECT * FROM recent WHERE EXISTS (SELECT 1 FROM APP.audits)"
); );
assert_eq!( assert_eq!(
sql_for_execution_context( sql_for_execution_context(
@@ -10231,7 +10219,7 @@ for line in sys.stdin:
"INSERT INTO events SELECT * FROM staging_events", "INSERT INTO events SELECT * FROM staging_events",
Some("APP") Some("APP")
), ),
"INSERT INTO \"APP\".events SELECT * FROM \"APP\".staging_events" "INSERT INTO APP.events SELECT * FROM APP.staging_events"
); );
assert_eq!( assert_eq!(
sql_for_execution_context( sql_for_execution_context(
@@ -10239,10 +10227,34 @@ for line in sys.stdin:
"UPDATE events SET status = 'done' WHERE id IN (SELECT event_id FROM audit_events)", "UPDATE events SET status = 'done' WHERE id IN (SELECT event_id FROM audit_events)",
Some("APP") Some("APP")
), ),
"UPDATE \"APP\".events SET status = 'done' WHERE id IN (SELECT event_id FROM \"APP\".audit_events)" "UPDATE APP.events SET status = 'done' WHERE id IN (SELECT event_id FROM APP.audit_events)"
); );
} }
#[test]
fn iris_execution_context_keeps_schema_unquoted_for_delimited_identifier_less_servers() {
// A double-quoted schema is turned into a `:%qpar` parameter by the
// Caché/IRIS JDBC preparser when delimited identifiers are disabled,
// which fails at prepare. Ordinary names must stay unquoted; only
// spellings that need a delimited name keep the quote characters.
let qualified = sql_for_execution_context_with_identifier_quote(
Some(DatabaseType::Iris),
"SELECT * FROM events",
Some("SQLUser"),
Some("\""),
);
assert_eq!(qualified, "SELECT * FROM SQLUser.events");
assert!(!qualified.contains('"'));
let quoted = sql_for_execution_context_with_identifier_quote(
Some(DatabaseType::Iris),
"SELECT * FROM events",
Some("My Schema"),
Some("\""),
);
assert_eq!(quoted, "SELECT * FROM \"My Schema\".events");
}
#[test] #[test]
fn iris_execution_context_leaves_ddl_and_unparseable_sql_unchanged() { fn iris_execution_context_leaves_ddl_and_unparseable_sql_unchanged() {
assert_eq!( assert_eq!(