fix(iris): keep ordinary schema names unquoted in qualified queries
This commit is contained in:
1 file changed
+50
-38
@@ -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<String> {
|
||||
fn qualify_iris_unqualified_dml(sql: &str, schema: &str, identifier_quote: Option<&str>) -> Option<String> {
|
||||
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<String> {
|
||||
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<String>
|
||||
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<String>,
|
||||
table_aliases: &'a HashSet<String>,
|
||||
parameterized_table_depth: usize,
|
||||
@@ -1029,13 +1033,7 @@ impl VisitorMut for SchemaRelationQualifier<'_> {
|
||||
|
||||
fn post_visit_relation(&mut self, relation: &mut ObjectName) -> ControlFlow<Self::Break> {
|
||||
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<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 {
|
||||
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!(
|
||||
|
||||
Reference in new issue
Block a user