fix: 修复 Oracle 表数据分页,避免 ROWID 分页重复列

* 修复 Oracle 表数据分页

* fix: 避免 Oracle ROWID 分页重复列

---------

Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
zipg 2026-06-25 22:46:02 +08:00 committed by GitHub
parent 6acf5e510c
commit b0f1be4df9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 99 additions and 22 deletions

View File

@ -65,12 +65,13 @@ pub fn uses_fetch_first(database_type: DatabaseType) -> bool {
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) if matches!(context, PaginationContext::TablePreview) => {
TablePaginationStrategy::Rownum
}
Some(DatabaseType::Oracle) => TablePaginationStrategy::FetchFirst,
Some(DatabaseType::Oracle) if matches!(context, PaginationContext::BoundedRead) => {
TablePaginationStrategy::FetchFirst
}
Some(DatabaseType::Oracle) => TablePaginationStrategy::Unbounded,
Some(DatabaseType::Dameng) => TablePaginationStrategy::FetchFirst,
Some(DatabaseType::Db2) => TablePaginationStrategy::Db2FetchFirst,
Some(DatabaseType::SqlServer) => TablePaginationStrategy::SqlServerTop,

View File

@ -35,6 +35,16 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
} else {
build_select_columns(database_type, &options.columns)
};
let rownum_select_columns = quoted_table_columns_or_star(database_type, &options.columns);
let page_select_columns = if options.include_row_id && database_type == Some(DatabaseType::Oracle) {
if options.columns.is_empty() {
"*".to_string()
} else {
format!("\"{DBX_ROWID_COLUMN}\", {rownum_select_columns}")
}
} else {
rownum_select_columns.clone()
};
let table_alias = if options.include_row_id && database_type.is_some_and(uses_fetch_first) {
format!("{table} t")
} else {
@ -70,7 +80,20 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
)
}
TablePaginationStrategy::Rownum => {
build_rownum_table_select_sql(&table_alias, &where_clause, &order, &select_columns, limit)
let rownum_inner_select_columns = if options.include_row_id && database_type == Some(DatabaseType::Oracle) {
&select_columns
} else {
&rownum_select_columns
};
build_rownum_table_select_sql(
&table_alias,
&where_clause,
&order,
rownum_inner_select_columns,
&page_select_columns,
limit,
options.offset.unwrap_or(0),
)
}
TablePaginationStrategy::Unbounded => {
format!("SELECT {select_columns} FROM {table_alias}{where_clause}{order}")
@ -108,16 +131,7 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
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::<Vec<_>>()
.join(", ")
};
let select_columns = quoted_table_columns_or_star(database_type, options.columns);
let order_by = if options.order_columns.is_empty() {
String::new()
} else {
@ -138,7 +152,9 @@ pub fn build_table_select_sql(options: TableSelectSqlOptions<'_>) -> String {
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::Rownum => {
build_rownum_table_select_sql(&table, "", &order_by, &select_columns, &select_columns, limit, 0)
}
TablePaginationStrategy::Db2FetchFirst | TablePaginationStrategy::FetchFirst => {
format!("SELECT {select_columns} FROM {table}{order_by} FETCH FIRST {limit} ROWS ONLY")
}
@ -161,15 +177,32 @@ fn informix_row_limit_clause(limit: usize, offset: usize) -> String {
}
}
fn quoted_table_columns_or_star(database_type: Option<DatabaseType>, columns: &[String]) -> String {
if columns.is_empty() {
return "*".to_string();
}
columns.iter().map(|column| quote_table_identifier(database_type, column)).collect::<Vec<_>>().join(", ")
}
fn build_rownum_table_select_sql(
table: &str,
where_clause: &str,
order: &str,
select_columns: &str,
inner_select_columns: &str,
outer_select_columns: &str,
limit: usize,
offset: usize,
) -> String {
let inner_select = format!("SELECT {select_columns} FROM {table}{where_clause}{order}");
format!("SELECT {select_columns} FROM ({inner_select}) WHERE ROWNUM <= {limit}")
let inner_select = format!("SELECT {inner_select_columns} FROM {table}{where_clause}{order}");
if offset == 0 {
return format!("SELECT {outer_select_columns} FROM ({inner_select}) WHERE ROWNUM <= {limit}");
}
let row_number_alias = quote_table_identifier(Some(DatabaseType::Oracle), "__dbx_row_num");
let end = offset + limit;
format!(
"SELECT {outer_select_columns} FROM (SELECT dbx_inner.*, ROWNUM AS {row_number_alias} FROM ({inner_select}) dbx_inner WHERE ROWNUM <= {end}) WHERE {row_number_alias} > {offset}"
)
}
pub(super) fn is_tdengine_tbname(database_type: Option<DatabaseType>, name: &str) -> bool {

View File

@ -63,7 +63,7 @@ fn maps_table_pagination_strategy_by_database_type() {
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!(table_pagination_strategy(Some(DatabaseType::Oracle)), TablePaginationStrategy::Rownum);
assert_eq!(
pagination_strategy(Some(DatabaseType::Oracle), PaginationContext::BoundedRead),
TablePaginationStrategy::FetchFirst
@ -114,6 +114,17 @@ fn builds_select_sql_with_limit_syntax_for_database_type() {
}),
"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::Oracle),
schema: Some("DBXTEST"),
table_name: "USERS",
columns: &columns,
order_columns: &keys,
limit: 100,
}),
"SELECT \"id\", \"name\" FROM (SELECT \"id\", \"name\" FROM \"DBXTEST\".\"USERS\" ORDER BY \"id\" ASC) WHERE ROWNUM <= 100"
);
assert_eq!(
build_table_select_sql(TableSelectSqlOptions {
database_type: Some(DatabaseType::OceanbaseOracle),
@ -304,6 +315,22 @@ fn builds_table_data_where_and_schema_queries() {
}),
"SELECT * FROM \"DB2INST1\".\"ORDERS\" WHERE (amount > 10) FETCH FIRST 50 ROWS ONLY"
);
assert_eq!(
build_table_data_select_sql(TableDataSelectSqlOptions {
database_type: Some(DatabaseType::Oracle),
schema: Some("DBXTEST".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: Some("\"ID\" ASC".to_string()),
limit: Some(50),
offset: Some(100),
where_input: Some("WHERE amount > 10".to_string()),
include_row_id: false,
}),
"SELECT \"ID\", \"AMOUNT\" FROM (SELECT dbx_inner.*, ROWNUM AS \"__dbx_row_num\" FROM (SELECT \"ID\", \"AMOUNT\" FROM \"DBXTEST\".\"ORDERS\" WHERE (amount > 10) ORDER BY \"ID\" ASC) dbx_inner WHERE ROWNUM <= 150) WHERE \"__dbx_row_num\" > 100"
);
assert_eq!(
build_table_data_select_sql(TableDataSelectSqlOptions {
database_type: Some(DatabaseType::OceanbaseOracle),
@ -557,7 +584,23 @@ fn builds_oracle_and_neo4j_table_data_queries() {
where_input: None,
include_row_id: true,
}),
"SELECT ROWIDTOCHAR(t.ROWID) AS \"__DBX_ROWID\", t.* FROM \"DBXTEST\".\"DBX_LOAD_TABLE_006\" t"
"SELECT * FROM (SELECT ROWIDTOCHAR(t.ROWID) AS \"__DBX_ROWID\", t.* FROM \"DBXTEST\".\"DBX_LOAD_TABLE_006\" t) WHERE ROWNUM <= 100"
);
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!["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: true,
}),
"SELECT \"__DBX_ROWID\", \"ID\", \"NAME\" FROM (SELECT ROWIDTOCHAR(t.ROWID) AS \"__DBX_ROWID\", t.* FROM \"DBXTEST\".\"DBX_LOAD_TABLE_006\" t) WHERE ROWNUM <= 100"
);
assert_eq!(
build_table_data_select_sql(TableDataSelectSqlOptions {