niceql 0.5.0 → 0.6.0

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