ソースを参照

feat(driver): database/sql 驱动 (vrtxsql) — ORM 子集翻译器, M1/M2/M3 全量

第三方工具/ORM (xorm/sqlx/自研) 可经标准 database/sql 直连 VRTX:
  sql.Open("vrtx", "<文件路径>")

- 手写零依赖 SQL 子集解析器 (词法/递归下降): SELECT (投影/WHERE 全条件族
  含 IN/LIKE/IS NULL/ORDER BY/LIMIT/OFFSET/聚合 COUNT SUM MIN MAX AVG)、
  INSERT (多行 VALUES/显式主键/省略列=NULL)、UPDATE/DELETE (WHERE 定位
  pkID 逐行)、CREATE TABLE ([UNIQUE] INDEX 内联 + 列级约束 + 表级 PK)、
  CREATE [UNIQUE] INDEX、DROP TABLE [IF EXISTS]。JOIN/子查询/GROUP BY/
  DEFAULT 非 NULL 等明确报错不静默。
- 驱动壳: 同 DSN 共享引擎实例 (注册表+引用计数), 全局串行锁对齐单写者模型
  (事务持有期间其它连接阻塞排队, 拥有者连接免重入自死锁); Conn/BeginTx/
  ExecerContext/QueryerContext/Pinger/Stmt/Rows/Result 全套。
- 语义对齐: SQL INSERT 省略列=NULL (显式写 nil 由 FlagNullable 裁决);
  显式主键整数值走 InsertRowWithID; AVG 分母=非 NULL 行数; LIKE 参数化。
- demo/sql_driver: M1/M2/M3 全特性走查 + 不支持语法报错展示。
- 驱动测试 4 组: CRUD+查询全链路 / 事务 / 不支持语法 / 共享引擎。
yl32940 3 週間 前
親
コミット
aa8857b65a
6 ファイル変更、2866 行追加、0 行削除
  1. 283 0
      DB/driver/convert.go
  2. 279 0
      DB/driver/driver.go
  3. 245 0
      DB/driver/driver_test.go
  4. 670 0
      DB/driver/exec.go
  5. 1168 0
      DB/driver/parser.go
  6. 221 0
      demo/sql_driver/main.go

+ 283 - 0
DB/driver/convert.go

@@ -0,0 +1,283 @@
+package vrtxsql
+
+// 值与类型的双向翻译:
+//   - SQL 字面量/driver.Value → 引擎列类型 (写入路径)
+//   - 引擎值 → driver.Value (结果集路径, database/sql 只接受基础类型)
+//   - SQL 类型名 → types.FieldInfo (CREATE TABLE 路径)
+
+import (
+	"database/sql/driver"
+	"fmt"
+	"math"
+	"strconv"
+	"strings"
+	"time"
+
+	"git.ycl.pub/yl32940/TsDB2/DB/core/types"
+)
+
+// =============================================================================
+// driver.Value → 引擎值 (按列类型)
+// =============================================================================
+
+func driverToEngineValue(fi *types.FieldInfo, v driver.Value) (interface{}, error) {
+	if v == nil {
+		return nil, nil // NULL: 由引擎按 FlagNullable 裁决
+	}
+	switch fi.FieldType {
+	case types.FieldType_Uint64, types.FieldType_Int64,
+		types.FieldType_Uint32, types.FieldType_Int32,
+		types.FieldType_Uint16, types.FieldType_Int16,
+		types.FieldType_Uint8, types.FieldType_Int8,
+		types.FieldType_Year:
+		return toInt64(v)
+	case types.FieldType_Float64, types.FieldType_Float32:
+		return toFloat64(v)
+	case types.FieldType_Bool:
+		switch t := v.(type) {
+		case bool:
+			return t, nil
+		case int64:
+			return t != 0, nil
+		case string:
+			return strings.EqualFold(t, "true") || t == "1", nil
+		}
+		return nil, typeMismatch(fi, v)
+	case types.FieldType_String:
+		switch t := v.(type) {
+		case string:
+			return t, nil
+		case []byte:
+			return string(t), nil
+		}
+		return nil, typeMismatch(fi, v)
+	case types.FieldType_Dynamicbyte:
+		switch t := v.(type) {
+		case []byte:
+			return t, nil
+		case string:
+			return []byte(t), nil
+		}
+		return nil, typeMismatch(fi, v)
+	case types.FieldType_Decimal:
+		switch t := v.(type) {
+		case string:
+			return types.MustParseDecimal(t), nil
+		case []byte:
+			return types.MustParseDecimal(string(t)), nil
+		case float64:
+			return types.MustParseDecimal(strconv.FormatFloat(t, 'f', -1, 64)), nil
+		}
+		return nil, typeMismatch(fi, v)
+	case types.FieldType_UUID:
+		switch t := v.(type) {
+		case string:
+			return types.MustParseUUID(t), nil // 若存在解析入口则用之
+		case []byte:
+			if len(t) == 16 {
+				var u [16]byte
+				copy(u[:], t)
+				return u, nil
+			}
+			return types.MustParseUUID(string(t)), nil
+		}
+		return nil, typeMismatch(fi, v)
+	case types.FieldType_Blob:
+		switch t := v.(type) {
+		case []byte:
+			return t, nil
+		case string:
+			return []byte(t), nil
+		}
+		return nil, typeMismatch(fi, v)
+	}
+	return nil, typeMismatch(fi, v)
+}
+
+func typeMismatch(fi *types.FieldInfo, v driver.Value) error {
+	return fmt.Errorf("值类型 %T 与列 %q 的类型 %d 不匹配", v, fi.FieldName, fi.FieldType)
+}
+
+func toInt64(v driver.Value) (interface{}, error) {
+	switch t := v.(type) {
+	case int64:
+		return t, nil
+	case float64:
+		return int64(t), nil
+	case bool:
+		if t {
+			return int64(1), nil
+		}
+		return int64(0), nil
+	case string:
+		n, err := strconv.ParseInt(strings.TrimSpace(t), 10, 64)
+		if err == nil {
+			return n, nil
+		}
+		f, ferr := strconv.ParseFloat(strings.TrimSpace(t), 64)
+		if ferr == nil {
+			return int64(f), nil
+		}
+	case []byte:
+		return toInt64(string(t))
+	case time.Time:
+		return int64(timeToMillis(t)), nil // 时间列: time.Time → 毫秒
+	}
+	return nil, fmt.Errorf("无法把 %T 转成整数", v)
+}
+
+func toFloat64(v driver.Value) (interface{}, error) {
+	switch t := v.(type) {
+	case float64:
+		return t, nil
+	case int64:
+		return float64(t), nil
+	case string:
+		f, err := strconv.ParseFloat(strings.TrimSpace(t), 64)
+		if err == nil {
+			return f, nil
+		}
+	case []byte:
+		return toFloat64(string(t))
+	}
+	return nil, fmt.Errorf("无法把 %T 转成浮点", v)
+}
+
+// =============================================================================
+// 字面量 → 引擎值 (WHERE/VALUES 里的纯文本数字/字符串)
+// =============================================================================
+
+// parseLiteralAsType 按列的实际类型解释字面量文本。数字字面量到整型列按 int64,
+// 到浮点按 float64; 字符串字面量按列类型解析 (数字字符串转数值, 其余原样)。
+func parseLiteralAsType(fi *types.FieldInfo, s string) (interface{}, error) {
+	switch fi.FieldType {
+	case types.FieldType_Float64, types.FieldType_Float32:
+		if f, err := strconv.ParseFloat(s, 64); err == nil {
+			return f, nil
+		}
+	case types.FieldType_Uint64, types.FieldType_Int64,
+		types.FieldType_Uint32, types.FieldType_Int32,
+		types.FieldType_Uint16, types.FieldType_Int16,
+		types.FieldType_Uint8, types.FieldType_Int8,
+		types.FieldType_Year:
+		if n, err := strconv.ParseInt(s, 10, 64); err == nil {
+			return n, nil
+		}
+		if f, ferr := strconv.ParseFloat(s, 64); ferr == nil {
+			return int64(f), nil
+		}
+	case types.FieldType_Bool:
+		if b, err := strconv.ParseBool(s); err == nil {
+			return b, nil
+		}
+	case types.FieldType_Decimal:
+		return types.MustParseDecimal(s), nil
+	default:
+		// 字符串/动态列: 原样
+		return s, nil
+	}
+	return nil, fmt.Errorf("字面量 %q 无法解释为列 %q 的类型 %d", s, fi.FieldName, fi.FieldType)
+}
+
+// =============================================================================
+// 引擎值 → driver.Value
+// =============================================================================
+
+func fieldValueToDriver(v interface{}) (driver.Value, error) {
+	switch t := v.(type) {
+	case nil:
+		return nil, nil
+	case int64:
+		return t, nil
+	case int:
+		return int64(t), nil
+	case uint64:
+		if t > math.MaxInt64 {
+			return nil, fmt.Errorf("uint64 值 %d 超出 database/sql 的 int64 表示范围", t)
+		}
+		return int64(t), nil
+	case uint32:
+		return int64(t), nil
+	case uint16:
+		return int64(t), nil
+	case uint8:
+		return int64(t), nil
+	case float64:
+		return t, nil
+	case float32:
+		return float64(t), nil
+	case bool:
+		return t, nil
+	case string:
+		return []byte(t), nil // 字符串统一以 []byte 出 (database/sql Scan 兼容)
+	case []byte:
+		return t, nil
+	case time.Time:
+		return t, nil
+	case types.BlobRef:
+		// Blob 列行内只有引用; v1 不物化 blob 内容, 明确报错引导用引擎 API
+		return nil, fmt.Errorf("BLOB 列的结果集物化暂不支持 (引用 %v), 请用引擎 Blob API 读取", t)
+	default:
+		// Decimal / UUID 等其余类型: 文本化出 (Scan 到 string 可用)
+		return []byte(fmt.Sprint(v)), nil
+	}
+}
+
+// =============================================================================
+// SQL 类型名 → types.FieldInfo (CREATE TABLE)
+// =============================================================================
+
+// sqlTypeToFieldInfo 映射 ORM 常见列类型。未覆盖的类型返回明确错误。
+func sqlTypeToFieldInfo(c colDef) (types.FieldInfo, error) {
+	fi := types.FieldInfo{FieldName: []byte(c.name)}
+	if !c.notNull {
+		fi.Flag |= types.FlagNullable // SQL 默认可空
+	}
+	name := c.typeName
+	switch name {
+	case "BIGINT", "INT8":
+		fi.FieldType = types.FieldType_Int64
+	case "INT", "INTEGER", "INT4", "MEDIUMINT":
+		fi.FieldType = types.FieldType_Int32
+	case "SMALLINT", "INT2":
+		fi.FieldType = types.FieldType_Int16
+	case "TINYINT", "INT1":
+		fi.FieldType = types.FieldType_Int8
+	case "BIGINT UNSIGNED":
+		fi.FieldType = types.FieldType_Uint64
+	case "INT UNSIGNED", "INTEGER UNSIGNED":
+		fi.FieldType = types.FieldType_Uint32
+	case "DOUBLE", "DOUBLE PRECISION", "REAL":
+		fi.FieldType = types.FieldType_Float64
+	case "FLOAT":
+		fi.FieldType = types.FieldType_Float32
+	case "BOOL", "BOOLEAN":
+		fi.FieldType = types.FieldType_Bool
+	case "VARCHAR", "CHAR", "CHARACTER":
+		w := 32
+		if c.arg > 0 {
+			w = c.arg
+		}
+		if w > 2048 {
+			return fi, fmt.Errorf("VARCHAR(%d) 超过引擎定长字符串上限 2048, 请用 TEXT (动态列)", w)
+		}
+		fi.FieldType = types.FieldType_String
+		fi.Flag |= types.WidthCodeFor(w)
+	case "TEXT", "LONGTEXT", "MEDIUMTEXT":
+		fi.FieldType = types.FieldType_Dynamicbyte
+	case "BLOB", "LONGBLOB", "MEDIUMBLOB", "BINARY", "VARBINARY":
+		fi.FieldType = types.FieldType_Blob
+	case "DECIMAL", "NUMERIC":
+		fi.FieldType = types.FieldType_Decimal
+		if c.arg >= 0 {
+			fi.Flag |= byte(c.arg) // DECIMAL(p,s) 的 s 落 Flag 低 5 位
+		}
+	case "DATETIME", "TIMESTAMP":
+		fi.FieldType = types.FieldType_Uint64 // 约定: 毫秒时间戳
+	case "UUID":
+		fi.FieldType = types.FieldType_UUID
+	default:
+		return fi, fmt.Errorf("不支持的列类型 %q", c.typeName)
+	}
+	return fi, nil
+}

+ 279 - 0
DB/driver/driver.go

@@ -0,0 +1,279 @@
+package vrtxsql
+
+// database/sql 驱动壳: 连接注册表 (同路径共享一个引擎 + 全局串行锁)、
+// Conn/Tx/Stmt/Rows 适配。引擎是单写者模型, 所有操作 (含事务全程) 由
+// shared.mu 串行化 —— 用并发换正确, database/sql 的连接池因此可以安全使用
+// (建议 SetMaxOpenConns(1) 避免无意义的阻塞排队)。
+
+import (
+	"context"
+	"database/sql"
+	"database/sql/driver"
+	"errors"
+	"fmt"
+	"io"
+	"sync"
+
+	"git.ycl.pub/yl32940/TsDB2/DB/core"
+)
+
+func init() {
+	sql.Register("vrtx", &vrtxDriver{})
+}
+
+// vrtxDriver 实现 driver.Driver。
+type vrtxDriver struct{}
+
+// registry 同一路径 (DSN) 的所有连接共享同一个 *core.DB —— 引擎的文件锁与
+// 单写者语义不允许一个库被打开成多个实例。
+var registry = struct {
+	sync.Mutex
+	m map[string]*shared
+}{m: make(map[string]*shared)}
+
+type shared struct {
+	db   *core.DB
+	dsn  string
+	mu   sync.Mutex // 全局串行: 引擎单写者模型; 事务持有至 Commit/Rollback
+	refs int
+}
+
+func sharedFor(dsn string) (*shared, error) {
+	registry.Lock()
+	defer registry.Unlock()
+	if s, ok := registry.m[dsn]; ok {
+		s.refs++
+		return s, nil
+	}
+	db, err := core.Open(dsn)
+	if err != nil {
+		return nil, err
+	}
+	s := &shared{db: db, dsn: dsn, refs: 1}
+	registry.m[dsn] = s
+	return s, nil
+}
+
+func (s *shared) release() error {
+	registry.Lock()
+	defer registry.Unlock()
+	s.refs--
+	if s.refs > 0 {
+		return nil
+	}
+	delete(registry.m, s.dsn)
+	return s.db.Close()
+}
+
+func (d *vrtxDriver) Open(dsn string) (driver.Conn, error) {
+	if dsn == "" {
+		return nil, errors.New("vrtx: DSN 为空 (应为数据库文件路径)")
+	}
+	s, err := sharedFor(dsn)
+	if err != nil {
+		return nil, fmt.Errorf("vrtx: 打开 %s: %w", dsn, err)
+	}
+	return &conn{s: s}, nil
+}
+
+// conn 实现 driver.Conn / driver.ConnBeginTx / driver.ExecerContext /
+// driver.QueryerContext / driver.Pinger。
+type conn struct {
+	s *shared
+	// activeTx 本连接上开着的事务: 事务期间持有 s.mu, 拥有者连接上的
+	// Exec/Query 不得重复加锁 (否则自死锁); 其它连接的操作照常抢锁,
+	// 事务提交/回滚前一律阻塞 —— 这就是全局串行化的实现方式。
+	activeTx *core.Transaction
+}
+
+var (
+	_ driver.Conn           = (*conn)(nil)
+	_ driver.ConnBeginTx    = (*conn)(nil)
+	_ driver.ExecerContext  = (*conn)(nil)
+	_ driver.QueryerContext = (*conn)(nil)
+	_ driver.Pinger         = (*conn)(nil)
+)
+
+func (c *conn) Prepare(query string) (driver.Stmt, error) {
+	st, err := parse(query)
+	if err != nil {
+		return nil, err
+	}
+	return &stmt{c: c, st: st}, nil
+}
+
+func (c *conn) Close() error {
+	if c.s == nil {
+		return nil
+	}
+	if c.activeTx != nil {
+		// 防御: 连接带着未决事务被回收 —— 回滚并放锁 (正常流程不会发生,
+		// database/sql 在事务结束前不会回收连接)
+		_ = c.activeTx.Rollback()
+		c.activeTx = nil
+		c.s.mu.Unlock()
+	}
+	s := c.s
+	c.s = nil
+	return s.release()
+}
+
+func (c *conn) Begin() (driver.Tx, error) {
+	return c.BeginTx(context.Background(), driver.TxOptions{})
+}
+
+func (c *conn) BeginTx(_ context.Context, _ driver.TxOptions) (driver.Tx, error) {
+	c.s.mu.Lock()
+	if c.s.db.HasActiveTransaction() {
+		c.s.mu.Unlock()
+		return nil, errors.New("vrtx: 已有活跃事务 (单写者模型禁止并发事务)")
+	}
+	c.activeTx = c.s.db.BeginTransaction()
+	return &tx{txn: c.activeTx, s: c.s, c: c}, nil
+}
+
+func (c *conn) Ping(_ context.Context) error {
+	// 引擎无活性探针: 句柄在 = 可服务 (打开时的 CRC/WAL 恢复已完成)。
+	return nil
+}
+
+func (c *conn) ExecContext(_ context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
+	st, err := parse(query)
+	if err != nil {
+		return nil, err
+	}
+	if st.kind == kSelect {
+		return nil, errors.New("vrtx: SELECT 请使用 QueryContext / Query")
+	}
+	if c.activeTx == nil {
+		c.s.mu.Lock()
+		defer c.s.mu.Unlock()
+	}
+	return execStatement(c.s.db, st, args)
+}
+
+func (c *conn) QueryContext(_ context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
+	st, err := parse(query)
+	if err != nil {
+		return nil, err
+	}
+	if st.kind != kSelect {
+		return nil, errors.New("vrtx: 非 SELECT 语句请使用 ExecContext / Exec")
+	}
+	if c.activeTx == nil {
+		c.s.mu.Lock()
+		defer c.s.mu.Unlock()
+	}
+	return queryStatement(c.s.db, st.sel, args)
+}
+
+// tx 实现 driver.Tx。Commit/Rollback 释放 shared.mu (Begin 时持有)。
+type tx struct {
+	txn *core.Transaction
+	s   *shared
+	c   *conn
+}
+
+func (t *tx) Commit() error {
+	err := t.txn.Commit()
+	t.c.activeTx = nil
+	t.s.mu.Unlock()
+	return err
+}
+
+func (t *tx) Rollback() error {
+	err := t.txn.Rollback()
+	t.c.activeTx = nil
+	t.s.mu.Unlock()
+	return err
+}
+
+// stmt 实现 driver.Stmt (Prepare 路径: 解析已完成, 执行时只绑定参数)。
+type stmt struct {
+	c  *conn
+	st *statement
+}
+
+var (
+	_ driver.Stmt = (*stmt)(nil)
+)
+
+func (s *stmt) Close() error  { return nil }
+func (s *stmt) NumInput() int { return s.st.argCount() }
+
+func (s *stmt) Exec(args []driver.Value) (driver.Result, error) {
+	return s.execContext(context.Background(), valuesToNamed(args))
+}
+
+func (s *stmt) Query(args []driver.Value) (driver.Rows, error) {
+	return s.queryContext(context.Background(), valuesToNamed(args))
+}
+
+// 兼容 database/sql 的 context 变体路径。
+func (s *stmt) ExecContext(_ context.Context, args []driver.NamedValue) (driver.Result, error) {
+	return s.execContext(context.Background(), args)
+}
+
+func (s *stmt) execContext(_ context.Context, args []driver.NamedValue) (driver.Result, error) {
+	if s.st.kind == kSelect {
+		return nil, errors.New("vrtx: SELECT 请使用 Query")
+	}
+	if s.c.activeTx == nil {
+		s.c.s.mu.Lock()
+		defer s.c.s.mu.Unlock()
+	}
+	return execStatement(s.c.s.db, s.st, args)
+}
+
+func (s *stmt) queryContext(_ context.Context, args []driver.NamedValue) (driver.Rows, error) {
+	if s.st.kind != kSelect {
+		return nil, errors.New("vrtx: 非 SELECT 请使用 Exec")
+	}
+	if s.c.activeTx == nil {
+		s.c.s.mu.Lock()
+		defer s.c.s.mu.Unlock()
+	}
+	return queryStatement(s.c.s.db, s.st.sel, args)
+}
+
+func valuesToNamed(args []driver.Value) []driver.NamedValue {
+	out := make([]driver.NamedValue, len(args))
+	for i, v := range args {
+		out[i] = driver.NamedValue{Ordinal: i + 1, Value: v}
+	}
+	return out
+}
+
+// rows 实现 driver.Rows (v1 物化: 简单正确; 大结果集后续可换流式)。
+type rows struct {
+	cols []string
+	data [][]driver.Value
+	pos  int
+}
+
+var _ driver.Rows = (*rows)(nil)
+
+func (r *rows) Columns() []string { return r.cols }
+
+func (r *rows) Close() error {
+	r.data = nil
+	return nil
+}
+
+func (r *rows) Next(dest []driver.Value) error {
+	if r.pos >= len(r.data) {
+		return io.EOF
+	}
+	copy(dest, r.data[r.pos])
+	r.pos++
+	return nil
+}
+
+// result 实现 driver.Result。
+type result struct {
+	lastID   int64
+	affected int64
+}
+
+func (r *result) LastInsertId() (int64, error) { return r.lastID, nil }
+func (r *result) RowsAffected() (int64, error) { return r.affected, nil }

+ 245 - 0
DB/driver/driver_test.go

@@ -0,0 +1,245 @@
+package vrtxsql
+
+// database/sql 驱动测试: 走标准库 API 全链路 (Open/Exec/Query/Tx/Prepare),
+// 覆盖 M1 (INSERT/SELECT/WHERE/LIMIT) + M2 (UPDATE/DELETE/事务/IS NULL)
+// + M3 (CREATE/DROP TABLE + 聚合)。
+
+import (
+	"git.ycl.pub/yl32940/TsDB2/DB/core"
+	"database/sql"
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+func openTestDB(t *testing.T) *sql.DB {
+	t.Helper()
+	dsn := filepath.Join(t.TempDir(), "driver_test.db")
+	db, err := sql.Open("vrtx", dsn)
+	if err != nil {
+		t.Fatalf("open: %v", err)
+	}
+	t.Cleanup(func() { _ = db.Close() })
+	return db
+}
+
+func TestDriverCRUDAndQuery(t *testing.T) {
+	db := openTestDB(t)
+
+	// M3: DDL
+	if _, err := db.Exec(`CREATE TABLE users (
+		id BIGINT PRIMARY KEY,
+		name VARCHAR(32) NOT NULL,
+		age INT,
+		email VARCHAR(64) UNIQUE
+	)`); err != nil {
+		t.Fatalf("create table: %v", err)
+	}
+
+	// M1: INSERT (LastInsertId)
+	res, err := db.Exec(`INSERT INTO users (id, name, age, email) VALUES (?, ?, ?, ?)`,
+		1, "alice", 30, "a@x.com")
+	if err != nil {
+		t.Fatalf("insert: %v", err)
+	}
+	if id, _ := res.LastInsertId(); id != 1 {
+		t.Fatalf("LastInsertId=%d, 想要 1", id)
+	}
+	if n, _ := res.RowsAffected(); n != 1 {
+		t.Fatalf("RowsAffected=%d, 想要 1", n)
+	}
+	// 省略列 (email NULL)
+	if _, err := db.Exec(`INSERT INTO users (id, name, age) VALUES (?, ?, ?)`, 2, "bob", 25); err != nil {
+		t.Fatalf("insert 2: %v", err)
+	}
+	// 唯一索引: 重复 email 必须报错
+	if _, err := db.Exec(`INSERT INTO users (id, name, email) VALUES (?, ?, ?)`, 3, "carl", "a@x.com"); err == nil {
+		t.Fatalf("唯一索引应拒绝重复 email")
+	}
+
+	// M1: SELECT 投影 + WHERE + ORDER BY + LIMIT
+	rows, err := db.Query(`SELECT id, name, age FROM users WHERE age >= ? ORDER BY age DESC LIMIT 10`, 18)
+	if err != nil {
+		t.Fatalf("query: %v", err)
+	}
+	defer rows.Close()
+	type u struct {
+		id      int64
+		name    string
+		age     int64
+		hasAge  bool
+	}
+	var got []u
+	for rows.Next() {
+		var r u
+		var name []byte
+		var age sql.NullInt64
+		if err := rows.Scan(&r.id, &name, &age); err != nil {
+			t.Fatalf("scan: %v", err)
+		}
+		r.name = string(name)
+		if age.Valid {
+			r.age = age.Int64
+			r.hasAge = true
+		}
+		got = append(got, r)
+	}
+	if len(got) != 2 || got[0].name != "alice" || got[1].name != "bob" {
+		t.Fatalf("查询结果 %+v 不对", got)
+	}
+	if !got[0].hasAge || got[0].age != 30 {
+		t.Fatalf("age 投影不对: %+v", got[0])
+	}
+
+	// IN / OR / NOT
+	var cnt int
+	if err := db.QueryRow(`SELECT COUNT(*) FROM users WHERE id IN (?, ?)`, 1, 2).Scan(&cnt); err != nil || cnt != 2 {
+		t.Fatalf("IN 查询: cnt=%d err=%v", cnt, err)
+	}
+	if err := db.QueryRow(`SELECT COUNT(*) FROM users WHERE NOT (id = ?)`, 1).Scan(&cnt); err != nil || cnt != 1 {
+		t.Fatalf("NOT 查询: cnt=%d err=%v", cnt, err)
+	}
+
+	// M2: IS NULL (email 为 NULL 的 bob)
+	if err := db.QueryRow(`SELECT COUNT(*) FROM users WHERE email IS NULL`).Scan(&cnt); err != nil || cnt != 1 {
+		t.Fatalf("IS NULL: cnt=%d err=%v", cnt, err)
+	}
+	if err := db.QueryRow(`SELECT COUNT(*) FROM users WHERE email IS NOT NULL`).Scan(&cnt); err != nil || cnt != 1 {
+		t.Fatalf("IS NOT NULL: cnt=%d err=%v", cnt, err)
+	}
+
+	// M2: UPDATE (RowsAffected + 唯一冲突传播)
+	res, err = db.Exec(`UPDATE users SET age = ? WHERE name = ?`, 31, "alice")
+	if err != nil {
+		t.Fatalf("update: %v", err)
+	}
+	if n, _ := res.RowsAffected(); n != 1 {
+		t.Fatalf("update affected=%d", n)
+	}
+	var age int64
+	if err := db.QueryRow(`SELECT age FROM users WHERE id = ?`, 1).Scan(&age); err != nil || age != 31 {
+		t.Fatalf("update 未生效: age=%d err=%v", age, err)
+	}
+
+	// M2: DELETE
+	res, err = db.Exec(`DELETE FROM users WHERE id = ?`, 2)
+	if err != nil {
+		t.Fatalf("delete: %v", err)
+	}
+	if n, _ := res.RowsAffected(); n != 1 {
+		t.Fatalf("delete affected=%d", n)
+	}
+
+	// M3: 聚合 SUM/MIN/MAX/AVG
+	var sum float64
+	if err := db.QueryRow(`SELECT SUM(age) FROM users`).Scan(&sum); err != nil || sum != 31 {
+		t.Fatalf("SUM: %v %v", sum, err)
+	}
+	var avg float64
+	if err := db.QueryRow(`SELECT AVG(age) FROM users`).Scan(&avg); err != nil || avg != 31 {
+		t.Fatalf("AVG: %v %v", avg, err)
+	}
+
+	// Prepare 路径 (database/sql stmt)
+	stmt, err := db.Prepare(`SELECT name FROM users WHERE id = ?`)
+	if err != nil {
+		t.Fatalf("prepare: %v", err)
+	}
+	defer stmt.Close()
+	var nm string
+	if err := stmt.QueryRow(1).Scan(&nm); err != nil || nm != "alice" {
+		t.Fatalf("prepared query: %q %v", nm, err)
+	}
+}
+
+func TestDriverTransaction(t *testing.T) {
+	db := openTestDB(t)
+	if _, err := db.Exec(`CREATE TABLE t (id BIGINT PRIMARY KEY, v INT)`); err != nil {
+		t.Fatalf("create: %v", err)
+	}
+	tx, err := db.Begin()
+	if err != nil {
+		t.Fatalf("begin: %v", err)
+	}
+	if _, err := tx.Exec(`INSERT INTO t (id, v) VALUES (?, ?)`, 1, 100); err != nil {
+		t.Fatalf("tx insert: %v", err)
+	}
+	if err := tx.Rollback(); err != nil {
+		t.Fatalf("rollback: %v", err)
+	}
+	var cnt int
+	if err := db.QueryRow(`SELECT COUNT(*) FROM t`).Scan(&cnt); err != nil || cnt != 0 {
+		t.Fatalf("rollback 后应有 0 行, 实际 %d (%v)", cnt, err)
+	}
+
+	tx2, err := db.Begin()
+	if err != nil {
+		t.Fatalf("begin2: %v", err)
+	}
+	if _, err := tx2.Exec(`INSERT INTO t (id, v) VALUES (?, ?)`, 2, 200); err != nil {
+		t.Fatalf("tx2 insert: %v", err)
+	}
+	if err := tx2.Commit(); err != nil {
+		t.Fatalf("commit: %v", err)
+	}
+	if err := db.QueryRow(`SELECT COUNT(*) FROM t`).Scan(&cnt); err != nil || cnt != 1 {
+		t.Fatalf("commit 后应有 1 行, 实际 %d (%v)", cnt, err)
+	}
+}
+
+func TestDriverUnsupportedAndErrors(t *testing.T) {
+	db := openTestDB(t)
+	if _, err := db.Exec(`CREATE TABLE t (id BIGINT PRIMARY KEY, v INT)`); err != nil {
+		t.Fatalf("create: %v", err)
+	}
+	// 不支持语法: 明确报错而非静默
+	if _, err := db.Query(`SELECT * FROM t GROUP BY v`); err == nil {
+		t.Fatalf("GROUP BY 应报错")
+	}
+	if _, err := db.Exec(`DROP TABLE t`); err != nil {
+		t.Fatalf("drop: %v", err)
+	}
+	// IF EXISTS
+	if _, err := db.Exec(`DROP TABLE IF EXISTS t`); err != nil {
+		t.Fatalf("drop if exists: %v", err)
+	}
+	// SELECT 走 Exec / INSERT 走 Query 的错路提示
+	if _, err := db.Exec(`SELECT * FROM t`); err == nil {
+		t.Fatalf("SELECT 用 Exec 应提示错路")
+	}
+}
+
+// TestDriverSharedEngine 同一路径的两个 *sql.DB 共享同一引擎实例
+// (注册表语义): 经 db1 建表, db2 立即可见。
+func TestDriverSharedEngine(t *testing.T) {
+	dsn := filepath.Join(t.TempDir(), "shared.db")
+	db1, err := sql.Open("vrtx", dsn)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer db1.Close()
+	db2, err := sql.Open("vrtx", dsn)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer db2.Close()
+
+	if _, err := db1.Exec(`CREATE TABLE s (id BIGINT PRIMARY KEY)`); err != nil {
+		t.Fatalf("db1 create: %v", err)
+	}
+	if _, err := db2.Exec(`INSERT INTO s (id) VALUES (?)`, 1); err != nil {
+		t.Fatalf("db2 insert (db1 建的表不可见 = 引擎没共享): %v", err)
+	}
+	_ = os.Remove(dsn)
+}
+
+// openCore 打开一个裸引擎供包内钻取 (不经 database/sql)。
+func openCore(t *testing.T) *core.DB {
+	t.Helper()
+	db, err := core.Open(t.TempDir() + "/exec_drill.db")
+	if err != nil {
+		t.Fatalf("open: %v", err)
+	}
+	t.Cleanup(func() { _ = db.Close() })
+	return db
+}

+ 670 - 0
DB/driver/exec.go

@@ -0,0 +1,670 @@
+package vrtxsql
+
+// 语句执行: AST → 引擎查询体系 (core.Query / Condition / Table)。
+// 调用约定: 进入本文件任一函数时 shared.mu 已由调用方持有。
+
+import (
+	"database/sql/driver"
+	"fmt"
+	"math"
+	"strings"
+	"time"
+
+	"git.ycl.pub/yl32940/TsDB2/DB/core"
+	"git.ycl.pub/yl32940/TsDB2/DB/core/page"
+	"git.ycl.pub/yl32940/TsDB2/DB/core/types"
+)
+
+// execStatement 执行非查询语句 (INSERT/UPDATE/DELETE/DDL)。
+func execStatement(db *core.DB, st *statement, args []driver.NamedValue) (driver.Result, error) {
+	switch st.kind {
+	case kInsert:
+		return execInsert(db, st.ins, args)
+	case kUpdate:
+		return execUpdate(db, st.upd, args)
+	case kDelete:
+		return execDelete(db, st.del, args)
+	case kCreate:
+		return execCreate(db, st.crt)
+	case kCreateIndex:
+		return execCreateIndex(db, st.cidx)
+	case kDrop:
+		return execDrop(db, st.drop)
+	default:
+		return nil, fmt.Errorf("vrtx: 该语句不能用 Exec 执行")
+	}
+}
+
+func execInsert(db *core.DB, ins *insertStmt, args []driver.NamedValue) (driver.Result, error) {
+	tbl, err := findTable(db, ins.table)
+	if err != nil {
+		return nil, err
+	}
+	cols, _, err := resolveInsertColumns(tbl, ins.columns)
+	if err != nil {
+		return nil, err
+	}
+	// SQL INSERT 语义: 未提供的列 = NULL (可空列置 NULL 位, 非空列报错)。
+	// 引擎原生约定"未提供的列 = 零值"——驱动层把缺失列显式补成 nil,
+	// 由 serializeRow 按 FlagNullable 裁决。
+	provided := make(map[string]int, len(cols))
+	for i, c := range cols {
+		provided[strings.ToLower(c)] = i
+	}
+	allCols := activeColumnNames(tbl)
+	// 显式主键: 提供了主键列的整数值就走 InsertRowWithID (SQL 语义: 给定 id 就是行号);
+	// 未提供则走引擎自增 (InsertRow 分配的号会回填进主键镜像列)
+	pkCol := pkColumnName(tbl)
+	var pkFi *types.FieldInfo
+	if pkCol != "" {
+		pkFi = findField(tbl, pkCol)
+	}
+	var lastID int64
+	for _, row := range ins.rows {
+		if len(row) != len(cols) {
+			return nil, fmt.Errorf("INSERT 列数 (%d) 与值数 (%d) 不一致", len(cols), len(row))
+		}
+		fvs := make([]types.FieldValue, 0, len(allCols))
+		for _, cn := range allCols {
+			fi := findField(tbl, cn)
+			if pi, ok := provided[strings.ToLower(cn)]; ok {
+				fv, err := buildFieldValue(fi, row[pi], args)
+				if err != nil {
+					return nil, fmt.Errorf("列 %q: %w", cn, err)
+				}
+				if fv != nil {
+					fvs = append(fvs, *fv)
+					continue
+				}
+			}
+			// 未提供 / 显式 NULL: 显式写 nil (引擎置 NULL 位或按非空拒绝)
+			fvs = append(fvs, types.FieldValue{FieldName: fi.FieldName, Value: nil})
+		}
+		// 主键判定
+		var explicitPK *uint64
+		if pkFi != nil {
+			if pi, ok := provided[strings.ToLower(pkCol)]; ok {
+				if _, isNull := row[pi].(nullValue); isNull {
+					return nil, fmt.Errorf("主键列 %q 不能为 NULL", pkCol)
+				}
+				v, err := resolveValue(pkFi, row[pi], args)
+				if err != nil {
+					return nil, fmt.Errorf("主键列 %q: %w", pkCol, err)
+				}
+				n, ok := v.(int64)
+				if !ok {
+					return nil, fmt.Errorf("主键列 %q 需要整数值, 实际 %T", pkCol, v)
+				}
+				if n < 0 {
+					return nil, fmt.Errorf("主键列 %q 不能为负: %d", pkCol, n)
+				}
+				u := uint64(n)
+				explicitPK = &u
+			}
+		}
+		var id uint64
+		if explicitPK != nil {
+			id, err = tbl.InsertRowWithID(*explicitPK, fvs)
+		} else {
+			id, err = tbl.InsertRow(fvs)
+		}
+		if err != nil {
+			return nil, err
+		}
+		if id > math.MaxInt64 {
+			return nil, fmt.Errorf("pkID %d 超出 int64 表示范围", id)
+		}
+		lastID = int64(id)
+	}
+	return &result{lastID: lastID, affected: int64(len(ins.rows))}, nil
+}
+
+// pkColumnName 返回行存储索引 (主键/时序) 的第一个分量列名; 无行存储返回 ""。
+func pkColumnName(tbl *core.Table) string {
+	meta, ok := tbl.IndexMeta().GetRowStoreIndex()
+	if !ok {
+		return ""
+	}
+	fields := meta.Fields()
+	if len(fields) == 0 {
+		return ""
+	}
+	idx := int(fields[0])
+	if idx < 0 || idx >= len(tbl.Field) {
+		return ""
+	}
+	return string(tbl.Field[idx].FieldName)
+}
+
+func execUpdate(db *core.DB, upd *updateStmt, args []driver.NamedValue) (driver.Result, error) {
+	tbl, err := findTable(db, upd.table)
+	if err != nil {
+		return nil, err
+	}
+	cond, err := translateWhere(tbl, upd.where, args)
+	if err != nil {
+		return nil, err
+	}
+	sets := make([]types.FieldValue, 0, len(upd.sets))
+	for _, set := range upd.sets {
+		fi := findField(tbl, set.col)
+		if fi == nil {
+			return nil, fmt.Errorf("列 %q 不存在", set.col)
+		}
+		fv, err := buildFieldValue(fi, set.val, args)
+		if err != nil {
+			return nil, fmt.Errorf("SET 列 %q: %w", set.col, err)
+		}
+		if fv != nil {
+			sets = append(sets, *fv)
+		}
+	}
+	// WHERE → pkID 集合 → 逐行更新 (引擎的 UpdateRowByPK 未提供的列沿用旧值)
+	q := tbl.Query()
+	if cond != nil {
+		q = q.Where(cond)
+	}
+	var pkIDs []uint64
+	err = q.ScanWithPKID(func(pkID uint64, row []types.FieldValue) bool {
+		pkIDs = append(pkIDs, pkID)
+		return true
+	})
+	if err != nil {
+		return nil, err
+	}
+	for _, pkID := range pkIDs {
+		if err := tbl.UpdateRowByPK(pkID, sets); err != nil {
+			return nil, fmt.Errorf("更新 pkID=%d: %w", pkID, err)
+		}
+	}
+	return &result{affected: int64(len(pkIDs))}, nil
+}
+
+func execDelete(db *core.DB, del *deleteStmt, args []driver.NamedValue) (driver.Result, error) {
+	tbl, err := findTable(db, del.table)
+	if err != nil {
+		return nil, err
+	}
+	cond, err := translateWhere(tbl, del.where, args)
+	if err != nil {
+		return nil, err
+	}
+	q := tbl.Query()
+	if cond != nil {
+		q = q.Where(cond)
+	}
+	var pkIDs []uint64
+	err = q.ScanWithPKID(func(pkID uint64, row []types.FieldValue) bool {
+		pkIDs = append(pkIDs, pkID)
+		return true
+	})
+	if err != nil {
+		return nil, err
+	}
+	for _, pkID := range pkIDs {
+		if err := tbl.DeleteRowByPK(pkID); err != nil {
+			return nil, fmt.Errorf("删除 pkID=%d: %w", pkID, err)
+		}
+	}
+	return &result{affected: int64(len(pkIDs))}, nil
+}
+
+// execCreate CREATE TABLE → 引擎建表 + 主键/唯一/普通索引。
+func execCreate(db *core.DB, crt *createStmt) (driver.Result, error) {
+	if _, ok := db.FindTable(crt.table); ok {
+		if crt.ifNotExists {
+			return &result{}, nil
+		}
+		return nil, fmt.Errorf("表 %q 已存在", crt.table)
+	}
+	infos := make([]types.FieldInfo, 0, len(crt.columns))
+	for _, c := range crt.columns {
+		fi, err := sqlTypeToFieldInfo(c)
+		if err != nil {
+			return nil, fmt.Errorf("列 %q: %w", c.name, err)
+		}
+		infos = append(infos, fi)
+	}
+	tbl, err := db.CreateTable(crt.table, infos)
+	if err != nil {
+		return nil, err
+	}
+	// 主键索引 (整型列, 引擎行存储要求)
+	if crt.pkCol != "" {
+		fi := findField(tbl, crt.pkCol)
+		if fi == nil {
+			return nil, fmt.Errorf("PRIMARY KEY 列 %q 不存在", crt.pkCol)
+		}
+		if !isIntType(fi.FieldType) {
+			return nil, fmt.Errorf("PRIMARY KEY 列 %q 必须是整数类型 (引擎行存储定位键)", crt.pkCol)
+		}
+		if _, err := tbl.CreateIndex("pk_"+crt.table, page.IndexTypePrimary, []byte{byte(fieldIndex(tbl, crt.pkCol))}); err != nil {
+			return nil, err
+		}
+	}
+	// 列级 UNIQUE → 唯一二级索引
+	for _, c := range crt.columns {
+		if !c.unique {
+			continue
+		}
+		idx := inlineIndex{name: "ux_" + crt.table + "_" + c.name, columns: []string{c.name}, unique: true}
+		crt.indexes = append(crt.indexes, idx)
+	}
+	// 内联索引 (KEY/UNIQUE KEY)
+	for _, idx := range crt.indexes {
+		fi := findField(tbl, idx.columns[0])
+		if fi == nil {
+			return nil, fmt.Errorf("索引 %q 的列 %q 不存在", idx.name, idx.columns[0])
+		}
+		if fi.IsDropped() || types.IsDynamic(fi.FieldType) || types.IsBlob(fi.FieldType) {
+			return nil, fmt.Errorf("索引 %q 的列 %q 不能建索引 (动态/二进制列)", idx.name, idx.columns[0])
+		}
+		name := idx.name
+		if name == "" {
+			name = "idx_" + crt.table + "_" + idx.columns[0]
+		}
+		if _, err := tbl.CreateIndex(name, page.IndexTypeBTree, []byte{byte(fieldIndex(tbl, idx.columns[0]))}, idx.unique); err != nil {
+			return nil, err
+		}
+	}
+	return &result{}, nil
+}
+
+// execCreateIndex CREATE [UNIQUE] INDEX name ON t (col)
+func execCreateIndex(db *core.DB, ci *createIndexStmt) (driver.Result, error) {
+	tbl, err := findTable(db, ci.table)
+	if err != nil {
+		return nil, err
+	}
+	fi := findField(tbl, ci.col)
+	if fi == nil {
+		return nil, fmt.Errorf("列 %q 不存在", ci.col)
+	}
+	if _, err := tbl.CreateIndex(ci.name, page.IndexTypeBTree, []byte{byte(fieldIndex(tbl, ci.col))}, ci.unique); err != nil {
+		return nil, err
+	}
+	return &result{}, nil
+}
+
+func execDrop(db *core.DB, d *dropStmt) (driver.Result, error) {
+	if _, ok := db.FindTable(d.table); !ok {
+		if d.ifExists {
+			return &result{}, nil
+		}
+		return nil, fmt.Errorf("表 %q 不存在", d.table)
+	}
+	if err := db.DropTable(d.table); err != nil {
+		return nil, err
+	}
+	return &result{}, nil
+}
+
+// queryStatement 执行 SELECT, 返回物化结果集。
+func queryStatement(db *core.DB, sel *selectStmt, args []driver.NamedValue) (driver.Rows, error) {
+	tbl, err := findTable(db, sel.table)
+	if err != nil {
+		return nil, err
+	}
+	// 聚合: SELECT COUNT(*)/SUM/... FROM t [WHERE ...]
+	if sel.agg != nil {
+		return queryAggregate(tbl, sel, args)
+	}
+	// 列清单: * = 全部活跃列; 否则按请求列
+	var colNames []string
+	if len(sel.columns) == 0 {
+		colNames = activeColumnNames(tbl)
+	} else {
+		colNames = make([]string, 0, len(sel.columns))
+		for _, c := range sel.columns {
+			if findField(tbl, c) == nil {
+				return nil, fmt.Errorf("列 %q 不存在", c)
+			}
+			colNames = append(colNames, c)
+		}
+	}
+	q := tbl.Query().Select(colNames...)
+	cond, err := translateWhere(tbl, sel.where, args)
+	if err != nil {
+		return nil, err
+	}
+	if cond != nil {
+		q = q.Where(cond)
+	}
+	for _, o := range sel.orderBy {
+		q = q.OrderBy(o.col, o.desc)
+	}
+	if sel.limit != nil {
+		q = q.Limit(int(*sel.limit))
+	}
+	if sel.offset != nil {
+		q = q.Offset(int(*sel.offset))
+	}
+	found, err := q.Find()
+	if err != nil {
+		return nil, err
+	}
+	data := make([][]driver.Value, 0, len(found))
+	for _, row := range found {
+		vals := make([]driver.Value, 0, len(row))
+		for _, fv := range row {
+			dv, err := fieldValueToDriver(fv.Value)
+			if err != nil {
+				return nil, err
+			}
+			vals = append(vals, dv)
+		}
+		data = append(data, vals)
+	}
+	return &rows{cols: colNames, data: data}, nil
+}
+
+func queryAggregate(tbl *core.Table, sel *selectStmt, args []driver.NamedValue) (driver.Rows, error) {
+	cond, err := translateWhere(tbl, sel.where, args)
+	if err != nil {
+		return nil, err
+	}
+	q := tbl.Query()
+	if cond != nil {
+		q = q.Where(cond)
+	}
+	col := sel.agg.col
+	var dv driver.Value
+	switch sel.agg.fn {
+	case "COUNT":
+		n, err := q.Count()
+		if err != nil {
+			return nil, err
+		}
+		dv = n
+	case "SUM":
+		v, err := q.Sum(col)
+		if err != nil {
+			return nil, err
+		}
+		dv = v
+	case "MIN":
+		v, err := q.Min(col)
+		if err != nil {
+			return nil, err
+		}
+		dv, err = fieldValueToDriver(v)
+		if err != nil {
+			return nil, err
+		}
+	case "MAX":
+		v, err := q.Max(col)
+		if err != nil {
+			return nil, err
+		}
+		dv, err = fieldValueToDriver(v)
+		if err != nil {
+			return nil, err
+		}
+	case "AVG":
+		s, err := q.Sum(col)
+		if err != nil {
+			return nil, err
+		}
+		// AVG 的分母是"该列非 NULL 的行数", 不是全表行数
+		qc := tbl.Query()
+		if cond != nil {
+			qc = qc.Where(cond)
+		}
+		n, err := qc.Where(core.IsNotNull(col)).Count()
+		if err != nil {
+			return nil, err
+		}
+		if n == 0 {
+			dv = nil // SQL: 无输入行 → NULL
+		} else {
+			dv = s / float64(n)
+		}
+	default:
+		return nil, fmt.Errorf("不支持的聚合函数 %q", sel.agg.fn)
+	}
+	name := strings.ToLower(sel.agg.fn)
+	if col != "" {
+		name += "_" + col
+	}
+	return &rows{cols: []string{name}, data: [][]driver.Value{{dv}}}, nil
+}
+
+// =============================================================================
+// WHERE 翻译: SQL 表达式 → core.Condition
+// =============================================================================
+
+func translateWhere(tbl *core.Table, e expr, args []driver.NamedValue) (core.Condition, error) {
+	if e == nil {
+		return nil, nil
+	}
+	switch t := e.(type) {
+	case *andExpr:
+		l, err := translateWhere(tbl, t.l, args)
+		if err != nil {
+			return nil, err
+		}
+		r, err := translateWhere(tbl, t.r, args)
+		if err != nil {
+			return nil, err
+		}
+		return core.And(l, r), nil
+	case *orExpr:
+		l, err := translateWhere(tbl, t.l, args)
+		if err != nil {
+			return nil, err
+		}
+		r, err := translateWhere(tbl, t.r, args)
+		if err != nil {
+			return nil, err
+		}
+		return core.Or(l, r), nil
+	case *notExpr:
+		inner, err := translateWhere(tbl, t.e, args)
+		if err != nil {
+			return nil, err
+		}
+		return core.Not(inner), nil
+	case *cmpExpr:
+		fi := findField(tbl, t.col)
+		if fi == nil {
+			return nil, fmt.Errorf("WHERE 列 %q 不存在", t.col)
+		}
+		v, err := resolveValue(fi, t.val, args)
+		if err != nil {
+			return nil, err
+		}
+		switch t.op {
+		case "=":
+			return core.Eq(t.col, v, fi.FieldType), nil
+		case "!=":
+			return core.Not(core.Eq(t.col, v, fi.FieldType)), nil
+		case ">":
+			return core.Gt(t.col, v, fi.FieldType), nil
+		case ">=":
+			return core.Ge(t.col, v, fi.FieldType), nil
+		case "<":
+			return core.Lt(t.col, v, fi.FieldType), nil
+		case "<=":
+			return core.Le(t.col, v, fi.FieldType), nil
+		}
+		return nil, fmt.Errorf("不支持的比较符 %q", t.op)
+	case *inExpr:
+		fi := findField(tbl, t.col)
+		if fi == nil {
+			return nil, fmt.Errorf("WHERE 列 %q 不存在", t.col)
+		}
+		vals := make([]interface{}, 0, len(t.vals))
+		for _, tv := range t.vals {
+			v, err := resolveValue(fi, tv, args)
+			if err != nil {
+				return nil, err
+			}
+			vals = append(vals, v)
+		}
+		c := core.In(t.col, vals, fi.FieldType)
+		if t.neg {
+			return core.Not(c), nil
+		}
+		return c, nil
+	case *isNullExpr:
+		// IS [NOT] NULL → 引擎的 NullCondition: 直接读行内 NULL 位图
+		// (与反序列化/索引提取同一口径; Eq 对 NULL 行一律不匹配, 表达不了 IS NULL)
+		if t.neg {
+			return core.IsNotNull(t.col), nil
+		}
+		return core.IsNull(t.col), nil
+	case *likeExpr:
+		fi := findField(tbl, t.col)
+		if fi == nil {
+			return nil, fmt.Errorf("WHERE 列 %q 不存在", t.col)
+		}
+		pat, err := resolveLikePattern(t.pattern, args)
+		if err != nil {
+			return nil, err
+		}
+		c := core.Like(t.col, pat)
+		if t.neg {
+			return core.Not(c), nil
+		}
+		return c, nil
+	}
+	return nil, fmt.Errorf("无法翻译的 WHERE 表达式")
+}
+
+// =============================================================================
+// 表/列/值解析辅助
+// =============================================================================
+
+func findTable(db *core.DB, name string) (*core.Table, error) {
+	tbl, ok := db.FindTable(name)
+	if !ok {
+		return nil, fmt.Errorf("表 %q 不存在", name)
+	}
+	return tbl, nil
+}
+
+// findField 按列名查活跃字段 (大小写不敏感)。
+func findField(tbl *core.Table, name string) *types.FieldInfo {
+	for i := range tbl.Field {
+		f := &tbl.Field[i]
+		if f.IsDropped() {
+			continue
+		}
+		if strings.EqualFold(string(f.FieldName), name) {
+			return f
+		}
+	}
+	return nil
+}
+
+func fieldIndex(tbl *core.Table, name string) int {
+	for i := range tbl.Field {
+		if !tbl.Field[i].IsDropped() && strings.EqualFold(string(tbl.Field[i].FieldName), name) {
+			return i
+		}
+	}
+	return -1
+}
+
+func activeColumnNames(tbl *core.Table) []string {
+	out := make([]string, 0, len(tbl.Field))
+	for i := range tbl.Field {
+		if !tbl.Field[i].IsDropped() {
+			out = append(out, string(tbl.Field[i].FieldName))
+		}
+	}
+	return out
+}
+
+func resolveInsertColumns(tbl *core.Table, columns []string) ([]string, []*types.FieldInfo, error) {
+	if len(columns) == 0 {
+		names := activeColumnNames(tbl)
+		infos := make([]*types.FieldInfo, 0, len(names))
+		for _, n := range names {
+			infos = append(infos, findField(tbl, n))
+		}
+		return names, infos, nil
+	}
+	infos := make([]*types.FieldInfo, 0, len(columns))
+	for _, c := range columns {
+		fi := findField(tbl, c)
+		if fi == nil {
+			return nil, nil, fmt.Errorf("列 %q 不存在", c)
+		}
+		infos = append(infos, fi)
+	}
+	return columns, infos, nil
+}
+
+// resolveValue 把 AST 值 (占位/字面量/字符串/NULL) 解析成引擎值。
+// 字面量按列类型解释 (数字 → int64/float64; 字符串按列类型编码)。
+func resolveValue(fi *types.FieldInfo, v value, args []driver.NamedValue) (interface{}, error) {
+	switch t := v.(type) {
+	case argValue:
+		if t.idx >= len(args) {
+			return nil, fmt.Errorf("占位符 ?%d 没有对应参数 (共 %d 个)", t.idx+1, len(args))
+		}
+		return driverToEngineValue(fi, args[t.idx].Value)
+	case strValue:
+		return parseLiteralAsType(fi, t.s)
+	case litValue:
+		return parseLiteralAsType(fi, t.raw)
+	case nullValue:
+		return nil, nil
+	}
+	return nil, fmt.Errorf("无法解析的值")
+}
+
+// buildFieldValue 构造 FieldValue; NULL 返回 nil (调用方跳过, 由引擎按可空/非空裁决)。
+func buildFieldValue(fi *types.FieldInfo, v value, args []driver.NamedValue) (*types.FieldValue, error) {
+	if _, isNull := v.(nullValue); isNull {
+		return nil, nil
+	}
+	ev, err := resolveValue(fi, v, args)
+	if err != nil {
+		return nil, err
+	}
+	return &types.FieldValue{FieldName: fi.FieldName, Value: ev}, nil
+}
+
+func resolveLikePattern(v value, args []driver.NamedValue) (string, error) {
+	switch t := v.(type) {
+	case strValue:
+		return t.s, nil
+	case argValue:
+		if t.idx >= len(args) {
+			return "", fmt.Errorf("LIKE 的占位符没有对应参数")
+		}
+		s, ok := args[t.idx].Value.(string)
+		if !ok {
+			if b, ok2 := args[t.idx].Value.([]byte); ok2 {
+				return string(b), nil
+			}
+			return "", fmt.Errorf("LIKE 参数必须是字符串")
+		}
+		return s, nil
+	}
+	return "", fmt.Errorf("LIKE 模式必须是字符串字面量或占位符")
+}
+
+func isIntType(ft types.FieldType) bool {
+	switch ft {
+	case types.FieldType_Int64, types.FieldType_Uint64,
+		types.FieldType_Int32, types.FieldType_Uint32,
+		types.FieldType_Int16, types.FieldType_Uint16,
+		types.FieldType_Int8, types.FieldType_Uint8:
+		return true
+	}
+	return false
+}
+
+// 时间戳约定: driver 的 time.Time 与引擎 uint64 毫秒互转。
+var unixEpoch = time.Unix(0, 0)
+
+func timeToMillis(t time.Time) uint64 {
+	return uint64(t.Sub(unixEpoch).Milliseconds())
+}
+
+func millisToTime(ms uint64) time.Time {
+	return unixEpoch.Add(time.Duration(ms) * time.Millisecond)
+}

+ 1168 - 0
DB/driver/parser.go

@@ -0,0 +1,1168 @@
+// Package vrtxsql — TsDB2 的 database/sql 驱动。
+//
+// 让任意基于 database/sql 的工具/ORM (xorm、sqlx 以及自研 ORM) 直接对接
+// VRTX 引擎: 标准 SQL 文本 → 引擎查询体系 (core.Query/Condition/Table)。
+//
+// 解析器是刻意收窄的【ORM 子集翻译器】, 不是通用 SQL 引擎:
+//
+//	SELECT cols|* FROM t [WHERE ...] [ORDER BY c [ASC|DESC], ...] [LIMIT n] [OFFSET n]
+//	SELECT COUNT(*)|COUNT/SUM/MIN/MAX/AVG(col) FROM t [WHERE ...]          (无 GROUP BY)
+//	INSERT INTO t [(cols)] VALUES (...),(...)
+//	UPDATE t SET c=v,... [WHERE ...]
+//	DELETE FROM t [WHERE ...]
+//	CREATE TABLE [IF NOT EXISTS] t (c TYPE[(arg)] [NULL|NOT NULL] [UNIQUE] ...,
+//	                                [PRIMARY KEY (c)], [KEY k (c)], [UNIQUE KEY k (c)])
+//	DROP TABLE [IF EXISTS] t
+//
+//	WHERE 支持: = != <> < <= > >= AND OR NOT (..) IN (..) IS [NOT] NULL LIKE。
+//	不支持: JOIN/子查询/GROUP BY/HAVING/函数表达式/DEFAULT 非 NULL 值 —— 遇到
+//	返回明确错误而不是静默做错。
+package vrtxsql
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+// =============================================================================
+// AST
+// =============================================================================
+
+type stmtKind int
+
+const (
+	kSelect stmtKind = iota + 1
+	kInsert
+	kUpdate
+	kDelete
+	kCreate
+	kCreateIndex
+	kDrop
+)
+
+type statement struct {
+	kind stmtKind
+	sel  *selectStmt
+	ins  *insertStmt
+	upd  *updateStmt
+	del  *deleteStmt
+	crt  *createStmt
+	cidx *createIndexStmt
+	drop *dropStmt
+}
+
+func (s *statement) argCount() int {
+	n := 0
+	switch s.kind {
+	case kSelect:
+		n = exprMaxArg(s.sel.where)
+	case kInsert:
+		for _, row := range s.ins.rows {
+			for _, v := range row {
+				if a, ok := v.(argValue); ok && a.idx+1 > n {
+					n = a.idx + 1
+				}
+			}
+		}
+	case kUpdate:
+		for _, set := range s.upd.sets {
+			if a, ok := set.val.(argValue); ok && a.idx+1 > n {
+				n = a.idx + 1
+			}
+		}
+		n = max2(n, exprMaxArg(s.upd.where))
+	case kDelete:
+		n = exprMaxArg(s.del.where)
+	}
+	return n
+}
+
+func exprMaxArg(e expr) int {
+	if e == nil {
+		return 0
+	}
+	m := 0
+	switch t := e.(type) {
+	case *andExpr:
+		m = max2(exprMaxArg(t.l), exprMaxArg(t.r))
+	case *orExpr:
+		m = max2(exprMaxArg(t.l), exprMaxArg(t.r))
+	case *notExpr:
+		m = exprMaxArg(t.e)
+	case *cmpExpr:
+		if a, ok := t.val.(argValue); ok {
+			m = a.idx + 1
+		}
+	case *inExpr:
+		for _, v := range t.vals {
+			if a, ok := v.(argValue); ok && a.idx+1 > m {
+				m = a.idx + 1
+			}
+		}
+	case *likeExpr:
+		if a, ok := t.pattern.(argValue); ok {
+			m = a.idx + 1
+		}
+	}
+	return m
+}
+
+func max2(a, b int) int {
+	if a > b {
+		return a
+	}
+	return b
+}
+
+type selectStmt struct {
+	table   string
+	columns []string // 空 = *
+	agg     *aggSpec // 非空 = 单行聚合
+	where   expr
+	orderBy []orderSpec
+	limit   *int64
+	offset  *int64
+}
+
+type aggSpec struct {
+	fn  string // COUNT / SUM / MIN / MAX / AVG
+	col string // COUNT(*) 时为空
+}
+
+type orderSpec struct {
+	col  string
+	desc bool
+}
+
+type insertStmt struct {
+	table   string
+	columns []string // 空 = 按建表列序
+	rows    [][]value
+}
+
+type updateStmt struct {
+	table string
+	sets  []setClause
+	where expr
+}
+
+type setClause struct {
+	col string
+	val value
+}
+
+type deleteStmt struct {
+	table string
+	where expr
+}
+
+type createStmt struct {
+	table       string
+	ifNotExists bool
+	columns     []colDef
+	pkCol       string // PRIMARY KEY (c) 或列级 PRIMARY KEY
+	indexes     []inlineIndex
+}
+
+type colDef struct {
+	name     string
+	typeName string
+	arg      int // VARCHAR(n)/DECIMAL(p,s→取s) 的参数, 无则 -1
+	notNull  bool
+	unique   bool
+	pk       bool // 列级 PRIMARY KEY
+}
+
+type inlineIndex struct {
+	name    string
+	columns []string
+	unique  bool
+}
+
+type dropStmt struct {
+	table    string
+	ifExists bool
+}
+
+// createIndexStmt CREATE [UNIQUE] INDEX name ON t (col)
+type createIndexStmt struct {
+	unique bool
+	name   string
+	table  string
+	col    string
+}
+
+// ---- WHERE 表达式 ----
+
+type expr interface{ isExpr() }
+
+type andExpr struct{ l, r expr }
+type orExpr struct{ l, r expr }
+type notExpr struct{ e expr }
+
+type cmpExpr struct {
+	col string
+	op  string // = != < <= > >=
+	val value
+}
+
+type inExpr struct {
+	col  string
+	vals []value
+	neg  bool
+}
+
+type isNullExpr struct {
+	col string
+	neg bool
+}
+
+type likeExpr struct {
+	col     string
+	pattern value
+	neg     bool
+}
+
+func (*andExpr) isExpr()     {}
+func (*orExpr) isExpr()      {}
+func (*notExpr) isExpr()     {}
+func (*cmpExpr) isExpr()     {}
+func (*inExpr) isExpr()      {}
+func (*isNullExpr) isExpr()  {}
+func (*likeExpr) isExpr()    {}
+
+// ---- 值 ----
+
+type value interface{ isValue() }
+
+type argValue struct{ idx int } // ? 占位 → args[idx]
+type litValue struct{ raw string }
+type strValue struct{ s string }
+type nullValue struct{}
+
+func (argValue) isValue() {}
+func (litValue) isValue() {}
+func (strValue) isValue() {}
+func (nullValue) isValue() {}
+
+// =============================================================================
+// 词法分析
+// =============================================================================
+
+type tokenKind int
+
+const (
+	tkEOF tokenKind = iota + 1
+	tkIdent
+	tkNumber
+	tkString
+	tkArg // ?
+	tkPunct // , ( ) * = != <> < <= > >=
+	tkKeyword
+)
+
+type token struct {
+	kind tokenKind
+	text string // 原文 (标识符保留原大小写, 关键字统一大写)
+}
+
+var keywords = map[string]bool{
+	"SELECT": true, "FROM": true, "WHERE": true, "AND": true, "OR": true,
+	"NOT": true, "IN": true, "LIKE": true, "IS": true, "NULL": true,
+	"ORDER": true, "BY": true, "ASC": true, "DESC": true,
+	"LIMIT": true, "OFFSET": true,
+	"INSERT": true, "INTO": true, "VALUES": true, "VALUE": true,
+	"UPDATE": true, "SET": true, "DELETE": true,
+	"CREATE": true, "TABLE": true, "DROP": true, "IF": true, "EXISTS": true,
+	"PRIMARY": true, "KEY": true, "UNIQUE": true,
+	"INDEX": true, "ON": true,
+	"COUNT": true, "SUM": true, "MIN": true, "MAX": true, "AVG": true,
+	"AS": true,
+}
+
+func tokenize(sqlText string) ([]token, error) {
+	var toks []token
+	i, n := 0, len(sqlText)
+	for i < n {
+		c := sqlText[i]
+		switch {
+		case c == ' ' || c == '\t' || c == '\n' || c == '\r':
+			i++
+		case c == '-' && i+1 < n && sqlText[i+1] == '-': // 行注释
+			for i < n && sqlText[i] != '\n' {
+				i++
+			}
+		case c == '/' && i+1 < n && sqlText[i+1] == '*': // 块注释
+			j := strings.Index(sqlText[i+2:], "*/")
+			if j < 0 {
+				return nil, fmt.Errorf("未闭合的块注释")
+			}
+			i += 2 + j + 2
+		case c == '\'' || c == '"' || c == '`':
+			quote := c
+			i++
+			var sb strings.Builder
+			closed := false
+			for i < n {
+				if sqlText[i] == quote {
+					// '' 转义
+					if i+1 < n && sqlText[i+1] == quote {
+						sb.WriteByte(quote)
+						i += 2
+						continue
+					}
+					i++
+					closed = true
+					break
+				}
+				sb.WriteByte(sqlText[i])
+				i++
+			}
+			if !closed {
+				return nil, fmt.Errorf("未闭合的字符串")
+			}
+			kind := tkString
+			if quote == '`' {
+				kind = tkIdent
+			}
+			toks = append(toks, token{kind, sb.String()})
+		case c == '?' :
+			toks = append(toks, token{tkArg, "?"})
+			i++
+		case isDigit(c):
+			j := i
+			for j < n && (isDigit(sqlText[j]) || sqlText[j] == '.') {
+				j++
+			}
+			toks = append(toks, token{tkNumber, sqlText[i:j]})
+			i = j
+		case isIdentStart(c):
+			j := i
+			for j < n && (isIdentChar(sqlText[j])) {
+				j++
+			}
+			word := sqlText[i:j]
+			up := strings.ToUpper(word)
+			if keywords[up] {
+				toks = append(toks, token{tkKeyword, up})
+			} else {
+				toks = append(toks, token{tkIdent, word})
+			}
+			i = j
+		case c == ',':
+			toks = append(toks, token{tkPunct, ","})
+			i++
+		case c == '(':
+			toks = append(toks, token{tkPunct, "("})
+			i++
+		case c == ')':
+			toks = append(toks, token{tkPunct, ")"})
+			i++
+		case c == '*':
+			toks = append(toks, token{tkPunct, "*"})
+			i++
+		case c == '=':
+			toks = append(toks, token{tkPunct, "="})
+			i++
+		case c == '!':
+			if i+1 < n && sqlText[i+1] == '=' {
+				toks = append(toks, token{tkPunct, "!="})
+				i += 2
+			} else {
+				return nil, fmt.Errorf("意外的字符 '!'")
+			}
+		case c == '<':
+			if i+1 < n && sqlText[i+1] == '=' {
+				toks = append(toks, token{tkPunct, "<="})
+				i += 2
+			} else if i+1 < n && sqlText[i+1] == '>' {
+				toks = append(toks, token{tkPunct, "<>"})
+				i += 2
+			} else {
+				toks = append(toks, token{tkPunct, "<"})
+				i++
+			}
+		case c == '>':
+			if i+1 < n && sqlText[i+1] == '=' {
+				toks = append(toks, token{tkPunct, ">="})
+				i += 2
+			} else {
+				toks = append(toks, token{tkPunct, ">"})
+				i++
+			}
+		default:
+			return nil, fmt.Errorf("无法识别的字符 %q", string(c))
+		}
+	}
+	toks = append(toks, token{tkEOF, ""})
+	return toks, nil
+}
+
+func isDigit(c byte) bool { return c >= '0' && c <= '9' }
+func isIdentStart(c byte) bool {
+	return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c >= 0x80
+}
+func isIdentChar(c byte) bool { return isIdentStart(c) || isDigit(c) }
+
+// =============================================================================
+// 语法分析 (递归下降, ORM 子集)
+// =============================================================================
+
+type parser struct {
+	toks   []token
+	pos    int
+	argSeq int // ? 占位符序号分配器 (每条语句从 0 开始)
+}
+
+func parse(sqlText string) (*statement, error) {
+	toks, err := tokenize(sqlText)
+	if err != nil {
+		return nil, err
+	}
+	p := &parser{toks: toks}
+	st, err := p.parseStatement()
+	if err != nil {
+		return nil, err
+	}
+	if p.peek().kind != tkEOF {
+		return nil, fmt.Errorf("语句末尾有多余内容: %q", p.peek().text)
+	}
+	return st, nil
+}
+
+func (p *parser) peek() token  { return p.toks[p.pos] }
+func (p *parser) next() token  { t := p.toks[p.pos]; p.pos++; return t }
+func (p *parser) backup()      { p.pos-- }
+
+func (p *parser) expectKeyword(kw string) error {
+	t := p.next()
+	if t.kind != tkKeyword || t.text != kw {
+		return fmt.Errorf("期望 %s, 实际是 %q", kw, t.text)
+	}
+	return nil
+}
+
+func (p *parser) acceptKeyword(kw string) bool {
+	if p.peek().kind == tkKeyword && p.peek().text == kw {
+		p.pos++
+		return true
+	}
+	return false
+}
+
+func (p *parser) expectPunct(sym string) error {
+	t := p.next()
+	if t.kind != tkPunct || t.text != sym {
+		return fmt.Errorf("期望 %q, 实际是 %q", sym, t.text)
+	}
+	return nil
+}
+
+func (p *parser) acceptPunct(sym string) bool {
+	if p.peek().kind == tkPunct && p.peek().text == sym {
+		p.pos++
+		return true
+	}
+	return false
+}
+
+func (p *parser) expectIdent() (string, error) {
+	t := p.next()
+	if t.kind != tkIdent {
+		return "", fmt.Errorf("期望标识符, 实际是 %q", t.text)
+	}
+	return t.text, nil
+}
+
+func (p *parser) parseStatement() (*statement, error) {
+	t := p.peek()
+	if t.kind != tkKeyword {
+		return nil, fmt.Errorf("语句必须以 SELECT/INSERT/UPDATE/DELETE/CREATE/DROP 开头")
+	}
+	switch t.text {
+	case "SELECT":
+		return p.parseSelect()
+	case "INSERT":
+		return p.parseInsert()
+	case "UPDATE":
+		return p.parseUpdate()
+	case "DELETE":
+		return p.parseDelete()
+	case "CREATE":
+		return p.parseCreate()
+	case "DROP":
+		return p.parseDrop()
+	default:
+		return nil, fmt.Errorf("不支持的语句开头: %s", t.text)
+	}
+}
+
+// parseCreateIndex CREATE [UNIQUE] INDEX|KEY name ON t (col)
+func (p *parser) parseCreateIndex(unique bool) (*statement, error) {
+	t := p.next()
+	if t.kind != tkKeyword || (t.text != "INDEX" && t.text != "KEY") {
+		return nil, fmt.Errorf("期望 INDEX/KEY, 实际是 %q", t.text)
+	}
+	name, err := p.expectIdent()
+	if err != nil {
+		return nil, err
+	}
+	if err := p.expectKeyword("ON"); err != nil {
+		return nil, err
+	}
+	table, err := p.expectIdent()
+	if err != nil {
+		return nil, err
+	}
+	cols, err := p.parseParenCols()
+	if err != nil {
+		return nil, err
+	}
+	if len(cols) != 1 {
+		return nil, fmt.Errorf("CREATE INDEX 仅支持单列, 实际 %d 列", len(cols))
+	}
+	return &statement{kind: kCreateIndex, cidx: &createIndexStmt{
+		unique: unique, name: name, table: table, col: cols[0],
+	}}, nil
+}
+
+func (p *parser) parseSelect() (*statement, error) {
+	p.next() // SELECT
+	sel := &selectStmt{}
+
+	// SELECT 列表: * / 列名 / 聚合
+	if p.acceptPunct("*") {
+		// 纯 * 或 COUNT(*) 在聚合分支处理
+	} else if p.peek().kind == tkKeyword && isAggFn(p.peek().text) {
+		agg, err := p.parseAgg()
+		if err != nil {
+			return nil, err
+		}
+		sel.agg = agg
+	} else {
+		for {
+			col, err := p.expectIdent()
+			if err != nil {
+				return nil, err
+			}
+			sel.columns = append(sel.columns, col)
+			// 容忍 "col AS alias" (alias 丢弃, 引擎列名以表为准)
+			if p.acceptKeyword("AS") {
+				if _, err := p.expectIdent(); err != nil {
+					return nil, err
+				}
+			}
+			if !p.acceptPunct(",") {
+				break
+			}
+		}
+	}
+
+	if err := p.expectKeyword("FROM"); err != nil {
+		return nil, err
+	}
+	table, err := p.expectIdent()
+	if err != nil {
+		return nil, err
+	}
+	sel.table = table
+
+	if p.acceptKeyword("WHERE") {
+		e, err := p.parseExpr()
+		if err != nil {
+			return nil, err
+		}
+		sel.where = e
+	}
+	if p.acceptKeyword("GROUP") {
+		return nil, fmt.Errorf("GROUP BY 暂不支持")
+	}
+	if p.acceptKeyword("ORDER") {
+		if err := p.expectKeyword("BY"); err != nil {
+			return nil, err
+		}
+		for {
+			col, err := p.expectIdent()
+			if err != nil {
+				return nil, err
+			}
+			spec := orderSpec{col: col}
+			if p.acceptKeyword("DESC") {
+				spec.desc = true
+			} else {
+				p.acceptKeyword("ASC")
+			}
+			sel.orderBy = append(sel.orderBy, spec)
+			if !p.acceptPunct(",") {
+				break
+			}
+		}
+	}
+	if p.acceptKeyword("LIMIT") {
+		v, err := p.parseIntLiteral()
+		if err != nil {
+			return nil, err
+		}
+		sel.limit = &v
+	}
+	if p.acceptKeyword("OFFSET") {
+		v, err := p.parseIntLiteral()
+		if err != nil {
+			return nil, err
+		}
+		sel.offset = &v
+	}
+	return &statement{kind: kSelect, sel: sel}, nil
+}
+
+func isAggFn(w string) bool {
+	switch w {
+	case "COUNT", "SUM", "MIN", "MAX", "AVG":
+		return true
+	}
+	return false
+}
+
+func (p *parser) parseAgg() (*aggSpec, error) {
+	fn := p.next().text
+	spec := &aggSpec{fn: fn}
+	if err := p.expectPunct("("); err != nil {
+		return nil, err
+	}
+	if fn == "COUNT" && p.acceptPunct("*") {
+		spec.col = ""
+	} else {
+		col, err := p.expectIdent()
+		if err != nil {
+			return nil, err
+		}
+		spec.col = col
+	}
+	if err := p.expectPunct(")"); err != nil {
+		return nil, err
+	}
+	return spec, nil
+}
+
+func (p *parser) parseIntLiteral() (int64, error) {
+	t := p.next()
+	if t.kind == tkNumber {
+		v, err := strconv.ParseInt(t.text, 10, 64)
+		if err == nil {
+			return v, nil
+		}
+	}
+	return 0, fmt.Errorf("期望整数, 实际是 %q", t.text)
+}
+
+// parseExpr: OR → AND → NOT → 原子
+func (p *parser) parseExpr() (expr, error) { return p.parseOr() }
+
+func (p *parser) parseOr() (expr, error) {
+	left, err := p.parseAnd()
+	if err != nil {
+		return nil, err
+	}
+	for p.peek().kind == tkKeyword && p.peek().text == "OR" {
+		p.next()
+		right, err := p.parseAnd()
+		if err != nil {
+			return nil, err
+		}
+		left = &orExpr{l: left, r: right}
+	}
+	return left, nil
+}
+
+func (p *parser) parseAnd() (expr, error) {
+	left, err := p.parseNot()
+	if err != nil {
+		return nil, err
+	}
+	for p.peek().kind == tkKeyword && p.peek().text == "AND" {
+		p.next()
+		right, err := p.parseNot()
+		if err != nil {
+			return nil, err
+		}
+		left = &andExpr{l: left, r: right}
+	}
+	return left, nil
+}
+
+func (p *parser) parseNot() (expr, error) {
+	if p.acceptKeyword("NOT") {
+		e, err := p.parseNot()
+		if err != nil {
+			return nil, err
+		}
+		return &notExpr{e: e}, nil
+	}
+	return p.parseAtom()
+}
+
+func (p *parser) parseAtom() (expr, error) {
+	// ( expr )
+	if p.acceptPunct("(") {
+		e, err := p.parseExpr()
+		if err != nil {
+			return nil, err
+		}
+		if err := p.expectPunct(")"); err != nil {
+			return nil, err
+		}
+		return e, nil
+	}
+	col, err := p.expectIdent()
+	if err != nil {
+		return nil, err
+	}
+	// IS [NOT] NULL
+	if p.peek().kind == tkKeyword && p.peek().text == "IS" {
+		p.next()
+		neg := p.acceptKeyword("NOT")
+		if err := p.expectKeyword("NULL"); err != nil {
+			return nil, err
+		}
+		return &isNullExpr{col: col, neg: neg}, nil
+	}
+	// [NOT] IN (...)
+	neg := false
+	if p.peek().kind == tkKeyword && p.peek().text == "NOT" {
+		p.next()
+		neg = true
+	}
+	if p.acceptKeyword("IN") {
+		if err := p.expectPunct("("); err != nil {
+			return nil, err
+		}
+		var vals []value
+		for {
+			v, err := p.parseValue()
+			if err != nil {
+				return nil, err
+			}
+			vals = append(vals, v)
+			if !p.acceptPunct(",") {
+				break
+			}
+		}
+		if err := p.expectPunct(")"); err != nil {
+			return nil, err
+		}
+		return &inExpr{col: col, vals: vals, neg: neg}, nil
+	}
+	// [NOT] LIKE
+	if p.acceptKeyword("LIKE") {
+		pat, err := p.parseValue()
+		if err != nil {
+			return nil, err
+		}
+		return &likeExpr{col: col, pattern: pat, neg: neg}, nil
+	}
+	if neg {
+		return nil, fmt.Errorf("NOT 后面期望 IN 或 LIKE")
+	}
+	// 比较符
+	op := p.next()
+	if op.kind != tkPunct {
+		return nil, fmt.Errorf("列 %q 后期望比较符, 实际是 %q", col, op.text)
+	}
+	switch op.text {
+	case "=", "!=", "<>", "<", "<=", ">", ">=":
+	default:
+		return nil, fmt.Errorf("不支持的比较符 %q", op.text)
+	}
+	v, err := p.parseValue()
+	if err != nil {
+		return nil, err
+	}
+	if op.text == "<>" {
+		op.text = "!="
+	}
+	return &cmpExpr{col: col, op: op.text, val: v}, nil
+}
+
+func (p *parser) parseValue() (value, error) {
+	t := p.next()
+	switch t.kind {
+	case tkArg:
+		idx := p.argSeq
+		p.argSeq++
+		return argValue{idx: idx}, nil
+	case tkNumber:
+		return litValue{raw: t.text}, nil
+	case tkString:
+		return strValue{s: t.text}, nil
+	case tkKeyword:
+		if t.text == "NULL" {
+			return nullValue{}, nil
+		}
+	}
+	return nil, fmt.Errorf("无法解析的值: %q", t.text)
+}
+
+// =============================================================================
+// INSERT / UPDATE / DELETE / CREATE / DROP
+// =============================================================================
+
+func (p *parser) parseInsert() (*statement, error) {
+	p.next() // INSERT
+	if err := p.expectKeyword("INTO"); err != nil {
+		return nil, err
+	}
+	table, err := p.expectIdent()
+	if err != nil {
+		return nil, err
+	}
+	ins := &insertStmt{table: table}
+
+	if p.acceptPunct("(") {
+		for {
+			col, err := p.expectIdent()
+			if err != nil {
+				return nil, err
+			}
+			ins.columns = append(ins.columns, col)
+			if !p.acceptPunct(",") {
+				break
+			}
+		}
+		if err := p.expectPunct(")"); err != nil {
+			return nil, err
+		}
+	}
+	if !p.acceptKeyword("VALUES") {
+		if err := p.expectKeyword("VALUE"); err != nil {
+			return nil, err
+		}
+	}
+	for {
+		if err := p.expectPunct("("); err != nil {
+			return nil, err
+		}
+		var row []value
+		for {
+			v, err := p.parseValue()
+			if err != nil {
+				return nil, err
+			}
+			row = append(row, v)
+			if !p.acceptPunct(",") {
+				break
+			}
+		}
+		if err := p.expectPunct(")"); err != nil {
+			return nil, err
+		}
+		ins.rows = append(ins.rows, row)
+		if !p.acceptPunct(",") {
+			break
+		}
+	}
+	return &statement{kind: kInsert, ins: ins}, nil
+}
+
+func (p *parser) parseUpdate() (*statement, error) {
+	p.next() // UPDATE
+	table, err := p.expectIdent()
+	if err != nil {
+		return nil, err
+	}
+	if err := p.expectKeyword("SET"); err != nil {
+		return nil, err
+	}
+	upd := &updateStmt{table: table}
+	for {
+		col, err := p.expectIdent()
+		if err != nil {
+			return nil, err
+		}
+		if err := p.expectPunct("="); err != nil {
+			return nil, err
+		}
+		v, err := p.parseValue()
+		if err != nil {
+			return nil, err
+		}
+		upd.sets = append(upd.sets, setClause{col: col, val: v})
+		if !p.acceptPunct(",") {
+			break
+		}
+	}
+	if p.acceptKeyword("WHERE") {
+		e, err := p.parseExpr()
+		if err != nil {
+			return nil, err
+		}
+		upd.where = e
+	}
+	return &statement{kind: kUpdate, upd: upd}, nil
+}
+
+func (p *parser) parseDelete() (*statement, error) {
+	p.next() // DELETE
+	if err := p.expectKeyword("FROM"); err != nil {
+		return nil, err
+	}
+	table, err := p.expectIdent()
+	if err != nil {
+		return nil, err
+	}
+	del := &deleteStmt{table: table}
+	if p.acceptKeyword("WHERE") {
+		e, err := p.parseExpr()
+		if err != nil {
+			return nil, err
+		}
+		del.where = e
+	}
+	return &statement{kind: kDelete, del: del}, nil
+}
+
+func (p *parser) parseCreate() (*statement, error) {
+	p.next() // CREATE
+	// CREATE UNIQUE INDEX ...
+	if p.peek().kind == tkKeyword && p.peek().text == "UNIQUE" {
+		p.next()
+		return p.parseCreateIndex(true)
+	}
+	// CREATE INDEX ...
+	if p.peek().kind == tkKeyword && (p.peek().text == "INDEX" || p.peek().text == "KEY") {
+		return p.parseCreateIndex(false)
+	}
+	if err := p.expectKeyword("TABLE"); err != nil {
+		return nil, fmt.Errorf("仅支持 CREATE TABLE / CREATE [UNIQUE] INDEX")
+	}
+	crt := &createStmt{}
+	if p.acceptKeyword("IF") {
+		if err := p.expectKeyword("NOT"); err != nil {
+			return nil, err
+		}
+		if err := p.expectKeyword("EXISTS"); err != nil {
+			return nil, err
+		}
+		crt.ifNotExists = true
+	}
+	table, err := p.expectIdent()
+	if err != nil {
+		return nil, err
+	}
+	crt.table = table
+	if err := p.expectPunct("("); err != nil {
+		return nil, err
+	}
+	for {
+		// 表级约束: PRIMARY KEY (c) / [UNIQUE] KEY k (c)
+		if p.peek().kind == tkKeyword {
+			switch p.peek().text {
+			case "PRIMARY":
+				p.next()
+				if err := p.expectKeyword("KEY"); err != nil {
+					return nil, err
+				}
+				col, err := p.parseParenSingleCol()
+				if err != nil {
+					return nil, err
+				}
+				crt.pkCol = col
+			case "UNIQUE":
+				p.next()
+				idx, err := p.parseInlineIndex(true)
+				if err != nil {
+					return nil, err
+				}
+				crt.indexes = append(crt.indexes, idx)
+			case "KEY", "INDEX":
+				idx, err := p.parseInlineIndex(false)
+				if err != nil {
+					return nil, err
+				}
+				crt.indexes = append(crt.indexes, idx)
+			default:
+				return nil, fmt.Errorf("CREATE TABLE 里不支持的表级项: %s", p.peek().text)
+			}
+		} else {
+			col, err := p.parseColumnDef()
+			if err != nil {
+				return nil, err
+			}
+			crt.columns = append(crt.columns, col)
+		}
+		if !p.acceptPunct(",") {
+			break
+		}
+	}
+	if err := p.expectPunct(")"); err != nil {
+		return nil, err
+	}
+	// 列级 PRIMARY KEY 提升为表级 pk (单列); 主键隐含 NOT NULL
+	if crt.pkCol == "" {
+		for _, c := range crt.columns {
+			if c.pk {
+				crt.pkCol = c.name
+				break
+			}
+		}
+	}
+	if crt.pkCol != "" {
+		for i := range crt.columns {
+			if strings.EqualFold(crt.columns[i].name, crt.pkCol) {
+				crt.columns[i].notNull = true
+			}
+		}
+	}
+	// 容忍 CREATE TABLE ... 后的表选项被整体忽略? 不: 显式报错以免静默错配。
+	if p.peek().kind == tkKeyword && (p.peek().text == "ENGINE" || p.peek().text == "DEFAULT" || p.peek().text == "CHARSET") {
+		return nil, fmt.Errorf("CREATE TABLE 表选项 (ENGINE/DEFAULT CHARSET 等) 不支持")
+	}
+	return &statement{kind: kCreate, crt: crt}, nil
+}
+
+func (p *parser) parseInlineIndex(unique bool) (inlineIndex, error) {
+	// 已消费 UNIQUE (若有); 当前在 KEY/INDEX
+	if p.peek().kind != tkKeyword || (p.peek().text != "KEY" && p.peek().text != "INDEX") {
+		return inlineIndex{}, fmt.Errorf("期望 KEY/INDEX, 实际是 %q", p.peek().text)
+	}
+	p.next()
+	name := ""
+	if p.peek().kind == tkIdent {
+		name = p.next().text
+	}
+	cols, err := p.parseParenCols()
+	if err != nil {
+		return inlineIndex{}, err
+	}
+	if len(cols) != 1 {
+		return inlineIndex{}, fmt.Errorf("内联索引仅支持单列, 实际 %d 列", len(cols))
+	}
+	return inlineIndex{name: name, columns: cols, unique: unique}, nil
+}
+
+func (p *parser) parseParenCols() ([]string, error) {
+	if err := p.expectPunct("("); err != nil {
+		return nil, err
+	}
+	var cols []string
+	for {
+		col, err := p.expectIdent()
+		if err != nil {
+			return nil, err
+		}
+		cols = append(cols, col)
+		if !p.acceptPunct(",") {
+			break
+		}
+	}
+	if err := p.expectPunct(")"); err != nil {
+		return nil, err
+	}
+	return cols, nil
+}
+
+func (p *parser) parseParenSingleCol() (string, error) {
+	cols, err := p.parseParenCols()
+	if err != nil {
+		return "", err
+	}
+	if len(cols) != 1 {
+		return "", fmt.Errorf("PRIMARY KEY 仅支持单列, 实际 %d 列", len(cols))
+	}
+	return cols[0], nil
+}
+
+func (p *parser) parseColumnDef() (colDef, error) {
+	name, err := p.expectIdent()
+	if err != nil {
+		return colDef{}, err
+	}
+	def := colDef{name: name, arg: -1}
+	t := p.next()
+	if t.kind != tkIdent && t.kind != tkKeyword {
+		return colDef{}, fmt.Errorf("列 %q 后期望类型名, 实际是 %q", name, t.text)
+	}
+	def.typeName = strings.ToUpper(t.text)
+	// UNSIGNED 后缀: "BIGINT UNSIGNED" 是两词, 合并进类型名
+	if p.peek().kind == tkKeyword && p.peek().text == "UNSIGNED" {
+		p.next()
+		def.typeName += " UNSIGNED"
+	}
+	// 类型参数: VARCHAR(n) / DECIMAL(p,s) / CHAR(n)
+	if p.acceptPunct("(") {
+		a, err := p.parseIntLiteral()
+		if err != nil {
+			return colDef{}, err
+		}
+		def.arg = int(a)
+		if def.typeName == "DECIMAL" && p.acceptPunct(",") {
+			s, err := p.parseIntLiteral()
+			if err != nil {
+				return colDef{}, err
+			}
+			def.arg = int(s) // DECIMAL(p,s) 取 scale
+		}
+		if err := p.expectPunct(")"); err != nil {
+			return colDef{}, err
+		}
+	}
+	// 列级约束
+	for {
+		switch {
+		case p.acceptKeyword("NOT"):
+			if err := p.expectKeyword("NULL"); err != nil {
+				return colDef{}, err
+			}
+			def.notNull = true
+		case p.acceptKeyword("NULL"):
+			// 显式 NULL = 可空 (默认即可空, 忽略)
+		case p.acceptKeyword("UNIQUE"):
+			def.unique = true
+		case p.acceptKeyword("PRIMARY"):
+			if err := p.expectKeyword("KEY"); err != nil {
+				return colDef{}, err
+			}
+			def.pk = true
+			def.notNull = true // SQL: 主键列隐含 NOT NULL
+		case p.acceptKeyword("DEFAULT"):
+			// DEFAULT NULL 忽略; 其他默认值不支持 (避免静默错配)
+			v, err := p.parseValue()
+			if err != nil {
+				return colDef{}, err
+			}
+			if _, isNull := v.(nullValue); !isNull {
+				return colDef{}, fmt.Errorf("列 %q 的 DEFAULT 非 NULL 值暂不支持", name)
+			}
+		case p.acceptKeyword("AUTO_INCREMENT"):
+			// 自增列 = 引擎 pkID 镜像, 忽略标记
+		case p.acceptKeyword("COMMENT"):
+			if _, err := p.parseValue(); err != nil {
+				return colDef{}, err
+			}
+		default:
+			return def, nil
+		}
+	}
+}
+
+func (p *parser) parseDrop() (*statement, error) {
+	p.next() // DROP
+	if err := p.expectKeyword("TABLE"); err != nil {
+		return nil, fmt.Errorf("仅支持 DROP TABLE")
+	}
+	d := &dropStmt{}
+	if p.acceptKeyword("IF") {
+		if err := p.expectKeyword("EXISTS"); err != nil {
+			return nil, err
+		}
+		d.ifExists = true
+	}
+	table, err := p.expectIdent()
+	if err != nil {
+		return nil, err
+	}
+	d.table = table
+	return &statement{kind: kDrop, drop: d}, nil
+}
+
+

+ 221 - 0
demo/sql_driver/main.go

@@ -0,0 +1,221 @@
+package main
+
+// =============================================================================
+// sql_driver — database/sql 驱动演示: 标准库 API 全特性走查 (M1/M2/M3)
+//
+// 	go run main.go
+//
+// 覆盖: CREATE TABLE (含主键/唯一/普通索引) → INSERT (显式/自增主键, NULL) →
+// SELECT (投影/WHERE 全条件族/ORDER BY/LIMIT/聚合) → UPDATE → DELETE →
+// 事务 (Rollback 与 Commit) → 唯一约束冲突 → 不支持语法的明确报错。
+// 任何一步断言失败立即非零退出。
+// =============================================================================
+
+import (
+	"database/sql"
+	"fmt"
+	"os"
+
+	_ "git.ycl.pub/yl32940/TsDB2/DB/driver"
+)
+
+const dbPath = "sql_driver_demo.db"
+
+var failed bool
+
+func check(err error, what string) {
+	if err != nil {
+		fmt.Printf("❌ %s: %v\n", what, err)
+		os.Exit(1)
+	}
+}
+
+func section(s string) { fmt.Printf("\n=== %s ===\n", s) }
+
+func main() {
+	os.Remove(dbPath)
+	db, err := sql.Open("vrtx", dbPath)
+	check(err, "open")
+	defer db.Close()
+	// 引擎单写者模型: 连接池串行化, 设 1 路连接避免无谓排队
+	db.SetMaxOpenConns(1)
+
+	// ---------- M3: DDL ----------
+	section("M3: CREATE TABLE (主键/唯一/普通索引)")
+	_, err = db.Exec(`CREATE TABLE users (
+		id   BIGINT PRIMARY KEY,
+		name VARCHAR(32) NOT NULL,
+		age  INT,
+		email VARCHAR(64) UNIQUE,
+		city VARCHAR(32)
+	)`)
+	check(err, "create table")
+	_, err = db.Exec(`CREATE INDEX idx_city ON users (city)`)
+	check(err, "create index")
+	fmt.Println("✅ 建表 users (id 主键 / email 唯一 / city 普通索引)")
+
+	// ---------- M1: INSERT ----------
+	section("M1: INSERT (显式主键 + 省略列=NULL)")
+	res, err := db.Exec(`INSERT INTO users (id, name, age, email, city) VALUES (?, ?, ?, ?, ?)`,
+		1, "alice", 30, "alice@x.com", "beijing")
+	check(err, "insert alice")
+	id, _ := res.LastInsertId()
+	fmt.Printf("✅ 插入 alice LastInsertId=%d\n", id)
+
+	res, err = db.Exec(`INSERT INTO users (id, name, city) VALUES (?, ?, ?)`, 2, "bob", "shanghai") // age/email = NULL
+	check(err, "insert bob")
+	id, _ = res.LastInsertId()
+	fmt.Printf("✅ 插入 bob (age/email 省略 → NULL) LastInsertId=%d\n", id)
+
+	// 唯一约束: 重复 email 必须报错
+	_, err = db.Exec(`INSERT INTO users (id, name, email) VALUES (?, ?, ?)`, 3, "mallory", "alice@x.com")
+	if err == nil {
+		fmt.Println("❌ 唯一约束未生效: 重复 email 插入成功")
+		os.Exit(1)
+	}
+	fmt.Printf("✅ 唯一索引拦截重复 email: %v\n", err)
+
+	// ---------- M1: SELECT ----------
+	section("M1: SELECT (投影 + WHERE + ORDER BY + LIMIT)")
+	rows, err := db.Query(`SELECT id, name, age FROM users WHERE age >= ? ORDER BY age DESC LIMIT 10`, 18)
+	check(err, "query")
+	type user struct {
+		id   int64
+		name string
+		age  sql.NullInt64
+	}
+	var users []user
+	for rows.Next() {
+		var u user
+		var nm []byte
+		check(rows.Scan(&u.id, &nm, &u.age), "scan")
+		u.name = string(nm)
+		users = append(users, u)
+	}
+	rows.Close()
+	for _, u := range users {
+		age := "NULL"
+		if u.age.Valid {
+			age = fmt.Sprint(u.age.Int64)
+		}
+		fmt.Printf("   id=%d name=%-8s age=%v\n", u.id, u.name, age)
+	}
+	// SQL 语义: NULL >= 18 不为真 → bob (age=NULL) 不在结果里
+	if len(users) != 1 || users[0].name != "alice" || !users[0].age.Valid {
+		fmt.Println("❌ SELECT 结果不对")
+		os.Exit(1)
+	}
+	fmt.Println("✅ 投影/条件/排序/分页正确 (NULL 不参与比较: bob 被 age>=18 过滤)")
+
+	// ---------- M2: WHERE 全条件族 ----------
+	section("M2: WHERE 条件族 (IN / OR / NOT / IS NULL / LIKE)")
+	show := func(label, q string, args ...interface{}) int64 {
+		var n int64
+		check(db.QueryRow(q, args...).Scan(&n), label)
+		fmt.Printf("   %-14s = %d\n", label, n)
+		return n
+	}
+	if show("IN", `SELECT COUNT(*) FROM users WHERE id IN (?, ?)`, 1, 2) != 2 ||
+		show("OR", `SELECT COUNT(*) FROM users WHERE city = ? OR city = ?`, "beijing", "shanghai") != 2 ||
+		show("NOT", `SELECT COUNT(*) FROM users WHERE NOT (id = ?)`, 1) != 1 ||
+		show("IS NULL", `SELECT COUNT(*) FROM users WHERE age IS NULL`) != 1 ||
+		show("LIKE", `SELECT COUNT(*) FROM users WHERE name LIKE ?`, "al%") != 1 {
+		fmt.Println("❌ WHERE 条件族结果不对")
+		os.Exit(1)
+	}
+	fmt.Println("✅ 全条件族通过")
+
+	// ---------- M3: 聚合 ----------
+	section("M3: 聚合 (COUNT/SUM/AVG/MIN/MAX)")
+	agg := func(label, q string) float64 {
+		var v float64
+		check(db.QueryRow(q).Scan(&v), label)
+		fmt.Printf("   %-10s = %v\n", label, v)
+		return v
+	}
+	if agg("SUM(age)", `SELECT SUM(age) FROM users`) != 30 ||
+		agg("COUNT(*)", `SELECT COUNT(*) FROM users`) != 2 ||
+		agg("AVG(age)", `SELECT AVG(age) FROM users`) != 30 {
+		fmt.Println("❌ 聚合结果不对")
+		os.Exit(1)
+	}
+	agg("MIN(age)", `SELECT MIN(age) FROM users`)
+	agg("MAX(age)", `SELECT MAX(age) FROM users`)
+	fmt.Println("✅ 聚合通过")
+
+	// ---------- M2: UPDATE / DELETE ----------
+	section("M2: UPDATE / DELETE")
+	res, err = db.Exec(`UPDATE users SET age = ? WHERE name = ?`, 31, "alice")
+	check(err, "update")
+	if n, _ := res.RowsAffected(); n != 1 {
+		fmt.Printf("❌ UPDATE affected=%d\n", n)
+		os.Exit(1)
+	}
+	var age int64
+	check(db.QueryRow(`SELECT age FROM users WHERE id = ?`, 1).Scan(&age), "select age")
+	if age != 31 {
+		fmt.Printf("❌ UPDATE 未生效 age=%d\n", age)
+		os.Exit(1)
+	}
+	fmt.Println("✅ UPDATE 生效 (alice.age 30→31)")
+
+	res, err = db.Exec(`DELETE FROM users WHERE city = ?`, "shanghai")
+	check(err, "delete")
+	if n, _ := res.RowsAffected(); n != 1 {
+		fmt.Printf("❌ DELETE affected=%d\n", n)
+		os.Exit(1)
+	}
+	fmt.Println("✅ DELETE 生效 (bob 已删)")
+
+	// ---------- M2: 事务 ----------
+	section("M2: 事务 (Rollback 回滚 / Commit 提交)")
+	tx, err := db.Begin()
+	check(err, "begin")
+	_, err = tx.Exec(`INSERT INTO users (id, name, age) VALUES (?, ?, ?)`, 10, "temp", 99)
+	check(err, "tx insert")
+	check(tx.Rollback(), "rollback")
+	var cnt int64
+	check(db.QueryRow(`SELECT COUNT(*) FROM users WHERE id = ?`, 10).Scan(&cnt), "count after rollback")
+	if cnt != 0 {
+		fmt.Println("❌ Rollback 后 temp 仍在")
+		os.Exit(1)
+	}
+	fmt.Println("✅ Rollback 撤销了事务内插入")
+
+	tx2, err := db.Begin()
+	check(err, "begin2")
+	_, err = tx2.Exec(`INSERT INTO users (id, name, age) VALUES (?, ?, ?)`, 11, "carol", 22)
+	check(err, "tx2 insert")
+	check(tx2.Commit(), "commit")
+	check(db.QueryRow(`SELECT COUNT(*) FROM users WHERE id = ?`, 11).Scan(&cnt), "count after commit")
+	if cnt != 1 {
+		fmt.Println("❌ Commit 后 carol 不在")
+		os.Exit(1)
+	}
+	fmt.Println("✅ Commit 提交了事务内插入")
+
+	// ---------- Prepare 路径 ----------
+	section("附加: Prepare (预编译语句)")
+	stmt, err := db.Prepare(`SELECT name FROM users WHERE id = ?`)
+	check(err, "prepare")
+	defer stmt.Close()
+	var nm string
+	check(stmt.QueryRow(11).Scan(&nm), "prepared query")
+	if nm != "carol" {
+		fmt.Printf("❌ prepared 查询结果 %q\n", nm)
+		os.Exit(1)
+	}
+	fmt.Println("✅ Prepare/QueryRow 通过")
+
+	// ---------- 不支持语法的明确报错 ----------
+	section("附加: 不支持语法明确报错 (不静默)")
+	if _, err := db.Query(`SELECT * FROM users GROUP BY city`); err == nil {
+		fmt.Println("❌ GROUP BY 应报错")
+		os.Exit(1)
+	} else {
+		fmt.Printf("✅ GROUP BY 明确拒绝: %v\n", err)
+	}
+
+	fmt.Println("\n🎉 全部通过 — database/sql 驱动 M1/M2/M3 可用")
+	_ = failed
+}