use std::thread;
use rudb::{Config, Database};
use rudb_common::{Clustering, Field, LogicalType, Value, Width};
#[test]
fn a_database_opens_by_name_and_two_spellings_mean_memory() {
assert!(Database::open(":memory:").is_ok());
assert!(Database::open("").is_ok(), "the empty name is the other way to say it");
}
#[test]
fn a_file_name_opens_a_native_database() {
let path = std::env::temp_dir().join(format!("rudb-api-open-{}.rudb", std::process::id()));
let database = Database::open(path.to_str().expect("a UTF-8 temporary path"))
.expect("a file name starts a native database");
database.execute("CREATE TABLE t (a INTEGER)").expect("creates");
database.execute("INSERT INTO t VALUES (1), (2), (3)").expect("inserts");
database.execute("CHECKPOINT").expect("commits");
drop(database);
let reopened = Database::open(path.to_str().expect("a UTF-8 temporary path"))
.expect("the native database reopens");
assert_eq!(reopened.value("SELECT sum(a) FROM t").expect("reads"), Value::HugeInt(6));
std::fs::remove_file(path).expect("removes the temporary database");
}
#[test]
fn several_tables_go_into_one_file_and_come_back_out_of_it() {
let path = std::env::temp_dir().join(format!("rudb-api-many-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
let database = Database::open(&name).expect("a file name starts a native database");
database.execute("CREATE TABLE a (x INTEGER, s VARCHAR)").expect("creates");
database.execute("INSERT INTO a VALUES (1, 'one'), (2, 'two')").expect("inserts");
database.execute("CREATE TABLE b (y BIGINT)").expect("creates the second");
database.execute("INSERT INTO b VALUES (10), (20), (30)").expect("inserts");
database.execute("CHECKPOINT").expect("commits both tables at once");
drop(database);
let reopened = Database::open(&name).expect("the native database reopens");
assert_eq!(reopened.value("SELECT sum(x) FROM a").expect("reads"), Value::HugeInt(3));
assert_eq!(reopened.value("SELECT sum(y) FROM b").expect("reads"), Value::HugeInt(60));
assert_eq!(
reopened
.value("SELECT count(*) FROM a JOIN b ON b.y = a.x * 10")
.expect("joins across the two"),
Value::BigInt(2)
);
reopened.execute("CREATE TABLE c (z INTEGER)").expect("creates the third");
reopened.execute("INSERT INTO c VALUES (5)").expect("inserts");
reopened.execute("CHECKPOINT").expect("commits three");
drop(reopened);
let again = Database::open(&name).expect("the native database reopens again");
assert_eq!(again.value("SELECT sum(z) FROM c").expect("reads"), Value::HugeInt(5));
assert_eq!(
again.value("SELECT sum(x) FROM a").expect("the first is still there"),
Value::HugeInt(3)
);
again.execute("CHECKPOINT").expect("a second checkpoint is not an error");
assert_eq!(again.value("SELECT sum(y) FROM b").expect("reads"), Value::HugeInt(60));
drop(again);
std::fs::remove_file(path).expect("removes the temporary database");
}
#[test]
fn a_committed_table_is_carried_forward_and_not_written_again() {
fn add_one_row_to_a_file_of(rows: u64) -> std::time::Duration {
let path = std::env::temp_dir()
.join(format!("rudb-api-append-{rows}-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
let database = Database::open(&name).expect("a file name starts a native database");
database
.execute(&format!(
"CREATE TABLE big AS SELECT i AS a, i * 2 AS b FROM range(0, {rows}) t(i)"
))
.expect("creates");
database.execute("CHECKPOINT").expect("commits");
database.execute("CREATE TABLE one AS SELECT 1 AS a").expect("creates the second");
let start = std::time::Instant::now();
database.execute("CHECKPOINT").expect("commits the second");
let taken = start.elapsed();
assert_eq!(
database.value("SELECT count(*) FROM big").expect("reads"),
Value::BigInt(rows as i64)
);
assert_eq!(database.value("SELECT sum(a) FROM one").expect("reads"), Value::HugeInt(1));
drop(database);
let reopened = Database::open(&name).expect("the native database reopens");
assert_eq!(
reopened.value("SELECT count(*) FROM big").expect("reads after reopening"),
Value::BigInt(rows as i64)
);
drop(reopened);
std::fs::remove_file(path).expect("removes the temporary database");
taken
}
let small = add_one_row_to_a_file_of(1_000);
let large = add_one_row_to_a_file_of(2_000_000);
assert!(
large < small.max(std::time::Duration::from_millis(50)) * 10,
"adding one row to a file of two million took {large:?} against {small:?} for a file of a \
thousand, which is the whole file being written again"
);
}
#[test]
fn a_load_beside_a_committed_table_reaches_the_file_without_a_checkpoint() {
let path = std::env::temp_dir().join(format!("rudb-api-stream-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
let first = Database::open(&name).expect("a file name starts a native database");
first.execute("CREATE TABLE a (x INTEGER)").expect("creates");
first.execute("INSERT INTO a SELECT * FROM range(100)").expect("inserts");
drop(first);
let second = Database::open(&name).expect("the native database reopens");
second.execute("CREATE TABLE b (y INTEGER, s VARCHAR)").expect("creates the second");
second
.execute("INSERT INTO b SELECT i, 'row' || i FROM range(0, 100) t(i)")
.expect("inserts into the second");
drop(second);
let third = Database::open(&name).expect("the native database reopens again");
assert_eq!(
third.value("SELECT count(*) FROM b").expect("the second table is in the file"),
Value::BigInt(100)
);
assert_eq!(
third.value("SELECT s FROM b WHERE y = 7").expect("reads"),
Value::Varchar("row7".into())
);
assert_eq!(
third.value("SELECT sum(x) FROM a").expect("the first is still there"),
Value::HugeInt(4950)
);
drop(third);
std::fs::remove_file(path).expect("removes the temporary database");
}
#[test]
fn a_create_table_as_select_reaches_the_file_without_a_checkpoint() {
let path = std::env::temp_dir().join(format!("rudb-api-ctas-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
let first = Database::open(&name).expect("a file name starts a native database");
first
.execute("CREATE TABLE a AS SELECT i AS x, 'row' || i AS s FROM range(0, 100) t(i)")
.expect("creates and fills");
drop(first);
let second = Database::open(&name).expect("the native database reopens");
assert_eq!(
second.value("SELECT count(*) FROM a").expect("the first table is in the file"),
Value::BigInt(100)
);
second
.execute("CREATE TABLE b AS SELECT i AS y FROM range(0, 50) t(i)")
.expect("creates the second beside the first");
drop(second);
let third = Database::open(&name).expect("the native database reopens again");
assert_eq!(
third.value("SELECT count(*) FROM b").expect("the second table is in the file"),
Value::BigInt(50)
);
assert_eq!(
third.value("SELECT s FROM a WHERE x = 7").expect("reads"),
Value::Varchar("row7".into())
);
assert_eq!(
third.value("SELECT sum(y) FROM b").expect("the second table reads back"),
Value::HugeInt(1225)
);
drop(third);
std::fs::remove_file(path).expect("removes the temporary database");
}
#[test]
fn a_create_table_as_select_that_fails_leaves_no_table() {
let path = std::env::temp_dir().join(format!("rudb-api-ctas-err-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
let database = Database::open(&name).expect("a file name starts a native database");
database
.execute("CREATE TABLE t AS SELECT CAST('x' || i AS INTEGER) AS c FROM range(0, 100) r(i)")
.expect_err("x1 is not an integer");
database.execute("SELECT count(*) FROM t").expect_err("and the table is not in the catalog");
database
.execute("CREATE TABLE t AS SELECT i AS x FROM range(0, 10) r(i)")
.expect("the name is free");
assert_eq!(database.value("SELECT count(*) FROM t").expect("reads"), Value::BigInt(10));
drop(database);
std::fs::remove_file(path).expect("removes the temporary database");
}
#[test]
fn a_file_takes_one_table_after_another_and_reads_back_every_one() {
let path = std::env::temp_dir().join(format!("rudb-api-slots-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
for table in 1..=5 {
let database = Database::open(&name).expect("the native database opens");
database
.execute(&format!(
"CREATE TABLE t{table} AS SELECT i AS a, 'row' || i AS s FROM range(0, 100) x(i)"
))
.expect("creates");
database.execute("CHECKPOINT").expect("commits");
drop(database);
}
let reopened = Database::open(&name).expect("the native database reopens");
for table in 1..=5 {
assert_eq!(
reopened
.value(&format!("SELECT count(*) FROM t{table}"))
.unwrap_or_else(|error| panic!("t{table} reads back: {error}")),
Value::BigInt(100)
);
assert_eq!(
reopened
.value(&format!("SELECT s FROM t{table} WHERE a = 7"))
.expect("the varchar reads back"),
Value::Varchar("row7".into())
);
}
drop(reopened);
std::fs::remove_file(path).expect("removes the temporary database");
}
#[test]
fn a_dropped_table_is_gone_from_the_file_and_the_rest_are_not() {
let path = std::env::temp_dir().join(format!("rudb-api-drop-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
let database = Database::open(&name).expect("a file name starts a native database");
for table in ["a", "b", "c"] {
database
.execute(&format!("CREATE TABLE {table} AS SELECT {} AS x", table.len()))
.expect("creates");
database.execute("CHECKPOINT").expect("commits");
}
database.execute("DROP TABLE b").expect("drops");
database.execute("CHECKPOINT").expect("commits the drop");
drop(database);
let reopened = Database::open(&name).expect("the native database reopens");
assert_eq!(
reopened.value("SELECT sum(x) FROM a").expect("the first is there"),
Value::HugeInt(1)
);
assert_eq!(
reopened.value("SELECT sum(x) FROM c").expect("the third is there"),
Value::HugeInt(1)
);
assert!(
reopened.execute("SELECT * FROM b").is_err(),
"the dropped table is not in the file the next process opens"
);
drop(reopened);
std::fs::remove_file(path).expect("removes the temporary database");
}
#[test]
fn the_last_table_can_be_dropped_and_the_file_says_so() {
let path = std::env::temp_dir().join(format!("rudb-api-last-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
let database = Database::open(&name).expect("a file name starts a native database");
database.execute("CREATE TABLE t AS SELECT 1 AS x").expect("creates");
database.execute("CHECKPOINT").expect("commits");
database.execute("DROP TABLE t").expect("drops the only table");
database.execute("CHECKPOINT").expect("commits a database with nothing in it");
database.execute("CHECKPOINT").expect("and a second one has nothing to do");
drop(database);
assert!(path.exists(), "the file is still there, the way DuckDB leaves it");
let reopened = Database::open(&name).expect("a database with no tables opens");
assert!(
reopened.execute("SELECT * FROM t").is_err(),
"the dropped table does not come back at the next open"
);
reopened.execute("CREATE TABLE u AS SELECT 2 AS x").expect("creates");
reopened.execute("CHECKPOINT").expect("commits");
drop(reopened);
let again = Database::open(&name).expect("the native database reopens");
assert_eq!(again.value("SELECT sum(x) FROM u").expect("the new table"), Value::HugeInt(2));
drop(again);
std::fs::remove_file(path).expect("removes the temporary database");
}
#[test]
fn a_database_on_a_file_is_written_when_the_last_handle_goes_away() {
let path = std::env::temp_dir().join(format!("rudb-api-close-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
let database = Database::open(&name).expect("a file name starts a native database");
database.execute("CREATE TABLE t (x INTEGER)").expect("creates");
database.execute("CREATE TABLE u (y INTEGER)").expect("creates the second");
let second = database.clone();
let connection = database.connect();
drop(database);
connection.execute("INSERT INTO t VALUES (7), (5)").expect("inserts");
assert!(!path.exists(), "nothing is on the disk yet, because nobody said CHECKPOINT");
drop(connection);
assert!(!path.exists(), "a handle going away while others are open writes nothing");
drop(second);
assert!(path.exists(), "the last handle going away is what writes the file");
let reopened = Database::open(&name).expect("the native database reopens");
assert_eq!(
reopened.value("SELECT sum(x) FROM t").expect("both rows are in the file"),
Value::HugeInt(12)
);
reopened.execute("SELECT * FROM u").expect("the empty table was written too");
drop(reopened);
std::fs::remove_file(path).expect("removes the temporary database");
}
#[test]
fn close_writes_the_file_and_says_whether_it_worked() {
let path = std::env::temp_dir().join(format!("rudb-api-explicit-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
let database = Database::open(&name).expect("a file name starts a native database");
database.execute("CREATE TABLE t (x INTEGER)").expect("creates");
database.execute("CREATE TABLE u (y INTEGER)").expect("creates the second");
database.execute("INSERT INTO t VALUES (3)").expect("inserts");
assert!(!path.exists(), "nothing is on the disk until the close below");
database.close().expect("the file is written and nothing went wrong");
assert!(path.exists(), "the file is there as soon as close returns");
let reopened = Database::open(&name).expect("the native database reopens");
assert_eq!(reopened.value("SELECT sum(x) FROM t").expect("the row"), Value::HugeInt(3));
reopened.close().expect("closing a second time writes nothing and works");
std::fs::remove_file(path).expect("removes the temporary database");
}
#[test]
fn a_read_only_database_leaves_the_file_alone() {
let path = std::env::temp_dir().join(format!("rudb-api-frozen-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
let database = Database::open(&name).expect("a file name starts a native database");
database.execute("CREATE TABLE t AS SELECT 1 AS x").expect("creates");
database.close().expect("writes the file");
let written = std::fs::metadata(&path).expect("the file is there").len();
let frozen = Database::open_with(&name, Config::default().with_read_only(true))
.expect("the file opens read only");
assert!(frozen.config().read_only(), "the setting is what it was opened with");
frozen.execute("CREATE TABLE u AS SELECT 2 AS y").expect("the statement is not refused yet");
assert_eq!(frozen.value("SELECT sum(y) FROM u").expect("the rows"), Value::HugeInt(2));
frozen.execute("CHECKPOINT").expect("a checkpoint on a read only database does nothing");
frozen.close().expect("closing writes nothing");
assert_eq!(std::fs::metadata(&path).expect("the file").len(), written, "the file is untouched");
let reopened = Database::open(&name).expect("the native database reopens");
assert_eq!(
reopened.value("SELECT sum(x) FROM t").expect("the file is what it was"),
Value::HugeInt(1)
);
assert!(
reopened.execute("SELECT * FROM u").is_err(),
"the table written under it is not there"
);
drop(reopened);
std::fs::remove_file(path).expect("removes the temporary database");
}
#[test]
fn two_connections_are_two_views_of_one_database() {
let database = Database::new();
let writer = database.connect();
let reader = database.connect();
writer.execute("CREATE TABLE t (a INTEGER)").expect("creates");
writer.execute("INSERT INTO t VALUES (1), (2), (3)").expect("inserts");
assert_eq!(reader.value("SELECT sum(a) FROM t").expect("reads"), Value::HugeInt(6));
}
#[test]
fn a_cloned_handle_is_the_same_database_and_not_a_copy_of_it() {
let database = Database::new();
let second = database.clone();
database.execute("CREATE TABLE t (a INTEGER)").expect("creates");
second.execute("INSERT INTO t VALUES (7)").expect("inserts");
assert_eq!(database.value("SELECT a FROM t").expect("reads"), Value::Integer(7));
}
#[test]
fn the_programmatic_write_path_and_sql_see_the_same_tables() {
let database = Database::new();
database.create_table("t", vec![Field::new("x", LogicalType::Integer)]).expect("creates");
database.append("t", &[vec![Value::Integer(4)]]).expect("appends");
let connection = database.connect();
connection.execute("INSERT INTO t VALUES (5)").expect("inserts");
assert_eq!(database.table_len("t").expect("counts"), 2);
assert_eq!(connection.value("SELECT sum(x) FROM t").expect("reads"), Value::HugeInt(9));
}
#[test]
fn many_threads_read_one_database_at_once() {
let database = Database::new();
database.execute("CREATE TABLE t (a INTEGER)").expect("creates");
database.execute("INSERT INTO t SELECT * FROM range(1000)").expect("inserts");
let readers: Vec<_> = (0..8)
.map(|_| {
let connection = database.connect();
thread::spawn(move || connection.value("SELECT count(*) FROM t").expect("reads"))
})
.collect();
for reader in readers {
assert_eq!(reader.join().expect("the thread finished"), Value::BigInt(1000));
}
}
#[test]
fn a_writer_in_another_thread_is_visible_once_it_is_done() {
let database = Database::new();
database.execute("CREATE TABLE t (a INTEGER)").expect("creates");
let writer = database.connect();
let worker = thread::spawn(move || {
for row in 0..100 {
writer.execute(&format!("INSERT INTO t VALUES ({row})")).expect("inserts");
}
});
worker.join().expect("the thread finished");
assert_eq!(database.value("SELECT count(*) FROM t").expect("reads"), Value::BigInt(100));
}
#[test]
fn the_catalog_is_readable_and_writable_for_the_call_and_no_longer() {
let database = Database::new();
database.execute("CREATE TABLE t (a INTEGER)").expect("creates");
assert!(database.with_catalog(|catalog| catalog.tables().any(|t| t.name().table == "t")));
database.with_catalog_mut(|catalog| {
let name = catalog.resolve(&["t"]).expect("resolves");
catalog.drop_table(&name).expect("drops");
});
assert!(database.table_names().is_empty());
}
#[test]
fn a_declared_row_order_survives_a_checkpoint_and_a_reopen() {
let path = std::env::temp_dir().join(format!("rudb-api-cluster-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
let columns = [
Field::new("l_orderkey", LogicalType::BigInt),
Field::new("l_linenumber", LogicalType::Integer),
Field::new("l_shipdate", LogicalType::Date),
];
let stage_zero = Clustering::new(vec![2, 0, 1], Width::Month, &columns).expect("valid");
let database = Database::open(&name).expect("a file name starts a native database");
database
.execute("CREATE TABLE lineitem (l_orderkey BIGINT, l_linenumber INTEGER, l_shipdate DATE)")
.expect("creates");
database
.execute("INSERT INTO lineitem VALUES (1, 1, DATE '1995-09-02'), (2, 1, DATE '1995-09-03')")
.expect("inserts");
database.execute("CREATE TABLE nation (n_nationkey INTEGER)").expect("creates");
database.execute("INSERT INTO nation VALUES (1)").expect("inserts");
database.with_catalog_mut(|catalog| {
let table = catalog.resolve(&["lineitem"]).expect("resolves");
catalog
.table_mut(&table)
.expect("the table is there")
.cluster_by(Some(stage_zero.clone()))
.expect("the columns are the table's");
});
database.execute("CHECKPOINT").expect("commits");
drop(database);
let reopened = Database::open(&name).expect("the native database reopens");
reopened.with_catalog(|catalog| {
let table = catalog.resolve(&["lineitem"]).expect("resolves");
assert_eq!(
catalog.table(&table).expect("the table is there").clustering(),
Some(&stage_zero),
"the order the table was declared with came back out of the file"
);
let plain = catalog.resolve(&["nation"]).expect("resolves");
assert_eq!(
catalog.table(&plain).expect("the table is there").clustering(),
None,
"and a table nobody declared one for did not pick one up"
);
});
assert_eq!(
reopened.value("SELECT sum(l_orderkey) FROM lineitem").expect("reads"),
Value::HugeInt(3)
);
std::fs::remove_file(path).expect("removes the temporary database");
}
#[test]
fn declaring_an_order_after_a_checkpoint_gets_the_file_rewritten() {
let path = std::env::temp_dir().join(format!("rudb-api-recluster-{}.rudb", std::process::id()));
let name = path.to_str().expect("a UTF-8 temporary path").to_owned();
let columns = [Field::new("a", LogicalType::Integer), Field::new("d", LogicalType::Date)];
let asked = Clustering::new(vec![1], Width::Year, &columns).expect("valid");
let database = Database::open(&name).expect("a file name starts a native database");
database.execute("CREATE TABLE t (a INTEGER, d DATE)").expect("creates");
database.execute("INSERT INTO t VALUES (1, DATE '2020-01-01')").expect("inserts");
database.execute("CHECKPOINT").expect("commits once, with no declaration");
database.with_catalog_mut(|catalog| {
let table = catalog.resolve(&["t"]).expect("resolves");
catalog.table_mut(&table).expect("there").cluster_by(Some(asked.clone())).expect("valid");
});
database.execute("CHECKPOINT").expect("commits again, for the declaration alone");
drop(database);
let reopened = Database::open(&name).expect("the native database reopens");
reopened.with_catalog(|catalog| {
let table = catalog.resolve(&["t"]).expect("resolves");
assert_eq!(catalog.table(&table).expect("there").clustering(), Some(&asked));
});
std::fs::remove_file(path).expect("removes the temporary database");
}