niceql 0.1.2 → 0.1.3

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.
@@ -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