Skip to content

Commit b40d286

Browse files
Benoit Aubuchonclaude
andcommitted
fix(dictionaries): parse DDL clauses by keyword instead of by line
Replaces line-by-line splitting in dicts_ddl_equivalent with keyword-based clause extraction (PRIMARY KEY, SOURCE, LAYOUT, LIFETIME, SETTINGS). This prevents multi-line SOURCE clauses (e.g. HTTP/MySQL external sources) from being interleaved with unrelated clauses during sorting, which would cause permanent false mismatches on every reconciliation cycle. Also updates the drift test to insert the extra column inside the column block (inside parens) — the correct location for dictionary column definitions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ddc4e02 commit b40d286

1 file changed

Lines changed: 44 additions & 7 deletions

File tree

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

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,47 @@ fn dicts_ddl_equivalent(actual_ddl: &str, desired_ddl: &str) -> bool {
163163
}
164164

165165
let col_block = ddl[start..end].to_string();
166-
let clauses: Vec<String> = ddl[end..]
167-
.lines()
168-
.map(|l| l.trim().to_string())
169-
.filter(|l| !l.is_empty())
170-
.collect();
166+
167+
// Parse top-level clauses by their keyword prefix rather than splitting by lines.
168+
// This handles multi-line clauses like SOURCE(...) that may span multiple lines.
169+
let remainder = ddl[end..].trim();
170+
let mut clauses = Vec::new();
171+
let clause_keywords = ["PRIMARY KEY", "SOURCE", "LAYOUT", "LIFETIME", "SETTINGS"];
172+
173+
let mut current_pos = 0;
174+
while current_pos < remainder.len() {
175+
// Find the next clause keyword
176+
let next_clause_start = clause_keywords
177+
.iter()
178+
.filter_map(|&keyword| {
179+
remainder[current_pos..]
180+
.find(keyword)
181+
.map(|pos| (current_pos + pos, keyword))
182+
})
183+
.min_by_key(|(pos, _)| *pos);
184+
185+
if let Some((keyword_pos, keyword)) = next_clause_start {
186+
// Find where this clause ends (either at the next keyword or end of string)
187+
let clause_start = keyword_pos;
188+
let clause_end = clause_keywords
189+
.iter()
190+
.filter_map(|&kw| {
191+
remainder[clause_start + keyword.len()..]
192+
.find(kw)
193+
.map(|pos| clause_start + keyword.len() + pos)
194+
})
195+
.min()
196+
.unwrap_or(remainder.len());
197+
198+
let clause_text = remainder[clause_start..clause_end].trim();
199+
if !clause_text.is_empty() {
200+
clauses.push(clause_text.to_string());
201+
}
202+
current_pos = clause_end;
203+
} else {
204+
break;
205+
}
206+
}
171207

172208
(col_block, clauses)
173209
}
@@ -2228,9 +2264,10 @@ mod tests {
22282264

22292265
// Desired DDL generated from the infra map dict
22302266
let desired_ddl = dict.to_create_if_not_exists_sql();
2231-
// Simulate a drifted CH definition (extra column added manually)
2267+
// Simulate a drifted CH definition: extra column added inside the column block
2268+
// (dictionary columns live inside the parens, not between ) and PRIMARY KEY)
22322269
let drifted_ddl =
2233-
desired_ddl.replace("PRIMARY KEY", " `extra_col` String,\nPRIMARY KEY");
2270+
desired_ddl.replace("`id` UInt64", "`id` UInt64,\n `extra_col` String");
22342271
// Simulate SHOW CREATE DICTIONARY output (no IF NOT EXISTS)
22352272
let actual_ddl =
22362273
drifted_ddl.replace("CREATE DICTIONARY IF NOT EXISTS", "CREATE DICTIONARY");

0 commit comments

Comments
 (0)