niceql 0.5.1 → 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,6 +85,7 @@ 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?
88
+
65
89
  # LINE 2: ... -> err_line_num = 2
66
90
  err_line_num = err.match(/LINE (\d+):/)[1].to_i
67
91
  # LINE 1: SELECT usr FROM users ORDER BY 1
@@ -80,130 +104,335 @@ module Niceql
80
104
  # and to apply a full red colorizing schema on an SQL line with error
81
105
  err_line = sql_body_lines[err_line_num - 1]
82
106
 
83
- #colorizing verbs, strings and error line
84
- err_body = sql_body_lines.map { |ln| ln == err_line ? StringColorize.colorize_err( ln ) : colorize_err_line(ln) }
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
111
+
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))
85
114
 
86
- err_caret_line = extract_err_caret_line( err_address_line, err_line, sql_body_lines, err )
87
- err_body.insert( err_line_num, StringColorize.colorize_err( err_caret_line ) )
115
+ err.lines[0..sql_start_line_num - 1].join + err_body.join
116
+ end
88
117
 
89
- err.lines[0..sql_start_line_num-1].join + err_body.join
118
+ def prettify_sql(sql, colorize = true)
119
+ QueryNormalizer.new(sql, colorize).prettified_sql
90
120
  end
91
121
 
92
- def prettify_sql( sql, colorize = true )
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)
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]
147
+
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
154
+
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
162
+
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
229
+
230
+ def prettified_sql
231
+ @parametrized_sql % @guids_to_content.transform_keys(&:to_sym)
232
+ end
233
+
234
+ private
235
+
236
+ def prettify_parametrized_sql
93
237
  indent = 0
94
- parentness = []
95
-
96
- sql = sql.split( SQL_COMMENTS ).each_slice(2).map{ | sql_part, comment |
97
- # remove additional formatting for sql_parts but leave comment intact
98
- [sql_part.gsub(/[\s]+/, ' '),
99
- # comment.match?(/\A\s*$/) - SQL_COMMENTS gets all comment content + all whitespaced chars around
100
- # so this sql_part.length == 0 || comment.match?(/\A\s*$/) checks does the comment starts from new line
101
- comment && ( sql_part.length == 0 || comment.match?(/\A\s*$/) ? "\n#{comment[COMMENT_CONTENT]}\n" : comment[COMMENT_CONTENT] ) ]
102
- }.flatten.join(' ')
103
-
104
- sql.gsub!(/ \n/, "\n")
105
-
106
- sql.gsub!(STRINGS){ |str| StringColorize.colorize_str(str) } if colorize
107
-
108
- first_verb = true
109
- prev_was_comment = false
110
-
111
- sql.gsub!( /(#{VERBS}|#{BRACKETS}|#{SQL_COMMENTS_CLEARED})/) do |verb|
112
- if 'SELECT' == verb
113
- indent += config.indentation_base if !config.open_bracket_is_newliner || parentness.last.nil? || parentness.last[:nested]
114
- parentness.last[:nested] = true if parentness.last
115
- add_new_line = !first_verb
116
- elsif verb == '('
117
- 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(")")
118
254
  # check if brackets contains SELECT statement
119
- add_new_line = !!Regexp.last_match.post_match[0..next_closing_bracket][/SELECT/] && config.open_bracket_is_newliner
120
- parentness << { nested: add_new_line }
121
- 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 == ")"
122
258
  # this also covers case when right bracket is used without corresponding left one
123
- add_new_line = parentness.last.nil? || parentness.last[:nested]
124
- 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
125
261
  indent = 0 if indent < 0
126
- parentness.pop
127
- elsif verb[POSSIBLE_INLINER]
262
+ brackets.pop
263
+ elsif matched_part[POSSIBLE_INLINER]
128
264
  # in postgres ORDER BY can be used in aggregation function this will keep it
129
265
  # inline with its agg function
130
- add_new_line = parentness.last.nil? || parentness.last[:nested]
266
+ add_new_line = brackets.last.nil? || brackets.last[:nested]
131
267
  else
132
- 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})/)
133
271
  end
134
272
 
135
- # !add_new_line && previous_was_comment means we had newlined comment, and now even
136
- # if verb is inline verb we will need to add new line with indentation BUT all
137
- # inliners match with a space before so we need to strip it
138
- verb.lstrip! if !add_new_line && prev_was_comment
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
139
275
 
140
- add_new_line = prev_was_comment unless add_new_line
141
- add_indent = !first_verb && add_new_line
142
-
143
- if verb[SQL_COMMENTS_CLEARED]
144
- verb = verb[COMMENT_CONTENT]
145
- 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
146
282
  else
147
- first_verb = false
148
- 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
149
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
150
295
 
151
- verb = StringColorize.colorize_verb(verb) if !%w[( )].include?(verb) && colorize
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
309
+
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
317
+
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
152
325
 
153
- subs = ( add_indent ? indent_multiline(verb, indent) : verb)
154
- !first_verb && add_new_line ? "\n" + subs : subs
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})/
155
331
  end
332
+ end
156
333
 
157
- # clear all spaces before newlines, and all whitespaces before strings endings
158
- 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)
159
338
  end
160
339
 
161
- def prettify_multiple( sql_multi, colorize = true )
162
- sql_multi.split( /(?>#{SQL_COMMENTS})|(\;)/ ).inject(['']) { |queries, pattern|
163
- queries.last << pattern
164
- queries << '' if pattern == ';'
165
- queries
166
- }.map!{ |sql|
167
- # we were splitting by comments and ;, so if next sql start with comment we've got a misplaced \n\n
168
- sql.match?(/\A\s+\z/) ? nil : prettify_sql( sql, colorize )
169
- }.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
170
349
  end
171
350
 
172
- private_class_method
173
- def indent_multiline( verb, indent )
174
- if verb.match?(/.\s*\n\s*./)
175
- 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")
176
359
  else
177
- 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) : "")
178
364
  end
179
365
  end
180
366
 
181
- def colorize_err_line( line )
182
- line.gsub(/#{VERBS}/ ) { |verb| StringColorize.colorize_verb(verb) }
183
- .gsub(STRINGS) { |str| StringColorize.colorize_str(str) }
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
184
373
  end
185
374
 
186
- def extract_err_caret_line( err_address_line, err_line, sql_body, err )
187
- # LINE could be quoted ( both sides and sometimes only from one ):
188
- # "LINE 1: ...t_id\" = $13 AND \"products\".\"carrier_id\" = $14 AND \"product_t...\n",
189
- err_quote = (err_address_line.match(/\.\.\.(.+)\.\.\./) || err_address_line.match(/\.\.\.(.+)/) )&.send(:[], 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
190
382
 
191
- # line[2] is original err caret line i.e.: ' ^'
192
- # err_address_line[/LINE \d+:/].length+1..-1 - is a position from error quote begin
193
- err_caret_line = err.lines[2][err_address_line[/LINE \d+:/].length+1..-1]
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
194
391
 
195
- # when err line is too long postgres quotes it in double '...'
196
- # so we need to reposition caret against original line
197
- if err_quote
198
- err_quote_caret_offset = err_caret_line.length - err_address_line.index( '...' ).to_i + 3
199
- err_caret_line = ' ' * ( err_line.index( err_quote ) + err_quote_caret_offset ) + "^\n"
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
200
405
  end
406
+ .flatten
407
+ .map { |str| str.gsub!("$", '\$') }
408
+ end
201
409
 
202
- # older versions of ActiveRecord were adding ': ' before an original query :(
203
- err_caret_line.prepend(' ') if sql_body[0].start_with?(': ')
204
- # if mistake is on last string than err_line.last != \n then we need to prepend \n to caret line
205
- err_caret_line.prepend("\n") unless err_line[-1] == "\n"
206
- 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
207
436
  end
208
437
  end
209
438
  end
@@ -217,8 +446,11 @@ module Niceql
217
446
  end
218
447
  end
219
448
 
220
- def self.configure; yield( config ) end
221
-
222
- def self.config; @config ||= NiceQLConfig.new end
449
+ def self.configure
450
+ yield(config)
451
+ end
223
452
 
453
+ def self.config
454
+ @config ||= NiceQLConfig.new
455
+ end
224
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