//go:build sqlite_vtable || vtable package vtab_test // ─── 演示:过滤和分页全部走 API 接口 ────────────────────────────── // // 模拟场景:后端有一个分页 API,支持 name=? 和 age 比较条件。 // 通过 BestIndex + Filter 将 WHERE 约束下推给 API, // 避免全量拉取数据再由 SQLite 过滤。 // // HIDDEN 列:token / page_size 不出现在 SELECT * 结果里, // 但可以在 WHERE 里传递 API 参数,例如: // SELECT id, name FROM api_users WHERE age > 28 AND token = 'Bearer xxx' AND page_size = 2 // // 验证点: // - 全量查询 6 条 / pageSize=3 → 发起 2 次 API 调用 // - WHERE name='Alice' 下推 → 1 次 API 调用 // - WHERE age > 28 下推 → 1 次 API 调用(结果 3 条,恰好 1 页) // - WHERE page_size=2 通过 HIDDEN 列动态指定分页大小 import ( "database/sql" "testing" "git.fsdpf.net/go/db/dialect/sqlite3/vtab" "github.com/stretchr/testify/suite" ) // ── 列索引常量 ──────────────────────────────────────────────────── const ( apiColID = 0 apiColName = 1 apiColAge = 2 apiColToken = 3 // HIDDEN:API 鉴权 token apiColPageSize = 4 // HIDDEN:自定义每页大小 // OpLIMIT / OpOFFSET 的 Column 字段固定为 -1,不对应任何列。 apiColLimit = -1 apiColOffset = -1 ) // ── Mock API 数据层 ─────────────────────────────────────────────── type apiUser struct { ID int64 Name string Age int64 } type apiListResult struct { Items []apiUser HasMore bool } // apiQueryFilter 对应 API 接口支持的查询参数。 type apiQueryFilter struct { Name string // name = ?(精确匹配) AgeOp vtab.Op // age 的比较运算符 AgeVal int64 // age 的比较值 hasAge bool // 是否有 age 过滤 Token string // API 鉴权 token(HIDDEN 列传入) PageSize int // 每页大小(HIDDEN 列传入,0 表示使用默认值) Limit int64 // SQL LIMIT 下推值(0 表示无限制) Offset int64 // SQL OFFSET 下推值 } // mockUserAPI 模拟支持过滤和分页的 HTTP API。 type mockUserAPI struct { data []apiUser pageSize int Calls int // 记录 API 被调用次数,供测试断言 } func newMockUserAPI() *mockUserAPI { return &mockUserAPI{ pageSize: 3, // 每页 3 条,便于测试翻页 data: []apiUser{ {1, "Alice", 30}, {2, "Bob", 25}, {3, "Charlie", 35}, {4, "Dave", 28}, {5, "Eve", 22}, {6, "Frank", 40}, }, } } // List 模拟 GET /users?page=N&name=X&age_op=GT&age_val=28 func (a *mockUserAPI) List(page int, f apiQueryFilter) apiListResult { a.Calls++ // 服务端过滤(模拟 API 的 WHERE 逻辑) var filtered []apiUser for _, u := range a.data { if f.Name != "" && u.Name != f.Name { continue } if f.hasAge { switch f.AgeOp { case vtab.OpEQ: if u.Age != f.AgeVal { continue } case vtab.OpGT: if !(u.Age > f.AgeVal) { continue } case vtab.OpGE: if !(u.Age >= f.AgeVal) { continue } case vtab.OpLT: if !(u.Age < f.AgeVal) { continue } case vtab.OpLE: if !(u.Age <= f.AgeVal) { continue } } } filtered = append(filtered, u) } // 服务端分页 start := (page - 1) * a.pageSize if start >= len(filtered) { return apiListResult{} } end := start + a.pageSize hasMore := end < len(filtered) if end > len(filtered) { end = len(filtered) } return apiListResult{Items: filtered[start:end], HasMore: hasMore} } // ── vtab Module ─────────────────────────────────────────────────── type apiUsersModule struct { api *mockUserAPI } func (m *apiUsersModule) Create(args []string, declare func(string) error) (vtab.Table, error) { // HIDDEN 列不出现在 SELECT * 结果里,但可以在 WHERE 里传递 API 参数。 if err := declare(`CREATE TABLE api_users( id INTEGER, name TEXT, age INTEGER, token TEXT HIDDEN, page_size INTEGER HIDDEN )`); err != nil { return nil, err } return &apiUsersTable{api: m.api}, nil } func (m *apiUsersModule) Connect(args []string, declare func(string) error) (vtab.Table, error) { return m.Create(args, declare) } // ── vtab Table ──────────────────────────────────────────────────── type apiUsersTable struct { api *mockUserAPI } // BestIndex 告知 SQLite 哪些约束由本表(API)处理: // - name = ? → 下推 // - age =/>/>=/ 0 { c.api.pageSize = c.filter.PageSize } result := c.api.List(c.page, c.filter) c.items = result.Items c.hasMore = result.HasMore // OFFSET 下推:第一页跳过前 Offset 条(假设 Offset < pageSize) if c.page == 1 && c.filter.Offset > 0 { skip := int(c.filter.Offset) if skip >= len(c.items) { c.items = nil } else { c.items = c.items[skip:] } } c.pos = 0 return nil } // Next 移动到下一行;当前页耗尽且还有下一页时自动翻页。 // LIMIT 下推时,到达限制行数后不再翻页。 func (c *apiUsersCursor) Next() error { c.pos++ c.emitted++ // 已达 LIMIT,不再翻页 if c.filter.Limit > 0 && c.emitted >= c.filter.Limit { return nil } if c.pos >= len(c.items) && c.hasMore { c.page++ return c.fetchPage() } return nil } func (c *apiUsersCursor) EOF() bool { if c.filter.Limit > 0 && c.emitted >= c.filter.Limit { return true } return c.pos >= len(c.items) && !c.hasMore } func (c *apiUsersCursor) Rowid() (int64, error) { return c.items[c.pos].ID, nil } func (c *apiUsersCursor) Column(col int) (any, error) { u := c.items[c.pos] switch col { case apiColID: return u.ID, nil case apiColName: return u.Name, nil case apiColAge: return u.Age, nil } return nil, nil } func (c *apiUsersCursor) Close() error { return nil } // ── 注册模块 & 测试套件 ─────────────────────────────────────────── var _apiMock = newMockUserAPI() func init() { vtab.Register("api_users_mod", &apiUsersModule{api: _apiMock}) } type APIVtabSuite struct { suite.Suite db *sql.DB } func (s *APIVtabSuite) SetupSuite() { db, err := sql.Open(vtab.DriverName, ":memory:") s.Require().NoError(err) _, err = db.Exec(`CREATE VIRTUAL TABLE api_users USING api_users_mod()`) s.Require().NoError(err) s.db = db } func (s *APIVtabSuite) TearDownSuite() { s.db.Close() } // SetupTest 每个用例前重置 API 状态。 func (s *APIVtabSuite) SetupTest() { _apiMock.Calls = 0 _apiMock.pageSize = 3 } // ── SELECT 全量 ─────────────────────────────────────────────────── // 6 条数据 / pageSize=3 → 需要翻 2 页 → 2 次 API 调用。 func (s *APIVtabSuite) TestSelect_All_TwoAPIPages() { rows, err := s.db.Query(`SELECT id, name, age FROM api_users ORDER BY id`) s.Require().NoError(err) defer rows.Close() var result []apiUser for rows.Next() { var u apiUser s.Require().NoError(rows.Scan(&u.ID, &u.Name, &u.Age)) result = append(result, u) } s.Require().NoError(rows.Err()) s.Equal(6, len(result), "应返回全部 6 条数据") s.Equal(2, _apiMock.Calls, "pageSize=3,全量扫描应发起 2 次 API 调用") } // ── WHERE 下推:name ────────────────────────────────────────────── // name='Alice' 下推给 API,结果 1 条,1 页即止。 func (s *APIVtabSuite) TestSelect_FilterName_PushedToAPI() { rows, err := s.db.Query(`SELECT id, name, age FROM api_users WHERE name = 'Alice'`) s.Require().NoError(err) defer rows.Close() var result []apiUser for rows.Next() { var u apiUser s.Require().NoError(rows.Scan(&u.ID, &u.Name, &u.Age)) result = append(result, u) } s.Require().NoError(rows.Err()) s.Equal([]apiUser{{1, "Alice", 30}}, result) s.Equal(1, _apiMock.Calls, "API 过滤后 1 页,只调用 1 次") } // ── WHERE 下推:age ─────────────────────────────────────────────── // age > 28 下推给 API:Alice(30)、Charlie(35)、Frank(40) → 3 条,恰好 1 页。 func (s *APIVtabSuite) TestSelect_FilterAge_PushedToAPI() { rows, err := s.db.Query(`SELECT name FROM api_users WHERE age > 28 ORDER BY id`) s.Require().NoError(err) defer rows.Close() var names []string for rows.Next() { var name string s.Require().NoError(rows.Scan(&name)) names = append(names, name) } s.Require().NoError(rows.Err()) s.Equal([]string{"Alice", "Charlie", "Frank"}, names) s.Equal(1, _apiMock.Calls, "API 过滤后 3 条恰好 1 页,只调用 1 次") } // age <= 25 下推:Bob(25)、Eve(22) → 2 条,1 页。 func (s *APIVtabSuite) TestSelect_FilterAgeLE_PushedToAPI() { rows, err := s.db.Query(`SELECT name FROM api_users WHERE age <= 25 ORDER BY id`) s.Require().NoError(err) defer rows.Close() var names []string for rows.Next() { var name string s.Require().NoError(rows.Scan(&name)) names = append(names, name) } s.Require().NoError(rows.Err()) s.Equal([]string{"Bob", "Eve"}, names) s.Equal(1, _apiMock.Calls, "过滤后 2 条,1 次 API 调用") } // ── WHERE 下推:name + age 组合 ─────────────────────────────────── // name='Alice' AND age >= 30 → 同时下推两个约束。 func (s *APIVtabSuite) TestSelect_FilterNameAndAge_BothPushed() { rows, err := s.db.Query(`SELECT name, age FROM api_users WHERE name = 'Alice' AND age >= 30`) s.Require().NoError(err) defer rows.Close() var result []apiUser for rows.Next() { var u apiUser s.Require().NoError(rows.Scan(&u.Name, &u.Age)) result = append(result, u) } s.Require().NoError(rows.Err()) s.Equal([]apiUser{{Name: "Alice", Age: 30}}, result) s.Equal(1, _apiMock.Calls, "组合过滤后 1 条,1 次 API 调用") } // ── HIDDEN 列:通过 WHERE 传递 API 参数 ─────────────────────────── // page_size=2 通过 HIDDEN 列传入,6 条数据需要 3 次 API 调用。 func (s *APIVtabSuite) TestHidden_PageSize() { // 先重置为默认 pageSize=3 _apiMock.pageSize = 3 rows, err := s.db.Query(`SELECT id, name FROM api_users WHERE page_size = 2`) s.Require().NoError(err) defer rows.Close() var ids []int64 for rows.Next() { var id int64 var name string s.Require().NoError(rows.Scan(&id, &name)) ids = append(ids, id) } s.Require().NoError(rows.Err()) s.Equal(6, len(ids), "page_size=2 依然能拿到全部 6 条") s.Equal(3, _apiMock.Calls, "page_size=2 时 6 条数据需要 3 次 API 调用") } // HIDDEN 列不出现在 SELECT * 中。 func (s *APIVtabSuite) TestHidden_NotInSelectStar() { _apiMock.pageSize = 3 rows, err := s.db.Query(`SELECT * FROM api_users WHERE id = 1`) s.Require().NoError(err) defer rows.Close() // SELECT * 应该只有 3 列(id/name/age),不含 token/page_size cols, err := rows.Columns() s.Require().NoError(err) s.Equal([]string{"id", "name", "age"}, cols) } // ── LIMIT / OFFSET 下推 ─────────────────────────────────────────── // LIMIT 3 下推:vtab 直接截断,只发 1 次 API 调用(不翻页)。 func (s *APIVtabSuite) TestLimitOffset_LimitPushdown() { rows, err := s.db.Query(`SELECT id, name FROM api_users ORDER BY id LIMIT 3`) s.Require().NoError(err) defer rows.Close() var ids []int64 for rows.Next() { var id int64 var name string s.Require().NoError(rows.Scan(&id, &name)) ids = append(ids, id) } s.Require().NoError(rows.Err()) s.Equal([]int64{1, 2, 3}, ids) s.Equal(1, _apiMock.Calls, "LIMIT 3 下推后只需 1 次 API 调用,不翻页") } // LIMIT 2 OFFSET 2:跳过前 2 条,取第 3、4 条。 func (s *APIVtabSuite) TestLimitOffset_LimitAndOffset() { rows, err := s.db.Query(`SELECT id FROM api_users ORDER BY id LIMIT 2 OFFSET 2`) s.Require().NoError(err) defer rows.Close() var ids []int64 for rows.Next() { var id int64 s.Require().NoError(rows.Scan(&id)) ids = append(ids, id) } s.Require().NoError(rows.Err()) s.Equal([]int64{3, 4}, ids) // OFFSET=2 跳过 page1 前 2 条只剩 1 条,不足 LIMIT=2,必须取 page2 补齐 → 2 次 API 调用 s.Equal(2, _apiMock.Calls, "OFFSET 跨页时需要 2 次 API 调用") } func TestAPIVtabSuite(t *testing.T) { suite.Run(t, new(APIVtabSuite)) }