fix(sql): centralize table pagination strategies
This commit is contained in:
parent
e27d34a34a
commit
535d247ae1
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()];
|
||||
|
|
|
|||
Loading…
Reference in New Issue