diff --git a/crates/dbx-core/src/data_compare.rs b/crates/dbx-core/src/data_compare.rs index c79b9ea4a..9673e964d 100644 --- a/crates/dbx-core/src/data_compare.rs +++ b/crates/dbx-core/src/data_compare.rs @@ -9,7 +9,10 @@ use crate::data_grid_sql::{format_grid_sql_literal as format_data_grid_sql_liter use crate::models::connection::DatabaseType; use crate::query::{execute_sql_statement_with_options, QueryExecutionOptions}; use crate::schema::get_columns_core; -use crate::sql_dialect::{build_count_table_sql, qualified_table_name, quote_table_identifier, uses_fetch_first}; +use crate::sql_dialect::{ + build_count_table_sql, pagination_strategy, qualified_table_name, quote_table_identifier, PaginationContext, + TablePaginationStrategy, +}; use crate::transfer::{generate_comment_ddl, generate_create_table_ddl}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -770,30 +773,78 @@ fn build_data_compare_select_sql( .join(", ") }; - if uses_fetch_first(database_type) { - let offset_sql = if offset > 0 { format!(" OFFSET {offset} ROWS") } else { String::new() }; - return format!("SELECT {select_columns} FROM {table}{order_by}{offset_sql} FETCH FIRST {row_limit} ROWS ONLY"); - } - - if database_type == DatabaseType::SqlServer { - if offset == 0 { - return format!("SELECT TOP ({row_limit}) {select_columns} FROM {table}{order_by}"); + match pagination_strategy(Some(database_type), PaginationContext::BoundedRead) { + TablePaginationStrategy::Db2FetchFirst | TablePaginationStrategy::FetchFirst => { + let offset_sql = if offset > 0 { format!(" OFFSET {offset} ROWS") } else { String::new() }; + format!("SELECT {select_columns} FROM {table}{order_by}{offset_sql} FETCH FIRST {row_limit} ROWS ONLY") } - 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 + row_limit; - return format!( - "WITH {page_alias} AS (SELECT {select_columns}, ROW_NUMBER() OVER (ORDER BY {order_expression}) AS {row_number_alias} FROM {table}) SELECT {select_columns} FROM {page_alias} WHERE {row_number_alias} > {offset} AND {row_number_alias} <= {end} ORDER BY {row_number_alias}" - ); + TablePaginationStrategy::Rownum => build_rownum_data_compare_select_sql( + database_type, + &table, + &select_columns, + &order_by, + columns, + row_limit, + offset, + ), + TablePaginationStrategy::SqlServerTop => { + if offset == 0 { + return format!("SELECT TOP ({row_limit}) {select_columns} FROM {table}{order_by}"); + } + 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 + row_limit; + format!( + "WITH {page_alias} AS (SELECT {select_columns}, ROW_NUMBER() OVER (ORDER BY {order_expression}) AS {row_number_alias} FROM {table}) SELECT {select_columns} FROM {page_alias} WHERE {row_number_alias} > {offset} AND {row_number_alias} <= {end} ORDER BY {row_number_alias}" + ) + } + TablePaginationStrategy::IrisTop => format!("SELECT TOP {row_limit} {select_columns} FROM {table}{order_by}"), + TablePaginationStrategy::InformixFirst => { + let row_limit_clause = + if offset > 0 { format!("SKIP {offset} FIRST {row_limit}") } else { format!("FIRST {row_limit}") }; + format!("SELECT {row_limit_clause} {select_columns} FROM {table}{order_by}") + } + TablePaginationStrategy::AgentMaxRows => format!("SELECT {select_columns} FROM {table}{order_by};"), + TablePaginationStrategy::Unbounded => format!("SELECT {select_columns} FROM {table}{order_by}"), + TablePaginationStrategy::QuestDbLimit => { + if offset > 0 { + let upper_bound = offset + row_limit; + format!("SELECT {select_columns} FROM {table}{order_by} LIMIT {offset}, {upper_bound}") + } else { + format!("SELECT {select_columns} FROM {table}{order_by} LIMIT {row_limit}") + } + } + TablePaginationStrategy::LimitOffset => { + let offset_sql = if offset > 0 { format!(" OFFSET {offset}") } else { String::new() }; + format!("SELECT {select_columns} FROM {table}{order_by} LIMIT {row_limit}{offset_sql};") + } + } +} + +fn build_rownum_data_compare_select_sql( + database_type: DatabaseType, + table: &str, + select_columns: &str, + order_by: &str, + columns: &[String], + row_limit: usize, + offset: usize, +) -> String { + let base = format!("SELECT {select_columns} FROM {table}{order_by}"); + if offset == 0 { + return format!("SELECT {select_columns} FROM ({base}) WHERE ROWNUM <= {row_limit}"); } - // JDBC connections avoid SQL-level LIMIT; the agent truncates rows while reading. - if database_type == DatabaseType::Jdbc { - return format!("SELECT {select_columns} FROM {table}{order_by};"); - } - - let offset_sql = if offset > 0 { format!(" OFFSET {offset}") } else { String::new() }; - format!("SELECT {select_columns} FROM {table}{order_by} LIMIT {row_limit}{offset_sql};") + let row_number_alias = quote_table_identifier(Some(database_type), "__dbx_row_num"); + let end = offset + row_limit; + let outer_columns = if columns.is_empty() { + "*".to_string() + } else { + columns.iter().map(|column| quote_table_identifier(Some(database_type), column)).collect::>().join(", ") + }; + format!( + "SELECT {outer_columns} FROM (SELECT dbx_inner.*, ROWNUM AS {row_number_alias} FROM ({base}) dbx_inner WHERE ROWNUM <= {end}) WHERE {row_number_alias} > {offset}" + ) } #[allow(clippy::too_many_arguments)] @@ -1183,6 +1234,34 @@ mod tests { ); } + #[test] + fn builds_backend_table_select_sql_for_oceanbase_oracle_rownum_pages() { + assert_eq!( + build_data_compare_select_sql( + DatabaseType::OceanbaseOracle, + "APP", + "EVENTS", + &["ID".to_string(), "NAME".to_string()], + &["ID".to_string()], + 25, + 0, + ), + "SELECT \"ID\", \"NAME\" FROM (SELECT \"ID\", \"NAME\" FROM \"APP\".\"EVENTS\" ORDER BY \"ID\" ASC) WHERE ROWNUM <= 25" + ); + assert_eq!( + build_data_compare_select_sql( + DatabaseType::OceanbaseOracle, + "APP", + "EVENTS", + &["ID".to_string(), "NAME".to_string()], + &["ID".to_string()], + 25, + 50, + ), + "SELECT \"ID\", \"NAME\" FROM (SELECT dbx_inner.*, ROWNUM AS \"__dbx_row_num\" FROM (SELECT \"ID\", \"NAME\" FROM \"APP\".\"EVENTS\" ORDER BY \"ID\" ASC) dbx_inner WHERE ROWNUM <= 75) WHERE \"__dbx_row_num\" > 50" + ); + } + #[test] fn shared_sql_dialect_helpers_build_data_compare_table_sql() { use crate::sql_dialect::build_count_table_sql as build_shared_count_table_sql; diff --git a/crates/dbx-core/src/database_search_sql.rs b/crates/dbx-core/src/database_search_sql.rs index 109a21b79..2bfb615b4 100644 --- a/crates/dbx-core/src/database_search_sql.rs +++ b/crates/dbx-core/src/database_search_sql.rs @@ -2,7 +2,9 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::models::connection::DatabaseType; -use crate::sql_dialect::{qualified_table_name, quote_table_identifier, uses_fetch_first}; +use crate::sql_dialect::{ + pagination_strategy, qualified_table_name, quote_table_identifier, PaginationContext, TablePaginationStrategy, +}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DatabaseSearchColumn { @@ -107,15 +109,21 @@ pub fn build_database_search_sql(options: DatabaseSearchSqlOptions) -> Option format!("SELECT TOP {limit} * FROM {table} WHERE ({where_clause})"), + TablePaginationStrategy::IrisTop => format!("SELECT TOP {limit} * FROM {table} WHERE ({where_clause})"), + TablePaginationStrategy::InformixFirst => format!("SELECT FIRST {limit} * FROM {table} WHERE ({where_clause})"), + TablePaginationStrategy::Db2FetchFirst | TablePaginationStrategy::FetchFirst => { + format!("SELECT * FROM {table} WHERE ({where_clause}) FETCH FIRST {limit} ROWS ONLY") + } + TablePaginationStrategy::Rownum => { + format!("SELECT * FROM (SELECT * FROM {table} WHERE ({where_clause})) WHERE ROWNUM <= {limit}") + } + TablePaginationStrategy::AgentMaxRows => format!("SELECT * FROM {table} WHERE ({where_clause});"), + TablePaginationStrategy::QuestDbLimit | TablePaginationStrategy::LimitOffset => { + format!("SELECT * FROM {table} WHERE ({where_clause}) LIMIT {limit};") + } + TablePaginationStrategy::Unbounded => format!("SELECT * FROM {table} WHERE ({where_clause})"), }; Some(DatabaseSearchSql { sql, searchable_columns }) @@ -220,7 +228,9 @@ fn text_cast_expression(database_type: Option, identifier: &str) - match database_type { Some(DatabaseType::Mysql) => format!("LOWER(CAST({identifier} AS CHAR))"), Some(DatabaseType::SqlServer) => format!("LOWER(CAST({identifier} AS NVARCHAR(MAX)))"), - Some(DatabaseType::Oracle) => format!("LOWER(CAST({identifier} AS VARCHAR2(4000)))"), + Some(DatabaseType::Oracle | DatabaseType::OceanbaseOracle) => { + format!("LOWER(CAST({identifier} AS VARCHAR2(4000)))") + } Some(DatabaseType::ClickHouse) => format!("lower(toString({identifier}))"), _ => format!("LOWER(CAST({identifier} AS TEXT))"), } @@ -295,6 +305,25 @@ mod tests { assert!(query.sql.contains("FROM \"public\".\"orders\"")); } + #[test] + fn builds_oceanbase_oracle_search_query_with_rownum_limit() { + let query = build_database_search_sql(DatabaseSearchSqlOptions { + database_type: Some(DatabaseType::OceanbaseOracle), + schema: Some("APP".to_string()), + table_name: "USERS".to_string(), + columns: vec![col("NAME", "varchar2", false)], + term: "alice".to_string(), + limit: Some(20), + }) + .unwrap(); + + assert_eq!(query.searchable_columns, vec!["NAME"]); + assert_eq!( + query.sql, + "SELECT * FROM (SELECT * FROM \"APP\".\"USERS\" WHERE (LOWER(CAST(\"NAME\" AS VARCHAR2(4000))) LIKE '%alice%' ESCAPE '~')) WHERE ROWNUM <= 20" + ); + } + #[test] fn returns_none_for_tables_without_searchable_columns() { assert_eq!( diff --git a/crates/dbx-core/src/query_result_sql.rs b/crates/dbx-core/src/query_result_sql.rs index 0c497b6d6..5a3a932c7 100644 --- a/crates/dbx-core/src/query_result_sql.rs +++ b/crates/dbx-core/src/query_result_sql.rs @@ -4,7 +4,7 @@ use std::sync::LazyLock; use crate::models::connection::DatabaseType; use crate::sql::find_statement_at_cursor; -use crate::sql_dialect::{quote_table_identifier, uses_fetch_first}; +use crate::sql_dialect::{pagination_strategy, quote_table_identifier, PaginationContext, TablePaginationStrategy}; static LIMIT_OFFSET_STRIP_RE: LazyLock = LazyLock::new(|| { Regex::new(r"(?i)(\s+LIMIT\s+\d+(\s+OFFSET\s+\d+)?|\s+LIMIT\s+\d+(\s*,\s*\d+)?|\s+OFFSET\s+\d+(\s+LIMIT\s+\d+)?|\s+OFFSET\s+\d+\s+ROWS?\s+FETCH\s+(?:FIRST|NEXT)\s+\d+\s+ROWS?\s+ONLY|\s+FETCH\s+(?:FIRST|NEXT)\s+\d+\s+ROWS?\s+ONLY)\s*$").unwrap() @@ -167,21 +167,6 @@ pub fn build_paginated_query_sql(options: PaginatedQuerySqlOptions) -> QuerySqlB let safe_limit = options.limit.max(1); let safe_offset = options.offset; - if options.database_type == Some(DatabaseType::SqlServer) { - if safe_offset > 0 { - return err("unsupported"); - } - return ok(add_sql_server_top(&statement, safe_limit)); - } - - if options.database_type == Some(DatabaseType::Mysql) { - return ok(add_mysql_limit(&statement, safe_limit, safe_offset)); - } - - if options.database_type == Some(DatabaseType::Questdb) { - return ok(add_questdb_limit(&statement, safe_limit, safe_offset)); - } - if options.database_type == Some(DatabaseType::Elasticsearch) { // If the user wrote their own LIMIT, leave the SQL alone — they // explicitly bounded the result set and the front-end will paginate @@ -195,24 +180,24 @@ pub fn build_paginated_query_sql(options: PaginatedQuerySqlOptions) -> QuerySqlB return ok(format!("{statement} LIMIT {safe_limit} OFFSET {safe_offset};")); } - if options.database_type == Some(DatabaseType::Oracle) { - return ok(format!("{statement};")); + match pagination_strategy(options.database_type, PaginationContext::UserQuery) { + TablePaginationStrategy::SqlServerTop => { + if safe_offset > 0 { + err("unsupported") + } else { + ok(add_sql_server_top(&statement, safe_limit)) + } + } + TablePaginationStrategy::QuestDbLimit => ok(add_questdb_limit(&statement, safe_limit, safe_offset)), + TablePaginationStrategy::InformixFirst => ok(add_informix_first_limit(&statement, safe_limit, safe_offset)), + TablePaginationStrategy::Db2FetchFirst | TablePaginationStrategy::FetchFirst => { + ok(add_fetch_first_limit(&statement, safe_limit, safe_offset)) + } + TablePaginationStrategy::Rownum => ok(add_rownum_limit(&statement, safe_limit, safe_offset)), + TablePaginationStrategy::AgentMaxRows | TablePaginationStrategy::Unbounded => ok(format!("{statement};")), + TablePaginationStrategy::IrisTop => ok(add_iris_top_limit(&statement, safe_limit)), + TablePaginationStrategy::LimitOffset => ok(add_standard_limit(&statement, safe_limit, safe_offset)), } - - if options.database_type == Some(DatabaseType::Informix) { - return ok(add_informix_first_limit(&statement, safe_limit, safe_offset)); - } - - if options.database_type.is_some_and(uses_fetch_first) { - return ok(add_fetch_first_limit(&statement, safe_limit, safe_offset)); - } - - // JDBC connections avoid SQL-level LIMIT; the agent truncates rows while reading. - if options.database_type == Some(DatabaseType::Jdbc) { - return ok(format!("{statement};")); - } - - ok(add_standard_limit(&statement, safe_limit, safe_offset)) } pub fn build_count_query_sql(options: CountQuerySqlOptions) -> QuerySqlBuildResult { @@ -411,14 +396,6 @@ fn sql_server_statement_for_derived_table(statement: &str) -> String { statement[..order_by].trim_end().to_string() } -fn add_mysql_limit(statement: &str, limit: usize, offset: usize) -> String { - if has_top_level_limit(statement) { - return format!("{statement};"); - } - let offset_sql = if offset > 0 { format!(" OFFSET {offset}") } else { String::new() }; - format!("{statement} LIMIT {limit}{offset_sql};") -} - fn add_informix_first_limit(statement: &str, limit: usize, offset: usize) -> String { if has_top_level_informix_row_limit(statement) { return format!("{statement};"); @@ -440,6 +417,18 @@ fn add_informix_first_limit(statement: &str, limit: usize, offset: usize) -> Str format!("SELECT {row_limit} * FROM ({statement}) dbx_page;") } +fn add_iris_top_limit(statement: &str, limit: usize) -> String { + if has_top_level_select_top(statement) { + return format!("{statement};"); + } + if statement.len() >= 6 && statement[..6].eq_ignore_ascii_case("SELECT") { + let rest = &statement[6..]; + format!("SELECT TOP {limit}{rest};") + } else { + format!("SELECT TOP {limit} * FROM ({statement}) dbx_page;") + } +} + fn add_questdb_limit(statement: &str, limit: usize, offset: usize) -> String { if has_top_level_limit(statement) { return format!("{statement};"); @@ -478,6 +467,10 @@ fn has_top_level_fetch_first(sql: &str) -> bool { tokens.windows(2).any(|w| w[0].text == "FETCH" && w[1].text == "FIRST") } +fn has_top_level_rownum(sql: &str) -> bool { + top_level_sql_tokens(sql).iter().any(|token| token.text == "ROWNUM") +} + fn has_top_level_offset_fetch_next(sql: &str) -> bool { let tokens = top_level_sql_tokens(sql); let has_offset = tokens.iter().any(|token| token.text == "OFFSET"); @@ -493,6 +486,19 @@ fn add_fetch_first_limit(statement: &str, limit: usize, offset: usize) -> String format!("{statement}{offset_sql} FETCH FIRST {limit} ROWS ONLY;") } +fn add_rownum_limit(statement: &str, limit: usize, offset: usize) -> String { + if has_top_level_rownum(statement) { + return format!("{statement};"); + } + if offset == 0 { + return format!("SELECT * FROM ({statement}) WHERE ROWNUM <= {limit};"); + } + let end = offset + limit; + format!( + "SELECT * FROM (SELECT dbx_inner.*, ROWNUM AS \"__dbx_row_num\" FROM ({statement}) dbx_inner WHERE ROWNUM <= {end}) WHERE \"__dbx_row_num\" > {offset};" + ) +} + fn add_standard_limit(statement: &str, limit: usize, offset: usize) -> String { if has_top_level_limit(statement) { return format!("{statement};"); @@ -882,6 +888,33 @@ mod tests { assert_eq!(result.sql.unwrap(), "SELECT id FROM users;"); } + #[test] + fn oceanbase_oracle_pagination_wraps_with_rownum() { + let result = build_paginated_query_sql(PaginatedQuerySqlOptions { + original_sql: "SELECT id FROM users ORDER BY id".to_string(), + database_type: Some(DatabaseType::OceanbaseOracle), + limit: 100, + offset: 0, + }); + + assert_eq!(result.sql.unwrap(), "SELECT * FROM (SELECT id FROM users ORDER BY id) WHERE ROWNUM <= 100;"); + } + + #[test] + fn oceanbase_oracle_pagination_wraps_offset_with_rownum_bounds() { + let result = build_paginated_query_sql(PaginatedQuerySqlOptions { + original_sql: "SELECT id FROM users ORDER BY id".to_string(), + database_type: Some(DatabaseType::OceanbaseOracle), + limit: 100, + offset: 200, + }); + + assert_eq!( + result.sql.unwrap(), + "SELECT * FROM (SELECT dbx_inner.*, ROWNUM AS \"__dbx_row_num\" FROM (SELECT id FROM users ORDER BY id) dbx_inner WHERE ROWNUM <= 300) WHERE \"__dbx_row_num\" > 200;" + ); + } + #[test] fn uses_fetch_first_pagination_for_db2() { let result = build_paginated_query_sql(PaginatedQuerySqlOptions { diff --git a/crates/dbx-core/src/sql_dialect.rs b/crates/dbx-core/src/sql_dialect.rs index cff869326..e14fa10f2 100644 --- a/crates/dbx-core/src/sql_dialect.rs +++ b/crates/dbx-core/src/sql_dialect.rs @@ -6,7 +6,10 @@ mod types; #[cfg(test)] mod tests; -pub use capabilities::{is_schema_aware, uses_fetch_first}; +pub use capabilities::{ + is_schema_aware, pagination_strategy, table_pagination_strategy, uses_fetch_first, PaginationContext, + TablePaginationStrategy, +}; pub use identifiers::{normalize_where_input, qualified_table_name, quote_table_identifier}; pub(crate) use identifiers::{parse_sqlserver_linked_schema_ref, qualified_transfer_table, quote_transfer_identifier}; pub use table_select::{build_count_table_sql, build_table_data_select_sql, build_table_select_sql}; diff --git a/crates/dbx-core/src/sql_dialect/capabilities.rs b/crates/dbx-core/src/sql_dialect/capabilities.rs index 80a682dde..37d41e59e 100644 --- a/crates/dbx-core/src/sql_dialect/capabilities.rs +++ b/crates/dbx-core/src/sql_dialect/capabilities.rs @@ -1,5 +1,26 @@ use crate::models::connection::DatabaseType; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TablePaginationStrategy { + LimitOffset, + FetchFirst, + Db2FetchFirst, + SqlServerTop, + IrisTop, + InformixFirst, + Rownum, + QuestDbLimit, + Unbounded, + AgentMaxRows, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PaginationContext { + TablePreview, + BoundedRead, + UserQuery, +} + pub fn is_schema_aware(database_type: DatabaseType) -> bool { matches!( database_type, @@ -40,6 +61,30 @@ pub fn uses_fetch_first(database_type: DatabaseType) -> bool { matches!(database_type, DatabaseType::Oracle | DatabaseType::Dameng | DatabaseType::Db2) } +pub fn pagination_strategy(database_type: Option, context: PaginationContext) -> TablePaginationStrategy { + match database_type { + Some(DatabaseType::Jdbc) => TablePaginationStrategy::AgentMaxRows, + Some(DatabaseType::Oracle) + if matches!(context, PaginationContext::TablePreview | PaginationContext::UserQuery) => + { + TablePaginationStrategy::Unbounded + } + Some(DatabaseType::Oracle) => TablePaginationStrategy::FetchFirst, + Some(DatabaseType::Dameng) => TablePaginationStrategy::FetchFirst, + Some(DatabaseType::Db2) => TablePaginationStrategy::Db2FetchFirst, + Some(DatabaseType::SqlServer) => TablePaginationStrategy::SqlServerTop, + Some(DatabaseType::Iris) => TablePaginationStrategy::IrisTop, + Some(DatabaseType::Informix) => TablePaginationStrategy::InformixFirst, + Some(DatabaseType::OceanbaseOracle) => TablePaginationStrategy::Rownum, + Some(DatabaseType::Questdb) => TablePaginationStrategy::QuestDbLimit, + _ => TablePaginationStrategy::LimitOffset, + } +} + +pub fn table_pagination_strategy(database_type: Option) -> TablePaginationStrategy { + pagination_strategy(database_type, PaginationContext::TablePreview) +} + pub(super) fn is_simple_informix_identifier(name: &str) -> bool { let mut chars = name.chars(); let Some(first) = chars.next() else { diff --git a/crates/dbx-core/src/sql_dialect/table_select.rs b/crates/dbx-core/src/sql_dialect/table_select.rs index b8a560780..7643e4191 100644 --- a/crates/dbx-core/src/sql_dialect/table_select.rs +++ b/crates/dbx-core/src/sql_dialect/table_select.rs @@ -1,6 +1,6 @@ use crate::models::connection::DatabaseType; -use super::capabilities::uses_fetch_first; +use super::capabilities::{table_pagination_strategy, uses_fetch_first, TablePaginationStrategy}; use super::identifiers::{normalize_where_input, qualified_table_name, quote_table_identifier}; use super::types::{ TableDataSelectSqlOptions, TableSelectSqlOptions, DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN, @@ -62,75 +62,68 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String 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::Informix) { - let row_limit = informix_row_limit_clause(limit, options.offset.unwrap_or(0)); - return format!("SELECT {row_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::OceanbaseOracle) { - return build_oceanbase_oracle_table_select_sql(&table_alias, &where_clause, &order, &select_columns, limit); - } - - 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( + match table_pagination_strategy(database_type) { + TablePaginationStrategy::IrisTop => { + format!("SELECT TOP {limit} {select_columns} FROM {table_alias}{where_clause}{order}") + } + TablePaginationStrategy::InformixFirst => { + let row_limit = informix_row_limit_clause(limit, options.offset.unwrap_or(0)); + format!("SELECT {row_limit} {select_columns} FROM {table_alias}{where_clause}{order}") + } + TablePaginationStrategy::Db2FetchFirst if options.offset.is_some_and(|offset| offset > 0) => { + build_db2_table_select_page_sql( + &table_alias, + &where_clause, + order_by, + &options.columns, + limit, + options.offset.unwrap_or(0), + ) + } + TablePaginationStrategy::Db2FetchFirst | TablePaginationStrategy::FetchFirst => { + let offset = options + .offset + .filter(|offset| *offset > 0) + .map(|offset| format!(" OFFSET {offset} ROWS")) + .unwrap_or_default(); + format!( + "SELECT {select_columns} FROM {table_alias}{where_clause}{order}{offset} FETCH FIRST {limit} ROWS ONLY" + ) + } + TablePaginationStrategy::Rownum => { + build_rownum_table_select_sql(&table_alias, &where_clause, &order, &select_columns, limit) + } + TablePaginationStrategy::Unbounded => { + format!("SELECT {select_columns} FROM {table_alias}{where_clause}{order}") + } + TablePaginationStrategy::SqlServerTop => build_sqlserver_table_select_sql( &table_alias, &where_clause, order_by.unwrap_or("(SELECT NULL)"), &options.columns, limit, options.offset.unwrap_or(0), - ); - } - - if database_type == Some(DatabaseType::Questdb) { - return build_questdb_table_select_sql( + ), + TablePaginationStrategy::QuestDbLimit => build_questdb_table_select_sql( &table_alias, &where_clause, &order, &options.columns, limit, options.offset.unwrap_or(0), - ); + ), + TablePaginationStrategy::AgentMaxRows => { + format!("SELECT {select_columns} FROM {table_alias}{where_clause}{order};") + } + TablePaginationStrategy::LimitOffset => { + 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};") + } } - - let offset = - options.offset.filter(|offset| *offset > 0).map(|offset| format!(" OFFSET {offset}")).unwrap_or_default(); - // JDBC connections avoid SQL-level LIMIT because it is not universally - // supported across all JDBC drivers; the agent truncates rows while reading. - if database_type == Some(DatabaseType::Jdbc) { - return format!("SELECT {select_columns} FROM {table_alias}{where_clause}{order};"); - } - format!("SELECT {select_columns} FROM {table_alias}{where_clause}{order} LIMIT {limit}{offset};") } pub fn build_table_select_sql(options: TableSelectSqlOptions<'_>) -> String { @@ -161,33 +154,24 @@ pub fn build_table_select_sql(options: TableSelectSqlOptions<'_>) -> String { }; let limit = options.limit; - if database_type == Some(DatabaseType::Iris) { - return format!("SELECT TOP {limit} {select_columns} FROM {table}{order_by}"); + match table_pagination_strategy(database_type) { + TablePaginationStrategy::IrisTop => format!("SELECT TOP {limit} {select_columns} FROM {table}{order_by}"), + TablePaginationStrategy::InformixFirst => { + format!("SELECT FIRST {limit} {select_columns} FROM {table}{order_by}") + } + TablePaginationStrategy::Rownum => build_rownum_table_select_sql(&table, "", &order_by, &select_columns, limit), + TablePaginationStrategy::Db2FetchFirst | TablePaginationStrategy::FetchFirst => { + format!("SELECT {select_columns} FROM {table}{order_by} FETCH FIRST {limit} ROWS ONLY") + } + TablePaginationStrategy::SqlServerTop => { + format!("SELECT TOP ({limit}) {select_columns} FROM {table}{order_by}") + } + TablePaginationStrategy::AgentMaxRows => format!("SELECT {select_columns} FROM {table}{order_by};"), + TablePaginationStrategy::Unbounded => format!("SELECT {select_columns} FROM {table}{order_by}"), + TablePaginationStrategy::QuestDbLimit | TablePaginationStrategy::LimitOffset => { + format!("SELECT {select_columns} FROM {table}{order_by} LIMIT {limit};") + } } - - if database_type == Some(DatabaseType::Informix) { - return format!("SELECT FIRST {limit} {select_columns} FROM {table}{order_by}"); - } - - if database_type == Some(DatabaseType::OceanbaseOracle) { - let inner_select = format!("SELECT {select_columns} FROM {table}{order_by}"); - return format!("SELECT {select_columns} FROM ({inner_select}) WHERE ROWNUM <= {limit}"); - } - - 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}"); - } - - // JDBC connections avoid SQL-level LIMIT; the agent truncates rows while reading. - if database_type == Some(DatabaseType::Jdbc) { - return format!("SELECT {select_columns} FROM {table}{order_by};"); - } - - format!("SELECT {select_columns} FROM {table}{order_by} LIMIT {limit};") } fn informix_row_limit_clause(limit: usize, offset: usize) -> String { @@ -198,7 +182,7 @@ fn informix_row_limit_clause(limit: usize, offset: usize) -> String { } } -fn build_oceanbase_oracle_table_select_sql( +fn build_rownum_table_select_sql( table: &str, where_clause: &str, order: &str, diff --git a/crates/dbx-core/src/sql_dialect/tests.rs b/crates/dbx-core/src/sql_dialect/tests.rs index 11e4f956a..ac8770f1f 100644 --- a/crates/dbx-core/src/sql_dialect/tests.rs +++ b/crates/dbx-core/src/sql_dialect/tests.rs @@ -52,6 +52,29 @@ fn qualifies_schema_only_for_schema_aware_databases() { ); } +#[test] +fn maps_table_pagination_strategy_by_database_type() { + assert_eq!(table_pagination_strategy(Some(DatabaseType::Mysql)), TablePaginationStrategy::LimitOffset); + assert_eq!(table_pagination_strategy(Some(DatabaseType::Dameng)), TablePaginationStrategy::FetchFirst); + assert_eq!(table_pagination_strategy(Some(DatabaseType::Db2)), TablePaginationStrategy::Db2FetchFirst); + assert_eq!(table_pagination_strategy(Some(DatabaseType::SqlServer)), TablePaginationStrategy::SqlServerTop); + assert_eq!(table_pagination_strategy(Some(DatabaseType::Iris)), TablePaginationStrategy::IrisTop); + assert_eq!(table_pagination_strategy(Some(DatabaseType::Informix)), TablePaginationStrategy::InformixFirst); + assert_eq!(table_pagination_strategy(Some(DatabaseType::OceanbaseOracle)), TablePaginationStrategy::Rownum); + assert_eq!(table_pagination_strategy(Some(DatabaseType::Questdb)), TablePaginationStrategy::QuestDbLimit); + assert_eq!(table_pagination_strategy(Some(DatabaseType::Oracle)), TablePaginationStrategy::Unbounded); + assert_eq!( + pagination_strategy(Some(DatabaseType::Oracle), PaginationContext::BoundedRead), + TablePaginationStrategy::FetchFirst + ); + assert_eq!( + pagination_strategy(Some(DatabaseType::Oracle), PaginationContext::UserQuery), + TablePaginationStrategy::Unbounded + ); + assert_eq!(table_pagination_strategy(Some(DatabaseType::Jdbc)), TablePaginationStrategy::AgentMaxRows); + assert_eq!(table_pagination_strategy(None), TablePaginationStrategy::LimitOffset); +} + #[test] fn builds_select_sql_with_limit_syntax_for_database_type() { let columns = vec!["id".to_string(), "name".to_string()];