@@ -100,142 +100,183 @@ def replace_keyword(match):
100100 return mark_safe (highlighted )
101101
102102
103- def truncate_sql ( sql , max_length = 150 ) :
104- """Intelligently truncate long SQL queries while preserving structure.
103+ class SQLTruncator :
104+ """Handles intelligent SQL query truncation while preserving the structure."""
105105
106- Attempts to preserve the most important parts of the query:
107- - Extracts first column from SELECT clause
108- - Includes primary table from FROM clause
109- - Preserves as many JOINs as fit within max_length
110- - Respects max_length limit (default 150 chars)
106+ def __init__ (self , max_length = 150 ):
107+ self .max_length = max_length
111108
112- Falls back to clause-boundary truncation if structure parsing fails.
109+ def truncate (self , sql ):
110+ """Intelligently truncate long SQL queries while preserving the structure.
113111
114- Args:
115- sql: SQL query string
116- max_length: Maximum length of truncated output (default 150)
112+ Args:
113+ sql: SQL query string
117114
118- Returns:
119- Truncated SQL string with "..." at the end if shortened
120- """
121- if len (sql ) <= max_length :
122- return sql
115+ Returns:
116+ Truncated SQL string with "..." at the end if shortened
117+ """
118+ if len (sql ) <= self . max_length :
119+ return sql
123120
124- sql_upper = sql .upper ()
121+ sql_upper = sql .upper ()
125122
126- if sql_upper .startswith ("SELECT" ):
123+ if sql_upper .startswith ("SELECT" ):
124+ result = self ._truncate_select (sql , sql_upper )
125+ if result :
126+ return result
127+
128+ return self ._fallback_truncate (sql )
129+
130+ def _truncate_select (self , sql , sql_upper ):
131+ """Truncate SELECT queries while preserving columns and joins."""
127132 distinct_match = re .search (r"\bDISTINCT\b" , sql_upper [:20 ])
128133 has_distinct = distinct_match is not None
129134 from_match = re .search (r"\bFROM\b" , sql_upper )
130- if from_match :
131- from_pos = from_match .start ()
132135
133- if has_distinct :
134- distinct_end = distinct_match .end ()
135- select_part = sql [distinct_end :from_pos ].strip ()
136+ if not from_match :
137+ return None
138+
139+ from_pos = from_match .start ()
140+
141+ if has_distinct :
142+ distinct_end = distinct_match .end ()
143+ select_part = sql [distinct_end :from_pos ].strip ()
144+ else :
145+ select_part = sql [6 :from_pos ].strip ()
146+
147+ first_column = self ._extract_first_column (select_part )
148+ select_clause = "SELECT DISTINCT" if has_distinct else "SELECT"
149+
150+ from_part = sql [from_pos :]
151+ table_match = re .search (r'FROM\s+(["\[]?)(\w+)' , from_part , re .IGNORECASE )
152+
153+ if not table_match :
154+ return None
155+
156+ table_name = table_match .group (2 )
157+ result = f"{ select_clause } { first_column } FROM { table_name } "
158+
159+ # Add joins if they fit
160+ join_result = self ._add_joins_if_fit (result , from_part )
161+ if join_result :
162+ return join_result
163+
164+ return result if len (result ) <= self .max_length else None
165+
166+ def _extract_first_column (self , select_part ):
167+ """Extract the first column from SELECT clause, handling quotes and parentheses."""
168+ if not select_part .strip ():
169+ return "..."
170+ columns = self ._parse_columns (select_part )
171+ return columns [0 ] + ", ..." if columns else "..."
172+
173+ def _parse_columns (self , select_part ):
174+ """Parse columns from SELECT clause, handling complex expressions."""
175+ columns = []
176+ in_quotes = False
177+ quote_char = None
178+ in_parens = False
179+ paren_depth = 0
180+ current_col = ""
181+
182+ for i , char in enumerate (select_part ):
183+ if char in ('"' , "'" ) and (i == 0 or select_part [i - 1 ] != "\\ " ):
184+ if not in_quotes :
185+ in_quotes = True
186+ quote_char = char
187+ elif char == quote_char :
188+ in_quotes = False
189+ quote_char = None
190+ current_col += char
191+ elif in_quotes :
192+ current_col += char
193+ elif char == "(" :
194+ in_parens = True
195+ paren_depth += 1
196+ current_col += char
197+ elif char == ")" :
198+ paren_depth -= 1
199+ if paren_depth == 0 :
200+ in_parens = False
201+ current_col += char
202+ elif char == "," and not in_parens and not in_quotes :
203+ if current_col .strip ():
204+ columns .append (current_col .strip ())
205+ break
206+ current_col = ""
207+ elif char in " \t \n " and not in_parens and not in_quotes :
208+ if current_col .strip ():
209+ columns .append (current_col .strip ())
210+ current_col = ""
136211 else :
137- select_part = sql [6 :from_pos ].strip ()
138-
139- columns = []
140- in_quotes = False
141- quote_char = None
142- in_parens = False
143- paren_depth = 0
144- current_col = ""
145-
146- for i , char in enumerate (select_part ):
147- if char in ('"' , "'" ) and (i == 0 or select_part [i - 1 ] != "\\ " ):
148- if not in_quotes :
149- in_quotes = True
150- quote_char = char
151- elif char == quote_char :
152- in_quotes = False
153- quote_char = None
154- current_col += char
155- elif in_quotes :
156- current_col += char
157- elif char == "(" :
158- in_parens = True
159- paren_depth += 1
160- current_col += char
161- elif char == ")" :
162- paren_depth -= 1
163- if paren_depth == 0 :
164- in_parens = False
165- current_col += char
166- elif char == "," and not in_parens and not in_quotes :
167- if current_col .strip ():
168- columns .append (current_col .strip ())
169- break
170- current_col = ""
171- elif char in " \t \n " and not in_parens and not in_quotes :
172- if current_col .strip ():
173- columns .append (current_col .strip ())
174- current_col = ""
175- else :
176- current_col += char
177-
178- if not columns and current_col .strip ():
179- columns .append (current_col .strip ())
180-
181- if columns :
182- columns_str = columns [0 ] + ", ..."
212+ current_col += char
213+
214+ if not columns and current_col .strip ():
215+ columns .append (current_col .strip ())
216+
217+ return columns
218+
219+ def _add_joins_if_fit (self , base_result , from_part ):
220+ """Add JOIN clauses to result if they fit within max_length."""
221+ join_matches = re .finditer (
222+ r'\b(LEFT\s+|RIGHT\s+|INNER\s+|OUTER\s+)?JOIN\s+(["\[]?)(\w+)' ,
223+ from_part ,
224+ re .IGNORECASE ,
225+ )
226+
227+ result = base_result
228+
229+ for join_match in join_matches :
230+ join_type = (join_match .group (1 ) or "" ).strip ()
231+ join_table = join_match .group (3 )
232+ new_result = (
233+ f"{ result } { join_type } JOIN { join_table } "
234+ if join_type
235+ else f"{ result } JOIN { join_table } "
236+ )
237+ if len (new_result ) <= self .max_length :
238+ result = new_result
183239 else :
184- columns_str = "..."
185-
186- select_clause = "SELECT DISTINCT" if has_distinct else "SELECT"
187-
188- from_part = sql [from_pos :]
189- table_match = re .search (r'FROM\s+(["\[]?)(\w+)' , from_part , re .IGNORECASE )
190- if table_match :
191- table_name = table_match .group (2 )
192-
193- join_matches = re .finditer (
194- r'\b(LEFT\s+|RIGHT\s+|INNER\s+|OUTER\s+)?JOIN\s+(["\[]?)(\w+)' ,
195- from_part ,
196- re .IGNORECASE ,
197- )
198-
199- result = f"{ select_clause } { columns_str } FROM { table_name } "
200-
201- for join_match in join_matches :
202- join_type = (join_match .group (1 ) or "" ).strip ()
203- join_table = join_match .group (3 )
204- new_result = (
205- f"{ result } { join_type } JOIN { join_table } "
206- if join_type
207- else f"{ result } JOIN { join_table } "
208- )
209- if len (new_result ) <= max_length :
210- result = new_result
211- else :
212- break
213-
214- if len (result ) <= max_length :
215- return result
216-
217- result = f"{ select_clause } { columns_str } FROM { table_name } "
218- if len (result ) <= max_length :
219- return result
220-
221- clause_positions = [
222- (match .start (), match .group ())
223- for match in SQL_CLAUSES_RE .finditer (sql [: max_length + 50 ])
224- ]
225-
226- if clause_positions :
240+ break
241+
242+ return result if len (result ) <= self .max_length else None
243+
244+ def _fallback_truncate (self , sql ):
245+ """Fallback truncation at clause boundaries or simple length limit."""
246+ # First try simple length truncation
247+ if len (sql ) <= self .max_length :
248+ return sql
249+
250+ # Try clause boundary truncation
251+ clause_positions = [
252+ (match .start (), match .group ())
253+ for match in SQL_CLAUSES_RE .finditer (sql [: self .max_length + 50 ])
254+ ]
255+
256+ if clause_positions :
257+ best_pos = self ._find_best_truncate_position (clause_positions )
258+ if best_pos > 0 :
259+ return sql [:best_pos ].rstrip () + "..."
260+
261+ # Final fallback: simple truncation
262+ return sql [: self .max_length ].rstrip () + "..."
263+
264+ def _find_best_truncate_position (self , clause_positions ):
265+ """Find the best position to truncate based on clause boundaries."""
227266 best_pos = 0
228267 for pos , _ in clause_positions :
229- if max_length > pos > best_pos :
268+ if self . max_length > pos > best_pos :
230269 best_pos = pos
231270
232- if best_pos > max_length - 30 :
271+ if best_pos > self . max_length - 30 :
233272 best_pos = 0
234273
235- if best_pos > 0 :
236- return sql [:best_pos ].rstrip () + "..."
274+ return best_pos
237275
238- return sql [:max_length ].rstrip () + "..."
276+
277+ def truncate_sql (sql , max_length = 150 ):
278+ truncator = SQLTruncator (max_length )
279+ return truncator .truncate (sql )
239280
240281
241282def tracking_wrapper (execute , sql , params , many , context ):
0 commit comments