fix(sql): centralize table pagination strategies

This commit is contained in:
t8y2 2026-06-19 10:23:14 +08:00
parent e27d34a34a
commit 535d247ae1
7 changed files with 355 additions and 159 deletions

View File

@ -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::<Vec<_>>().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;

View File

@ -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<Da
let searchable_columns =
text_columns.iter().chain(numeric_columns.iter()).map(|column| column.name.clone()).collect();
let sql = if options.database_type == Some(DatabaseType::SqlServer) {
format!("SELECT TOP {limit} * FROM {table} WHERE ({where_clause})")
} else if options.database_type.is_some_and(uses_fetch_first) {
format!("SELECT * FROM {table} WHERE ({where_clause}) FETCH FIRST {limit} ROWS ONLY")
} else if options.database_type == Some(DatabaseType::Jdbc) {
// JDBC connections avoid SQL-level LIMIT; the agent truncates rows while reading.
format!("SELECT * FROM {table} WHERE ({where_clause});")
} else {
format!("SELECT * FROM {table} WHERE ({where_clause}) LIMIT {limit};")
let sql = match pagination_strategy(options.database_type, PaginationContext::BoundedRead) {
TablePaginationStrategy::SqlServerTop => 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<DatabaseType>, 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!(

View File

@ -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<Regex> = 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 {

View File

@ -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};

View File

@ -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<DatabaseType>, 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<DatabaseType>) -> 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 {

View File

@ -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,

View File

@ -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()];