niceql 0.4.1 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/niceql.rb CHANGED
@@ -1,48 +1,60 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "niceql/version"
4
+ require "securerandom"
5
+ require "forwardable"
2
6
 
3
7
  module Niceql
4
-
5
8
  module StringColorize
6
- def self.colorize_verb( str)
7
- # yellow ANSI color
8
- "\e[0;33;49m#{str}\e[0m"
9
- end
10
- def self.colorize_str(str)
11
- # cyan ANSI color
12
- "\e[0;36;49m#{str}\e[0m"
13
- end
14
- def self.colorize_err(err)
15
- # red ANSI color
16
- "\e[0;31;49m#{err}\e[0m"
17
- end
18
- end
9
+ class << self
10
+ def colorize_keyword(str)
11
+ # yellow ANSI color
12
+ "\e[0;33;49m#{str}\e[0m"
13
+ end
19
14
 
20
- module ArExtentions
21
- def exec_niceql
22
- connection.execute( to_niceql )
23
- end
15
+ def colorize_str(str)
16
+ # cyan ANSI color
17
+ "\e[0;36;49m#{str}\e[0m"
18
+ end
24
19
 
25
- def to_niceql
26
- Prettifier.prettify_sql(to_sql, false)
27
- end
20
+ def colorize_err(err)
21
+ # red ANSI color
22
+ "\e[0;31;49m#{err}\e[0m"
23
+ end
28
24
 
29
- def niceql( colorize = true )
30
- puts Prettifier.prettify_sql( to_sql, colorize )
25
+ def colorize_comment(comment)
26
+ # bright black bold ANSI color
27
+ "\e[0;90;1;49m#{comment}\e[0m"
28
+ end
31
29
  end
32
-
33
30
  end
34
31
 
35
32
  module Prettifier
36
- INLINE_VERBS = %w(WITH ASC (IN\s) COALESCE AS WHEN THEN ELSE END AND UNION ALL ON DISTINCT INTERSECT EXCEPT EXISTS NOT COUNT ROUND CAST).join('| ')
37
- NEW_LINE_VERBS = 'SELECT|FROM|WHERE|CASE|ORDER BY|LIMIT|GROUP BY|(RIGHT |LEFT )*(INNER |OUTER )*JOIN( LATERAL)*|HAVING|OFFSET|UPDATE'
33
+ # ?= -- should be present but without being added to MatchData
34
+ AFTER_KEYWORD_SPACE = '(?=\s{1})'
35
+ JOIN_KEYWORDS = '(RIGHT\s+|LEFT\s+){0,1}(INNER\s+|OUTER\s+){0,1}JOIN(\s+LATERAL){0,1}'
36
+ INLINE_KEYWORDS = "WITH|ASC|COALESCE|AS|WHEN|THEN|ELSE|END|AND|UNION|ALL|ON|DISTINCT|"\
37
+ "INTERSECT|EXCEPT|EXISTS|NOT|COUNT|ROUND|CAST|IN"
38
+ NEW_LINE_KEYWORDS = "SELECT|FROM|WHERE|CASE|ORDER BY|LIMIT|GROUP BY|HAVING|OFFSET|UPDATE|SET|#{JOIN_KEYWORDS}"
39
+
38
40
  POSSIBLE_INLINER = /(ORDER BY|CASE)/
39
- VERBS = "#{NEW_LINE_VERBS}|#{INLINE_VERBS}"
40
- STRINGS = /("[^"]+")|('[^']+')/
41
+ KEYWORDS = "(#{NEW_LINE_KEYWORDS}|#{INLINE_KEYWORDS})#{AFTER_KEYWORD_SPACE}"
42
+ # ?: -- will not match partial enclosed by (..)
43
+ MULTILINE_INDENTABLE_LITERAL = /(?:'[^']+'\s*\n+\s*)+(?:'[^']+')+/
44
+ # STRINGS matched both kind of strings the multiline solid
45
+ # and single quoted multiline strings with \s*\n+\s* separation
46
+ STRINGS = /("[^"]+")|((?:'[^']+'\s*\n+\s*)*(?:'[^']+')+)/
41
47
  BRACKETS = '[\(\)]'
42
- SQL_COMMENTS = /(\s*?--.+\s*)|(\s*?\/\*[^\/\*]*\*\/\s*)/
43
- # only newlined comments will be matched
44
- SQL_COMMENTS_CLEARED = /(\s*?--.+\s{1})|(\s*$\s*\/\*[^\/\*]*\*\/\s{1})/
48
+ # will match all /* single line and multiline comments */ and -- based comments
49
+ # the last will be matched as single block whenever comment lines followed each other.
50
+ # For instance:
51
+ # SELECT * -- comment 1
52
+ # -- comment 2
53
+ # all comments will be matched as a single block
54
+ SQL_COMMENTS = %r{(\s*?--[^\n]+\n*)+|(\s*?/\*[^/\*]*\*/\s*)}m
45
55
  COMMENT_CONTENT = /[\S]+[\s\S]*[\S]+/
56
+ NAMED_DOLLAR_QUOTED_STRINGS_REGEX = /[^\$](\$[^\$]+\$)[^\$]/
57
+ DOLLAR_QUOTED_STRINGS = /(\$\$.*\$\$)/
46
58
 
47
59
  class << self
48
60
  def config
@@ -50,10 +62,9 @@ module Niceql
50
62
  end
51
63
 
52
64
  def prettify_err(err, original_sql_query = nil)
53
- prettify_pg_err( err.to_s, original_sql_query )
65
+ prettify_pg_err(err.to_s, original_sql_query)
54
66
  end
55
67
 
56
-
57
68
  # Postgres error output:
58
69
  # ERROR: VALUES in FROM must have an alias
59
70
  # LINE 2: FROM ( VALUES(1), (2) );
@@ -69,7 +80,7 @@ module Niceql
69
80
  # ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column "usr" does not exist
70
81
  # LINE 1: SELECT usr FROM users ORDER BY 1
71
82
  # ^
72
- #: SELECT usr FROM users ORDER BY 1
83
+ # : SELECT usr FROM users ORDER BY 1
73
84
 
74
85
  # prettify_pg_err parses ActiveRecord::StatementInvalid string,
75
86
  # but you may use it without ActiveRecord either way:
@@ -77,223 +88,381 @@ module Niceql
77
88
  # don't mess with original sql query, or prettify_pg_err will deliver incorrect results
78
89
  def prettify_pg_err(err, original_sql_query = nil)
79
90
  return err if err[/LINE \d+/].nil?
80
- err_line_num = err[/LINE \d+/][5..-1].to_i
91
+
92
+ # LINE 2: ... -> err_line_num = 2
93
+ err_line_num = err.match(/LINE (\d+):/)[1].to_i
81
94
  # LINE 1: SELECT usr FROM users ORDER BY 1
82
95
  err_address_line = err.lines[1]
83
96
 
84
- start_sql_line = 3 if err.lines.length <= 3
97
+ sql_start_line_num = 3 if err.lines.length <= 3
85
98
  # error not always contains HINT
86
- start_sql_line ||= err.lines[3][/(HINT|DETAIL)/] ? 4 : 3
87
- sql_body = start_sql_line < err.lines.length ? err.lines[start_sql_line..-1] : original_sql_query&.lines
99
+ sql_start_line_num ||= err.lines[3][/(HINT|DETAIL)/] ? 4 : 3
100
+ sql_body_lines = if sql_start_line_num < err.lines.length
101
+ err.lines[sql_start_line_num..-1]
102
+ else
103
+ original_sql_query&.lines
104
+ end
88
105
 
89
106
  # this means original query is missing so it's nothing to prettify
90
- return err unless sql_body
107
+ return err unless sql_body_lines
108
+
109
+ # this is an SQL line with an error.
110
+ # we need err_line to properly align the caret in the caret line
111
+ # and to apply a full red colorizing schema on an SQL line with error
112
+ err_line = sql_body_lines[err_line_num - 1]
113
+
114
+ # colorizing keywords, strings and error line
115
+ err_body = sql_body_lines.map do |ln|
116
+ ln == err_line ? StringColorize.colorize_err(ln) : colorize_err_line(ln)
117
+ end
118
+
119
+ err_caret_line = extract_err_caret_line(err_address_line, err_line, sql_body_lines, err)
120
+ err_body.insert(err_line_num, StringColorize.colorize_err(err_caret_line))
121
+
122
+ err.lines[0..sql_start_line_num - 1].join + err_body.join
123
+ end
124
+
125
+ def prettify_sql(sql, colorize = true)
126
+ QueryNormalizer.new(sql, colorize).prettified_sql
127
+ end
128
+
129
+ def prettify_multiple(sql_multi, colorize = true)
130
+ sql_multi.split(/(?>#{SQL_COMMENTS})|(\;)/).each_with_object([""]) do |pattern, queries|
131
+ queries[-1] += pattern
132
+ queries << "" if pattern == ";"
133
+ end.map! do |sql|
134
+ # we were splitting by comments and ';', so if next sql start with comment we've got a misplaced \n\n
135
+ sql.match?(/\A\s+\z/) ? nil : prettify_sql(sql, colorize)
136
+ end.compact.join("\n")
137
+ end
138
+
139
+ private
91
140
 
92
- # err line will be painted in red completely, so we just remembering it and use
93
- # to replace after painting the verbs
94
- err_line = sql_body[err_line_num - 1]
141
+ def colorize_err_line(line)
142
+ line.gsub(/#{KEYWORDS}/) { |keyword| StringColorize.colorize_keyword(keyword) }
143
+ .gsub(STRINGS) { |str| StringColorize.colorize_str(str) }
144
+ end
95
145
 
146
+ def extract_err_caret_line(err_address_line, err_line, sql_body, err)
147
+ # LINE could be quoted ( both sides and sometimes only from one ):
148
+ # "LINE 1: ...t_id\" = $13 AND \"products\".\"carrier_id\" = $14 AND \"product_t...\n",
149
+ err_quote = (err_address_line.match(/\.\.\.(.+)\.\.\./) || err_address_line.match(/\.\.\.(.+)/))&.send(:[], 1)
96
150
 
97
- #colorizing verbs and strings
98
- colorized_sql_body = sql_body.join.gsub(/#{VERBS}/ ) { |verb| StringColorize.colorize_verb(verb) }
99
- .gsub(STRINGS){ |str| StringColorize.colorize_str(str) }
151
+ # line[2] is original err caret line i.e.: ' ^'
152
+ # err_address_line[/LINE \d+:/].length+1..-1 - is a position from error quote begin
153
+ err_caret_line = err.lines[2][err_address_line[/LINE \d+:/].length + 1..-1]
100
154
 
101
- #reassemling error message
102
- err_body = colorized_sql_body.lines
103
- # replacing colorized line contained error and adding caret line
104
- err_body[err_line_num - 1]= StringColorize.colorize_err( err_line )
155
+ # when err line is too long postgres quotes it in double '...'
156
+ # so we need to reposition caret against original line
157
+ if err_quote
158
+ err_quote_caret_offset = err_caret_line.length - err_address_line.index("...").to_i + 3
159
+ err_caret_line = " " * (err_line.index(err_quote) + err_quote_caret_offset) + "^\n"
160
+ end
161
+
162
+ # older versions of ActiveRecord were adding ': ' before an original query :(
163
+ err_caret_line.prepend(" ") if sql_body[0].start_with?(": ")
164
+ # if mistake is on last string than err_line.last != \n then we need to prepend \n to caret line
165
+ err_caret_line.prepend("\n") unless err_line[-1] == "\n"
166
+ err_caret_line
167
+ end
168
+ end
105
169
 
106
- err_caret_line = extract_err_caret_line( err_address_line, err_line, sql_body, err )
107
- err_body.insert( err_line_num, StringColorize.colorize_err( err_caret_line ) )
170
+ # The normalizing and formatting logic:
171
+ # 1. Split the original query onto the query part + literals + comments
172
+ # a. find all potential dollar-signed separators
173
+ # b. prepare full literal extractor regex
174
+ # 2. Find and separate all literals and comments into mutable/format-able types
175
+ # and immutable ( see the typing and formatting rules below )
176
+ # 3. Replace all literals and comments with uniq ids on the original query to get the parametrized query
177
+ # 4. Format parametrized query alongside with mutable/format-able comments and literals
178
+ # a. clear space characters: replace all \s+ to \s, remove all "\n" e.t.c
179
+ # b. split in lines -> indent -> colorize
180
+ # 5. Restore literals and comments with their values
181
+ class QueryNormalizer
182
+ extend Forwardable
183
+ def_delegator :Niceql, :config
184
+
185
+ # Literals content should not be indented, only string parts separated by new lines can be indented
186
+ # indentable_string:
187
+ # UPDATE docs SET body = 'First line'
188
+ # 'Second line'
189
+ # 'Third line', ...
190
+ #
191
+ # SQL standard allow such multiline separation.
192
+
193
+ # newline_end_comments:
194
+ # SELECT * -- get all column
195
+ # SELECT * /* get all column */
196
+ #
197
+ # SELECT * -- get all column
198
+ # -- we need all columns for this request
199
+ # SELECT * /* get all column
200
+ # we need all columns for this request */
201
+ #
202
+ # rare case newline_start_comments:
203
+ # SELECT *
204
+ # /* get all column
205
+ # we need all columns for this request */ FROM table
206
+ #
207
+ # newline_wrapped_comments:
208
+ # SELECT *
209
+ # /* get all column
210
+ # we need all columns for this request */
211
+ # FROM table
212
+ #
213
+ # SELECT *
214
+ # -- get all column
215
+ # -- we need all columns for this request
216
+ # FROM ...
217
+ # Potentially we could prettify different type of comments and strings a little bit differently,
218
+ # but right now there is no difference between the
219
+ # newline_wrapped_comment, newline_start_comment, newline_end_comment, they all will be wrapped in newlines
220
+ COMMENT_AND_LITERAL_TYPES = [:immutable_string, :indentable_string, :inline_comment, :newline_wrapped_comment,
221
+ :newline_start_comment, :newline_end_comment,]
222
+
223
+ attr_reader :parametrized_sql, :initial_sql, :string_regex, :literals_and_comments_types, :colorize
224
+
225
+ def initialize(sql, colorize)
226
+ @initial_sql = sql
227
+ @colorize = colorize
228
+ @parametrized_sql = ""
229
+ @guids_to_content = {}
230
+ @literals_and_comments_types = {}
231
+ @counter = Hash.new(0)
232
+
233
+ init_strings_regex
234
+ prepare_parametrized_sql
235
+ prettify_parametrized_sql
236
+ end
108
237
 
109
- err.lines[0..start_sql_line-1].join + err_body.join
238
+ def prettified_sql
239
+ @parametrized_sql % @guids_to_content.transform_keys(&:to_sym)
110
240
  end
111
241
 
112
- def prettify_sql( sql, colorize = true )
242
+ private
243
+
244
+ def prettify_parametrized_sql
113
245
  indent = 0
114
- parentness = []
115
-
116
- sql = sql.split( SQL_COMMENTS ).each_slice(2).map{ | sql_part, comment |
117
- # remove additional formatting for sql_parts but leave comment intact
118
- [sql_part.gsub(/[\s]+/, ' '),
119
- # comment.match?(/\A\s*$/) - SQL_COMMENTS gets all comment content + all whitespaced chars around
120
- # so this sql_part.length == 0 || comment.match?(/\A\s*$/) checks does the comment starts from new line
121
- comment && ( sql_part.length == 0 || comment.match?(/\A\s*$/) ? "\n#{comment[COMMENT_CONTENT]}\n" : comment[COMMENT_CONTENT] ) ]
122
- }.flatten.join(' ')
123
-
124
- sql.gsub!(/ \n/, "\n")
125
-
126
- sql.gsub!(STRINGS){ |str| StringColorize.colorize_str(str) } if colorize
127
-
128
- first_verb = true
129
- prev_was_comment = false
130
-
131
- sql.gsub!( /(#{VERBS}|#{BRACKETS}|#{SQL_COMMENTS_CLEARED})/) do |verb|
132
- if 'SELECT' == verb
133
- indent += config.indentation_base if !config.open_bracket_is_newliner || parentness.last.nil? || parentness.last[:nested]
134
- parentness.last[:nested] = true if parentness.last
135
- add_new_line = !first_verb
136
- elsif verb == '('
137
- next_closing_bracket = Regexp.last_match.post_match.index(')')
246
+ brackets = []
247
+ first_keyword = true
248
+
249
+ parametrized_sql.gsub!(query_split_regex) do |matched_part|
250
+ if inline_piece?(matched_part)
251
+ first_keyword = false
252
+ next matched_part
253
+ end
254
+ post_match_str = Regexp.last_match.post_match
255
+
256
+ if ["SELECT", "UPDATE", "INSERT"].include?(matched_part)
257
+ indent_block = !config.open_bracket_is_newliner || brackets.last.nil? || brackets.last[:nested]
258
+ indent += config.indentation_base if indent_block
259
+ brackets.last[:nested] = true if brackets.last
260
+ add_new_line = !first_keyword
261
+ elsif matched_part == "("
262
+ next_closing_bracket = post_match_str.index(")")
138
263
  # check if brackets contains SELECT statement
139
- add_new_line = !!Regexp.last_match.post_match[0..next_closing_bracket][/SELECT/] && config.open_bracket_is_newliner
140
- parentness << { nested: add_new_line }
141
- elsif verb == ')'
264
+ add_new_line = !!post_match_str[0..next_closing_bracket][/SELECT/] && config.open_bracket_is_newliner
265
+ brackets << { nested: add_new_line }
266
+ elsif matched_part == ")"
142
267
  # this also covers case when right bracket is used without corresponding left one
143
- add_new_line = parentness.last.nil? || parentness.last[:nested]
144
- indent -= ( parentness.last.nil? ? 2 * config.indentation_base : (parentness.last[:nested] ? config.indentation_base : 0) )
268
+ add_new_line = brackets.last.nil? || brackets.last[:nested]
269
+ indent -= (brackets.last.nil? && 2 || brackets.last[:nested] && 1 || 0) * config.indentation_base
145
270
  indent = 0 if indent < 0
146
- parentness.pop
147
- elsif verb[POSSIBLE_INLINER]
271
+ brackets.pop
272
+ elsif matched_part[POSSIBLE_INLINER]
148
273
  # in postgres ORDER BY can be used in aggregation function this will keep it
149
274
  # inline with its agg function
150
- add_new_line = parentness.last.nil? || parentness.last[:nested]
275
+ add_new_line = brackets.last.nil? || brackets.last[:nested]
151
276
  else
152
- add_new_line = verb[/(#{INLINE_VERBS})/].nil?
277
+ # since we are matching KEYWORD without space on the end
278
+ # IN will be present in JOIN, DISTINCT e.t.c, so we need to exclude it explicitly
279
+ add_new_line = matched_part.match?(/(#{NEW_LINE_KEYWORDS})/)
153
280
  end
154
281
 
155
- # !add_new_line && previous_was_comment means we had newlined comment, and now even
156
- # if verb is inline verb we will need to add new line with indentation BUT all
157
- # inliners match with a space before so we need to strip it
158
- verb.lstrip! if !add_new_line && prev_was_comment
159
-
160
- add_new_line = prev_was_comment unless add_new_line
161
- add_indent = !first_verb && add_new_line
282
+ # do not indent first keyword in query, and indent everytime we started new line
283
+ add_indent_to_keyword = !first_keyword && add_new_line
162
284
 
163
- if verb[SQL_COMMENTS_CLEARED]
164
- verb = verb[COMMENT_CONTENT]
165
- prev_was_comment = true
285
+ if literals_and_comments_types[matched_part]
286
+ # this is a case when comment followed by ordinary SQL part not by any keyword
287
+ # this means that it will not be gsubed and no indent will be added before this part, while needed
288
+ last_comment_followed_by_keyword = post_match_str.match?(/\A\}\s{0,1}(?:#{KEYWORDS})/)
289
+ indent_parametrized_part(matched_part, indent, !last_comment_followed_by_keyword, !first_keyword)
290
+ matched_part
166
291
  else
167
- first_verb = false
168
- prev_was_comment = false
292
+ first_keyword = false
293
+ indented_sql = (add_indent_to_keyword ? indent_multiline(matched_part, indent) : matched_part)
294
+ add_new_line ? "\n" + indented_sql : indented_sql
169
295
  end
296
+ end
297
+
298
+ parametrized_sql.gsub!(" \n", "\n") # moved keywords could keep space before it, we can crop it anyway
170
299
 
171
- verb = StringColorize.colorize_verb(verb) if !%w[( )].include?(verb) && colorize
300
+ clear_extra_newline_after_comments
172
301
 
173
- subs = ( add_indent ? indent_multiline(verb, indent) : verb)
174
- !first_verb && add_new_line ? "\n" + subs : subs
302
+ colorize_query if colorize
303
+ end
304
+
305
+ def add_string_or_comment(string_or_comment)
306
+ # when we splitting original SQL, it could and could not end with literal/comment
307
+ # hence we could try to add nil...
308
+ return if string_or_comment.nil?
309
+
310
+ type = get_placeholder_type(string_or_comment)
311
+ # will be formatted to comment_1_guid
312
+ typed_id = new_placeholder_name(type)
313
+ @guids_to_content[typed_id] = string_or_comment
314
+ @counter[type] += 1
315
+ @literals_and_comments_types[typed_id] = type
316
+ "%{#{typed_id}}"
317
+ end
318
+
319
+ def literal_and_comments_placeholders_regex
320
+ /(#{@literals_and_comments_types.keys.join("|")})/
321
+ end
322
+
323
+ def inline_piece?(comment_or_string)
324
+ [:immutable_string, :inline_comment].include?(literals_and_comments_types[comment_or_string])
325
+ end
326
+
327
+ def prepare_parametrized_sql
328
+ @parametrized_sql = @initial_sql.split(/#{SQL_COMMENTS}|#{string_regex}/)
329
+ .each_slice(2).map do |sql_part, comment_or_string|
330
+ # remove additional formatting for sql_parts and replace comment and strings with a guids
331
+ [sql_part.gsub(/[\s]+/, " "), add_string_or_comment(comment_or_string)]
332
+ end.flatten.compact.join("")
333
+ end
334
+
335
+ def query_split_regex(with_brackets = true)
336
+ if with_brackets
337
+ /(#{KEYWORDS}|#{BRACKETS}|#{literal_and_comments_placeholders_regex})/
338
+ else
339
+ /(#{KEYWORDS}|#{literal_and_comments_placeholders_regex})/
175
340
  end
341
+ end
176
342
 
177
- # clear all spaces before newlines, and all whitespaces before strings endings
178
- sql.tap{ |slf| slf.gsub!( /\s+\n/, "\n" ) }.tap{ |slf| slf.gsub!(/\s+\z/, '') }
343
+ # when comment ending with newline followed by a keyword we should remove double newlines
344
+ def clear_extra_newline_after_comments
345
+ newlined_comments = @literals_and_comments_types.select { |k,| new_line_ending_comment?(k) }
346
+ return if newlined_comments.length == 0
347
+ parametrized_sql.gsub!(/(#{newlined_comments.keys.join("}\n|")}}\n)/, &:chop)
179
348
  end
180
349
 
181
- def prettify_multiple( sql_multi, colorize = true )
182
- sql_multi.split( /(?>#{SQL_COMMENTS})|(\;)/ ).inject(['']) { |queries, pattern|
183
- queries.last << pattern
184
- queries << '' if pattern == ';'
185
- queries
186
- }.map!{ |sql|
187
- # we were splitting by comments and ;, so if next sql start with comment we've got a misplaced \n\n
188
- sql.match?(/\A\s+\z/) ? nil : prettify_sql( sql, colorize )
189
- }.compact.join("\n\n")
350
+ def colorize_query
351
+ parametrized_sql.gsub!(query_split_regex(false)) do |matched_part|
352
+ if literals_and_comments_types[matched_part]
353
+ colorize_comment_or_literal(matched_part)
354
+ matched_part
355
+ else
356
+ StringColorize.colorize_keyword(matched_part)
357
+ end
358
+ end
190
359
  end
191
360
 
192
- private_class_method
193
- def indent_multiline( verb, indent )
194
- if verb.match?(/.\s*\n\s*./)
195
- verb.lines.map!{|ln| ln.prepend(' ' * indent)}.join("\n")
361
+ def indent_parametrized_part(matched_typed_id, indent, indent_after_comment, start_with_newline = true)
362
+ case @literals_and_comments_types[matched_typed_id]
363
+ # technically we will not get here, since this types of literals/comments are not indentable
364
+ when :inline_comment, :immutable_string
365
+ when :indentable_string
366
+ lines = @guids_to_content[matched_typed_id].lines
367
+ @guids_to_content[matched_typed_id] = lines[0] +
368
+ lines[1..-1].map! { |ln| indent_multiline(ln[/'[^']+'/], indent) }.join("\n")
196
369
  else
197
- verb.prepend(' ' * indent)
370
+ content = @guids_to_content[matched_typed_id][COMMENT_CONTENT]
371
+ @guids_to_content[matched_typed_id] = (start_with_newline ? "\n" : "") +
372
+ "#{indent_multiline(content, indent)}\n" +
373
+ (indent_after_comment ? indent_multiline("", indent) : "")
198
374
  end
199
375
  end
200
376
 
201
- private_class_method
202
- def extract_err_caret_line( err_address_line, err_line, sql_body, err )
203
- # LINE could be quoted ( both sides and sometimes only from one ):
204
- # "LINE 1: ...t_id\" = $13 AND \"products\".\"carrier_id\" = $14 AND \"product_t...\n",
205
- err_quote = (err_address_line.match(/\.\.\.(.+)\.\.\./) || err_address_line.match(/\.\.\.(.+)/) ).try(:[], 1)
377
+ def colorize_comment_or_literal(matched_typed_id)
378
+ @guids_to_content[matched_typed_id] = if comment?(@literals_and_comments_types[matched_typed_id])
379
+ StringColorize.colorize_comment(@guids_to_content[matched_typed_id])
380
+ else
381
+ StringColorize.colorize_str(@guids_to_content[matched_typed_id])
382
+ end
383
+ end
206
384
 
207
- # line[2] is original err caret line i.e.: ' ^'
208
- # err_address_line[/LINE \d+:/].length+1..-1 - is a position from error quote begin
209
- err_caret_line = err.lines[2][err_address_line[/LINE \d+:/].length+1..-1]
385
+ def get_placeholder_type(comment_or_string)
386
+ if SQL_COMMENTS.match?(comment_or_string)
387
+ get_comment_type(comment_or_string)
388
+ else
389
+ get_string_type(comment_or_string)
390
+ end
391
+ end
210
392
 
211
- # when err line is too long postgres quotes it in double '...'
212
- # so we need to reposition caret against original line
213
- if err_quote
214
- err_quote_caret_offset = err_caret_line.length - err_address_line.index( '...' ).to_i + 3
215
- err_caret_line = ' ' * ( err_line.index( err_quote ) + err_quote_caret_offset ) + "^\n"
393
+ def get_comment_type(comment)
394
+ case comment
395
+ when /\s*\n+\s*.+\s*\n+\s*/ then :newline_wrapped_comment
396
+ when /\s*\n+\s*.+/ then :newline_start_comment
397
+ when /.+\s*\n+\s*/ then :newline_end_comment
398
+ else :inline_comment
216
399
  end
400
+ end
217
401
 
218
- # older versions of ActiveRecord were adding ': ' before an original query :(
219
- err_caret_line.prepend(' ') if sql_body[0].start_with?(': ')
220
- # if mistake is on last string than err_line.last != \n then we need to prepend \n to caret line
221
- err_caret_line.prepend("\n") unless err_line[-1] == "\n"
222
- err_caret_line
402
+ def get_string_type(string)
403
+ MULTILINE_INDENTABLE_LITERAL.match?(string) ? :indentable_string : :immutable_string
223
404
  end
224
- end
225
- end
226
405
 
227
- module PostgresAdapterNiceQL
228
- def exec_query(sql, name = "SQL", binds = [], prepare: false)
229
- # replacing sql with prettified sql, thats all
230
- super( Prettifier.prettify_sql(sql, false), name, binds, prepare: prepare )
231
- end
232
- end
406
+ def new_placeholder_name(placeholder_type)
407
+ "#{placeholder_type}_#{@counter[placeholder_type]}_#{SecureRandom.uuid}"
408
+ end
233
409
 
234
- module AbstractAdapterLogPrettifier
235
- def log( sql, *args, &block )
236
- # \n need to be placed because AR log will start with action description + time info.
237
- # rescue sql - just to be sure Prettifier wouldn't break production
238
- formatted_sql = "\n" + Prettifier.prettify_sql(sql) rescue sql
239
- super( formatted_sql, *args, &block )
240
- end
241
- end
410
+ def get_sql_named_strs(sql)
411
+ freq = Hash.new(0)
412
+ sql.scan(NAMED_DOLLAR_QUOTED_STRINGS_REGEX).select do |str|
413
+ freq[str] += 1
414
+ freq[str] == 2
415
+ end
416
+ .flatten
417
+ .map { |str| str.gsub!("$", '\$') }
418
+ end
242
419
 
243
- module ErrorExt
244
- def to_s
245
- # older rails version do not provide sql as a standalone query, instead they
246
- # deliver joined message
247
- Niceql.config.prettify_pg_errors ? Prettifier.prettify_err(super, try(:sql) ) : super
248
- end
249
- end
420
+ def init_strings_regex
421
+ # /($STR$.+$STR$|$$[^$]$$|'[^']'|"[^"]")/
422
+ strs = get_sql_named_strs(initial_sql).map { |dq_str| "#{dq_str}.+#{dq_str}" }
423
+ strs = ["(#{strs.join("|")})"] if strs != []
424
+ @string_regex ||= /#{[*strs, DOLLAR_QUOTED_STRINGS, STRINGS].join("|")}/m
425
+ end
250
426
 
251
- class NiceQLConfig
252
- def ar_using_pg_adapter?
253
- return false unless defined?(::ActiveRecord::Base)
427
+ def comment?(piece_type)
428
+ !literal?(piece_type)
429
+ end
254
430
 
255
- adapter = ActiveRecord::Base.try(:connection_db_config).try(:adapter) ||
256
- ActiveRecord::Base.try(:connection_config)&.with_indifferent_access&.dig(:adapter)
431
+ def literal?(piece_type)
432
+ [:indentable_string, :immutable_string].include?(piece_type)
433
+ end
257
434
 
258
- adapter == 'postgresql'
259
- end
435
+ def new_line_ending_comment?(comment_or_literal)
436
+ [:newline_wrapped_comment, :newline_end_comment, :newline_start_comment]
437
+ .include?(@literals_and_comments_types[comment_or_literal])
438
+ end
260
439
 
261
- attr_accessor :pg_adapter_with_nicesql,
262
- :indentation_base,
263
- :open_bracket_is_newliner,
264
- :prettify_active_record_log_output,
265
- :prettify_pg_errors
440
+ def indent_multiline(keyword, indent)
441
+ if keyword.match?(/.\s*\n\s*./)
442
+ keyword.lines.map! { |ln| " " * indent + ln }.join("")
443
+ else
444
+ " " * indent + keyword
445
+ end
446
+ end
447
+ end
448
+ end
266
449
 
450
+ class NiceQLConfig
451
+ attr_accessor :indentation_base, :open_bracket_is_newliner
267
452
 
268
453
  def initialize
269
- self.pg_adapter_with_nicesql = false
270
454
  self.indentation_base = 2
271
455
  self.open_bracket_is_newliner = false
272
- self.prettify_active_record_log_output = false
273
- self.prettify_pg_errors = ar_using_pg_adapter?
274
456
  end
275
457
  end
276
458
 
277
- def self.configure
278
- yield( config )
279
-
280
- return unless defined? ::ActiveRecord::Base
281
-
282
- ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(PostgresAdapterNiceQL) if config.pg_adapter_with_nicesql
283
-
284
- ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend( AbstractAdapterLogPrettifier ) if config.prettify_active_record_log_output
285
-
286
- ::ActiveRecord::StatementInvalid.include( Niceql::ErrorExt ) if config.prettify_pg_errors && config.ar_using_pg_adapter?
287
- end
288
-
289
- def self.config
290
- @config ||= NiceQLConfig.new
291
- end
459
+ class << self
460
+ def configure
461
+ yield(config)
462
+ end
292
463
 
293
- if defined? ::ActiveRecord
294
- [::ActiveRecord::Relation,
295
- ::Arel::TreeManager,
296
- ::Arel::Nodes::Node].each { |klass| klass.send(:include, ArExtentions) }
464
+ def config
465
+ @config ||= NiceQLConfig.new
466
+ end
297
467
  end
298
-
299
468
  end