niceql 0.4.1 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 +21 -0
- data/Gemfile +3 -1
- data/README.md +6 -3
- 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 +362 -193
- data/niceql.gemspec +19 -12
- metadata +32 -17
- 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.rb
CHANGED
@@ -1,48 +1,60 @@
|
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
# cyan ANSI color
|
12
|
-
"\e[0;36;49m#{str}\e[0m"
|
13
|
-
end
|
14
|
-
def self.colorize_err(err)
|
15
|
-
# red ANSI color
|
16
|
-
"\e[0;31;49m#{err}\e[0m"
|
17
|
-
end
|
18
|
-
end
|
9
|
+
class << self
|
10
|
+
def colorize_keyword(str)
|
11
|
+
# yellow ANSI color
|
12
|
+
"\e[0;33;49m#{str}\e[0m"
|
13
|
+
end
|
19
14
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
15
|
+
def colorize_str(str)
|
16
|
+
# cyan ANSI color
|
17
|
+
"\e[0;36;49m#{str}\e[0m"
|
18
|
+
end
|
24
19
|
|
25
|
-
|
26
|
-
|
27
|
-
|
20
|
+
def colorize_err(err)
|
21
|
+
# red ANSI color
|
22
|
+
"\e[0;31;49m#{err}\e[0m"
|
23
|
+
end
|
28
24
|
|
29
|
-
|
30
|
-
|
25
|
+
def colorize_comment(comment)
|
26
|
+
# bright black bold ANSI color
|
27
|
+
"\e[0;90;1;49m#{comment}\e[0m"
|
28
|
+
end
|
31
29
|
end
|
32
|
-
|
33
30
|
end
|
34
31
|
|
35
32
|
module Prettifier
|
36
|
-
|
37
|
-
|
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
|
+
|
38
40
|
POSSIBLE_INLINER = /(ORDER BY|CASE)/
|
39
|
-
|
40
|
-
|
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*)*(?:'[^']+')+)/
|
41
47
|
BRACKETS = '[\(\)]'
|
42
|
-
|
43
|
-
#
|
44
|
-
|
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
|
45
55
|
COMMENT_CONTENT = /[\S]+[\s\S]*[\S]+/
|
56
|
+
NAMED_DOLLAR_QUOTED_STRINGS_REGEX = /[^\$](\$[^\$]+\$)[^\$]/
|
57
|
+
DOLLAR_QUOTED_STRINGS = /(\$\$.*\$\$)/
|
46
58
|
|
47
59
|
class << self
|
48
60
|
def config
|
@@ -50,10 +62,9 @@ module Niceql
|
|
50
62
|
end
|
51
63
|
|
52
64
|
def prettify_err(err, original_sql_query = nil)
|
53
|
-
prettify_pg_err(
|
65
|
+
prettify_pg_err(err.to_s, original_sql_query)
|
54
66
|
end
|
55
67
|
|
56
|
-
|
57
68
|
# Postgres error output:
|
58
69
|
# ERROR: VALUES in FROM must have an alias
|
59
70
|
# LINE 2: FROM ( VALUES(1), (2) );
|
@@ -69,7 +80,7 @@ module Niceql
|
|
69
80
|
# ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column "usr" does not exist
|
70
81
|
# LINE 1: SELECT usr FROM users ORDER BY 1
|
71
82
|
# ^
|
72
|
-
|
83
|
+
# : SELECT usr FROM users ORDER BY 1
|
73
84
|
|
74
85
|
# prettify_pg_err parses ActiveRecord::StatementInvalid string,
|
75
86
|
# but you may use it without ActiveRecord either way:
|
@@ -77,223 +88,381 @@ module Niceql
|
|
77
88
|
# don't mess with original sql query, or prettify_pg_err will deliver incorrect results
|
78
89
|
def prettify_pg_err(err, original_sql_query = nil)
|
79
90
|
return err if err[/LINE \d+/].nil?
|
80
|
-
|
91
|
+
|
92
|
+
# LINE 2: ... -> err_line_num = 2
|
93
|
+
err_line_num = err.match(/LINE (\d+):/)[1].to_i
|
81
94
|
# LINE 1: SELECT usr FROM users ORDER BY 1
|
82
95
|
err_address_line = err.lines[1]
|
83
96
|
|
84
|
-
|
97
|
+
sql_start_line_num = 3 if err.lines.length <= 3
|
85
98
|
# error not always contains HINT
|
86
|
-
|
87
|
-
|
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
|
88
105
|
|
89
106
|
# this means original query is missing so it's nothing to prettify
|
90
|
-
return err unless
|
107
|
+
return err unless sql_body_lines
|
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]
|
113
|
+
|
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
|
118
|
+
|
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))
|
121
|
+
|
122
|
+
err.lines[0..sql_start_line_num - 1].join + err_body.join
|
123
|
+
end
|
124
|
+
|
125
|
+
def prettify_sql(sql, colorize = true)
|
126
|
+
QueryNormalizer.new(sql, colorize).prettified_sql
|
127
|
+
end
|
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
|
91
140
|
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
95
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)
|
96
150
|
|
97
|
-
#
|
98
|
-
|
99
|
-
|
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]
|
100
154
|
|
101
|
-
#
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
105
169
|
|
106
|
-
|
107
|
-
|
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', ...
|
190
|
+
#
|
191
|
+
# SQL standard allow such multiline separation.
|
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
|
108
237
|
|
109
|
-
|
238
|
+
def prettified_sql
|
239
|
+
@parametrized_sql % @guids_to_content.transform_keys(&:to_sym)
|
110
240
|
end
|
111
241
|
|
112
|
-
|
242
|
+
private
|
243
|
+
|
244
|
+
def prettify_parametrized_sql
|
113
245
|
indent = 0
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
sql.gsub!( /(#{VERBS}|#{BRACKETS}|#{SQL_COMMENTS_CLEARED})/) do |verb|
|
132
|
-
if 'SELECT' == verb
|
133
|
-
indent += config.indentation_base if !config.open_bracket_is_newliner || parentness.last.nil? || parentness.last[:nested]
|
134
|
-
parentness.last[:nested] = true if parentness.last
|
135
|
-
add_new_line = !first_verb
|
136
|
-
elsif verb == '('
|
137
|
-
next_closing_bracket = Regexp.last_match.post_match.index(')')
|
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(")")
|
138
263
|
# check if brackets contains SELECT statement
|
139
|
-
add_new_line = !!
|
140
|
-
|
141
|
-
elsif
|
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 == ")"
|
142
267
|
# this also covers case when right bracket is used without corresponding left one
|
143
|
-
add_new_line =
|
144
|
-
indent -= (
|
268
|
+
add_new_line = brackets.last.nil? || brackets.last[:nested]
|
269
|
+
indent -= (brackets.last.nil? && 2 || brackets.last[:nested] && 1 || 0) * config.indentation_base
|
145
270
|
indent = 0 if indent < 0
|
146
|
-
|
147
|
-
elsif
|
271
|
+
brackets.pop
|
272
|
+
elsif matched_part[POSSIBLE_INLINER]
|
148
273
|
# in postgres ORDER BY can be used in aggregation function this will keep it
|
149
274
|
# inline with its agg function
|
150
|
-
add_new_line =
|
275
|
+
add_new_line = brackets.last.nil? || brackets.last[:nested]
|
151
276
|
else
|
152
|
-
|
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})/)
|
153
280
|
end
|
154
281
|
|
155
|
-
#
|
156
|
-
|
157
|
-
# inliners match with a space before so we need to strip it
|
158
|
-
verb.lstrip! if !add_new_line && prev_was_comment
|
159
|
-
|
160
|
-
add_new_line = prev_was_comment unless add_new_line
|
161
|
-
add_indent = !first_verb && add_new_line
|
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
|
162
284
|
|
163
|
-
if
|
164
|
-
|
165
|
-
|
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
|
166
291
|
else
|
167
|
-
|
168
|
-
|
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
|
169
295
|
end
|
296
|
+
end
|
297
|
+
|
298
|
+
parametrized_sql.gsub!(" \n", "\n") # moved keywords could keep space before it, we can crop it anyway
|
170
299
|
|
171
|
-
|
300
|
+
clear_extra_newline_after_comments
|
172
301
|
|
173
|
-
|
174
|
-
|
302
|
+
colorize_query if colorize
|
303
|
+
end
|
304
|
+
|
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
|
318
|
+
|
319
|
+
def literal_and_comments_placeholders_regex
|
320
|
+
/(#{@literals_and_comments_types.keys.join("|")})/
|
321
|
+
end
|
322
|
+
|
323
|
+
def inline_piece?(comment_or_string)
|
324
|
+
[:immutable_string, :inline_comment].include?(literals_and_comments_types[comment_or_string])
|
325
|
+
end
|
326
|
+
|
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
|
334
|
+
|
335
|
+
def query_split_regex(with_brackets = true)
|
336
|
+
if with_brackets
|
337
|
+
/(#{KEYWORDS}|#{BRACKETS}|#{literal_and_comments_placeholders_regex})/
|
338
|
+
else
|
339
|
+
/(#{KEYWORDS}|#{literal_and_comments_placeholders_regex})/
|
175
340
|
end
|
341
|
+
end
|
176
342
|
|
177
|
-
|
178
|
-
|
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)
|
179
348
|
end
|
180
349
|
|
181
|
-
def
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
190
359
|
end
|
191
360
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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")
|
196
369
|
else
|
197
|
-
|
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) : "")
|
198
374
|
end
|
199
375
|
end
|
200
376
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
206
384
|
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
391
|
+
end
|
210
392
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
216
399
|
end
|
400
|
+
end
|
217
401
|
|
218
|
-
|
219
|
-
|
220
|
-
# if mistake is on last string than err_line.last != \n then we need to prepend \n to caret line
|
221
|
-
err_caret_line.prepend("\n") unless err_line[-1] == "\n"
|
222
|
-
err_caret_line
|
402
|
+
def get_string_type(string)
|
403
|
+
MULTILINE_INDENTABLE_LITERAL.match?(string) ? :indentable_string : :immutable_string
|
223
404
|
end
|
224
|
-
end
|
225
|
-
end
|
226
405
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
super( Prettifier.prettify_sql(sql, false), name, binds, prepare: prepare )
|
231
|
-
end
|
232
|
-
end
|
406
|
+
def new_placeholder_name(placeholder_type)
|
407
|
+
"#{placeholder_type}_#{@counter[placeholder_type]}_#{SecureRandom.uuid}"
|
408
|
+
end
|
233
409
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
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
|
242
419
|
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
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
|
250
426
|
|
251
|
-
|
252
|
-
|
253
|
-
|
427
|
+
def comment?(piece_type)
|
428
|
+
!literal?(piece_type)
|
429
|
+
end
|
254
430
|
|
255
|
-
|
256
|
-
|
431
|
+
def literal?(piece_type)
|
432
|
+
[:indentable_string, :immutable_string].include?(piece_type)
|
433
|
+
end
|
257
434
|
|
258
|
-
|
259
|
-
|
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
|
260
439
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
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
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
266
449
|
|
450
|
+
class NiceQLConfig
|
451
|
+
attr_accessor :indentation_base, :open_bracket_is_newliner
|
267
452
|
|
268
453
|
def initialize
|
269
|
-
self.pg_adapter_with_nicesql = false
|
270
454
|
self.indentation_base = 2
|
271
455
|
self.open_bracket_is_newliner = false
|
272
|
-
self.prettify_active_record_log_output = false
|
273
|
-
self.prettify_pg_errors = ar_using_pg_adapter?
|
274
456
|
end
|
275
457
|
end
|
276
458
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(PostgresAdapterNiceQL) if config.pg_adapter_with_nicesql
|
283
|
-
|
284
|
-
::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend( AbstractAdapterLogPrettifier ) if config.prettify_active_record_log_output
|
285
|
-
|
286
|
-
::ActiveRecord::StatementInvalid.include( Niceql::ErrorExt ) if config.prettify_pg_errors && config.ar_using_pg_adapter?
|
287
|
-
end
|
288
|
-
|
289
|
-
def self.config
|
290
|
-
@config ||= NiceQLConfig.new
|
291
|
-
end
|
459
|
+
class << self
|
460
|
+
def configure
|
461
|
+
yield(config)
|
462
|
+
end
|
292
463
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
::Arel::Nodes::Node].each { |klass| klass.send(:include, ArExtentions) }
|
464
|
+
def config
|
465
|
+
@config ||= NiceQLConfig.new
|
466
|
+
end
|
297
467
|
end
|
298
|
-
|
299
468
|
end
|