mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
fix(mysql): disable session stats cache for fresh row counts
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
//! Live regression coverage for the MySQL object-browser row counts: MySQL 8
|
||||
//! caches `information_schema.TABLES` statistics per session for
|
||||
//! `information_schema_stats_expiry` seconds (86400 by default), so a table
|
||||
//! that was read while still empty kept reporting `TABLE_ROWS = 0` long after
|
||||
//! rows were inserted, and the sidebar/对象浏览器 showed the stale count
|
||||
//! (#9736). The statistics path must clear that cache before reading.
|
||||
|
||||
use dbx_core::connection::AppState;
|
||||
use dbx_core::models::connection::{ConnectionConfig, DatabaseType};
|
||||
use dbx_core::query::execute_sql_statement;
|
||||
use dbx_core::storage::Storage;
|
||||
|
||||
fn live_mysql_config(connection_id: &str, database: &str) -> ConnectionConfig {
|
||||
let host = std::env::var("DBX_LIVE_MYSQL_HOST").expect("DBX_LIVE_MYSQL_HOST");
|
||||
let port: u16 =
|
||||
std::env::var("DBX_LIVE_MYSQL_PORT").expect("DBX_LIVE_MYSQL_PORT").parse().expect("valid DBX_LIVE_MYSQL_PORT");
|
||||
let username = std::env::var("DBX_LIVE_MYSQL_USER").expect("DBX_LIVE_MYSQL_USER");
|
||||
let password = std::env::var("DBX_LIVE_MYSQL_PASSWORD").expect("DBX_LIVE_MYSQL_PASSWORD");
|
||||
|
||||
serde_json::from_value::<ConnectionConfig>(serde_json::json!({
|
||||
"id": connection_id,
|
||||
"name": connection_id,
|
||||
"db_type": DatabaseType::Mysql,
|
||||
"host": host,
|
||||
"port": port,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"database": database,
|
||||
"connect_timeout_secs": 15,
|
||||
"query_timeout_secs": 30,
|
||||
"idle_timeout_secs": 60,
|
||||
"keepalive_interval_secs": 0
|
||||
}))
|
||||
.expect("live MySQL config should deserialize")
|
||||
}
|
||||
|
||||
async fn estimated_rows_for(state: &AppState, connection_id: &str, database: &str, table: &str) -> Option<i64> {
|
||||
let statistics = dbx_core::schema::list_object_statistics_core(state, connection_id, database, "")
|
||||
.await
|
||||
.expect("object statistics for a live MySQL schema");
|
||||
statistics.iter().find(|entry| entry.name == table).and_then(|entry| entry.estimated_rows)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires DBX_LIVE_MYSQL_HOST/PORT/USER/PASSWORD/DATABASE pointing at a writable MySQL 8 server"]
|
||||
async fn live_mysql_object_statistics_refresh_after_writes() {
|
||||
let database = std::env::var("DBX_LIVE_MYSQL_DATABASE").expect("DBX_LIVE_MYSQL_DATABASE");
|
||||
let suffix = uuid::Uuid::new_v4().simple().to_string();
|
||||
let connection_id = format!("live-mysql-object-statistics-{suffix}");
|
||||
let table = format!("dbx_stats_fresh_{}", &suffix[..12]);
|
||||
let dir = std::env::temp_dir().join(format!("dbx-live-mysql-object-statistics-{suffix}"));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let storage = Storage::open(&dir.join("storage.db")).await.unwrap();
|
||||
let state = AppState::new(storage);
|
||||
state.configs.write().await.insert(connection_id.clone(), live_mysql_config(&connection_id, &database));
|
||||
|
||||
execute_sql_statement(&state, &connection_id, &database, &format!("DROP TABLE IF EXISTS `{table}`"), None, None)
|
||||
.await
|
||||
.expect("clean the live statistics fixture table");
|
||||
execute_sql_statement(
|
||||
&state,
|
||||
&connection_id,
|
||||
&database,
|
||||
&format!("CREATE TABLE `{table}` (id INT PRIMARY KEY AUTO_INCREMENT, label VARCHAR(32) NOT NULL)"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("create the live statistics fixture table");
|
||||
|
||||
// The desktop app reads the table list statistics right after opening a
|
||||
// database, which is what fills MySQL's session-scoped statistics cache.
|
||||
assert_eq!(
|
||||
estimated_rows_for(&state, &connection_id, &database, &table).await,
|
||||
Some(0),
|
||||
"an empty table reports zero rows before the writes"
|
||||
);
|
||||
|
||||
let values = (1..=7).map(|id| format!("({id}, 'row-{id}')")).collect::<Vec<_>>().join(", ");
|
||||
execute_sql_statement(
|
||||
&state,
|
||||
&connection_id,
|
||||
&database,
|
||||
&format!("INSERT INTO `{table}` (id, label) VALUES {values}"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("insert the live statistics rows");
|
||||
|
||||
let refreshed = estimated_rows_for(&state, &connection_id, &database, &table).await;
|
||||
let cleanup = execute_sql_statement(
|
||||
&state,
|
||||
&connection_id,
|
||||
&database,
|
||||
&format!("DROP TABLE IF EXISTS `{table}`"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(refreshed, Some(7), "the cached row count must be refreshed after the inserts");
|
||||
cleanup.expect("drop the live statistics fixture table");
|
||||
}
|
||||
@@ -3107,6 +3107,17 @@ pub(super) struct TableStatusMeta {
|
||||
|
||||
const MYSQL_FRESH_TABLE_STATUS_SESSION_SQL: &str = "/*!80000 SET SESSION information_schema_stats_expiry = 0 */";
|
||||
|
||||
/// MySQL 8 caches `information_schema.TABLES` statistics per session for
|
||||
/// `information_schema_stats_expiry` seconds (86400 by default), so a table
|
||||
/// that was read while still empty keeps reporting its old `TABLE_ROWS`
|
||||
/// estimate long after rows were inserted (#9736). The version comment turns
|
||||
/// the directive into a no-op on MySQL 5.7.
|
||||
async fn enable_fresh_table_statistics(conn: &mut mysql_async::Conn, context: &str) {
|
||||
if let Err(error) = conn.query_drop(MYSQL_FRESH_TABLE_STATUS_SESSION_SQL).await {
|
||||
log::debug!("Failed to disable cached MySQL table statistics before {context}: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_table_status_show(pool: &MySqlPool, database: &str) -> Result<HashMap<String, TableStatusMeta>, String> {
|
||||
query_table_status_show(pool, database, None).await
|
||||
}
|
||||
@@ -3172,9 +3183,7 @@ pub async fn get_table_auto_increment(pool: &MySqlPool, database: &str, table: &
|
||||
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}");
|
||||
}
|
||||
enable_fresh_table_statistics(&mut conn, "reading AUTO_INCREMENT").await;
|
||||
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))
|
||||
}
|
||||
@@ -3814,6 +3823,9 @@ pub async fn list_object_statistics(pool: &MySqlPool, database: &str) -> Result<
|
||||
quote_value(database),
|
||||
);
|
||||
let mut conn = get_conn_with_timeout(pool, super::connection_timeout()).await?;
|
||||
// The session-scoped statistics cache is what makes the tree keep showing
|
||||
// a stale row count for tables written after the first read (#9736).
|
||||
enable_fresh_table_statistics(&mut conn, "reading object statistics").await;
|
||||
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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Reference in New Issue
Block a user