Переглянути джерело

feat(driver): VACUUM 语句 — db.Exec("VACUUM") 直接整库重建

- parser: 新增 kVacuum, 裸语句 (大小写/空白不敏感), 无子句
- exec: db.VACUUM() 透传, RowsAffected=复制行数; 活跃事务中引擎拒
  (ErrTxnActive), 只读模式报错不动文件; Close/Open 切换由引擎串行锁
  与驱动 shared.mu 双重挡住并发
- 场景: 生产库 analyzer 报 CORRUPT (panic 时代遗留: 1302 WAL 残留页 /
  28 悬空索引项 / 缺条目 / 坏槽位), VACUUM 重建后全部归零, 文件 227→198MB
- 测试: TestP1Vacuum (重建保真/继续读写/大小写形态) + TestP1VacuumInTxn
- 文档: SQL_DRIVER_GUIDE 维护语句章节
yl32940 3 тижнів тому
батько
коміт
a7d0c72713
4 змінених файлів з 124 додано та 2 видалено
  1. 15 0
      DB/driver/exec.go
  2. 82 0
      DB/driver/p1_features_test.go
  3. 12 2
      DB/driver/parser.go
  4. 15 0
      docs/SQL_DRIVER_GUIDE.md

+ 15 - 0
DB/driver/exec.go

@@ -37,11 +37,26 @@ func execStatement(db *core.DB, st *statement, args []driver.NamedValue) (driver
 		return execAlter(db, st.alt)
 	case kTruncate:
 		return execTruncate(db, st.trunc)
+	case kVacuum:
+		return execVacuum(db)
 	default:
 		return nil, fmt.Errorf("vrtx: 该语句不能用 Exec 执行")
 	}
 }
 
+// execVacuum VACUUM —— 整库重建。引擎层会全程持有操作级串行锁并自行
+// Close/Open 句柄 (句柄指针不变, 驱动层的 shared.db 引用不需要跟着换),
+// 驱动侧的 shared.mu 正好把并发语句挡在门外, 两者不冲突。
+// 活跃事务中拒绝 (引擎层 ErrTxnActive); 只读模式 (force-inspect) 下
+// Commit 阶段即报错, 不会真动文件。
+func execVacuum(db *core.DB) (driver.Result, error) {
+	res, err := db.VACUUM()
+	if err != nil {
+		return nil, err
+	}
+	return &result{affected: res.RowsCopied}, nil
+}
+
 func execInsert(db *core.DB, ins *insertStmt, args []driver.NamedValue) (driver.Result, error) {
 	tbl, err := findTable(db, ins.table)
 	if err != nil {

+ 82 - 0
DB/driver/p1_features_test.go

@@ -334,3 +334,85 @@ func TestP1ReadOnlyDSN(t *testing.T) {
 		t.Fatalf("持久化后应 2 行, 实际 %d (%s)", cnt, fmt.Sprint(cnt))
 	}
 }
+
+// TestP1Vacuum VACUUM 语句: 整库重建后行数据原样、文件可继续读写。
+// 顺带验证三种典型调用形态: 裸语句 / 多行空白 / 大小写混合。
+func TestP1Vacuum(t *testing.T) {
+	db := openTestDB(t)
+	if _, err := db.Exec(`CREATE TABLE t (id BIGINT PRIMARY KEY, v INT)`); err != nil {
+		t.Fatal(err)
+	}
+	for i := 1; i <= 10; i++ {
+		if _, err := db.Exec(`INSERT INTO t (id, v) VALUES (?, ?)`, i, i*10); err != nil {
+			t.Fatal(err)
+		}
+	}
+	// 删掉一半再 VACUUM: 有垃圾可收, 更接近真实场景
+	if _, err := db.Exec(`DELETE FROM t WHERE id > ?`, 5); err != nil {
+		t.Fatal(err)
+	}
+
+	res, err := db.Exec(`VACUUM`)
+	if err != nil {
+		t.Fatalf("vacuum: %v", err)
+	}
+	if n, err := res.RowsAffected(); err != nil || n != 5 {
+		t.Fatalf("RowsAffected 应为复制的 5 行, 实际 %d (%v)", n, err)
+	}
+
+	// 行数据原样
+	var cnt int
+	if err := db.QueryRow(`SELECT COUNT(*) FROM t`).Scan(&cnt); err != nil {
+		t.Fatal(err)
+	}
+	if cnt != 5 {
+		t.Fatalf("VACUUM 后应 5 行, 实际 %d", cnt)
+	}
+	var v int
+	if err := db.QueryRow(`SELECT v FROM t WHERE id = ?`, 3).Scan(&v); err != nil {
+		t.Fatal(err)
+	}
+	if v != 30 {
+		t.Fatalf("行数据应为 30, 实际 %d", v)
+	}
+
+	// 重建后可继续读写
+	if _, err := db.Exec(`INSERT INTO t (id, v) VALUES (?, ?)`, 100, 1); err != nil {
+		t.Fatalf("VACUUM 后插入失败: %v", err)
+	}
+	// 大小写/空白形态
+	if _, err := db.Exec("  vacuum  "); err != nil {
+		t.Fatalf("小写带空白 VACUUM: %v", err)
+	}
+	if err := db.QueryRow(`SELECT COUNT(*) FROM t`).Scan(&cnt); err != nil {
+		t.Fatal(err)
+	}
+	if cnt != 6 {
+		t.Fatalf("第二次 VACUUM 后应 6 行, 实际 %d", cnt)
+	}
+}
+
+// TestP1VacuumInTxn 活跃事务中 VACUUM 必须被拒 (引擎层 ErrTxnActive 透传)。
+func TestP1VacuumInTxn(t *testing.T) {
+	db := openTestDB(t)
+	if _, err := db.Exec(`CREATE TABLE t (id BIGINT PRIMARY KEY, v INT)`); err != nil {
+		t.Fatal(err)
+	}
+	tx, err := db.Begin()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if _, err := tx.Exec(`INSERT INTO t (id, v) VALUES (?, ?)`, 1, 1); err != nil {
+		t.Fatal(err)
+	}
+	if _, err := tx.Exec(`VACUUM`); err == nil {
+		t.Fatal("事务中 VACUUM 应被拒绝")
+	}
+	if err := tx.Rollback(); err != nil {
+		t.Fatal(err)
+	}
+	// 回滚后 VACUUM 恢复可用
+	if _, err := db.Exec(`VACUUM`); err != nil {
+		t.Fatalf("回滚后 VACUUM: %v", err)
+	}
+}

+ 12 - 2
DB/driver/parser.go

@@ -49,6 +49,7 @@ const (
 	kAlter
 	kShow
 	kTruncate
+	kVacuum
 )
 
 type statement struct {
@@ -375,7 +376,7 @@ var keywords = map[string]bool{
 	"AS": true,
 	"ALTER": true, "ADD": true, "COLUMN": true,
 	"SHOW": true, "DESCRIBE": true, "FULL": true, "TABLES": true, "COLUMNS": true,
-	"TRUNCATE": true, "DISTINCT": true,
+	"TRUNCATE": true, "DISTINCT": true, "VACUUM": true,
 	"JOIN": true, "INNER": true, "LEFT": true, "OUTER": true,
 }
 
@@ -596,7 +597,7 @@ func (p *parser) expectIdent() (string, error) {
 func (p *parser) parseStatement() (*statement, error) {
 	t := p.peek()
 	if t.kind != tkKeyword {
-		return nil, fmt.Errorf("语句必须以 SELECT/INSERT/UPDATE/DELETE/CREATE/DROP/ALTER/SHOW/TRUNCATE 开头")
+		return nil, fmt.Errorf("语句必须以 SELECT/INSERT/UPDATE/DELETE/CREATE/DROP/ALTER/SHOW/TRUNCATE/VACUUM 开头")
 	}
 	switch t.text {
 	case "SELECT":
@@ -619,11 +620,20 @@ func (p *parser) parseStatement() (*statement, error) {
 		return p.parseDescribe()
 	case "TRUNCATE":
 		return p.parseTruncate()
+	case "VACUUM":
+		return p.parseVacuum()
 	default:
 		return nil, fmt.Errorf("不支持的语句开头: %s", t.text)
 	}
 }
 
+// parseVacuum VACUUM —— 整库重建 (回收孤儿页 / 重建索引 / 收缩文件)。
+// 只接受裸语句, 不带任何子句; 细节 (目标表 / 并行度) 由引擎选项管, SQL 层不做。
+func (p *parser) parseVacuum() (*statement, error) {
+	p.next() // 吃掉 VACUUM
+	return &statement{kind: kVacuum}, nil
+}
+
 // parseCreateIndex CREATE [UNIQUE] INDEX|KEY name ON t (col)
 func (p *parser) parseCreateIndex(unique bool) (*statement, error) {
 	t := p.next()

+ 15 - 0
docs/SQL_DRIVER_GUIDE.md

@@ -114,6 +114,21 @@ DROP TABLE [IF EXISTS] t
 列级 `AUTO_INCREMENT` / `COMMENT '...'` 可写(被忽略);`DEFAULT` 仅支持
 `DEFAULT NULL`,其它默认值报错(避免静默错配)。
 
+### 维护语句
+
+```
+VACUUM
+```
+
+整库重建:回收孤儿页 / 重建全部索引 / 收缩文件。行数据从旧库逐行复制,
+索引从行数据重新生成,所以它也是"索引出问题时"的修复手段(等价于
+SQLite 的 `REINDEX` + `VACUUM` 合体)。注意:
+
+- 走 `Exec` 执行(`db.Exec("VACUUM")`),`RowsAffected` 返回复制的行数;
+- 执行期间引擎会做 Close/Open 句柄切换,**并发语句自动排队**(驱动串行锁),无需停应用;
+- **活跃事务中拒绝**(`ErrTxnActive`)——先提交或回滚再执行;
+- 只读(force-inspect)模式下报错,不会动文件。
+
 ---
 
 ## 不支持的语句(明确报错,不静默)