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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +23 -0
- data/.github/workflows/rubocop.yml +17 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +46 -0
- data/Gemfile +3 -1
- data/README.md +24 -16
- data/Rakefile +3 -1
- data/bin/console +1 -0
- data/lib/generators/niceql/install_generator.rb +4 -2
- data/lib/generators/templates/niceql_initializer.rb +3 -1
- data/lib/niceql/version.rb +3 -1
- data/lib/niceql.rb +399 -216
- data/niceql.gemspec +20 -10
- metadata +66 -24
- data/.travis.yml +0 -7
- data/lib/benchmark/cat.rb +0 -34
- data/lib/benchmark/gsub.rb +0 -34
- data/lib/benchmark/txt +0 -748
- data/lib/niceql/string.rb +0 -5
data/lib/niceql.rb
CHANGED
@@ -1,285 +1,468 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "niceql/version"
|
2
|
-
require
|
4
|
+
require "securerandom"
|
5
|
+
require "forwardable"
|
3
6
|
|
4
7
|
module Niceql
|
5
|
-
|
6
8
|
module StringColorize
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
15
|
+
def colorize_str(str)
|
16
|
+
# cyan ANSI color
|
17
|
+
"\e[0;36;49m#{str}\e[0m"
|
18
|
+
end
|
25
19
|
|
26
|
-
|
27
|
-
|
28
|
-
|
20
|
+
def colorize_err(err)
|
21
|
+
# red ANSI color
|
22
|
+
"\e[0;31;49m#{err}\e[0m"
|
23
|
+
end
|
29
24
|
|
30
|
-
|
31
|
-
|
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
|
-
|
38
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
44
|
-
#
|
45
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
65
|
-
|
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
|
-
|
70
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
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
|
-
|
89
|
-
|
238
|
+
def prettified_sql
|
239
|
+
@parametrized_sql % @guids_to_content.transform_keys(&:to_sym)
|
240
|
+
end
|
90
241
|
|
91
|
-
|
92
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
319
|
+
def literal_and_comments_placeholders_regex
|
320
|
+
/(#{@literals_and_comments_types.keys.join("|")})/
|
321
|
+
end
|
114
322
|
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
121
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
339
|
+
/(#{KEYWORDS}|#{literal_and_comments_placeholders_regex})/
|
164
340
|
end
|
341
|
+
end
|
165
342
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
172
|
-
|
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
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
179
|
-
|
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
|
-
|
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
|
-
|
185
|
-
|
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
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
204
|
-
|
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
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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 :
|
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
|
-
|
259
|
-
|
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
|
-
|
270
|
-
|
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
|
-
|