fix(oracle): omit synthetic rowid from table exports
This commit is contained in:
parent
872e59f207
commit
1e7a33bd99
|
|
@ -638,11 +638,12 @@ pub fn build_export_insert_statements(options: BuildExportInsertStatementsOption
|
|||
.columns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, _)| {
|
||||
!is_postgres_tsvector_export_column(
|
||||
options.database_type,
|
||||
options.column_types.get(*index).and_then(|value| value.as_deref()),
|
||||
)
|
||||
.filter(|(index, column)| {
|
||||
!is_internal_export_column(options.database_type, column)
|
||||
&& !is_postgres_tsvector_export_column(
|
||||
options.database_type,
|
||||
options.column_types.get(*index).and_then(|value| value.as_deref()),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if insert_columns.is_empty() {
|
||||
|
|
@ -695,6 +696,12 @@ pub fn build_export_insert_statements(options: BuildExportInsertStatementsOption
|
|||
Ok(statements)
|
||||
}
|
||||
|
||||
pub(crate) fn is_internal_export_column(database_type: Option<DatabaseType>, column: &str) -> bool {
|
||||
// Oracle ROWID is injected only to identify editable rows. It is not a
|
||||
// physical table column and must never propagate into exported SQL.
|
||||
database_type == Some(DatabaseType::Oracle) && column.eq_ignore_ascii_case(crate::sql_dialect::DBX_ROWID_COLUMN)
|
||||
}
|
||||
|
||||
fn is_postgres_tsvector_export_column(database_type: Option<DatabaseType>, column_type: Option<&str>) -> bool {
|
||||
database_type == Some(DatabaseType::Postgres)
|
||||
&& column_type
|
||||
|
|
@ -1733,6 +1740,42 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_export_omits_synthetic_rowid_from_insert_columns() {
|
||||
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
|
||||
database_type: Some(DatabaseType::Oracle),
|
||||
schema: Some("APP".to_string()),
|
||||
table_name: Some("USERS".to_string()),
|
||||
qualified_table_name: None,
|
||||
columns: vec!["__DBX_ROWID".to_string(), "ID".to_string(), "NAME".to_string()],
|
||||
column_types: vec![Some("VARCHAR2".to_string()), Some("NUMBER".to_string()), Some("VARCHAR2".to_string())],
|
||||
column_extras: Vec::new(),
|
||||
rows: vec![vec![json!("AAAPr9AAEAAAAGfAAA"), json!(1), json!("Ada")]],
|
||||
batch_size: Some(100),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(statements, vec!["INSERT INTO \"APP\".\"USERS\" (\"ID\", \"NAME\") VALUES (1, 'Ada');"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_oracle_export_preserves_dbx_rowid_named_column() {
|
||||
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
schema: None,
|
||||
table_name: Some("users".to_string()),
|
||||
qualified_table_name: None,
|
||||
columns: vec!["__DBX_ROWID".to_string(), "name".to_string()],
|
||||
column_types: Vec::new(),
|
||||
column_extras: Vec::new(),
|
||||
rows: vec![vec![json!(7), json!("Ada")]],
|
||||
batch_size: Some(100),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(statements, vec!["INSERT INTO `users` (`__DBX_ROWID`, `name`) VALUES (7, 'Ada');"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_date_columns_export_as_date_literals() {
|
||||
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ use crate::connection::MysqlMode;
|
|||
use crate::connection::{task_client_session_id, AppState, PoolKind};
|
||||
use crate::csv_export::{escape_csv, format_csv, format_tsv, format_tsv_rows, value_to_csv_text};
|
||||
pub use crate::database_export::ExportStatus;
|
||||
use crate::database_export::{build_export_insert_statements, is_export_cancelled, BuildExportInsertStatementsOptions};
|
||||
use crate::database_export::{
|
||||
build_export_insert_statements, is_export_cancelled, is_internal_export_column, BuildExportInsertStatementsOptions,
|
||||
};
|
||||
use crate::db::agent_driver::AgentTableReadStartParams;
|
||||
use crate::models::connection::DatabaseType;
|
||||
use crate::transfer::{
|
||||
|
|
@ -87,6 +89,37 @@ fn export_column_types(request: &TableExportRequest) -> Vec<String> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn resolve_requested_export_columns(
|
||||
database_type: DatabaseType,
|
||||
columns: &[String],
|
||||
column_types: Option<&[Option<String>]>,
|
||||
primary_keys: Option<&[String]>,
|
||||
) -> (Vec<String>, Vec<Option<String>>, Vec<String>) {
|
||||
let mut resolved_columns = Vec::with_capacity(columns.len());
|
||||
let mut resolved_column_types = column_types.map(|_| Vec::with_capacity(columns.len())).unwrap_or_default();
|
||||
|
||||
// Filter names and their index-aligned type metadata together so the
|
||||
// fetched rows and later SQL literal formatting keep the same positions.
|
||||
for (index, column) in columns.iter().enumerate() {
|
||||
if is_internal_export_column(Some(database_type), column) {
|
||||
continue;
|
||||
}
|
||||
resolved_columns.push(column.clone());
|
||||
if let Some(column_types) = column_types {
|
||||
resolved_column_types.push(column_types.get(index).cloned().unwrap_or(None));
|
||||
}
|
||||
}
|
||||
|
||||
let resolved_primary_keys = primary_keys
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter(|column| !is_internal_export_column(Some(database_type), column))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
(resolved_columns, resolved_column_types, resolved_primary_keys)
|
||||
}
|
||||
|
||||
fn write_json_row_object<W: Write>(writer: &mut W, columns: &[String], row: &[Value]) -> Result<(), String> {
|
||||
writer.write_all(b"{\n").map_err(|e| format!("Failed to write JSON: {e}"))?;
|
||||
let mut first = true;
|
||||
|
|
@ -825,9 +858,13 @@ pub async fn export_table_data_core(
|
|||
// directly, which avoids expensive metadata round-trips on JDBC drivers.
|
||||
let requested_columns = request.columns.as_ref().filter(|columns| !columns.is_empty());
|
||||
let (col_names, column_types, column_extras, primary_keys) = if let Some(requested_columns) = requested_columns {
|
||||
let primary_keys = request.primary_keys.clone().unwrap_or_default();
|
||||
let column_types = request.column_types.clone().unwrap_or_default();
|
||||
(requested_columns.clone(), column_types, Vec::new(), primary_keys)
|
||||
let (col_names, column_types, primary_keys) = resolve_requested_export_columns(
|
||||
db_type,
|
||||
requested_columns,
|
||||
request.column_types.as_deref(),
|
||||
request.primary_keys.as_deref(),
|
||||
);
|
||||
(col_names, column_types, Vec::new(), primary_keys)
|
||||
} else {
|
||||
let columns = crate::schema::get_columns_core(
|
||||
state,
|
||||
|
|
@ -1603,6 +1640,66 @@ mod tests {
|
|||
assert!(!sql.contains("ROWNUM"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_requested_export_columns_omit_synthetic_rowid_and_keep_metadata_aligned() {
|
||||
let columns = vec!["__DBX_ROWID".to_string(), "ID".to_string(), "NAME".to_string()];
|
||||
let column_types = vec![Some("VARCHAR2".to_string()), Some("NUMBER".to_string()), Some("VARCHAR2".to_string())];
|
||||
let primary_keys = vec!["__DBX_ROWID".to_string()];
|
||||
|
||||
let (columns, column_types, primary_keys) =
|
||||
resolve_requested_export_columns(DatabaseType::Oracle, &columns, Some(&column_types), Some(&primary_keys));
|
||||
|
||||
assert_eq!(columns, vec!["ID", "NAME"]);
|
||||
assert_eq!(column_types, vec![Some("NUMBER".to_string()), Some("VARCHAR2".to_string())]);
|
||||
assert!(primary_keys.is_empty());
|
||||
|
||||
let request = TableExportRequest {
|
||||
export_id: "export-rowid".to_string(),
|
||||
connection_id: "conn-1".to_string(),
|
||||
database: "ORCL".to_string(),
|
||||
schema: Some("APP".to_string()),
|
||||
table_name: "USERS".to_string(),
|
||||
file_path: "users.sql".to_string(),
|
||||
format: "sql".to_string(),
|
||||
columns: None,
|
||||
column_types: None,
|
||||
primary_keys: None,
|
||||
where_input: None,
|
||||
order_by: None,
|
||||
skip_count: false,
|
||||
batch_size: Some(100),
|
||||
row_limit: None,
|
||||
};
|
||||
let sql = table_cursor_sql(&request, &DatabaseType::Oracle, &columns, &primary_keys);
|
||||
assert_eq!(sql, "SELECT \"ID\", \"NAME\" FROM \"APP\".\"USERS\"");
|
||||
|
||||
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
|
||||
database_type: Some(DatabaseType::Oracle),
|
||||
schema: request.schema,
|
||||
table_name: Some(request.table_name),
|
||||
qualified_table_name: None,
|
||||
columns,
|
||||
column_types,
|
||||
column_extras: Vec::new(),
|
||||
rows: vec![vec![json!(1), json!("Ada")]],
|
||||
batch_size: Some(100),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(statements, vec!["INSERT INTO \"APP\".\"USERS\" (\"ID\", \"NAME\") VALUES (1, 'Ada');"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requested_export_columns_preserve_regular_oracle_and_non_oracle_columns() {
|
||||
let oracle_columns = vec!["ROW_ID".to_string(), "NAME".to_string()];
|
||||
let (resolved_oracle, _, _) =
|
||||
resolve_requested_export_columns(DatabaseType::Oracle, &oracle_columns, None, None);
|
||||
assert_eq!(resolved_oracle, oracle_columns);
|
||||
|
||||
let mysql_columns = vec!["__DBX_ROWID".to_string(), "name".to_string()];
|
||||
let (resolved_mysql, _, _) = resolve_requested_export_columns(DatabaseType::Mysql, &mysql_columns, None, None);
|
||||
assert_eq!(resolved_mysql, mysql_columns);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_table_read_unsupported_detects_old_agent_errors() {
|
||||
assert!(is_agent_table_read_unsupported("Agent RPC error (-1): unknown method: start_table_read"));
|
||||
|
|
|
|||
Loading…
Reference in New Issue