@@ -68,7 +68,7 @@ module StringCteParser
6868 # Examples that DON'T match:
6969 # users (no backticks)
7070 # `users (missing closing backtick)
71- BACKTICK_QUOTED_TABLE = /`([^`]+)`/
71+ BACKTICK_QUOTED_TABLE = /`([^`]+)`/ . freeze
7272
7373 # Matches table names enclosed in double quotes: "table_name"
7474 # Examples that MATCH:
@@ -78,7 +78,7 @@ module StringCteParser
7878 # Examples that DON'T match:
7979 # users (no quotes)
8080 # "users (missing closing quote)
81- DOUBLE_QUOTED_TABLE = /"([^"]+)"/
81+ DOUBLE_QUOTED_TABLE = /"([^"]+)"/ . freeze
8282
8383 # Matches unquoted table names: must start with letter/underscore, can contain letters/numbers/underscores
8484 # Examples that MATCH:
@@ -92,23 +92,23 @@ module StringCteParser
9292 # user-posts (contains hyphen)
9393 # user.posts (contains dot)
9494 # "users" (has quotes - handled by other patterns)
95- UNQUOTED_TABLE = /([a-zA-Z_][a-zA-Z0-9_]*)/
95+ UNQUOTED_TABLE = /([a-zA-Z_][a-zA-Z0-9_]*)/ . freeze
9696
9797 # Combines all table name patterns with non-capturing group
9898 # Examples that MATCH:
9999 # `users` → captures "users" from backtick group
100100 # "user_posts" → captures "user_posts" from quote group
101101 # popular_posts → captures "popular_posts" from unquoted group
102102 # Note: Only one capture group will have a value, the others will be nil
103- TABLE_NAME_PATTERN = /(?:#{ BACKTICK_QUOTED_TABLE } |#{ DOUBLE_QUOTED_TABLE } |#{ UNQUOTED_TABLE } )/
103+ TABLE_NAME_PATTERN = /(?:#{ BACKTICK_QUOTED_TABLE } |#{ DOUBLE_QUOTED_TABLE } |#{ UNQUOTED_TABLE } )/ . freeze
104104
105105 # Matches the AS keyword (case insensitive)
106106 # Examples that MATCH:
107107 # AS, as, As, aS (any case combination)
108108 # Examples that DON'T match:
109109 # A S (space in between)
110110 # ASS (too many letters)
111- AS_KEYWORD = /AS/i
111+ AS_KEYWORD = /AS/i . freeze
112112
113113 # Matches the SQL expression inside parentheses (greedy match for everything inside)
114114 # Examples that MATCH:
@@ -119,7 +119,7 @@ module StringCteParser
119119 # SELECT * FROM posts (no parentheses)
120120 # (SELECT * FROM posts (missing closing paren)
121121 # SELECT * FROM posts) (missing opening paren)
122- EXPRESSION_PATTERN = /\( (.+)\) /
122+ EXPRESSION_PATTERN = /\( (.+)\) / . freeze
123123
124124 # Complete CTE string pattern: optional whitespace + table_name + whitespace + AS + whitespace + (expression) + optional whitespace
125125 # Examples that MATCH:
@@ -132,7 +132,7 @@ module StringCteParser
132132 # "popular_posts AS SELECT * FROM posts" (missing parentheses)
133133 # "AS (SELECT * FROM posts)" (missing table name)
134134 # "123_table AS (SELECT * FROM posts)" (invalid table name)
135- CTE_STRING_PATTERN = /\A \s *#{ TABLE_NAME_PATTERN } \s +#{ AS_KEYWORD } \s +#{ EXPRESSION_PATTERN } \s *\z /i
135+ CTE_STRING_PATTERN = /\A \s *#{ TABLE_NAME_PATTERN } \s +#{ AS_KEYWORD } \s +#{ EXPRESSION_PATTERN } \s *\z /i . freeze
136136
137137 # ---------------------------------------------------------------------------
138138 # Main parsing method that converts a CTE string into an Arel::Nodes::As node
@@ -146,9 +146,11 @@ def self.parse(string)
146146 if string . strip . empty?
147147 raise ArgumentError , "CTE string cannot be empty"
148148 elsif !string . match ( /\s AS\s /i )
149- raise ArgumentError , "CTE string must contain 'AS' keyword. Expected 'table_name AS (SELECT ...)' but got: #{ string } "
150- elsif !string . include? ( '(' ) || !string . include? ( ')' )
151- raise ArgumentError , "CTE expression must be enclosed in parentheses. Expected 'table_name AS (SELECT ...)' but got: #{ string } "
149+ raise ArgumentError ,
150+ "CTE string must contain 'AS' keyword. Expected 'table_name AS (SELECT ...)' but got: #{ string } "
151+ elsif !string . include? ( "(" ) || !string . include? ( ")" )
152+ raise ArgumentError ,
153+ "CTE expression must be enclosed in parentheses. Expected 'table_name AS (SELECT ...)' but got: #{ string } "
152154 else
153155 raise ArgumentError , "Invalid CTE string format. Expected 'table_name AS (SELECT ...)' but got: #{ string } "
154156 end
@@ -174,13 +176,9 @@ def self.parse(string)
174176 # ---------------------------------------------------------------------------
175177 # Validates that the extracted CTE components are not empty or whitespace-only
176178 def self . validate_cte_components ( table_name , expression )
177- if table_name . nil? || table_name . strip . empty?
178- raise ArgumentError , "Empty table name in CTE string"
179- end
179+ raise ArgumentError , "Empty table name in CTE string" if table_name . nil? || table_name . strip . empty?
180180
181- if expression . nil? || expression . strip . empty?
182- raise ArgumentError , "Empty expression in CTE string"
183- end
181+ raise ArgumentError , "Empty expression in CTE string" if expression . nil? || expression . strip . empty?
184182 end
185183
186184 # ---------------------------------------------------------------------------
@@ -191,18 +189,16 @@ def self.validate_expression_syntax(expression)
191189 paren_count = 0
192190 expression . each_char do |char |
193191 case char
194- when '('
192+ when "("
195193 paren_count += 1
196- when ')'
194+ when ")"
197195 paren_count -= 1
198196 # If we have more closing than opening parens, fail immediately
199- break if paren_count < 0
197+ break if paren_count . negative?
200198 end
201199 end
202200
203- unless paren_count == 0
204- raise ArgumentError , "Unbalanced parentheses in CTE expression: #{ expression } "
205- end
201+ raise ArgumentError , "Unbalanced parentheses in CTE expression: #{ expression } " unless paren_count . zero?
206202 end
207203 end
208204 end
0 commit comments