use crate::models::connection::DatabaseType; use serde::{Deserialize, Serialize}; pub const DBX_ROWID_COLUMN: &str = "__DBX_ROWID"; pub const DBX_NEO4J_ELEMENT_ID_COLUMN: &str = "__DBX_ELEMENT_ID"; pub const DBX_TDENGINE_TBNAME_COLUMN: &str = "tbname"; #[derive(Debug, Clone, Copy)] pub struct TableSelectSqlOptions<'a> { pub database_type: Option, pub schema: Option<&'a str>, pub table_name: &'a str, pub columns: &'a [String], pub order_columns: &'a [String], pub limit: usize, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TableDataSelectSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, #[serde(default)] pub primary_keys: Vec, #[serde(default)] pub columns: Vec, #[serde(default)] pub fallback_order_columns: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub order_by: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub limit: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub offset: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub where_input: Option, #[serde(default)] pub include_row_id: bool, } pub fn build_count_table_sql(database_type: Option, schema: Option<&str>, table_name: &str) -> String { format!("SELECT COUNT(*) AS row_count FROM {}", qualified_table_name(database_type, schema, table_name)) } pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String { let database_type = options.database_type; let limit = options.limit.unwrap_or(100); if database_type == Some(DatabaseType::Neo4j) { return build_neo4j_table_select_sql(&options, limit); } let table = qualified_table_name(database_type, options.schema.as_deref(), &options.table_name); let predicate = normalize_where_input(options.where_input.as_deref()); let where_clause = if predicate.is_empty() { String::new() } else { format!(" WHERE ({predicate})") }; let row_id_alias = if options.include_row_id && database_type == Some(DatabaseType::Oracle) { Some("t") } else { None }; let default_order_alias = if database_type == Some(DatabaseType::Jdbc) { Some("dbx_t") } else { row_id_alias }; let default_order_by = if !options.primary_keys.is_empty() { Some( options .primary_keys .iter() .map(|pk| format!("{} ASC", quote_order_identifier(database_type, pk, default_order_alias))) .collect::>() .join(", "), ) } else if !options.fallback_order_columns.is_empty() { Some( options .fallback_order_columns .iter() .map(|column| format!("{} ASC", quote_table_identifier(database_type, column))) .collect::>() .join(", "), ) } else { None }; let order_by = options.order_by.as_deref().filter(|order| !order.trim().is_empty()).or(default_order_by.as_deref()); let order = order_by.map(|order_by| format!(" ORDER BY {order_by}")).unwrap_or_default(); let select_columns = if options.include_row_id && database_type == Some(DatabaseType::Oracle) { format!("ROWIDTOCHAR(t.ROWID) AS \"{DBX_ROWID_COLUMN}\", t.*") } else { build_select_columns(database_type, &options.columns) }; let table_alias = if options.include_row_id && database_type.is_some_and(uses_fetch_first) { format!("{table} t") } else if database_type == Some(DatabaseType::Jdbc) && default_order_by.is_some() { format!("{table} dbx_t") } else { table }; if database_type == Some(DatabaseType::Iris) { return format!("SELECT TOP {limit} {select_columns} FROM {table_alias}{where_clause}{order}"); } if database_type == Some(DatabaseType::Db2) && options.offset.is_some_and(|offset| offset > 0) { return build_db2_table_select_page_sql( &table_alias, &where_clause, order_by, &options.columns, limit, options.offset.unwrap_or(0), ); } if database_type == Some(DatabaseType::Oracle) { return format!("SELECT {select_columns} FROM {table_alias}{where_clause}{order}"); } if database_type.is_some_and(uses_fetch_first) { let offset = options .offset .filter(|offset| *offset > 0) .map(|offset| format!(" OFFSET {offset} ROWS")) .unwrap_or_default(); return format!( "SELECT {select_columns} FROM {table_alias}{where_clause}{order}{offset} FETCH FIRST {limit} ROWS ONLY" ); } if database_type == Some(DatabaseType::SqlServer) { return build_sqlserver_table_select_sql( &table_alias, &where_clause, order_by.unwrap_or("(SELECT NULL)"), &options.columns, limit, options.offset.unwrap_or(0), ); } let offset = options.offset.filter(|offset| *offset > 0).map(|offset| format!(" OFFSET {offset}")).unwrap_or_default(); format!("SELECT {select_columns} FROM {table_alias}{where_clause}{order} LIMIT {limit}{offset};") } pub fn build_table_select_sql(options: TableSelectSqlOptions<'_>) -> String { let database_type = options.database_type; let table = qualified_table_name(database_type, options.schema, options.table_name); let select_columns = if options.columns.is_empty() { "*".to_string() } else { options .columns .iter() .map(|column| quote_table_identifier(database_type, column)) .collect::>() .join(", ") }; let order_by = if options.order_columns.is_empty() { String::new() } else { format!( " ORDER BY {}", options .order_columns .iter() .map(|column| format!("{} ASC", quote_table_identifier(database_type, column))) .collect::>() .join(", ") ) }; let limit = options.limit; if database_type == Some(DatabaseType::Iris) { return format!("SELECT TOP {limit} {select_columns} FROM {table}{order_by}"); } if database_type.is_some_and(uses_fetch_first) { return format!("SELECT {select_columns} FROM {table}{order_by} FETCH FIRST {limit} ROWS ONLY"); } if database_type == Some(DatabaseType::SqlServer) { return format!("SELECT TOP ({limit}) {select_columns} FROM {table}{order_by}"); } format!("SELECT {select_columns} FROM {table}{order_by} LIMIT {limit};") } pub fn qualified_table_name(database_type: Option, schema: Option<&str>, table_name: &str) -> String { if database_type == Some(DatabaseType::Iotdb) { let table_name = quote_table_identifier(database_type, table_name); let schema = schema.map(str::trim).filter(|schema| !schema.is_empty()); if let Some(schema) = schema { if table_name == schema || table_name.starts_with(&format!("{schema}.")) { return table_name; } return format!("{}.{}", quote_table_identifier(database_type, schema), table_name); } return table_name; } if database_type.is_some_and(is_schema_aware) && database_type != Some(DatabaseType::Jdbc) && schema.is_some_and(|schema| !schema.trim().is_empty()) { return format!( "{}.{}", quote_table_identifier(database_type, schema.unwrap()), quote_table_identifier(database_type, table_name) ); } quote_table_identifier(database_type, table_name) } pub fn quote_table_identifier(database_type: Option, name: &str) -> String { match database_type { Some(DatabaseType::Iotdb) => name.to_string(), Some(DatabaseType::Jdbc) if is_simple_jdbc_identifier(name) => name.to_string(), Some(DatabaseType::Jdbc) => format!("`{}`", name.replace('`', "``")), Some( DatabaseType::Mysql | DatabaseType::Goldendb | DatabaseType::StarRocks | DatabaseType::Hive | DatabaseType::Databend | DatabaseType::Tdengine | DatabaseType::Access | DatabaseType::Bigquery, ) => { format!("`{}`", name.replace('`', "``")) } Some(DatabaseType::Informix) if is_simple_informix_identifier(name) => name.to_string(), Some(DatabaseType::Neo4j) => format!("`{}`", name.replace('`', "``")), Some(DatabaseType::SqlServer) => format!("[{}]", name.replace(']', "]]")), _ => format!("\"{}\"", name.replace('"', "\"\"")), } } pub fn normalize_where_input(where_input: Option<&str>) -> String { let trimmed = where_input.unwrap_or("").trim().trim_end_matches(';').trim(); let mut chars = trimmed.chars(); let prefix = chars.by_ref().take(5).collect::(); if prefix.eq_ignore_ascii_case("where") { chars.as_str().trim().to_string() } else { trimmed.to_string() } } fn is_oracle_row_id(database_type: Option, name: &str) -> bool { database_type == Some(DatabaseType::Oracle) && name.eq_ignore_ascii_case(DBX_ROWID_COLUMN) } fn is_tdengine_tbname(database_type: Option, name: &str) -> bool { database_type == Some(DatabaseType::Tdengine) && name.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN) } fn quote_order_identifier(database_type: Option, name: &str, table_alias: Option<&str>) -> String { if is_oracle_row_id(database_type, name) { return table_alias.map(|alias| format!("{alias}.ROWID")).unwrap_or_else(|| "ROWID".to_string()); } if is_tdengine_tbname(database_type, name) { return DBX_TDENGINE_TBNAME_COLUMN.to_string(); } let quoted = quote_table_identifier(database_type, name); table_alias.map(|alias| format!("{alias}.{quoted}")).unwrap_or(quoted) } fn build_select_columns(database_type: Option, columns: &[String]) -> String { if columns.is_empty() { return "*".to_string(); } if database_type == Some(DatabaseType::Tdengine) { let mut tdengine_columns = Vec::new(); if !columns.iter().any(|column| column.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN)) { tdengine_columns.push(DBX_TDENGINE_TBNAME_COLUMN.to_string()); } tdengine_columns.extend(columns.iter().cloned()); return tdengine_columns .iter() .map(|column| { if is_tdengine_tbname(database_type, column) { DBX_TDENGINE_TBNAME_COLUMN.to_string() } else { let ident = quote_table_identifier(database_type, column); format!("{ident} AS {ident}") } }) .collect::>() .join(", "); } if database_type != Some(DatabaseType::Hive) { return "*".to_string(); } columns .iter() .map(|column| { let ident = quote_table_identifier(database_type, column); format!("{ident} AS {ident}") }) .collect::>() .join(", ") } fn build_sqlserver_table_select_sql( table: &str, where_clause: &str, order_by: &str, columns: &[String], limit: usize, offset: usize, ) -> String { let columns_sql = if columns.is_empty() { "*".to_string() } else { columns .iter() .map(|column| quote_table_identifier(Some(DatabaseType::SqlServer), column)) .collect::>() .join(", ") }; let order = if order_by == "(SELECT NULL)" { String::new() } else { format!(" ORDER BY {order_by}") }; if offset == 0 { return format!("SELECT TOP ({limit}) {columns_sql} FROM {table}{where_clause}{order}"); } let page_alias = quote_table_identifier(Some(DatabaseType::SqlServer), "dbx_page"); let row_number_alias = quote_table_identifier(Some(DatabaseType::SqlServer), "__dbx_row_num"); let end = offset + limit; format!( "WITH {page_alias} AS (SELECT {columns_sql}, ROW_NUMBER() OVER (ORDER BY {order_by}) AS {row_number_alias} FROM {table}{where_clause}) SELECT {columns_sql} FROM {page_alias} WHERE {row_number_alias} > {offset} AND {row_number_alias} <= {end} ORDER BY {row_number_alias}" ) } fn build_db2_table_select_page_sql( table: &str, where_clause: &str, order_by: Option<&str>, columns: &[String], limit: usize, offset: usize, ) -> String { let columns_sql = if columns.is_empty() { "*".to_string() } else { columns .iter() .map(|column| quote_table_identifier(Some(DatabaseType::Db2), column)) .collect::>() .join(", ") }; let inner_columns = if columns.is_empty() { "dbx_t.*".to_string() } else { columns .iter() .map(|column| format!("dbx_t.{}", quote_table_identifier(Some(DatabaseType::Db2), column))) .collect::>() .join(", ") }; let order = order_by.map(|order_by| format!("ORDER BY {order_by}")).unwrap_or_default(); let row_number = quote_table_identifier(Some(DatabaseType::Db2), "__dbx_row_num"); let end = offset + limit; format!( "SELECT {columns_sql} FROM (SELECT {inner_columns}, ROW_NUMBER() OVER ({order}) AS {row_number} FROM {table} dbx_t{where_clause}) dbx_page WHERE {row_number} > {offset} AND {row_number} <= {end} ORDER BY {row_number}" ) } fn build_neo4j_table_select_sql(options: &TableDataSelectSqlOptions, limit: usize) -> String { let label = quote_table_identifier(Some(DatabaseType::Neo4j), &options.table_name); let predicate = normalize_where_input(options.where_input.as_deref()); let where_clause = if predicate.is_empty() { String::new() } else { format!(" WHERE {predicate}") }; let returned_columns = if options.columns.is_empty() { "n".to_string() } else { options .columns .iter() .map(|column| { let ident = quote_table_identifier(Some(DatabaseType::Neo4j), column); format!("n.{ident} AS {ident}") }) .collect::>() .join(", ") }; let returns = format!( "elementId(n) AS {}, {returned_columns}", quote_table_identifier(Some(DatabaseType::Neo4j), DBX_NEO4J_ELEMENT_ID_COLUMN) ); let default_order_by = if options.primary_keys.is_empty() { None } else { Some( options .primary_keys .iter() .map(|pk| format!("n.{} ASC", quote_table_identifier(Some(DatabaseType::Neo4j), pk))) .collect::>() .join(", "), ) }; let order_by = options.order_by.as_deref().filter(|order| !order.trim().is_empty()).or(default_order_by.as_deref()); let order = order_by.map(|order_by| format!(" ORDER BY {order_by}")).unwrap_or_default(); let skip = options.offset.filter(|offset| *offset > 0).map(|offset| format!(" SKIP {offset}")).unwrap_or_default(); format!("MATCH (n:{label}){where_clause} RETURN {returns}{order}{skip} LIMIT {limit};") } pub fn is_schema_aware(database_type: DatabaseType) -> bool { matches!( database_type, DatabaseType::Postgres | DatabaseType::SqlServer | DatabaseType::Oracle | DatabaseType::Redshift | DatabaseType::Dameng | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Vastbase | DatabaseType::Yashandb | DatabaseType::Databricks | DatabaseType::SapHana | DatabaseType::Teradata | DatabaseType::Vertica | DatabaseType::Exasol | DatabaseType::OpenGauss | DatabaseType::OceanbaseOracle | DatabaseType::Gbase | DatabaseType::Databend | DatabaseType::Jdbc | DatabaseType::H2 | DatabaseType::Snowflake | DatabaseType::Trino | DatabaseType::Hive | DatabaseType::Db2 | DatabaseType::Tdengine | DatabaseType::DuckDb | DatabaseType::Iris ) } pub fn uses_fetch_first(database_type: DatabaseType) -> bool { matches!(database_type, DatabaseType::Oracle | DatabaseType::Dameng | DatabaseType::Db2) } fn is_simple_informix_identifier(name: &str) -> bool { let mut chars = name.chars(); let Some(first) = chars.next() else { return false; }; (first.is_ascii_alphabetic() || first == '_') && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '$') } fn is_simple_jdbc_identifier(name: &str) -> bool { let mut chars = name.chars(); let Some(first) = chars.next() else { return false; }; (first.is_ascii_alphabetic() || first == '_') && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '$') } #[cfg(test)] mod tests { use super::*; #[test] fn quotes_identifiers_by_database_type() { assert_eq!(quote_table_identifier(Some(DatabaseType::Mysql), "user`name"), "`user``name`"); assert_eq!(quote_table_identifier(Some(DatabaseType::Goldendb), "user`name"), "`user``name`"); assert_eq!(quote_table_identifier(Some(DatabaseType::StarRocks), "user`name"), "`user``name`"); assert_eq!(quote_table_identifier(Some(DatabaseType::SqlServer), "user]name"), "[user]]name]"); assert_eq!(quote_table_identifier(Some(DatabaseType::Postgres), "user\"name"), "\"user\"\"name\""); assert_eq!(quote_table_identifier(Some(DatabaseType::Informix), "users_1"), "users_1"); assert_eq!(quote_table_identifier(Some(DatabaseType::Jdbc), "users_1"), "users_1"); assert_eq!(quote_table_identifier(Some(DatabaseType::Jdbc), "user name"), "`user name`"); assert_eq!(quote_table_identifier(Some(DatabaseType::Iotdb), "root.test.device2"), "root.test.device2"); } #[test] fn qualifies_schema_only_for_schema_aware_databases() { assert_eq!(qualified_table_name(Some(DatabaseType::Postgres), Some("public"), "users"), "\"public\".\"users\""); assert_eq!(qualified_table_name(Some(DatabaseType::Kwdb), Some("public"), "users"), "\"public\".\"users\""); assert_eq!(qualified_table_name(Some(DatabaseType::Mysql), Some("public"), "users"), "`users`"); assert_eq!(qualified_table_name(Some(DatabaseType::Goldendb), Some("public"), "users"), "`users`"); assert_eq!(qualified_table_name(Some(DatabaseType::Databend), Some("dbx_test"), "users"), "`dbx_test`.`users`"); assert_eq!(qualified_table_name(Some(DatabaseType::Jdbc), Some("cbsdw_dwd"), "dwd_test_df"), "dwd_test_df"); assert_eq!(qualified_table_name(Some(DatabaseType::Iotdb), Some("root.test"), "device2"), "root.test.device2"); assert_eq!( qualified_table_name(Some(DatabaseType::Iotdb), Some("root.test"), "root.test.device2"), "root.test.device2" ); } #[test] fn builds_select_sql_with_limit_syntax_for_database_type() { let columns = vec!["id".to_string(), "name".to_string()]; let keys = vec!["id".to_string()]; assert_eq!( build_table_select_sql(TableSelectSqlOptions { database_type: Some(DatabaseType::Postgres), schema: Some("public"), table_name: "users", columns: &columns, order_columns: &keys, limit: 100, }), "SELECT \"id\", \"name\" FROM \"public\".\"users\" ORDER BY \"id\" ASC LIMIT 100;" ); assert_eq!( build_table_select_sql(TableSelectSqlOptions { database_type: Some(DatabaseType::SqlServer), schema: Some("dbo"), table_name: "users", columns: &columns, order_columns: &keys, limit: 100, }), "SELECT TOP (100) [id], [name] FROM [dbo].[users] ORDER BY [id] ASC" ); assert_eq!( build_table_select_sql(TableSelectSqlOptions { database_type: Some(DatabaseType::Db2), schema: Some("DB2INST1"), table_name: "USERS", columns: &columns, order_columns: &keys, limit: 100, }), "SELECT \"id\", \"name\" FROM \"DB2INST1\".\"USERS\" ORDER BY \"id\" ASC FETCH FIRST 100 ROWS ONLY" ); assert_eq!( build_table_select_sql(TableSelectSqlOptions { database_type: Some(DatabaseType::Jdbc), schema: Some("cbsdw_dwd"), table_name: "dwd_test_df", columns: &[], order_columns: &[], limit: 100, }), "SELECT * FROM dwd_test_df LIMIT 100;" ); assert_eq!( build_table_select_sql(TableSelectSqlOptions { database_type: Some(DatabaseType::Databend), schema: Some("dbx_test"), table_name: "jdbc_probe", columns: &[], order_columns: &[], limit: 500, }), "SELECT * FROM `dbx_test`.`jdbc_probe` LIMIT 500;" ); assert_eq!( build_table_select_sql(TableSelectSqlOptions { database_type: Some(DatabaseType::Hive), schema: Some("test"), table_name: "dws_event_analyse", columns: &[], order_columns: &[], limit: 100, }), "SELECT * FROM `test`.`dws_event_analyse` LIMIT 100;" ); assert_eq!( build_table_select_sql(TableSelectSqlOptions { database_type: Some(DatabaseType::StarRocks), schema: None, table_name: "sales_report", columns: &["customer_name".to_string()], order_columns: &[], limit: 100, }), "SELECT `customer_name` FROM `sales_report` LIMIT 100;" ); assert_eq!( build_table_select_sql(TableSelectSqlOptions { database_type: Some(DatabaseType::Iris), schema: Some("Ens"), table_name: "AlarmResponse", columns: &[], order_columns: &[], limit: 100, }), "SELECT TOP 100 * FROM \"Ens\".\"AlarmResponse\"" ); assert_eq!( build_table_select_sql(TableSelectSqlOptions { database_type: Some(DatabaseType::Iotdb), schema: Some("root.test"), table_name: "device2", columns: &[], order_columns: &[], limit: 100, }), "SELECT * FROM root.test.device2 LIMIT 100;" ); } #[test] fn builds_table_data_where_and_schema_queries() { assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Mysql), schema: None, table_name: "users".to_string(), primary_keys: vec!["id".to_string()], columns: Vec::new(), fallback_order_columns: Vec::new(), order_by: None, limit: Some(100), offset: None, where_input: Some("where status = 'active'".to_string()), include_row_id: false, }), "SELECT * FROM `users` WHERE (status = 'active') ORDER BY `id` ASC LIMIT 100;" ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Goldendb), schema: None, table_name: "sys_dic".to_string(), primary_keys: Vec::new(), columns: Vec::new(), fallback_order_columns: Vec::new(), order_by: None, limit: Some(100), offset: None, where_input: None, include_row_id: false, }), "SELECT * FROM `sys_dic` LIMIT 100;" ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), table_name: "orders".to_string(), primary_keys: Vec::new(), columns: Vec::new(), fallback_order_columns: Vec::new(), order_by: None, limit: Some(50), offset: Some(100), where_input: Some("WHERE amount > 10".to_string()), include_row_id: false, }), "SELECT * FROM \"public\".\"orders\" WHERE (amount > 10) LIMIT 50 OFFSET 100;" ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::StarRocks), schema: None, table_name: "sales_report".to_string(), primary_keys: Vec::new(), columns: vec!["customer_name".to_string(), "amount".to_string()], fallback_order_columns: Vec::new(), order_by: None, limit: Some(100), offset: None, where_input: Some("`customer_name` = 'Acme'".to_string()), include_row_id: false, }), "SELECT * FROM `sales_report` WHERE (`customer_name` = 'Acme') LIMIT 100;" ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Db2), schema: Some("DB2INST1".to_string()), table_name: "ORDERS".to_string(), primary_keys: Vec::new(), columns: Vec::new(), fallback_order_columns: Vec::new(), order_by: None, limit: Some(50), offset: None, where_input: Some("WHERE amount > 10".to_string()), include_row_id: false, }), "SELECT * FROM \"DB2INST1\".\"ORDERS\" WHERE (amount > 10) FETCH FIRST 50 ROWS ONLY" ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Db2), schema: Some("DB2INST1".to_string()), table_name: "ORDERS".to_string(), primary_keys: vec!["ID".to_string()], columns: vec!["ID".to_string(), "AMOUNT".to_string()], fallback_order_columns: Vec::new(), order_by: None, limit: Some(50), offset: Some(100), where_input: Some("WHERE amount > 10".to_string()), include_row_id: false, }), "SELECT \"ID\", \"AMOUNT\" FROM (SELECT dbx_t.\"ID\", dbx_t.\"AMOUNT\", ROW_NUMBER() OVER (ORDER BY \"ID\" ASC) AS \"__dbx_row_num\" FROM \"DB2INST1\".\"ORDERS\" dbx_t WHERE (amount > 10)) dbx_page WHERE \"__dbx_row_num\" > 100 AND \"__dbx_row_num\" <= 150 ORDER BY \"__dbx_row_num\"" ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Iris), schema: Some("Ens".to_string()), table_name: "AlarmResponse".to_string(), primary_keys: Vec::new(), columns: Vec::new(), fallback_order_columns: Vec::new(), order_by: None, limit: Some(100), offset: None, where_input: None, include_row_id: false, }), "SELECT TOP 100 * FROM \"Ens\".\"AlarmResponse\"" ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Iotdb), schema: Some("root.test".to_string()), table_name: "device2".to_string(), primary_keys: Vec::new(), columns: Vec::new(), fallback_order_columns: Vec::new(), order_by: None, limit: Some(100), offset: None, where_input: None, include_row_id: false, }), "SELECT * FROM root.test.device2 LIMIT 100;" ); } #[test] fn explicit_table_data_order_overrides_default_key_order() { assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), table_name: "country_gdp".to_string(), primary_keys: vec!["year".to_string()], columns: vec!["iso3".to_string(), "year".to_string(), "gdp_pc".to_string()], fallback_order_columns: Vec::new(), order_by: Some("\"iso3\" ASC".to_string()), limit: Some(100), offset: None, where_input: None, include_row_id: false, }), "SELECT * FROM \"public\".\"country_gdp\" ORDER BY \"iso3\" ASC LIMIT 100;" ); } #[test] fn builds_iris_table_data_sql_with_literal_top_and_quoted_object() { let sql = build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Iris), schema: Some("Ens".to_string()), table_name: "AlarmResponse".to_string(), primary_keys: vec!["ID".to_string()], columns: vec!["ID".to_string(), "Status".to_string()], fallback_order_columns: Vec::new(), order_by: Some("\"Status\" DESC".to_string()), limit: Some(25), offset: None, where_input: Some("WHERE \"Status\" = 'Open'".to_string()), include_row_id: false, }); assert_eq!( sql, "SELECT TOP 25 * FROM \"Ens\".\"AlarmResponse\" WHERE (\"Status\" = 'Open') ORDER BY \"Status\" DESC" ); assert!(!sql.contains("?")); assert!(!sql.contains(":%qpar")); assert!(!sql.contains(" LIMIT ")); } #[test] fn builds_table_data_special_column_queries() { assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Tdengine), schema: Some("test_db".to_string()), table_name: "meters".to_string(), primary_keys: vec!["ts".to_string()], columns: vec![ "ts".to_string(), "current".to_string(), "voltage".to_string(), "location".to_string(), "groupid".to_string(), ], fallback_order_columns: Vec::new(), order_by: None, limit: Some(100), offset: None, where_input: None, include_row_id: false, }), "SELECT tbname, `ts` AS `ts`, `current` AS `current`, `voltage` AS `voltage`, `location` AS `location`, `groupid` AS `groupid` FROM `test_db`.`meters` ORDER BY `ts` ASC LIMIT 100;" ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Hive), schema: None, table_name: "departments".to_string(), primary_keys: Vec::new(), columns: vec!["id".to_string(), "name".to_string()], fallback_order_columns: Vec::new(), order_by: None, limit: Some(100), offset: None, where_input: None, include_row_id: false, }), "SELECT `id` AS `id`, `name` AS `name` FROM `departments` LIMIT 100;" ); } #[test] fn builds_sqlserver_table_data_pages() { assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::SqlServer), schema: Some("dbo".to_string()), table_name: "accounts".to_string(), primary_keys: vec!["id".to_string()], columns: Vec::new(), fallback_order_columns: Vec::new(), order_by: None, limit: Some(25), offset: None, where_input: Some("where id = 1".to_string()), include_row_id: false, }), "SELECT TOP (25) * FROM [dbo].[accounts] WHERE (id = 1) ORDER BY [id] ASC" ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::SqlServer), schema: Some("sales".to_string()), table_name: "orders".to_string(), primary_keys: vec!["order_id".to_string()], columns: vec!["order_id".to_string(), "customer".to_string()], fallback_order_columns: Vec::new(), order_by: None, limit: Some(50), offset: Some(100), where_input: None, include_row_id: false, }), "WITH [dbx_page] AS (SELECT [order_id], [customer], ROW_NUMBER() OVER (ORDER BY [order_id] ASC) AS [__dbx_row_num] FROM [sales].[orders]) SELECT [order_id], [customer] FROM [dbx_page] WHERE [__dbx_row_num] > 100 AND [__dbx_row_num] <= 150 ORDER BY [__dbx_row_num]" ); } #[test] fn builds_oracle_and_neo4j_table_data_queries() { assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Oracle), schema: Some("DBXTEST".to_string()), table_name: "DBX_LOAD_TABLE_006".to_string(), primary_keys: vec![DBX_ROWID_COLUMN.to_string()], columns: Vec::new(), fallback_order_columns: Vec::new(), order_by: None, limit: Some(100), offset: None, where_input: None, include_row_id: true, }), "SELECT ROWIDTOCHAR(t.ROWID) AS \"__DBX_ROWID\", t.* FROM \"DBXTEST\".\"DBX_LOAD_TABLE_006\" t ORDER BY t.ROWID ASC" ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Neo4j), schema: None, table_name: "Employee".to_string(), primary_keys: vec!["id".to_string()], columns: vec!["id".to_string(), "first name".to_string(), "role".to_string()], fallback_order_columns: Vec::new(), order_by: None, limit: Some(100), offset: None, where_input: None, include_row_id: false, }), "MATCH (n:`Employee`) RETURN elementId(n) AS `__DBX_ELEMENT_ID`, n.`id` AS `id`, n.`first name` AS `first name`, n.`role` AS `role` ORDER BY n.`id` ASC LIMIT 100;" ); } #[test] fn normalizes_where_input_with_multibyte_identifier_prefix() { assert_eq!(normalize_where_input(Some("`客户名称` = '示例客户'")), "`客户名称` = '示例客户'"); assert_eq!(normalize_where_input(Some("WHERE `客户名称` = '示例客户';")), "`客户名称` = '示例客户'"); } }