Skip to content

Commit cd1464b

Browse files
committed
Added tests for auto create
1 parent ae4bbd8 commit cd1464b

4 files changed

Lines changed: 317 additions & 0 deletions

File tree

src/config.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,92 @@ mod tests {
792792
);
793793
}
794794

795+
fn auto_create_set(
796+
base: &EmitterConfig,
797+
overlay: &ConfigOverlay,
798+
) -> std::collections::HashSet<String> {
799+
use crate::emit::ch_ddl::DdlConfig;
800+
let (r, _) = ConfigResolver::resolve(
801+
base,
802+
overlay,
803+
&CliOverrides::default(),
804+
&OptInState::default(),
805+
&HashMap::new(),
806+
);
807+
DdlConfig::from_resolved(&r, "db".into(), false).auto_create_namespaces
808+
}
809+
810+
#[test]
811+
fn ddl_config_auto_create_from_toml() {
812+
let base = EmitterConfig::from_toml_str(
813+
"[ch]\n[namespace.s1]\nauto_create = true\n[namespace.s2]\nauto_create = false\n",
814+
)
815+
.unwrap();
816+
let ns = auto_create_set(&base, &ConfigOverlay::default());
817+
assert!(ns.contains("s1"), "TOML auto_create=true enables");
818+
assert!(!ns.contains("s2"), "TOML auto_create=false stays off");
819+
}
820+
821+
#[test]
822+
fn ddl_config_auto_create_from_overlay() {
823+
// No TOML namespace; the overlay alone turns it on.
824+
let base = base_with("retain");
825+
let mut overlay = ConfigOverlay::default();
826+
overlay.namespaces.insert(
827+
"s1".into(),
828+
NamespaceRow {
829+
auto_create: Some(true),
830+
..Default::default()
831+
},
832+
);
833+
let ns = auto_create_set(&base, &overlay);
834+
assert!(ns.contains("s1"), "overlay auto_create=true enables");
835+
}
836+
837+
#[test]
838+
fn overlay_auto_create_overrides_toml() {
839+
// TOML false, overlay true → enabled.
840+
let base =
841+
EmitterConfig::from_toml_str("[ch]\n[namespace.s1]\nauto_create = false\n").unwrap();
842+
let mut overlay = ConfigOverlay::default();
843+
overlay.namespaces.insert(
844+
"s1".into(),
845+
NamespaceRow {
846+
auto_create: Some(true),
847+
..Default::default()
848+
},
849+
);
850+
assert!(
851+
auto_create_set(&base, &overlay).contains("s1"),
852+
"overlay true beats TOML false"
853+
);
854+
855+
// TOML true, overlay false → disabled.
856+
let base =
857+
EmitterConfig::from_toml_str("[ch]\n[namespace.s1]\nauto_create = true\n").unwrap();
858+
let mut overlay = ConfigOverlay::default();
859+
overlay.namespaces.insert(
860+
"s1".into(),
861+
NamespaceRow {
862+
auto_create: Some(false),
863+
..Default::default()
864+
},
865+
);
866+
assert!(
867+
!auto_create_set(&base, &overlay).contains("s1"),
868+
"overlay false beats TOML true"
869+
);
870+
}
871+
872+
#[test]
873+
fn ddl_config_no_auto_create_when_unset() {
874+
let base = base_with("retain");
875+
assert!(
876+
auto_create_set(&base, &ConfigOverlay::default()).is_empty(),
877+
"no source sets auto_create → empty set"
878+
);
879+
}
880+
795881
fn rel_desc(namespace: &str, name: &str) -> RelDescriptor {
796882
use crate::schema::{RelAttr, ReplIdent};
797883
use walrus::pg::walparser::RelFileNode;

src/emit/ch_emitter.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2126,6 +2126,28 @@ mod tests {
21262126
assert!(c.soft_delete);
21272127
}
21282128

2129+
#[test]
2130+
fn namespace_toml_parses_auto_create() {
2131+
let c = EmitterConfig::from_toml_str(
2132+
"[ch]\n\
2133+
[namespace.s1]\n\
2134+
auto_create = true\n\
2135+
[namespace.s2]\n\
2136+
auto_create = false\n\
2137+
[namespace.s3]\n\
2138+
target_database = \"warehouse\"\n",
2139+
)
2140+
.unwrap();
2141+
assert!(c.namespaces["s1"].auto_create, "explicit true");
2142+
assert!(!c.namespaces["s2"].auto_create, "explicit false");
2143+
// Key absent defaults off (unwrap_or(false)).
2144+
assert!(!c.namespaces["s3"].auto_create, "absent defaults off");
2145+
assert_eq!(
2146+
c.namespaces["s3"].target_database.as_deref(),
2147+
Some("warehouse")
2148+
);
2149+
}
2150+
21292151
/// Dotted names stay inside their TOML key level: schema `a.b` table `c`
21302152
/// and schema `a` table `b.c` are distinct rels, distinct targets.
21312153
#[test]

tests/ddl_replicates.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ mod fx;
2828

2929
use std::time::Duration;
3030

31+
use walshadow::ch_emitter::EmitterConfig;
3132
use walshadow::mapping::TableTarget;
3233
use walshadow::mapping::{ColumnMapping, NamespaceMapping};
3334
use walshadow::schema::RelName;
@@ -634,3 +635,110 @@ async fn auto_create_honors_per_namespace_target_database() {
634635
"table must not be created in the global database",
635636
);
636637
}
638+
639+
/// Same as `create_table_auto_replicates_in_namespace`, but the auto-create
640+
/// namespace comes from a parsed TOML `[namespace.<ns>]` block rather than a
641+
/// programmatic `NamespaceMapping` — exercising the real
642+
/// `EmitterConfig::from_toml_str` → resolve → `CREATE TABLE` path end-to-end.
643+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
644+
async fn create_table_auto_replicates_from_toml_namespace() {
645+
if !fx::pg_available() || !fx::pg_basebackup_available() || !fx::clickhouse_available() {
646+
eprintln!("skip: missing initdb / pg_basebackup / clickhouse");
647+
return;
648+
}
649+
650+
let tmp = tempfile::tempdir().unwrap();
651+
let source_port = SOURCE_PORT + 60;
652+
let shadow_port = SHADOW_PORT + 60;
653+
let ch_tcp_port = CH_TCP_PORT + 60;
654+
let ch_http_port = CH_HTTP_PORT + 60;
655+
let walsender_port = WALSENDER_PORT + 60;
656+
let (
657+
fx::BootstrappedClusters {
658+
source,
659+
shadow,
660+
shadow_filter_dir,
661+
},
662+
shadow_stream_state,
663+
) = fx::bootstrap_clusters(
664+
&tmp,
665+
"CREATE SCHEMA s15toml;\n",
666+
source_port,
667+
shadow_port,
668+
walsender_port,
669+
)
670+
.await;
671+
let _src_stop = fx::StopOnDrop { sh: &source };
672+
let _shd_stop = fx::StopOnDrop { sh: &shadow };
673+
674+
let ch_tmp = tempfile::tempdir().unwrap();
675+
let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch");
676+
ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test")
677+
.expect("create db");
678+
679+
// Empty programmatic namespaces — the TOML parsed in `tune` is the only
680+
// source of the auto-create flag.
681+
let mut pipeline = fx::build_pipeline_with(
682+
fx::BuildPipelineArgs {
683+
tmp: &tmp,
684+
source: &source,
685+
shadow: &shadow,
686+
shadow_filter_dir: &shadow_filter_dir,
687+
shadow_stream_state,
688+
ch_database: "walshadow_test",
689+
ch_tcp_port,
690+
mappings: vec![],
691+
app_name: "walshadow-ddl-create-auto-toml",
692+
ddl: Some(fx::DdlPipelineArgs::default()),
693+
},
694+
|cfg| {
695+
cfg.namespaces = EmitterConfig::from_toml_str(
696+
"[ch]\n\
697+
[namespace.s15toml]\n\
698+
target_database = \"walshadow_test\"\n\
699+
auto_create = true\n",
700+
)
701+
.expect("namespace toml parses")
702+
.namespaces;
703+
},
704+
)
705+
.await;
706+
707+
let driver = fx::spawn_workload(
708+
&source,
709+
vec![
710+
"CREATE TABLE s15toml.new_t (id bigint PRIMARY KEY, body text)".into(),
711+
"INSERT INTO s15toml.new_t (id, body) VALUES (1, 'toml-auto')".into(),
712+
"SELECT pg_switch_wal()".into(),
713+
],
714+
);
715+
716+
let shipped = fx::pump_segments(&mut pipeline, 1, Duration::from_secs(60)).await;
717+
let _ = driver.join();
718+
assert!(shipped >= 1, "no segments shipped in 60s");
719+
720+
let target = pipeline.stream.dispatched_lsn();
721+
let observed = shadow
722+
.wait_for_replay(target, Duration::from_secs(30))
723+
.expect("shadow replay");
724+
assert!(observed >= target);
725+
pipeline.shutdown().await.expect("pipeline drains clean");
726+
727+
let tbls = ch
728+
.query(
729+
"SELECT name FROM system.tables WHERE database = 'walshadow_test' AND name = 'new_t'",
730+
)
731+
.expect("ch table existence");
732+
assert_eq!(
733+
tbls, "new_t",
734+
"TOML [namespace] auto_create must create the CH table"
735+
);
736+
737+
let body = ch
738+
.query(
739+
"SELECT argMax(body, _lsn) FROM walshadow_test.new_t \
740+
WHERE _is_deleted = 0 AND id = 1",
741+
)
742+
.expect("ch body");
743+
assert_eq!(body, "toml-auto");
744+
}

tests/runtime_config_e2e.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@
4646
//! stored `123.45` (vs a scale-0 `123`) proves the override reached
4747
//! the projection (plans/config.md §Column overrides).
4848
//!
49+
//! 7. `auto_create_namespace_via_config_namespace`
50+
//! * Operator inserts `config_namespace (auto_create=true)`, no
51+
//! `config_table` row and no TOML mapping.
52+
//! * Source `CREATE TABLE` in the namespace + INSERT.
53+
//! * Expect: the namespace flag alone auto-creates the CH table and the
54+
//! row lands.
55+
//!
4956
//! Source-side `config_*` install runs the real `sql/runtime_config_install.sql`
5057
//! inside the bootstrap schema dump, so the drills double as install-script
5158
//! coverage (psql `\if` default-schema guard included).
@@ -641,6 +648,100 @@ async fn opt_in_then_alter_add_column_reaches_ch() {
641648
assert_eq!(pre, "pre-alter\t1");
642649
}
643650

651+
/// Drill 7: `config_namespace.auto_create = true` alone (no `config_table`
652+
/// row, no TOML mapping) authorises namespace-wide auto-create. A source
653+
/// `CREATE TABLE` in the flagged namespace must run `CREATE TABLE` on CH and
654+
/// the trailing INSERT must land — proving the overlay's namespace layer
655+
/// drives auto-create, not just the per-table `replicate=true` opt-in.
656+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
657+
async fn auto_create_namespace_via_config_namespace() {
658+
if !fx::pg_available() || !fx::pg_basebackup_available() || !fx::clickhouse_available() {
659+
eprintln!("skip: missing initdb / pg_basebackup / clickhouse");
660+
return;
661+
}
662+
663+
let tmp = tempfile::tempdir().unwrap();
664+
let source_port = SOURCE_PORT + 60;
665+
let shadow_port = SHADOW_PORT + 60;
666+
let ch_tcp_port = CH_TCP_PORT + 60;
667+
let ch_http_port = CH_HTTP_PORT + 60;
668+
let walsender_port = WALSENDER_PORT + 60;
669+
let schema_sql = format!("{INSTALL_SQL}\nCREATE SCHEMA app;\n");
670+
let (
671+
fx::BootstrappedClusters {
672+
source,
673+
shadow,
674+
shadow_filter_dir,
675+
},
676+
shadow_stream_state,
677+
) = fx::bootstrap_clusters(&tmp, &schema_sql, source_port, shadow_port, walsender_port).await;
678+
let _src_stop = fx::StopOnDrop { sh: &source };
679+
let _shd_stop = fx::StopOnDrop { sh: &shadow };
680+
681+
let ch_tmp = tempfile::tempdir().unwrap();
682+
let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch");
683+
ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test")
684+
.expect("create db");
685+
686+
// No TOML namespaces — the config_namespace row alone authorises it.
687+
let mut pipeline = fx::build_pipeline(fx::BuildPipelineArgs {
688+
tmp: &tmp,
689+
source: &source,
690+
shadow: &shadow,
691+
shadow_filter_dir: &shadow_filter_dir,
692+
shadow_stream_state,
693+
ch_database: "walshadow_test",
694+
ch_tcp_port,
695+
mappings: vec![],
696+
app_name: "walshadow-config-ns-auto-create",
697+
ddl: Some(overlay_ddl_args()),
698+
})
699+
.await;
700+
701+
// The auto_create row commits before the CREATE TABLE, so `apply_added`
702+
// sees the namespace in `auto_create_namespaces` when the DDL drains.
703+
let driver = fx::spawn_workload(
704+
&source,
705+
vec![
706+
"INSERT INTO walshadow.config_namespace (namespace, target_database, auto_create) \
707+
VALUES ('app', 'walshadow_test', true)"
708+
.into(),
709+
"CREATE TABLE app.thing (id bigint PRIMARY KEY, body text)".into(),
710+
"INSERT INTO app.thing (id, body) VALUES (1, 'ns-auto')".into(),
711+
"SELECT pg_switch_wal()".into(),
712+
],
713+
);
714+
715+
let shipped = fx::pump_segments(&mut pipeline, 1, Duration::from_secs(45)).await;
716+
let _ = driver.join();
717+
assert!(shipped >= 1, "no segments shipped in 45s");
718+
719+
let target = pipeline.stream.dispatched_lsn();
720+
let observed = shadow
721+
.wait_for_replay(target, Duration::from_secs(30))
722+
.expect("shadow replay");
723+
assert!(observed >= target);
724+
pipeline.shutdown().await.expect("pipeline drains clean");
725+
726+
let tbls = ch
727+
.query(
728+
"SELECT name FROM system.tables WHERE database = 'walshadow_test' AND name = 'thing'",
729+
)
730+
.expect("ch table existence");
731+
assert_eq!(
732+
tbls, "thing",
733+
"config_namespace.auto_create must create the CH table"
734+
);
735+
736+
let body = ch
737+
.query(
738+
"SELECT argMax(body, _lsn) FROM walshadow_test.thing \
739+
WHERE _is_deleted = 0 AND id = 1",
740+
)
741+
.expect("ch body");
742+
assert_eq!(body, "ns-auto");
743+
}
744+
644745
/// Drill 6: `config_column.target_type` reaches the emitted projection
645746
/// (plans/config.md §Column overrides). CH dest pre-created with
646747
/// `Decimal(38, 2)` while TOML deliberately maps the stale bridge default

0 commit comments

Comments
 (0)