lib/niceql.rb in niceql-0.1.18 vs lib/niceql.rb in niceql-0.1.20

- old
+ new

@@ -37,50 +37,81 @@ NEW_LINE_VERBS = 'SELECT|FROM|WHERE|CASE|ORDER BY|LIMIT|GROUP BY|(RIGHT |LEFT )*(INNER |OUTER )*JOIN|HAVING|OFFSET|UPDATE' POSSIBLE_INLINER = /(ORDER BY|CASE)/ VERBS = "#{NEW_LINE_VERBS}|#{INLINE_VERBS}" STRINGS = /("[^"]+")|('[^']+')/ BRACKETS = '[\(\)]' + SQL_COMMENTS = /(\s*?--.+\s*)|(\s*?\/\*[^\/\*]*\*\/\s*)/ + # only newlined comments will be matched + SQL_COMMENTS_CLEARED = /(\s*?--.+\s{1})|(\s*$\s*\/\*[^\/\*]*\*\/\s{1})/ + COMMENT_CONTENT = /[\S]+[\s\S]*[\S]+/ - def self.config Niceql.config end def self.prettify_err(err) prettify_pg_err( err.to_s ) end - def self.prettify_pg_err(err) + # Postgres error output: + # ERROR: VALUES in FROM must have an alias + # LINE 2: FROM ( VALUES(1), (2) ); + # ^ + # HINT: For example, FROM (VALUES ...) [AS] foo. + + # May go without HINT or DETAIL: + # ERROR: column "usr" does not exist + # LINE 1: SELECT usr FROM users ORDER BY 1 + # ^ + + # ActiveRecord::StatementInvalid will add original SQL query to the bottom like this: + # ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column "usr" does not exist + # LINE 1: SELECT usr FROM users ORDER BY 1 + # ^ + #: SELECT usr FROM users ORDER BY 1 + + # prettify_pg_err parses ActiveRecord::StatementInvalid string, + # but you may use it without ActiveRecord either way: + # prettify_pg_err( err + "\n" + sql ) OR prettify_pg_err( err, sql ) + # don't mess with original sql query, or prettify_pg_err will deliver incorrect results + def self.prettify_pg_err(err, original_sql_query = nil) return err if err[/LINE \d+/].nil? err_line_num = err[/LINE \d+/][5..-1].to_i + # start_sql_line = err.lines[3][/(HINT|DETAIL)/] ? 4 : 3 - err_body = err.lines[start_sql_line..-1] + err_body = start_sql_line < err.lines.length ? err.lines[start_sql_line..-1] : original_sql_query&.lines + + + # this means original query is missing so it's nothing to prettify + return err unless err_body + err_quote = ( err.lines[1][/\.\.\..+\.\.\./] && err.lines[1][/\.\.\..+\.\.\./][3..-4] ) || ( err.lines[1][/\.\.\..+/] && err.lines[1][/\.\.\..+/][3..-1] ) - # line 2 is err carret line + # line[2] is err carret line i.e.: ' ^' # err.lines[1][/LINE \d+:/].length+1..-1 - is a position from error quote begin err_carret_line = err.lines[2][err.lines[1][/LINE \d+:/].length+1..-1] - # err line painted red completly, so we just remembering it and use + # err line will be painted in red completely, so we just remembering it and use # to replace after paiting the verbs err_line = err_body[err_line_num-1] - # when err line is too long postgres quotes it part in doble ... + # when err line is too long postgres quotes it part in double '...' if err_quote err_quote_carret_offset = err_carret_line.length - err.lines[1].index( '...' ) + 3 err_carret_line = ' ' * ( err_line.index( err_quote ) + err_quote_carret_offset ) + "^\n" end + err_carret_line = " " + err_carret_line if err_body[0].start_with?(': ') # if mistake is on last string than err_line.last != \n so we need to prepend \n to carret line - err_carret_line = "\n" + err_carret_line unless err_line.last == "\n" + err_carret_line = "\n" + err_carret_line unless err_line[-1] == "\n" #colorizing verbs and strings err_body = err_body.join.gsub(/#{VERBS}/ ) { |verb| StringColorize.colorize_verb(verb) } - err_body = err_body.gsub(STRINGS){ |str| StringColorize.colorize_str(str) } + .gsub(STRINGS){ |str| StringColorize.colorize_str(str) } #reassemling error message err_body = err_body.lines err_body[err_line_num-1]= StringColorize.colorize_err( err_line ) err_body.insert( err_line_num, StringColorize.colorize_err( err_carret_line ) ) @@ -90,20 +121,25 @@ def self.prettify_sql( sql, colorize = true ) indent = 0 parentness = [] - #it's better to remove all new lines because it will break formatting - sql = sql.gsub("\n", ' ') - # remove any additional formatting - sql = sql.gsub(/[\s]+/, ' ') + #it's better to remove all new lines because it will break formatting + remove any additional formatting with many spaces + # second map! is add newlines at start and begining for all comments started with new line + sql = sql.split( SQL_COMMENTS ).each_slice(2).map{ | crmb, cmmnt | + [crmb.gsub(/[\s]+/, ' '), + cmmnt && ( cmmnt&.match?(/\A\s*$/) ? "\n" + cmmnt[/[\S]+[\s\S]*[\S]+/] + "\n" : cmmnt[/[\S]+[\s\S]*[\S]+/] ) ] + }.flatten.join(' ') - sql = sql.gsub(STRINGS){ |str| StringColorize.colorize_str(str) } if colorize + sql.gsub!(/ \n/, "\n") + + sql.gsub!(STRINGS){ |str| StringColorize.colorize_str(str) } if colorize + first_verb = true + previous_was_comment = false - sql.gsub( /(#{VERBS}|#{BRACKETS})/).with_index do |verb, index| - add_new_line = false + sql.gsub!( /(#{VERBS}|#{BRACKETS}|#{SQL_COMMENTS_CLEARED})/) do |verb| if 'SELECT' == verb indent += config.indentation_base if !config.open_bracket_is_newliner || parentness.last.nil? || parentness.last[:nested] parentness.last[:nested] = true if parentness.last add_new_line = !first_verb elsif verb == '(' @@ -122,14 +158,31 @@ add_new_line = parentness.last.nil? || parentness.last[:nested] else add_new_line = verb[/(#{INLINE_VERBS})/].nil? end first_verb = false + + verb = verb[COMMENT_CONTENT] if verb[SQL_COMMENTS_CLEARED] + # !add_new_line && previous_was_comment means we had newlined comment, and now even + # if verb is inline verb we will need to add new line with indentation BUT all + # newliners match with a space before so we need to strip it + verb.lstrip! if !add_new_line && previous_was_comment verb = StringColorize.colorize_verb(verb) if !['(', ')'].include?(verb) && colorize - add_new_line ? "\n#{' ' * indent}" + verb : verb + (previous_was_comment || add_new_line ? indent_multiline(verb, indent) : verb).tap{ previous_was_comment = !verb.to_s[SQL_COMMENTS_CLEARED].nil? } end + sql.gsub( /\s+\n/, "\n" ).gsub(/\s+\z/, '') end + + private + def self.indent_multiline( verb, indent ) + # byebug + if verb.match?(/.\n./) + verb.lines.map!{|ln| "\n#{' ' * indent}" + ln}.join + else + "\n#{' ' * indent}" + verb.to_s + end + end end module PostgresAdapterNiceQL def exec_query(sql, name = "SQL", binds = [], prepare: false) # replacing sql with prettified sql, thats all @@ -138,37 +191,40 @@ end module AbstractAdapterLogPrettifier def log( sql, *args, &block ) # \n need to be placed because AR log will start with action description + time info. - # rescue sql - just to be sure Prettifier didn't break production + # rescue sql - just to be sure Prettifier wouldn't break production formatted_sql = "\n" + Prettifier.prettify_sql(sql) rescue sql super( formatted_sql, *args, &block ) end end module ErrorExt def to_s - if ActiveRecord::Base.configurations[Rails.env]['adapter'] == 'postgresql' - Prettifier.prettify_err( super ) + if Niceql.config.prettify_pg_errors && ActiveRecord::Base.configurations[Rails.env]['adapter'] == 'postgresql' + Prettifier.prettify_err(super) else super end end end class NiceQLConfig attr_accessor :pg_adapter_with_nicesql, :indentation_base, :open_bracket_is_newliner, - :prettify_active_record_log_output + :prettify_active_record_log_output, + :prettify_pg_errors + def initialize self.pg_adapter_with_nicesql = false self.indentation_base = 2 self.open_bracket_is_newliner = false self.prettify_active_record_log_output = false + self.prettify_pg_errors = defined? ::ActiveRecord::Base && ActiveRecord::Base.configurations[Rails.env]['adapter'] == 'postgresql' end end def self.configure @@ -177,20 +233,22 @@ if config.pg_adapter_with_nicesql ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(PostgresAdapterNiceQL) end if config.prettify_active_record_log_output - ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(AbstractAdapterLogPrettifier) + ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend( AbstractAdapterLogPrettifier ) end + + if config.prettify_pg_errors + ::ActiveRecord::StatementInvalid.include( Niceql::ErrorExt ) + end end def self.config @config ||= NiceQLConfig.new end - if defined? ::ActiveRecord::Base - ActiveRecord::StatementInvalid.include( Niceql::ErrorExt ) ::ActiveRecord::Base.extend ArExtentions [::ActiveRecord::Relation, ::ActiveRecord::Associations::CollectionProxy].each { |klass| klass.send(:include, ArExtentions) } end end