Skip to content

Commit 23daf2e

Browse files
fixes
1 parent a92ee60 commit 23daf2e

File tree

14 files changed

+542
-121
lines changed

14 files changed

+542
-121
lines changed

apps/framework-cli-e2e/test/cluster.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* 4. Mixed environments (some tables with cluster, some without) work correctly
1414
* 5. Both TypeScript and Python SDKs support cluster configuration
1515
* 6. ReplicatedMergeTree with explicit keeper_path/replica_name (no cluster) works correctly
16+
* 7. ReplicatedMergeTree with auto-injected params (ClickHouse Cloud mode) works correctly
1617
*/
1718

1819
import { spawn, ChildProcess } from "child_process";
@@ -320,7 +321,7 @@ const createClusterTestSuite = (config: ClusterTestConfig) => {
320321
try {
321322
const result = await client.query({
322323
query:
323-
"SELECT name FROM system.tables WHERE database = 'local' AND name IN ('TableA', 'TableB', 'TableC', 'TableD') ORDER BY name",
324+
"SELECT name FROM system.tables WHERE database = 'local' AND name IN ('TableA', 'TableB', 'TableC', 'TableD', 'TableE') ORDER BY name",
324325
format: "JSONEachRow",
325326
});
326327

@@ -331,6 +332,7 @@ const createClusterTestSuite = (config: ClusterTestConfig) => {
331332
expect(tableNames).to.include("TableB");
332333
expect(tableNames).to.include("TableC");
333334
expect(tableNames).to.include("TableD");
335+
expect(tableNames).to.include("TableE");
334336
} finally {
335337
await client.close();
336338
}
@@ -353,6 +355,7 @@ const createClusterTestSuite = (config: ClusterTestConfig) => {
353355
{ name: "TableB", cluster: "cluster_b" },
354356
{ name: "TableC", cluster: null },
355357
{ name: "TableD", cluster: null },
358+
{ name: "TableE", cluster: null },
356359
]);
357360
});
358361

@@ -365,6 +368,7 @@ const createClusterTestSuite = (config: ClusterTestConfig) => {
365368
await verifyTableExists("TableB");
366369
await verifyTableExists("TableC");
367370
await verifyTableExists("TableD");
371+
await verifyTableExists("TableE");
368372
});
369373

370374
it("should create TableD with explicit keeper args and no cluster", async function () {
@@ -401,6 +405,39 @@ const createClusterTestSuite = (config: ClusterTestConfig) => {
401405
await client.close();
402406
}
403407
});
408+
409+
it("should create TableE with auto-injected params (ClickHouse Cloud mode)", async function () {
410+
this.timeout(TIMEOUTS.SCHEMA_VALIDATION_MS);
411+
412+
// Verify TableE was created with ReplicatedMergeTree and auto-injected params
413+
const client = createClient({
414+
url: CLICKHOUSE_CONFIG.url,
415+
username: CLICKHOUSE_CONFIG.username,
416+
password: CLICKHOUSE_CONFIG.password,
417+
database: CLICKHOUSE_CONFIG.database,
418+
});
419+
420+
try {
421+
const result = await client.query({
422+
query: "SHOW CREATE TABLE local.TableE",
423+
format: "JSONEachRow",
424+
});
425+
426+
const data = await result.json<{ statement: string }>();
427+
const createStatement = data[0].statement;
428+
429+
console.log(`TableE CREATE statement: ${createStatement}`);
430+
431+
// Verify it's ReplicatedMergeTree
432+
expect(createStatement).to.include("ReplicatedMergeTree");
433+
// Verify it has auto-injected params (Moose injects these in dev mode)
434+
expect(createStatement).to.match(/ReplicatedMergeTree\(/);
435+
// Verify it does NOT have ON CLUSTER (no cluster specified)
436+
expect(createStatement).to.not.include("ON CLUSTER");
437+
} finally {
438+
await client.close();
439+
}
440+
});
404441
});
405442
};
406443

apps/framework-cli/src/cli/display/infrastructure.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,11 @@ fn format_table_display(
236236
details.push(format!("Order by: {}", table.order_by));
237237
}
238238

239+
// Cluster section (if present)
240+
if let Some(ref cluster) = table.cluster_name {
241+
details.push(format!("Cluster: {}", cluster));
242+
}
243+
239244
// Engine section (if present)
240245
if let Some(ref engine) = table.engine {
241246
details.push(format!("Engine: {}", Into::<String>::into(engine.clone())));

apps/framework-cli/src/framework/core/infrastructure/table.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,10 @@ impl Table {
511511
})
512512
});
513513

514+
// Normalize auto-injected replication params so they match user code
515+
// This prevents spurious diffs when comparing tables from inframap with code
516+
let engine = engine.map(|e| e.normalize_auto_injected_params(&proto.name));
517+
514518
// Engine settings are now handled via table_settings field
515519

516520
let fallback = || -> OrderBy {

apps/framework-cli/src/framework/core/infrastructure_map.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,6 +1749,9 @@ impl InfrastructureMap {
17491749
// Detect engine change (e.g., MergeTree -> ReplacingMergeTree)
17501750
let engine_changed = table.engine != target_table.engine;
17511751

1752+
// Detect cluster name change
1753+
let cluster_changed = table.cluster_name != target_table.cluster_name;
1754+
17521755
let order_by_change = if order_by_changed {
17531756
OrderByChange {
17541757
before: table.order_by.clone(),
@@ -1793,6 +1796,7 @@ impl InfrastructureMap {
17931796
|| order_by_changed
17941797
|| engine_changed
17951798
|| indexes_changed
1799+
|| cluster_changed
17961800
{
17971801
// Use the strategy to determine the appropriate changes
17981802
let strategy_changes = strategy.diff_table_update(

apps/framework-cli/src/infrastructure/olap/clickhouse.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1700,6 +1700,12 @@ impl OlapOperations for ConfiguredDBClient {
17001700
debug!("Could not extract engine from CREATE TABLE query, falling back to system.tables engine column");
17011701
engine.as_str().try_into().ok()
17021702
};
1703+
1704+
// Normalize auto-injected replication params so they match user code
1705+
// This prevents spurious diffs when comparing tables from the DB with code
1706+
let engine_parsed = engine_parsed
1707+
.map(|e: ClickhouseEngine| e.normalize_auto_injected_params(&table_name));
1708+
17031709
let engine_params_hash = engine_parsed
17041710
.as_ref()
17051711
.map(|e: &ClickhouseEngine| e.non_alterable_params_hash());

apps/framework-cli/src/infrastructure/olap/clickhouse/diff_strategy.rs

Lines changed: 30 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -50,49 +50,6 @@ fn format_database_change_error(table_name: &str, before_db: &str, after_db: &st
5050
)
5151
}
5252

53-
/// Generates a formatted error message for cluster field changes.
54-
///
55-
/// This function creates a user-friendly error message explaining that cluster field
56-
/// changes require manual intervention to prevent data loss. Cluster changes are even
57-
/// more critical than database changes because they affect replication topology.
58-
///
59-
/// # Arguments
60-
/// * `table_name` - The name of the table being changed
61-
/// * `before_cluster` - The original cluster name (or "<none>" if None)
62-
/// * `after_cluster` - The new cluster name (or "<none>" if None)
63-
///
64-
/// # Returns
65-
/// A formatted string with migration instructions
66-
fn format_cluster_change_error(
67-
table_name: &str,
68-
before_cluster: &str,
69-
after_cluster: &str,
70-
) -> String {
71-
format!(
72-
"\n\n\
73-
ERROR: Cluster field change detected for table '{}'\n\
74-
\n\
75-
The cluster field changed from '{}' to '{}'\n\
76-
\n\
77-
Changing the cluster field is a destructive operation that requires\n\
78-
manual intervention to ensure data safety and proper replication.\n\
79-
\n\
80-
The cluster name is embedded in the table's replication path and cannot\n\
81-
be changed without recreating the table. This would cause data loss.\n\
82-
\n\
83-
To migrate this table to a different cluster:\n\
84-
\n\
85-
1. Export your existing data\n\
86-
2. Delete the old table definition from your code\n\
87-
3. Create a new table definition with the target cluster\n\
88-
4. Re-import your data if needed\n\
89-
\n\
90-
This ensures you maintain control over data migration and prevents\n\
91-
accidental data loss.\n",
92-
table_name, before_cluster, after_cluster
93-
)
94-
}
95-
9653
/// ClickHouse-specific table diff strategy
9754
///
9855
/// ClickHouse has several limitations that require drop+create operations instead of ALTER:
@@ -348,25 +305,15 @@ impl TableDiffStrategy for ClickHouseTableDiffStrategy {
348305
}
349306

350307
// Check if cluster has changed
351-
// Cluster name cannot be changed after table creation for replicated tables
352-
// as it's embedded in the replication path
353-
let cluster_changed = before.cluster_name != after.cluster_name;
354-
355-
if cluster_changed {
356-
let before_cluster = before.cluster_name.as_deref().unwrap_or("<none>");
357-
let after_cluster = after.cluster_name.as_deref().unwrap_or("<none>");
358-
359-
let error_message =
360-
format_cluster_change_error(&before.name, before_cluster, after_cluster);
361-
362-
log::error!("{}", error_message);
363-
364-
return vec![OlapChange::Table(TableChange::ValidationError {
365-
table_name: before.name.clone(),
366-
message: error_message,
367-
before: Box::new(before.clone()),
368-
after: Box::new(after.clone()),
369-
})];
308+
if before.cluster_name != after.cluster_name {
309+
log::warn!(
310+
"ClickHouse: cluster changed for table '{}' (from {:?} to {:?}), requiring drop+create",
311+
before.name, before.cluster_name, after.cluster_name
312+
);
313+
return vec![
314+
OlapChange::Table(TableChange::Removed(before.clone())),
315+
OlapChange::Table(TableChange::Added(after.clone())),
316+
];
370317
}
371318

372319
// Check if PARTITION BY has changed
@@ -1337,27 +1284,16 @@ mod tests {
13371284

13381285
let changes = strategy.diff_table_update(&before, &after, vec![], order_by_change, "local");
13391286

1340-
// Should return exactly one ValidationError
1341-
assert_eq!(changes.len(), 1);
1287+
// Should return DROP + CREATE (like ORDER BY changes)
1288+
assert_eq!(changes.len(), 2);
13421289
assert!(matches!(
13431290
changes[0],
1344-
OlapChange::Table(TableChange::ValidationError { .. })
1291+
OlapChange::Table(TableChange::Removed(_))
1292+
));
1293+
assert!(matches!(
1294+
changes[1],
1295+
OlapChange::Table(TableChange::Added(_))
13451296
));
1346-
1347-
// Check the error message contains expected information
1348-
if let OlapChange::Table(TableChange::ValidationError {
1349-
table_name,
1350-
message,
1351-
..
1352-
}) = &changes[0]
1353-
{
1354-
assert_eq!(table_name, "test");
1355-
assert!(message.contains("<none>"));
1356-
assert!(message.contains("test_cluster"));
1357-
assert!(message.contains("manual intervention"));
1358-
} else {
1359-
panic!("Expected ValidationError variant");
1360-
}
13611297
}
13621298

13631299
#[test]
@@ -1378,27 +1314,16 @@ mod tests {
13781314

13791315
let changes = strategy.diff_table_update(&before, &after, vec![], order_by_change, "local");
13801316

1381-
// Should return exactly one ValidationError
1382-
assert_eq!(changes.len(), 1);
1317+
// Should return DROP + CREATE (like ORDER BY changes)
1318+
assert_eq!(changes.len(), 2);
13831319
assert!(matches!(
13841320
changes[0],
1385-
OlapChange::Table(TableChange::ValidationError { .. })
1321+
OlapChange::Table(TableChange::Removed(_))
1322+
));
1323+
assert!(matches!(
1324+
changes[1],
1325+
OlapChange::Table(TableChange::Added(_))
13861326
));
1387-
1388-
// Check the error message contains expected information
1389-
if let OlapChange::Table(TableChange::ValidationError {
1390-
table_name,
1391-
message,
1392-
..
1393-
}) = &changes[0]
1394-
{
1395-
assert_eq!(table_name, "test");
1396-
assert!(message.contains("test_cluster"));
1397-
assert!(message.contains("<none>"));
1398-
assert!(message.contains("manual intervention"));
1399-
} else {
1400-
panic!("Expected ValidationError variant");
1401-
}
14021327
}
14031328

14041329
#[test]
@@ -1419,27 +1344,16 @@ mod tests {
14191344

14201345
let changes = strategy.diff_table_update(&before, &after, vec![], order_by_change, "local");
14211346

1422-
// Should return exactly one ValidationError
1423-
assert_eq!(changes.len(), 1);
1347+
// Should return DROP + CREATE (like ORDER BY changes)
1348+
assert_eq!(changes.len(), 2);
14241349
assert!(matches!(
14251350
changes[0],
1426-
OlapChange::Table(TableChange::ValidationError { .. })
1351+
OlapChange::Table(TableChange::Removed(_))
1352+
));
1353+
assert!(matches!(
1354+
changes[1],
1355+
OlapChange::Table(TableChange::Added(_))
14271356
));
1428-
1429-
// Check the error message contains expected information
1430-
if let OlapChange::Table(TableChange::ValidationError {
1431-
table_name,
1432-
message,
1433-
..
1434-
}) = &changes[0]
1435-
{
1436-
assert_eq!(table_name, "test");
1437-
assert!(message.contains("cluster_a"));
1438-
assert!(message.contains("cluster_b"));
1439-
assert!(message.contains("replication path"));
1440-
} else {
1441-
panic!("Expected ValidationError variant");
1442-
}
14431357
}
14441358

14451359
#[test]

0 commit comments

Comments
 (0)