niceql 0.4.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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