ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Turso sqltest 快照测试实战指南:用 EXPLAIN 输出守护查询执行计划

Turso sqltest 快照测试实战指南:用 EXPLAIN 输出守护查询执行计划 Turso sqltest 快照测试实战指南用 EXPLAIN 输出守护查询执行计划【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso导读本文围绕 Turso/Limbo 仓库中 sqltest 测试运行器的快照测试能力展开。sqltest 是仓库自带的 SQL 测试框架位于testing/sqltest其中的快照测试用于捕获并校验 SQL 查询的EXPLAIN QUERY PLAN查询计划与EXPLAINVDBE 字节码输出从而在数据库引擎演进过程中守护执行计划的稳定性。读完本文你将掌握快照测试文件的编写语法、四种快照更新模式auto/new/always/no的取舍、快照文件的格式与命名规范、CI 集成方式以及从源码层面理解快照的生成、比对与格式化原理。快照测试是什么为什么需要它快照测试Snapshot Testing捕获 SQL 查询的两种 EXPLAIN 输出EXPLAIN QUERY PLAN查询优化器选出的执行计划树EXPLAIN查询被编译成的 VDBE 字节码指令序列。与普通测试只比较查询结果不同快照测试验证的是查询的执行方式是否保持一致。它能够帮助检测查询计划回归如从索引扫描退化为全表扫描索引使用情况的意外变化字节码生成差异。在 testing/sqltest/src/snapshot/mod.rs 的模块注释中这一设计被明确描述为为 SQL EXPLAIN 输出提供 insta 兼容的快照测试能力用于验证 SQL EXPLAIN 输出保持一致。注意快照测试目前只在 Rust 后端运行。其他后端CLI、JS、PG会自动跳过快照测试原因在于不同后端的 EXPLAIN 输出格式可能存在差异。这一点在 testing/sqltest/docs/dsl-spec.md 的 Snapshot Cases 一节中有同样说明。快速开始四步跑通第一个快照测试1. 编写一个快照测试创建一个.sqltest文件声明内存数据库、定义 setup 块再用snapshot关键字声明快照用例database :memory: setup schema { CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); CREATE INDEX idx_users_name ON users(name); } setup schema snapshot my-query-plan { SELECT * FROM users WHERE id 1; }仓库中提供了一个可直接运行的示例文件 testing/sqltest/examples/snapshot_example.sqltest其中既有普通test ... expect用例也有snapshot query-plan-by-id、snapshot query-plan-by-name两个快照用例可用于对照学习。2. 运行测试生成快照首次运行会生成.snap.new文件供人工审查# 通过 Makefile 运行CLI 后端 make -C sqlite/conformance run-cli # 或直接运行 sqltest cargo run --bin sqltest -- run sqlite/conformance/sqlite-sqltests/my-test.sqltest注意示例文件建议配合 Rust 后端运行快照测试cargo run --bin sqltest -- run testing/sqltest/examples/ --backend rust3. 接受快照人工审查.snap.new内容无误后以 always 模式接受cargo run --bin sqltest -- run sqlite/conformance/sqlite-sqltests/ --snapshot-modealways4. 提交快照文件git add sqlite/conformance/sqlite-sqltests/snapshots/ git commit -m Add query plan snapshots快照更新模式auto / new / always / no--snapshot-mode标志控制快照的更新行为四种模式对比如下模式行为适用场景autoCI 中表现为no本地表现为new默认日常开发new写入.snap.new文件供审查接受变更前的人工审查always直接写入.snap文件接受所有变更no只读不写任何文件CI 校验auto默认自动检测运行环境在 CIGitHub Actions、Travis、CircleCI 等中行为等同no本地行为等同new。CI 检测通过检查环境变量实现。在 testing/sqltest/src/snapshot/mod.rs 中可以看到完整的检测列表共 9 个变量const CI_ENV_NAMES: [str; 9] [ CI, GITHUB_ACTIONS, TRAVIS, CIRCLECI, GITLAB_CI, JENKINS_URL, BUILDKITE, TF_BUILD, // Azure Pipelines CODEBUILD_BUILD_ID, // AWS CodeBuild ];IS_CI是一个惰性初始化的静态布尔值只要任一变量存在且其值为 true1或大小写不敏感的true即判定为 CI 环境。SnapshotUpdateMode::resolve()方法负责把Auto解析为实际模式CI 中解析为No本地解析为New。new在现有快照旁生成.snap.new文件便于审查变更sqlite/conformance/sqlite-sqltests/snapshots/ my-test__query-plan.snap # 现有快照 my-test__query-plan.snap.new # 新增/变更的快照手动审查 diff 后再用--snapshot-modealways接受。从源码看new模式在比对不匹配时调用write_pending()写入.snap.new文件并返回SnapshotResult::Mismatch包含 expected、actual 和 diff 字段当快照尚不存在时返回SnapshotResult::New。always不经过.snap.new中间态直接更新.snap文件。适用于已经审查过变更、准备接受的情况。源码中always模式在匹配时会清理陈旧的.snap.new文件remove_pending在不匹配或不存在时直接调用write_snapshot()写入并返回Updated或New结果。no只读模式不写任何快照文件。以下情况会导致测试失败快照不存在快照内容不匹配。这是 CI 使用的模式确保所有快照变更都已被显式提交。快照文件格式与元数据快照文件由 YAML frontmatter 加捕获的输出正文组成--- source: my-test.sqltest expression: SELECT * FROM users WHERE id 1; info: statement_type: SELECT tables: - users setup_blocks: - schema database: :memory: --- QUERY PLAN --SEARCH users USING INTEGER PRIMARY KEY (rowid?) BYTECODE addr opcode p1 p2 p3 p4 p5 comment 0 Init 0 8 0 0 Start at 8 1 OpenRead 0 2 0 k(3,B,B,B) 0 tableusers, root2, iDb0 ...元数据字段字段描述source测试文件名expression被快照的 SQL 查询info.statement_type自动检测的语句类型SELECT、INSERT、UPDATE、DELETE 等info.tables从查询中自动提取的表名info.setup_blocks快照执行前应用的 setup 块info.database使用的数据库类型info.line测试文件中的行号可选序列化时仅在存在时输出源码中的SnapshotMetadata/SnapshotInfo结构体与split_frontmatter()解析函数位于 testing/sqltest/src/snapshot/mod.rs其中tables、setup_blocks、database、line均通过#[serde(default, skip_serializing_if ...)]在为空时省略输出。仓库中真实生成的快照文件可以参考 testing/sqltest/examples/snapshots/snapshot_example__query-plan-by-id.snap 与 testing/sqltest/examples/snapshots/snapshot_example__query-plan-by-name.snap后者展示了走idx_users_name索引时SeekGE/IdxGT/DeferredSeek等指令组成的字节码序列。元数据如何自动生成create_snapshot()在写入时自动完成两件事提取表名通过一组正则模式匹配FROM、JOIN、INSERT INTO、UPDATE、DELETE FROM、CREATE TABLE、DROP TABLE、ALTER TABLE、CREATE INDEX ... ON等子句并使用is_sql_keyword()过滤SELECT、WHERE等 SQL 关键字结果以BTreeSet去重排序检测语句类型detect_statement_type()依据 SQL 前缀判断支持SELECT、INSERT、UPDATE、DELETE、CREATE TABLE、CREATE INDEX、CREATE、DROP、ALTER、WITH (CTE)与兜底的OTHER。文件组织与命名约定快照文件存放在测试文件同级的snapshots/目录中sqlite/conformance/sqlite-sqltests/ queries.sqltest aggregates.sqltest snapshots/ queries__select-by-id.snap queries__select-by-name.snap aggregates__count-all.snap命名约定{test-file-stem}__{snapshot-name}.snap。这一约定由SnapshotManager::snapshot_path()实现取测试文件的 stem与快照名拼接为{stem}__{name}.snap放入同目录snapshots/子目录pending 文件则在相同位置以.snap.new结尾pending_path()。CLI 命令详解运行带快照的测试# 默认模式auto cargo run --bin sqltest -- run sqlite/conformance/sqlite-sqltests/ # 接受所有快照变更 cargo run --bin sqltest -- run sqlite/conformance/sqlite-sqltests/ --snapshot-modealways # 审查模式生成 .snap.new 文件 cargo run --bin sqltest -- run sqlite/conformance/sqlite-sqltests/ --snapshot-modenew # 只读模式CI cargo run --bin sqltest -- run sqlite/conformance/sqlite-sqltests/ --snapshot-modeno # 过滤特定快照 cargo run --bin sqltest -- run sqlite/conformance/sqlite-sqltests/ --snapshot-filterquery-plan*在 testing/sqltest/src/main.rs 中run子命令还支持--backendrust/cli/js/pg默认 rust、--binary、--filter、--jobs、--outputpretty/json、--timeout、--mvcc等选项--snapshot-filter与--filter相互独立前者只筛选快照用例。若路径不存在sqltest 会自动尝试补上.sqltest扩展名后再解析。检查待处理快照cargo run --bin sqltest -- check sqlite/conformance/sqlite-sqltests/该命令会校验测试文件语法检测待处理的.snap.new文件若存在任何待处理快照则失败适合 CI。从 testing/sqltest/src/main.rs 的check_files()看它通过find_all_pending_snapshots()递归扫描目标目录跳过snapshots目录本身只要发现扩展名为new且 stem 以.snap结尾的文件即报错随后对目录内所有*.sqltest文件做语法解析解析错误同样导致非零退出码。语法检查通过时会打印形如select.sqltest - OK (1 databases, 2 setups, 5 tests, 2 snapshots)的摘要。使用 Makefilesqlite/conformance/Makefile 封装了常用入口# 运行全部测试含快照 make -C sqlite/conformance run-cli # 运行示例含快照示例 make -C sqlite/conformance run-examples # 检查语法与待处理快照 make -C sqlite/conformance check此外还提供run-rust原生 Rust 后端快照测试的推荐后端、run-js、run-filter、run-one FILE...等目标Makefile 会根据是否定义CI自动切换 release/debug 构建并支持MVCC1、BACKENDrust、CROSS_CHECK_BINARYpath等变量。快照测试 DSL 语法基本快照database :memory: snapshot query-plan { SELECT * FROM users; }带 setup 块的快照database :memory: setup schema { CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); } setup data { INSERT INTO users VALUES (1, Alice); } setup schema setup data snapshot query-plan-with-data { SELECT * FROM users WHERE id 1; }setup 块可以重复应用setup装饰器可以连续出现多次。值得注意快照正文中的 SQL 会原样存入expression元数据而info.tables则通过正则提取因此 setup 块中建立索引、插入数据会直接影响生成计划的形态例如是否走索引。跳过快照# 无条件跳过 skip query plan not stable yet snapshot unstable-plan { SELECT * FROM complex_view; } # 条件跳过MVCC 模式 skip-if mvcc different plan in MVCC mode snapshot standard-plan { SELECT * FROM users; }skip-if的条件可以是mvcc、sqlite等运行时条件用于在特定模式下计划不稳定的场景。后端限定由于快照测试只在 Rust 后端运行通常不需要backend装饰器但可以显式声明以强调依赖# 显式要求 Rust 后端可选因为只有该后端会运行快照 backend rust snapshot turso-query-plan { SELECT * FROM users WHERE id 1; }能力要求# 快照需要触发器支持 requires trigger query plan involves triggers setup schema-with-triggers snapshot trigger-query-plan { INSERT INTO audit_log SELECT * FROM events; }支持的装饰器汇总快照支持测试的全部装饰器装饰器描述setup name在快照前应用某个 setup 块skip reason无条件跳过该快照skip-if cond reason条件跳过如mvcc、sqlitebackend name仅在指定后端运行快照仅在rust上运行requires cap reason仅当后端支持该能力时运行文件级指令skip-file、skip-file-if、requires-file同样作用于快照。完整的 DSL 语法含snapshot_case { decorator } snapshot IDENTIFIER block的产生式见 testing/sqltest/docs/dsl-spec.md。输出格式QUERY PLAN 与 BYTECODE每个快照捕获两段内容。QUERY PLANEXPLAIN QUERY PLAN的输出被格式化为树形结构QUERY PLAN --SEARCH users USING INTEGER PRIMARY KEY (rowid?)含子查询或 JOIN 的复杂查询QUERY PLAN |--SCAN users --SEARCH orders USING INDEX idx_orders_user (user_id?)源码中的format_explain_query_plan_output()读取 EXPLAIN QUERY PLAN 返回的id/parent/notused/detail四列仅保留 detail 列做展示利用 id/parent 关系构建树递归输出|--与--分支符号。BYTECODEEXPLAIN的输出被格式化为对齐的表格BYTECODE addr opcode p1 p2 p3 p4 p5 comment 0 Init 0 8 0 0 Start at 8 1 OpenRead 0 2 0 k(3,B,B,B) 0 tableusers, root2, iDb0 2 SeekRowid 0 4 7 0 if (r[4]!cursor 0...) goto 7format_explain_output()的实现细节值得关注列定义固定为addr右对齐、opcode左对齐、p1/p2/p3右对齐、p4左对齐、p5右对齐、comment左对齐与 SQLite 官方 EXPLAIN 输出保持一致循环缩进根据指令的跳转关系计算每行缩进。AZ_NEXT指令集Next、Prev、VPrev、VNext、SorterNext、Return会把其p2跳转目标与自身之间的指令整体缩进Goto仅在向后跳转p2 addr且目标属于AZ_YIELDYield、SeekLT、SeekGT、RowSetRead、Rewind或p1非零时才产生缩进嵌套循环会形成多层缩进每层两个空格与 CLI 的 EXPLAIN 输出行为一致。calculate_row_indents()先收集所有循环区间(start, end)再统计每条指令落入多少个区间得到缩进层级snapshot/mod.rs内附有单层循环、嵌套循环、空结果等场景的单元测试如test_format_explain_output_with_loop_indentation、test_format_explain_output_nested_loops验证Column/ResultRow位于循环体内被缩进、Next/Halt不被缩进的行为。CI 集成推荐的 CI 配置# .github/workflows/test.yml - name: Run SQL tests run: | cargo run --bin sqltest -- run sqlite/conformance/sqlite-sqltests/ --snapshot-modeno - name: Check for pending snapshots run: | cargo run --bin sqltest -- check sqlite/conformance/sqlite-sqltests/check命令在存在任何.snap.new文件时会失败从而保证所有快照变更都已被提交。此外即使不显式传--snapshot-modeno默认的auto模式在 CI 环境检测到CI、GITHUB_ACTIONS等环境变量下也会自动退化为只读双保险防止 CI 被意外写入文件。更新快照的工作流修改影响查询计划的代码本地运行测试生成.snap.new文件审查变更diff sqlite/conformance/sqlite-sqltests/snapshots/*.snap sqlite/conformance/sqlite-sqltests/snapshots/*.snap.new接受变更--snapshot-modealways提交更新后的.snap文件推送到 CI。与 cargo-insta 的差异工作流与 cargo-insta 类似但 sqltest 使用自定义快照实现特性sqltestcargo-insta文件格式YAML frontmatter 内容YAML frontmatter 内容审查工具手动 diff /--snapshot-modecargo insta reviewCI 模式--snapshot-modeno--check接受全部--snapshot-modealwayscargo insta accept元数据SQL 专属表名、语句类型通用sqltest 的元数据info.statement_type、info.tables、info.setup_blocks、info.database是 SQL 领域专属的这一点在比对与排查时能提供更多上下文。比对差异时使用similarcrate 生成统一 diffgenerate_diff()并以SnapshotResult::Mismatch的形式把 expected/actual/diff 一并返回给上层报告。故障排查CI 中提示 Snapshot mismatch本地运行测试生成.snap.new文件审查差异用--snapshot-modealways接受提交更新后的.snap文件。提示 Found pending snapshot filescheck命令发现了.snap.new文件。两种处理方式接受它们运行--snapshot-modealways删除它们rm sqlite/conformance/sqlite-sqltests/snapshots/*.snap.new。多次运行结果不一致查询计划可能因以下因素变化数据库统计信息索引可用性SQLite/Turso 版本。请确保 setup 块创建了稳定的 schema 与数据。快照没有更新确认使用了--snapshot-modealways或--snapshot-modenew。默认的auto模式在 CI 环境中表现为no只读这是最常见的原因。此外注意快照只在 Rust 后端运行若用--backend cli或--backend js运行快照会被自动跳过也就不会生成新文件。最佳实践使用描述性快照名——query-plan-user-by-id远好于test1归类相关快照—— 把相似功能的快照放在同一个测试文件中包含必要的 setup—— 快照需要索引与数据才能产生有意义的计划接受前务必审查—— 不要盲目接受快照变更要理解计划为何变化例如是否意外丢失了索引随代码变更一起提交快照—— 修改查询逻辑时在同一提交中更新快照对不稳定计划使用 skip—— 若计划在不同环境间波动先用skip跳过待稳定后再启用。延伸阅读DSL 完整语法testing/sqltest/docs/dsl-spec.mdsqltest CLI 用法testing/sqltest/docs/cli-usage.md快照实现源码testing/sqltest/src/snapshot/mod.rsCLI 入口与参数定义testing/sqltest/src/main.rs可运行的示例testing/sqltest/examples/snapshot_example.sqltest 及其快照目录 testing/sqltest/examples/snapshots/测试语料库与 Makefile 封装sqlite/conformance/Makefile【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表