niceql 0.1.25 → 0.6.1

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