import fs from "fs"; const MYSQL_INDEX_PREFIX_LENGTH = 191; const MYSQL_TABLE_OPTIONS = "ENGINE = InnoDB ROW_FORMAT = DYNAMIC"; /** * ## sqlite与postgres不同点 * 1. * sqlite: AUTOINCREAMENT * postgresql: GENERATED BY DEFAULT AS IDENTITY * * 2. * sqlite: datetime * postgresql: timestamp * * 3. * sqlite: update sqlite_sequence set seq = 1000 where name = 'sys_role' ; * postgresql: select setval('sys_role_id_seq', 1000); * * 4. * sqlite: "disabled" boolean DEFAULT (0) * postgresql: "disabled" boolean DEFAULT (false) * * 5. * sqlite: last_insert_rowid() * postgresql: LASTVAL() * * 6. * sqlite: integer * postgresql: bigint */ function transformPG() { // 读取文件列表 const sqliteFiles = fs.readdirSync("./migration/"); const pgFiles = fs.readdirSync("./migration-pg/"); //找出pg里面没有的文件 const notFiles = sqliteFiles.filter(file => !pgFiles.includes(file)); for (const notFile of notFiles) { //开始转换 const sqliteSql = fs.readFileSync(`./migration/${notFile}`, "utf-8"); let pgSql = sqliteSql.replaceAll(/AUTOINCREMENT/g, "GENERATED BY DEFAULT AS IDENTITY"); pgSql = pgSql.replaceAll(/datetime/g, "timestamp"); pgSql = pgSql.replaceAll(/boolean DEFAULT \(0\)/g, "boolean DEFAULT (false)"); pgSql = pgSql.replaceAll(/boolean DEFAULT \(1\)/g, "boolean DEFAULT (true)"); pgSql = pgSql.replaceAll(/boolean.*NOT NULL DEFAULT \(0\)/g, "boolean NOT NULL DEFAULT (false)"); pgSql = pgSql.replaceAll(/boolean.*NOT NULL DEFAULT \(1\)/g, "boolean NOT NULL DEFAULT (true)"); pgSql = pgSql.replaceAll(/integer/g, "bigint"); pgSql = pgSql.replaceAll(/INTEGER/g, "bigint"); pgSql = pgSql.replaceAll(/last_insert_rowid\(\)/g, "LASTVAL()"); fs.writeFileSync(`./migration-pg/${notFile}`, pgSql); } if (notFiles.length > 0) { console.log("sqlite->pg 转换完成"); throw new Error("sqlite->pg 转换完成,有更新,需要测试pg"); } else { console.log("sql无需更新"); } } function buildMysqlTableColumnMap(sql) { const tableColumnMap = new Map(); const createTableReg = /CREATE TABLE `([^`]*)`[\s\S]*?;/gi; for (const match of sql.matchAll(createTableReg)) { const [statement, tableName] = match; const tableBody = getCreateTableBody(statement); const columnMap = new Map(); const columnReg = /`([^`]*)`\s+varchar\((\d+)\)/gi; for (const columnMatch of tableBody.matchAll(columnReg)) { const [, columnName, length] = columnMatch; columnMap.set(columnName, Number(length)); } tableColumnMap.set(tableName, columnMap); } const alterAddColumnReg = /ALTER TABLE\s+`?([^`\s]+)`?\s+ADD COLUMN\s+`?([^`\s]+)`?\s+varchar\((\d+)\)/gi; for (const match of sql.matchAll(alterAddColumnReg)) { const [, tableName, columnName, length] = match; const columnMap = tableColumnMap.get(tableName) || new Map(); columnMap.set(columnName, Number(length)); tableColumnMap.set(tableName, columnMap); } return tableColumnMap; } function getCreateTableBody(statement) { const start = statement.indexOf("("); const end = statement.lastIndexOf(")"); if (start === -1 || end === -1 || end <= start) { return ""; } return statement.substring(start + 1, end); } function appendMysqlTableOptions(sql) { const createTableReg = /CREATE TABLE `([^`]*)`[\s\S]*?;/gi; return sql.replace(createTableReg, statement => { const sqlWithoutSemicolon = statement.replace(/;\s*$/, ""); const sqlWithoutOptions = sqlWithoutSemicolon.replace(/\s+ENGINE\s*=\s*\w+(?:\s+ROW_FORMAT\s*=\s*\w+)?\s*$/i, ""); return `${sqlWithoutOptions} ${MYSQL_TABLE_OPTIONS};`; }); } function addMysqlIndexPrefix(sql) { const tableColumnMap = buildMysqlTableColumnMap(sql); const createIndexReg = /CREATE\s+(UNIQUE\s+)?INDEX\s+`([^`]*)`\s+ON\s+`([^`]*)`\s*\(([^;]*)\);/gi; return sql.replace(createIndexReg, (statement, uniqueKeyword, indexName, tableName, columns) => { const columnMap = tableColumnMap.get(tableName); if (!columnMap) { return statement; } const parsedColumns = columns.split(",").map(item => { const columnMatch = item.trim().match(/^`([^`]*)`(?:\((\d+)\))?$/); if (!columnMatch) { return item.trim(); } const [, columnName, prefixLength] = columnMatch; const columnLength = columnMap.get(columnName); if (!columnLength || columnLength <= MYSQL_INDEX_PREFIX_LENGTH) { return item.trim(); } if (prefixLength && Number(prefixLength) <= MYSQL_INDEX_PREFIX_LENGTH) { return item.trim(); } // MySQL 5.7 老配置下 utf8mb4 varchar(255) 完整索引可能超过 767/1000 字节限制。 if (uniqueKeyword) { throw new Error(`唯一索引 ${indexName} 的字段 ${tableName}.${columnName} 长度超过 ${MYSQL_INDEX_PREFIX_LENGTH},不能自动改成前缀索引,请缩短字段长度或增加 hash 字段`); } return `\`${columnName}\`(${MYSQL_INDEX_PREFIX_LENGTH})`; }); const unique = uniqueKeyword || ""; return `CREATE ${unique}INDEX \`${indexName}\` ON \`${tableName}\` (${parsedColumns.join(", ")});`; }); } function transformMysql() { // 读取文件列表 const sqliteFiles = fs.readdirSync("./migration/"); const pgFiles = fs.readdirSync("./migration-mysql/"); //找出pg里面没有的文件 const notFiles = sqliteFiles.filter(file => !pgFiles.includes(file)); for (const notFile of notFiles) { //开始转换 const sqliteSql = fs.readFileSync(`./migration/${notFile}`, "utf-8"); let pgSql = sqliteSql.replaceAll(/AUTOINCREMENT/g, "AUTO_INCREMENT"); pgSql = pgSql.replaceAll(/datetime/g, "timestamp"); //DEFAULT (xxx) 替换成 DEFAULT xxx pgSql = pgSql.replaceAll(/DEFAULT \(([^)]*)\)/g, "DEFAULT $1"); pgSql = pgSql.replaceAll(/integer/g, "bigint"); pgSql = pgSql.replaceAll(/INTEGER/g, "bigint"); pgSql = pgSql.replaceAll(/last_insert_rowid\(\)/g, "LAST_INSERT_ID()"); //text 改成longtext pgSql = pgSql.replaceAll(/text/g, "longtext"); //双引号 替换成反引号 pgSql = pgSql.replaceAll(/"/g, "`"); pgSql = appendMysqlTableOptions(pgSql); pgSql = addMysqlIndexPrefix(pgSql); fs.writeFileSync(`./migration-mysql/${notFile}`, pgSql); } if (notFiles.length > 0) { console.log("sqlite->mysql 转换完成"); throw new Error("sqlite->mysql 转换完成,有更新,需要测试mysql"); } else { console.log("sql无需更新"); } } transformPG(); transformMysql();