niceql 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ module Niceql
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../../templates", __FILE__)
5
+ desc "Creates Niceql initializer for your application"
6
+
7
+ def copy_initializer
8
+ template "niceql_initializer.rb", "config/initializers/niceql.rb"
9
+
10
+ puts "Install complete!"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ Niceql.configure do |c|
2
+ # You can adjust pg_adapter in prooduction at your own risk!
3
+ # If you need it in production use exec_niceql
4
+ # default: false
5
+ # c.pg_adapter_with_nicesql = Rails.env.development?
6
+
7
+ # this are default settings, change it to your project needs
8
+ # c.indentation_base = 2
9
+ # c.open_bracket_is_newliner = false
10
+ # c.prettify_active_record_log_output = false
11
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def match?(pattern)
3
+ self =~ pattern
4
+ end
5
+ end unless String.method_defined?(:match?)
@@ -1,3 +1,3 @@
1
1
  module Niceql
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
data/lib/niceql.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require "niceql/version"
2
+ require 'niceql/string'
2
3
 
3
4
  module Niceql
5
+
4
6
  module StringColorize
5
7
  def self.colorize_verb( str)
6
8
  #yellow ANSI color
@@ -17,92 +19,264 @@ module Niceql
17
19
  end
18
20
 
19
21
  module ArExtentions
20
- def explain_err
21
- begin
22
- connection.execute( "EXPLAIN #{to_nicesql}" )
23
- rescue StandardError => e
24
- puts Prettifier.prettify_err(e )
25
- end
22
+ def exec_niceql
23
+ connection.execute( to_niceql )
26
24
  end
27
25
 
28
26
  def to_niceql
29
27
  Prettifier.prettify_sql(to_sql, false)
30
28
  end
31
29
 
32
- def puts_niceql( colorize = true )
30
+ def niceql( colorize = true )
33
31
  puts Prettifier.prettify_sql( to_sql, colorize )
34
32
  end
33
+
35
34
  end
36
35
 
37
36
  module Prettifier
38
- INLINE_VERBS = %w(ASC IN AS WHEN THEN ELSE END AND UNION ALL WITH ON DISTINCT INTERSECT EXCEPT EXISTS NOT).join('| ')
39
- NEW_LINE_VERBS = 'SELECT|FROM|WHERE|CASE|ORDER BY|LIMIT|GROUP BY|WITH|LEFT JOIN|RIGHT JOIN|JOIN|HAVING|OFFSET'
40
- VERBS = "#{INLINE_VERBS}|#{NEW_LINE_VERBS}"
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'
39
+ POSSIBLE_INLINER = /(ORDER BY|CASE)/
40
+ VERBS = "#{NEW_LINE_VERBS}|#{INLINE_VERBS}"
41
41
  STRINGS = /("[^"]+")|('[^']+')/
42
42
  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})/
46
+ COMMENT_CONTENT = /[\S]+[\s\S]*[\S]+/
47
+
48
+ def self.config
49
+ Niceql.config
50
+ end
43
51
 
44
52
  def self.prettify_err(err)
45
- if ActiveRecord::Base.configurations[Rails.env]['adapter'] == 'postgresql'
46
- prettify_pg_err( err.to_s )
47
- else
48
- puts err
49
- end
53
+ prettify_pg_err( err.to_s )
50
54
  end
51
55
 
52
56
 
53
- def self.prettify_pg_err(err)
57
+ # Postgres error output:
58
+ # ERROR: VALUES in FROM must have an alias
59
+ # LINE 2: FROM ( VALUES(1), (2) );
60
+ # ^
61
+ # HINT: For example, FROM (VALUES ...) [AS] foo.
62
+
63
+ # May go without HINT or DETAIL:
64
+ # ERROR: column "usr" does not exist
65
+ # LINE 1: SELECT usr FROM users ORDER BY 1
66
+ # ^
67
+
68
+ # ActiveRecord::StatementInvalid will add original SQL query to the bottom like this:
69
+ # ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column "usr" does not exist
70
+ # LINE 1: SELECT usr FROM users ORDER BY 1
71
+ # ^
72
+ #: SELECT usr FROM users ORDER BY 1
73
+
74
+ # prettify_pg_err parses ActiveRecord::StatementInvalid string,
75
+ # but you may use it without ActiveRecord either way:
76
+ # prettify_pg_err( err + "\n" + sql ) OR prettify_pg_err( err, sql )
77
+ # don't mess with original sql query, or prettify_pg_err will deliver incorrect results
78
+ def self.prettify_pg_err(err, original_sql_query = nil)
79
+ return err if err[/LINE \d+/].nil?
54
80
  err_line_num = err[/LINE \d+/][5..-1].to_i
55
- start_sql_line = err.lines[3][/HINT/] ? 4 : 3
56
- err_body = err.lines[start_sql_line..-1]
57
- err_line = StringColorize.colorize_err( err_body[err_line_num-1] )
58
81
 
82
+ #
83
+ start_sql_line = err.lines[3][/(HINT|DETAIL)/] ? 4 : 3
84
+ err_body = start_sql_line < err.lines.length ? err.lines[start_sql_line..-1] : original_sql_query&.lines
85
+
86
+
87
+ # this means original query is missing so it's nothing to prettify
88
+ return err unless err_body
89
+
90
+ err_quote = ( err.lines[1][/\.\.\..+\.\.\./] && err.lines[1][/\.\.\..+\.\.\./][3..-4] ) ||
91
+ ( err.lines[1][/\.\.\..+/] && err.lines[1][/\.\.\..+/][3..-1] )
92
+
93
+ # line[2] is err carret line i.e.: ' ^'
94
+ # err.lines[1][/LINE \d+:/].length+1..-1 - is a position from error quote begin
95
+ err_carret_line = err.lines[2][err.lines[1][/LINE \d+:/].length+1..-1]
96
+ # err line will be painted in red completely, so we just remembering it and use
97
+ # to replace after paiting the verbs
98
+ err_line = err_body[err_line_num-1]
99
+
100
+ # when err line is too long postgres quotes it part in double '...'
101
+ if err_quote
102
+ err_quote_carret_offset = err_carret_line.length - err.lines[1].index( '...' ) + 3
103
+ err_carret_line = ' ' * ( err_line.index( err_quote ) + err_quote_carret_offset ) + "^\n"
104
+ end
105
+
106
+ err_carret_line = " " + err_carret_line if err_body[0].start_with?(': ')
107
+ # if mistake is on last string than err_line.last != \n so we need to prepend \n to carret line
108
+ err_carret_line = "\n" + err_carret_line unless err_line[-1] == "\n"
109
+
110
+ #colorizing verbs and strings
59
111
  err_body = err_body.join.gsub(/#{VERBS}/ ) { |verb| StringColorize.colorize_verb(verb) }
60
- err_body = err_body.gsub(STRINGS){ |str| StringColorize.colorize_str(str) }
112
+ .gsub(STRINGS){ |str| StringColorize.colorize_str(str) }
61
113
 
114
+ #reassemling error message
62
115
  err_body = err_body.lines
63
- err_body[err_line_num-1]= err_line
64
- err_body.insert( err_line_num, StringColorize.colorize_err( err.lines[2][err.lines[1][/LINE \d+:/].length+1..-1] ) )
65
- puts err.lines[0..start_sql_line-1].join + err_body.join
116
+ err_body[err_line_num-1]= StringColorize.colorize_err( err_line )
117
+ err_body.insert( err_line_num, StringColorize.colorize_err( err_carret_line ) )
118
+
119
+ err.lines[0..start_sql_line-1].join + err_body.join
66
120
  end
67
121
 
68
122
  def self.prettify_sql( sql, colorize = true )
69
123
  indent = 0
70
124
  parentness = []
71
125
 
72
- sql = sql.gsub(STRINGS){ |str| StringColorize.colorize_str(str) } if colorize
126
+ sql = sql.split( SQL_COMMENTS ).each_slice(2).map{ | sql_part, comment |
127
+ # remove additional formatting for sql_parts but leave comment intact
128
+ [sql_part.gsub(/[\s]+/, ' '),
129
+ # comment.match?(/\A\s*$/) - SQL_COMMENTS gets all comment content + all whitespaced chars around
130
+ # so this sql_part.length == 0 || comment.match?(/\A\s*$/) checks does the comment starts from new line
131
+ comment && ( sql_part.length == 0 || comment.match?(/\A\s*$/) ? "\n#{comment[COMMENT_CONTENT]}\n" : comment[COMMENT_CONTENT] ) ]
132
+ }.flatten.join(' ')
133
+
134
+ sql.gsub!(/ \n/, "\n")
135
+
136
+ sql.gsub!(STRINGS){ |str| StringColorize.colorize_str(str) } if colorize
137
+
73
138
  first_verb = true
139
+ prev_was_comment = false
74
140
 
75
- sql.gsub( /(#{VERBS}|#{BRACKETS})/) do |verb|
76
- add_new_line = false
141
+ sql.gsub!( /(#{VERBS}|#{BRACKETS}|#{SQL_COMMENTS_CLEARED})/) do |verb|
77
142
  if 'SELECT' == verb
78
- indent += 1
143
+ indent += config.indentation_base if !config.open_bracket_is_newliner || parentness.last.nil? || parentness.last[:nested]
79
144
  parentness.last[:nested] = true if parentness.last
80
145
  add_new_line = !first_verb
81
- first_verb = false
82
146
  elsif verb == '('
83
- parentness << { nested: false }
84
- indent += 1
147
+ next_closing_bracket = Regexp.last_match.post_match.index(')')
148
+ # check if brackets contains SELECT statement
149
+ add_new_line = !!Regexp.last_match.post_match[0..next_closing_bracket][/SELECT/] && config.open_bracket_is_newliner
150
+ parentness << { nested: add_new_line }
85
151
  elsif verb == ')'
86
152
  # this also covers case when right bracket is used without corresponding left one
87
153
  add_new_line = parentness.last.nil? || parentness.last[:nested]
88
- indent -= add_new_line ? 2 : 1
154
+ indent -= ( parentness.last.nil? ? 2 * config.indentation_base : (parentness.last[:nested] ? config.indentation_base : 0) )
89
155
  indent = 0 if indent < 0
90
156
  parentness.pop
91
- elsif verb == 'ORDER BY'
157
+ elsif verb[POSSIBLE_INLINER]
92
158
  # in postgres ORDER BY can be used in aggregation function this will keep it
93
159
  # inline with its agg function
94
160
  add_new_line = parentness.last.nil? || parentness.last[:nested]
95
161
  else
96
162
  add_new_line = verb[/(#{INLINE_VERBS})/].nil?
97
163
  end
164
+
165
+ # !add_new_line && previous_was_comment means we had newlined comment, and now even
166
+ # if verb is inline verb we will need to add new line with indentation BUT all
167
+ # inliners match with a space before so we need to strip it
168
+ verb.lstrip! if !add_new_line && prev_was_comment
169
+
170
+ add_new_line = prev_was_comment unless add_new_line
171
+ add_indent = !first_verb && add_new_line
172
+
173
+ if verb[SQL_COMMENTS_CLEARED]
174
+ verb = verb[COMMENT_CONTENT]
175
+ prev_was_comment = true
176
+ else
177
+ first_verb = false
178
+ prev_was_comment = false
179
+ end
180
+
98
181
  verb = StringColorize.colorize_verb(verb) if !['(', ')'].include?(verb) && colorize
99
- add_new_line ? "\n#{' ' * indent}" + verb : verb
182
+
183
+ subs = ( add_indent ? indent_multiline(verb, indent) : verb)
184
+ !first_verb && add_new_line ? "\n" + subs : subs
185
+ end
186
+
187
+ # clear all spaces before newlines, and all whitespaces before string end
188
+ sql.tap{ |slf| slf.gsub!( /\s+\n/, "\n" ) }.tap{ |slf| slf.gsub!(/\s+\z/, '') }
189
+ end
190
+
191
+ def self.prettify_multiple( sql_multi, colorize = true )
192
+ sql_multi.split( /(?>#{SQL_COMMENTS})|(\;)/ ).inject(['']) { |queries, pattern|
193
+ queries.last << pattern
194
+ queries << '' if pattern == ';'
195
+ queries
196
+ }.map!{ |sql|
197
+ # we were splitting by comments and ;, so if next sql start with comment we've got a misplaced \n\n
198
+ sql.match?(/\A\s+\z/) ? nil : prettify_sql( sql, colorize )
199
+ }.compact.join("\n\n")
200
+ end
201
+
202
+ private
203
+ def self.indent_multiline( verb, indent )
204
+ #
205
+ if verb.match?(/.\s*\n\s*./)
206
+ verb.lines.map!{|ln| "#{' ' * indent}" + ln}.join("\n")
207
+ else
208
+ "#{' ' * indent}" + verb.to_s
100
209
  end
101
210
  end
102
211
  end
103
212
 
213
+ module PostgresAdapterNiceQL
214
+ def exec_query(sql, name = "SQL", binds = [], prepare: false)
215
+ # replacing sql with prettified sql, thats all
216
+ super( Prettifier.prettify_sql(sql, false), name, binds, prepare: prepare )
217
+ end
218
+ end
219
+
220
+ module AbstractAdapterLogPrettifier
221
+ def log( sql, *args, &block )
222
+ # \n need to be placed because AR log will start with action description + time info.
223
+ # rescue sql - just to be sure Prettifier wouldn't break production
224
+ formatted_sql = "\n" + Prettifier.prettify_sql(sql) rescue sql
225
+ super( formatted_sql, *args, &block )
226
+ end
227
+ end
228
+
229
+ module ErrorExt
230
+ def to_s
231
+ Niceql.config.prettify_pg_errors ? Prettifier.prettify_err(super) : super
232
+ end
233
+ end
234
+
235
+ class NiceQLConfig
236
+ def ar_using_pg_adapter?
237
+ return false unless defined?(::ActiveRecord::Base)
238
+
239
+ config = ActiveRecord::Base.try(:connection_db_config) || ActiveRecord::Base.try(:connection_config)
240
+ config&.dig('adapter') == 'postgresql'
241
+ end
242
+
243
+ attr_accessor :pg_adapter_with_nicesql,
244
+ :indentation_base,
245
+ :open_bracket_is_newliner,
246
+ :prettify_active_record_log_output,
247
+ :prettify_pg_errors
248
+
249
+
250
+ def initialize
251
+ self.pg_adapter_with_nicesql = false
252
+ self.indentation_base = 2
253
+ self.open_bracket_is_newliner = false
254
+ self.prettify_active_record_log_output = false
255
+ self.prettify_pg_errors = ar_using_pg_adapter?
256
+ end
257
+ end
258
+
259
+ def self.configure
260
+ yield( config )
261
+
262
+ return unless defined? ::ActiveRecord::Base
263
+
264
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(PostgresAdapterNiceQL) if config.pg_adapter_with_nicesql
265
+
266
+ ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend( AbstractAdapterLogPrettifier ) if config.prettify_active_record_log_output
267
+
268
+ ::ActiveRecord::StatementInvalid.include( Niceql::ErrorExt ) if config.prettify_pg_errors && config.ar_using_pg_adapter?
269
+ end
270
+
271
+ def self.config
272
+ @config ||= NiceQLConfig.new
273
+ end
274
+
104
275
  if defined? ::ActiveRecord::Base
105
276
  ::ActiveRecord::Base.extend ArExtentions
106
277
  [::ActiveRecord::Relation, ::ActiveRecord::Associations::CollectionProxy].each { |klass| klass.send(:include, ArExtentions) }
107
278
  end
279
+
108
280
  end
281
+
282
+
data/niceql.gemspec CHANGED
@@ -30,8 +30,12 @@ Gem::Specification.new do |spec|
30
30
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
31
  spec.require_paths = ["lib"]
32
32
 
33
- spec.add_development_dependency "bundler", "~> 1.15"
34
- spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.required_ruby_version = '>= 2.3'
34
+ spec.add_development_dependency "bundler", ">= 1"
35
+ spec.add_development_dependency "rake", ">= 12.3.3"
35
36
  spec.add_development_dependency "minitest", "~> 5.0"
36
37
 
38
+ spec.add_development_dependency "differ"
39
+ spec.add_development_dependency "pry-byebug"
40
+ spec.add_development_dependency "benchmark-ips"
37
41
  end
data/to_niceql.png CHANGED
Binary file
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: niceql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - alekseyl
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-09-08 00:00:00.000000000 Z
11
+ date: 2021-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.15'
19
+ version: '1'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.15'
26
+ version: '1'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: 12.3.3
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: 12.3.3
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +52,48 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: differ
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: benchmark-ips
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
55
97
  description: 'This is simple and nice sql prettifier, it splits, indent and colorize
56
98
  SQL query and PG error if any '
57
99
  email:
@@ -62,6 +104,7 @@ extra_rdoc_files: []
62
104
  files:
63
105
  - ".gitignore"
64
106
  - ".travis.yml"
107
+ - CHANGELOG.md
65
108
  - Gemfile
66
109
  - LICENSE.txt
67
110
  - README.md
@@ -70,7 +113,13 @@ files:
70
113
  - bin/setup
71
114
  - err_now.png
72
115
  - err_was.png
116
+ - lib/benchmark/cat.rb
117
+ - lib/benchmark/gsub.rb
118
+ - lib/benchmark/txt
119
+ - lib/generators/niceql/install_generator.rb
120
+ - lib/generators/templates/niceql_initializer.rb
73
121
  - lib/niceql.rb
122
+ - lib/niceql/string.rb
74
123
  - lib/niceql/version.rb
75
124
  - niceql.gemspec
76
125
  - to_niceql.png
@@ -79,7 +128,7 @@ licenses:
79
128
  - MIT
80
129
  metadata:
81
130
  allowed_push_host: https://rubygems.org
82
- post_install_message:
131
+ post_install_message:
83
132
  rdoc_options: []
84
133
  require_paths:
85
134
  - lib
@@ -87,16 +136,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
87
136
  requirements:
88
137
  - - ">="
89
138
  - !ruby/object:Gem::Version
90
- version: '0'
139
+ version: '2.3'
91
140
  required_rubygems_version: !ruby/object:Gem::Requirement
92
141
  requirements:
93
142
  - - ">="
94
143
  - !ruby/object:Gem::Version
95
144
  version: '0'
96
145
  requirements: []
97
- rubyforge_project:
98
- rubygems_version: 2.6.13
99
- signing_key:
146
+ rubyforge_project:
147
+ rubygems_version: 2.7.6
148
+ signing_key:
100
149
  specification_version: 4
101
150
  summary: This is simple and nice sql prettifier, it splits, indent and colorize SQL
102
151
  query and PG errors if any