@@ -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