1 parent
44dc2a88f8
commit
277386ec81
13 files changed
+513
-32
No files matched your search
@@ -320,6 +320,19 @@ fn get_opt_metadata_string(row: &mysql_async::Row, name: &str) -> Option<String>
|
||||
.or_else(|| row_get::<NaiveTime, _>(row, name).map(|value| value.to_string()))
|
||||
}
|
||||
|
||||
fn get_opt_unsigned_metadata_string(row: &mysql_async::Row, name: &str) -> Option<String> {
|
||||
row_get::<u64, _>(row, name)
|
||||
.map(|value| value.to_string())
|
||||
.or_else(|| {
|
||||
row_get::<i64, _>(row, name).and_then(|value| u64::try_from(value).ok()).map(|value| value.to_string())
|
||||
})
|
||||
.or_else(|| {
|
||||
get_opt_str(row, name)
|
||||
.filter(|value| !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
|
||||
.and_then(|value| value.parse::<u64>().ok().map(|parsed| parsed.to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
fn numeric_metadata_u64_to_i32(value: Option<u64>) -> Option<i32> {
|
||||
value.and_then(|v| i32::try_from(v).ok())
|
||||
}
|
||||
@@ -2842,8 +2855,11 @@ pub(super) struct TableStatusMeta {
|
||||
comment: Option<String>,
|
||||
created_at: Option<String>,
|
||||
updated_at: Option<String>,
|
||||
auto_increment: Option<String>,
|
||||
}
|
||||
|
||||
const MYSQL_FRESH_TABLE_STATUS_SESSION_SQL: &str = "/*!80000 SET SESSION information_schema_stats_expiry = 0 */";
|
||||
|
||||
async fn list_table_status_show(pool: &MySqlPool, database: &str) -> Result<HashMap<String, TableStatusMeta>, String> {
|
||||
query_table_status_show(pool, database, None).await
|
||||
}
|
||||
@@ -2872,8 +2888,19 @@ async fn query_table_status_show(
|
||||
filter: Option<&str>,
|
||||
) -> Result<HashMap<String, TableStatusMeta>, String> {
|
||||
let sql = show_table_status_sql(database, filter);
|
||||
query_table_status_sql(pool, &sql).await
|
||||
}
|
||||
|
||||
async fn query_table_status_sql(pool: &MySqlPool, sql: &str) -> Result<HashMap<String, TableStatusMeta>, String> {
|
||||
let mut conn = get_conn_with_timeout(pool, super::connection_timeout()).await?;
|
||||
let result = conn.query_iter(&sql).await.map_err(|e| e.to_string())?;
|
||||
query_table_status_sql_with_conn(&mut conn, sql).await
|
||||
}
|
||||
|
||||
async fn query_table_status_sql_with_conn(
|
||||
conn: &mut mysql_async::Conn,
|
||||
sql: &str,
|
||||
) -> Result<HashMap<String, TableStatusMeta>, String> {
|
||||
let result = conn.query_iter(sql).await.map_err(|e| e.to_string())?;
|
||||
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|e| e.to_string())?;
|
||||
Ok(rows
|
||||
.iter()
|
||||
@@ -2886,6 +2913,7 @@ async fn query_table_status_show(
|
||||
.filter(|s| !s.is_empty()),
|
||||
created_at: get_opt_metadata_string(row, "Create_time"),
|
||||
updated_at: get_opt_metadata_string(row, "Update_time"),
|
||||
auto_increment: get_opt_unsigned_metadata_string(row, "Auto_increment"),
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -2893,6 +2921,17 @@ async fn query_table_status_show(
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_table_auto_increment(pool: &MySqlPool, database: &str, table: &str) -> Result<Option<String>, String> {
|
||||
let mut conn = get_conn_with_timeout(pool, super::connection_timeout()).await?;
|
||||
// MySQL 8 caches SHOW TABLE STATUS statistics by default, including the
|
||||
// counter after ALTER TABLE. The version comment is a no-op on MySQL 5.7.
|
||||
if let Err(error) = conn.query_drop(MYSQL_FRESH_TABLE_STATUS_SESSION_SQL).await {
|
||||
log::debug!("Failed to disable cached MySQL table statistics before reading AUTO_INCREMENT: {error}");
|
||||
}
|
||||
let status = query_table_status_sql_with_conn(&mut conn, &show_table_status_exact_sql(database, table)).await?;
|
||||
Ok(status.into_values().next().and_then(|meta| meta.auto_increment))
|
||||
}
|
||||
|
||||
fn filter_table_status_fallback(
|
||||
status: HashMap<String, TableStatusMeta>,
|
||||
filter: Option<&str>,
|
||||
@@ -3067,6 +3106,15 @@ fn show_table_status_sql(database: &str, filter: Option<&str>) -> String {
|
||||
sql
|
||||
}
|
||||
|
||||
fn show_table_status_exact_sql(database: &str, table: &str) -> String {
|
||||
let prefix = if database.trim().is_empty() {
|
||||
"SHOW TABLE STATUS".to_string()
|
||||
} else {
|
||||
format!("SHOW TABLE STATUS FROM {}", quote_identifier(database))
|
||||
};
|
||||
format!("{prefix} WHERE Name = {}", quote_value(table))
|
||||
}
|
||||
|
||||
fn show_tables_filter_conditions(database: &str, filter: Option<&str>, exact_names: &[String]) -> Vec<String> {
|
||||
if database.trim().is_empty() {
|
||||
// Catalogless services do not expose a stable Tables_in_<db> column name.
|
||||
@@ -6506,6 +6554,33 @@ mod tests {
|
||||
assert_eq!(filtered.keys().map(String::as_str).collect::<Vec<_>>(), vec!["orders"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_table_status_auto_increment_is_lossless_and_nullable() {
|
||||
let auto_increment_column = Column::new(ColumnType::MYSQL_TYPE_LONGLONG).with_name(b"Auto_increment");
|
||||
let maximum = mysql_test_row_with_columns(vec![Value::UInt(u64::MAX)], vec![auto_increment_column.clone()]);
|
||||
let null = mysql_test_row_with_columns(vec![Value::NULL], vec![auto_increment_column]);
|
||||
|
||||
assert_eq!(
|
||||
get_opt_unsigned_metadata_string(&maximum, "Auto_increment").as_deref(),
|
||||
Some("18446744073709551615")
|
||||
);
|
||||
assert_eq!(get_opt_unsigned_metadata_string(&null, "Auto_increment"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_exact_table_status_sql_quotes_identifiers_and_names() {
|
||||
assert_eq!(
|
||||
show_table_status_exact_sql("sales`archive", "order's"),
|
||||
"SHOW TABLE STATUS FROM `sales``archive` WHERE Name = 'order\\'s'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_table_status_refresh_directive_is_version_gated() {
|
||||
assert!(MYSQL_FRESH_TABLE_STATUS_SESSION_SQL.starts_with("/*!80000 "));
|
||||
assert!(MYSQL_FRESH_TABLE_STATUS_SESSION_SQL.contains("information_schema_stats_expiry = 0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_list_tables_sql_applies_table_name_filter_before_pagination() {
|
||||
let filter = TableNameFilter {
|
||||
|
||||
@@ -1761,19 +1761,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn builds_mysql_auto_increment_sql_and_rejects_invalid_values() {
|
||||
let options = MysqlAutoIncrementSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
driver_profile: Some("mysql".to_string()),
|
||||
schema: Some("sales`archive".to_string()),
|
||||
table_name: "order`items".to_string(),
|
||||
value: "18446744073709551615".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
build_mysql_auto_increment_sql(options),
|
||||
Ok("ALTER TABLE `sales``archive`.`order``items` AUTO_INCREMENT = 18446744073709551615;".to_string())
|
||||
);
|
||||
for value in ["1", "9007199254740993", "18446744073709551615"] {
|
||||
assert_eq!(
|
||||
build_mysql_auto_increment_sql(MysqlAutoIncrementSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
driver_profile: Some("mysql".to_string()),
|
||||
schema: Some("sales`archive".to_string()),
|
||||
table_name: "order`items".to_string(),
|
||||
value: value.to_string(),
|
||||
}),
|
||||
Ok(format!("ALTER TABLE `sales``archive`.`order``items` AUTO_INCREMENT = {value};"))
|
||||
);
|
||||
}
|
||||
|
||||
for value in ["", "0", "01", " 1", "1 ", "+1", "-1", "1; DROP TABLE users", "18446744073709551616"] {
|
||||
for value in
|
||||
["", "0", "01", " 1", "1 ", "+1", "-1", "1.0", "1e3", "1; DROP TABLE users", "18446744073709551616"]
|
||||
{
|
||||
assert!(
|
||||
build_mysql_auto_increment_sql(MysqlAutoIncrementSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
|
||||
@@ -1162,6 +1162,38 @@ pub async fn get_table_comment_core(
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn get_mysql_table_auto_increment_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
database: &str,
|
||||
table: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
let db_config = connection_config(state, connection_id).await;
|
||||
let native_mysql = db_config.as_ref().is_some_and(|config| {
|
||||
config.db_type == DatabaseType::Mysql
|
||||
&& config
|
||||
.driver_profile
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_none_or(|profile| profile.is_empty() || profile.eq_ignore_ascii_case("mysql"))
|
||||
});
|
||||
if !native_mysql {
|
||||
return Err("AUTO_INCREMENT metadata is supported only for native MySQL connections.".to_string());
|
||||
}
|
||||
|
||||
retry_metadata_connection_for_session(state, connection_id, Some(database), None, || async {
|
||||
let pool_key = state.get_or_create_metadata_pool_for_session(connection_id, Some(database), None).await?;
|
||||
let pool = clone_metadata_pool(state, &pool_key).await.ok_or("Pool not found")?;
|
||||
match &pool {
|
||||
PoolKind::Mysql(pool, mode) if *mode != MysqlMode::OceanBaseOracle => {
|
||||
db::mysql::get_table_auto_increment(pool, database, table).await
|
||||
}
|
||||
_ => Err("AUTO_INCREMENT metadata is supported only for native MySQL connections.".to_string()),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_table_comment_core_for_session(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
|
||||
Reference in new issue
Block a user