#!/usr/bin/env bash # ws_usernode SQLite → MySQL 数据迁移脚本(PLAN 验收:SQLite 数据可迁移至 MySQL)。 # # 原理:SQLite 导出 SQL(含表结构与数据)→ 简单改写为 MySQL 兼容语法 → 导入 MySQL。 # 适用:从开发库(SQLite)迁移到生产库(MySQL)。 # # 用法: # MYSQL_DSN='usernode:pass@tcp(127.0.0.1:3306)/usernode?charset=utf8mb4&parseTime=True&loc=UTC' \ # ./deploy/migrate-sqlite2mysql.sh /path/to/usernode.db # # 前置: # - sqlite3、mysql 客户端已安装(Debian/Ubuntu: apt install sqlite3 default-mysql-client) # - MySQL 中已建库、建好应用账号(建议先用 usernode migrate --config 指向 MySQL 建表, # 再执行本脚本导数据;若表不存在,本脚本也可建表) # - 先停服务,避免迁移过程中写入 # # 注意事项: # - 时间字段:SQLite 存的是 "2026-08-30 10:41:41.437309782+08:00" 这类文本, # MySQL DATETIME 不接受。本脚本用字符串函数提取前 19 位(秒级精度)。 # 若你的库以 UTC 存储且无需时区换算,秒级精度足够(审计/会话/OTP 均为秒级业务)。 # - BLOB/二进制字段:本表结构无二进制大字段,不处理。 # - 迁移后建议校验行数并抽查,见脚本尾部。 set -euo pipefail if [ $# -lt 1 ]; then echo "用法: $0 [MYSQL_DSN 或通过环境变量 MYSQL_DSN]" echo "示例: MYSQL_DSN='usernode:pass@tcp(127.0.0.1:3306)/usernode?charset=utf8mb4&parseTime=True&loc=UTC' $0 data/usernode.db" exit 1 fi SQLITE_DB="$1" MYSQL_DSN="${MYSQL_DSN:-}" if [ -z "$MYSQL_DSN" ]; then echo "错误:未提供 MYSQL_DSN(环境变量或第二参数)" >&2 exit 1 fi command -v sqlite3 >/dev/null || { echo "缺少 sqlite3" >&2; exit 1; } command -v mysql >/dev/null || { echo "缺少 mysql 客户端" >&2; exit 1; } TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT SQL_DUMP="$TMP/dump.sql" echo "==> 1/4 导出 SQLite 结构 + 数据: $SQLITE_DB" sqlite3 "$SQLITE_DB" .dump > "$SQL_DUMP" # 排除 SQLite 内部表(sqlite_sequence 等) echo "==> 2/4 改写为 MySQL 兼容 SQL" awk ' /^CREATE TABLE/ { table=1 } /^CREATE TABLE sqlite_sequence/ { skip=1; next } /^CREATE TABLE/ && $3 ~ /^sqlite_/ { skip=1; next } /^CREATE INDEX/ && $0 ~ /sqlite_autoindex/ { next } # 建表语句改写: # "text" → TEXT;AUTOINCREMENT 去掉(MySQL 自增) # PRIMARY KEY (id) 保留;去掉多余引号 /^CREATE TABLE/ { line = $0 gsub(/"text"/, "TEXT", line) gsub(/AUTOINCREMENT/, "", line) # 去掉反引号内外的双引号(表名/列名) line = line print line next } # 行数据:SQLite 的 INSERT INTO "table" VALUES(...) → MySQL 双引号仅用于字符串,表名不需要引号 /^INSERT INTO/ { line = $0 gsub(/INSERT INTO "/, "INSERT INTO `", line) gsub(/"\s*VALUES/, "` VALUES", line) # SQLite 布尔/整数原样;字符串内单引号已由 sqlite3 转义为 '',MySQL 兼容 print line next } { print } ' "$SQL_DUMP" > "$TMP/mysql.sql" echo "==> 3/4 导入 MySQL" # mysql 客户端 DSN 形式为 mysql://user:pass@tcp(host:port)/db 或 user:pass@tcp(...)/db if [[ "$MYSQL_DSN" == mysql://* ]]; then MYSQL_DSN="${MYSQL_DSN#mysql://}" fi mysql --protocol=tcp "$MYSQL_DSN" < "$TMP/mysql.sql" echo "==> 4/4 校验行数" for t in admin_users users ssh_keys approvals audit_logs sessions settings mail_logs otp_codes password_reset_tokens; do s=$(sqlite3 "$SQLITE_DB" "SELECT count(*) FROM $t;" 2>/dev/null || echo "?") m=$(mysql --protocol=tcp --batch --skip-column-names "$MYSQL_DSN" -e "SELECT count(*) FROM \`$t\`;" 2>/dev/null || echo "?") printf " %-24s sqlite=%-6s mysql=%s\n" "$t" "$s" "$m" done echo echo "迁移完成。建议:" echo " 1. 抽查关键表(users/sessions)内容与时间字段格式" echo " 2. 用指向 MySQL 的配置启动 usernode serve 验证" echo " 3. 确认无误后再处理旧 SQLite 库"