mm_mysql 2.2.7 → 2.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backup/test_mysql_backup1.sql +6 -0
- package/backup/test_mysql_backup2.sql +13 -0
- package/backup/test_mysql_backup_with_comments.sql +13 -0
- package/package.json +1 -1
- package/sql.js +437 -0
- package/test_backup.js +101 -0
- package/test_backup_with_comments.js +97 -0
- package/test_parser.js +76 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- 备份表: test_table
|
|
2
|
+
-- 备份时间: 2025-12-25T16:36:48.450Z
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS `test_table` (
|
|
5
|
+
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
6
|
+
`name` varchar(50) NOT NULL,
|
|
7
|
+
`age` int(11) DEFAULT NULL,
|
|
8
|
+
PRIMARY KEY (`id`)
|
|
9
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
10
|
+
|
|
11
|
+
INSERT INTO `test_table` (`id`, `name`, `age`) VALUES (1, '张三', 25);
|
|
12
|
+
INSERT INTO `test_table` (`id`, `name`, `age`) VALUES (2, '李四', 30);
|
|
13
|
+
INSERT INTO `test_table` (`id`, `name`, `age`) VALUES (3, '王五', 28);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- 备份表: test_table
|
|
2
|
+
-- 备份时间: 2025-12-25T16:34:30.386Z
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS `test_table` (
|
|
5
|
+
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
|
6
|
+
`name` varchar(50) NOT NULL COMMENT '用户姓名',
|
|
7
|
+
`age` int(11) DEFAULT NULL COMMENT '用户年龄',
|
|
8
|
+
PRIMARY KEY (`id`)
|
|
9
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测试用户表';
|
|
10
|
+
|
|
11
|
+
INSERT INTO `test_table` (`id`, `name`, `age`) VALUES (1, '张三', 25);
|
|
12
|
+
INSERT INTO `test_table` (`id`, `name`, `age`) VALUES (2, '李四', 30);
|
|
13
|
+
INSERT INTO `test_table` (`id`, `name`, `age`) VALUES (3, '王五', 28);
|
package/package.json
CHANGED
package/sql.js
CHANGED
|
@@ -1666,6 +1666,443 @@ Sql.prototype.addOrSet = async function (where, set, like) {
|
|
|
1666
1666
|
return 0;
|
|
1667
1667
|
};
|
|
1668
1668
|
|
|
1669
|
+
/**
|
|
1670
|
+
* @description 获取创建表的 SQL 语句
|
|
1671
|
+
* @returns {Promise<string>} 创建表的 SQL 语句
|
|
1672
|
+
* @private
|
|
1673
|
+
*/
|
|
1674
|
+
Sql.prototype._getCreateTableSql = async function() {
|
|
1675
|
+
try {
|
|
1676
|
+
// 查询表结构信息(MySQL 使用 SHOW CREATE TABLE)
|
|
1677
|
+
const sql = `SHOW CREATE TABLE \`${this.table}\``;
|
|
1678
|
+
const result = await this.run(sql);
|
|
1679
|
+
|
|
1680
|
+
if (result && result.length > 0 && result[0]['Create Table']) {
|
|
1681
|
+
// 在创建表语句中添加 IF NOT EXISTS 条件
|
|
1682
|
+
let create_sql = result[0]['Create Table'];
|
|
1683
|
+
if (create_sql.toUpperCase().startsWith('CREATE TABLE')) {
|
|
1684
|
+
create_sql = create_sql.replace('CREATE TABLE', 'CREATE TABLE IF NOT EXISTS');
|
|
1685
|
+
}
|
|
1686
|
+
return create_sql + ';';
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
return '';
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
this.log('warn', '获取表结构失败,将仅备份数据', err);
|
|
1692
|
+
return '';
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* @description 获取表的注释信息
|
|
1698
|
+
* @returns {Promise<string>} 注释信息 SQL 语句
|
|
1699
|
+
* @private
|
|
1700
|
+
*/
|
|
1701
|
+
Sql.prototype._getTableComments = async function() {
|
|
1702
|
+
try {
|
|
1703
|
+
let commentSql = '';
|
|
1704
|
+
|
|
1705
|
+
// 获取表注释
|
|
1706
|
+
const tableCommentSql = `
|
|
1707
|
+
SELECT TABLE_COMMENT
|
|
1708
|
+
FROM information_schema.TABLES
|
|
1709
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
1710
|
+
AND TABLE_NAME = '${this.table}'
|
|
1711
|
+
`;
|
|
1712
|
+
const tableCommentResult = await this.run(tableCommentSql);
|
|
1713
|
+
|
|
1714
|
+
if (tableCommentResult && tableCommentResult.length > 0 && tableCommentResult[0].TABLE_COMMENT) {
|
|
1715
|
+
const tableComment = tableCommentResult[0].TABLE_COMMENT;
|
|
1716
|
+
if (tableComment && tableComment.trim() !== '') {
|
|
1717
|
+
commentSql += `\n-- 表注释: ${tableComment}\n`;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// 获取列注释
|
|
1722
|
+
const columnCommentSql = `
|
|
1723
|
+
SELECT COLUMN_NAME, COLUMN_COMMENT
|
|
1724
|
+
FROM information_schema.COLUMNS
|
|
1725
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
1726
|
+
AND TABLE_NAME = '${this.table}'
|
|
1727
|
+
AND COLUMN_COMMENT IS NOT NULL
|
|
1728
|
+
AND COLUMN_COMMENT != ''
|
|
1729
|
+
ORDER BY ORDINAL_POSITION
|
|
1730
|
+
`;
|
|
1731
|
+
const columnCommentResult = await this.run(columnCommentSql);
|
|
1732
|
+
|
|
1733
|
+
if (columnCommentResult && columnCommentResult.length > 0) {
|
|
1734
|
+
commentSql += '\n-- 列注释:\n';
|
|
1735
|
+
for (const column of columnCommentResult) {
|
|
1736
|
+
commentSql += `-- ${column.COLUMN_NAME}: ${column.COLUMN_COMMENT}\n`;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
return commentSql;
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
this.log('warn', '获取表注释信息失败,将仅备份表结构', err);
|
|
1743
|
+
return '';
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
|
|
1747
|
+
/**
|
|
1748
|
+
* @description 备份数据库到 SQL 文件
|
|
1749
|
+
* @param {string} file 备份文件路径
|
|
1750
|
+
* @param {boolean} create 是否导出创建表语句,默认为false
|
|
1751
|
+
* @returns {Promise<boolean>} 备份是否成功
|
|
1752
|
+
*/
|
|
1753
|
+
Sql.prototype.backup = async function(file, create = false) {
|
|
1754
|
+
if (!file || typeof file !== 'string') {
|
|
1755
|
+
throw new TypeError('文件路径必须为字符串');
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (!this.table) {
|
|
1759
|
+
throw new TypeError('表名未设置');
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
try {
|
|
1763
|
+
// 获取所有数据
|
|
1764
|
+
const data = await this.getSql('', '', '*');
|
|
1765
|
+
|
|
1766
|
+
if (!data || !Array.isArray(data)) {
|
|
1767
|
+
throw new Error('获取数据失败');
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// 构建 SQL 语句
|
|
1771
|
+
let sqlContent = '';
|
|
1772
|
+
|
|
1773
|
+
// 添加表结构(简化版,实际应用中可能需要更复杂的表结构导出)
|
|
1774
|
+
sqlContent += `-- 备份表: ${this.table}\n`;
|
|
1775
|
+
sqlContent += `-- 备份时间: ${new Date().toISOString()}\n\n`;
|
|
1776
|
+
|
|
1777
|
+
// 如果create为true,添加创建表语句
|
|
1778
|
+
if (create) {
|
|
1779
|
+
const createTableSql = await this._getCreateTableSql();
|
|
1780
|
+
if (createTableSql) {
|
|
1781
|
+
sqlContent += createTableSql + '\n\n';
|
|
1782
|
+
|
|
1783
|
+
// 添加表注释信息
|
|
1784
|
+
const commentSql = await this._getTableComments();
|
|
1785
|
+
if (commentSql) {
|
|
1786
|
+
sqlContent += commentSql + '\n';
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// 添加数据插入语句
|
|
1792
|
+
for (const row of data) {
|
|
1793
|
+
const keys = Object.keys(row).map(key => this.escapeId(key)).join(', ');
|
|
1794
|
+
const values = Object.values(row).map(value => this.escape(value)).join(', ');
|
|
1795
|
+
sqlContent += `INSERT INTO \`${this.table}\` (${keys}) VALUES (${values});\n`;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// 写入文件(这里需要文件系统模块,实际使用时需要引入 fs)
|
|
1799
|
+
const fs = require('fs');
|
|
1800
|
+
const path = require('path');
|
|
1801
|
+
|
|
1802
|
+
// 确保目录存在
|
|
1803
|
+
const dir = path.dirname(file);
|
|
1804
|
+
if (!fs.existsSync(dir)) {
|
|
1805
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
fs.writeFileSync(file, sql_content, 'utf8');
|
|
1809
|
+
|
|
1810
|
+
return true;
|
|
1811
|
+
} catch (err) {
|
|
1812
|
+
this.error = err.message;
|
|
1813
|
+
this.log('error', '备份数据库失败', err);
|
|
1814
|
+
return false;
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* @description 从 SQL 文件恢复数据库
|
|
1820
|
+
* @param {string} file 备份文件路径
|
|
1821
|
+
* @returns {Promise<boolean>} 恢复是否成功
|
|
1822
|
+
*/
|
|
1823
|
+
Sql.prototype.restore = async function(file) {
|
|
1824
|
+
if (!file || typeof file !== 'string') {
|
|
1825
|
+
throw new TypeError('文件路径必须为字符串');
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
if (!this.table) {
|
|
1829
|
+
throw new TypeError('表名未设置');
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
try {
|
|
1833
|
+
const fs = require('fs');
|
|
1834
|
+
|
|
1835
|
+
if (!fs.existsSync(file)) {
|
|
1836
|
+
throw new Error('备份文件不存在');
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// 读取 SQL 文件
|
|
1840
|
+
const sqlContent = fs.readFileSync(file, 'utf8');
|
|
1841
|
+
|
|
1842
|
+
// 解析 SQL 语句
|
|
1843
|
+
const statements = this._parseSqlStatements(sqlContent);
|
|
1844
|
+
|
|
1845
|
+
// 执行每个 SQL 语句
|
|
1846
|
+
for (const stmt of statements) {
|
|
1847
|
+
if (stmt.trim() && !stmt.startsWith('--')) {
|
|
1848
|
+
await this.exec(stmt);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
return true;
|
|
1853
|
+
} catch (err) {
|
|
1854
|
+
this.error = err.message;
|
|
1855
|
+
this.log('error', '恢复数据库失败', err);
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* @description 解析 SQL 语句
|
|
1862
|
+
* @param {string} sql SQL 内容
|
|
1863
|
+
* @returns {string[]} SQL 语句数组
|
|
1864
|
+
* @private
|
|
1865
|
+
*/
|
|
1866
|
+
Sql.prototype._parseSqlStatements = function(sql) {
|
|
1867
|
+
const stmts = [];
|
|
1868
|
+
let cur_stmt = '';
|
|
1869
|
+
let in_str = false;
|
|
1870
|
+
let in_cmt = false;
|
|
1871
|
+
let quote_char = '';
|
|
1872
|
+
|
|
1873
|
+
for (let i = 0; i < sql.length; i++) {
|
|
1874
|
+
const char = sql[i];
|
|
1875
|
+
const next_char = sql[i + 1] || '';
|
|
1876
|
+
|
|
1877
|
+
// 处理字符串
|
|
1878
|
+
if (!in_cmt && (char === "'" || char === '"') && (i === 0 || sql[i - 1] !== '\\')) {
|
|
1879
|
+
if (!in_str) {
|
|
1880
|
+
in_str = true;
|
|
1881
|
+
quote_char = char;
|
|
1882
|
+
} else if (char === quote_char) {
|
|
1883
|
+
in_str = false;
|
|
1884
|
+
quote_char = '';
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// 处理单行注释
|
|
1889
|
+
if (!in_str && !in_cmt && char === '-' && next_char === '-') {
|
|
1890
|
+
in_cmt = true;
|
|
1891
|
+
i++; // 跳过第二个'-'
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
if (in_cmt && char === '\n') {
|
|
1896
|
+
in_cmt = false;
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// 处理多行注释开始
|
|
1901
|
+
if (!in_str && !in_cmt && char === '/' && next_char === '*') {
|
|
1902
|
+
in_cmt = true;
|
|
1903
|
+
i++; // 跳过'*'
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// 处理多行注释结束
|
|
1908
|
+
if (in_cmt && char === '*' && next_char === '/') {
|
|
1909
|
+
in_cmt = false;
|
|
1910
|
+
i++; // 跳过'/'字符
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// 跳过注释内容
|
|
1915
|
+
if (in_cmt) {
|
|
1916
|
+
continue;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// 分割语句
|
|
1920
|
+
if (!in_str && !in_cmt && char === ';') {
|
|
1921
|
+
if (cur_stmt.trim()) {
|
|
1922
|
+
stmts.push(cur_stmt.trim());
|
|
1923
|
+
}
|
|
1924
|
+
cur_stmt = '';
|
|
1925
|
+
} else {
|
|
1926
|
+
cur_stmt += char;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// 添加最后一个语句(如果有)
|
|
1931
|
+
if (cur_stmt.trim()) {
|
|
1932
|
+
stmts.push(cur_stmt.trim());
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
return stmts;
|
|
1936
|
+
};
|
|
1937
|
+
|
|
1938
|
+
/**
|
|
1939
|
+
* @description 备份数据库到 SQL 文件
|
|
1940
|
+
* @param {string} file 备份文件路径
|
|
1941
|
+
* @param {boolean} create 是否导出创建表语句,默认为false
|
|
1942
|
+
* @returns {Promise<boolean>} 备份是否成功
|
|
1943
|
+
*/
|
|
1944
|
+
Sql.prototype.backup = async function(file, create = false) {
|
|
1945
|
+
if (!file || typeof file !== 'string') {
|
|
1946
|
+
throw new TypeError('文件路径必须为字符串');
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
if (!this.table) {
|
|
1950
|
+
throw new TypeError('表名未设置');
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
try {
|
|
1954
|
+
// 获取所有数据
|
|
1955
|
+
const data = await this.getSql('', '', '*');
|
|
1956
|
+
|
|
1957
|
+
if (!data || !Array.isArray(data)) {
|
|
1958
|
+
throw new Error('获取数据失败');
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// 构建 SQL 语句
|
|
1962
|
+
let sql_content = '';
|
|
1963
|
+
|
|
1964
|
+
// 添加表结构(简化版,实际应用中可能需要更复杂的表结构导出)
|
|
1965
|
+
sql_content += `-- 备份表: ${this.table}\n`;
|
|
1966
|
+
sql_content += `-- 备份时间: ${new Date().toISOString()}\n\n`;
|
|
1967
|
+
|
|
1968
|
+
// 如果create为true,添加创建表语句
|
|
1969
|
+
if (create) {
|
|
1970
|
+
const create_table_sql = await this._getCreateTableSql();
|
|
1971
|
+
if (create_table_sql) {
|
|
1972
|
+
sql_content += create_table_sql + '\n\n';
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// 添加数据插入语句
|
|
1977
|
+
for (const row of data) {
|
|
1978
|
+
const keys = Object.keys(row).map(key => this.escapeId(key)).join(', ');
|
|
1979
|
+
const values = Object.values(row).map(value => this.escape(value)).join(', ');
|
|
1980
|
+
sql_content += `INSERT INTO \`${this.table}\` (${keys}) VALUES (${values});\n`;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// 写入文件(这里需要文件系统模块,实际使用时需要引入 fs)
|
|
1984
|
+
const fs = require('fs');
|
|
1985
|
+
const path = require('path');
|
|
1986
|
+
|
|
1987
|
+
// 确保目录存在
|
|
1988
|
+
const dir = path.dirname(file);
|
|
1989
|
+
if (!fs.existsSync(dir)) {
|
|
1990
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
fs.writeFileSync(file, sql_content, 'utf8');
|
|
1994
|
+
|
|
1995
|
+
return true;
|
|
1996
|
+
} catch (err) {
|
|
1997
|
+
this.error = err.message;
|
|
1998
|
+
this.log('error', '备份数据库失败', err);
|
|
1999
|
+
return false;
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
/**
|
|
2004
|
+
* @description 从 SQL 文件恢复数据库
|
|
2005
|
+
* @param {string} file 恢复文件路径
|
|
2006
|
+
* @returns {Promise<boolean>} 恢复是否成功
|
|
2007
|
+
*/
|
|
2008
|
+
Sql.prototype.restore = async function(file) {
|
|
2009
|
+
if (!file || typeof file !== 'string') {
|
|
2010
|
+
throw new TypeError('文件路径必须为字符串');
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
if (!this.table) {
|
|
2014
|
+
throw new TypeError('表名未设置');
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
try {
|
|
2018
|
+
const fs = require('fs');
|
|
2019
|
+
|
|
2020
|
+
// 检查文件是否存在
|
|
2021
|
+
if (!fs.existsSync(file)) {
|
|
2022
|
+
throw new Error('备份文件不存在');
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// 读取文件内容
|
|
2026
|
+
const sql_content = fs.readFileSync(file, 'utf8');
|
|
2027
|
+
|
|
2028
|
+
if (!sql_content) {
|
|
2029
|
+
throw new Error('备份文件为空');
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// 解析 SQL 语句
|
|
2033
|
+
const sql_stmts = this._parseSqlStatements(sql_content);
|
|
2034
|
+
|
|
2035
|
+
if (!sql_stmts || sql_stmts.length === 0) {
|
|
2036
|
+
throw new Error('未找到有效的 SQL 语句');
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// 开始事务
|
|
2040
|
+
await this.exec('START TRANSACTION');
|
|
2041
|
+
|
|
2042
|
+
try {
|
|
2043
|
+
// 逐个执行SQL语句
|
|
2044
|
+
for (const sql of sql_stmts) {
|
|
2045
|
+
const trim_sql = sql.trim();
|
|
2046
|
+
if (trim_sql) {
|
|
2047
|
+
await this.exec(trim_sql);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
// 提交事务
|
|
2052
|
+
await this.exec('COMMIT');
|
|
2053
|
+
|
|
2054
|
+
if (this.config && this.config.debug) {
|
|
2055
|
+
this.log('info', '数据库恢复成功', {
|
|
2056
|
+
file: file,
|
|
2057
|
+
stmt_num: sql_stmts.length
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
return true;
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
// 回滚事务
|
|
2064
|
+
await this.exec('ROLLBACK').catch(rollback_err => {
|
|
2065
|
+
this.log('error', '事务回滚失败', rollback_err);
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
// 记录错误日志
|
|
2069
|
+
this.log('error', 'SQL语句执行失败', err);
|
|
2070
|
+
throw err;
|
|
2071
|
+
}
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
this.error = err.message;
|
|
2074
|
+
this.log('error', '数据库恢复失败', err);
|
|
2075
|
+
return false;
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
|
|
2079
|
+
/**
|
|
2080
|
+
* @description 获取创建表的 SQL 语句
|
|
2081
|
+
* @returns {Promise<string>} 创建表的 SQL 语句
|
|
2082
|
+
* @private
|
|
2083
|
+
*/
|
|
2084
|
+
Sql.prototype._getCreateTableSql = async function() {
|
|
2085
|
+
try {
|
|
2086
|
+
// 查询表结构信息(MySQL 使用 SHOW CREATE TABLE)
|
|
2087
|
+
const sql = `SHOW CREATE TABLE \`${this.table}\``;
|
|
2088
|
+
const result = await this.run(sql);
|
|
2089
|
+
|
|
2090
|
+
if (result && result.length > 0 && result[0]['Create Table']) {
|
|
2091
|
+
// 在创建表语句中添加 IF NOT EXISTS 条件
|
|
2092
|
+
let create_sql = result[0]['Create Table'];
|
|
2093
|
+
if (create_sql.toUpperCase().startsWith('CREATE TABLE')) {
|
|
2094
|
+
create_sql = create_sql.replace('CREATE TABLE', 'CREATE TABLE IF NOT EXISTS');
|
|
2095
|
+
}
|
|
2096
|
+
return create_sql + ';';
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
return '';
|
|
2100
|
+
} catch (err) {
|
|
2101
|
+
this.log('warn', '获取表结构失败,将仅备份数据', err);
|
|
2102
|
+
return '';
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
|
|
1669
2106
|
module.exports = {
|
|
1670
2107
|
Sql
|
|
1671
2108
|
};
|
package/test_backup.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileOverview 备份恢复功能测试脚本
|
|
3
|
+
* @version 1.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {Sql} = require('./sql');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// 模拟数据库操作函数
|
|
10
|
+
async function mockRun(sql) {
|
|
11
|
+
console.log('执行查询SQL:', sql);
|
|
12
|
+
|
|
13
|
+
// 模拟返回数据
|
|
14
|
+
if (sql.includes('SELECT')) {
|
|
15
|
+
return [
|
|
16
|
+
{ id: 1, name: '张三', age: 25 },
|
|
17
|
+
{ id: 2, name: '李四', age: 30 },
|
|
18
|
+
{ id: 3, name: '王五', age: 28 }
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (sql.includes('count')) {
|
|
23
|
+
return [{ count: 3 }];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 模拟SHOW CREATE TABLE查询
|
|
27
|
+
if (sql.includes('SHOW CREATE TABLE')) {
|
|
28
|
+
return [{
|
|
29
|
+
'Create Table': 'CREATE TABLE `test_table` (\n `id` int(11) NOT NULL AUTO_INCREMENT,\n `name` varchar(50) NOT NULL,\n `age` int(11) DEFAULT NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
|
30
|
+
}];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function mockExec(sql) {
|
|
37
|
+
console.log('执行更新SQL:', sql);
|
|
38
|
+
return { affectedRows: 1, insertId: 1 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function testBackupRestore() {
|
|
42
|
+
console.log('=== 测试MySQL备份恢复功能 ===');
|
|
43
|
+
|
|
44
|
+
// 创建 SQL 实例
|
|
45
|
+
const sql = new Sql(mockRun, mockExec);
|
|
46
|
+
sql.table = 'test_table';
|
|
47
|
+
|
|
48
|
+
const backupFile1 = './backup/test_mysql_backup1.sql';
|
|
49
|
+
const backupFile2 = './backup/test_mysql_backup2.sql';
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// 测试不带创建表语句的备份
|
|
53
|
+
console.log('\n1. 测试不带创建表语句的备份...');
|
|
54
|
+
const backupResult1 = await sql.backup(backupFile1);
|
|
55
|
+
console.log('备份结果:', backupResult1 ? '成功' : '失败');
|
|
56
|
+
|
|
57
|
+
if (backupResult1) {
|
|
58
|
+
// 读取备份文件内容
|
|
59
|
+
const fs = require('fs');
|
|
60
|
+
if (fs.existsSync(backupFile1)) {
|
|
61
|
+
const content = fs.readFileSync(backupFile1, 'utf8');
|
|
62
|
+
console.log('备份文件内容:');
|
|
63
|
+
console.log(content);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 测试带创建表语句的备份
|
|
68
|
+
console.log('\n2. 测试带创建表语句的备份...');
|
|
69
|
+
const backupResult2 = await sql.backup(backupFile2, true);
|
|
70
|
+
console.log('备份结果:', backupResult2 ? '成功' : '失败');
|
|
71
|
+
|
|
72
|
+
if (backupResult2) {
|
|
73
|
+
// 读取备份文件内容
|
|
74
|
+
const fs = require('fs');
|
|
75
|
+
if (fs.existsSync(backupFile2)) {
|
|
76
|
+
const content = fs.readFileSync(backupFile2, 'utf8');
|
|
77
|
+
console.log('备份文件内容:');
|
|
78
|
+
console.log(content);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 测试恢复
|
|
83
|
+
console.log('\n3. 测试恢复功能...');
|
|
84
|
+
const restoreResult = await sql.restore(backupFile2);
|
|
85
|
+
console.log('恢复结果:', restoreResult ? '成功' : '失败');
|
|
86
|
+
|
|
87
|
+
console.log('\n=== 测试完成 ===');
|
|
88
|
+
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('测试出错:', error.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 运行测试
|
|
95
|
+
if (require.main === module) {
|
|
96
|
+
testBackupRestore();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
testBackupRestore
|
|
101
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const { Sql } = require('./sql');
|
|
2
|
+
|
|
3
|
+
// 创建模拟的数据库操作函数,包含注释信息
|
|
4
|
+
const mockRun = function(sql) {
|
|
5
|
+
console.log('执行查询SQL:', sql);
|
|
6
|
+
|
|
7
|
+
// 模拟查询结果
|
|
8
|
+
if (sql.includes('SHOW CREATE TABLE')) {
|
|
9
|
+
return [{
|
|
10
|
+
'Create Table': `CREATE TABLE \`test_table\` (
|
|
11
|
+
\`id\` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
|
12
|
+
\`name\` varchar(50) NOT NULL COMMENT '用户姓名',
|
|
13
|
+
\`age\` int(11) DEFAULT NULL COMMENT '用户年龄',
|
|
14
|
+
PRIMARY KEY (\`id\`)
|
|
15
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测试用户表'`
|
|
16
|
+
}];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 模拟表注释查询
|
|
20
|
+
if (sql.includes('information_schema.TABLES')) {
|
|
21
|
+
return [{
|
|
22
|
+
TABLE_COMMENT: '测试用户表'
|
|
23
|
+
}];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 模拟列注释查询
|
|
27
|
+
if (sql.includes('information_schema.COLUMNS')) {
|
|
28
|
+
return [
|
|
29
|
+
{ COLUMN_NAME: 'id', COLUMN_COMMENT: '主键ID' },
|
|
30
|
+
{ COLUMN_NAME: 'name', COLUMN_COMMENT: '用户姓名' },
|
|
31
|
+
{ COLUMN_NAME: 'age', COLUMN_COMMENT: '用户年龄' }
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 模拟数据查询
|
|
36
|
+
if (sql.includes('SELECT * FROM')) {
|
|
37
|
+
return [
|
|
38
|
+
{ id: 1, name: '张三', age: 25 },
|
|
39
|
+
{ id: 2, name: '李四', age: 30 },
|
|
40
|
+
{ id: 3, name: '王五', age: 28 }
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const mockExec = function(sql) {
|
|
48
|
+
console.log('执行更新SQL:', sql);
|
|
49
|
+
return { affectedRows: 1 };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
async function testBackupWithComments() {
|
|
53
|
+
console.log('=== 测试MySQL备份恢复功能(包含注释) ===');
|
|
54
|
+
|
|
55
|
+
// 创建 SQL 实例
|
|
56
|
+
const sql = new Sql(mockRun, mockExec);
|
|
57
|
+
sql.table = 'test_table';
|
|
58
|
+
|
|
59
|
+
const backupFile = './backup/test_mysql_backup_with_comments.sql';
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// 测试带创建表语句和注释的备份
|
|
63
|
+
console.log('\n1. 测试带创建表语句和注释的备份...');
|
|
64
|
+
const backupResult = await sql.backup(backupFile, true);
|
|
65
|
+
console.log('备份结果:', backupResult ? '成功' : '失败');
|
|
66
|
+
|
|
67
|
+
if (backupResult) {
|
|
68
|
+
// 读取备份文件内容
|
|
69
|
+
const fs = require('fs');
|
|
70
|
+
if (fs.existsSync(backupFile)) {
|
|
71
|
+
const content = fs.readFileSync(backupFile, 'utf8');
|
|
72
|
+
console.log('备份文件内容:');
|
|
73
|
+
console.log(content);
|
|
74
|
+
|
|
75
|
+
// 检查是否包含注释信息
|
|
76
|
+
if (content.includes("COMMENT '主键ID'") && content.includes("COMMENT '用户姓名'") &&
|
|
77
|
+
content.includes("COMMENT '用户年龄'") && content.includes("COMMENT='测试用户表'")) {
|
|
78
|
+
console.log('✅ 备份文件成功包含注释信息');
|
|
79
|
+
} else {
|
|
80
|
+
console.log('❌ 备份文件未包含注释信息');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 测试恢复功能
|
|
86
|
+
console.log('\n2. 测试恢复功能...');
|
|
87
|
+
const restoreResult = await sql.restore(backupFile);
|
|
88
|
+
console.log('恢复结果:', restoreResult ? '成功' : '失败');
|
|
89
|
+
|
|
90
|
+
console.log('\n=== 测试完成 ===');
|
|
91
|
+
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('测试出错:', error.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
testBackupWithComments().catch(console.error);
|
package/test_parser.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const { Sql } = require('./sql');
|
|
2
|
+
|
|
3
|
+
// 创建模拟的数据库操作函数
|
|
4
|
+
const mockRun = function(sql) {
|
|
5
|
+
console.log('执行查询SQL:', sql);
|
|
6
|
+
// 模拟查询结果
|
|
7
|
+
if (sql.includes('SHOW CREATE TABLE')) {
|
|
8
|
+
return [{
|
|
9
|
+
'Create Table': `CREATE TABLE \`test_table\` (
|
|
10
|
+
\`id\` int(11) NOT NULL AUTO_INCREMENT,
|
|
11
|
+
\`name\` varchar(50) NOT NULL,
|
|
12
|
+
\`age\` int(11) DEFAULT NULL,
|
|
13
|
+
PRIMARY KEY (\`id\`)
|
|
14
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`
|
|
15
|
+
}];
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const mockExec = function(sql) {
|
|
21
|
+
console.log('执行更新SQL:', sql);
|
|
22
|
+
return { affectedRows: 1 };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function testSqlParser() {
|
|
26
|
+
console.log('=== 测试SQL解析器注释处理功能 ===');
|
|
27
|
+
|
|
28
|
+
// 创建 SQL 实例
|
|
29
|
+
const sql = new Sql(mockRun, mockExec);
|
|
30
|
+
|
|
31
|
+
// 测试包含各种注释的SQL内容
|
|
32
|
+
const testSql = `
|
|
33
|
+
-- 这是一个单行注释
|
|
34
|
+
/* 这是一个多行注释
|
|
35
|
+
可以跨多行 */
|
|
36
|
+
CREATE TABLE IF NOT EXISTS \`test_table\` (
|
|
37
|
+
\`id\` int(11) NOT NULL AUTO_INCREMENT,
|
|
38
|
+
\`name\` varchar(50) NOT NULL,
|
|
39
|
+
\`age\` int(11) DEFAULT NULL,
|
|
40
|
+
PRIMARY KEY (\`id\`)
|
|
41
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
42
|
+
|
|
43
|
+
-- 另一个单行注释
|
|
44
|
+
INSERT INTO \`test_table\` (\`id\`, \`name\`, \`age\`) VALUES (1, '张三', 25);
|
|
45
|
+
/* 另一个多行注释 */
|
|
46
|
+
INSERT INTO \`test_table\` (\`id\`, \`name\`, \`age\`) VALUES (2, '李四', 30);
|
|
47
|
+
|
|
48
|
+
-- 包含字符串的测试
|
|
49
|
+
INSERT INTO \`test_table\` (\`id\`, \`name\`, \`age\`) VALUES (3, '王--五', 28);
|
|
50
|
+
INSERT INTO \`test_table\` (\`id\`, \`name\`, \`age\`) VALUES (4, '李/*四*/', 35);
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
console.log('原始SQL内容:');
|
|
54
|
+
console.log(testSql);
|
|
55
|
+
console.log('\n解析后的SQL语句:');
|
|
56
|
+
|
|
57
|
+
const statements = sql._parseSqlStatements(testSql);
|
|
58
|
+
statements.forEach((stmt, index) => {
|
|
59
|
+
console.log(`\n语句 ${index + 1}:`);
|
|
60
|
+
console.log(stmt);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
console.log('\n=== 解析完成 ===');
|
|
64
|
+
console.log(`共解析出 ${statements.length} 个有效语句`);
|
|
65
|
+
|
|
66
|
+
// 验证解析结果
|
|
67
|
+
const expectedStatements = 5; // 应该解析出5个有效语句
|
|
68
|
+
if (statements.length === expectedStatements) {
|
|
69
|
+
console.log('✅ SQL解析器注释处理功能正常');
|
|
70
|
+
} else {
|
|
71
|
+
console.log('❌ SQL解析器注释处理功能异常');
|
|
72
|
+
console.log(`期望解析出 ${expectedStatements} 个语句,实际解析出 ${statements.length} 个`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
testSqlParser().catch(console.error);
|