diff --git a/src-tauri/src/commands/schema.rs b/src-tauri/src/commands/schema.rs index 45f88a038..0f280389e 100644 --- a/src-tauri/src/commands/schema.rs +++ b/src-tauri/src/commands/schema.rs @@ -51,6 +51,7 @@ fn duckdb_query_columns(con: &duckdb::Connection, table: &str) -> Result Resu extra: None, comment: None, numeric_precision: None, numeric_scale: None, + character_maximum_length: None, } }).collect()) } diff --git a/src-tauri/src/db/mod.rs b/src-tauri/src/db/mod.rs index 2c0e96630..754bab761 100644 --- a/src-tauri/src/db/mod.rs +++ b/src-tauri/src/db/mod.rs @@ -33,6 +33,7 @@ pub struct ColumnInfo { pub comment: Option, pub numeric_precision: Option, pub numeric_scale: Option, + pub character_maximum_length: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src-tauri/src/db/mysql.rs b/src-tauri/src/db/mysql.rs index 3e58f9785..f444a4743 100644 --- a/src-tauri/src/db/mysql.rs +++ b/src-tauri/src/db/mysql.rs @@ -215,9 +215,9 @@ pub async fn get_columns( table: &str, ) -> Result, String> { let sql = format!( - "SELECT c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT, c.EXTRA, c.COLUMN_COMMENT, \ + "SELECT c.COLUMN_NAME, c.COLUMN_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT, c.EXTRA, c.COLUMN_COMMENT, \ CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PK, \ - c.NUMERIC_PRECISION, c.NUMERIC_SCALE \ + c.NUMERIC_PRECISION, c.NUMERIC_SCALE, c.CHARACTER_MAXIMUM_LENGTH \ FROM information_schema.COLUMNS c \ LEFT JOIN information_schema.KEY_COLUMN_USAGE kcu \ ON c.TABLE_SCHEMA = kcu.TABLE_SCHEMA \ @@ -238,7 +238,7 @@ pub async fn get_columns( .iter() .map(|row| ColumnInfo { name: get_str_by_name(row, "COLUMN_NAME"), - data_type: get_str_by_name(row, "DATA_TYPE"), + data_type: get_str_by_name(row, "COLUMN_TYPE"), is_nullable: get_str_by_name(row, "IS_NULLABLE") == "YES", column_default: get_opt_str(row, "COLUMN_DEFAULT"), is_primary_key: row.get::("IS_PK") == 1, @@ -246,6 +246,7 @@ pub async fn get_columns( comment: get_opt_str(row, "COLUMN_COMMENT").filter(|s| !s.is_empty()), numeric_precision: get_opt_i32(row, "NUMERIC_PRECISION"), numeric_scale: get_opt_i32(row, "NUMERIC_SCALE"), + character_maximum_length: get_opt_i32(row, "CHARACTER_MAXIMUM_LENGTH"), }) .collect()) } @@ -331,10 +332,10 @@ pub async fn execute_query(pool: &MySqlPool, sql: &str, bare: bool) -> Result Result, String> { let sql = format!( "SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS columns, \ - NOT NON_UNIQUE AS is_unique, INDEX_NAME = 'PRIMARY' AS is_primary \ + MIN(NON_UNIQUE) = 0 AS is_unique, INDEX_NAME = 'PRIMARY' AS is_primary \ FROM information_schema.STATISTICS \ WHERE TABLE_SCHEMA = {} AND TABLE_NAME = {} \ - GROUP BY INDEX_NAME, NON_UNIQUE \ + GROUP BY INDEX_NAME \ ORDER BY INDEX_NAME", quote_value(database), quote_value(table), diff --git a/src-tauri/src/db/oracle_driver.rs b/src-tauri/src/db/oracle_driver.rs index 9af63c5c5..d09f2bd6a 100644 --- a/src-tauri/src/db/oracle_driver.rs +++ b/src-tauri/src/db/oracle_driver.rs @@ -81,7 +81,7 @@ pub async fn get_columns(conn: &OracleClient, schema: &str, table: &str) -> Resu let col_result = conn.query( &format!( - "SELECT COLUMN_NAME, DATA_TYPE, NULLABLE, DATA_PRECISION, DATA_SCALE \ + "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" @@ -91,15 +91,40 @@ pub async fn get_columns(conn: &OracleClient, schema: &str, table: &str) -> Resu Ok(col_result.rows.iter().map(|row| { let name = row.get_string(0).unwrap_or("").to_string(); + let base = row.get_string(1).unwrap_or("").to_string(); + let data_len = row.get_i64(5).map(|v| v as i32); + let char_len = row.get_i64(6).map(|v| v as i32); + let num_prec = row.get_i64(3).map(|v| v as i32); + let num_scale = row.get_i64(4).map(|v| v as i32); + let data_type = match base.to_uppercase().as_str() { + "VARCHAR2" | "NVARCHAR2" | "CHAR" | "NCHAR" => { + let len = char_len.or(data_len); + match len { + Some(n) => format!("{base}({n})"), + None => base, + } + } + "NUMBER" => match (num_prec, num_scale) { + (Some(p), Some(s)) if s > 0 => format!("NUMBER({p},{s})"), + (Some(p), _) if p > 0 => format!("NUMBER({p})"), + _ => "NUMBER".to_string(), + }, + "RAW" => match data_len { + Some(n) => format!("RAW({n})"), + None => "RAW".to_string(), + }, + _ => base, + }; ColumnInfo { is_primary_key: pk_names.contains(&name), name, - data_type: row.get_string(1).unwrap_or("").to_string(), + data_type, is_nullable: row.get_string(2).unwrap_or("N") == "Y", column_default: None, extra: None, comment: None, - numeric_precision: row.get_i64(3).map(|v| v as i32), - numeric_scale: row.get_i64(4).map(|v| v as i32), + numeric_precision: num_prec, + numeric_scale: num_scale, + character_maximum_length: char_len, } }).collect()) } @@ -111,7 +136,7 @@ pub async fn list_indexes(conn: &OracleClient, schema: &str, table: &str) -> Res i.UNIQUENESS, \ CASE WHEN c.CONSTRAINT_TYPE = 'P' THEN 1 ELSE 0 END AS IS_PK \ FROM ALL_INDEXES i \ - JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.TABLE_OWNER = ic.TABLE_OWNER \ + JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.OWNER = ic.INDEX_OWNER AND i.TABLE_OWNER = ic.TABLE_OWNER \ LEFT JOIN ALL_CONSTRAINTS c ON i.INDEX_NAME = c.INDEX_NAME AND i.TABLE_OWNER = c.OWNER \ AND c.CONSTRAINT_TYPE = 'P' \ WHERE i.TABLE_OWNER = '{s}' AND i.TABLE_NAME = '{t}' \ diff --git a/src-tauri/src/db/postgres.rs b/src-tauri/src/db/postgres.rs index c251aa9f9..5f11c70e1 100644 --- a/src-tauri/src/db/postgres.rs +++ b/src-tauri/src/db/postgres.rs @@ -154,21 +154,29 @@ pub async fn get_columns( table: &str, ) -> Result, String> { let rows: Vec = sqlx::query( - "SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, \ - CASE WHEN tc.constraint_type = 'PRIMARY KEY' THEN true ELSE false END AS is_pk, \ - col_description((c.table_schema || '.' || c.table_name)::regclass, c.ordinal_position) AS column_comment, \ - c.numeric_precision, c.numeric_scale \ - FROM information_schema.columns c \ - LEFT JOIN information_schema.key_column_usage kcu \ - ON c.table_schema = kcu.table_schema \ - AND c.table_name = kcu.table_name \ - AND c.column_name = kcu.column_name \ - LEFT JOIN information_schema.table_constraints tc \ - ON kcu.constraint_name = tc.constraint_name \ - AND kcu.table_schema = tc.table_schema \ - AND tc.constraint_type = 'PRIMARY KEY' \ - WHERE c.table_schema = $1 AND c.table_name = $2 \ - ORDER BY c.ordinal_position", + "SELECT a.attname AS column_name, \ + format_type(a.atttypid, a.atttypmod) AS full_type, \ + NOT a.attnotnull AS is_nullable, \ + pg_get_expr(ad.adbin, ad.adrelid) AS column_default, \ + EXISTS ( \ + SELECT 1 FROM pg_constraint co \ + JOIN pg_index i ON i.indrelid = co.conrelid AND co.conindid = i.indexrelid \ + WHERE co.conrelid = a.attrelid AND co.contype = 'p' \ + AND a.attnum = ANY(i.indkey) \ + ) AS is_pk, \ + col_description(a.attrelid, a.attnum) AS column_comment, \ + CASE WHEN t.typname = 'numeric' AND a.atttypmod > 0 \ + THEN ((a.atttypmod - 4) >> 16) & 65535 ELSE NULL END AS numeric_precision, \ + CASE WHEN t.typname = 'numeric' AND a.atttypmod > 0 \ + THEN ((a.atttypmod - 4) & 2047) - 1024 ELSE NULL END AS numeric_scale, \ + CASE WHEN t.typname IN ('varchar', 'bpchar') AND a.atttypmod > 0 \ + THEN a.atttypmod - 4 ELSE NULL END AS character_maximum_length \ + FROM pg_attribute a \ + JOIN pg_type t ON t.oid = a.atttypid \ + LEFT JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum \ + WHERE a.attrelid = ($1 || '.' || $2)::regclass \ + AND a.attnum > 0 AND NOT a.attisdropped \ + ORDER BY a.attnum", ) .bind(schema) .bind(table) @@ -178,16 +186,20 @@ pub async fn get_columns( Ok(rows .iter() - .map(|row| ColumnInfo { - name: row.get::("column_name"), - data_type: row.get::("data_type"), - is_nullable: row.get::("is_nullable") == "YES", - column_default: row.get::, _>("column_default"), - is_primary_key: row.get::("is_pk"), - extra: None, - comment: row.get::, _>("column_comment"), - numeric_precision: row.get::, _>("numeric_precision"), - numeric_scale: row.get::, _>("numeric_scale"), + .map(|row| { + let full_type = row.get::, _>("full_type").unwrap_or_default(); + ColumnInfo { + name: row.get::("column_name"), + data_type: full_type, + is_nullable: row.get::("is_nullable"), + column_default: row.get::, _>("column_default"), + is_primary_key: row.get::("is_pk"), + extra: None, + comment: row.get::, _>("column_comment"), + numeric_precision: row.get::, _>("numeric_precision"), + numeric_scale: row.get::, _>("numeric_scale"), + character_maximum_length: row.get::, _>("character_maximum_length"), + } }) .collect()) } @@ -202,19 +214,26 @@ pub async fn execute_query(pool: &PgPool, sql: &str) -> Result = desc.columns().iter().map(|c| c.name().to_string()).collect(); - let column_types: Vec = desc - .columns() - .iter() - .map(|c| c.type_info().name().to_string()) - .collect(); - let rows: Vec = sqlx::query(sql) + .persistent(false) .fetch_all(pool) .await .map_err(|e| e.to_string())?; + let (columns, column_types): (Vec, Vec) = if let Some(first) = rows.first() { + let cols = first.columns(); + ( + cols.iter().map(|c| c.name().to_string()).collect(), + cols.iter().map(|c| c.type_info().name().to_string()).collect(), + ) + } else { + let desc = pool.describe(sql).await.map_err(|e| e.to_string())?; + ( + desc.columns().iter().map(|c| c.name().to_string()).collect(), + desc.columns().iter().map(|c| c.type_info().name().to_string()).collect(), + ) + }; + let result_rows: Vec> = rows .iter() .map(|row| { diff --git a/src-tauri/src/db/sqlite.rs b/src-tauri/src/db/sqlite.rs index 228084008..aa4056b0a 100644 --- a/src-tauri/src/db/sqlite.rs +++ b/src-tauri/src/db/sqlite.rs @@ -55,12 +55,14 @@ pub async fn get_columns(pool: &SqlitePool, _schema: &str, table: &str) -> Resul extra: None, comment: None, numeric_precision: None, numeric_scale: None, + character_maximum_length: None, }) .collect()) } pub async fn list_indexes(pool: &SqlitePool, _schema: &str, table: &str) -> Result, String> { - let idx_rows: Vec = sqlx::query(&format!("PRAGMA index_list(\"{}\")", table)) + let safe_table = table.replace('"', "\"\""); + let idx_rows: Vec = sqlx::query(&format!("PRAGMA index_list(\"{safe_table}\")")) .fetch_all(pool) .await .map_err(|e| e.to_string())?; @@ -69,8 +71,11 @@ pub async fn list_indexes(pool: &SqlitePool, _schema: &str, table: &str) -> Resu for idx_row in &idx_rows { let name: String = idx_row.get("name"); let is_unique: bool = idx_row.get::("unique") != 0; + let origin: String = idx_row.get::("origin"); + let is_primary = origin == "pk"; - let col_rows: Vec = sqlx::query(&format!("PRAGMA index_info(\"{}\")", name)) + let safe_name = name.replace('"', "\"\""); + let col_rows: Vec = sqlx::query(&format!("PRAGMA index_info(\"{safe_name}\")")) .fetch_all(pool) .await .map_err(|e| e.to_string())?; @@ -81,7 +86,7 @@ pub async fn list_indexes(pool: &SqlitePool, _schema: &str, table: &str) -> Resu name, columns, is_unique, - is_primary: false, + is_primary, }); } Ok(indexes) diff --git a/src-tauri/src/db/sqlserver.rs b/src-tauri/src/db/sqlserver.rs index 7445bdaaa..7b6f19b41 100644 --- a/src-tauri/src/db/sqlserver.rs +++ b/src-tauri/src/db/sqlserver.rs @@ -97,7 +97,7 @@ pub async fn get_columns(client: &mut SqlServerClient, schema: &str, table: &str let sql = format!( "SELECT c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT, \ CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PK, \ - c.NUMERIC_PRECISION, c.NUMERIC_SCALE \ + c.NUMERIC_PRECISION, c.NUMERIC_SCALE, c.CHARACTER_MAXIMUM_LENGTH, c.DATETIME_PRECISION \ FROM INFORMATION_SCHEMA.COLUMNS c \ LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu \ ON c.TABLE_SCHEMA = kcu.TABLE_SCHEMA AND c.TABLE_NAME = kcu.TABLE_NAME AND c.COLUMN_NAME = kcu.COLUMN_NAME \ @@ -109,15 +109,46 @@ pub async fn get_columns(client: &mut SqlServerClient, schema: &str, table: &str let stream = client.query(&*sql, &[]).await.map_err(|e| e.to_string())?; let rows = stream.into_first_result().await.map_err(|e| e.to_string())?; Ok(rows.iter().map(|row| { + let base = row.get::<&str, _>(1).unwrap_or("").to_string(); + let max_len = row.get::(7); + let dt_prec = row.get::(8); + let num_prec = row.get::(5); + let num_scale = row.get::(6); + let data_type = match base.to_lowercase().as_str() { + "varchar" => match max_len { + Some(-1) => "varchar(max)".to_string(), + Some(n) => format!("varchar({n})"), + None => "varchar".to_string(), + }, + "nvarchar" => match max_len { + Some(-1) => "nvarchar(max)".to_string(), + Some(n) => format!("nvarchar({n})"), + None => "nvarchar".to_string(), + }, + "char" | "nchar" | "binary" | "varbinary" => match max_len { + Some(n) if n > 0 => format!("{base}({n})"), + _ => base, + } + "decimal" | "numeric" => match (num_prec, num_scale) { + (Some(p), Some(s)) => format!("{base}({p},{s})"), + _ => base, + }, + "datetime2" | "datetimeoffset" | "time" => match dt_prec { + Some(p) => format!("{base}({p})"), + _ => base, + }, + _ => base, + }; ColumnInfo { name: row.get::<&str, _>(0).unwrap_or("").to_string(), - data_type: row.get::<&str, _>(1).unwrap_or("").to_string(), + data_type, is_nullable: row.get::<&str, _>(2).unwrap_or("NO") == "YES", column_default: row.get::<&str, _>(3).map(|s| s.to_string()), is_primary_key: row.get::(4).unwrap_or(0) == 1, extra: None, comment: None, - numeric_precision: row.get::(5), - numeric_scale: row.get::(6), + numeric_precision: num_prec, + numeric_scale: num_scale, + character_maximum_length: max_len, } }).collect()) }