@@ -134,6 +134,12 @@ fn normalize_database(db: &Option<String>, default_database: &str) -> String {
134134/// our generated DDL (LAYOUT before LIFETIME) and ClickHouse's SHOW CREATE
135135/// output (LIFETIME before LAYOUT)
136136///
137+ /// Recognised top-level clauses: `PRIMARY KEY`, `SOURCE`, `LAYOUT`, `LIFETIME`,
138+ /// `INVALIDATE_QUERY`, `SETTINGS`, `COMMENT`. Clause boundaries are detected with
139+ /// a depth/quote-aware scanner so keywords inside parentheses or quoted strings
140+ /// (e.g. an `INVALIDATE_QUERY` body containing the word `SETTINGS`) are not
141+ /// mistaken for clause starts.
142+ ///
137143/// **Limitation**: identifier quoting and type-alias differences (e.g.
138144/// `Int64` vs `Int64`) are preserved as-is; ClickHouse and our generator both
139145/// use backtick-quoted identifiers so these should agree in practice.
@@ -164,44 +170,102 @@ fn dicts_ddl_equivalent(actual_ddl: &str, desired_ddl: &str) -> bool {
164170
165171 let col_block = ddl[ start..end] . to_string ( ) ;
166172
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.
173+ // Parse top-level clauses by keyword prefix using a depth/quote-aware scanner.
174+ // Skips keyword matches inside parentheses or quoted strings so that e.g.
175+ // a SOURCE url containing "LIFETIME" or an INVALIDATE_QUERY containing "SETTINGS"
176+ // never splits a clause incorrectly.
169177 let remainder = ddl[ end..] . trim ( ) ;
170- let mut clauses = Vec :: new ( ) ;
171- let clause_keywords = [ "PRIMARY KEY" , "SOURCE" , "LAYOUT" , "LIFETIME" , "SETTINGS" ] ;
178+ let clause_keywords: & [ & str ] = & [
179+ "PRIMARY KEY" ,
180+ "SOURCE" ,
181+ "LAYOUT" ,
182+ "LIFETIME" ,
183+ "INVALIDATE_QUERY" ,
184+ "SETTINGS" ,
185+ "COMMENT" ,
186+ ] ;
187+
188+ // Returns the byte position (in `text`) and keyword for the first
189+ // top-level keyword occurrence at or after `from`, skipping over
190+ // parenthesised content and single/double-quoted strings.
191+ fn find_top_level_keyword < ' a > (
192+ text : & str ,
193+ from : usize ,
194+ keywords : & [ & ' a str ] ,
195+ ) -> Option < ( usize , & ' a str ) > {
196+ let mut depth: usize = 0 ;
197+ let mut in_single_quote = false ;
198+ let mut in_double_quote = false ;
199+
200+ for ( i, c) in text[ from..] . char_indices ( ) {
201+ let abs_pos = from + i;
202+
203+ if in_single_quote {
204+ if c == '\'' {
205+ in_single_quote = false ;
206+ }
207+ continue ;
208+ }
209+ if in_double_quote {
210+ if c == '"' {
211+ in_double_quote = false ;
212+ }
213+ continue ;
214+ }
215+ match c {
216+ '(' => depth += 1 ,
217+ ')' => depth = depth. saturating_sub ( 1 ) ,
218+ '\'' if depth == 0 => in_single_quote = true ,
219+ '"' if depth == 0 => in_double_quote = true ,
220+ _ if depth == 0 => {
221+ for & kw in keywords {
222+ if text[ abs_pos..] . starts_with ( kw) {
223+ // Require a word boundary before (start or non-alnum/underscore)
224+ let prev_ok = abs_pos == from
225+ || text[ ..abs_pos]
226+ . chars ( )
227+ . next_back ( )
228+ . map ( |p| !p. is_alphanumeric ( ) && p != '_' )
229+ . unwrap_or ( true ) ;
230+ // Require a word boundary after (end or non-alnum/underscore)
231+ let after = abs_pos + kw. len ( ) ;
232+ let next_ok = text[ after..]
233+ . chars ( )
234+ . next ( )
235+ . map ( |n| !n. is_alphanumeric ( ) && n != '_' )
236+ . unwrap_or ( true ) ;
237+ if prev_ok && next_ok {
238+ return Some ( ( abs_pos, kw) ) ;
239+ }
240+ }
241+ }
242+ }
243+ _ => { }
244+ }
245+ }
246+ None
247+ }
172248
249+ let mut clauses = Vec :: new ( ) ;
173250 let mut current_pos = 0 ;
174251 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 ( )
252+ match find_top_level_keyword ( remainder, current_pos, clause_keywords) {
253+ None => break ,
254+ Some ( ( clause_start, keyword) ) => {
255+ let clause_end = find_top_level_keyword (
256+ remainder,
257+ clause_start + keyword. len ( ) ,
258+ clause_keywords,
259+ )
260+ . map ( |( pos, _) | pos)
196261 . unwrap_or ( remainder. len ( ) ) ;
197262
198- let clause_text = remainder[ clause_start..clause_end] . trim ( ) ;
199- if !clause_text. is_empty ( ) {
200- clauses. push ( clause_text. to_string ( ) ) ;
263+ let clause_text = remainder[ clause_start..clause_end] . trim ( ) ;
264+ if !clause_text. is_empty ( ) {
265+ clauses. push ( clause_text. to_string ( ) ) ;
266+ }
267+ current_pos = clause_end;
201268 }
202- current_pos = clause_end;
203- } else {
204- break ;
205269 }
206270 }
207271
0 commit comments