前四篇我们已经能读取任意一页的 B-Tree、遍历到叶节点、解码每一行的列值。但一直有一个未解决的问题:用户表的根页号从哪里来? 我们一直在硬编码 root_page = 2。
这一篇解决这个问题。答案是 sqlite_schema 表——SQLite 把数据库里所有表、索引、视图、触发器的元数据都存在这张特殊的表里。它本身也是一张普通的 B-Tree 表,根页永远是 Page 1(文件头之后的部分)。
一、sqlite_schema 表结构
用 SQLite CLI 可以直接看它的结构:
sqlite3 test.db ".schema sqlite_schema"
-- 输出:
-- CREATE TABLE sqlite_schema (
-- type TEXT, -- 'table', 'index', 'view', 'trigger'
-- name TEXT, -- 对象名称(如 'users')
-- tbl_name TEXT, -- 所属表名(索引时为被索引的表名)
-- rootpage INTEGER, -- 该对象 B-Tree 的根页页号
-- sql TEXT -- 创建该对象的 SQL 语句
-- );
对我们最关键的两列是 name(表名)和 rootpage(根页号)。
验证一下:
sqlite3 test.db "SELECT type, name, rootpage FROM sqlite_schema;"
table|users|2
果然,users 表的根页是 Page 2。
二、解析 sqlite_schema
sqlite_schema 的根页永远是 Page 1。我们已经有了所有工具——遍历 B-Tree、解析 Record——直接复用就好。唯一的特殊性是 Page 1 的页头从偏移 100 开始(文件头占了前 100 字节),这在第 02、03 篇已经处理过了。
// src/schema.rs
use crate::btree::collect_leaf_cells;
use crate::pager::Pager;
use crate::record::{parse_leaf_cell, Value};
/// sqlite_schema 中一行记录的结构化表示
#[derive(Debug)]
pub struct SchemaEntry {
pub object_type: String, // 'table', 'index', 'view', 'trigger'
pub name: String, // 对象名
pub tbl_name: String, // 所属表名
pub root_page: u32, // B-Tree 根页页号
pub sql: Option<String>, // CREATE SQL,可能为 NULL
}
/// 读取 sqlite_schema 表的所有行,返回结构化列表
pub fn read_schema(pager: &mut Pager) -> Vec<SchemaEntry> {
// sqlite_schema 根页永远是 Page 1
let cells = collect_leaf_cells(pager, 1);
let mut entries = Vec::new();
for cell_addr in &cells {
let page_data = pager.read_page(cell_addr.page_num).unwrap();
let (_rowid, values) = parse_leaf_cell(&page_data, cell_addr.offset as usize);
// sqlite_schema 的列顺序:type, name, tbl_name, rootpage, sql
if values.len() < 4 {
continue;
}
let object_type = match &values[0] {
Value::Text(s) => s.clone(),
_ => continue,
};
let name = match &values[1] {
Value::Text(s) => s.clone(),
_ => continue,
};
let tbl_name = match &values[2] {
Value::Text(s) => s.clone(),
_ => continue,
};
let root_page = match &values[3] {
Value::Integer(n) => *n as u32,
_ => continue,
};
let sql = match values.get(4) {
Some(Value::Text(s)) => Some(s.clone()),
_ => None,
};
entries.push(SchemaEntry { object_type, name, tbl_name, root_page, sql });
}
entries
}
/// 按表名查找根页号,找不到返回 None
pub fn find_table_root_page(pager: &mut Pager, table_name: &str) -> Option<u32> {
let entries = read_schema(pager);
entries.iter()
.find(|e| e.object_type == "table" && e.name == table_name)
.map(|e| e.root_page)
}
三、实现 .tables 命令
更新 main.rs,支持两种用法:
sqlite-rs <db> .tables— 列出所有表名sqlite-rs <db> <table_name>— 全表扫描并打印数据
// src/main.rs
mod header;
mod page;
mod pager;
mod btree;
mod varint;
mod record;
mod schema;
use header::DbHeader;
use pager::Pager;
use record::{parse_leaf_cell, Value};
use btree::collect_leaf_cells;
use schema::{read_schema, find_table_root_page};
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 3 {
eprintln!("Usage: sqlite-rs <database.db> <.tables | table_name>");
std::process::exit(1);
}
let db_path = &args[1];
let command = &args[2];
let db_header = DbHeader::read_from_file(db_path).unwrap_or_else(|e| {
eprintln!("Error: {}", e);
std::process::exit(1);
});
let mut pager = Pager::open(db_path, db_header.page_size).unwrap();
match command.as_str() {
".tables" => cmd_tables(&mut pager),
table_name => cmd_scan(&mut pager, table_name),
}
}
/// .tables:列出所有用户表名
fn cmd_tables(pager: &mut Pager) {
let entries = read_schema(pager);
let tables: Vec<&str> = entries.iter()
.filter(|e| e.object_type == "table")
.map(|e| e.name.as_str())
.collect();
println!("{}", tables.join(" "));
}
/// 全表扫描指定表,打印所有行
fn cmd_scan(pager: &mut Pager, table_name: &str) {
let root_page = find_table_root_page(pager, table_name)
.unwrap_or_else(|| {
eprintln!("Error: table '{}' not found", table_name);
std::process::exit(1);
});
let cells = collect_leaf_cells(pager, root_page);
for cell_addr in &cells {
let page_data = pager.read_page(cell_addr.page_num).unwrap();
let (rowid, values) = parse_leaf_cell(&page_data, cell_addr.offset as usize);
let cols: Vec<String> = values.iter().map(|v| match v {
Value::Null => "NULL".to_string(),
Value::Integer(n) => n.to_string(),
Value::Float(f) => f.to_string(),
Value::Text(s) => s.clone(),
Value::Blob(b) => format!("<blob {}>", b.len()),
}).collect();
// 第一列是 INTEGER PRIMARY KEY 时,把 rowid 插回第一列位置
// (sqlite_schema 里 rootpage 是整数,不是 rowid 别名,所以要判断)
println!("{}", cols.join("|"));
}
}
四、测试
单表数据库
cargo run -- test.db .tables
users
cargo run -- test.db users
Alice|30
Bob|25
Charlie|35
多表数据库
sqlite3 multi.db "
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, dept TEXT);
CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT, budget REAL);
CREATE TABLE projects (id INTEGER PRIMARY KEY, title TEXT, owner_id INTEGER);
INSERT INTO employees VALUES (1,'Alice','Engineering'),(2,'Bob','Marketing');
INSERT INTO departments VALUES (1,'Engineering',500000.0),(2,'Marketing',200000.0);
INSERT INTO projects VALUES (1,'Rewrite in Rust',1),(2,'Brand Campaign',2);
"
cargo run -- multi.db .tables
employees departments projects
cargo run -- multi.db departments
Engineering|500000
Marketing|200000
cargo run -- multi.db projects
Rewrite in Rust|1
Brand Campaign|2
所有表都能正确识别和扫描,不再需要硬编码根页号。
五、sqlite_schema 的几个特殊细节
rootpage 为 0
视图(VIEW)和触发器(TRIGGER)没有 B-Tree,它们的 rootpage 为 0。我们在 find_table_root_page 里只查找 type = 'table' 的行,所以不受影响。
内部表(sqlite_ 前缀)
SQLite 内部使用的辅助表(如 sqlite_sequence,用于 AUTOINCREMENT)也存在 sqlite_schema 里,但它们的名字以 sqlite_ 开头。.tables 命令通常过滤掉这些:
fn cmd_tables(pager: &mut Pager) {
let entries = read_schema(pager);
let tables: Vec<&str> = entries.iter()
.filter(|e| e.object_type == "table"
&& !e.name.starts_with("sqlite_")) // 过滤内部表
.map(|e| e.name.as_str())
.collect();
println!("{}", tables.join(" "));
}
旧版本兼容名
SQLite 3.33.0 之前,这张表叫 sqlite_master,之后改名为 sqlite_schema,两个名字都能用。我们直接读 Page 1 的 B-Tree,不依赖名字,天然兼容两种叫法。
六、当前代码结构
sqlite-rs/
└── src/
├── main.rs ← .tables 和全表扫描命令(本篇)
├── header.rs ← 文件头(第 01 篇)
├── page.rs ← 页头 + Cell 指针(第 02 篇)
├── pager.rs ← 页读取器(第 02 篇)
├── btree.rs ← B-Tree 遍历(第 03 篇)
├── varint.rs ← varint 解码(第 04 篇)
├── record.rs ← Record 解析(第 04 篇)
└── schema.rs ← sqlite_schema 解析(本篇)
七、关键点总结
sqlite_schema是 SQLite 的元数据表,存在 Page 1,有 5 列:type / name / tbl_name / rootpage / sql- 用已有的 B-Tree 遍历 + Record 解析就能直接读它,不需要任何特殊处理
- 通过
name查rootpage,就能动态找到任意用户表的根页,彻底告别硬编码 - 过滤
type = 'table'且!name.starts_with("sqlite_")得到所有用户表 - VIEW 和 TRIGGER 的 rootpage 为 0,不能对它们做 B-Tree 遍历
下一篇:实现 SELECT * FROM table——在全表扫描的基础上,解析 sqlite_schema.sql 字段拿到列名,让输出从 Alice|30 变成带列名的格式,同时正确处理 INTEGER PRIMARY KEY 别名 rowid 的情况。