feat(mysql): edit table auto-increment counter

Closes #7065
This commit is contained in:
t8y2 committed 2026-08-25 01:24:09 +00:00
1 parent 44dc2a88f8
commit 277386ec81
13 files changed
+513 -32

No files matched your search

+76 -1
View File
@@ -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 {
+15 -12
View File
@@ -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),
+32
View File
@@ -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,