use faucet_common_mssql::{MssqlConnectionConfig, MssqlTls, MssqlTlsMode, build_pool};
use faucet_conformance::assert_config_schema_valid_value;
use faucet_source_mssql::{MssqlReplication, MssqlSource, MssqlSourceConfig};
use serde_json::Value;
use testcontainers_modules::mssql_server::MssqlServer;
use testcontainers_modules::testcontainers::ContainerAsync;
use testcontainers_modules::testcontainers::runners::AsyncRunner;
#[test]
fn conformance_config_schema_valid() {
let schema = serde_json::to_value(schemars::schema_for!(MssqlSourceConfig)).unwrap();
assert_config_schema_valid_value(&schema, "faucet-source-mssql");
}
const ENCODED_PW: &str = "yourStrong%28%21%29Password";
static SERIAL: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
async fn start_mssql() -> (ContainerAsync<MssqlServer>, u16) {
let container = MssqlServer::default()
.with_accept_eula()
.start()
.await
.expect("start mssql container");
let port = container
.get_host_port_ipv4(1433)
.await
.expect("mssql host port");
(container, port)
}
fn conn_cfg(port: u16) -> MssqlConnectionConfig {
MssqlConnectionConfig {
connection_url: Some(format!("mssql://sa:{ENCODED_PW}@127.0.0.1:{port}/master")),
connection_string: None,
tls: MssqlTls {
mode: MssqlTlsMode::TrustServerCertificate,
ca_cert_path: None,
},
}
}
async fn exec(pool: &faucet_common_mssql::MssqlPool, sql: &str) {
let mut conn = pool.get().await.expect("checkout");
conn.execute(sql, &[]).await.expect("execute setup sql");
}
async fn seed_events(pool: &faucet_common_mssql::MssqlPool, n: i64) {
exec(pool, "CREATE TABLE dbo.events (id BIGINT)").await;
for chunk in (1..=n).collect::<Vec<_>>().chunks(1000) {
let values: Vec<String> = chunk.iter().map(|i| format!("({i})")).collect();
exec(
pool,
&format!("INSERT INTO dbo.events (id) VALUES {}", values.join(", ")),
)
.await;
}
}
#[tokio::test(flavor = "multi_thread")]
async fn conformance_bounded_memory() {
let _serial = SERIAL.lock().await;
let (_container, port) = start_mssql().await;
let cfg = conn_cfg(port);
let pool = build_pool(&cfg, 4).await.expect("pool");
seed_events(&pool, 5_000).await;
let mut scfg = MssqlSourceConfig::new(
cfg.connection_url.clone().unwrap(),
"SELECT id FROM dbo.events ORDER BY id",
);
scfg.connection.tls = cfg.tls.clone();
scfg.batch_size = 250;
let source = MssqlSource::new(scfg).await.expect("source new");
faucet_conformance::assert_bounded_memory(&source, 250, 5_000).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn conformance_bookmark_roundtrip() {
let _serial = SERIAL.lock().await;
let (_container, port) = start_mssql().await;
let cfg = conn_cfg(port);
let pool = build_pool(&cfg, 4).await.expect("pool");
exec(
&pool,
"CREATE TABLE dbo.events (id INT, updated_at NVARCHAR(30))",
)
.await;
for (id, ts) in [(1, "2024-01-01"), (2, "2024-02-01"), (3, "2024-03-01")] {
exec(
&pool,
&format!("INSERT INTO dbo.events (id, updated_at) VALUES ({id}, '{ts}')"),
)
.await;
}
let mut scfg = MssqlSourceConfig::new(
cfg.connection_url.clone().unwrap(),
"SELECT id, updated_at FROM dbo.events WHERE updated_at > @bookmark ORDER BY updated_at",
);
scfg.connection.tls = cfg.tls.clone();
scfg.replication = MssqlReplication::Incremental {
column: "updated_at".into(),
initial_value: Value::from("2024-01-15"),
};
let source = MssqlSource::new(scfg).await.expect("source new");
exec(
&pool,
"INSERT INTO dbo.events (id, updated_at) VALUES (4, '2024-04-01')",
)
.await;
faucet_conformance::assert_bookmark_roundtrip(&source).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn conformance_errors_not_panics() {
let _serial = SERIAL.lock().await;
let (_container, port) = start_mssql().await;
let cfg = conn_cfg(port);
let mut scfg = MssqlSourceConfig::new(
cfg.connection_url.clone().unwrap(),
"SELECT * FROM dbo.missing_table",
);
scfg.connection.tls = cfg.tls.clone();
let source = MssqlSource::new(scfg)
.await
.expect("builds; query fails at read");
faucet_conformance::assert_errors_not_panics(&source).await;
}