274 lines
10 KiB
Rust
274 lines
10 KiB
Rust
use super::*;
|
|
use dever_runtime::{database::scalar, value::decimal::Decimal};
|
|
|
|
#[test]
|
|
fn decimal_storage_covers_every_precision_and_scale() {
|
|
for precision in 1..=34 {
|
|
let limit = 10_i128.pow(precision);
|
|
for scale in 0..=precision {
|
|
for coefficient in [-limit + 1, -1, 0, 1, limit - 1] {
|
|
let value = Decimal::parse(&format!("{coefficient}E-{scale}")).unwrap();
|
|
let key = scalar::decimal::encode(value, precision, scale).unwrap();
|
|
assert_eq!(
|
|
key,
|
|
format!(
|
|
"{:0width$}",
|
|
coefficient + limit,
|
|
width = precision as usize + 1
|
|
)
|
|
);
|
|
assert_eq!(
|
|
scalar::decimal::decode(&key, precision, scale).unwrap(),
|
|
value
|
|
);
|
|
}
|
|
for coefficient in [-limit, limit] {
|
|
let value = Decimal::parse(&format!("{coefficient}E-{scale}")).unwrap();
|
|
assert!(scalar::decimal::encode(value, precision, scale).is_err());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decimal_storage_rejects_non_digit_keys() {
|
|
for position in 0..6 {
|
|
for byte in 0..=127u8 {
|
|
if byte.is_ascii_digit() {
|
|
continue;
|
|
}
|
|
let mut key = b"100100".to_vec();
|
|
key[position] = byte;
|
|
assert!(scalar::decimal::decode(std::str::from_utf8(&key).unwrap(), 5, 2).is_err());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decimal_keys_are_exact_unique_and_sort_like_numbers() {
|
|
let values = [
|
|
"-999.99", "-100", "-1.01", "-0.01", "0", "0.01", "1.01", "100", "999.99",
|
|
];
|
|
let encoded =
|
|
values.map(|text| scalar::decimal::encode(Decimal::parse(text).unwrap(), 5, 2).unwrap());
|
|
assert!(encoded.windows(2).all(|pair| pair[0] < pair[1]));
|
|
for (text, key) in values.iter().zip(&encoded) {
|
|
assert_eq!(
|
|
scalar::decimal::decode(key, 5, 2).unwrap(),
|
|
Decimal::parse(text).unwrap()
|
|
);
|
|
}
|
|
for text in ["1", "1.0", "1.00", "1.000", "1E0"] {
|
|
assert_eq!(
|
|
scalar::decimal::encode(Decimal::parse(text).unwrap(), 5, 2).unwrap(),
|
|
"100100"
|
|
);
|
|
}
|
|
for text in ["0", "-0", "0E-6176"] {
|
|
assert_eq!(
|
|
scalar::decimal::encode(Decimal::parse(text).unwrap(), 5, 2).unwrap(),
|
|
"100000"
|
|
);
|
|
}
|
|
for text in ["1000", "-1000", "0.001", "-0.001", "1E6144", "1E-6176"] {
|
|
assert_eq!(
|
|
scalar::decimal::encode(Decimal::parse(text).unwrap(), 5, 2)
|
|
.unwrap_err()
|
|
.kind,
|
|
Kind::InvalidData,
|
|
"{text}"
|
|
);
|
|
}
|
|
for (precision, scale, text) in [
|
|
(34, 0, "9999999999999999999999999999999999"),
|
|
(34, 34, "0.9999999999999999999999999999999999"),
|
|
(34, 4, "-999999999999999999999999999999.9999"),
|
|
] {
|
|
let value = Decimal::parse(text).unwrap();
|
|
let key = scalar::decimal::encode(value, precision, scale).unwrap();
|
|
assert_eq!(
|
|
scalar::decimal::decode(&key, precision, scale).unwrap(),
|
|
value
|
|
);
|
|
}
|
|
for key in [
|
|
"000000", "200000", "10001", "0100000", "+00001", "10000x", "100\0xx",
|
|
] {
|
|
assert_eq!(
|
|
scalar::decimal::decode(key, 5, 2).unwrap_err().kind,
|
|
Kind::InvalidData
|
|
);
|
|
}
|
|
let directory = Directory::new();
|
|
runtime()
|
|
.run(async |scope| {
|
|
let database = Database::open(&directory.0, options(), scope)
|
|
.await
|
|
.unwrap();
|
|
let mut executor = database.executor().await.unwrap();
|
|
executor
|
|
.initialize(
|
|
scope,
|
|
"CREATE TABLE numbers (amount TEXT UNIQUE, optional TEXT)",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
for key in encoded.iter().rev() {
|
|
executor
|
|
.execute(
|
|
scope,
|
|
"INSERT INTO numbers(amount) VALUES (?1)",
|
|
vec![Parameter::Text(key.clone())],
|
|
)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
let rows = executor
|
|
.query(
|
|
scope,
|
|
"SELECT amount, optional FROM numbers ORDER BY amount",
|
|
vec![],
|
|
20,
|
|
|row| {
|
|
assert_eq!(
|
|
row.get_ref(1)?.data_type(),
|
|
dever_runtime::database::DataType::Null
|
|
);
|
|
scalar::decimal(row, 0, 5, 2)
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(rows, values.map(|text| Decimal::parse(text).unwrap()));
|
|
assert_eq!(
|
|
executor
|
|
.execute(
|
|
scope,
|
|
"INSERT INTO numbers(amount) VALUES (?1)",
|
|
vec![Parameter::Text(encoded[4].clone())]
|
|
)
|
|
.await
|
|
.unwrap_err()
|
|
.kind,
|
|
Kind::Constraint
|
|
);
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[cfg(all(feature = "calendar", feature = "uuid"))]
|
|
#[test]
|
|
fn typed_scalars_preserve_storage_bytes_and_reject_corrupt_values() {
|
|
use dever_runtime::{
|
|
time::calendar::{Date, Time},
|
|
value::uuid::Uuid,
|
|
wire::json::Json,
|
|
};
|
|
let directory = Directory::new();
|
|
runtime()
|
|
.run(async |scope| {
|
|
let database = Database::open(&directory.0, options(), scope)
|
|
.await
|
|
.unwrap();
|
|
let mut executor = database.executor().await.unwrap();
|
|
executor
|
|
.initialize(
|
|
scope,
|
|
"CREATE TABLE values_test (uid BLOB, day INTEGER, time INTEGER, document TEXT)",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let uid = Uuid::parse("ffffffff-ffff-ffff-ffff-ffffffffffff").unwrap();
|
|
let document = "{\"value\":123456789012345678901234567890.0010,\"nested\":[true,null]}";
|
|
for (day, time) in [
|
|
("0000-01-01", "00:00:00"),
|
|
("1969-12-31", "23:59:59.999"),
|
|
("9999-12-31", "12:30:42.001"),
|
|
] {
|
|
let day = Date::parse(day).unwrap();
|
|
let time = Time::parse(time).unwrap();
|
|
executor
|
|
.execute(
|
|
scope,
|
|
"INSERT INTO values_test VALUES (?1,?2,?3,?4)",
|
|
vec![
|
|
Parameter::Blob(uid.as_bytes().to_vec()),
|
|
Parameter::Integer(day.millis()),
|
|
Parameter::Integer(time.millis()),
|
|
Parameter::Text(document.into()),
|
|
],
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let actual = executor
|
|
.query(
|
|
scope,
|
|
"SELECT * FROM values_test ORDER BY rowid DESC LIMIT 1",
|
|
vec![],
|
|
1,
|
|
|row| {
|
|
assert_eq!(
|
|
row.get_ref(0)?.data_type(),
|
|
dever_runtime::database::DataType::Blob
|
|
);
|
|
assert_eq!(
|
|
row.get_ref(1)?.data_type(),
|
|
dever_runtime::database::DataType::Integer
|
|
);
|
|
Ok((
|
|
scalar::uuid(row, 0)?,
|
|
scalar::date(row, 1)?,
|
|
scalar::time(row, 2)?,
|
|
scalar::json(row, 3)?,
|
|
))
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(actual, [(uid, day, time, Json::parse(document).unwrap())]);
|
|
assert_eq!(actual[0].3.as_str(), document);
|
|
}
|
|
executor
|
|
.execute(scope, "DELETE FROM values_test", vec![])
|
|
.await
|
|
.unwrap();
|
|
for (sql, kind) in [
|
|
("INSERT INTO values_test(uid) VALUES (zeroblob(15))", "Uuid"),
|
|
("INSERT INTO values_test(day) VALUES (1)", "Date"),
|
|
(
|
|
"INSERT INTO values_test(day) VALUES (-62167219200001)",
|
|
"Date",
|
|
),
|
|
("INSERT INTO values_test(time) VALUES (86400000)", "Time"),
|
|
("INSERT INTO values_test(time) VALUES (-1)", "Time"),
|
|
(
|
|
"INSERT INTO values_test(document) VALUES ('{\"a\":1,\"a\":2}')",
|
|
"Json",
|
|
),
|
|
(
|
|
"INSERT INTO values_test(document) VALUES ('invalid')",
|
|
"Json",
|
|
),
|
|
] {
|
|
executor.execute(scope, sql, vec![]).await.unwrap();
|
|
let failure = executor
|
|
.query(
|
|
scope,
|
|
"SELECT * FROM values_test ORDER BY rowid DESC LIMIT 1",
|
|
vec![],
|
|
1,
|
|
move |row| match kind {
|
|
"Uuid" => scalar::uuid(row, 0).map(|_| ()),
|
|
"Date" => scalar::date(row, 1).map(|_| ()),
|
|
"Time" => scalar::time(row, 2).map(|_| ()),
|
|
_ => scalar::json(row, 3).map(|_| ()),
|
|
},
|
|
)
|
|
.await
|
|
.unwrap_err();
|
|
assert_eq!(failure.kind, Kind::InvalidData, "{sql}");
|
|
}
|
|
})
|
|
.unwrap();
|
|
}
|