From abc32f1c71d6b03779cf0bad270a140638432912 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 8 Sep 2026 01:33:32 +0800 Subject: [PATCH] fix(cache): avoid quoted identifiers and alias lists for cache sql Closes #8340 --- .../__tests__/database/jdbcDialect.spec.ts | 15 +++++ apps/desktop/src/lib/database/jdbcDialect.ts | 4 +- crates/dbx-core/src/query_result_sql.rs | 57 ++++++++++++++++++- .../dbx-core/src/sql_dialect/table_select.rs | 27 ++++++++- crates/dbx-core/src/sql_dialect/tests.rs | 27 +++++++++ 5 files changed, 124 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts b/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts index 3337230202..d15626fd51 100644 --- a/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts @@ -56,6 +56,20 @@ describe("jdbc dialect inference", () => { jdbc_driver_paths: ["/drivers/intersystems-jdbc-3.10.5.jar"], }), ).toBe("iris"); + // Legacy Caché connections pick the CacheDB.jar from the driver store; the + // jar file name and driver label are the only Intersystems markers there. + expect( + inferJdbcDialect({ + db_type: "jdbc", + jdbc_driver_paths: ["/drivers/CacheDB.jar"], + }), + ).toBe("iris"); + expect( + inferJdbcDialect({ + db_type: "jdbc", + driver_label: "CacheDB", + }), + ).toBe("iris"); }); it("uses IRIS table preview dialect for generic JDBC IRIS connections", () => { @@ -284,6 +298,7 @@ describe("jdbc dialect inference", () => { expect(inferJdbcDialect({ db_type: "jdbc", driver_label: "Kyuubi JDBC", connection_string: "jdbc:hive2://kyuubi.example.com/default" })).toBe("mysql"); expect(inferJdbcDialect({ db_type: "jdbc", connection_string: "jdbc:hive2://hiveserver.example.com/default" })).toBe("mysql"); expect(inferJdbcDialect({ db_type: "jdbc", connection_string: "jdbc:mysql://mysql.example.com/app" })).toBe("mysql"); + expect(inferJdbcDialect({ db_type: "jdbc", jdbc_driver_paths: ["/drivers/mysql-connector-j-8.0.33.jar"] })).toBe("mysql"); }); it("prefers explicit Kyuubi identity over Apache Hive product metadata", () => { diff --git a/apps/desktop/src/lib/database/jdbcDialect.ts b/apps/desktop/src/lib/database/jdbcDialect.ts index 3f7631a53c..9823d2a1e7 100644 --- a/apps/desktop/src/lib/database/jdbcDialect.ts +++ b/apps/desktop/src/lib/database/jdbcDialect.ts @@ -42,7 +42,9 @@ const JDBC_DIALECT_MATCHERS: Array<{ type: DatabaseType; patterns: RegExp[] }> = { type: "sqlite", patterns: [/jdbc:sqlite:/i, /sqlite/i] }, { type: "db2", patterns: [/jdbc:db2:/i, /\bdb2\b/i] }, { type: "informix", patterns: [/jdbc:informix/i, /informix/i] }, - { type: "iris", patterns: [/jdbc:(?:iris|cache):/i, /com\.intersystems\.jdbc\.(?:IRIS|Cache)Driver/i, /intersystems-jdbc/i] }, + // CacheDB.jar (legacy Caché driver) carries none of the intersystems URL or + // class-name markers, so match the jar file name / driver label directly. + { type: "iris", patterns: [/jdbc:(?:iris|cache):/i, /com\.intersystems\.jdbc\.(?:IRIS|Cache)Driver/i, /intersystems-jdbc/i, /cachedb/i] }, ]; // ASE uses Transact-SQL, but treating it as SQL Server globally would also diff --git a/crates/dbx-core/src/query_result_sql.rs b/crates/dbx-core/src/query_result_sql.rs index 9c8501bcd4..5fb85f7e82 100644 --- a/crates/dbx-core/src/query_result_sql.rs +++ b/crates/dbx-core/src/query_result_sql.rs @@ -6,7 +6,8 @@ use serde::{Deserialize, Serialize}; use crate::models::connection::DatabaseType; use crate::sql::{find_statement_at_cursor, find_statement_at_cursor_for_database}; use crate::sql_dialect::{ - firebird_rows_clause, pagination_strategy, quote_table_identifier, PaginationContext, TablePaginationStrategy, + firebird_rows_clause, pagination_strategy, quote_iris_identifier, quote_table_identifier, PaginationContext, + TablePaginationStrategy, }; use sqlparser::ast::{ visit_expressions, Expr, GroupByExpr, LimitClause, ObjectNamePart, OrderByKind, Select, SelectItem, @@ -391,6 +392,8 @@ pub fn build_sorted_query_sql(options: SortedQuerySqlOptions) -> QuerySqlBuildRe } let aliases = build_derived_column_aliases(&options.result_columns); + // Caché/IRIS rejects derived-table column alias lists (`t(col, col)`) + // outright (SQLCODE -25), regardless of delimited-identifier support. let use_derived_column_aliases = options.database_type != Some(DatabaseType::Mysql) && options.database_type != Some(DatabaseType::ClickHouse) // Doris accepts the derived-table alias but not its column-name list. @@ -400,7 +403,8 @@ pub fn build_sorted_query_sql(options: SortedQuerySqlOptions) -> QuerySqlBuildRe && options.database_type != Some(DatabaseType::Dameng) && options.database_type != Some(DatabaseType::Oracle) && options.database_type != Some(DatabaseType::OceanbaseOracle) - && options.database_type != Some(DatabaseType::SapHana); + && options.database_type != Some(DatabaseType::SapHana) + && options.database_type != Some(DatabaseType::Iris); let sort_alias = if use_derived_column_aliases { aliases .get(options.column_index) @@ -422,13 +426,24 @@ pub fn build_sorted_query_sql(options: SortedQuerySqlOptions) -> QuerySqlBuildRe let use_sort_ordinal = !use_derived_column_aliases && matches!( options.database_type, - Some(DatabaseType::Dameng | DatabaseType::Oracle | DatabaseType::OceanbaseOracle | DatabaseType::SapHana) + Some( + DatabaseType::Dameng + | DatabaseType::Oracle + | DatabaseType::OceanbaseOracle + | DatabaseType::SapHana + | DatabaseType::Iris + ) ) && options.result_columns.get(options.column_index).is_some_and(|column| { options.result_columns.iter().filter(|candidate| candidate.eq_ignore_ascii_case(column)).count() > 1 }); let sort_reference = if use_sort_ordinal { (options.column_index + 1).to_string() + } else if options.database_type == Some(DatabaseType::Iris) { + // With delimited identifiers disabled, a quoted ORDER BY name becomes + // a string literal on Caché and the sort silently degrades to a + // constant. Ordinary names must be sent unquoted. + quote_iris_identifier(&sort_alias, None) } else { quote_table_identifier(options.database_type, &sort_alias) }; @@ -4854,6 +4869,42 @@ WHERE u.id = picked.id; ); } + #[test] + fn builds_iris_sorted_query_without_derived_column_alias_list() { + // Caché/IRIS rejects `t(col, col)` derived alias lists (SQLCODE -25) and + // a quoted ORDER BY name becomes a string literal when delimited + // identifiers are disabled, so the wrap must stay alias-free and + // unquoted (#8340). + let result = build_sorted_query_sql(SortedQuerySqlOptions { + original_sql: "SELECT ID, Name FROM SQLUser.CT_Country".to_string(), + database_type: Some(DatabaseType::Iris), + result_columns: vec!["ID".to_string(), "Name".to_string()], + column_index: 1, + column: "Name".to_string(), + direction: QuerySortDirection::Asc, + }); + let sql = result.sql.unwrap(); + assert!(!sql.contains(") t("), "derived column alias list must not be emitted: {sql}"); + assert_eq!(sql, "SELECT * FROM (SELECT ID, Name FROM SQLUser.CT_Country) t ORDER BY Name ASC;"); + } + + #[test] + fn builds_iris_sorted_query_by_ordinal_for_duplicate_columns() { + let result = build_sorted_query_sql(SortedQuerySqlOptions { + original_sql: "SELECT a.id, b.id FROM a JOIN b ON b.a_id = a.id".to_string(), + database_type: Some(DatabaseType::Iris), + result_columns: vec!["ID".to_string(), "id".to_string()], + column_index: 1, + column: "id".to_string(), + direction: QuerySortDirection::Desc, + }); + + assert_eq!( + result.sql.unwrap(), + "SELECT * FROM (SELECT a.id, b.id FROM a JOIN b ON b.a_id = a.id) t ORDER BY 2 DESC;" + ); + } + #[test] fn mysql_sort_preserves_directives_at_outermost_start() { for prefix in [ diff --git a/crates/dbx-core/src/sql_dialect/table_select.rs b/crates/dbx-core/src/sql_dialect/table_select.rs index 332421982c..7cf5f6e1ed 100644 --- a/crates/dbx-core/src/sql_dialect/table_select.rs +++ b/crates/dbx-core/src/sql_dialect/table_select.rs @@ -555,7 +555,17 @@ pub fn build_table_select_sql(options: TableSelectSqlOptions<'_>) -> String { options .order_columns .iter() - .map(|column| format!("{} ASC", quote_table_identifier(database_type, column))) + .map(|column| { + let quoted = if database_type == Some(DatabaseType::Iris) { + // Caché/IRIS may run with delimited identifiers disabled, + // where a quoted ORDER BY name becomes a string literal and + // silently degrades to a constant sort. + quote_iris_identifier(column, None) + } else { + quote_table_identifier(database_type, column) + }; + format!("{quoted} ASC") + }) .collect::>() .join(", ") ) @@ -605,7 +615,20 @@ fn quoted_table_columns_or_star(database_type: Option, columns: &[ if columns.is_empty() { return "*".to_string(); } - columns.iter().map(|column| quote_table_identifier(database_type, column)).collect::>().join(", ") + columns + .iter() + .map(|column| { + if database_type == Some(DatabaseType::Iris) { + // With delimited identifiers disabled, the Caché/IRIS JDBC + // preparser turns a quoted column name into a `:%qpar` host + // variable, so ordinary names must stay unquoted. + quote_iris_identifier(column, None) + } else { + quote_table_identifier(database_type, column) + } + }) + .collect::>() + .join(", ") } fn build_rownum_table_select_sql( diff --git a/crates/dbx-core/src/sql_dialect/tests.rs b/crates/dbx-core/src/sql_dialect/tests.rs index fa13ad7427..97266709fc 100644 --- a/crates/dbx-core/src/sql_dialect/tests.rs +++ b/crates/dbx-core/src/sql_dialect/tests.rs @@ -339,6 +339,33 @@ fn builds_select_sql_with_limit_syntax_for_database_type() { }), "SELECT TOP 100 * FROM Ens.AlarmResponse" ); + // Caché 2016 often runs with delimited identifiers disabled: the JDBC + // preparser turns a quoted column into a `:%qpar` host variable and a + // quoted ORDER BY name into a string literal (constant sort), so ordinary + // column names must stay unquoted (#8340). + assert_eq!( + build_table_select_sql(TableSelectSqlOptions { + database_type: Some(DatabaseType::Iris), + schema: Some("SQLUser"), + table_name: "CT_Country", + columns: &columns, + order_columns: &keys, + limit: 200, + }), + "SELECT TOP 200 id, name FROM SQLUser.CT_Country ORDER BY id ASC" + ); + // Non-Iris dialects keep their own quoting for the same input. + assert_eq!( + build_table_select_sql(TableSelectSqlOptions { + database_type: Some(DatabaseType::Mysql), + schema: None, + table_name: "users", + columns: &columns, + order_columns: &keys, + limit: 100, + }), + "SELECT `id`, `name` FROM `users` ORDER BY `id` ASC LIMIT 100;" + ); assert_eq!( build_table_select_sql(TableSelectSqlOptions { database_type: Some(DatabaseType::Iotdb),