niceql 0.1.25 → 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,285 +1,468 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "niceql/version"
2
- require 'niceql/string'
4
+ require "securerandom"
5
+ require "forwardable"
3
6
 
4
7
  module Niceql
5
-
6
8
  module StringColorize
7
- def self.colorize_verb( str)
8
- #yellow ANSI color
9
- "\e[0;33;49m#{str}\e[0m"
10
- end
11
- def self.colorize_str(str)
12
- #cyan ANSI color
13
- "\e[0;36;49m#{str}\e[0m"
14
- end
15
- def self.colorize_err(err)
16
- #red ANSI color
17
- "\e[0;31;49m#{err}\e[0m"
18
- end
19
- end
9
+ class << self
10
+ def colorize_keyword(str)
11
+ # yellow ANSI color
12
+ "\e[0;33;49m#{str}\e[0m"
13
+ end
20
14
 
21
- module ArExtentions
22
- def exec_niceql
23
- connection.execute( to_niceql )
24
- end
15
+ def colorize_str(str)
16
+ # cyan ANSI color
17
+ "\e[0;36;49m#{str}\e[0m"
18
+ end
25
19
 
26
- def to_niceql
27
- Prettifier.prettify_sql(to_sql, false)
28
- end
20
+ def colorize_err(err)
21
+ # red ANSI color
22
+ "\e[0;31;49m#{err}\e[0m"
23
+ end
29
24
 
30
- def niceql( colorize = true )
31
- 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
32
29
  end
33
-
34
30
  end
35
31
 
36
32
  module Prettifier
37
- 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('| ')
38
- 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
+
39
40
  POSSIBLE_INLINER = /(ORDER BY|CASE)/
40
- VERBS = "#{NEW_LINE_VERBS}|#{INLINE_VERBS}"
41
- 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*)*(?:'[^']+')+)/
42
47
  BRACKETS = '[\(\)]'
43
- SQL_COMMENTS = /(\s*?--.+\s*)|(\s*?\/\*[^\/\*]*\*\/\s*)/
44
- # only newlined comments will be matched
45
- 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
46
55
  COMMENT_CONTENT = /[\S]+[\s\S]*[\S]+/
56
+ NAMED_DOLLAR_QUOTED_STRINGS_REGEX = /[^\$](\$[^\$]+\$)[^\$]/
57
+ DOLLAR_QUOTED_STRINGS = /(\$\$.*\$\$)/
47
58
 
48
- def self.config
49
- Niceql.config
50
- end
59
+ class << self
60
+ def config
61
+ Niceql.config
62
+ end
51
63
 
64
+ def prettify_err(err, original_sql_query = nil)
65
+ prettify_pg_err(err.to_s, original_sql_query)
66
+ end
52
67
 
53
- def self.prettify_err(err)
54
- prettify_pg_err( err.to_s )
55
- end
68
+ # Postgres error output:
69
+ # ERROR: VALUES in FROM must have an alias
70
+ # LINE 2: FROM ( VALUES(1), (2) );
71
+ # ^
72
+ # HINT: For example, FROM (VALUES ...) [AS] foo.
73
+
74
+ # May go without HINT or DETAIL:
75
+ # ERROR: column "usr" does not exist
76
+ # LINE 1: SELECT usr FROM users ORDER BY 1
77
+ # ^
78
+
79
+ # ActiveRecord::StatementInvalid will add original SQL query to the bottom like this:
80
+ # ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column "usr" does not exist
81
+ # LINE 1: SELECT usr FROM users ORDER BY 1
82
+ # ^
83
+ # : SELECT usr FROM users ORDER BY 1
84
+
85
+ # prettify_pg_err parses ActiveRecord::StatementInvalid string,
86
+ # but you may use it without ActiveRecord either way:
87
+ # prettify_pg_err( err + "\n" + sql ) OR prettify_pg_err( err, sql )
88
+ # don't mess with original sql query, or prettify_pg_err will deliver incorrect results
89
+ def prettify_pg_err(err, original_sql_query = nil)
90
+ return err if err[/LINE \d+/].nil?
91
+
92
+ # LINE 2: ... -> err_line_num = 2
93
+ err_line_num = err.match(/LINE (\d+):/)[1].to_i
94
+ # LINE 1: SELECT usr FROM users ORDER BY 1
95
+ err_address_line = err.lines[1]
96
+
97
+ sql_start_line_num = 3 if err.lines.length <= 3
98
+ # error not always contains HINT
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
105
+
106
+ # this means original query is missing so it's nothing to prettify
107
+ return err unless sql_body_lines
56
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]
57
113
 
58
- # Postgres error output:
59
- # ERROR: VALUES in FROM must have an alias
60
- # LINE 2: FROM ( VALUES(1), (2) );
61
- # ^
62
- # HINT: For example, FROM (VALUES ...) [AS] foo.
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
63
118
 
64
- # May go without HINT or DETAIL:
65
- # ERROR: column "usr" does not exist
66
- # LINE 1: SELECT usr FROM users ORDER BY 1
67
- # ^
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))
68
121
 
69
- # ActiveRecord::StatementInvalid will add original SQL query to the bottom like this:
70
- # ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column "usr" does not exist
71
- # LINE 1: SELECT usr FROM users ORDER BY 1
72
- # ^
73
- #: SELECT usr FROM users ORDER BY 1
122
+ err.lines[0..sql_start_line_num - 1].join + err_body.join
123
+ end
74
124
 
75
- # prettify_pg_err parses ActiveRecord::StatementInvalid string,
76
- # but you may use it without ActiveRecord either way:
77
- # prettify_pg_err( err + "\n" + sql ) OR prettify_pg_err( err, sql )
78
- # don't mess with original sql query, or prettify_pg_err will deliver incorrect results
79
- def self.prettify_pg_err(err, original_sql_query = nil)
80
- return err if err[/LINE \d+/].nil?
81
- err_line_num = err[/LINE \d+/][5..-1].to_i
125
+ def prettify_sql(sql, colorize = true)
126
+ QueryNormalizer.new(sql, colorize).prettified_sql
127
+ end
82
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
140
+
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
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)
150
+
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]
154
+
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
169
+
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', ...
83
190
  #
84
- start_sql_line = err.lines[3][/(HINT|DETAIL)/] ? 4 : 3
85
- err_body = start_sql_line < err.lines.length ? err.lines[start_sql_line..-1] : original_sql_query&.lines
191
+ # SQL standard allow such multiline separation.
86
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
87
237
 
88
- # this means original query is missing so it's nothing to prettify
89
- return err unless err_body
238
+ def prettified_sql
239
+ @parametrized_sql % @guids_to_content.transform_keys(&:to_sym)
240
+ end
90
241
 
91
- err_quote = ( err.lines[1][/\.\.\..+\.\.\./] && err.lines[1][/\.\.\..+\.\.\./][3..-4] ) ||
92
- ( err.lines[1][/\.\.\..+/] && err.lines[1][/\.\.\..+/][3..-1] )
242
+ private
243
+
244
+ def prettify_parametrized_sql
245
+ indent = 0
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(")")
263
+ # check if brackets contains SELECT statement
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 == ")"
267
+ # this also covers case when right bracket is used without corresponding left one
268
+ add_new_line = brackets.last.nil? || brackets.last[:nested]
269
+ indent -= (brackets.last.nil? && 2 || brackets.last[:nested] && 1 || 0) * config.indentation_base
270
+ indent = 0 if indent < 0
271
+ brackets.pop
272
+ elsif matched_part[POSSIBLE_INLINER]
273
+ # in postgres ORDER BY can be used in aggregation function this will keep it
274
+ # inline with its agg function
275
+ add_new_line = brackets.last.nil? || brackets.last[:nested]
276
+ else
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})/)
280
+ end
281
+
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
284
+
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
291
+ else
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
295
+ end
296
+ end
297
+
298
+ parametrized_sql.gsub!(" \n", "\n") # moved keywords could keep space before it, we can crop it anyway
93
299
 
94
- # line[2] is err carret line i.e.: ' ^'
95
- # err.lines[1][/LINE \d+:/].length+1..-1 - is a position from error quote begin
96
- err_carret_line = err.lines[2][err.lines[1][/LINE \d+:/].length+1..-1]
97
- # err line will be painted in red completely, so we just remembering it and use
98
- # to replace after paiting the verbs
99
- err_line = err_body[err_line_num-1]
300
+ clear_extra_newline_after_comments
100
301
 
101
- # when err line is too long postgres quotes it part in double '...'
102
- if err_quote
103
- err_quote_carret_offset = err_carret_line.length - err.lines[1].index( '...' ) + 3
104
- err_carret_line = ' ' * ( err_line.index( err_quote ) + err_quote_carret_offset ) + "^\n"
302
+ colorize_query if colorize
105
303
  end
106
304
 
107
- err_carret_line = " " + err_carret_line if err_body[0].start_with?(': ')
108
- # if mistake is on last string than err_line.last != \n so we need to prepend \n to carret line
109
- err_carret_line = "\n" + err_carret_line unless err_line[-1] == "\n"
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
110
318
 
111
- #colorizing verbs and strings
112
- err_body = err_body.join.gsub(/#{VERBS}/ ) { |verb| StringColorize.colorize_verb(verb) }
113
- .gsub(STRINGS){ |str| StringColorize.colorize_str(str) }
319
+ def literal_and_comments_placeholders_regex
320
+ /(#{@literals_and_comments_types.keys.join("|")})/
321
+ end
114
322
 
115
- #reassemling error message
116
- err_body = err_body.lines
117
- err_body[err_line_num-1]= StringColorize.colorize_err( err_line )
118
- err_body.insert( err_line_num, StringColorize.colorize_err( err_carret_line ) )
323
+ def inline_piece?(comment_or_string)
324
+ [:immutable_string, :inline_comment].include?(literals_and_comments_types[comment_or_string])
325
+ end
119
326
 
120
- err.lines[0..start_sql_line-1].join + err_body.join
121
- end
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
122
334
 
123
- def self.prettify_sql( sql, colorize = true )
124
- indent = 0
125
- parentness = []
126
-
127
- sql = sql.split( SQL_COMMENTS ).each_slice(2).map{ | sql_part, comment |
128
- # remove additional formatting for sql_parts but leave comment intact
129
- [sql_part.gsub(/[\s]+/, ' '),
130
- # comment.match?(/\A\s*$/) - SQL_COMMENTS gets all comment content + all whitespaced chars around
131
- # so this sql_part.length == 0 || comment.match?(/\A\s*$/) checks does the comment starts from new line
132
- comment && ( sql_part.length == 0 || comment.match?(/\A\s*$/) ? "\n#{comment[COMMENT_CONTENT]}\n" : comment[COMMENT_CONTENT] ) ]
133
- }.flatten.join(' ')
134
-
135
- sql.gsub!(/ \n/, "\n")
136
-
137
- sql.gsub!(STRINGS){ |str| StringColorize.colorize_str(str) } if colorize
138
-
139
- first_verb = true
140
- prev_was_comment = false
141
-
142
- sql.gsub!( /(#{VERBS}|#{BRACKETS}|#{SQL_COMMENTS_CLEARED})/) do |verb|
143
- if 'SELECT' == verb
144
- indent += config.indentation_base if !config.open_bracket_is_newliner || parentness.last.nil? || parentness.last[:nested]
145
- parentness.last[:nested] = true if parentness.last
146
- add_new_line = !first_verb
147
- elsif verb == '('
148
- next_closing_bracket = Regexp.last_match.post_match.index(')')
149
- # check if brackets contains SELECT statement
150
- add_new_line = !!Regexp.last_match.post_match[0..next_closing_bracket][/SELECT/] && config.open_bracket_is_newliner
151
- parentness << { nested: add_new_line }
152
- elsif verb == ')'
153
- # this also covers case when right bracket is used without corresponding left one
154
- add_new_line = parentness.last.nil? || parentness.last[:nested]
155
- indent -= ( parentness.last.nil? ? 2 * config.indentation_base : (parentness.last[:nested] ? config.indentation_base : 0) )
156
- indent = 0 if indent < 0
157
- parentness.pop
158
- elsif verb[POSSIBLE_INLINER]
159
- # in postgres ORDER BY can be used in aggregation function this will keep it
160
- # inline with its agg function
161
- add_new_line = parentness.last.nil? || parentness.last[:nested]
335
+ def query_split_regex(with_brackets = true)
336
+ if with_brackets
337
+ /(#{KEYWORDS}|#{BRACKETS}|#{literal_and_comments_placeholders_regex})/
162
338
  else
163
- add_new_line = verb[/(#{INLINE_VERBS})/].nil?
339
+ /(#{KEYWORDS}|#{literal_and_comments_placeholders_regex})/
164
340
  end
341
+ end
165
342
 
166
- # !add_new_line && previous_was_comment means we had newlined comment, and now even
167
- # if verb is inline verb we will need to add new line with indentation BUT all
168
- # inliners match with a space before so we need to strip it
169
- verb.lstrip! if !add_new_line && prev_was_comment
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)
348
+ end
170
349
 
171
- add_new_line = prev_was_comment unless add_new_line
172
- add_indent = !first_verb && add_new_line
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
359
+ end
173
360
 
174
- if verb[SQL_COMMENTS_CLEARED]
175
- verb = verb[COMMENT_CONTENT]
176
- prev_was_comment = true
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")
177
369
  else
178
- first_verb = false
179
- prev_was_comment = false
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) : "")
180
374
  end
375
+ end
181
376
 
182
- verb = StringColorize.colorize_verb(verb) if !['(', ')'].include?(verb) && colorize
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
183
384
 
184
- subs = ( add_indent ? indent_multiline(verb, indent) : verb)
185
- !first_verb && add_new_line ? "\n" + subs : subs
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
186
391
  end
187
392
 
188
- # clear all spaces before newlines, and all whitespaces before string end
189
- sql.tap{ |slf| slf.gsub!( /\s+\n/, "\n" ) }.tap{ |slf| slf.gsub!(/\s+\z/, '') }
190
- end
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
399
+ end
400
+ end
191
401
 
192
- def self.prettify_multiple( sql_multi, colorize = true )
193
- sql_multi.split( /(?>#{SQL_COMMENTS})|(\;)/ ).inject(['']) { |queries, pattern|
194
- queries.last << pattern
195
- queries << '' if pattern == ';'
196
- queries
197
- }.map!{ |sql|
198
- # we were splitting by comments and ;, so if next sql start with comment we've got a misplaced \n\n
199
- sql.match?(/\A\s+\z/) ? nil : prettify_sql( sql, colorize )
200
- }.compact.join("\n\n")
201
- end
402
+ def get_string_type(string)
403
+ MULTILINE_INDENTABLE_LITERAL.match?(string) ? :indentable_string : :immutable_string
404
+ end
202
405
 
203
- private
204
- def self.indent_multiline( verb, indent )
205
- #
206
- if verb.match?(/.\s*\n\s*./)
207
- verb.lines.map!{|ln| "#{' ' * indent}" + ln}.join("\n")
208
- else
209
- "#{' ' * indent}" + verb.to_s
406
+ def new_placeholder_name(placeholder_type)
407
+ "#{placeholder_type}_#{@counter[placeholder_type]}_#{SecureRandom.uuid}"
210
408
  end
211
- end
212
- end
213
409
 
214
- module PostgresAdapterNiceQL
215
- def exec_query(sql, name = "SQL", binds = [], prepare: false)
216
- # replacing sql with prettified sql, thats all
217
- super( Prettifier.prettify_sql(sql, false), name, binds, prepare: prepare )
218
- end
219
- 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
220
419
 
221
- module AbstractAdapterLogPrettifier
222
- def log( sql, *args, &block )
223
- # \n need to be placed because AR log will start with action description + time info.
224
- # rescue sql - just to be sure Prettifier wouldn't break production
225
- formatted_sql = "\n" + Prettifier.prettify_sql(sql) rescue sql
226
- super( formatted_sql, *args, &block )
227
- end
228
- 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
426
+
427
+ def comment?(piece_type)
428
+ !literal?(piece_type)
429
+ end
430
+
431
+ def literal?(piece_type)
432
+ [:indentable_string, :immutable_string].include?(piece_type)
433
+ end
434
+
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
229
439
 
230
- module ErrorExt
231
- def to_s
232
- if Niceql.config.prettify_pg_errors && ActiveRecord::Base.connection_config['adapter'] == 'postgresql'
233
- Prettifier.prettify_err(super)
234
- else
235
- super
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
236
446
  end
237
447
  end
238
448
  end
239
449
 
240
450
  class NiceQLConfig
241
- attr_accessor :pg_adapter_with_nicesql,
242
- :indentation_base,
243
- :open_bracket_is_newliner,
244
- :prettify_active_record_log_output,
245
- :prettify_pg_errors
246
-
451
+ attr_accessor :indentation_base, :open_bracket_is_newliner
247
452
 
248
453
  def initialize
249
- self.pg_adapter_with_nicesql = false
250
454
  self.indentation_base = 2
251
455
  self.open_bracket_is_newliner = false
252
- self.prettify_active_record_log_output = false
253
- self.prettify_pg_errors = defined? ::ActiveRecord::Base && ActiveRecord::Base.connection_config['adapter'] == 'postgresql'
254
456
  end
255
457
  end
256
458
 
257
-
258
- def self.configure
259
- yield( config )
260
-
261
- if config.pg_adapter_with_nicesql
262
- ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(PostgresAdapterNiceQL)
263
- end
264
-
265
- if config.prettify_active_record_log_output
266
- ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend( AbstractAdapterLogPrettifier )
459
+ class << self
460
+ def configure
461
+ yield(config)
267
462
  end
268
463
 
269
- if config.prettify_pg_errors
270
- ::ActiveRecord::StatementInvalid.include( Niceql::ErrorExt )
464
+ def config
465
+ @config ||= NiceQLConfig.new
271
466
  end
272
467
  end
273
-
274
- def self.config
275
- @config ||= NiceQLConfig.new
276
- end
277
-
278
- if defined? ::ActiveRecord::Base
279
- ::ActiveRecord::Base.extend ArExtentions
280
- [::ActiveRecord::Relation, ::ActiveRecord::Associations::CollectionProxy].each { |klass| klass.send(:include, ArExtentions) }
281
- end
282
-
283
468
  end
284
-
285
-