niceql 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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