修复bug

This commit is contained in:
LIm1tZz7 committed 2026-10-11 17:32:12 +08:00
1 parent fc9bbc20e4
commit e87c4ee6a8
90 files changed
+3828 -452

No files matched your search

+78 -16
View File
@@ -4,6 +4,68 @@ use serde_json::json;
// 数据字典测试:补列、去重、未知表。
/// 结构变化递增版本,数据及重复应用不变
#[test]
fn schema_versions_track_structure_and_survive_reload() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
let mut c = Catalog::default();
c.create_table("t", vec![]).unwrap();
assert_eq!(c.describe("t").unwrap().schema_version, 1);
let values = row(&[("id", json!(1)), ("name", json!("a"))]);
c.record_insert("t", &values).unwrap();
assert_eq!(c.describe("t").unwrap().schema_version, 2);
c.rebuild_stats("t", &[values]).unwrap();
let columns = c.describe("t").unwrap().columns.clone();
c.set_columns("t", columns).unwrap();
assert_eq!(c.describe("t").unwrap().schema_version, 2);
c.remove_column("t", "name").unwrap();
c.remove_column("t", "name").unwrap();
assert_eq!(c.describe("t").unwrap().schema_version, 3);
c.rename_table("t", "renamed").unwrap();
c.save(&path).unwrap();
let c = Catalog::load(&path).unwrap();
assert_eq!(c.describe("renamed").unwrap().schema_version, 4);
}
/// 旧目录版本未知,首个结构变更才递增
#[test]
fn legacy_schema_version_remains_unknown_until_change() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
std::fs::write(&path, json!({"tables": {
"t": {"object_id": 42, "columns": []}
}}).to_string()).unwrap();
let mut c = Catalog::load(&path).unwrap();
assert_eq!(c.describe("t").unwrap().schema_version, 0);
c.save(&path).unwrap();
c = Catalog::load(&path).unwrap();
assert_eq!(c.describe("t").unwrap().schema_version, 0);
c.ensure_columns("t", &row(&[("id", json!(1))])).unwrap();
assert_eq!(c.describe("t").unwrap().schema_version, 1);
assert_eq!(c.describe("t").unwrap().object_id, 42);
}
/// 版本耗尽拒绝结构变更并保留原定义
#[test]
fn exhausted_schema_version_preserves_structure() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
std::fs::write(&path, json!({"tables": {
"t": {"object_id": 42, "schema_version": u64::MAX,
"columns": [{"name": "id", "ty": "int"}]}
}}).to_string()).unwrap();
let mut c = Catalog::load(&path).unwrap();
assert!(c.remove_column("t", "id").is_err());
assert!(c.set_columns("t", vec![]).is_err());
assert!(c.rename_table("t", "other").is_err());
assert!(c.ensure_columns("t", &row(&[("new", json!(1))])).is_err());
assert_eq!(c.describe("t").unwrap().columns.len(), 1);
assert_eq!(c.describe("t").unwrap().schema_version, u64::MAX);
assert!(c.describe("other").is_none());
c.record_insert("t", &row(&[("id", json!(1))])).unwrap();
}
/// 表删除重建和重载均保留编号规则
#[test]
fn object_ids_survive_reload_and_are_not_reused() {
@@ -80,7 +142,7 @@ fn ensure_then_describe() {
let path = dir.path().join("catalog.json");
let mut c = Catalog::load(&path).unwrap();
c.ensure_columns("users", &row(&[("id", json!(1)), ("name", json!("Alice"))]));
c.ensure_columns("users", &row(&[("id", json!(1)), ("name", json!("Alice"))])).unwrap();
c.save(&path).unwrap();
let c2 = Catalog::load(&path).unwrap();
@@ -102,8 +164,8 @@ fn second_insert_does_not_duplicate_columns() {
let path = dir.path().join("catalog.json");
let mut c = Catalog::load(&path).unwrap();
c.ensure_columns("users", &row(&[("id", json!(1))]));
c.ensure_columns("users", &row(&[("id", json!(2))]));
c.ensure_columns("users", &row(&[("id", json!(1))])).unwrap();
c.ensure_columns("users", &row(&[("id", json!(2))])).unwrap();
assert_eq!(c.describe("users").unwrap().columns.len(), 1);
}
@@ -114,9 +176,9 @@ fn remove_column_drops_definition_stats_and_index() {
let path = dir.path().join("catalog.json");
let mut c = Catalog::load(&path).unwrap();
c.ensure_columns("t", &row(&[("id", json!(1)), ("code", json!(5))]));
c.ensure_columns("t", &row(&[("id", json!(1)), ("code", json!(5))])).unwrap();
c.add_index("t", "code").unwrap();
c.rebuild_stats("t", &[row(&[("id", json!(1)), ("code", json!(5))])]);
c.rebuild_stats("t", &[row(&[("id", json!(1)), ("code", json!(5))])]).unwrap();
let removed = c.remove_column("t", "code").unwrap();
assert!(removed, "first remove should hit the column");
@@ -142,7 +204,7 @@ fn stats_track_a_histogram_for_numeric_columns() {
let rows: Vec<Row> = (0..100)
.map(|n| row(&[("id", json!(n)), ("name", json!(format!("u{}", n)))]))
.collect();
c.rebuild_stats("t", &rows);
c.rebuild_stats("t", &rows).unwrap();
let s = c.describe("t").unwrap();
let id = s.stats.get("id").unwrap();
@@ -155,7 +217,7 @@ fn stats_track_a_histogram_for_numeric_columns() {
assert!(name.hist.is_empty(), "字符串列不进直方图");
assert_eq!(name.lo, None);
c.record_insert("t", &row(&[("id", json!(1000)), ("name", json!("x"))]));
c.record_insert("t", &row(&[("id", json!(1000)), ("name", json!("x"))])).unwrap();
let s = c.describe("t").unwrap();
let id = s.stats.get("id").unwrap();
assert_eq!(id.lo, Some(0.0));
@@ -178,7 +240,7 @@ fn stats_track_a_histogram_for_numeric_columns() {
fn duplicate_deletion_keeps_distinct_until_last_occurrence() {
let mut catalog = Catalog::default();
let duplicate = row(&[("id", json!(1)), ("v", json!(7))]);
catalog.rebuild_stats("t", &[duplicate.clone(), duplicate.clone()]);
catalog.rebuild_stats("t", &[duplicate.clone(), duplicate.clone()]).unwrap();
catalog.record_delete("t", std::slice::from_ref(&duplicate));
assert_eq!(catalog.describe("t").unwrap().stats["v"].distinct, 1);
catalog.record_delete("t", &[duplicate]);
@@ -190,7 +252,7 @@ fn duplicate_deletion_keeps_distinct_until_last_occurrence() {
fn capped_distinct_keeps_histogram_and_other_columns_accurate() {
let mut catalog = Catalog::default();
let rows: Vec<Row> = (0..5000).map(|n| row(&[("id", json!(n)), ("v", json!(n % 2))])).collect();
catalog.rebuild_stats("t", &rows);
catalog.rebuild_stats("t", &rows).unwrap();
let schema = catalog.describe("t").unwrap();
assert!(schema.stats["id"].capped);
assert_eq!(schema.stats["id"].hist.iter().sum::<u64>(), 5000);
@@ -199,7 +261,7 @@ fn capped_distinct_keeps_histogram_and_other_columns_accurate() {
let schema = catalog.describe("t").unwrap();
assert_eq!(schema.stats["id"].hist.iter().sum::<u64>(), 4000);
assert_eq!(schema.stats["v"].distinct, 2);
catalog.rebuild_stats("t", &rows[1000..]);
catalog.rebuild_stats("t", &rows[1000..]).unwrap();
assert_eq!(catalog.describe("t").unwrap().stats["id"].distinct, 4000);
}
@@ -208,16 +270,16 @@ fn capped_distinct_keeps_histogram_and_other_columns_accurate() {
fn rebuilt_histogram_has_exact_buckets_in_any_order() {
let rows: Vec<Row> = (0..160).map(|n| row(&[("v", json!(n))])).collect();
let mut forward = Catalog::default();
forward.rebuild_stats("t", &rows);
forward.rebuild_stats("t", &rows).unwrap();
let mut reverse = Catalog::default();
let reversed: Vec<Row> = rows.iter().rev().cloned().collect();
reverse.rebuild_stats("t", &reversed);
reverse.rebuild_stats("t", &reversed).unwrap();
let stat = &forward.describe("t").unwrap().stats["v"];
assert_eq!(stat.hist, vec![10; 16]);
assert_eq!(stat.hist, reverse.describe("t").unwrap().stats["v"].hist);
forward.record_insert("t", &row(&[("v", json!(10000))]));
forward.record_insert("t", &row(&[("v", json!(10000))])).unwrap();
forward.record_delete("t", &[row(&[("v", json!(10000))])]);
forward.rebuild_stats("t", &rows[80..]);
forward.rebuild_stats("t", &rows[80..]).unwrap();
let repaired = &forward.describe("t").unwrap().stats["v"];
assert_eq!(repaired.lo, Some(80.0));
assert_eq!(repaired.hi, Some(159.0));
@@ -253,8 +315,8 @@ fn legacy_catalog_loads_and_normalizes() {
let s = c.describe("legacy").unwrap();
assert_eq!(s.columns.len(), 2);
assert!(s.columns[0].nullable, "老列默认可空");
assert!(c.normalize_types());
assert!(c.normalize_types().unwrap());
let types: Vec<&str> = c.describe("legacy").unwrap().columns.iter().map(|col| col.ty.as_str()).collect();
assert_eq!(types, vec!["int", "str"]);
assert!(!c.normalize_types(), "第二次没有可改的");
assert!(!c.normalize_types().unwrap(), "第二次没有可改的");
}
+91
View File
@@ -0,0 +1,91 @@
use std::ffi::CString;
use std::ptr;
use std::slice;
use chusql_core_storage::ffi::*;
use chusql_core_storage::runtime::Storage;
// 验证 FFI 错误输出、长度边界与响应所有权。
/// 读取当前线程的 FFI 错误
fn last_error() -> String {
let mut len = 0;
// SAFETY: 长度可写,返回缓冲在复制前没有被修改。
unsafe {
let data = chusql_storage_last_error(&mut len);
assert!(!data.is_null());
String::from_utf8(slice::from_raw_parts(data, len).to_vec()).unwrap()
}
}
/// 维护失败清空输出并拒绝超长请求
#[test]
fn maintenance_rejects_invalid_arguments_and_resets_outputs() {
let input = b"{}";
let mut out = input.as_ptr() as *mut u8;
let mut len = 99;
// SAFETY: 输出均可写;无效输入应在任何读取之前被拒绝。
unsafe {
assert_eq!(chusql_storage_maintenance(ptr::null(), ptr::null(), 0, &mut out, &mut len), -1);
assert!(out.is_null());
assert_eq!(len, 0);
len = 99;
assert_eq!(chusql_storage_maintenance(ptr::null(), input.as_ptr(), input.len(), ptr::null_mut(), &mut len), -1);
assert_eq!(len, 0);
out = input.as_ptr() as *mut u8;
assert_eq!(chusql_storage_maintenance(ptr::null(), input.as_ptr(), input.len(), &mut out, ptr::null_mut()), -1);
assert!(out.is_null());
assert_eq!(chusql_storage_maintenance(ptr::null(), input.as_ptr(), usize::MAX, &mut out, &mut len), -1);
}
assert!(out.is_null());
assert_eq!(len, 0);
assert_eq!(last_error(), "invalid maintenance argument");
}
/// 请求验证长度和编码并归还响应
#[test]
fn request_validates_input_and_returns_owned_response() -> Result<(), Box<dyn std::error::Error>> {
let temp = tempfile::tempdir()?;
let config = temp.path().join("settings.toml");
std::fs::write(&config, format!("[storage]\ndata_dir = '{}'\nlog_files = '{}'\n[log]\nlevel = 'off'\n", temp.path().join("data").display(), temp.path().join("logs").display()))?;
let storage = Storage::open(config.to_str())?;
let input = br#"{"method":"ping"}"#;
let mut out = ptr::null_mut();
let mut len = 0;
// SAFETY: 句柄和实际请求有效;超长长度必须在构造切片前被拒绝。
unsafe {
assert_eq!(chusql_storage_request(&storage, input.as_ptr(), usize::MAX, &mut out, &mut len), -1);
assert!(out.is_null());
assert_eq!(len, 0);
assert_eq!(last_error(), "invalid argument");
assert_eq!(chusql_storage_request(&storage, [0xff].as_ptr(), 1, &mut out, &mut len), -1);
assert!(last_error().starts_with("request is not valid UTF-8:"));
assert_eq!(chusql_storage_request(&storage, input.as_ptr(), input.len(), &mut out, &mut len), 0);
assert!(!out.is_null());
let response = slice::from_raw_parts(out, len).to_vec();
chusql_storage_free(out, len);
let response: serde_json::Value = serde_json::from_slice(&response)?;
assert_eq!(response["status"], "pong");
}
drop(storage);
let config = CString::new(config.to_str().unwrap())?;
// SAFETY: 路径与输出有效;失败不产生需要释放的缓冲。
unsafe {
assert_eq!(chusql_storage_maintenance(config.as_ptr(), [0xff].as_ptr(), 1, &mut out, &mut len), -1);
}
assert!(out.is_null());
assert_eq!(len, 0);
assert!(!last_error().is_empty());
let maintenance = br#"{"mode":"reset","user":"root","password_hash":"hash","types":[{"name":"int","base_type":"int","parameterized":false}]}"#;
// SAFETY: 维护仅使用本测试临时目录;成功响应复制后由配套函数释放。
unsafe {
assert_eq!(chusql_storage_maintenance(config.as_ptr(), maintenance.as_ptr(), maintenance.len(), &mut out, &mut len), 0, "{}", last_error());
assert!(!out.is_null());
let response = slice::from_raw_parts(out, len).to_vec();
chusql_storage_free(out, len);
let response: serde_json::Value = serde_json::from_slice(&response)?;
assert_eq!(response["status"], "maintenance");
assert_eq!(response["mode"], "reset");
}
Ok(())
}
+126 -1
View File
@@ -171,6 +171,131 @@ fn object_id_survives_committed_create_recovery() -> Result<(), Box<dyn std::err
Ok(())
}
/// 读取复查跨库依赖而非只复查扫描表
#[test]
fn bound_reads_recheck_dependencies_in_other_databases() -> Result<(), Box<dyn std::error::Error>> {
let (_server, _data) = start_server();
let mut c = connect()?;
request_ok(&mut c, serde_json::json!({"method":"create_table","table":"source","columns":[]}))?;
request_ok(&mut c, serde_json::json!({"method":"create_database","database":"dependency"}))?;
request_ok(&mut c, serde_json::json!({"method":"create_table","database":"dependency","table":"related","columns":[]}))?;
let catalog: serde_json::Value = serde_json::from_str(&send_raw(&mut c, r#"{"method":"all_catalogs"}"#))?;
let bindings: Vec<_> = catalog["schemas"].as_array().unwrap().iter().filter(|entry|
entry["table"] == "main.source" || entry["table"] == "dependency.related").map(|entry| {
let mut binding = entry.clone();
binding["database_id"] = binding["database_id"].as_u64().unwrap().to_string().into();
binding["object_id"] = binding["object_id"].as_u64().unwrap().to_string().into();
binding
}).collect();
assert_eq!(bindings.len(), 2);
let request = serde_json::json!({"method":"scan","table":"source","schema_guard":bindings});
let valid: serde_json::Value = serde_json::from_str(&send(&mut c, &request.to_string()))?;
assert_eq!(valid["status"], "guarded");
request_ok(&mut c, serde_json::json!({"method":"replace_schema","database":"dependency","table":"related","columns":[{"name":"new","ty":"int"}],"rows":[]}))?;
let invalid: serde_json::Value = serde_json::from_str(&send(&mut c, &request.to_string()))?;
assert_eq!(invalid["status"], "error");
assert!(invalid["message"].as_str().unwrap().contains("bound query schema changed: dependency.related"));
Ok(())
}
/// 每种读取都复查守卫,拒绝旧版本和写入
#[test]
fn bound_reads_validate_dependencies_and_acknowledge_guards() -> Result<(), Box<dyn std::error::Error>> {
let (_server, _data) = start_server();
let mut c = connect()?;
request_ok(&mut c, serde_json::json!({"method":"create_table","table":"guarded","columns":[{"name":"id","ty":"int"}]}))?;
request_ok(&mut c, serde_json::json!({"method":"insert","table":"guarded","row":{"id":7}}))?;
let catalog: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"list_catalog"}"#))?;
let mut binding = catalog["schemas"][0].clone();
binding["database_id"] = binding["database_id"].as_u64().unwrap().to_string().into();
binding["object_id"] = binding["object_id"].as_u64().unwrap().to_string().into();
for mut request in [
serde_json::json!({"method":"scan","table":"guarded"}),
serde_json::json!({"method":"scan_shard","table":"guarded","shard":0,"shards":1}),
serde_json::json!({"method":"lookup_by_index","table":"guarded","column":"id","key":7}),
serde_json::json!({"method":"range_by_index","table":"guarded","column":"id","lo":null,"hi":null}),
] {
request["schema_guard"] = serde_json::json!([binding]);
let response: serde_json::Value = serde_json::from_str(&send(&mut c, &request.to_string()))?;
assert_eq!(response["status"], "guarded", "{response}");
assert!(response["response"]["status"] == "rows" || response["response"]["status"] == "no_index", "{response}");
}
request_ok(&mut c, serde_json::json!({"method":"replace_schema","table":"guarded","columns":[{"name":"id","ty":"int"},{"name":"extra","ty":"str"}],"rows":[{"id":7,"extra":"x"}]}))?;
let stale: serde_json::Value = serde_json::from_str(&send(&mut c, &serde_json::json!({"method":"scan","table":"guarded","schema_guard":[binding]}).to_string()))?;
assert_eq!(stale["status"], "error");
assert!(stale["message"].as_str().unwrap().contains("bound query schema changed"));
let write: serde_json::Value = serde_json::from_str(&send(&mut c, &serde_json::json!({"method":"insert","table":"guarded","row":{"id":9},"schema_guard":[binding]}).to_string()))?;
assert_eq!(write["status"], "error");
let management: serde_json::Value = serde_json::from_str(&send_raw(&mut c, &serde_json::json!({"method":"create_database","database":"blocked","schema_guard":[]}).to_string()))?;
assert_eq!(management["status"], "error");
let databases: serde_json::Value = serde_json::from_str(&send_raw(&mut c, r#"{"method":"list_databases"}"#))?;
assert!(!databases["tables"].as_array().unwrap().iter().any(|database| database == "blocked"));
let rows: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","table":"guarded"}"#))?;
assert_eq!(rows["rows"].as_array().unwrap().len(), 1);
Ok(())
}
/// 描述与目录均透传精确版本字符串
#[test]
fn catalog_schema_versions_are_exact_and_validated() -> Result<(), Box<dyn std::error::Error>> {
use chusql_core_storage::protocol::TableSchemaWire;
let (_server, _data) = start_server();
let mut c = connect()?;
request_ok(&mut c, serde_json::json!({"method":"create_table","table":"versioned","columns":[{"name":"id","ty":"int"}]}))?;
let describe: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"describe_table","table":"versioned"}"#))?;
assert_eq!(describe["schema_version"], "1");
let catalog: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"list_catalog"}"#))?;
assert_eq!(catalog["schemas"][0]["schema_version"], "1");
let wire: TableSchemaWire = serde_json::from_value(serde_json::json!({"table":"t","columns":[],"schema_version":u64::MAX.to_string()}))?;
assert_eq!(wire.schema_version, u64::MAX);
assert_eq!(serde_json::to_value(wire)?["schema_version"], u64::MAX.to_string());
for version in [serde_json::json!(-1), serde_json::json!(1.5), serde_json::json!("18446744073709551616"), serde_json::json!("+1")] {
assert!(serde_json::from_value::<TableSchemaWire>(serde_json::json!({"table":"t","columns":[],"schema_version":version})).is_err());
}
Ok(())
}
/// 检查点失败及反复恢复保留结构版本
#[test]
fn schema_versions_survive_checkpoint_failure_and_repeated_recovery() -> Result<(), Box<dyn std::error::Error>> {
use chusql_core_storage::catalog::Catalog;
use chusql_core_storage::wal::{Wal, WalOp};
let (server, data) = start_server();
let mut c = connect()?;
request_ok(&mut c, serde_json::json!({"method":"create_table","table":"versioned","columns":[{"name":"id","ty":"int"}]}))?;
let directory = data.path().join("databases/main");
let pending = directory.join("wal.checkpoint.pending");
std::fs::create_dir(&pending)?;
let response = send(&mut c, r#"{"method":"replace_schema","table":"versioned","columns":[{"name":"id","ty":"int"},{"name":"tag","ty":"str"}],"rows":[]}"#);
assert!(response.contains("error"), "{response}");
let catalog = Catalog::load(directory.join("catalog.json"))?;
let version = catalog.describe("versioned").unwrap().schema_version;
assert_eq!(version, 2);
let wal = Wal::new(directory.join("wal.log"));
let records = wal.read_all()?.records;
assert!(records.iter().any(|(_, op)| matches!(op, WalOp::ReplaceSchema { schema_version: Some(2), .. })));
let columns = catalog.describe("versioned").unwrap().columns.clone();
drop(wal);
drop(c);
drop(server);
std::fs::remove_dir(pending)?;
for _ in 0..2 {
let wal = Wal::new(directory.join("wal.log"));
wal.append_group(&[
WalOp::ReplaceSchema { table: "versioned".into(), columns: vec![], rows: vec![], schema_version: Some(1) },
WalOp::ReplaceSchema { table: "versioned".into(), columns: columns.clone(), rows: vec![], schema_version: Some(version) },
])?;
drop(wal);
let restarted = restart_at(data.path())?;
let catalog = Catalog::load(directory.join("catalog.json"))?;
assert_eq!(catalog.describe("versioned").unwrap().schema_version, version);
assert_eq!(catalog.describe("versioned").unwrap().columns, columns);
assert_eq!(std::fs::metadata(directory.join("wal.log"))?.len(), 0);
drop(restarted);
}
Ok(())
}
/// 并发建表分配互不重复的编号
#[test]
fn concurrent_table_creation_allocates_distinct_object_ids() -> Result<(), Box<dyn std::error::Error>> {
@@ -1026,7 +1151,7 @@ fn hidden_column_wal_replays_twice_without_touching_heap() -> Result<(), Box<dyn
let mut compacted: Option<Vec<u8>> = None;
for round in 0..2 {
let wal = Wal::new(data.path().join("databases/main/wal.log"));
let op = WalOp::HideColumn { table: "items".into(), column: "secret".into() };
let op = WalOp::HideColumn { table: "items".into(), column: "secret".into(), schema_version: None };
wal.truncate()?;
wal.append(&op)?;
assert_eq!(wal.read()?, Some(op));
+28
View File
@@ -4,6 +4,33 @@ use serde_json::json;
// WAL 测试:追加、读取、清空、截断尾巴、LSN 与提交标记。
/// 结构版本往返且旧隐藏列载荷可读
#[test]
fn schema_versions_round_trip_and_legacy_hide_decodes() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let wal = Wal::new(&path);
let ops = [
WalOp::HideColumn { table: "t".into(), column: "name".into(), schema_version: Some(u64::MAX) },
WalOp::DropColumn { table: "t".into(), column: "name".into(), rows: vec![], schema_version: Some(3) },
WalOp::ReplaceSchema { table: "t".into(), columns: vec![], rows: vec![], schema_version: Some(4) },
];
wal.append_group(&ops).unwrap();
assert_eq!(committed_ops(&wal.read_all().unwrap().records).into_iter().map(|(_, op)| op).collect::<Vec<_>>(), ops);
drop(wal);
let mut body = 1_u64.to_le_bytes().to_vec();
body.push(8);
body.extend_from_slice(&1_u16.to_le_bytes());
body.extend_from_slice(b"t");
body.extend_from_slice(br#""name""#);
let mut frame = (body.len() as u32).to_le_bytes().to_vec();
frame.extend_from_slice(&body);
std::fs::write(&path, frame).unwrap();
assert_eq!(Wal::new(&path).read().unwrap(), Some(WalOp::HideColumn {
table: "t".into(), column: "name".into(), schema_version: None,
}));
}
/// 建表编号完整往返且兼容旧载荷
#[test]
fn create_table_ids_round_trip_and_legacy_payloads_decode() {
@@ -58,6 +85,7 @@ fn append_and_read_round_trips() {
WalOp::DropColumn {
table: "users".into(),
column: "age".into(),
schema_version: None,
rows: vec![
row(&[("id", json!(1)), ("name", json!("Alice"))]),
row(&[("id", json!(2)), ("name", json!("Bob"))]),