feat(metadata): display object comments across databases
This commit is contained in:
1 parent
ad7a66c64f
commit
bd4cfd6b99
2 files changed
+143
-2
No files matched your search
@@ -1776,8 +1776,12 @@ pub async fn export_database_sql_core(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Sort tables by foreign key dependency so referenced (parent) tables are
|
// Sort tables by foreign key dependency so referenced (parent) tables are
|
||||||
// exported before referencing (child) tables.
|
// exported before referencing (child) tables. MySQL exports always emit
|
||||||
if tables.len() > 1 {
|
// `SET FOREIGN_KEY_CHECKS = 0`, so both the generated DDL and any INSERTs
|
||||||
|
// already tolerate child-before-parent ordering; reordering away from the
|
||||||
|
// alphabetical listing there only makes the output look shuffled to users
|
||||||
|
// without providing any correctness benefit.
|
||||||
|
if tables.len() > 1 && db_type != DatabaseType::Mysql {
|
||||||
let table_names: Vec<String> = tables.iter().map(|t| t.name.clone()).collect();
|
let table_names: Vec<String> = tables.iter().map(|t| t.name.clone()).collect();
|
||||||
match crate::transfer::sort_tables_by_fk_dependency(
|
match crate::transfer::sort_tables_by_fk_dependency(
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
use dbx_core::connection::AppState;
|
||||||
|
use dbx_core::database_export::{export_database_sql_core, DatabaseExportRequest};
|
||||||
|
use dbx_core::models::connection::{ConnectionConfig, DatabaseType};
|
||||||
|
use dbx_core::query::execute_sql_statement;
|
||||||
|
use dbx_core::storage::Storage;
|
||||||
|
|
||||||
|
fn live_mysql_config(id: &str) -> ConnectionConfig {
|
||||||
|
let host = std::env::var("DBX_LIVE_SQL_FILE_MYSQL_HOST").expect("DBX_LIVE_SQL_FILE_MYSQL_HOST");
|
||||||
|
let port =
|
||||||
|
std::env::var("DBX_LIVE_SQL_FILE_MYSQL_PORT").ok().and_then(|value| value.parse::<u16>().ok()).unwrap_or(3306);
|
||||||
|
let username = std::env::var("DBX_LIVE_SQL_FILE_MYSQL_USER").expect("DBX_LIVE_SQL_FILE_MYSQL_USER");
|
||||||
|
let password = std::env::var("DBX_LIVE_SQL_FILE_MYSQL_PASSWORD").expect("DBX_LIVE_SQL_FILE_MYSQL_PASSWORD");
|
||||||
|
|
||||||
|
serde_json::from_value(serde_json::json!({
|
||||||
|
"id": id,
|
||||||
|
"name": id,
|
||||||
|
"db_type": DatabaseType::Mysql,
|
||||||
|
"host": host,
|
||||||
|
"port": port,
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
"database": null,
|
||||||
|
"connect_timeout_secs": 10,
|
||||||
|
"query_timeout_secs": 30,
|
||||||
|
"idle_timeout_secs": 60,
|
||||||
|
"keepalive_interval_secs": 0
|
||||||
|
}))
|
||||||
|
.expect("live MySQL export config should deserialize")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the order tables appear in exported DDL, by scanning for
|
||||||
|
/// `CREATE TABLE ... \`name\`` occurrences in file order.
|
||||||
|
fn exported_table_order(sql: &str, candidate_names: &[&str]) -> Vec<String> {
|
||||||
|
let mut hits: Vec<(usize, String)> = Vec::new();
|
||||||
|
for name in candidate_names {
|
||||||
|
if let Some(pos) = regex::Regex::new(&format!(r"(?is)\bCREATE\s+TABLE\s+`{}`", regex::escape(name)))
|
||||||
|
.unwrap()
|
||||||
|
.find(sql)
|
||||||
|
.map(|m| m.start())
|
||||||
|
{
|
||||||
|
hits.push((pos, name.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hits.sort_by_key(|(pos, _)| *pos);
|
||||||
|
hits.into_iter().map(|(_, name)| name).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore = "requires a disposable MySQL endpoint"]
|
||||||
|
async fn live_mysql_database_export_table_order_is_not_alphabetical_when_fk_reordered() {
|
||||||
|
let suffix = uuid::Uuid::new_v4().simple().to_string();
|
||||||
|
let connection_id = format!("live-mysql-export-order-{suffix}");
|
||||||
|
let database = format!("dbx_export_order_{suffix}");
|
||||||
|
let dir = std::env::temp_dir().join(format!("dbx-live-mysql-export-order-{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));
|
||||||
|
|
||||||
|
for sql in [
|
||||||
|
format!("DROP DATABASE IF EXISTS `{database}`"),
|
||||||
|
format!("CREATE DATABASE `{database}`"),
|
||||||
|
format!("CREATE TABLE `{database}`.`alpha` (id INT PRIMARY KEY)"),
|
||||||
|
format!("CREATE TABLE `{database}`.`bravo` (id INT PRIMARY KEY)"),
|
||||||
|
format!("CREATE TABLE `{database}`.`mango` (id INT PRIMARY KEY)"),
|
||||||
|
format!("CREATE TABLE `{database}`.`zoo` (id INT PRIMARY KEY)"),
|
||||||
|
format!(
|
||||||
|
"CREATE TABLE `{database}`.`orders` (id INT PRIMARY KEY, alpha_id INT, \
|
||||||
|
FOREIGN KEY (alpha_id) REFERENCES `{database}`.`alpha`(id))"
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"CREATE TABLE `{database}`.`order_items` (id INT PRIMARY KEY, orders_id INT, mango_id INT, \
|
||||||
|
FOREIGN KEY (orders_id) REFERENCES `{database}`.`orders`(id), \
|
||||||
|
FOREIGN KEY (mango_id) REFERENCES `{database}`.`mango`(id))"
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
execute_sql_statement(&state, &connection_id, "", &sql, None, None).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let names = ["alpha", "bravo", "mango", "order_items", "orders", "zoo"];
|
||||||
|
let alphabetical: Vec<String> = {
|
||||||
|
let mut v: Vec<String> = names.iter().map(|s| s.to_string()).collect();
|
||||||
|
v.sort();
|
||||||
|
v
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_path = dir.join("export.sql");
|
||||||
|
let export_request = DatabaseExportRequest {
|
||||||
|
export_id: format!("live-mysql-export-order-{suffix}"),
|
||||||
|
connection_id: connection_id.clone(),
|
||||||
|
database: database.clone(),
|
||||||
|
schema: database.clone(),
|
||||||
|
file_path: file_path.to_string_lossy().to_string(),
|
||||||
|
selected_tables: Vec::new(),
|
||||||
|
excluded_tables: Vec::new(),
|
||||||
|
include_structure: true,
|
||||||
|
include_data: false,
|
||||||
|
include_objects: false,
|
||||||
|
include_create_database: false,
|
||||||
|
drop_table_if_exists: false,
|
||||||
|
omit_auto_increment: false,
|
||||||
|
fail_on_error: true,
|
||||||
|
snapshot_session_id: None,
|
||||||
|
batch_size: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
let test_result = async {
|
||||||
|
export_database_sql_core(&state, &export_request, |_| {}).await?;
|
||||||
|
let exported_1 = std::fs::read_to_string(&file_path).map_err(|error| error.to_string())?;
|
||||||
|
let order_1 = exported_table_order(&exported_1, &names);
|
||||||
|
|
||||||
|
// Export a second time against the exact same, unchanged schema.
|
||||||
|
export_database_sql_core(&state, &export_request, |_| {}).await?;
|
||||||
|
let exported_2 = std::fs::read_to_string(&file_path).map_err(|error| error.to_string())?;
|
||||||
|
let order_2 = exported_table_order(&exported_2, &names);
|
||||||
|
|
||||||
|
Ok::<_, String>((order_1, order_2))
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let cleanup =
|
||||||
|
execute_sql_statement(&state, &connection_id, "", &format!("DROP DATABASE `{database}`"), None, None).await;
|
||||||
|
cleanup.unwrap();
|
||||||
|
std::fs::remove_dir_all(dir).unwrap();
|
||||||
|
|
||||||
|
let (order_1, order_2) = test_result.unwrap();
|
||||||
|
eprintln!("run 1 order: {order_1:?}");
|
||||||
|
eprintln!("run 2 order: {order_2:?}");
|
||||||
|
eprintln!("alphabetical (expected by user): {alphabetical:?}");
|
||||||
|
|
||||||
|
assert_eq!(order_1, order_2, "export order should at least be stable across repeated runs of the same schema");
|
||||||
|
assert_eq!(
|
||||||
|
order_1, alphabetical,
|
||||||
|
"exported table structure order should match the alphabetical order tables are shown in, \
|
||||||
|
not the FK-dependency topological order"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user