mm_sqlite 1.2.7 → 1.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_backup.sql +6 -0
- package/backup/test_backup1.sql +5 -0
- package/backup/test_backup2.sql +7 -0
- package/package.json +1 -1
- package/sql.js +340 -2
- package/test_backup.js +78 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- 备份表: test_table
|
|
2
|
+
-- 备份时间: 2025-12-25T15:56:50.301Z
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER PRIMARY KEY, `name` TEXT, `age` INTEGER);
|
|
5
|
+
|
|
6
|
+
INSERT INTO `test_table` (`id`, `name`, `age`) VALUES (1, '测试数据1', 25);
|
|
7
|
+
INSERT INTO `test_table` (`id`, `name`, `age`) VALUES (2, '测试数据2', 30);
|
package/package.json
CHANGED
package/sql.js
CHANGED
|
@@ -548,6 +548,159 @@ Sql.prototype.toStr = function (...args) {
|
|
|
548
548
|
return args.map(arg => String(arg)).join(' ');
|
|
549
549
|
};
|
|
550
550
|
|
|
551
|
+
/* === 备份恢复函数 === */
|
|
552
|
+
/**
|
|
553
|
+
* @description 备份数据库到 SQL 文件
|
|
554
|
+
* @param {string} file 备份文件路径
|
|
555
|
+
* @returns {Promise<boolean>} 备份是否成功
|
|
556
|
+
*/
|
|
557
|
+
Sql.prototype.backup = async function(file) {
|
|
558
|
+
if (!file || typeof file !== 'string') {
|
|
559
|
+
throw new TypeError('文件路径必须为字符串');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (!this.table) {
|
|
563
|
+
throw new TypeError('表名未设置');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
// 获取所有数据
|
|
568
|
+
const data = await this.getSql('', '', '*');
|
|
569
|
+
|
|
570
|
+
if (!data || !Array.isArray(data)) {
|
|
571
|
+
throw new Error('获取数据失败');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// 构建 SQL 语句
|
|
575
|
+
let sqlContent = '';
|
|
576
|
+
|
|
577
|
+
// 添加表结构(简化版,实际应用中可能需要更复杂的表结构导出)
|
|
578
|
+
sqlContent += `-- 备份表: ${this.table}\n`;
|
|
579
|
+
sqlContent += `-- 备份时间: ${new Date().toISOString()}\n\n`;
|
|
580
|
+
|
|
581
|
+
// 添加数据插入语句
|
|
582
|
+
for (const row of data) {
|
|
583
|
+
const keys = Object.keys(row).map(key => this.escapeId(key)).join(', ');
|
|
584
|
+
const values = Object.values(row).map(value => this.escape(value)).join(', ');
|
|
585
|
+
sqlContent += `INSERT INTO \`${this.table}\` (${keys}) VALUES (${values});\n`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// 写入文件(这里需要文件系统模块,实际使用时需要引入 fs)
|
|
589
|
+
const fs = require('fs');
|
|
590
|
+
const path = require('path');
|
|
591
|
+
|
|
592
|
+
// 确保目录存在
|
|
593
|
+
const dir = path.dirname(file);
|
|
594
|
+
if (!fs.existsSync(dir)) {
|
|
595
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
fs.writeFileSync(file, sqlContent, 'utf8');
|
|
599
|
+
|
|
600
|
+
return true;
|
|
601
|
+
} catch (err) {
|
|
602
|
+
this.error = err.message;
|
|
603
|
+
this.log('error', '备份数据库失败', err);
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* @description 从 SQL 文件恢复数据库
|
|
610
|
+
* @param {string} file 恢复文件路径
|
|
611
|
+
* @returns {Promise<boolean>} 恢复是否成功
|
|
612
|
+
*/
|
|
613
|
+
Sql.prototype.restore = async function(file) {
|
|
614
|
+
if (!file || typeof file !== 'string') {
|
|
615
|
+
throw new TypeError('文件路径必须为字符串');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (!this.table) {
|
|
619
|
+
throw new TypeError('表名未设置');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
const fs = require('fs');
|
|
624
|
+
|
|
625
|
+
// 检查文件是否存在
|
|
626
|
+
if (!fs.existsSync(file)) {
|
|
627
|
+
throw new Error('备份文件不存在');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// 读取文件内容
|
|
631
|
+
const sqlContent = fs.readFileSync(file, 'utf8');
|
|
632
|
+
|
|
633
|
+
if (!sqlContent) {
|
|
634
|
+
throw new Error('备份文件为空');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// 解析 SQL 语句(按分号分割,但需要处理字符串中的分号)
|
|
638
|
+
const statements = this._parseSqlStatements(sqlContent);
|
|
639
|
+
|
|
640
|
+
// 执行所有 SQL 语句
|
|
641
|
+
for (const statement of statements) {
|
|
642
|
+
if (statement.trim()) {
|
|
643
|
+
// 跳过注释行
|
|
644
|
+
if (!statement.trim().startsWith('--')) {
|
|
645
|
+
await this.exec(statement);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return true;
|
|
651
|
+
} catch (err) {
|
|
652
|
+
this.error = err.message;
|
|
653
|
+
this.log('error', '恢复数据库失败', err);
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* @description 解析 SQL 语句,正确处理字符串中的分号
|
|
660
|
+
* @param {string} sql SQL 内容
|
|
661
|
+
* @returns {string[]} SQL 语句数组
|
|
662
|
+
* @private
|
|
663
|
+
*/
|
|
664
|
+
Sql.prototype._parseSqlStatements = function(sql) {
|
|
665
|
+
const statements = [];
|
|
666
|
+
let currentStatement = '';
|
|
667
|
+
let inString = false;
|
|
668
|
+
let stringChar = '';
|
|
669
|
+
|
|
670
|
+
for (let i = 0; i < sql.length; i++) {
|
|
671
|
+
const char = sql[i];
|
|
672
|
+
|
|
673
|
+
if (char === "'" || char === '"') {
|
|
674
|
+
if (!inString) {
|
|
675
|
+
inString = true;
|
|
676
|
+
stringChar = char;
|
|
677
|
+
} else if (char === stringChar) {
|
|
678
|
+
// 检查是否是转义字符
|
|
679
|
+
if (i > 0 && sql[i-1] === '\\') {
|
|
680
|
+
// 转义字符,继续字符串
|
|
681
|
+
} else {
|
|
682
|
+
inString = false;
|
|
683
|
+
stringChar = '';
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (!inString && char === ';') {
|
|
689
|
+
statements.push(currentStatement.trim());
|
|
690
|
+
currentStatement = '';
|
|
691
|
+
} else {
|
|
692
|
+
currentStatement += char;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// 添加最后一个语句(如果没有分号结尾)
|
|
697
|
+
if (currentStatement.trim()) {
|
|
698
|
+
statements.push(currentStatement.trim());
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return statements.filter(stmt => stmt.length > 0);
|
|
702
|
+
};
|
|
703
|
+
|
|
551
704
|
/* === sql语句拼接函数 === */
|
|
552
705
|
/**
|
|
553
706
|
* @description 转为where语句
|
|
@@ -1463,8 +1616,6 @@ Sql.prototype.getObj = async function (query, sort, view, like, timeout = 30000)
|
|
|
1463
1616
|
return null;
|
|
1464
1617
|
};
|
|
1465
1618
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
1619
|
/**
|
|
1469
1620
|
* 从SQL文件加载数据库
|
|
1470
1621
|
* @param {string} file - SQL文件路径
|
|
@@ -1618,6 +1769,193 @@ Sql.prototype._splitSqlStatements = function (sql_content) {
|
|
|
1618
1769
|
return stmts;
|
|
1619
1770
|
};
|
|
1620
1771
|
|
|
1772
|
+
/**
|
|
1773
|
+
* @description 获取创建表的 SQL 语句
|
|
1774
|
+
* @returns {Promise<string>} 创建表的 SQL 语句
|
|
1775
|
+
* @private
|
|
1776
|
+
*/
|
|
1777
|
+
Sql.prototype._getCreateTableSql = async function() {
|
|
1778
|
+
try {
|
|
1779
|
+
// 查询表结构信息(SQLite 使用 sqlite_master 表)
|
|
1780
|
+
const sql = `SELECT sql FROM sqlite_master WHERE type='table' AND name='${this.table}'`;
|
|
1781
|
+
const result = await this.run(sql);
|
|
1782
|
+
|
|
1783
|
+
if (result && result.length > 0 && result[0].sql) {
|
|
1784
|
+
// 在创建表语句中添加 IF NOT EXISTS 条件
|
|
1785
|
+
let create_sql = result[0].sql;
|
|
1786
|
+
if (create_sql.toUpperCase().startsWith('CREATE TABLE')) {
|
|
1787
|
+
create_sql = create_sql.replace('CREATE TABLE', 'CREATE TABLE IF NOT EXISTS');
|
|
1788
|
+
}
|
|
1789
|
+
return create_sql + ';';
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
return '';
|
|
1793
|
+
} catch (err) {
|
|
1794
|
+
this.log('warn', '获取表结构失败,将仅备份数据', err);
|
|
1795
|
+
return '';
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
|
|
1799
|
+
/**
|
|
1800
|
+
* @description 备份数据库到 SQL 文件
|
|
1801
|
+
* @param {string} file 备份文件路径
|
|
1802
|
+
* @param {boolean} create 是否导出创建表语句,默认为false
|
|
1803
|
+
* @returns {Promise<boolean>} 备份是否成功
|
|
1804
|
+
*/
|
|
1805
|
+
Sql.prototype.backup = async function(file, create = false) {
|
|
1806
|
+
if (!file || typeof file !== 'string') {
|
|
1807
|
+
throw new TypeError('文件路径必须为字符串');
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
if (!this.table) {
|
|
1811
|
+
throw new TypeError('表名未设置');
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
try {
|
|
1815
|
+
// 获取所有数据
|
|
1816
|
+
const data = await this.getSql('', '', '*');
|
|
1817
|
+
|
|
1818
|
+
if (!data || !Array.isArray(data)) {
|
|
1819
|
+
throw new Error('获取数据失败');
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// 构建 SQL 语句
|
|
1823
|
+
let sql_content = '';
|
|
1824
|
+
|
|
1825
|
+
// 添加表结构(简化版,实际应用中可能需要更复杂的表结构导出)
|
|
1826
|
+
sql_content += `-- 备份表: ${this.table}\n`;
|
|
1827
|
+
sql_content += `-- 备份时间: ${new Date().toISOString()}\n\n`;
|
|
1828
|
+
|
|
1829
|
+
// 如果create为true,添加创建表语句
|
|
1830
|
+
if (create) {
|
|
1831
|
+
const create_table_sql = await this._getCreateTableSql();
|
|
1832
|
+
if (create_table_sql) {
|
|
1833
|
+
sql_content += create_table_sql + '\n\n';
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// 添加数据插入语句
|
|
1838
|
+
for (const row of data) {
|
|
1839
|
+
const keys = Object.keys(row).map(key => this.escapeId(key)).join(', ');
|
|
1840
|
+
const values = Object.values(row).map(value => this.escape(value)).join(', ');
|
|
1841
|
+
sql_content += `INSERT INTO \`${this.table}\` (${keys}) VALUES (${values});\n`;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// 写入文件(这里需要文件系统模块,实际使用时需要引入 fs)
|
|
1845
|
+
const fs = require('fs');
|
|
1846
|
+
const path = require('path');
|
|
1847
|
+
|
|
1848
|
+
// 确保目录存在
|
|
1849
|
+
const dir = path.dirname(file);
|
|
1850
|
+
if (!fs.existsSync(dir)) {
|
|
1851
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
fs.writeFileSync(file, sql_content, 'utf8');
|
|
1855
|
+
|
|
1856
|
+
return true;
|
|
1857
|
+
} catch (err) {
|
|
1858
|
+
this.error = err.message;
|
|
1859
|
+
this.log('error', '备份数据库失败', err);
|
|
1860
|
+
return false;
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1864
|
+
/**
|
|
1865
|
+
* @description 从 SQL 文件恢复数据库
|
|
1866
|
+
* @param {string} file 备份文件路径
|
|
1867
|
+
* @returns {Promise<boolean>} 恢复是否成功
|
|
1868
|
+
*/
|
|
1869
|
+
Sql.prototype.restore = async function(file) {
|
|
1870
|
+
if (!file || typeof file !== 'string') {
|
|
1871
|
+
throw new TypeError('文件路径必须为字符串');
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
if (!this.table) {
|
|
1875
|
+
throw new TypeError('表名未设置');
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
try {
|
|
1879
|
+
const fs = require('fs');
|
|
1880
|
+
|
|
1881
|
+
if (!fs.existsSync(file)) {
|
|
1882
|
+
throw new Error('备份文件不存在');
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// 读取 SQL 文件
|
|
1886
|
+
const sql_content = fs.readFileSync(file, 'utf8');
|
|
1887
|
+
|
|
1888
|
+
// 解析 SQL 语句
|
|
1889
|
+
const stmts = this._parseSqlStatements(sql_content);
|
|
1890
|
+
|
|
1891
|
+
// 执行每个 SQL 语句
|
|
1892
|
+
for (const stmt of stmts) {
|
|
1893
|
+
if (stmt.trim() && !stmt.startsWith('--')) {
|
|
1894
|
+
await this.exec(stmt);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
return true;
|
|
1899
|
+
} catch (err) {
|
|
1900
|
+
this.error = err.message;
|
|
1901
|
+
this.log('error', '恢复数据库失败', err);
|
|
1902
|
+
return false;
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
|
|
1906
|
+
/**
|
|
1907
|
+
* @description 解析 SQL 语句
|
|
1908
|
+
* @param {string} sql SQL 内容
|
|
1909
|
+
* @returns {string[]} SQL 语句数组
|
|
1910
|
+
* @private
|
|
1911
|
+
*/
|
|
1912
|
+
Sql.prototype._parseSqlStatements = function(sql) {
|
|
1913
|
+
const stmts = [];
|
|
1914
|
+
let cur_stmt = '';
|
|
1915
|
+
let in_str = false;
|
|
1916
|
+
let in_cmt = false;
|
|
1917
|
+
let quote_char = '';
|
|
1918
|
+
|
|
1919
|
+
for (let i = 0; i < sql.length; i++) {
|
|
1920
|
+
const char = sql[i];
|
|
1921
|
+
const next_char = sql[i + 1] || '';
|
|
1922
|
+
|
|
1923
|
+
// 处理字符串
|
|
1924
|
+
if (!in_cmt && (char === "'" || char === '"') && (i === 0 || sql[i - 1] !== '\\')) {
|
|
1925
|
+
if (!in_str) {
|
|
1926
|
+
in_str = true;
|
|
1927
|
+
quote_char = char;
|
|
1928
|
+
} else if (char === quote_char) {
|
|
1929
|
+
in_str = false;
|
|
1930
|
+
quote_char = '';
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// 处理注释
|
|
1935
|
+
if (!in_str && char === '-' && next_char === '-') {
|
|
1936
|
+
in_cmt = true;
|
|
1937
|
+
}
|
|
1938
|
+
if (in_cmt && char === '\n') {
|
|
1939
|
+
in_cmt = false;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// 分割语句
|
|
1943
|
+
if (!in_str && !in_cmt && char === ';') {
|
|
1944
|
+
stmts.push(cur_stmt.trim());
|
|
1945
|
+
cur_stmt = '';
|
|
1946
|
+
} else {
|
|
1947
|
+
cur_stmt += char;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// 添加最后一个语句(如果有)
|
|
1952
|
+
if (cur_stmt.trim()) {
|
|
1953
|
+
stmts.push(cur_stmt.trim());
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
return stmts;
|
|
1957
|
+
};
|
|
1958
|
+
|
|
1621
1959
|
module.exports = {
|
|
1622
1960
|
Sql
|
|
1623
1961
|
};
|
package/test_backup.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function mockExec(sql) {
|
|
30
|
+
console.log('执行更新SQL:', sql);
|
|
31
|
+
return { affectedRows: 1, insertId: 1 };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function testBackupRestore() {
|
|
35
|
+
console.log('=== 测试备份恢复功能 ===');
|
|
36
|
+
|
|
37
|
+
// 创建 SQL 实例
|
|
38
|
+
const sql = new Sql(mockRun, mockExec);
|
|
39
|
+
sql.table = 'test_table';
|
|
40
|
+
|
|
41
|
+
const backupFile = './backup/test_backup.sql';
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// 测试备份
|
|
45
|
+
console.log('\n1. 测试备份功能...');
|
|
46
|
+
const backupResult = await sql.backup(backupFile);
|
|
47
|
+
console.log('备份结果:', backupResult ? '成功' : '失败');
|
|
48
|
+
|
|
49
|
+
if (backupResult) {
|
|
50
|
+
// 读取备份文件内容
|
|
51
|
+
const fs = require('fs');
|
|
52
|
+
if (fs.existsSync(backupFile)) {
|
|
53
|
+
const content = fs.readFileSync(backupFile, 'utf8');
|
|
54
|
+
console.log('备份文件内容:');
|
|
55
|
+
console.log(content);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 测试恢复
|
|
60
|
+
console.log('\n2. 测试恢复功能...');
|
|
61
|
+
const restoreResult = await sql.restore(backupFile);
|
|
62
|
+
console.log('恢复结果:', restoreResult ? '成功' : '失败');
|
|
63
|
+
|
|
64
|
+
console.log('\n=== 测试完成 ===');
|
|
65
|
+
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('测试出错:', error.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 运行测试
|
|
72
|
+
if (require.main === module) {
|
|
73
|
+
testBackupRestore();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
testBackupRestore
|
|
78
|
+
};
|