lib/niceql.rb in niceql-0.2.0 vs lib/niceql.rb in niceql-0.3.0

- old
+ new

@@ -1,7 +1,6 @@ require "niceql/version" -require 'niceql/string' module Niceql module StringColorize def self.colorize_verb( str) @@ -43,171 +42,185 @@ 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 + class << self + def config + Niceql.config + end - def self.prettify_err(err) - prettify_pg_err( err.to_s ) - end + def prettify_err(err, original_sql_query = nil) + prettify_pg_err( err.to_s, original_sql_query ) + end - # Postgres error output: - # ERROR: VALUES in FROM must have an alias - # LINE 2: FROM ( VALUES(1), (2) ); - # ^ - # HINT: For example, FROM (VALUES ...) [AS] foo. + # 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 - # ^ + # 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 + # 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 + # 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 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 + # LINE 1: SELECT usr FROM users ORDER BY 1 + err_address_line = err.lines[1] - # - start_sql_line = err.lines[3][/(HINT|DETAIL)/] ? 4 : 3 - err_body = start_sql_line < err.lines.length ? err.lines[start_sql_line..-1] : original_sql_query&.lines + start_sql_line = 3 if err.lines.length <= 3 + # error not always contains HINT + start_sql_line ||= err.lines[3][/(HINT|DETAIL)/] ? 4 : 3 + sql_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 sql_body - # this means original query is missing so it's nothing to prettify - return err unless err_body + # err line will be painted in red completely, so we just remembering it and use + # to replace after painting the verbs + err_line = sql_body[err_line_num - 1] - err_quote = ( err.lines[1][/\.\.\..+\.\.\./] && err.lines[1][/\.\.\..+\.\.\./][3..-4] ) || - ( err.lines[1][/\.\.\..+/] && err.lines[1][/\.\.\..+/][3..-1] ) - # 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 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] + #colorizing verbs and strings + colorized_sql_body = sql_body.join.gsub(/#{VERBS}/ ) { |verb| StringColorize.colorize_verb(verb) } + .gsub(STRINGS){ |str| StringColorize.colorize_str(str) } - # 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" + #reassemling error message + err_body = colorized_sql_body.lines + # replacing colorized line contained error and adding caret line + err_body[err_line_num - 1]= StringColorize.colorize_err( err_line ) + + err_caret_line = extract_err_caret_line( err_address_line, err_line, sql_body, err ) + err_body.insert( err_line_num, StringColorize.colorize_err( err_caret_line ) ) + + err.lines[0..start_sql_line-1].join + err_body.join 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[-1] == "\n" + def prettify_sql( sql, colorize = true ) + indent = 0 + parentness = [] - #colorizing verbs and strings - err_body = err_body.join.gsub(/#{VERBS}/ ) { |verb| StringColorize.colorize_verb(verb) } - .gsub(STRINGS){ |str| StringColorize.colorize_str(str) } + sql = sql.split( SQL_COMMENTS ).each_slice(2).map{ | sql_part, comment | + # remove additional formatting for sql_parts but leave comment intact + [sql_part.gsub(/[\s]+/, ' '), + # comment.match?(/\A\s*$/) - SQL_COMMENTS gets all comment content + all whitespaced chars around + # so this sql_part.length == 0 || comment.match?(/\A\s*$/) checks does the comment starts from new line + comment && ( sql_part.length == 0 || comment.match?(/\A\s*$/) ? "\n#{comment[COMMENT_CONTENT]}\n" : comment[COMMENT_CONTENT] ) ] + }.flatten.join(' ') - #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 ) ) + sql.gsub!(/ \n/, "\n") - err.lines[0..start_sql_line-1].join + err_body.join - end + sql.gsub!(STRINGS){ |str| StringColorize.colorize_str(str) } if colorize - def self.prettify_sql( sql, colorize = true ) - indent = 0 - parentness = [] + first_verb = true + prev_was_comment = false - sql = sql.split( SQL_COMMENTS ).each_slice(2).map{ | sql_part, comment | - # remove additional formatting for sql_parts but leave comment intact - [sql_part.gsub(/[\s]+/, ' '), - # comment.match?(/\A\s*$/) - SQL_COMMENTS gets all comment content + all whitespaced chars around - # so this sql_part.length == 0 || comment.match?(/\A\s*$/) checks does the comment starts from new line - comment && ( sql_part.length == 0 || comment.match?(/\A\s*$/) ? "\n#{comment[COMMENT_CONTENT]}\n" : comment[COMMENT_CONTENT] ) ] - }.flatten.join(' ') + 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 == '(' + next_closing_bracket = Regexp.last_match.post_match.index(')') + # check if brackets contains SELECT statement + add_new_line = !!Regexp.last_match.post_match[0..next_closing_bracket][/SELECT/] && config.open_bracket_is_newliner + parentness << { nested: add_new_line } + elsif verb == ')' + # this also covers case when right bracket is used without corresponding left one + add_new_line = parentness.last.nil? || parentness.last[:nested] + indent -= ( parentness.last.nil? ? 2 * config.indentation_base : (parentness.last[:nested] ? config.indentation_base : 0) ) + indent = 0 if indent < 0 + parentness.pop + elsif verb[POSSIBLE_INLINER] + # in postgres ORDER BY can be used in aggregation function this will keep it + # inline with its agg function + add_new_line = parentness.last.nil? || parentness.last[:nested] + else + add_new_line = verb[/(#{INLINE_VERBS})/].nil? + end - sql.gsub!(/ \n/, "\n") + # !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 + # inliners match with a space before so we need to strip it + verb.lstrip! if !add_new_line && prev_was_comment - sql.gsub!(STRINGS){ |str| StringColorize.colorize_str(str) } if colorize + add_new_line = prev_was_comment unless add_new_line + add_indent = !first_verb && add_new_line - first_verb = true - prev_was_comment = false + if verb[SQL_COMMENTS_CLEARED] + verb = verb[COMMENT_CONTENT] + prev_was_comment = true + else + first_verb = false + prev_was_comment = false + end - 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 == '(' - next_closing_bracket = Regexp.last_match.post_match.index(')') - # check if brackets contains SELECT statement - add_new_line = !!Regexp.last_match.post_match[0..next_closing_bracket][/SELECT/] && config.open_bracket_is_newliner - parentness << { nested: add_new_line } - elsif verb == ')' - # this also covers case when right bracket is used without corresponding left one - add_new_line = parentness.last.nil? || parentness.last[:nested] - indent -= ( parentness.last.nil? ? 2 * config.indentation_base : (parentness.last[:nested] ? config.indentation_base : 0) ) - indent = 0 if indent < 0 - parentness.pop - elsif verb[POSSIBLE_INLINER] - # in postgres ORDER BY can be used in aggregation function this will keep it - # inline with its agg function - add_new_line = parentness.last.nil? || parentness.last[:nested] - else - add_new_line = verb[/(#{INLINE_VERBS})/].nil? + verb = StringColorize.colorize_verb(verb) if !%w[( )].include?(verb) && colorize + + subs = ( add_indent ? indent_multiline(verb, indent) : verb) + !first_verb && add_new_line ? "\n" + subs : subs end - # !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 - # inliners match with a space before so we need to strip it - verb.lstrip! if !add_new_line && prev_was_comment + # clear all spaces before newlines, and all whitespaces before strings endings + sql.tap{ |slf| slf.gsub!( /\s+\n/, "\n" ) }.tap{ |slf| slf.gsub!(/\s+\z/, '') } + end - add_new_line = prev_was_comment unless add_new_line - add_indent = !first_verb && add_new_line + def prettify_multiple( sql_multi, colorize = true ) + sql_multi.split( /(?>#{SQL_COMMENTS})|(\;)/ ).inject(['']) { |queries, pattern| + queries.last << pattern + queries << '' if pattern == ';' + queries + }.map!{ |sql| + # we were splitting by comments and ;, so if next sql start with comment we've got a misplaced \n\n + sql.match?(/\A\s+\z/) ? nil : prettify_sql( sql, colorize ) + }.compact.join("\n\n") + end - if verb[SQL_COMMENTS_CLEARED] - verb = verb[COMMENT_CONTENT] - prev_was_comment = true + private_class_method + def indent_multiline( verb, indent ) + if verb.match?(/.\s*\n\s*./) + verb.lines.map!{|ln| ln.prepend(' ' * indent)}.join("\n") else - first_verb = false - prev_was_comment = false + verb.prepend(' ' * indent) end - - verb = StringColorize.colorize_verb(verb) if !['(', ')'].include?(verb) && colorize - - subs = ( add_indent ? indent_multiline(verb, indent) : verb) - !first_verb && add_new_line ? "\n" + subs : subs end - # clear all spaces before newlines, and all whitespaces before string end - sql.tap{ |slf| slf.gsub!( /\s+\n/, "\n" ) }.tap{ |slf| slf.gsub!(/\s+\z/, '') } - end + def extract_err_caret_line( err_address_line, err_line, sql_body, err ) + # LINE could be quoted ( both sides and sometimes only from one ): + # "LINE 1: ...t_id\" = $13 AND \"products\".\"carrier_id\" = $14 AND \"product_t...\n", + err_quote = (err_address_line.match(/\.\.\.(.+)\.\.\./) || err_address_line.match(/\.\.\.(.+)/) ).try(:[], 1) - def self.prettify_multiple( sql_multi, colorize = true ) - sql_multi.split( /(?>#{SQL_COMMENTS})|(\;)/ ).inject(['']) { |queries, pattern| - queries.last << pattern - queries << '' if pattern == ';' - queries - }.map!{ |sql| - # we were splitting by comments and ;, so if next sql start with comment we've got a misplaced \n\n - sql.match?(/\A\s+\z/) ? nil : prettify_sql( sql, colorize ) - }.compact.join("\n\n") - end + # line[2] is original err caret line i.e.: ' ^' + # err_address_line[/LINE \d+:/].length+1..-1 - is a position from error quote begin + err_caret_line = err.lines[2][err_address_line[/LINE \d+:/].length+1..-1] - private - def self.indent_multiline( verb, indent ) - # - if verb.match?(/.\s*\n\s*./) - verb.lines.map!{|ln| "#{' ' * indent}" + ln}.join("\n") - else - "#{' ' * indent}" + verb.to_s + # when err line is too long postgres quotes it in double '...' + # so we need to reposition caret against original line + if err_quote + err_quote_caret_offset = err_caret_line.length - err_address_line.index( '...' ).to_i + 3 + err_caret_line = ' ' * ( err_line.index( err_quote ) + err_quote_caret_offset ) + "^\n" + end + + # older versions of ActiveRecord were adding ': ' before an original query :( + err_caret_line.prepend(' ') if sql_body[0].start_with?(': ') + # if mistake is on last string than err_line.last != \n then we need to prepend \n to caret line + err_caret_line.prepend("\n") unless err_line[-1] == "\n" + err_caret_line end end end module PostgresAdapterNiceQL @@ -226,11 +239,13 @@ end end module ErrorExt def to_s - Niceql.config.prettify_pg_errors ? Prettifier.prettify_err(super) : super + # older rails version do not provide sql as a standalone query, instead they + # deliver joined message + Niceql.config.prettify_pg_errors ? Prettifier.prettify_err(super, try(:sql) ) : super end end class NiceQLConfig def ar_using_pg_adapter? @@ -278,7 +293,5 @@ ::ActiveRecord::Base.extend ArExtentions [::ActiveRecord::Relation, ::ActiveRecord::Associations::CollectionProxy].each { |klass| klass.send(:include, ArExtentions) } end end - -