fix: improve column type precision and SQL correctness across all databases
- PostgreSQL: rewrite column query from information_schema to pg_attribute for accurate type info via format_type(atttypid, atttypmod); extract character_maximum_length from atttypmod for varchar/bpchar; fix execute_query with persistent(false) and empty result fallback - SQL Server: reconstruct column types with precision (varchar(max), decimal(p,s), datetime2(p)); stop dividing nvarchar/nchar CHARACTER_MAXIMUM_LENGTH by 2 - MySQL: use COLUMN_TYPE instead of DATA_TYPE for full type definitions; add CHARACTER_MAXIMUM_LENGTH; fix ONLY_FULL_GROUP_BY with MIN(NON_UNIQUE) - Oracle: add CHAR_LENGTH/DATA_LENGTH for column type precision; fix INDEX_OWNER join to prevent cartesian product - SQLite: fix PRAGMA injection via double-quote escaping; detect primary key indexes via origin column
This commit is contained in:
parent
26429aa5d6
commit
b88e1b8ccb
|
|
@ -51,6 +51,7 @@ fn duckdb_query_columns(con: &duckdb::Connection, table: &str) -> Result<Vec<db:
|
|||
extra: None, comment: None,
|
||||
numeric_precision: None,
|
||||
numeric_scale: None,
|
||||
character_maximum_length: None,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?;
|
||||
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||
|
|
|
|||
|
|
@ -442,6 +442,7 @@ async fn get_columns_for_transfer(
|
|||
comment: None,
|
||||
numeric_precision: None,
|
||||
numeric_scale: None,
|
||||
character_maximum_length: None,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?;
|
||||
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ pub async fn get_columns(client: &ChClient, database: &str, table: &str) -> Resu
|
|||
extra: None, comment: None,
|
||||
numeric_precision: None,
|
||||
numeric_scale: None,
|
||||
character_maximum_length: None,
|
||||
}
|
||||
}).collect())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ pub struct ColumnInfo {
|
|||
pub comment: Option<String>,
|
||||
pub numeric_precision: Option<i32>,
|
||||
pub numeric_scale: Option<i32>,
|
||||
pub character_maximum_length: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -215,9 +215,9 @@ pub async fn get_columns(
|
|||
table: &str,
|
||||
) -> Result<Vec<ColumnInfo>, 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::<i32, _>("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<Qu
|
|||
pub async fn list_indexes(pool: &MySqlPool, database: &str, table: &str) -> Result<Vec<IndexInfo>, 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),
|
||||
|
|
|
|||
|
|
@ -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}' \
|
||||
|
|
|
|||
|
|
@ -154,21 +154,29 @@ pub async fn get_columns(
|
|||
table: &str,
|
||||
) -> Result<Vec<ColumnInfo>, String> {
|
||||
let rows: Vec<PgRow> = 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::<String, _>("column_name"),
|
||||
data_type: row.get::<String, _>("data_type"),
|
||||
is_nullable: row.get::<String, _>("is_nullable") == "YES",
|
||||
column_default: row.get::<Option<String>, _>("column_default"),
|
||||
is_primary_key: row.get::<bool, _>("is_pk"),
|
||||
extra: None,
|
||||
comment: row.get::<Option<String>, _>("column_comment"),
|
||||
numeric_precision: row.get::<Option<i32>, _>("numeric_precision"),
|
||||
numeric_scale: row.get::<Option<i32>, _>("numeric_scale"),
|
||||
.map(|row| {
|
||||
let full_type = row.get::<Option<String>, _>("full_type").unwrap_or_default();
|
||||
ColumnInfo {
|
||||
name: row.get::<String, _>("column_name"),
|
||||
data_type: full_type,
|
||||
is_nullable: row.get::<bool, _>("is_nullable"),
|
||||
column_default: row.get::<Option<String>, _>("column_default"),
|
||||
is_primary_key: row.get::<bool, _>("is_pk"),
|
||||
extra: None,
|
||||
comment: row.get::<Option<String>, _>("column_comment"),
|
||||
numeric_precision: row.get::<Option<i32>, _>("numeric_precision"),
|
||||
numeric_scale: row.get::<Option<i32>, _>("numeric_scale"),
|
||||
character_maximum_length: row.get::<Option<i32>, _>("character_maximum_length"),
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
|
@ -202,19 +214,26 @@ pub async fn execute_query(pool: &PgPool, sql: &str) -> Result<QueryResult, Stri
|
|||
|| trimmed.starts_with("WITH")
|
||||
|| trimmed.starts_with("TABLE")
|
||||
{
|
||||
let desc = pool.describe(sql).await.map_err(|e| e.to_string())?;
|
||||
let columns: Vec<String> = desc.columns().iter().map(|c| c.name().to_string()).collect();
|
||||
let column_types: Vec<String> = desc
|
||||
.columns()
|
||||
.iter()
|
||||
.map(|c| c.type_info().name().to_string())
|
||||
.collect();
|
||||
|
||||
let rows: Vec<PgRow> = sqlx::query(sql)
|
||||
.persistent(false)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let (columns, column_types): (Vec<String>, Vec<String>) = 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<Vec<serde_json::Value>> = rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
|
|
|
|||
|
|
@ -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<Vec<IndexInfo>, String> {
|
||||
let idx_rows: Vec<SqliteRow> = sqlx::query(&format!("PRAGMA index_list(\"{}\")", table))
|
||||
let safe_table = table.replace('"', "\"\"");
|
||||
let idx_rows: Vec<SqliteRow> = 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::<i32, _>("unique") != 0;
|
||||
let origin: String = idx_row.get::<String, _>("origin");
|
||||
let is_primary = origin == "pk";
|
||||
|
||||
let col_rows: Vec<SqliteRow> = sqlx::query(&format!("PRAGMA index_info(\"{}\")", name))
|
||||
let safe_name = name.replace('"', "\"\"");
|
||||
let col_rows: Vec<SqliteRow> = 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)
|
||||
|
|
|
|||
|
|
@ -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::<i32, _>(7);
|
||||
let dt_prec = row.get::<i32, _>(8);
|
||||
let num_prec = row.get::<i32, _>(5);
|
||||
let num_scale = row.get::<i32, _>(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::<i32, _>(4).unwrap_or(0) == 1,
|
||||
extra: None, comment: None,
|
||||
numeric_precision: row.get::<i32, _>(5),
|
||||
numeric_scale: row.get::<i32, _>(6),
|
||||
numeric_precision: num_prec,
|
||||
numeric_scale: num_scale,
|
||||
character_maximum_length: max_len,
|
||||
}
|
||||
}).collect())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue