diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index 8a552684b1..8f5b15fcc2 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -25,7 +25,7 @@ use crate::db::agent_driver::{AgentCallError, AgentErrorStage, AgentOperationOut 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::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}; 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(); }; 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) => { 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 { +fn qualify_iris_unqualified_dml(sql: &str, schema: &str, identifier_quote: Option<&str>) -> Option { let dialect = GenericDialect {}; let mut statements = Parser::parse_sql(&dialect, sql).ok()?; if statements.is_empty() { 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; for statement in &mut statements { if !statement_uses_schema_context(statement) { @@ -933,7 +940,7 @@ fn qualify_iris_unqualified_dml(sql: &str, schema: &str) -> Option { let cte_names = statement_cte_names(statement); let table_aliases = statement_table_aliases(statement); 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; } ControlFlow::<()>::Continue(()) @@ -958,8 +965,7 @@ fn qualify_sqlserver_unqualified_dml(sql: &str, schema: &str) -> Option let cte_names = statement_cte_names(statement); let table_aliases = statement_table_aliases(statement); let mut qualifier = SchemaRelationQualifier { - schema, - identifier_quote: '[', + schema_identifier: Ident::with_quote('[', schema), cte_names: &cte_names, table_aliases: &table_aliases, 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 table_aliases = statement_table_aliases(statement); let mut qualifier = SchemaRelationQualifier { - schema, - identifier_quote: identifier_quote_char(identifier_quote), + schema_identifier: Ident::with_quote(identifier_quote_char(identifier_quote), schema), cte_names: &cte_names, table_aliases: &table_aliases, parameterized_table_depth: 0, @@ -1002,8 +1007,7 @@ fn qualify_kingbase_unqualified_relations(sql: &str, schema: &str, identifier_qu } struct SchemaRelationQualifier<'a> { - schema: &'a str, - identifier_quote: char, + schema_identifier: Ident, cte_names: &'a HashSet, table_aliases: &'a HashSet, parameterized_table_depth: usize, @@ -1029,13 +1033,7 @@ impl VisitorMut for SchemaRelationQualifier<'_> { fn post_visit_relation(&mut self, relation: &mut ObjectName) -> ControlFlow { if self.parameterized_table_depth == 0 - && qualify_unqualified_relation_name_with_quote( - relation, - self.schema, - self.cte_names, - self.table_aliases, - self.identifier_quote, - ) + && qualify_unqualified_relation_name(relation, &self.schema_identifier, self.cte_names, self.table_aliases) { 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( name: &mut ObjectName, - schema: &str, + schema_identifier: &Ident, cte_names: &HashSet, table_aliases: &HashSet, -) -> 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, - table_aliases: &HashSet, - identifier_quote: char, ) -> bool { let [ObjectNamePart::Identifier(table)] = name.0.as_slice() else { return false; @@ -1082,10 +1073,7 @@ fn qualify_unqualified_relation_name_with_quote( } let table = table.clone(); - name.0 = vec![ - ObjectNamePart::Identifier(Ident::with_quote(identifier_quote, schema)), - ObjectNamePart::Identifier(table), - ]; + name.0 = vec![ObjectNamePart::Identifier(schema_identifier.clone()), ObjectNamePart::Identifier(table)]; true } @@ -10199,15 +10187,15 @@ for line in sys.stdin: fn iris_execution_context_qualifies_unqualified_dml_tables() { assert_eq!( 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( Some(DatabaseType::Iris), "SELECT * FROM orders o JOIN customers c ON c.id = o.customer_id", Some("Sales"), ); - assert!(qualified_join.contains("FROM \"Sales\".orders")); - assert!(qualified_join.contains("JOIN \"Sales\".customers")); + assert!(qualified_join.contains("FROM Sales.orders")); + assert!(qualified_join.contains("JOIN Sales.customers")); assert!(qualified_join.contains("c.id = o.customer_id")); assert_eq!( 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)", 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!( sql_for_execution_context( @@ -10231,7 +10219,7 @@ for line in sys.stdin: "INSERT INTO events SELECT * FROM staging_events", Some("APP") ), - "INSERT INTO \"APP\".events SELECT * FROM \"APP\".staging_events" + "INSERT INTO APP.events SELECT * FROM APP.staging_events" ); assert_eq!( 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)", 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] fn iris_execution_context_leaves_ddl_and_unparseable_sql_unchanged() { assert_eq!(