使用React js完成web前端
This commit is contained in:
1 parent
9e8c5f1ef0
commit
7600f0f9ed
130 files changed
+12708
-788
No files matched your search
@@ -169,21 +169,19 @@ fn delete_removes_only_that_key() {
|
||||
t.insert(i, i as u64 + 1).unwrap();
|
||||
}
|
||||
|
||||
assert!(t.delete(50).unwrap(), "删一个存在的键应该返回 true");
|
||||
assert!(t.delete(50).unwrap(), "deleting an existing key should return true");
|
||||
assert_eq!(t.get(50).unwrap(), None);
|
||||
assert_eq!(t.get(49).unwrap(), Some(50));
|
||||
assert_eq!(t.get(51).unwrap(), Some(52));
|
||||
|
||||
// 再删一次:它已经不在了
|
||||
assert!(!t.delete(50).unwrap());
|
||||
|
||||
// 遍历里也不能再有它
|
||||
let all = t.iter_all().unwrap();
|
||||
assert_eq!(all.len(), 199);
|
||||
assert!(all.iter().all(|(k, _)| *k != 50));
|
||||
}
|
||||
|
||||
/// 删过之后树还能继续插(惰性删除会留下稀疏节点,但不能影响正确性)
|
||||
/// 删过之后树还能继续插
|
||||
#[test]
|
||||
fn insert_after_delete_still_works() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@@ -197,11 +195,9 @@ fn insert_after_delete_still_works() {
|
||||
for i in 0..300 {
|
||||
t.insert(i, i as u64).unwrap();
|
||||
}
|
||||
// 把偶数键全删掉
|
||||
for i in (0..300).step_by(2) {
|
||||
assert!(t.delete(i).unwrap());
|
||||
}
|
||||
// 再插一批新的
|
||||
for i in 1000..1100 {
|
||||
t.insert(i, i as u64).unwrap();
|
||||
}
|
||||
|
||||
@@ -65,3 +65,28 @@ fn describe_unknown_returns_none() {
|
||||
let c = Catalog::load(&path).unwrap();
|
||||
assert!(c.describe("nope").is_none());
|
||||
}
|
||||
|
||||
/// 删列同时清掉统计与索引
|
||||
#[test]
|
||||
fn remove_column_drops_definition_stats_and_index() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
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.add_index("t", "code").unwrap();
|
||||
c.rebuild_stats("t", &[row(&[("id", json!(1)), ("code", json!(5))])]);
|
||||
|
||||
let removed = c.remove_column("t", "code").unwrap();
|
||||
assert!(removed, "first remove should hit the column");
|
||||
let s = c.describe("t").unwrap();
|
||||
assert_eq!(s.columns.len(), 1);
|
||||
assert_eq!(s.columns[0].name, "id");
|
||||
assert!(s.indexes.is_empty(), "index on code should be gone");
|
||||
assert!(s.stats.get("code").is_none(), "stats for code should be gone");
|
||||
|
||||
let again = c.remove_column("t", "code").unwrap();
|
||||
assert!(!again);
|
||||
|
||||
assert!(c.remove_column("nope", "code").is_err());
|
||||
}
|
||||
@@ -135,9 +135,9 @@ fn replace_all_rebuilds_index() {
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(t.get_by_key(1).unwrap().is_none(), "旧键必须作废");
|
||||
assert!(t.get_by_key(1).unwrap().is_none(), "old key must be invalidated");
|
||||
assert!(t.get_by_key(2).unwrap().is_none());
|
||||
let got = t.get_by_key(7).unwrap().expect("新行应该按 id 建好索引");
|
||||
let got = t.get_by_key(7).unwrap().expect("new row should be indexed by id");
|
||||
assert_eq!(got["name"], json!("Zoe"));
|
||||
assert_eq!(t.scan().unwrap().len(), 2);
|
||||
}
|
||||
@@ -161,7 +161,7 @@ fn delete_by_keys_removes_rows() {
|
||||
}
|
||||
|
||||
let deleted = t.delete_by_keys(&[2, 4, 99]).unwrap();
|
||||
assert_eq!(deleted.len(), 2, "只有 2、4 真的删掉了");
|
||||
assert_eq!(deleted.len(), 2, "only 2 and 4 were actually deleted");
|
||||
|
||||
let ids: Vec<i64> = t
|
||||
.scan()
|
||||
@@ -169,13 +169,12 @@ fn delete_by_keys_removes_rows() {
|
||||
.iter()
|
||||
.map(|r| r["id"].as_i64().unwrap())
|
||||
.collect();
|
||||
assert_eq!(ids, vec![1, 3, 5], "剩下的行要保持原顺序");
|
||||
assert_eq!(ids, vec![1, 3, 5], "remaining rows must keep their order");
|
||||
|
||||
assert!(t.get_by_key(2).unwrap().is_none(), "索引条目要跟着删");
|
||||
assert!(t.get_by_key(2).unwrap().is_none(), "index entry should be deleted too");
|
||||
assert!(t.get_by_key(4).unwrap().is_none());
|
||||
assert!(t.get_by_key(3).unwrap().is_some());
|
||||
|
||||
// 删掉的位置可以再插回来
|
||||
t.insert_row(&row(&[("id", json!(2)), ("name", json!("u2b"))]))
|
||||
.unwrap();
|
||||
assert_eq!(t.get_by_key(2).unwrap().unwrap()["name"], json!("u2b"));
|
||||
@@ -202,7 +201,6 @@ fn insert_rows_batch() {
|
||||
assert_eq!(t.scan().unwrap().len(), 20);
|
||||
assert_eq!(t.get_by_key(20).unwrap().unwrap()["name"], json!("u20"));
|
||||
|
||||
// 批里出现重复键要报错(整批之前就查出来,不会插一半)
|
||||
let dup = vec![
|
||||
row(&[("id", json!(100))]),
|
||||
row(&[("id", json!(100))]),
|
||||
@@ -241,16 +239,13 @@ fn secondary_index_is_maintained() {
|
||||
assert!(t.has_index("code"));
|
||||
assert_eq!(t.get_by_column_key("code", 7002).unwrap().unwrap()["id"], json!(2));
|
||||
|
||||
// 建索引之后新插入的行也进索引
|
||||
t.insert_row(&row(&[("id", json!(3)), ("code", json!(7003))]))
|
||||
.unwrap();
|
||||
assert_eq!(t.get_by_column_key("code", 7003).unwrap().unwrap()["id"], json!(3));
|
||||
|
||||
// 删掉一行,索引条目跟着走
|
||||
t.delete_by_keys(&[2]).unwrap();
|
||||
assert!(t.get_by_column_key("code", 7002).unwrap().is_none());
|
||||
|
||||
// code 已经有索引,重复的 code 插不进去(索引列是唯一的)
|
||||
assert!(t.insert_row(&row(&[("id", json!(4)), ("code", json!(7001))])).is_err());
|
||||
let mut t2 = HeapTable::open(&dir.path().join("d.db"), DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||
t2.insert_rows(&[
|
||||
@@ -265,8 +260,8 @@ fn secondary_index_is_maintained() {
|
||||
chusql_storage::config::DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
);
|
||||
assert!(err.is_err(), "重复值不给建索引");
|
||||
assert!(!t2.has_index("age"), "失败之后不留半成品");
|
||||
assert!(err.is_err(), "duplicate values must not be indexed");
|
||||
assert!(!t2.has_index("age"), "no half-built index after failure");
|
||||
}
|
||||
|
||||
/// 同一个 id 插两次要报错
|
||||
@@ -285,6 +280,6 @@ fn duplicate_key_is_rejected() {
|
||||
t.insert_row(&row(&[("id", json!(1))])).unwrap();
|
||||
let r = t.insert_row(&row(&[("id", json!(1))]));
|
||||
assert!(r.is_err());
|
||||
assert_eq!(t.scan().unwrap().len(), 1, "失败的那一行不应该进堆表");
|
||||
assert_eq!(t.scan().unwrap().len(), 1, "the failed row should not reach the heap");
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ fn unique_pipe_name() -> String {
|
||||
format!("chusql-test-{}-{}", id, ns)
|
||||
}
|
||||
|
||||
/// 持有 server 子进程,退出时杀掉
|
||||
struct ServerProc(Child);
|
||||
|
||||
impl Drop for ServerProc {
|
||||
@@ -36,7 +35,7 @@ impl Drop for ServerProc {
|
||||
fn start_server(pipe: &str) -> (ServerProc, tempfile::TempDir) {
|
||||
let data = tempfile::tempdir().unwrap();
|
||||
|
||||
let child = Command::new(env!("CARGO_BIN_EXE_server"))
|
||||
let child = Command::new(env!("CARGO_BIN_EXE_chusql-storage"))
|
||||
.env("CHUSQL_PIPE", pipe)
|
||||
.env("CHUSQL_DATA_DIR", data.path())
|
||||
.env("CHUSQL_PAGE_SIZE", "4096")
|
||||
@@ -251,7 +250,7 @@ fn delete_keys_removes_row_and_index_entry() {
|
||||
assert!(r.contains(r#""rows":[]"#), "index entry should be gone: {}", r);
|
||||
|
||||
let r = send(&mut c, r#"{"method":"insert","table":"del_t","row":{"id":2,"name":"Bob2"}}"#);
|
||||
assert!(r.contains(r#""status":"ok""#), "id 2 应该能再插进来: {}", r);
|
||||
assert!(r.contains(r#""status":"ok""#), "id 2 should be insertable again: {}", r);
|
||||
}
|
||||
|
||||
/// 给第二列建索引后能按那一列查
|
||||
@@ -269,7 +268,6 @@ fn create_index_on_secondary_column() {
|
||||
send(&mut c, &req);
|
||||
}
|
||||
|
||||
// 没建之前:明确回 no_index,而不是回空结果
|
||||
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"sec_t","column":"code","key":5002}"#);
|
||||
assert!(r.contains(r#""status":"no_index""#), "got: {}", r);
|
||||
|
||||
@@ -279,23 +277,64 @@ fn create_index_on_secondary_column() {
|
||||
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"sec_t","column":"code","key":5002}"#);
|
||||
assert!(r.contains(r#""id":2"#), "lookup by code: {}", r);
|
||||
|
||||
// 建完索引之后新插入的行也要进索引
|
||||
send(&mut c, r#"{"method":"insert","table":"sec_t","row":{"id":4,"code":5004}}"#);
|
||||
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"sec_t","column":"code","key":5004}"#);
|
||||
assert!(r.contains(r#""id":4"#), "lookup new row: {}", r);
|
||||
|
||||
// schema 里能看到这条索引
|
||||
let r = send(&mut c, r#"{"method":"describe_table","table":"sec_t"}"#);
|
||||
assert!(r.contains(r#""column":"code""#), "describe: {}", r);
|
||||
|
||||
// 删掉索引后回到 no_index
|
||||
let r = send(&mut c, r#"{"method":"drop_index","table":"sec_t","column":"code"}"#);
|
||||
assert!(r.contains(r#""status":"ok""#), "drop_index: {}", r);
|
||||
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"sec_t","column":"code","key":5002}"#);
|
||||
assert!(r.contains(r#""status":"no_index""#), "after drop: {}", r);
|
||||
}
|
||||
|
||||
/// 有重复值的列不给建索引(不然"走索引"和"全表扫"会给出不同答案)
|
||||
/// 删列同时摘掉索引与行内该格
|
||||
#[test]
|
||||
fn drop_column_removes_column_index_and_values() {
|
||||
let pipe = unique_pipe_name();
|
||||
let (_srv, _data) = start_server(&pipe);
|
||||
let mut c = connect(&pipe).unwrap();
|
||||
|
||||
for (id, code, note) in [(1, 5001, "a"), (2, 5002, "b")] {
|
||||
let req = format!(
|
||||
r#"{{"method":"insert","table":"drop_t","row":{{"id":{},"code":{},"note":"{}"}}}}"#,
|
||||
id, code, note
|
||||
);
|
||||
send(&mut c, &req);
|
||||
}
|
||||
let r = send(&mut c, r#"{"method":"create_index","table":"drop_t","column":"code"}"#);
|
||||
assert!(r.contains(r#""status":"ok""#), "create_index: {}", r);
|
||||
|
||||
let r = send(&mut c, r#"{"method":"drop_column","table":"drop_t","column":"code"}"#);
|
||||
assert!(r.contains(r#""status":"ok""#), "drop_column: {}", r);
|
||||
|
||||
let r = send(&mut c, r#"{"method":"describe_table","table":"drop_t"}"#);
|
||||
assert!(!r.contains(r#""name":"code""#), "code column should be gone: {}", r);
|
||||
assert!(r.contains(r#""name":"note""#), "note column should still be there: {}", r);
|
||||
assert!(!r.contains(r#""column":"code""#), "index on code should be gone: {}", r);
|
||||
|
||||
let r = send(&mut c, r#"{"method":"scan","table":"drop_t"}"#);
|
||||
assert!(r.contains(r#""note":"a""#), "row should still be there: {}", r);
|
||||
assert!(!r.contains(r#""code""#), "row should no longer carry code: {}", r);
|
||||
|
||||
let r = send(
|
||||
&mut c,
|
||||
r#"{"method":"lookup_by_index","table":"drop_t","column":"code","key":5001}"#,
|
||||
);
|
||||
assert!(r.contains(r#""status":"no_index""#), "{}", r);
|
||||
|
||||
let r = send(&mut c, r#"{"method":"describe_table","table":"drop_t"}"#);
|
||||
assert!(r.contains(r#""row_count":2"#), "row count should still be 2: {}", r);
|
||||
|
||||
let r = send(&mut c, r#"{"method":"drop_column","table":"drop_t","column":"id"}"#);
|
||||
assert!(r.contains(r#""status":"error""#), "the built-in id column must not be dropped: {}", r);
|
||||
let r = send(&mut c, r#"{"method":"drop_column","table":"drop_t","column":"nope"}"#);
|
||||
assert!(r.contains(r#""status":"error""#), "a missing column should error: {}", r);
|
||||
}
|
||||
|
||||
/// 重复值列不给建索引
|
||||
#[test]
|
||||
fn create_index_rejects_duplicate_values() {
|
||||
let pipe = unique_pipe_name();
|
||||
@@ -309,7 +348,6 @@ fn create_index_rejects_duplicate_values() {
|
||||
assert!(r.contains(r#""status":"error""#), "got: {}", r);
|
||||
assert!(r.contains("duplicate value"), "got: {}", r);
|
||||
|
||||
// 失败之后不留半成品:查这一列仍然是 no_index
|
||||
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"dup_t","column":"age","key":30}"#);
|
||||
assert!(r.contains(r#""status":"no_index""#), "got: {}", r);
|
||||
}
|
||||
@@ -331,10 +369,9 @@ fn stats_report_distinct_values() {
|
||||
|
||||
let r = send(&mut c, r#"{"method":"describe_table","table":"stat_t"}"#);
|
||||
assert!(r.contains(r#""row_count":3"#), "row_count: {}", r);
|
||||
assert!(r.contains(r#""name":"age","distinct":2"#), "age 有 2 个不同值: {}", r);
|
||||
assert!(r.contains(r#""name":"id","distinct":3"#), "id 有 3 个不同值: {}", r);
|
||||
assert!(r.contains(r#""name":"age","distinct":2"#), "age has 2 distinct values: {}", r);
|
||||
assert!(r.contains(r#""name":"id","distinct":3"#), "id has 3 distinct values: {}", r);
|
||||
|
||||
// 删掉一行,统计跟着掉
|
||||
send(&mut c, r#"{"method":"delete_keys","table":"stat_t","keys":[1]}"#);
|
||||
let r = send(&mut c, r#"{"method":"describe_table","table":"stat_t"}"#);
|
||||
assert!(r.contains(r#""row_count":2"#), "after delete: {}", r);
|
||||
@@ -348,7 +385,7 @@ fn lookup_survives_reopen() {
|
||||
let data = tempfile::tempdir().unwrap();
|
||||
|
||||
{
|
||||
let exe = env!("CARGO_BIN_EXE_server");
|
||||
let exe = env!("CARGO_BIN_EXE_chusql-storage");
|
||||
let child = Command::new(exe)
|
||||
.env("CHUSQL_PIPE", &pipe)
|
||||
.env("CHUSQL_DATA_DIR", data.path())
|
||||
@@ -375,7 +412,7 @@ fn lookup_survives_reopen() {
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
|
||||
let (_srv, _data) = {
|
||||
let child = Command::new(env!("CARGO_BIN_EXE_server"))
|
||||
let child = Command::new(env!("CARGO_BIN_EXE_chusql-storage"))
|
||||
.env("CHUSQL_PIPE", &pipe)
|
||||
.env("CHUSQL_DATA_DIR", data.path())
|
||||
.stdout(Stdio::null())
|
||||
@@ -410,7 +447,7 @@ fn config_file_is_used() {
|
||||
);
|
||||
std::fs::write(&config_path, text).unwrap();
|
||||
|
||||
let child = Command::new(env!("CARGO_BIN_EXE_server"))
|
||||
let child = Command::new(env!("CARGO_BIN_EXE_chusql-storage"))
|
||||
.env("CHUSQL_CONFIG", &config_path)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
|
||||
@@ -48,6 +48,26 @@ fn append_and_read_replace_all() {
|
||||
assert_eq!(back, op);
|
||||
}
|
||||
|
||||
/// 写读 DropColumn 操作,行随日志落盘
|
||||
#[test]
|
||||
fn append_and_read_drop_column() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let wal = Wal::new(dir.path().join("wal.log"));
|
||||
|
||||
let op = WalOp::DropColumn {
|
||||
table: "users".into(),
|
||||
column: "age".into(),
|
||||
rows: vec![
|
||||
row(&[("id", json!(1)), ("name", json!("Alice"))]),
|
||||
row(&[("id", json!(2)), ("name", json!("Bob"))]),
|
||||
],
|
||||
};
|
||||
wal.append(&op).unwrap();
|
||||
|
||||
let back = wal.read().unwrap().unwrap();
|
||||
assert_eq!(back, op);
|
||||
}
|
||||
|
||||
/// 文件不存在返回 None
|
||||
#[test]
|
||||
fn missing_file_returns_none() {
|
||||
|
||||
Reference in new issue
Block a user