use hyperdb_api::{Catalog, Inserter, Numeric, Row, Rowset, SqlType, TableDefinition};
mod common;
use common::TestConnection;
fn collect_rows<T, F>(mut result: Rowset<'_>, f: F) -> Vec<T>
where
F: Fn(&Row) -> T,
{
let mut rows = Vec::new();
while let Some(chunk) = result.next_chunk().expect("Failed to get chunk") {
for row in &chunk {
rows.push(f(row));
}
}
rows
}
#[test]
fn test_roundtrip_smallint() {
let test = TestConnection::new().expect("Failed to create test connection");
let table_def =
TableDefinition::new("rt_smallint").add_nullable_column("val", SqlType::small_int());
Catalog::new(&test.connection)
.create_table(&table_def)
.expect("Failed to create table");
let test_values: Vec<Option<i16>> = vec![
Some(0),
Some(1),
Some(-1),
Some(i16::MAX),
Some(i16::MIN),
Some(32767),
Some(-32768),
None, ];
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for val in &test_values {
inserter.add_row(&[val]).expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let result = test
.connection
.execute_query("SELECT val FROM rt_smallint")
.expect("Failed to query");
let returned: Vec<Option<i16>> = collect_rows(result, |row| row.get(0));
assert_eq!(
returned, test_values,
"SMALLINT round-trip mismatch: expected {test_values:?}, got {returned:?}"
);
}
#[test]
fn test_roundtrip_integer() {
let test = TestConnection::new().expect("Failed to create test connection");
let table_def = TableDefinition::new("rt_integer").add_nullable_column("val", SqlType::int());
Catalog::new(&test.connection)
.create_table(&table_def)
.expect("Failed to create table");
let test_values: Vec<Option<i32>> = vec![
Some(0),
Some(1),
Some(-1),
Some(i32::MAX),
Some(i32::MIN),
Some(2147483647),
Some(-2147483648),
Some(42),
Some(-42),
None,
];
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for val in &test_values {
inserter.add_row(&[val]).expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let result = test
.connection
.execute_query("SELECT val FROM rt_integer")
.expect("Failed to query");
let returned: Vec<Option<i32>> = collect_rows(result, |row| row.get(0));
assert_eq!(
returned, test_values,
"INTEGER round-trip mismatch: expected {test_values:?}, got {returned:?}"
);
}
#[test]
fn test_roundtrip_bigint() {
let test = TestConnection::new().expect("Failed to create test connection");
let table_def =
TableDefinition::new("rt_bigint").add_nullable_column("val", SqlType::big_int());
Catalog::new(&test.connection)
.create_table(&table_def)
.expect("Failed to create table");
let test_values: Vec<Option<i64>> = vec![
Some(0),
Some(1),
Some(-1),
Some(i64::MAX),
Some(i64::MIN),
Some(9223372036854775807),
Some(-9223372036854775808),
Some(1234567890123456789),
None,
];
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for val in &test_values {
inserter.add_row(&[val]).expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let result = test
.connection
.execute_query("SELECT val FROM rt_bigint")
.expect("Failed to query");
let returned: Vec<Option<i64>> = collect_rows(result, |row| row.get(0));
assert_eq!(
returned, test_values,
"BIGINT round-trip mismatch: expected {test_values:?}, got {returned:?}"
);
}
#[test]
#[expect(
clippy::approx_constant,
reason = "test literal chosen for readability; not intended as an approximation"
)]
fn test_roundtrip_double() {
let test = TestConnection::new().expect("Failed to create test connection");
let table_def = TableDefinition::new("rt_double").add_nullable_column("val", SqlType::double());
Catalog::new(&test.connection)
.create_table(&table_def)
.expect("Failed to create table");
let test_values: Vec<Option<f64>> = vec![
Some(0.0),
Some(1.0),
Some(-1.0),
Some(3.14159265358979),
Some(1e50), Some(-1e50), Some(f64::MIN_POSITIVE),
Some(1e-100),
None,
];
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for val in &test_values {
inserter.add_row(&[val]).expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let result = test
.connection
.execute_query("SELECT val FROM rt_double")
.expect("Failed to query");
let returned: Vec<Option<f64>> = collect_rows(result, |row| row.get(0));
assert_eq!(returned.len(), test_values.len());
for (i, (expected, actual)) in test_values.iter().zip(returned.iter()).enumerate() {
match (expected, actual) {
(None, None) => {}
(Some(e), Some(a)) => {
assert!(
(e - a).abs() < 1e-10 || (e.is_nan() && a.is_nan()),
"DOUBLE round-trip mismatch at index {i}: expected {e}, got {a}"
);
}
_ => panic!(
"DOUBLE round-trip NULL mismatch at index {i}: expected {expected:?}, got {actual:?}"
),
}
}
}
#[test]
#[ignore = "Hyper does not support 32-bit floating points"]
fn test_roundtrip_real() {
let test = TestConnection::new().expect("Failed to create test connection");
let table_def = TableDefinition::new("rt_real").add_nullable_column("val", SqlType::float());
Catalog::new(&test.connection)
.create_table(&table_def)
.expect("Failed to create table");
}
#[test]
fn test_roundtrip_boolean() {
let test = TestConnection::new().expect("Failed to create test connection");
let table_def = TableDefinition::new("rt_bool").add_nullable_column("val", SqlType::bool());
Catalog::new(&test.connection)
.create_table(&table_def)
.expect("Failed to create table");
let test_values: Vec<Option<bool>> = vec![Some(true), Some(false), None];
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for val in &test_values {
inserter.add_row(&[val]).expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let result = test
.connection
.execute_query("SELECT val FROM rt_bool")
.expect("Failed to query");
let returned: Vec<Option<bool>> = collect_rows(result, |row| row.get(0));
assert_eq!(
returned, test_values,
"BOOLEAN round-trip mismatch: expected {test_values:?}, got {returned:?}"
);
}
#[test]
fn test_roundtrip_text() {
let test = TestConnection::new().expect("Failed to create test connection");
let table_def = TableDefinition::new("rt_text").add_nullable_column("val", SqlType::text());
Catalog::new(&test.connection)
.create_table(&table_def)
.expect("Failed to create table");
let long_string = "very long string ".repeat(100);
let test_values: Vec<Option<&str>> = vec![
Some(""),
Some("hello"),
Some("Hello, World!"),
Some("special chars: !@#$%^&*()"),
Some("quotes: \"double\" and 'single'"),
Some("unicode: ä½ å¥½ä¸–ç•Œ 🎉 émojis"),
Some("newlines:\nand\ttabs"),
Some(long_string.as_str()),
Some("null byte in middle: \0 after"),
None,
];
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for val in &test_values {
inserter.add_row(&[val]).expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let result = test
.connection
.execute_query("SELECT val FROM rt_text")
.expect("Failed to query");
let returned: Vec<Option<String>> = collect_rows(result, |row| row.get(0));
let expected: Vec<Option<String>> = test_values
.iter()
.map(|v| v.map(std::string::ToString::to_string))
.collect();
assert_eq!(
returned, expected,
"TEXT round-trip mismatch:\nexpected: {expected:?}\ngot: {returned:?}"
);
}
#[test]
fn test_roundtrip_varchar() {
let test = TestConnection::new().expect("Failed to create test connection");
test.execute_command("CREATE TABLE rt_varchar (val VARCHAR(100))")
.expect("Failed to create table");
let test_values: Vec<Option<&str>> = vec![
Some(""),
Some("short"),
Some("exactly one hundred chars - padded to make it longer for testing purposes!!!!!"),
None,
];
let catalog = Catalog::new(&test.connection);
let table_def = catalog
.get_table_definition("rt_varchar")
.expect("Failed to get table definition");
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for val in &test_values {
inserter.add_row(&[val]).expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let result = test
.connection
.execute_query("SELECT val FROM rt_varchar")
.expect("Failed to query");
let returned: Vec<Option<String>> = collect_rows(result, |row| row.get(0));
let expected: Vec<Option<String>> = test_values
.iter()
.map(|v| v.map(std::string::ToString::to_string))
.collect();
assert_eq!(returned, expected, "VARCHAR round-trip mismatch");
}
#[test]
fn test_roundtrip_date() {
use hyperdb_api::Date;
let test = TestConnection::new().expect("Failed to create test connection");
let table_def = TableDefinition::new("rt_date").add_nullable_column("val", SqlType::date());
Catalog::new(&test.connection)
.create_table(&table_def)
.expect("Failed to create table");
let test_values: Vec<Option<Date>> = vec![
Some(Date::new(2024, 1, 1)),
Some(Date::new(2024, 12, 31)),
Some(Date::new(1970, 1, 1)), Some(Date::new(2000, 2, 29)), Some(Date::new(1, 1, 1)), Some(Date::new(9999, 12, 31)), None,
];
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for val in &test_values {
inserter.add_row(&[val]).expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let result = test
.connection
.execute_query("SELECT val::TEXT FROM rt_date")
.expect("Failed to query");
let expected_strings: Vec<Option<String>> = vec![
Some("2024-01-01".to_string()),
Some("2024-12-31".to_string()),
Some("1970-01-01".to_string()),
Some("2000-02-29".to_string()),
Some("0001-01-01".to_string()),
Some("9999-12-31".to_string()),
None,
];
let returned: Vec<Option<String>> = collect_rows(result, |row| row.get(0));
assert_eq!(returned, expected_strings, "DATE round-trip mismatch");
}
#[test]
fn test_roundtrip_numeric_small() {
let test = TestConnection::new().expect("Failed to create test connection");
test.execute_command("CREATE TABLE rt_numeric_small (val NUMERIC(10, 2))")
.expect("Failed to create table");
let catalog = Catalog::new(&test.connection);
let table_def = catalog
.get_table_definition("rt_numeric_small")
.expect("Failed to get table definition");
let test_values: Vec<Option<Numeric>> = vec![
Some(Numeric::new(0, 2)), Some(Numeric::new(100, 2)), Some(Numeric::new(-100, 2)), Some(Numeric::new(12345, 2)), Some(Numeric::new(-12345, 2)), Some(Numeric::new(99999999, 2)), None,
];
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for val in &test_values {
inserter.add_row(&[val]).expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let result = test
.connection
.execute_query("SELECT val::TEXT FROM rt_numeric_small")
.expect("Failed to query");
let expected_strings: Vec<Option<String>> = vec![
Some("0.00".to_string()),
Some("1.00".to_string()),
Some("-1.00".to_string()),
Some("123.45".to_string()),
Some("-123.45".to_string()),
Some("999999.99".to_string()),
None,
];
let returned: Vec<Option<String>> = collect_rows(result, |row| row.get(0));
assert_eq!(
returned, expected_strings,
"NUMERIC(10,2) round-trip mismatch"
);
}
#[test]
fn test_roundtrip_bytea() {
let test = TestConnection::new().expect("Failed to create test connection");
let table_def = TableDefinition::new("rt_bytea").add_nullable_column("val", SqlType::bytes());
Catalog::new(&test.connection)
.create_table(&table_def)
.expect("Failed to create table");
let test_values: Vec<Option<Vec<u8>>> = vec![
Some(vec![]), Some(vec![0x00]), Some(vec![0xFF]), Some(vec![0x00, 0x01, 0x02, 0x03]), Some(vec![0xDE, 0xAD, 0xBE, 0xEF]), Some((0..=255).collect()), Some(vec![0x00; 1000]), None,
];
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for val in &test_values {
inserter.add_row(&[val]).expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let result = test
.connection
.execute_query("SELECT val FROM rt_bytea")
.expect("Failed to query");
let returned: Vec<Option<Vec<u8>>> = collect_rows(result, |row| row.get(0));
assert_eq!(returned, test_values, "BYTEA round-trip mismatch");
}
#[test]
fn test_roundtrip_mixed_types() {
let test = TestConnection::new().expect("Failed to create test connection");
test.execute_command(
"CREATE TABLE rt_mixed (
id INT NOT NULL,
name TEXT,
amount DOUBLE PRECISION,
active BOOL,
code SMALLINT
)",
)
.expect("Failed to create table");
let catalog = Catalog::new(&test.connection);
let table_def = catalog
.get_table_definition("rt_mixed")
.expect("Failed to get table definition");
#[expect(
clippy::type_complexity,
reason = "bespoke tuple of scalar sample rows for a single-use test vector"
)]
let test_rows: Vec<(i32, Option<&str>, Option<f64>, Option<bool>, Option<i16>)> = vec![
(1, Some("Alice"), Some(100.50), Some(true), Some(1)),
(2, Some("Bob"), Some(-50.25), Some(false), Some(-1)),
(3, None, None, None, None), (4, Some(""), Some(0.0), Some(false), Some(0)), (
5,
Some("Unicode: 日本語"),
Some(1e50),
Some(true),
Some(i16::MAX),
),
];
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for (id, name, amount, active, code) in &test_rows {
inserter
.add_row(&[id, name, amount, active, code])
.expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let result = test
.connection
.execute_query("SELECT id, name, amount, active, code FROM rt_mixed ORDER BY id")
.expect("Failed to query");
#[expect(
clippy::type_complexity,
reason = "bespoke tuple of scalar sample rows for a single-use test vector"
)]
let returned: Vec<(i32, Option<String>, Option<f64>, Option<bool>, Option<i16>)> =
collect_rows(result, |row| {
(
row.get::<i32>(0).unwrap(),
row.get::<String>(1),
row.get::<f64>(2),
row.get::<bool>(3),
row.get::<i16>(4),
)
});
assert_eq!(returned.len(), test_rows.len());
for (i, ((id, name, amount, active, code), ret)) in
test_rows.iter().zip(returned.iter()).enumerate()
{
assert_eq!(*id, ret.0, "ID mismatch at row {i}");
assert_eq!(
name.map(std::string::ToString::to_string),
ret.1,
"Name mismatch at row {i}"
);
match (amount, &ret.2) {
(None, None) => {}
(Some(e), Some(a)) => {
assert!(
(*e - *a).abs() < 1e-10,
"Amount mismatch at row {i}: {e} vs {a}"
);
}
_ => panic!("Amount NULL mismatch at row {i}"),
}
assert_eq!(*active, ret.3, "Active mismatch at row {i}");
assert_eq!(*code, ret.4, "Code mismatch at row {i}");
}
}
#[test]
fn test_roundtrip_large_batch() {
let test = TestConnection::new().expect("Failed to create test connection");
let table_def = TableDefinition::new("rt_large_batch")
.add_required_column("id", SqlType::int())
.add_nullable_column("data", SqlType::text());
Catalog::new(&test.connection)
.create_table(&table_def)
.expect("Failed to create table");
let row_count = 10_000;
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for i in 0..row_count {
let data = if i % 10 == 0 {
None
} else {
Some(format!("row_{i}"))
};
inserter.add_row(&[&i, &data]).expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let count: i64 = test
.connection
.execute_scalar_query("SELECT COUNT(*) FROM rt_large_batch")
.expect("Failed to count")
.expect("NULL count");
assert_eq!(count, i64::from(row_count), "Row count mismatch");
let result = test
.connection
.execute_query("SELECT id, data FROM rt_large_batch WHERE id IN (0, 1, 9999) ORDER BY id")
.expect("Failed to query");
let rows: Vec<(i32, Option<String>)> = collect_rows(result, |row| {
(row.get::<i32>(0).unwrap(), row.get::<String>(1))
});
assert_eq!(rows[0], (0, None)); assert_eq!(rows[1], (1, Some("row_1".to_string())));
assert_eq!(rows[2], (9999, Some("row_9999".to_string())));
}
#[test]
fn test_roundtrip_boundary_values() {
let test = TestConnection::new().expect("Failed to create test connection");
test.execute_command(
"CREATE TABLE rt_boundary (
i16_val SMALLINT,
i32_val INT,
i64_val BIGINT,
f64_val DOUBLE PRECISION
)",
)
.expect("Failed to create table");
let catalog = Catalog::new(&test.connection);
let table_def = catalog
.get_table_definition("rt_boundary")
.expect("Failed to get table definition");
#[expect(
clippy::type_complexity,
reason = "bespoke tuple of scalar sample rows for a single-use test vector"
)]
let test_data: Vec<(Option<i16>, Option<i32>, Option<i64>, Option<f64>)> = vec![
(Some(i16::MIN), Some(i32::MIN), Some(i64::MIN), Some(-1e50)),
(Some(i16::MAX), Some(i32::MAX), Some(i64::MAX), Some(1e50)),
(Some(0), Some(0), Some(0), Some(0.0)),
(Some(-1), Some(-1), Some(-1), Some(-1.0)),
(Some(1), Some(1), Some(1), Some(1.0)),
];
let mut inserter =
Inserter::new(&test.connection, &table_def).expect("Failed to create inserter");
for (i16_val, i32_val, i64_val, f64_val) in &test_data {
inserter
.add_row(&[i16_val, i32_val, i64_val, f64_val])
.expect("Failed to add row");
}
inserter.execute().expect("Failed to execute");
let result = test
.connection
.execute_query("SELECT * FROM rt_boundary")
.expect("Failed to query");
#[expect(
clippy::type_complexity,
reason = "bespoke tuple of scalar sample rows for a single-use test vector"
)]
let returned: Vec<(Option<i16>, Option<i32>, Option<i64>, Option<f64>)> =
collect_rows(result, |row| {
(
row.get::<i16>(0),
row.get::<i32>(1),
row.get::<i64>(2),
row.get::<f64>(3),
)
});
for (i, (expected, actual)) in test_data.iter().zip(returned.iter()).enumerate() {
assert_eq!(expected.0, actual.0, "i16 mismatch at row {i}");
assert_eq!(expected.1, actual.1, "i32 mismatch at row {i}");
assert_eq!(expected.2, actual.2, "i64 mismatch at row {i}");
match (expected.3, actual.3) {
(Some(e), Some(a)) => {
assert!((e - a).abs() < 1e-10, "f64 mismatch at row {i}: {e} vs {a}");
}
(None, None) => {}
_ => panic!("f64 NULL mismatch at row {i}"),
}
}
}