diff --git a/Cargo.lock b/Cargo.lock index 6576992b82..c2550a3365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1879,7 +1879,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2213,7 +2213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3211,7 +3211,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -4195,7 +4195,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4286,7 +4286,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5657,7 +5657,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6234,7 +6234,7 @@ dependencies = [ [[package]] name = "rust-oracle" version = "0.1.6" -source = "git+https://github.com/t8y2/rust-oracle?branch=main#6d16519f51791e04511b02ee7b3cbf5fc5524b4e" +source = "git+https://github.com/t8y2/rust-oracle?branch=main#5620073f31adda95b9781d175a77d535b9c198ed" dependencies = [ "aes 0.8.4", "async-trait", @@ -6336,7 +6336,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6349,7 +6349,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6450,7 +6450,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7102,7 +7102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8042,7 +8042,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -8541,7 +8541,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/crates/dbx-core/src/connection_secrets.rs b/crates/dbx-core/src/connection_secrets.rs index d3475cf712..1622805080 100644 --- a/crates/dbx-core/src/connection_secrets.rs +++ b/crates/dbx-core/src/connection_secrets.rs @@ -289,6 +289,8 @@ mod tests { ssl: false, sysdba: false, connection_string: None, + jdbc_driver_class: None, + jdbc_driver_paths: Vec::new(), } } diff --git a/crates/dbx-core/src/db/oracle_driver.rs b/crates/dbx-core/src/db/oracle_driver.rs index ccc59235ea..9dd0135d2e 100644 --- a/crates/dbx-core/src/db/oracle_driver.rs +++ b/crates/dbx-core/src/db/oracle_driver.rs @@ -7,6 +7,11 @@ use crate::sql::starts_with_executable_sql_keyword; use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; pub type OracleClient = Connection; +const ORACLE_QUERY_LIMIT: usize = crate::query::MAX_ROWS + 1; + +fn quote_literal(value: &str) -> String { + format!("'{}'", value.replace('\'', "''")) +} pub async fn connect( host: &str, @@ -33,6 +38,18 @@ fn value_to_json(val: &rust_oracle::Value) -> serde_json::Value { rust_oracle::Value::Float(f) => { serde_json::Number::from_f64(*f).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null) } + rust_oracle::Value::Date(d) => serde_json::Value::String(format!( + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}", + d.year, d.month, d.day, d.hour, d.minute, d.second + )), + rust_oracle::Value::Timestamp(ts) => { + let base = format!( + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}", + ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second + ); + let value = if ts.microsecond > 0 { format!("{base}.{:06}", ts.microsecond) } else { base }; + serde_json::Value::String(value) + } rust_oracle::Value::Boolean(b) => serde_json::Value::Bool(*b), rust_oracle::Value::Json(v) => v.clone(), _ => serde_json::Value::String(format!("{val:?}")), @@ -73,12 +90,15 @@ pub async fn list_schemas(conn: &OracleClient) -> Result, String> { } pub async fn list_tables(conn: &OracleClient, schema: &str) -> Result, String> { + let s = quote_literal(schema); let sql = format!( - "SELECT object_name, \ - CASE object_type WHEN 'VIEW' THEN 'VIEW' ELSE 'TABLE' END AS table_type \ - FROM all_objects WHERE owner = '{s}' AND object_type IN ('TABLE','VIEW') \ - ORDER BY object_name", - s = schema.replace('\'', "''") + "SELECT o.OBJECT_NAME, \ + CASE o.OBJECT_TYPE WHEN 'VIEW' THEN 'VIEW' ELSE 'TABLE' END AS TABLE_TYPE, \ + c.COMMENTS \ + FROM ALL_OBJECTS o \ + LEFT JOIN ALL_TAB_COMMENTS c ON c.OWNER = o.OWNER AND c.TABLE_NAME = o.OBJECT_NAME \ + WHERE o.OWNER = {s} AND o.OBJECT_TYPE IN ('TABLE','VIEW') \ + ORDER BY o.OBJECT_NAME" ); log::debug!("[oracle] list_tables: schema={schema}, sql={sql}"); let result = conn.query(&sql, &[]).await.map_err(|e| { @@ -91,40 +111,31 @@ pub async fn list_tables(conn: &OracleClient, schema: &str) -> Result Result, String> { log::debug!("[oracle] get_columns: schema={schema}, table={table}"); - let s = schema.replace('\'', "''"); - let t = table.replace('\'', "''"); - - let pk_result = conn - .query( - &format!( - "SELECT cols.COLUMN_NAME FROM ALL_CONS_COLUMNS cols \ - JOIN ALL_CONSTRAINTS cons ON cols.CONSTRAINT_NAME = cons.CONSTRAINT_NAME AND cols.OWNER = cons.OWNER \ - WHERE cons.CONSTRAINT_TYPE = 'P' AND cons.OWNER = '{s}' AND cons.TABLE_NAME = '{t}'" - ), - &[], - ) - .await - .map_err(|e| { - log::error!("[oracle] get_columns pk query failed: {e}"); - e.to_string() - })?; - let pk_names: std::collections::HashSet = - pk_result.rows.iter().filter_map(|row| row.get_string(0).map(|s| s.to_string())).collect(); + let s = quote_literal(schema); + let t = quote_literal(table); let col_result = conn .query( &format!( - "SELECT COLUMN_NAME, DATA_TYPE, NULLABLE, DATA_PRECISION, DATA_SCALE, DATA_LENGTH, CHAR_LENGTH \ - FROM ALL_TAB_COLUMNS \ - WHERE OWNER = '{s}' AND TABLE_NAME = '{t}' \ - ORDER BY COLUMN_ID" + "SELECT c.COLUMN_NAME, c.DATA_TYPE, c.NULLABLE, c.DATA_PRECISION, c.DATA_SCALE, c.DATA_LENGTH, \ + c.CHAR_LENGTH, cc.COMMENTS, CASE WHEN pk.COLUMN_NAME IS NULL THEN 0 ELSE 1 END AS IS_PK \ + FROM ALL_TAB_COLUMNS c \ + LEFT JOIN ALL_COL_COMMENTS cc ON cc.OWNER = c.OWNER AND cc.TABLE_NAME = c.TABLE_NAME AND cc.COLUMN_NAME = c.COLUMN_NAME \ + LEFT JOIN ( \ + SELECT cols.COLUMN_NAME \ + FROM ALL_CONS_COLUMNS cols \ + JOIN ALL_CONSTRAINTS cons ON cols.CONSTRAINT_NAME = cons.CONSTRAINT_NAME AND cols.OWNER = cons.OWNER \ + WHERE cons.CONSTRAINT_TYPE = 'P' AND cons.OWNER = {s} AND cons.TABLE_NAME = {t} \ + ) pk ON pk.COLUMN_NAME = c.COLUMN_NAME \ + WHERE c.OWNER = {s} AND c.TABLE_NAME = {t} \ + ORDER BY c.COLUMN_ID" ), &[], ) @@ -161,13 +172,13 @@ pub async fn get_columns(conn: &OracleClient, schema: &str, table: &str) -> Resu _ => base, }; ColumnInfo { - is_primary_key: pk_names.contains(&name), + is_primary_key: row.get_i64(8).unwrap_or(0) == 1, name, data_type, is_nullable: row.get_string(2).unwrap_or("N") == "Y", column_default: None, extra: None, - comment: None, + comment: row.get_string(7).filter(|s| !s.is_empty()).map(|s| s.to_string()), numeric_precision: num_prec, numeric_scale: num_scale, character_maximum_length: char_len, @@ -176,6 +187,16 @@ pub async fn get_columns(conn: &OracleClient, schema: &str, table: &str) -> Resu .collect()) } +pub async fn get_table_comment(conn: &OracleClient, schema: &str, table: &str) -> Result, String> { + let s = quote_literal(schema); + let t = quote_literal(table); + let result = conn + .query(&format!("SELECT COMMENTS FROM ALL_TAB_COMMENTS WHERE OWNER = {s} AND TABLE_NAME = {t}"), &[]) + .await + .map_err(|e| e.to_string())?; + Ok(result.rows.first().and_then(|row| row.get_string(0)).filter(|s| !s.is_empty()).map(|s| s.to_string())) +} + pub async fn list_indexes(conn: &OracleClient, schema: &str, table: &str) -> Result, String> { let sql = format!( "SELECT i.INDEX_NAME, \ @@ -262,17 +283,17 @@ pub async fn execute_query(conn: &OracleClient, sql: &str) -> Result = result.columns.iter().map(|c| c.name.clone()).collect(); - let rows: Vec> = result + let mut rows: Vec> = result .rows .iter() .map(|row| { @@ -281,16 +302,14 @@ pub async fn execute_query(conn: &OracleClient, sql: &str) -> Result crate::query::MAX_ROWS || result.has_more_rows; + if rows.len() > crate::query::MAX_ROWS { + rows.truncate(crate::query::MAX_ROWS); + } - Ok(QueryResult { - columns, - rows, - affected_rows: 0, - execution_time_ms: start.elapsed().as_millis(), - truncated: false, - }) + Ok(QueryResult { columns, rows, affected_rows: 0, execution_time_ms: start.elapsed().as_millis(), truncated }) } else { - match conn.execute(sql, &[]).await { + match conn.execute(sql.as_ref(), &[]).await { Ok(result) => { let _ = conn.commit().await; Ok(QueryResult { @@ -313,6 +332,18 @@ pub async fn execute_query(conn: &OracleClient, sql: &str) -> Result std::borrow::Cow<'_, str> { + if !starts_with_executable_sql_keyword(sql, &["SELECT", "WITH"]) || has_for_update_clause(sql) { + return std::borrow::Cow::Borrowed(sql); + } + + std::borrow::Cow::Owned(format!("SELECT * FROM ({sql}) WHERE ROWNUM <= {ORACLE_QUERY_LIMIT}")) +} + +fn has_for_update_clause(sql: &str) -> bool { + sql.to_uppercase().contains(" FOR UPDATE") +} + fn rewrite_fetch_first(sql: &str) -> std::borrow::Cow<'_, str> { let upper = sql.to_uppercase(); // Match: ... [OFFSET M ROWS] FETCH FIRST|NEXT N ROWS ONLY @@ -346,3 +377,34 @@ fn rewrite_fetch_first(sql: &str) -> std::borrow::Cow<'_, str> { std::borrow::Cow::Owned(format!("SELECT * FROM ({base}) WHERE ROWNUM <= {n}")) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cap_select_rows_wraps_selects() { + let sql = "SELECT * FROM users ORDER BY id"; + assert_eq!(cap_select_rows(sql), format!("SELECT * FROM ({sql}) WHERE ROWNUM <= {ORACLE_QUERY_LIMIT}")); + } + + #[test] + fn cap_select_rows_wraps_ctes() { + let sql = "WITH recent AS (SELECT * FROM users) SELECT * FROM recent"; + assert_eq!(cap_select_rows(sql), format!("SELECT * FROM ({sql}) WHERE ROWNUM <= {ORACLE_QUERY_LIMIT}")); + } + + #[test] + fn cap_select_rows_keeps_for_update_queries() { + let sql = "SELECT * FROM users FOR UPDATE"; + assert_eq!(cap_select_rows(sql), sql); + } + + #[test] + fn rewrite_fetch_first_to_rownum() { + assert_eq!( + rewrite_fetch_first("SELECT * FROM users FETCH FIRST 20 ROWS ONLY"), + "SELECT * FROM (SELECT * FROM users) WHERE ROWNUM <= 20" + ); + } +} diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index f12aad6e92..ea938b9efb 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -397,6 +397,8 @@ mod tests { ssl: false, sysdba: false, connection_string: None, + jdbc_driver_class: None, + jdbc_driver_paths: Vec::new(), } } diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index 7836581d80..32120c7350 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -737,6 +737,7 @@ pub async fn build_oracle_ddl( let columns = db::oracle_driver::get_columns(client, schema, table).await?; let indexes = db::oracle_driver::list_indexes(client, schema, table).await?; let fkeys = db::oracle_driver::list_foreign_keys(client, schema, table).await?; + let table_comment = db::oracle_driver::get_table_comment(client, schema, table).await?; let mut ddl = format!("CREATE TABLE \"{schema}\".\"{table}\" (\n"); let col_lines: Vec = columns @@ -769,6 +770,19 @@ pub async fn build_oracle_ddl( } ddl.push_str("\n);\n"); + if let Some(comment) = table_comment.as_deref().filter(|comment| !comment.trim().is_empty()) { + ddl.push_str(&format!("\nCOMMENT ON TABLE \"{schema}\".\"{table}\" IS '{}';", comment.replace('\'', "''"))); + } + for col in &columns { + if let Some(comment) = col.comment.as_deref().filter(|comment| !comment.trim().is_empty()) { + ddl.push_str(&format!( + "\nCOMMENT ON COLUMN \"{schema}\".\"{table}\".\"{}\" IS '{}';", + col.name, + comment.replace('\'', "''") + )); + } + } + for idx in &indexes { if idx.is_primary { continue;