#![cfg(feature = "testkit")]
use metering::interval::{MeterInterval, Sparte};
use metering::measurement_series::{MeasurementSeries, MeasurementSource};
use meterstore::Settings;
use meterstore::testkit::TestHarness;
use time::macros::datetime;
use time::{Duration, OffsetDateTime};
const START: OffsetDateTime = datetime!(2026-07-20 00:00 UTC);
const MALO: &str = "12345678905";
fn file(url: &str, warehouse: &std::path::Path) -> String {
format!(
r#"
[hot]
url = "{url}"
max_connections = 4
[cold]
catalog = "sql"
uri = "{url}"
warehouse = "file://{}"
namespace = "metering"
file_target_bytes = 8388608
metadata_pool_max_connections = 2
[[tables]]
name = "readings_versions"
[tables.archival]
settlement_lag = "1d"
archival_step = "1d"
"#,
warehouse.display(),
)
}
#[tokio::test]
async fn a_configuration_file_builds_a_working_store() {
let url = meterstore::testkit::postgres::fresh_database()
.await
.expect("postgres");
let warehouse = tempfile::tempdir().expect("temp warehouse");
let settings = Settings::from_toml(&file(&url, warehouse.path())).expect("parse");
let deployment = settings.connect().await.expect("connect");
assert_eq!(deployment.tables.len(), 1);
let config = deployment.tables[0].clone();
assert_eq!(config.name(), TestHarness::TABLE);
assert_eq!(config.settlement_lag(), Duration::DAY);
let store = deployment.store().await.expect("store");
let intervals = vec![MeterInterval {
from: START,
to: START + Duration::minutes(15),
value: rust_decimal::Decimal::new(42, 0),
quality: metering::QualityFlag::Measured,
obis_code: "1-0:1.8.0".parse().ok(),
}];
let series = meterstore::encode::StoredSeries::new(
MeasurementSeries::new(
MALO.parse().expect("a valid MaLo-ID"),
"1-0:1.8.0".parse().ok(),
intervals,
MeasurementSource::Mscons {
pid: 13_005,
message_ref: None,
sender_mp_id: "9900000000001".parse().expect("a valid Marktpartner-ID"),
},
START,
),
meterstore::ScopedVersion::new(
meterstore::VersionScope::for_interval("9900000000001", START, Sparte::Strom)
.expect("scope"),
meterstore::Version::new(20_260_720_000_001).expect("version"),
),
START,
);
store.append(&[series]).await.expect("append");
let back = store
.series(MALO)
.expect("malo")
.range(START, START + Duration::DAY)
.intervals()
.await
.expect("read");
assert_eq!(back.len(), 1);
assert_eq!(back[0].value, rust_decimal::Decimal::new(42, 0));
assert!(!deployment.pool.is_closed());
}
#[tokio::test]
async fn a_file_naming_an_unopenable_warehouse_fails_at_validation() {
let url = meterstore::testkit::postgres::fresh_database()
.await
.expect("postgres");
let warehouse = tempfile::tempdir().expect("temp warehouse");
let mut settings = Settings::from_toml(&file(&url, warehouse.path())).expect("parse");
"ftp://host/warehouse".clone_into(&mut settings.cold.warehouse);
let err = settings
.connect()
.await
.expect_err("an unsupported scheme must not build")
.to_string();
assert!(err.contains("ftp"), "{err}");
settings.validate().expect("the tables are complete");
}
#[tokio::test]
async fn a_multi_table_file_builds_a_catalog_and_refuses_a_single_store() {
let url = meterstore::testkit::postgres::fresh_database()
.await
.expect("postgres");
let warehouse = tempfile::tempdir().expect("temp warehouse");
let text = format!(
"{}\n[[tables]]\nname = \"esa_typ2_versions\"\n\n[tables.archival]\nsettlement_lag = \"1d\"\narchival_step = \"1d\"\n",
file(&url, warehouse.path()),
);
let deployment = Settings::from_toml(&text)
.expect("parse")
.connect()
.await
.expect("connect");
let err = deployment
.store()
.await
.expect_err("two tables, so there is no single store")
.to_string();
assert!(err.contains("readings_versions"), "{err}");
assert!(err.contains("esa_typ2_versions"), "{err}");
assert!(err.contains("catalog"), "{err}");
let catalog = deployment.catalog().await.expect("catalog");
let mut names: Vec<&str> = catalog.tables().map(|t| t.table()).collect();
names.sort_unstable();
assert_eq!(names, ["esa_typ2_versions", "readings_versions"]);
catalog
.query("SELECT count(*) FROM readings")
.await
.expect("the resolved relation is registered");
}
#[tokio::test]
async fn a_declared_identity_column_reaches_both_tiers() {
let url = meterstore::testkit::postgres::fresh_database()
.await
.expect("postgres");
let warehouse = tempfile::tempdir().expect("temp warehouse");
let text = format!(
r#"
[hot]
url = "{url}"
max_connections = 4
[cold]
catalog = "sql"
uri = "{url}"
warehouse = "file://{}"
namespace = "metering"
file_target_bytes = 8388608
metadata_pool_max_connections = 2
[[tables]]
name = "readings_versions"
extra_columns = [
{{ name = "tenant", identity = true }},
{{ name = "bilanzkreis" }},
]
[tables.archival]
settlement_lag = "1d"
archival_step = "1d"
"#,
warehouse.path().display(),
);
let deployment = Settings::from_toml(&text)
.expect("parse")
.connect()
.await
.expect("connect");
let store = deployment
.store()
.await
.expect("the cold table must be created with the declared identity columns");
assert!(
store.config().merge_key().iter().any(|c| c == "tenant"),
"{:?}",
store.config().merge_key()
);
deployment.store().await.expect("idempotent");
}