From: Darold Gilles Date: Tue, 21 Aug 2012 08:11:06 +0000 (+0200) Subject: Add SQL formater on SQL queries. When you will click on a query it will be beautified X-Git-Tag: v3.2~169 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=1f97bfbaf999a8aff8a5496f24eca8bc89e0946b;p=pgbadger Add SQL formater on SQL queries. When you will click on a query it will be beautified --- diff --git a/pgbadger b/pgbadger index d7d7ae8..36c6fa1 100755 --- a/pgbadger +++ b/pgbadger @@ -57,6 +57,7 @@ my $graph = 1; my $nograph = 0; my $debug = 0; my $nohighlight = 0; +my $noprettify = 0; my $from = ''; my $to = ''; my $regex_prefix_dbname = ''; @@ -108,6 +109,7 @@ my $result = GetOptions( "m|maxlength=i" => \$maxlength, "n|nohighlight!" => \$nohighlight, "o|outfile=s" => \$outfile, + "P|no-prettify!" => \$noprettify, "q|quiet!" => \$quiet, "s|sample=i" => \$sample, "t|top=i" => \$top, @@ -672,7 +674,7 @@ Usage: pgbadger [options] logfile [...] PostgreSQL log analyzer with fully detailed reports and graphs. -Arguments: +Arguments:² logfile can be a single log file, a list of files or a shell command returning a list of file. If you want to pass log content from stdin @@ -698,6 +700,7 @@ Options: -o | --outfile filename: define the filename for the output. Default depends of the output format: out.html or out.txt. To dump output to stdout use - as filename. + -P | --no-prettify : disable SQL queries prettify formatter. -q | --quiet : don't print anything to stdout, even not a progress bar. -s | --sample number : number of query sample to store/display. Default: 3 -t | --top number : number of query to store/display. Default: 20 @@ -2518,7 +2521,8 @@ qq{Wrote buffersAddedRemovedRecycledWrit for (my $i = 0 ; $i <= $#top_slowest ; $i++) { my $col = $i % 2; print $fh "", $i + 1, "", - &convert_time($top_slowest[$i]->[0]), "[1]\">
", + &convert_time($top_slowest[$i]->[0]), + "[1]\">
", &highlight_code($top_slowest[$i]->[2]), "
\n"; } print $fh "\n"; @@ -2571,7 +2575,8 @@ qq{Wrote buffersAddedRemovedRecycledWrit } } print $fh "
"; - print $fh "", &convert_time($normalyzed_info{$k}{average}), "
", + print $fh "", &convert_time($normalyzed_info{$k}{average}), + "
", &highlight_code($q), "
"; if ($normalyzed_info{$k}{count} > 1) { @@ -2580,7 +2585,8 @@ qq{Wrote buffersAddedRemovedRecycledWrit my $i = 0; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$k}{samples}}) { my $colb = $i % 2; - print $fh "
", + print $fh +"
", &convert_time($d), " | ", &highlight_code($normalyzed_info{$k}{samples}{$d}{query}), "
"; $i++; } @@ -2636,7 +2642,8 @@ qq{Wrote buffersAddedRemovedRecycledWrit } print $fh "
"; print $fh "", &convert_time($normalyzed_info{$k}{duration}), "", - &convert_time($normalyzed_info{$k}{average}), "
", &highlight_code($q), "
"; + &convert_time($normalyzed_info{$k}{average}), "
", + &highlight_code($q), "
"; if ($normalyzed_info{$k}{count} > 1) { print $fh @@ -2644,7 +2651,8 @@ qq{Wrote buffersAddedRemovedRecycledWrit my $i = 0; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$k}{samples}}) { my $colb = $i % 2; - print $fh "
", + print $fh +"
", &convert_time($d), " | ", &highlight_code($normalyzed_info{$k}{samples}{$d}{query}), "
"; $i++; } @@ -2701,7 +2709,8 @@ qq{Wrote buffersAddedRemovedRecycledWrit } } print $fh "
"; - print $fh "", &convert_time($normalyzed_info{$k}{duration}), "
", + print $fh "", &convert_time($normalyzed_info{$k}{duration}), + "
", &highlight_code($q), "
"; if ($normalyzed_info{$k}{count} > 1) { print $fh @@ -2709,7 +2718,8 @@ qq{Wrote buffersAddedRemovedRecycledWrit my $i = 0; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$k}{samples}}) { my $colb = $i % 2; - print $fh "
", + print $fh +"
", &convert_time($d), " | ", &highlight_code($normalyzed_info{$k}{samples}{$d}{query}), "
"; $i++; } @@ -2898,6 +2908,13 @@ sub highlight_code { my $code = shift; + # prettify SQL query + if (!$noprettify) { + my $sql = SQL::Beautify->new; + $sql->query($code); + $code = $sql->beautify; + } + return $code if ($nohighlight); foreach my $x (keys %SYMBOLS) { @@ -3694,9 +3711,437 @@ EOF } +{ + + package SQL::Beautify; + + use strict; + use warnings; + + our $VERSION = 0.04; + + use Carp; + + # Keywords from SQL-92, SQL-99 and SQL-2003. + use constant KEYWORDS => qw( + ABSOLUTE ACTION ADD AFTER ALL ALLOCATE ALTER AND ANY ARE ARRAY AS ASC + ASENSITIVE ASSERTION ASYMMETRIC AT ATOMIC AUTHORIZATION AVG BEFORE BEGIN + BETWEEN BIGINT BINARY BIT BIT_LENGTH BLOB BOOLEAN BOTH BREADTH BY CALL + CALLED CASCADE CASCADED CASE CAST CATALOG CHAR CHARACTER CHARACTER_LENGTH + CHAR_LENGTH CHECK CLOB CLOSE COALESCE COLLATE COLLATION COLUMN COMMIT + CONDITION CONNECT CONNECTION CONSTRAINT CONSTRAINTS CONSTRUCTOR CONTAINS + CONTINUE CONVERT CORRESPONDING COUNT CREATE CROSS CUBE CURRENT CURRENT_DATE + CURRENT_DEFAULT_TRANSFORM_GROUP CURRENT_PATH CURRENT_ROLE CURRENT_TIME + CURRENT_TIMESTAMP CURRENT_TRANSFORM_GROUP_FOR_TYPE CURRENT_USER CURSOR + CYCLE DATA DATE DAY DEALLOCATE DEC DECIMAL DECLARE DEFAULT DEFERRABLE + DEFERRED DELETE DEPTH DEREF DESC DESCRIBE DESCRIPTOR DETERMINISTIC + DIAGNOSTICS DISCONNECT DISTINCT DO DOMAIN DOUBLE DROP DYNAMIC EACH ELEMENT + ELSE ELSEIF END EPOCH EQUALS ESCAPE EXCEPT EXCEPTION EXEC EXECUTE EXISTS + EXIT EXTERNAL EXTRACT FALSE FETCH FILTER FIRST FLOAT FOR FOREIGN FOUND FREE + FROM FULL FUNCTION GENERAL GET GLOBAL GO GOTO GRANT GROUP GROUPING HANDLER + HAVING HOLD HOUR IDENTITY IF IMMEDIATE IN INDICATOR INITIALLY INNER INOUT + INPUT INSENSITIVE INSERT INT INTEGER INTERSECT INTERVAL INTO IS ISOLATION + ITERATE JOIN KEY LANGUAGE LARGE LAST LATERAL LEADING LEAVE LEFT LEVEL LIKE + LIMIT LOCAL LOCALTIME LOCALTIMESTAMP LOCATOR LOOP LOWER MAP MATCH MAX + MEMBER MERGE METHOD MIN MINUTE MODIFIES MODULE MONTH MULTISET NAMES + NATIONAL NATURAL NCHAR NCLOB NEW NEXT NO NONE NOT NULL NULLIF NUMERIC + OBJECT OCTET_LENGTH OF OLD ON ONLY OPEN OPTION OR ORDER ORDINALITY OUT + OUTER OUTPUT OVER OVERLAPS PAD PARAMETER PARTIAL PARTITION PATH POSITION + PRECISION PREPARE PRESERVE PRIMARY PRIOR PRIVILEGES PROCEDURE PUBLIC RANGE + READ READS REAL RECURSIVE REF REFERENCES REFERENCING RELATIVE RELEASE + REPEAT RESIGNAL RESTRICT RESULT RETURN RETURNS REVOKE RIGHT ROLE ROLLBACK + ROLLUP ROUTINE ROW ROWS SAVEPOINT SCHEMA SCOPE SCROLL SEARCH SECOND SECTION + SELECT SENSITIVE SESSION SESSION_USER SET SETS SIGNAL SIMILAR SIZE SMALLINT + SOME SPACE SPECIFIC SPECIFICTYPE SQL SQLCODE SQLERROR SQLEXCEPTION SQLSTATE + SQLWARNING START STATE STATIC SUBMULTISET SUBSTRING SUM SYMMETRIC SYSTEM + SYSTEM_USER TABLE TABLESAMPLE TEMPORARY TEXT THEN TIME TIMESTAMP + TIMEZONE_HOUR TIMEZONE_MINUTE TINYINT TO TRAILING TRANSACTION TRANSLATE + TRANSLATION TREAT TRIGGER TRIM TRUE UNDER UNDO UNION UNIQUE UNKNOWN UNNEST + UNTIL UPDATE UPPER USAGE USER USING VALUE VALUES VARCHAR VARYING VIEW WHEN + WHENEVER WHERE WHILE WINDOW WITH WITHIN WITHOUT WORK WRITE YEAR ZONE + ); + + sub tokenize_sql + { + my ($query, $remove_white_tokens) = @_; + + my $re = qr{ + ( + (?:--|\#)[\ \t\S]* # single line comments + | + (?:<>|<=>|>=|<=|==|=|!=|!|<<|>>|<|>|\|\||\||&&|&|-|\+|\*(?!/)|/(?!\*)|\%|~|\^|\?) + # operators and tests + | + [\[\]\(\),;.] # punctuation (parenthesis, comma) + | + \'\'(?!\') # empty single quoted string + | + \"\"(?!\"") # empty double quoted string + | + "(?>(?:(?>[^"\\]+)|""|\\.)*)+" + # anything inside double quotes, ungreedy + | + `(?>(?:(?>[^`\\]+)|``|\\.)*)+` + # anything inside backticks quotes, ungreedy + | + '(?>(?:(?>[^'\\]+)|''|\\.)*)+' + # anything inside single quotes, ungreedy. + | + /\*[\ \t\r\n\S]*?\*/ # C style comments + | + (?:[\w:@]+(?:\.(?:\w+|\*)?)*) + # words, standard named placeholders, db.table.*, db.* + | + (?: \$_\$ | \$\d+ | \${1,2} ) + # dollar expressions - eg $_$ $3 $$ + | + \n # newline + | + [\t\ ]+ # any kind of white spaces + ) +}smx; + + my @query = (); + @query = $query =~ m{$re}smxg; + + if ($remove_white_tokens) { + @query = grep(!/^[\s\n\r]*$/, @query); + } + + return wantarray ? @query : \@query; + } + + sub new + { + my ($class, %options) = @_; + + my $self = bless {%options}, $class; + + # Set some defaults. + $self->{query} = '' unless defined($self->{query}); + $self->{spaces} = 4 unless defined($self->{spaces}); + $self->{space} = ' ' unless defined($self->{space}); + $self->{break} = "\n" unless defined($self->{break}); + $self->{wrap} = {} unless defined($self->{wrap}); + $self->{keywords} = [] unless defined($self->{keywords}); + $self->{rules} = {} unless defined($self->{rules}); + $self->{uc_keywords} = 0 unless defined $self->{uc_keywords}; + + push @{$self->{keywords}}, KEYWORDS; + + # Initialize internal stuff. + $self->{_level} = 0; + + return $self; + } + + # Add more SQL. + sub add + { + my ($self, $addendum) = @_; + + $addendum =~ s/^\s*/ /; + + $self->{query} .= $addendum; + } + + # Set SQL to beautify. + sub query + { + my ($self, $query) = @_; + + $self->{query} = $query if (defined($query)); + + return $self->{query}; + } + + # Beautify SQL. + sub beautify + { + my ($self) = @_; + + $self->{_output} = ''; + $self->{_level_stack} = []; + $self->{_new_line} = 1; + + my $last; + $self->{_tokens} = [tokenize_sql($self->query, 1)]; + + while (defined(my $token = $self->_token)) { + my $rule = $self->_get_rule($token); + + # Allow custom rules to override defaults. + if ($rule) { + $self->_process_rule($rule, $token); + } + + elsif ($token eq '(') { + $self->_add_token($token); + $self->_new_line; + push @{$self->{_level_stack}}, $self->{_level}; + $self->_over unless $last and uc($last) eq 'WHERE'; + } + + elsif ($token eq ')') { + $self->_new_line; + $self->{_level} = pop(@{$self->{_level_stack}}) || 0; + $self->_add_token($token); + $self->_new_line; + } + + elsif ($token eq ',') { + $self->_add_token($token); + $self->_new_line; + } + + elsif ($token eq ';') { + $self->_add_token($token); + $self->_new_line; + + # End of statement; remove all indentation. + @{$self->{_level_stack}} = (); + $self->{_level} = 0; + } + + elsif ($token =~ /^(?:SELECT|FROM|WHERE|HAVING)$/i) { + $self->_back unless $last and $last eq '('; + $self->_new_line; + $self->_add_token($token); + $self->_new_line if ($self->_next_token and $self->_next_token ne '('); + $self->_over; + } + + elsif ($token =~ /^(?:GROUP|ORDER|LIMIT)$/i) { + $self->_back; + $self->_new_line; + $self->_add_token($token); + } + + elsif ($token =~ /^(?:BY)$/i) { + $self->_add_token($token); + $self->_new_line; + $self->_over; + } + + elsif ($token =~ /^(?:UNION|INTERSECT|EXCEPT)$/i) { + $self->_new_line; + $self->_add_token($token); + $self->_new_line; + } + + elsif ($token =~ /^(?:LEFT|RIGHT|INNER|OUTER|CROSS)$/i) { + $self->_back; + $self->_new_line; + $self->_add_token($token); + $self->_over; + } + + elsif ($token =~ /^(?:JOIN)$/i) { + if ($last and $last !~ /^(?:LEFT|RIGHT|INNER|OUTER|CROSS)$/) { + $self->_new_line; + } + + $self->_add_token($token); + } + + elsif ($token =~ /^(?:AND|OR)$/i) { + $self->_new_line; + $self->_add_token($token); + $self->_new_line; + } + + else { + $self->_add_token($token, $last); + } + + $last = $token; + } + + $self->_new_line; + + $self->{_output}; + } + + # Add a token to the beautified string. + sub _add_token + { + my ($self, $token, $last_token) = @_; + + if ($self->{wrap}) { + my $wrap; + if ($self->_is_keyword($token)) { + $wrap = $self->{wrap}->{keywords}; + } elsif ($self->_is_constant($token)) { + $wrap = $self->{wrap}->{constants}; + } + + if ($wrap) { + $token = $wrap->[0] . $token . $wrap->[1]; + } + } + + my $last_is_dot = defined($last_token) && $last_token eq '.'; + + if (!$self->_is_punctuation($token) and !$last_is_dot) { + $self->{_output} .= $self->_indent; + } + + # uppercase keywords + $token = uc $token + if $self->_is_keyword($token) + and $self->{uc_keywords}; + + $self->{_output} .= $token; + + # This can't be the beginning of a new line anymore. + $self->{_new_line} = 0; + } + + # Increase the indentation level. + sub _over + { + my ($self) = @_; + + ++$self->{_level}; + } + + # Decrease the indentation level. + sub _back + { + my ($self) = @_; + + --$self->{_level} if ($self->{_level} > 0); + } + + # Return a string of spaces according to the current indentation level and the + # spaces setting for indenting. + sub _indent + { + my ($self) = @_; + + if ($self->{_new_line}) { + return $self->{space} x ($self->{spaces} * $self->{_level}); + } else { + return $self->{space}; + } + } + + # Add a line break, but make sure there are no empty lines. + sub _new_line + { + my ($self) = @_; + + $self->{_output} .= $self->{break} unless ($self->{_new_line}); + $self->{_new_line} = 1; + } + + # Have a look at the token that's coming up next. + sub _next_token + { + my ($self) = @_; + + return @{$self->{_tokens}} ? $self->{_tokens}->[0] : undef; + } + + # Get the next token, removing it from the list of remaining tokens. + sub _token + { + my ($self) = @_; + + return shift @{$self->{_tokens}}; + } + + # Check if a token is a known SQL keyword. + sub _is_keyword + { + my ($self, $token) = @_; + + return ~~ grep {$_ eq uc($token)} @{$self->{keywords}}; + } + + # Add new keywords to highlight. + sub add_keywords + { + my $self = shift; + + for my $keyword (@_) { + push @{$self->{keywords}}, ref($keyword) ? @{$keyword} : $keyword; + } + } + + # Add new rules. + sub add_rule + { + my ($self, $format, $token) = @_; + + my $rules = $self->{rules} ||= {}; + my $group = $rules->{$format} ||= []; + + push @{$group}, ref($token) ? @{$token} : $token; + } + + # Find custom rule for a token. + sub _get_rule + { + my ($self, $token) = @_; + + values %{$self->{rules}}; # Reset iterator. + + while (my ($rule, $list) = each %{$self->{rules}}) { + return $rule if (grep {uc($token) eq uc($_)} @$list); + } + + return undef; + } + + sub _process_rule + { + my ($self, $rule, $token) = @_; + + my $format = { + break => sub {$self->_new_line}, + over => sub {$self->_over}, + back => sub {$self->_back}, + token => sub {$self->_add_token($token)}, + push => sub {push @{$self->{_level_stack}}, $self->{_level}}, + pop => sub {$self->{_level} = pop(@{$self->{_level_stack}}) || 0}, + reset => sub {$self->{_level} = 0; @{$self->{_level_stack}} = ();}, + }; + + for (split /-/, lc $rule) { + &{$format->{$_}} if ($format->{$_}); + } + } + + # Check if a token is a constant. + sub _is_constant + { + my ($self, $token) = @_; + + return ($token =~ /^\d+$/ or $token =~ /^(['"`]).*\1$/); + } + + # Check if a token is punctuation. + sub _is_punctuation + { + my ($self, $token) = @_; + return ($token =~ /^[,;.]$/); + } + +} + __DATA__