use std::collections::HashSet;
use torm::db::db_types::SqlValue;
use torm::db::database::{Database, DbError};
#[derive(Debug, Clone)]
struct Config {
sqlite_file: String,
m_host: String,
m_port: u16,
m_db: String,
m_user: String,
m_pass: String,
tables: Option<Vec<String>>, batch_size: usize, create_only: bool, data_only: bool, }
impl Default for Config {
fn default() -> Self {
Self {
sqlite_file: "data.db".to_string(),
m_host: "localhost".to_string(),
m_port: 3306,
m_db: "mydb".to_string(),
m_user: "root".to_string(),
m_pass: "".to_string(),
tables: None,
batch_size: 1000,
create_only: false,
data_only: false,
}
}
}
#[derive(Debug, Clone)]
struct SqliteColumn {
name: String,
decl_type: String, is_not_null: bool,
default_value: Option<String>,
is_pk: bool, is_rowid: bool, }
impl SqliteColumn {
fn is_auto_increment(&self) -> bool {
self.is_rowid
}
}
fn parse_args() -> Config {
let mut cfg = Config::default();
let mut seen: HashSet<String> = HashSet::new();
let args: Vec<String> = std::env::args().skip(1).collect();
if args.is_empty() {
println!("{}", USAGE);
std::process::exit(0);
}
let mut i = 0;
while i < args.len() {
let flag = args[i].clone();
let take = |idx: &mut usize| -> Option<String> {
if *idx + 1 < args.len() {
*idx += 1;
Some(args[*idx].clone())
} else {
None
}
};
match flag.as_str() {
"--mhost" => { seen.insert("mhost".into()); if let Some(v) = take(&mut i) { cfg.m_host = v; } },
"--mport" => { seen.insert("mport".into()); if let Some(v) = take(&mut i) { cfg.m_port = v.parse().unwrap_or(cfg.m_port); } },
"--mdb" => { seen.insert("mdb".into()); if let Some(v) = take(&mut i) { cfg.m_db = v; } },
"--muser" => { seen.insert("muser".into()); if let Some(v) = take(&mut i) { cfg.m_user = v; } },
"--mpass" => { seen.insert("mpass".into()); if let Some(v) = take(&mut i) { cfg.m_pass = v; } },
"--tables" => if let Some(v) = take(&mut i) {
cfg.tables = Some(v.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect());
},
"--batch" => if let Some(v) = take(&mut i) { cfg.batch_size = v.parse().unwrap_or(cfg.batch_size); },
"--create-only" => cfg.create_only = true,
"--data-only" => cfg.data_only = true,
"--help" | "-h" => {
println!("{}", USAGE);
std::process::exit(0);
}
_ if !flag.starts_with('-') && !seen.contains("sqlite_file") => {
seen.insert("sqlite_file".into());
cfg.sqlite_file = flag;
}
_ => {
eprintln!("未知参数: {}", flag);
eprintln!("{}", USAGE);
std::process::exit(1);
}
}
i += 1;
}
let mut missing: Vec<&str> = Vec::new();
if !seen.contains("sqlite_file") {
missing.push("sqlite_file");
}
for k in ["mhost", "mport", "mdb", "muser"] {
if !seen.contains(k) {
missing.push(k);
}
}
if !missing.is_empty() {
eprintln!("缺少必要参数: {}", missing.join(", "));
eprintln!("{}", USAGE);
std::process::exit(1);
}
cfg
}
const USAGE: &str = r#"
用法:
sqlite2mysql <sqlite_file>
[--mhost <host> --mport <port> --mdb <db> --muser <user> --mpass <pass>]
[--tables t1,t2] [--batch 1000] [--create-only] [--data-only]
选项:
<sqlite_file> 源 SQLite 数据库文件路径(必填)
--mhost/--mport/--mdb/--muser/--mpass MySQL 目标库连接(缺省用默认配置)
--tables <a,b,c> 只迁移指定表(默认全部)
--batch <n> 每批迁移行数(默认 1000)
--create-only 只创建表结构,不迁移数据
--data-only 只迁移数据,跳过建表
"#;
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
let cfg = parse_args();
println!(
"SQLite file: {} → MySQL {}:{} @ {}",
cfg.sqlite_file, cfg.m_host, cfg.m_port, cfg.m_db
);
println!("连接 SQLite...");
let sqlite = Database::sqlite(&cfg.sqlite_file).await?;
sqlite.ping().await?;
println!(" ✅ SQLite 连接成功 ({})", sqlite.db_type());
println!("连接 MySQL...");
let mysql = Database::mysql(&cfg.m_host, cfg.m_port, &cfg.m_db, &cfg.m_user, &cfg.m_pass).await?;
mysql.ping().await?;
println!(" ✅ MySQL 连接成功 ({})", mysql.db_type());
let tables = resolve_tables(&sqlite, &cfg).await?;
println!("\n待迁移表 ({}): {:?}\n", tables.len(), tables);
for table in &tables {
let columns = fetch_columns(&sqlite, table).await?;
if columns.is_empty() {
eprintln!(" ⚠️ 表 {} 无列定义,跳过", table);
continue;
}
if !cfg.data_only {
match create_mysql_table(&mysql, table, &columns).await {
Ok(_) => println!(" ✅ 已创建表结构: {}", table),
Err(e) => {
eprintln!(" ❌ 建表失败 {}: {}", table, e);
continue;
}
}
}
if cfg.create_only {
continue;
}
let migrated = migrate_table(&sqlite, &mysql, table, &columns, cfg.batch_size).await?;
println!(" ✅ 完成迁移 {}:{} 行", table, migrated);
}
sqlite.close().await?;
mysql.close().await?;
println!("\n🎉 迁移完成!");
Ok(())
}
async fn resolve_tables(sqlite: &Database, cfg: &Config) -> std::result::Result<Vec<String>, DbError> {
if let Some(tables) = &cfg.tables {
return Ok(tables.clone());
}
let result = sqlite
.query("SELECT name AS table_name FROM sqlite_master WHERE type = 'table' ORDER BY name", &[])
.await?;
let mut tables = Vec::new();
for row in &result.rows {
if let Some(SqlValue::String(name)) = row.get("table_name") {
if !name.starts_with("sqlite_") {
tables.push(name.clone());
}
}
}
Ok(tables)
}
async fn fetch_columns(sqlite: &Database, table: &str) -> std::result::Result<Vec<SqliteColumn>, DbError> {
let pragma_sql = format!("PRAGMA table_info(`{}`)", table);
let result = sqlite.query(&pragma_sql, &[]).await?;
let mut columns = Vec::new();
for row in &result.rows {
let get = |k: &str| -> Option<String> {
row.get(k).and_then(|v| v.as_str()).map(|s| s.to_string())
};
let name = get("name").unwrap_or_default();
let decl_type = get("type").unwrap_or_default();
let is_pk = row
.get("pk")
.and_then(|v| v.as_i64())
.map(|v| v > 0)
.unwrap_or(false);
let not_null = row
.get("notnull")
.and_then(|v| v.as_i64())
.map(|v| v == 1)
.unwrap_or(false);
let decl_upper = decl_type.to_ascii_uppercase();
let is_rowid = is_pk && decl_upper.contains("INT");
columns.push(SqliteColumn {
name,
decl_type,
is_not_null: not_null,
default_value: get("dflt_value"),
is_pk,
is_rowid,
});
}
Ok(columns)
}
async fn create_mysql_table(
mysql: &Database,
table: &str,
columns: &[SqliteColumn],
) -> std::result::Result<(), DbError> {
let _ = mysql.execute(&format!("DROP TABLE IF EXISTS `{}`", table), &[]).await;
let mut defs: Vec<String> = Vec::new();
let mut primary_keys: Vec<String> = Vec::new();
for col in columns {
let mut def = format!(" `{}` {}", col.name, sqlite_type_to_mysql(&col.decl_type));
if col.is_auto_increment() {
def = format!(" `{}` {} NOT NULL AUTO_INCREMENT", col.name, sqlite_type_to_mysql(&col.decl_type));
if col.is_pk {
def.push_str(" PRIMARY KEY");
}
defs.push(def);
continue;
}
if col.is_not_null {
def.push_str(" NOT NULL");
}
if let Some(default) = normalize_default(&col.default_value) {
def.push_str(&format!(" DEFAULT {}", default));
}
if col.is_pk {
primary_keys.push(format!("`{}`", col.name));
}
defs.push(def);
}
if !primary_keys.is_empty() {
defs.push(format!(" PRIMARY KEY ({})", primary_keys.join(", ")));
}
let sql = format!(
"CREATE TABLE IF NOT EXISTS `{}` (\n{}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
table,
defs.join(",\n")
);
mysql.execute(&sql, &[]).await?;
Ok(())
}
async fn migrate_table(
sqlite: &Database,
mysql: &Database,
table: &str,
columns: &[SqliteColumn],
batch_size: usize,
) -> std::result::Result<u64, DbError> {
let mysql_cols: Vec<String> = columns.iter().map(|c| format!("`{}`", c.name)).collect();
let placeholders: Vec<&str> = vec!["?"; columns.len()];
let insert_sql = format!(
"INSERT INTO `{}` ({}) VALUES ({})",
table,
mysql_cols.join(", "),
placeholders.join(", ")
);
let sqlite_cols = cols_sqlite(columns);
let order_by: String = if let Some(pk) = columns.iter().find(|c| c.is_pk) {
format!("`{}`", pk.name)
} else {
sqlite_cols.clone()
};
let select_sql = format!("SELECT {} FROM `{}` ORDER BY {}", sqlite_cols, table, order_by);
let mut offset: i64 = 0;
let mut total: u64 = 0;
loop {
let page_sql = format!("{} LIMIT {} OFFSET {}", select_sql, batch_size, offset);
let result = sqlite.query(&page_sql, &[]).await?;
if result.rows.is_empty() {
break;
}
let mut tx = mysql.begin_transaction().await?;
for row in &result.rows {
let mut params: Vec<SqlValue> = Vec::with_capacity(columns.len());
for col in columns {
params.push(row.get(&col.name).cloned().unwrap_or(SqlValue::Null));
}
tx.execute(&insert_sql, ¶ms).await?;
}
tx.commit().await?;
total += result.rows.len() as u64;
println!(" · {}: 已迁移 {} 行", table, total);
offset += result.rows.len() as i64;
}
Ok(total)
}
fn cols_sqlite(columns: &[SqliteColumn]) -> String {
columns
.iter()
.map(|c| format!("`{}`", c.name))
.collect::<Vec<_>>()
.join(", ")
}
fn sqlite_type_to_mysql(decl_type: &str) -> &'static str {
let t = decl_type.trim().to_ascii_uppercase();
if t.contains("INT") {
"BIGINT" } else if t.contains("CHAR") || t.contains("CLOB") || t.contains("TEXT") {
"TEXT"
} else if t.contains("BLOB") || t.is_empty() {
"LONGBLOB" } else if t.contains("REAL") || t.contains("FLOA") || t.contains("DOUB") {
"DOUBLE"
} else if t.contains("NUMERIC") || t.contains("DECIMAL") {
"DECIMAL(20,10)"
} else if t.contains("DATE") {
"DATE"
} else if t.contains("TIME") {
"DATETIME"
} else if t.contains("BOOL") {
"TINYINT(1)"
} else {
"TEXT" }
}
fn normalize_default(default: &Option<String>) -> Option<String> {
let d = default.as_ref()?.trim();
if d.is_empty() {
return None;
}
let lower = d.to_ascii_lowercase();
if lower == "current_timestamp"
|| lower == "current_timestamp()"
|| lower == "datetime('now')"
|| lower == "strftime('%s','now')"
{
return Some("CURRENT_TIMESTAMP".to_string());
}
if lower == "current_date" || lower == "current_date()" {
return Some("CURRENT_DATE".to_string());
}
if lower == "current_time" || lower == "current_time()" {
return Some("CURRENT_TIME".to_string());
}
if lower == "true" {
return Some("1".to_string());
}
if lower == "false" {
return Some("0".to_string());
}
if d.eq_ignore_ascii_case("null") {
return Some("NULL".to_string());
}
if d.parse::<f64>().is_ok() {
return Some(d.to_string());
}
let clean = d.trim_matches(|c| c == '\'' || c == '"');
Some(format!("'{}'", clean))
}