Skip to content

Commit 00010bf

Browse files
Benoit Aubuchonclaude
andcommitted
refactor: extract QuoteDepthState to eliminate duplicate DDL scanner logic
The quote-and-depth-aware character scanner was implemented identically in two places: the column block walker in `extract_body` and the `find_top_level_keyword` inner function. Extract a `QuoteDepthState` struct with a single `advance(ch) -> bool` method that owns all five state variables (depth, in_single/double/backtick, skip_next_single). Both call sites are reduced to a loop over char_indices that delegates state management to `QuoteDepthState::advance`, eliminating the risk of fixes landing in one copy but not the other. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d2000bb commit 00010bf

1 file changed

Lines changed: 81 additions & 102 deletions

File tree

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

Lines changed: 81 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,62 @@ fn fill_hidden_credentials(actual: &str, desired: &str) -> String {
181181
result
182182
}
183183

184+
/// Stateful depth-and-quote tracker for walking ClickHouse DDL text character
185+
/// by character.
186+
///
187+
/// Call [`advance`] once per character. When it returns `true` the character
188+
/// is "active" (not inside a quoted string) and `self.depth` reflects the paren
189+
/// depth *after* processing it. When it returns `false` the character was
190+
/// consumed as part of a quoted literal and the caller should skip it.
191+
#[derive(Default)]
192+
struct QuoteDepthState {
193+
pub depth: usize,
194+
in_single: bool,
195+
in_double: bool,
196+
in_backtick: bool,
197+
skip_next_single: bool,
198+
}
199+
200+
impl QuoteDepthState {
201+
fn advance(&mut self, ch: char) -> bool {
202+
if self.in_single {
203+
if self.skip_next_single {
204+
self.skip_next_single = false;
205+
return false;
206+
}
207+
if ch == '\\' {
208+
self.skip_next_single = true;
209+
return false;
210+
}
211+
if ch == '\'' {
212+
self.in_single = false;
213+
}
214+
return false;
215+
}
216+
if self.in_double {
217+
if ch == '"' {
218+
self.in_double = false;
219+
}
220+
return false;
221+
}
222+
if self.in_backtick {
223+
if ch == '`' {
224+
self.in_backtick = false;
225+
}
226+
return false;
227+
}
228+
match ch {
229+
'\'' => self.in_single = true,
230+
'"' => self.in_double = true,
231+
'`' => self.in_backtick = true,
232+
'(' => self.depth += 1,
233+
')' => self.depth = self.depth.saturating_sub(1),
234+
_ => {}
235+
}
236+
true
237+
}
238+
}
239+
184240
/// Strips the `CREATE DICTIONARY …` header from each (everything before the
185241
/// first `(`), then for the column block and each subsequent clause:
186242
/// - normalises whitespace (collapses runs of whitespace to a single space)
@@ -216,52 +272,15 @@ fn dicts_ddl_equivalent(actual_ddl: &str, desired_ddl: &str) -> bool {
216272
// matching `)` that closes the column list.
217273
// Quote-aware: a `)` inside a quoted value (single, double, or backtick)
218274
// must not prematurely end the column block.
219-
let mut depth = 0usize;
220-
let mut in_single = false;
221-
let mut in_double = false;
222-
let mut in_backtick = false;
223-
let mut skip_next_single = false;
275+
let mut qs = QuoteDepthState::default();
224276
let mut end = start;
225277
for (off, ch) in ddl[start..].char_indices() {
226-
if in_single {
227-
if skip_next_single {
228-
skip_next_single = false;
229-
continue;
230-
}
231-
if ch == '\\' {
232-
skip_next_single = true;
233-
continue;
234-
}
235-
if ch == '\'' {
236-
in_single = false;
237-
}
278+
if !qs.advance(ch) {
238279
continue;
239280
}
240-
if in_double {
241-
if ch == '"' {
242-
in_double = false;
243-
}
244-
continue;
245-
}
246-
if in_backtick {
247-
if ch == '`' {
248-
in_backtick = false;
249-
}
250-
continue;
251-
}
252-
match ch {
253-
'\'' => in_single = true,
254-
'"' => in_double = true,
255-
'`' => in_backtick = true,
256-
'(' => depth += 1,
257-
')' => {
258-
depth = depth.saturating_sub(1);
259-
if depth == 0 {
260-
end = start + off + ')'.len_utf8();
261-
break;
262-
}
263-
}
264-
_ => {}
281+
if ch == ')' && qs.depth == 0 {
282+
end = start + off + ')'.len_utf8();
283+
break;
265284
}
266285
}
267286

@@ -290,73 +309,33 @@ fn dicts_ddl_equivalent(actual_ddl: &str, desired_ddl: &str) -> bool {
290309
from: usize,
291310
keywords: &[&'a str],
292311
) -> Option<(usize, &'a str)> {
293-
let mut depth: usize = 0;
294-
let mut in_single_quote = false;
295-
let mut in_double_quote = false;
296-
let mut in_backtick = false;
297-
let mut skip_next_single = false;
298-
312+
let mut qs = QuoteDepthState::default();
299313
for (i, c) in text[from..].char_indices() {
300314
let abs_pos = from + i;
301-
302-
if in_single_quote {
303-
if skip_next_single {
304-
skip_next_single = false;
305-
continue;
306-
}
307-
if c == '\\' {
308-
skip_next_single = true;
309-
continue;
310-
}
311-
if c == '\'' {
312-
in_single_quote = false;
313-
}
314-
continue;
315-
}
316-
if in_double_quote {
317-
if c == '"' {
318-
in_double_quote = false;
319-
}
320-
continue;
321-
}
322-
if in_backtick {
323-
if c == '`' {
324-
in_backtick = false;
325-
}
315+
if !qs.advance(c) {
326316
continue;
327317
}
328-
match c {
329-
'(' => depth += 1,
330-
')' => depth = depth.saturating_sub(1),
331-
// Track quotes/backticks at any depth so special chars inside
332-
// SOURCE credentials or backtick-quoted identifiers don't
333-
// corrupt depth tracking or trigger false keyword matches.
334-
'\'' => in_single_quote = true,
335-
'"' => in_double_quote = true,
336-
'`' => in_backtick = true,
337-
_ if depth == 0 => {
338-
for &kw in keywords {
339-
if text[abs_pos..].starts_with(kw) {
340-
// Require a word boundary before (start of text or non-alnum/underscore)
341-
let prev_ok = text[..abs_pos]
342-
.chars()
343-
.next_back()
344-
.map(|p| !p.is_alphanumeric() && p != '_')
345-
.unwrap_or(true);
346-
// Require a word boundary after (end or non-alnum/underscore)
347-
let after = abs_pos + kw.len();
348-
let next_ok = text[after..]
349-
.chars()
350-
.next()
351-
.map(|n| !n.is_alphanumeric() && n != '_')
352-
.unwrap_or(true);
353-
if prev_ok && next_ok {
354-
return Some((abs_pos, kw));
355-
}
318+
if qs.depth == 0 {
319+
for &kw in keywords {
320+
if text[abs_pos..].starts_with(kw) {
321+
// Require a word boundary before (start of text or non-alnum/underscore)
322+
let prev_ok = text[..abs_pos]
323+
.chars()
324+
.next_back()
325+
.map(|p| !p.is_alphanumeric() && p != '_')
326+
.unwrap_or(true);
327+
// Require a word boundary after (end or non-alnum/underscore)
328+
let after = abs_pos + kw.len();
329+
let next_ok = text[after..]
330+
.chars()
331+
.next()
332+
.map(|n| !n.is_alphanumeric() && n != '_')
333+
.unwrap_or(true);
334+
if prev_ok && next_ok {
335+
return Some((abs_pos, kw));
356336
}
357337
}
358338
}
359-
_ => {}
360339
}
361340
}
362341
None

0 commit comments

Comments
 (0)