File Coverage

blib/lib/Mysql/PrettyPrinter.pm
Criterion Covered Total %
statement 6 163 3.6
branch 0 108 0.0
condition 0 40 0.0
subroutine 2 19 10.5
pod 9 9 100.0
total 17 339 5.0


line stmt bran cond sub pod time code
1             package Mysql::PrettyPrinter;
2              
3 1     1   22056 use strict;
  1         2  
  1         34  
4 1     1   5 use warnings;
  1         2  
  1         3707  
5              
6             our $VERSION = 0.10;
7              
8             # Knowledge of MySQL keywords is stored in class variables to aid efficiency in
9             # persistent environments (eg mod_perl)
10             my $Keywords = [];
11             # First-level keywords; outdent
12             $Keywords->[0] = [ qw(
13             ALTER CHANGE CREATE DELETE DROP FROM GRANT GROUP HAVING INSERT LIMIT MODIFY
14             ORDER SELECT SET SHOW UNION UPDATE WHERE
15             ) ];
16             # Second-level keywords; prepend a linebreak
17             $Keywords->[1] = [ qw(
18             CROSS INNER JOIN LEFT OUTER RIGHT
19             ELSE ELSEIF THEN WHEN
20             ) ];
21             # Third-level keywords; prepend a linebreak if within a join
22             $Keywords->[2] = [ qw(
23             AND OR
24             ) ];
25             # Other keywords; no intention to treat specially
26             $Keywords->[3] = [ qw(
27             ACCESSIBLE ADD ALL ANALYZE AS ASC ASENSITIVE BEFORE BETWEEN BIGINT BINARY
28             BLOB BOTH BY CALL CASCADE CASE CHAR CHARACTER CHECK COLLATE COLUMN
29             CONDITION CONNECTION CONSTRAINT CONTINUE CONVERT CURRENT_DATE CURRENT_TIME
30             CURRENT_TIMESTAMP CURRENT_USER CURSOR DATABASE DATABASES DAY_HOUR
31             DAY_MICROSECOND DAY_MINUTE DAY_SECOND DEC DECIMAL DECLARE DEFAULT DELAYED
32             DESC DESCRIBE DETERMINISTIC DISTINCT DISTINCTROW DIV DOUBLE DUAL EACH
33             ELSEIF ENCLOSED ESCAPED EXISTS EXIT EXPLAIN FALSE FETCH FLOAT FLOAT4 FLOAT8
34             FOR FORCE FOREIGN FULLTEXT GOTO HIGH_PRIORITY HOUR_MICROSECOND HOUR_MINUTE
35             HOUR_SECOND IF IGNORE IN INDEX INFILE INOUT INSENSITIVE INT INT1 INT2 INT3
36             INT4 INT8 INTEGER INTERVAL INTO IS ITERATE KEY KEYS KILL LABEL LEADING
37             LEAVE LIKE LIMIT LINEAR LINES LOAD LOCALTIME LOCALTIMESTAMP LOCK LONG
38             LONGBLOB LONGTEXT LOOP LOW_PRIORITY MASTER_SSL_VERIFY_SERVER_CERT MATCH
39             MEDIUMBLOB MEDIUMINT MEDIUMTEXT MIDDLEINT MINUTE_MICROSECOND MINUTE_SECOND
40             MOD MODIFIES NATURAL NOT NO_WRITE_TO_BINLOG NULL NUMERIC ON OPTIMIZE OPTION
41             OPTIONALLY OUT OUTFILE PRECISION PRIMARY PROCEDURE PURGE RANGE READ
42             READ_ONLY READS READ_WRITE REAL REFERENCES REGEXP RELEASE RENAME REPEAT
43             REPLACE REQUIRE RESTRICT RETURN REVOKE RLIKE SCHEMA SCHEMAS
44             SECOND_MICROSECOND SENSITIVE SEPARATOR SHOW SMALLINT SPATIAL SPECIFIC SQL
45             SQL_BIG_RESULT SQL_CALC_FOUND_ROWS SQLEXCEPTION SQL_SMALL_RESULT SQLSTATE
46             SQLWARNING SSL STARTING STRAIGHT_JOIN TABLE TABLES TEMPORARY TERMINATED
47             TINYBLOB TINYINT TINYTEXT TO TRAILING TRIGGER TRUE UNDO UNIQUE UNLOCK
48             UNSIGNED UPGRADE USAGE USE USING UTC_DATE UTC_TIME UTC_TIMESTAMP VALUES
49             VARBINARY VARCHAR VARCHARACTER VARYING WHILE WITH WRITE XOR YEAR_MONTH
50             ZEROFILL
51             ) ];
52             my $Keyword = { map { $_ => 4 } @{$Keywords->[3]} };
53             foreach my $n (1..3) {
54             %$Keyword = (%$Keyword, map { $_ => $n } @{$Keywords->[$n - 1]});
55             }
56              
57             sub new {
58 0     0 1   my ($class, %param) = @_;
59 0           return bless {
60             space => ' ',
61             break => "\n",
62             indent => ' ',
63             wrap => undef,
64             sql => '',
65             tokens => [],
66             _level => 0,
67             _pending => 1,
68             %param
69             }, $class;
70             }
71              
72             sub sql {
73 0     0 1   my ($self, $sql) = @_;
74 0 0         if (defined($sql)) {
75             # Setter
76 0           $self->{sql} = $sql;
77 0           $self->{_pending} = 1;
78 0           return $self;
79             }
80             else {
81             # Getter
82 0           return $self->{sql};
83             }
84             }
85              
86             sub add_sql {
87 0     0 1   my ($self, $sql) = @_;
88 0 0         $sql =~ s/^\s*/ / if length $self->{sql};
89 0           $self->{sql} .= $sql;
90 0           $self->{_pending} = 1;
91 0           return $self;
92             }
93              
94             sub make_tokens {
95 0     0 1   my ($self, %param) = @_;
96 0 0         if (%param) {
97 0           %$self = ( %$self, %param );
98             }
99 0 0         if (exists $param{sql}) {
100 0           $self->{_pending} = 1;
101             }
102 0 0 0       if ($self->{_pending} and length $self->{sql}) {
103 0           @{ $self->{tokens} } = $self->lexicals($self->{sql}, 1);
  0            
104 0           $self->{_pending} = 0;
105             }
106 0           return $self;
107             }
108              
109             sub tokens {
110 0     0 1   my ($self, @toks) = @_;
111 0 0         if (scalar @toks) {
112             # Setter
113 0           @{ $self->{sql} } = @toks;
  0            
114 0           $self->{_pending} = 0;
115 0           return $self;
116             }
117             else {
118             # Getter
119 0 0         if ($self->{_pending}) {
120 0           warn "Probable data conflict; suspicious invocation sequence";
121             }
122 0 0         return wantarray ? @{$self->{tokens}} : $self->{tokens};
  0            
123             }
124             }
125              
126             sub add_tokens {
127 0     0 1   my ($self, @toks) = @_;
128 0 0         if ($self->{_pending}) {
129 0           warn "Probable data conflict; suspicious invocation sequence";
130             }
131 0           push(@{$self->{tokens}}, @toks);
  0            
132 0           $self->{_pending} = 0;
133 0           return $self;
134             }
135              
136             sub format {
137 0     0 1   my $self = shift;
138 0 0         unless (ref $self) {
    0          
139             # Shortcut used; need to start from scratch
140 0           $self = $self->new(@_);
141 0           $self->make_tokens;
142             }
143             elsif ($self->{_pending}) {
144             # SQL waiting to be tokenised
145 0           $self->make_tokens(@_);
146             }
147              
148 0           $self->{_output} = ''; # Ultimate output
149 0           $self->{_levels} = []; # Nested levels
150 0           $self->{_blank_line} = 1; # Whether in a blank line
151 0           $self->{_previous} = ''; # Previous token
152 0           $self->{_joining} = 0; # Whether in a compound join
153 0           $self->{_conditioning} = 0; # Whether in a conditional
154              
155 0           while (defined(my $token = shift @{$self->{tokens}} )) {
  0            
156             # Some preprocessing of token
157 0 0         if ($self->_is_keyword(uc $token)) {
    0          
    0          
158             # Keyword => uppercase
159 0           $token = uc $token;
160             }
161             elsif ($token =~ /^[,.;\(\)]$/) {
162             # Punctuation
163             ; # nothing
164             }
165             elsif ($self->{_pending_nl}) {
166             # Non-keyword/punctuation
167 0           $self->_new_line;
168 0           $self->{_pending_nl} = 0;
169             }
170              
171             # Build output
172 0 0 0       if ($token eq '(') {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
173 0           push @{ $self->{_levels} }, $self->{_level};
  0            
174 0           $self->_add_token($token)->_new_line->_over;
175             }
176             elsif ($token eq ')') {
177 0   0       $self->{_level} = pop(@{ $self->{_levels} }) || 0;
178 0           $self->_new_line->_add_token($token);
179 0 0 0       $self->_new_line
180             unless uc($self->_next_token) eq 'AS'
181             || $self->_next_token eq ',';
182             }
183             elsif ($token eq ',') {
184 0           $self->_add_token($token)->_new_line;
185             }
186             elsif ($token eq ';') {
187 0           $self->_add_token($token)->_new_line;
188             # End of statement; remove all indentation
189 0           @{ $self->{_levels} } = ();
  0            
190 0           $self->{_level} = 0;
191             }
192             elsif ($token eq 'UNION') {
193             # End of statement; remove all indentation
194 0           @{ $self->{_levels} } = ();
  0            
195 0           $self->{_level} = 0;
196 0           $self->_new_line->_add_token($token, 'K')->_new_line;
197             }
198             elsif ($token eq 'JOIN') {
199 0 0         unless ($self->_is_keyword($self->{_previous})) {
200 0           $self->_new_line;
201             }
202 0           $self->_add_token($token, 'K');
203             }
204             elsif ($token eq 'ON') {
205 0           $self->_add_token($token, 'K');
206 0 0         if ($self->_is_keyword($self->_next_keyword) == 3) {
207 0           $self->_new_line->_over;
208 0           $self->{_joining} = 1;
209             }
210             }
211             elsif ($token eq 'CASE') {
212 0           $self->_add_token($token, 'K')->_over;
213 0           $self->{_conditioning} = 1;
214             }
215             elsif ($token eq 'END' and $self->{_conditioning}) {
216 0           $self->_back->_new_line->_add_token($token, 'K');
217 0           $self->{_conditioning} = 0;
218             }
219             elsif ($self->_is_keyword($token) == 1) {
220             # First-level keyword
221 0 0         $self->_back unless $self->{_previous} eq '(';
222 0 0         if ($self->{_joining}) {
223 0           $self->_back;
224 0           $self->{_joining} = 0;
225             }
226 0           $self->_new_line->_add_token($token, 'K')->_over;
227 0           $self->{_pending_nl} = 1;
228             }
229             elsif ($self->_is_keyword($token) == 2) {
230             # Second-level keyword
231 0 0         if ($self->{_joining}) {
232 0           $self->_back;
233 0           $self->{_joining} = 0;
234             }
235 0           $self->_new_line->_add_token($token, 'K');
236             }
237             elsif ($self->_is_keyword($token) == 3) {
238             # Third-level keyword
239 0           $self->_new_line->_add_token($token, 'K');
240             }
241             elsif ($self->_is_keyword($token)) {
242             # Other keyword
243 0           $self->_add_token($token, 'K');
244             }
245             elsif ($token =~ /^"[^"']*"$/) {
246             # Quoted string
247 0           $token =~ s/"/'/g;
248 0           $self->_add_token($token, 'L');
249             }
250             elsif ($token =~ /^'.*'$/) {
251             # Quoted string
252 0           $self->_add_token($token, 'L');
253             }
254             elsif ($token =~ /^\d+$/) {
255             # Number
256 0           $self->_add_token($token, 'L');
257             }
258             elsif ($self->_next_token eq '(') {
259 0           $token = lc $token;
260 0           $self->_add_token($token, 'F');
261             }
262             else {
263 0           $self->_add_token($token);
264             }
265             #TODO: Identify comments
266              
267 0           $self->{_previous} = $token;
268             }
269              
270 0           $self->_new_line;
271 0           return $self->{_output};
272             }
273              
274             # Add a token to the formatted string.
275             sub _add_token {
276 0     0     my ($self, $token, $type) = @_;
277 0   0       $type ||= '';
278              
279 0 0         if ($self->{wrap}) {
280             # Format wrapping of keywords, etc
281 0 0 0       if ($type eq 'K' and exists $self->{wrap}->{keyword}) {
    0 0        
    0 0        
    0 0        
282             # Keyword
283 0           $token = $self->{wrap}->{keyword}->[0]
284             . $token
285             . $self->{wrap}->{keyword}->[1];
286             }
287             elsif ($type eq 'F' and exists $self->{wrap}->{function}) {
288             # Function
289 0           $token = $self->{wrap}->{function}->[0]
290             . $token
291             . $self->{wrap}->{function}->[1];
292             }
293             elsif ($type eq 'L' and exists $self->{wrap}->{literal}) {
294             # Literal
295 0           $token = $self->{wrap}->{literal}->[0]
296             . $token
297             . $self->{wrap}->{literal}->[1];
298             }
299             elsif ($type eq 'C' and exists $self->{wrap}->{comment}) {
300             # Comment
301 0           $token = $self->{wrap}->{comment}->[0]
302             . $token
303             . $self->{wrap}->{comment}->[1];
304             }
305             }
306              
307 0 0 0       if ($token =~ /^[,.;]$/ or $self->{_previous} eq '.') {
    0 0        
      0        
308             # Punctuation => no indent
309             ;
310             }
311             elsif ($token eq '('
312             and not $self->_is_keyword($self->{_previous})
313             and not $self->{_previous} eq ',') {
314             # Function => no indent
315             ;
316             }
317             else {
318 0           $self->{_output} .= $self->_indent;
319             }
320              
321 0           $self->{_output} .= $token;
322              
323             # This can't be the beginning of a new line anymore.
324 0           $self->{_blank_line} = 0;
325 0           return $self;
326             }
327              
328             # Increase the indentation level.
329             sub _over {
330 0     0     my ($self) = @_;
331 0           ++$self->{_level};
332 0           return $self;
333             }
334              
335             # Decrease the indentation level.
336             sub _back {
337 0     0     my ($self) = @_;
338 0 0         --$self->{_level} if $self->{_level} > 0;
339 0           return $self;
340             }
341              
342             # Return a string of spaces according to the current indentation level and the
343             # spaces setting for indenting.
344             sub _indent {
345 0     0     my ($self) = @_;
346 0 0         if ($self->{_blank_line}) {
347 0           return $self->{indent} x $self->{_level};
348             }
349             else {
350 0           return $self->{space};
351             }
352             }
353              
354             # Add a line break, but make sure there are no empty lines.
355             sub _new_line {
356 0     0     my ($self) = @_;
357 0 0         $self->{_output} .= $self->{break} unless $self->{_blank_line};
358 0           $self->{_blank_line} = 1;
359 0           return $self;
360             }
361              
362 0 0   0     sub _next_token { scalar @{ $_[0]->{tokens} } ? $_[0]->{tokens}->[0] : '' }
  0            
363              
364             sub _next_keyword {
365 0     0     my ($self) = @_;
366 0           my $len = scalar @{ $self->{tokens} };
  0            
367 0           for (my $i = 0; $i < $len; $i++) {
368 0 0         if ($self->_is_keyword(uc $self->{tokens}->[$i])) {
369 0           return(uc $self->{tokens}->[$i]);
370             }
371             }
372 0           return '';
373             }
374              
375 0 0 0 0     sub _is_keyword { defined $_[1] && exists $Keyword->{$_[1]} ? $Keyword->{$_[1]} : 0 }
376              
377             sub lexicals {
378 0     0 1   my ($proto, $sql, $omit_whitespace_tokens) = @_;
379 0           my @tokens = $sql =~ m{
380             (?:--|\#)[\ \t\S]* # single line comments
381             |
382             (?:<>|<=>|>=|<=|==|=|!=|!|<<|>>|<|>|\|\||\||&&|&|-|\+|\*(?!/)|/(?!\*)|\%|~|\^|\?)
383             # operators and tests
384             |
385             [\[\]\(\),;.] # punctuation (parenthesis, comma)
386             |
387             \'\'(?!\') # empty single quoted string
388             |
389             \"\"(?!\"") # empty double quoted string
390             |
391             ".*?(?:(?:""){1,}"|(?
392             # anything inside double quotes, ungreedy
393             |
394             `.*?(?:(?:``){1,}`|(?
395             # anything inside backticks quotes, ungreedy
396             |
397             '.*?(?:(?:''){1,}'|(?
398             # anything inside single quotes, ungreedy.
399             |
400             /\*[\ \t\n\S]*?\*/ # C style comments
401             |
402             (?:[\w:@]+(?:\.(?:\w+|\*)?)*)
403             # words, standard named placeholders, db.table.*, db.*
404             |
405             (?:\${1,2}) # dollars
406             |
407             [\t\ ]+ # any kind of white spaces
408             }mxg;
409              
410 0 0         @tokens = grep(!/^[\s\n\r]*$/, @tokens) if $omit_whitespace_tokens;
411 0 0         return wantarray ? @tokens : \@tokens;
412             }
413              
414             sub passthrough {
415 0     0 1   my ($class, @args) = @_;
416 0           my %param;
417 0 0         if (ref($class)) {
418 0           die "'passthrough' is a class method";
419             }
420 0           eval {
421 0   0       %param = @args || ();
422             };
423 0 0         if ($@) {
424 0           die "Options should be passed in hash style\n". $@;
425             }
426 0           local($/) = '';
427 0           print $class->format(sql => <>);
428             }
429              
430             1;
431             __END__