From 71a30e921e83201ece9d8f3c1230ad4661a2218e Mon Sep 17 00:00:00 2001 From: zipg Date: Fri, 31 Jul 2026 13:51:26 +0800 Subject: [PATCH] fix(gaussdb): support M mode SQL generation and DDL --- apps/desktop/src/components/grid/DataGrid.vue | 1 + .../src/components/objects/ObjectBrowser.vue | 1 + .../sidebar/SidebarTreeRuntimeHost.vue | 7 +- .../src/composables/useDataGridExport.ts | 2 + .../useSidebarTreeExportRuntime.ts | 1 + apps/desktop/src/lib/backend/tauri.ts | 1 + apps/desktop/src/lib/table/tableSelectSql.ts | 12 +- .../src/lib/table/tableSqlTemplates.ts | 22 ++- crates/dbx-core/src/schema.rs | 27 +++- crates/dbx-core/src/table_export.rs | 95 +++++++++++- crates/dbx-core/src/transfer.rs | 146 ++++++++++++++++-- packages/app-tests/tableSqlTemplates.test.ts | 18 +++ 12 files changed, 296 insertions(+), 37 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 38fa789462..c2001fbeb8 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -5223,6 +5223,7 @@ const { copyInsertTargetLabel: computed(() => props.tableMeta?.tableName ?? props.customSaveHandler?.targetLabel), mongoUpdateTarget: computed(() => props.mongoUpdateTarget), databaseType: computed(() => props.databaseType), + identifierQuote: computed(() => connectionStore.connectionIdentifierQuote(props.connectionId)), connectionId: computed(() => props.connectionId), database: computed(() => props.executionDatabase ?? props.database), context: computed(() => props.context), diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 89ab4bca70..1ac98d39b5 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -1876,6 +1876,7 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx", co connectionId: props.connection.id, database: props.database, schema, + identifierQuote: connectionStore.connectionIdentifierQuote(props.connection.id), tableName: row.name, filePath, format, diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index ab05886394..c812920948 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -1248,7 +1248,8 @@ async function loadTemplateContext(allowView = false) { } } - return { node, dbType, tableSchema, columns, tableType }; + const identifierQuote = connectionStore.connectionIdentifierQuote(node.connectionId); + return { node, dbType, identifierQuote, tableSchema, columns, tableType }; } function openSqlTemplateTab(connectionId: string, database: string, schema: string | undefined, catalog: string | undefined, sql: string, title?: string) { @@ -1262,6 +1263,7 @@ async function newSelectTemplate() { if (!context) return; const sql = buildTableSelectTemplate({ databaseType: context.dbType, + identifierQuote: context.identifierQuote, catalog: context.node.catalog, database: context.node.database, schema: context.tableSchema, @@ -1280,6 +1282,7 @@ async function newInsertTemplate() { if (!context) return; const sql = buildTableInsertTemplate({ databaseType: context.dbType, + identifierQuote: context.identifierQuote, catalog: context.node.catalog, database: context.node.database, schema: context.tableSchema, @@ -1299,6 +1302,7 @@ async function newUpdateTemplate() { if (!context) return; const sql = buildTableUpdateTemplate({ databaseType: context.dbType, + identifierQuote: context.identifierQuote, catalog: context.node.catalog, database: context.node.database, schema: context.tableSchema, @@ -1317,6 +1321,7 @@ async function newDeleteTemplate() { if (!context) return; const sql = buildTableDeleteTemplate({ databaseType: context.dbType, + identifierQuote: context.identifierQuote, catalog: context.node.catalog, database: context.node.database, schema: context.tableSchema, diff --git a/apps/desktop/src/composables/useDataGridExport.ts b/apps/desktop/src/composables/useDataGridExport.ts index 3b518b96c4..b877adcbac 100644 --- a/apps/desktop/src/composables/useDataGridExport.ts +++ b/apps/desktop/src/composables/useDataGridExport.ts @@ -76,6 +76,7 @@ export interface UseDataGridExportOptions { copyInsertTargetLabel?: ComputedRef; mongoUpdateTarget?: ComputedRef; databaseType: ComputedRef; + identifierQuote?: ComputedRef; connectionId: ComputedRef; database: ComputedRef; context: ComputedRef<"results" | "table-data" | undefined>; @@ -1002,6 +1003,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) { connectionId: connectionId.value, database: database.value, schema: meta.schema, + identifierQuote: options.identifierQuote?.value, tableName: meta.tableName, filePath: outputPath, format, diff --git a/apps/desktop/src/composables/useSidebarTreeExportRuntime.ts b/apps/desktop/src/composables/useSidebarTreeExportRuntime.ts index 5d866a1e0d..ce12bfc44f 100644 --- a/apps/desktop/src/composables/useSidebarTreeExportRuntime.ts +++ b/apps/desktop/src/composables/useSidebarTreeExportRuntime.ts @@ -284,6 +284,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt connectionId, database, schema: node.schema || undefined, + identifierQuote: connectionStore.connectionIdentifierQuote(connectionId), tableName: node.label, filePath: outputPath, format, diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index e6a28e952e..d49e2d9aeb 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -3538,6 +3538,7 @@ export interface TableExportRequest { connectionId: string; database: string; schema?: string; + identifierQuote?: string; tableName: string; filePath: string; format: "csv" | "xlsx" | "json" | "markdown" | "sql" | "txt"; diff --git a/apps/desktop/src/lib/table/tableSelectSql.ts b/apps/desktop/src/lib/table/tableSelectSql.ts index f3cf435021..786ce8056f 100644 --- a/apps/desktop/src/lib/table/tableSelectSql.ts +++ b/apps/desktop/src/lib/table/tableSelectSql.ts @@ -50,8 +50,8 @@ function quoteCypherIdentifier(name: string): string { return `\`${name.replace(/`/g, "``")}\``; } -export function qualifiedTableName(options: Pick): string { - const { databaseType, schema, tableName, catalog, database } = options; +export function qualifiedTableName(options: Pick): string { + const { databaseType, identifierQuote, schema, tableName, catalog, database } = options; // Doris / StarRocks multi-catalog: address external-catalog tables with the // 3-part `catalog.database.table` form, which the engines accept directly. if (catalog && catalog !== "internal" && (databaseType === "doris" || databaseType === "starrocks")) { @@ -73,6 +73,14 @@ export function qualifiedTableName(options: Pick quoteTableIdentifier(options.databaseType, column.name)); + const selectColumns = columns.map((column) => templateIdentifier(options, column.name)); return `SELECT ${selectColumns.join(", ")}\nFROM ${tableName};`; } @@ -45,7 +46,7 @@ function buildDefaultInsertTemplate(options: TableSqlTemplateOptions): string { return `INSERT INTO ${tableName}\n/* TODO: add column values */\nVALUES ();`; } - const columnNames = columns.map((column) => quoteTableIdentifier(options.databaseType, column.name)); + const columnNames = columns.map((column) => templateIdentifier(options, column.name)); const values = columns.map((column) => columnPlaceholderValue(column, options.databaseType)); return `INSERT INTO ${tableName} (${columnNames.join(", ")})\nVALUES (${values.join(", ")});`; } @@ -84,20 +85,21 @@ export function buildTableUpdateTemplate(options: TableSqlTemplateOptions): stri const primaryKeys = columns.filter((column) => column.is_primary_key); const primaryKeyNames = new Set(primaryKeys.map((column) => column.name)); const updateColumns = columns.filter((column) => !primaryKeyNames.has(column.name)); - const setClause = updateColumns.length ? updateColumns.map((column) => `${quoteTableIdentifier(options.databaseType, column.name)} = ${columnPlaceholderValue(column, options.databaseType)}`).join(",\n ") : "/* TODO: set column = value */"; + const setClause = updateColumns.length ? updateColumns.map((column) => `${templateIdentifier(options, column.name)} = ${columnPlaceholderValue(column, options.databaseType)}`).join(",\n ") : "/* TODO: set column = value */"; - return `UPDATE ${tableName}\nSET ${setClause}\n${buildWhereClause(options.databaseType, primaryKeys)};`; + return `UPDATE ${tableName}\nSET ${setClause}\n${buildWhereClause(options, primaryKeys)};`; } export function buildTableDeleteTemplate(options: TableSqlTemplateOptions): string { const tableName = templateTableName(options); const primaryKeys = (options.columns ?? []).filter((column) => column.is_primary_key); - return `DELETE FROM ${tableName}\n${buildWhereClause(options.databaseType, primaryKeys)};`; + return `DELETE FROM ${tableName}\n${buildWhereClause(options, primaryKeys)};`; } function templateTableName(options: TableSqlTemplateOptions): string { return qualifiedTableName({ databaseType: options.databaseType, + identifierQuote: options.identifierQuote, catalog: options.catalog, database: options.database, schema: options.schema, @@ -105,9 +107,13 @@ function templateTableName(options: TableSqlTemplateOptions): string { }); } -function buildWhereClause(databaseType: DatabaseType | undefined, primaryKeys: ColumnInfo[]): string { +function templateIdentifier(options: TableSqlTemplateOptions, name: string): string { + return quoteTableDataIdentifier(options.databaseType, name, options.identifierQuote); +} + +function buildWhereClause(options: TableSqlTemplateOptions, primaryKeys: ColumnInfo[]): string { if (!primaryKeys.length) return "WHERE /* TODO: add WHERE clause */"; - const conditions = primaryKeys.map((column) => `${quoteTableIdentifier(databaseType, column.name)} = ${columnPlaceholderValue(column, databaseType)}`); + const conditions = primaryKeys.map((column) => `${templateIdentifier(options, column.name)} = ${columnPlaceholderValue(column, options.databaseType)}`); return `WHERE ${conditions.join(" AND ")}`; } diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index f9c296fb87..e2d2982942 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -1,5 +1,6 @@ use crate::connection::{ - connection_url_for_endpoint, database_connection_config, task_client_session_id, AppState, MysqlMode, PoolKind, + connection_url_for_endpoint, database_connection_config, gaussdb_uses_m_jdbc_driver, task_client_session_id, + AppState, MysqlMode, PoolKind, }; use crate::db; use crate::models::connection::{ConnectionConfig, DatabaseType}; @@ -2461,7 +2462,7 @@ mod tests { use super::{ clickhouse_metadata_database, dameng_object_statistics_dba_segments_sql, dameng_object_statistics_rows_only_sql, dameng_object_statistics_user_segments_sql, deduplicate_column_infos, - ephemeral_agent_metadata_session_id, filter_mongodb_agent_collections, + ephemeral_agent_metadata_session_id, external_driver_uses_mysql_ddl, filter_mongodb_agent_collections, filter_mysql_system_databases_for_config, filter_object_infos, filter_table_infos, filter_visible_schema_names, gbase8a_object_statistics_sql, is_agent_postgres_metadata_fallback_config, is_mysql_external_driver_config, is_retryable_metadata_error, metadata_name_or_comment_matches, mysql_external_driver_ddl_from_query_result, @@ -2591,6 +2592,22 @@ mod tests { assert!(!is_mysql_external_driver_config(&config)); } + #[test] + fn gaussdb_m_external_driver_uses_mysql_style_ddl() { + let mut config = test_connection_config(DatabaseType::Gaussdb); + config.driver_profile = Some("gaussdb-m".to_string()); + config.jdbc_driver_class = Some("com.huawei.gaussdb.jdbc.Driver".to_string()); + + assert!(external_driver_uses_mysql_ddl(&config)); + assert_eq!( + mysql_external_driver_ddl_sql("app", "app_schema", "order"), + "SHOW CREATE TABLE `app_schema`.`order`" + ); + + config.driver_profile = Some("gaussdb".to_string()); + assert!(!external_driver_uses_mysql_ddl(&config)); + } + #[test] fn mysql_external_driver_ddl_sql_uses_catalog_and_escaped_identifiers() { assert_eq!( @@ -5418,7 +5435,7 @@ async fn get_table_ddl_core_with_options( { let connections = state.connections.read().await; if let Some(PoolKind::ExternalDriver { config, session, .. }) = connections.get(&pool_key) { - if is_mysql_external_driver_config(config.as_ref()) { + if external_driver_uses_mysql_ddl(config.as_ref()) { let config = config.clone(); let session = session.clone(); drop(connections); @@ -5677,6 +5694,10 @@ fn is_mysql_external_driver_config(config: &ConnectionConfig) -> bool { } } +fn external_driver_uses_mysql_ddl(config: &ConnectionConfig) -> bool { + is_mysql_external_driver_config(config) || gaussdb_uses_m_jdbc_driver(config) +} + fn mysql_external_driver_ddl_sql(database: &str, schema: &str, table: &str) -> String { format!("SHOW CREATE TABLE {}", mysql_qualified_name(mysql_table_metadata_catalog(database, schema), table)) } diff --git a/crates/dbx-core/src/table_export.rs b/crates/dbx-core/src/table_export.rs index 49b8eed585..5cbfcfe43c 100644 --- a/crates/dbx-core/src/table_export.rs +++ b/crates/dbx-core/src/table_export.rs @@ -17,8 +17,8 @@ use crate::db::agent_driver::AgentTableReadStartParams; use crate::models::connection::DatabaseType; use crate::query::{close_query_session, execute_sql_statement_with_options, QueryExecutionOptions}; use crate::transfer::{ - count_sql_with_where, execute_read_on_pool, execute_read_on_pool_with_max_rows, keyset_pagination_sql, - pagination_sql_with_filter_order, qualified_table, quote_identifier, + count_sql_with_where_and_identifier_quote, execute_read_on_pool, execute_read_on_pool_with_max_rows, + keyset_pagination_sql_with_identifier_quote, pagination_sql_with_filter_order_and_identifier_quote, }; use crate::types::QueryResult; use crate::xlsx_export::{finish_streaming_xlsx_workbook, start_streaming_xlsx_workbook_with_options}; @@ -37,6 +37,8 @@ pub struct TableExportRequest { pub connection_id: String, pub database: String, pub schema: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub identifier_quote: Option, pub table_name: String, pub file_path: String, /// "csv", "xlsx", "json", "markdown", "sql", or "txt" @@ -218,7 +220,7 @@ fn table_page_sql( batch_size: usize, ) -> String { if use_keyset { - keyset_pagination_sql( + keyset_pagination_sql_with_identifier_quote( col_names, &request.table_name, request.schema.as_deref().unwrap_or(""), @@ -226,9 +228,10 @@ fn table_page_sql( primary_keys, last_pk_values, batch_size, + request.identifier_quote.as_deref(), ) } else { - pagination_sql_with_filter_order( + pagination_sql_with_filter_order_and_identifier_quote( col_names, &request.table_name, request.schema.as_deref().unwrap_or(""), @@ -238,6 +241,7 @@ fn table_page_sql( request.where_input.as_deref(), request.order_by.as_deref(), primary_keys, + request.identifier_quote.as_deref(), ) } } @@ -248,8 +252,19 @@ fn table_cursor_sql( col_names: &[String], primary_keys: &[String], ) -> String { - let full_table = qualified_table(&request.table_name, request.schema.as_deref().unwrap_or(""), db_type, None); - let col_list = col_names.iter().map(|column| quote_identifier(column, db_type)).collect::>().join(", "); + let full_table = crate::sql_dialect::table_data_qualified_table_name( + Some(*db_type), + request.schema.as_deref(), + &request.table_name, + request.identifier_quote.as_deref(), + ); + let col_list = col_names + .iter() + .map(|column| { + crate::sql_dialect::quote_table_data_identifier(Some(*db_type), column, request.identifier_quote.as_deref()) + }) + .collect::>() + .join(", "); let predicate = crate::sql_dialect::normalize_where_input(request.where_input.as_deref()); let where_clause = if predicate.is_empty() { String::new() } else { format!(" WHERE ({predicate})") }; let order_by = request @@ -265,7 +280,16 @@ fn table_cursor_sql( Some( primary_keys .iter() - .map(|column| format!("{} ASC", quote_identifier(column, db_type))) + .map(|column| { + format!( + "{} ASC", + crate::sql_dialect::quote_table_data_identifier( + Some(*db_type), + column, + request.identifier_quote.as_deref(), + ) + ) + }) .collect::>() .join(", "), ) @@ -1182,12 +1206,13 @@ async fn export_table_data_core_inner( let total_rows = if request.skip_count { None } else { - let count_query = count_sql_with_where( + let count_query = count_sql_with_where_and_identifier_quote( &request.table_name, request.schema.as_deref().unwrap_or(""), &db_type, request.where_input.as_deref(), None, + request.identifier_quote.as_deref(), ); match execute_table_export_count(state, &pool_key, request, &count_query, cancel_token.clone()).await { Ok(result) => result @@ -1906,6 +1931,7 @@ mod tests { connection_id: "conn-1".to_string(), database: "PUBLIC".to_string(), schema: Some("PUBLIC".to_string()), + identifier_quote: None, table_name: "EXPORT_SAMPLE".to_string(), file_path: output.to_string_lossy().into_owned(), format: "csv".to_string(), @@ -2068,6 +2094,7 @@ mod tests { connection_id: "conn-1".to_string(), database: "ORCL".to_string(), schema: Some("APP".to_string()), + identifier_quote: None, table_name: "events".to_string(), file_path: "events.csv".to_string(), format: "csv".to_string(), @@ -2100,6 +2127,57 @@ mod tests { assert!(!sql.contains("ROWNUM")); } + #[test] + fn gaussdb_m_table_export_uses_backticks_across_all_query_paths() { + let request = TableExportRequest { + export_id: "export-gaussdb-m".to_string(), + connection_id: "conn-1".to_string(), + database: "app".to_string(), + schema: Some("app_schema".to_string()), + identifier_quote: Some("`".to_string()), + table_name: "order".to_string(), + file_path: "order.csv".to_string(), + format: "csv".to_string(), + columns: None, + column_types: None, + primary_keys: None, + where_input: None, + order_by: None, + skip_count: false, + batch_size: Some(100), + row_limit: None, + date_time_format: None, + numeric_column_right_align: false, + column_comments: None, + }; + let columns = vec!["id".to_string(), "DisplayName".to_string()]; + let primary_keys = vec!["id".to_string()]; + + assert_eq!( + table_cursor_sql(&request, &DatabaseType::Gaussdb, &columns, &primary_keys), + "SELECT id, `DisplayName` FROM app_schema.`order` ORDER BY id ASC" + ); + assert_eq!( + table_page_sql(&request, &DatabaseType::Gaussdb, &columns, &primary_keys, false, &[], 100, 100), + "SELECT id, `DisplayName` FROM app_schema.`order` ORDER BY id LIMIT 100 OFFSET 100" + ); + assert_eq!( + table_page_sql(&request, &DatabaseType::Gaussdb, &columns, &primary_keys, true, &[json!(10)], 0, 100,), + "SELECT id, `DisplayName` FROM app_schema.`order` WHERE id > 10 ORDER BY id ASC LIMIT 100" + ); + assert_eq!( + count_sql_with_where_and_identifier_quote( + &request.table_name, + request.schema.as_deref().unwrap(), + &DatabaseType::Gaussdb, + None, + None, + request.identifier_quote.as_deref(), + ), + "SELECT COUNT(*) FROM app_schema.`order`" + ); + } + #[test] fn oracle_requested_export_columns_omit_synthetic_rowid_and_keep_metadata_aligned() { let columns = vec!["__DBX_ROWID".to_string(), "ID".to_string(), "NAME".to_string()]; @@ -2118,6 +2196,7 @@ mod tests { connection_id: "conn-1".to_string(), database: "ORCL".to_string(), schema: Some("APP".to_string()), + identifier_quote: None, table_name: "USERS".to_string(), file_path: "users.sql".to_string(), format: "sql".to_string(), diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs index a8ca244a93..4c1a1a3be3 100644 --- a/crates/dbx-core/src/transfer.rs +++ b/crates/dbx-core/src/transfer.rs @@ -143,6 +143,14 @@ pub fn quote_identifier(name: &str, db_type: &DatabaseType) -> String { quote_transfer_identifier(name, db_type) } +fn quote_identifier_with_identifier_quote( + name: &str, + db_type: &DatabaseType, + identifier_quote: Option<&str>, +) -> String { + crate::sql_dialect::quote_table_data_identifier(Some(*db_type), name, identifier_quote) +} + /// Resolve an optional catalog for external-catalog routing in the transfer /// pipeline. Returns `Some(catalog)` only when: /// - the catalog is non-empty and not a built-in catalog name, AND @@ -217,6 +225,25 @@ pub fn qualified_table(table: &str, schema: &str, db_type: &DatabaseType, catalo qualified_transfer_table(table, schema, db_type, effective_catalog) } +fn qualified_table_with_identifier_quote( + table: &str, + schema: &str, + db_type: &DatabaseType, + catalog: Option<&str>, + identifier_quote: Option<&str>, +) -> String { + if crate::sql_dialect::uses_connection_identifier_quote(Some(*db_type), identifier_quote) { + crate::sql_dialect::table_data_qualified_table_name( + Some(*db_type), + (!schema.trim().is_empty()).then_some(schema), + table, + identifier_quote, + ) + } else { + qualified_table(table, schema, db_type, catalog) + } +} + pub fn validate_transfer_target_table_names(request: &TransferRequest) -> Result<(), String> { let mut targets: HashMap = HashMap::new(); for source_table in &request.tables { @@ -851,10 +878,24 @@ fn parse_mysql_extra_clauses(extra: Option<&str>) -> MysqlExtraClauses { } fn postgres_order_by_expression(columns: &[String], db_type: &DatabaseType) -> Option { + postgres_order_by_expression_with_identifier_quote(columns, db_type, None) +} + +fn postgres_order_by_expression_with_identifier_quote( + columns: &[String], + db_type: &DatabaseType, + identifier_quote: Option<&str>, +) -> Option { if columns.is_empty() { return None; } - Some(columns.iter().map(|column| quote_identifier(column, db_type)).collect::>().join(", ")) + Some( + columns + .iter() + .map(|column| quote_identifier_with_identifier_quote(column, db_type, identifier_quote)) + .collect::>() + .join(", "), + ) } fn oracle_rownum_page_sql(col_list: &str, base_sql: String, offset: u64, limit: usize) -> String { @@ -2625,15 +2666,45 @@ pub fn pagination_sql_with_filter_order( order_by: Option<&str>, default_order_columns: &[String], ) -> String { - let full_table = qualified_table(table, schema, db_type, None); - let col_list = columns.iter().map(|c| quote_identifier(c, db_type)).collect::>().join(", "); + pagination_sql_with_filter_order_and_identifier_quote( + columns, + table, + schema, + db_type, + offset, + limit, + where_input, + order_by, + default_order_columns, + None, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn pagination_sql_with_filter_order_and_identifier_quote( + columns: &[String], + table: &str, + schema: &str, + db_type: &DatabaseType, + offset: u64, + limit: usize, + where_input: Option<&str>, + order_by: Option<&str>, + default_order_columns: &[String], + identifier_quote: Option<&str>, +) -> String { + let full_table = qualified_table_with_identifier_quote(table, schema, db_type, None, identifier_quote); + let col_list = columns + .iter() + .map(|c| quote_identifier_with_identifier_quote(c, db_type, identifier_quote)) + .collect::>() + .join(", "); let predicate = crate::sql_dialect::normalize_where_input(where_input); let where_clause = if predicate.is_empty() { String::new() } else { format!(" WHERE ({predicate})") }; - let order_expression = order_by - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(str::to_string) - .or_else(|| postgres_order_by_expression(default_order_columns, db_type)); + let order_expression = + order_by.map(str::trim).filter(|value| !value.is_empty()).map(str::to_string).or_else(|| { + postgres_order_by_expression_with_identifier_quote(default_order_columns, db_type, identifier_quote) + }); match db_type { DatabaseType::Oracle => { @@ -2670,7 +2741,18 @@ pub fn count_sql_with_where( where_input: Option<&str>, catalog: Option<&str>, ) -> String { - let full_table = qualified_table(table, schema, db_type, catalog); + count_sql_with_where_and_identifier_quote(table, schema, db_type, where_input, catalog, None) +} + +pub fn count_sql_with_where_and_identifier_quote( + table: &str, + schema: &str, + db_type: &DatabaseType, + where_input: Option<&str>, + catalog: Option<&str>, + identifier_quote: Option<&str>, +) -> String { + let full_table = qualified_table_with_identifier_quote(table, schema, db_type, catalog, identifier_quote); let predicate = crate::sql_dialect::normalize_where_input(where_input); let where_clause = if predicate.is_empty() { String::new() } else { format!(" WHERE ({predicate})") }; format!("SELECT COUNT(*) FROM {full_table}{where_clause}") @@ -2685,12 +2767,42 @@ pub fn keyset_pagination_sql( last_pk_values: &[serde_json::Value], limit: usize, ) -> String { - let full_table = qualified_table(table, schema, db_type, None); - let col_list = columns.iter().map(|c| quote_identifier(c, db_type)).collect::>().join(", "); - let order = - primary_keys.iter().map(|pk| format!("{} ASC", quote_identifier(pk, db_type))).collect::>().join(", "); + keyset_pagination_sql_with_identifier_quote( + columns, + table, + schema, + db_type, + primary_keys, + last_pk_values, + limit, + None, + ) +} - let where_clause = keyset_where_clause(primary_keys, last_pk_values, db_type); +#[allow(clippy::too_many_arguments)] +pub fn keyset_pagination_sql_with_identifier_quote( + columns: &[String], + table: &str, + schema: &str, + db_type: &DatabaseType, + primary_keys: &[String], + last_pk_values: &[serde_json::Value], + limit: usize, + identifier_quote: Option<&str>, +) -> String { + let full_table = qualified_table_with_identifier_quote(table, schema, db_type, None, identifier_quote); + let col_list = columns + .iter() + .map(|c| quote_identifier_with_identifier_quote(c, db_type, identifier_quote)) + .collect::>() + .join(", "); + let order = primary_keys + .iter() + .map(|pk| format!("{} ASC", quote_identifier_with_identifier_quote(pk, db_type, identifier_quote))) + .collect::>() + .join(", "); + + let where_clause = keyset_where_clause(primary_keys, last_pk_values, db_type, identifier_quote); match db_type { DatabaseType::Oracle => { @@ -2712,12 +2824,16 @@ fn keyset_where_clause( primary_keys: &[String], last_pk_values: &[serde_json::Value], db_type: &DatabaseType, + identifier_quote: Option<&str>, ) -> String { if primary_keys.is_empty() || last_pk_values.is_empty() { return String::new(); } - let quoted_keys = primary_keys.iter().map(|pk| quote_identifier(pk, db_type)).collect::>(); + let quoted_keys = primary_keys + .iter() + .map(|pk| quote_identifier_with_identifier_quote(pk, db_type, identifier_quote)) + .collect::>(); let literals = last_pk_values.iter().map(|v| value_to_sql_literal(v, db_type)).collect::>(); let comparison_count = quoted_keys.len().min(literals.len()); if comparison_count == 0 { diff --git a/packages/app-tests/tableSqlTemplates.test.ts b/packages/app-tests/tableSqlTemplates.test.ts index c7fac225df..7ea1d647d0 100644 --- a/packages/app-tests/tableSqlTemplates.test.ts +++ b/packages/app-tests/tableSqlTemplates.test.ts @@ -76,3 +76,21 @@ test("builds DELETE template with TODO WHERE clause when no primary key exists", 'DELETE FROM "audit"\nWHERE /* TODO: add WHERE clause */;', ); }); + +test("builds GaussDB M templates with the detected backtick identifier mode", () => { + const options = { + databaseType: "gaussdb" as const, + identifierQuote: "`", + schema: "app_schema", + tableName: "order", + columns: [ + col({ name: "id", data_type: "integer", is_primary_key: true }), + col({ name: "DisplayName", data_type: "varchar" }), + ], + }; + + assert.equal(buildTableSelectTemplate(options), "SELECT id, `DisplayName`\nFROM app_schema.`order`;"); + assert.equal(buildTableInsertTemplate(options), "INSERT INTO app_schema.`order` (id, `DisplayName`)\nVALUES (0, 'DisplayName_value');"); + assert.equal(buildTableUpdateTemplate(options), "UPDATE app_schema.`order`\nSET `DisplayName` = 'DisplayName_value'\nWHERE id = 0;"); + assert.equal(buildTableDeleteTemplate(options), "DELETE FROM app_schema.`order`\nWHERE id = 0;"); +});