File Coverage

blib/lib/SQL/Beautify.pm
Criterion Covered Total %
statement 145 162 89.5
branch 65 78 83.3
condition 21 32 65.6
subroutine 27 30 90.0
pod 6 6 100.0
total 264 308 85.7


line stmt bran cond sub pod time code
1              
2             package SQL::Beautify;
3              
4 8     8   267952 use strict;
  8         22  
  8         341  
5 8     8   46 use warnings;
  8         19  
  8         351  
6              
7             our $VERSION = 0.04;
8              
9 8     8   7077 use SQL::Tokenizer;
  8         14630  
  8         459  
10 8     8   81 use Carp;
  8         17  
  8         2024  
11              
12              
13             # Keywords from SQL-92, SQL-99 and SQL-2003.
14 8         18835 use constant KEYWORDS => qw(
15             ABSOLUTE ACTION ADD AFTER ALL ALLOCATE ALTER AND ANY ARE ARRAY AS ASC
16             ASENSITIVE ASSERTION ASYMMETRIC AT ATOMIC AUTHORIZATION AVG BEFORE BEGIN
17             BETWEEN BIGINT BINARY BIT BIT_LENGTH BLOB BOOLEAN BOTH BREADTH BY CALL
18             CALLED CASCADE CASCADED CASE CAST CATALOG CHAR CHARACTER CHARACTER_LENGTH
19             CHAR_LENGTH CHECK CLOB CLOSE COALESCE COLLATE COLLATION COLUMN COMMIT
20             CONDITION CONNECT CONNECTION CONSTRAINT CONSTRAINTS CONSTRUCTOR CONTAINS
21             CONTINUE CONVERT CORRESPONDING COUNT CREATE CROSS CUBE CURRENT CURRENT_DATE
22             CURRENT_DEFAULT_TRANSFORM_GROUP CURRENT_PATH CURRENT_ROLE CURRENT_TIME
23             CURRENT_TIMESTAMP CURRENT_TRANSFORM_GROUP_FOR_TYPE CURRENT_USER CURSOR
24             CYCLE DATA DATE DAY DEALLOCATE DEC DECIMAL DECLARE DEFAULT DEFERRABLE
25             DEFERRED DELETE DEPTH DEREF DESC DESCRIBE DESCRIPTOR DETERMINISTIC
26             DIAGNOSTICS DISCONNECT DISTINCT DO DOMAIN DOUBLE DROP DYNAMIC EACH ELEMENT
27             ELSE ELSEIF END EPOCH EQUALS ESCAPE EXCEPT EXCEPTION EXEC EXECUTE EXISTS
28             EXIT EXTERNAL EXTRACT FALSE FETCH FILTER FIRST FLOAT FOR FOREIGN FOUND FREE
29             FROM FULL FUNCTION GENERAL GET GLOBAL GO GOTO GRANT GROUP GROUPING HANDLER
30             HAVING HOLD HOUR IDENTITY IF IMMEDIATE IN INDICATOR INITIALLY INNER INOUT
31             INPUT INSENSITIVE INSERT INT INTEGER INTERSECT INTERVAL INTO IS ISOLATION
32             ITERATE JOIN KEY LANGUAGE LARGE LAST LATERAL LEADING LEAVE LEFT LEVEL LIKE
33             LIMIT LOCAL LOCALTIME LOCALTIMESTAMP LOCATOR LOOP LOWER MAP MATCH MAX
34             MEMBER MERGE METHOD MIN MINUTE MODIFIES MODULE MONTH MULTISET NAMES
35             NATIONAL NATURAL NCHAR NCLOB NEW NEXT NO NONE NOT NULL NULLIF NUMERIC
36             OBJECT OCTET_LENGTH OF OLD ON ONLY OPEN OPTION OR ORDER ORDINALITY OUT
37             OUTER OUTPUT OVER OVERLAPS PAD PARAMETER PARTIAL PARTITION PATH POSITION
38             PRECISION PREPARE PRESERVE PRIMARY PRIOR PRIVILEGES PROCEDURE PUBLIC RANGE
39             READ READS REAL RECURSIVE REF REFERENCES REFERENCING RELATIVE RELEASE
40             REPEAT RESIGNAL RESTRICT RESULT RETURN RETURNS REVOKE RIGHT ROLE ROLLBACK
41             ROLLUP ROUTINE ROW ROWS SAVEPOINT SCHEMA SCOPE SCROLL SEARCH SECOND SECTION
42             SELECT SENSITIVE SESSION SESSION_USER SET SETS SIGNAL SIMILAR SIZE SMALLINT
43             SOME SPACE SPECIFIC SPECIFICTYPE SQL SQLCODE SQLERROR SQLEXCEPTION SQLSTATE
44             SQLWARNING START STATE STATIC SUBMULTISET SUBSTRING SUM SYMMETRIC SYSTEM
45             SYSTEM_USER TABLE TABLESAMPLE TEMPORARY TEXT THEN TIME TIMESTAMP
46             TIMEZONE_HOUR TIMEZONE_MINUTE TINYINT TO TRAILING TRANSACTION TRANSLATE
47             TRANSLATION TREAT TRIGGER TRIM TRUE UNDER UNDO UNION UNIQUE UNKNOWN UNNEST
48             UNTIL UPDATE UPPER USAGE USER USING VALUE VALUES VARCHAR VARYING VIEW WHEN
49             WHENEVER WHERE WHILE WINDOW WITH WITHIN WITHOUT WORK WRITE YEAR ZONE
50 8     8   47 );
  8         18  
51              
52              
53             sub new {
54 8     8 1 179 my ($class, %options) = @_;
55              
56 8         60 my $self = bless { %options }, $class;
57              
58             # Set some defaults.
59 8 50       92 $self->{query} = '' unless defined($self->{query});
60 8 50       38 $self->{spaces} = 4 unless defined($self->{spaces});
61 8 100       57 $self->{space} = ' ' unless defined($self->{space});
62 8 100       43 $self->{break} = "\n" unless defined($self->{break});
63 8 100       37 $self->{wrap} = {} unless defined($self->{wrap});
64 8 100       34 $self->{keywords} = [] unless defined($self->{keywords});
65 8 50       42 $self->{rules} = {} unless defined($self->{rules});
66 8 100       42 $self->{uc_keywords} = 0 unless defined $self->{uc_keywords};
67              
68 8         16 push @{$self->{keywords}}, KEYWORDS;
  8         559  
69              
70             # Initialize internal stuff.
71 8         27 $self->{_level} = 0;
72              
73 8         35 return $self;
74             }
75              
76              
77             # Add more SQL.
78             sub add {
79 0     0 1 0 my ($self, $addendum) = @_;
80              
81 0         0 $addendum =~ s/^\s*/ /;
82              
83 0         0 $self->{query} .= $addendum;
84             }
85              
86              
87             # Set SQL to beautify.
88             sub query {
89 36     36 1 7398 my ($self, $query) = @_;
90              
91 36 100       118 $self->{query} = $query if(defined($query));
92              
93 36         206 return $self->{query};
94             }
95              
96              
97             # Beautify SQL.
98             sub beautify {
99 12     12 1 32 my ($self) = @_;
100              
101 12         34 $self->{_output} = '';
102 12         42 $self->{_level_stack} = [];
103 12         29 $self->{_new_line} = 1;
104              
105 12         22 my $last;
106              
107 12         43 $self->{_tokens} = [ SQL::Tokenizer->tokenize($self->query, 1) ];
108              
109 12         1626 while(defined(my $token = $self->_token)) {
110 287         655 my $rule = $self->_get_rule($token);
111              
112             # Allow custom rules to override defaults.
113 287 100       3390 if($rule) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    50          
    50          
    100          
114 14         37 $self->_process_rule($rule, $token);
115             }
116              
117             elsif($token eq '(') {
118 12         36 $self->_add_token($token);
119 12         34 $self->_new_line;
120 12         16 push @{$self->{_level_stack}}, $self->{_level};
  12         41  
121 12 50 33     72 $self->_over unless $last and uc($last) eq 'WHERE';
122             }
123              
124             elsif($token eq ')') {
125 12         29 $self->_new_line;
126 12   50     14 $self->{_level} = pop(@{$self->{_level_stack}}) || 0;
127 12         27 $self->_add_token($token);
128 12         26 $self->_new_line;
129             }
130              
131             elsif($token eq ',') {
132 18         51 $self->_add_token($token);
133 18         52 $self->_new_line;
134             }
135              
136             elsif($token eq ';') {
137 3         42 $self->_add_token($token);
138 3         9 $self->_new_line;
139              
140             # End of statement; remove all indentation.
141 3         4 @{$self->{_level_stack}} = ();
  3         8  
142 3         7 $self->{_level} = 0;
143             }
144              
145             elsif($token =~ /^(?:SELECT|FROM|WHERE|HAVING)$/i) {
146 49 100 100     1448 $self->_back unless $last and $last eq '(';
147 49         120 $self->_new_line;
148 49         113 $self->_add_token($token);
149 49 100 66     136 $self->_new_line if($self->_next_token and $self->_next_token ne '(');
150 49         137 $self->_over;
151             }
152              
153             elsif($token =~ /^(?:GROUP|ORDER|LIMIT)$/i) {
154 2         7 $self->_back;
155 2         5 $self->_new_line;
156 2         5 $self->_add_token($token);
157             }
158              
159             elsif($token =~ /^(?:BY)$/i) {
160 1         6 $self->_add_token($token);
161 1         3 $self->_new_line;
162 1         3 $self->_over;
163             }
164              
165             elsif($token =~ /^(?:UNION|INTERSECT|EXCEPT)$/i) {
166 0         0 $self->_new_line;
167 0         0 $self->_add_token($token);
168 0         0 $self->_new_line;
169             }
170              
171             elsif($token =~ /^(?:LEFT|RIGHT|INNER|OUTER|CROSS)$/i) {
172 0         0 $self->_back;
173 0         0 $self->_new_line;
174 0         0 $self->_add_token($token);
175 0         0 $self->_over;
176             }
177              
178             elsif($token =~ /^(?:JOIN)$/i) {
179 0 0 0     0 if($last and $last !~ /^(?:LEFT|RIGHT|INNER|OUTER|CROSS)$/) {
180 0         0 $self->_new_line;
181             }
182              
183 0         0 $self->_add_token($token);
184             }
185              
186             elsif($token =~ /^(?:AND|OR)$/i) {
187 10         34 $self->_new_line;
188 10         26 $self->_add_token($token);
189 10         29 $self->_new_line;
190             }
191              
192             else {
193 166         366 $self->_add_token($token, $last);
194             }
195              
196 287         1231 $last = $token;
197             }
198              
199 12         45 $self->_new_line;
200              
201 12         126 $self->{_output};
202             }
203              
204              
205             # Add a token to the beautified string.
206             sub _add_token {
207 287     287   448 my ($self, $token, $last_token) = @_;
208              
209 287 50       671 if($self->{wrap}) {
210 287         330 my $wrap;
211              
212 287 100       519 if($self->_is_keyword($token)) {
    100          
213 79         171 $wrap = $self->{wrap}->{keywords};
214             }
215             elsif($self->_is_constant($token)) {
216 40         87 $wrap = $self->{wrap}->{constants};
217             }
218              
219 287 100       738 if($wrap) {
220 10         51 $token = $wrap->[0] . $token . $wrap->[1];
221             }
222             }
223              
224 287   100     1434 my $last_is_dot =
225             defined($last_token) && $last_token eq '.';
226              
227 287 100 66     616 if(!$self->_is_punctuation($token) and !$last_is_dot) {
228 245         621 $self->{_output} .= $self->_indent;
229             }
230              
231             # uppercase keywords
232 287 100 100     634 $token = uc $token
233             if $self->_is_keyword($token) and $self->{uc_keywords};
234              
235 287         579 $self->{_output} .= $token;
236              
237             # This can't be the beginning of a new line anymore.
238 287         594 $self->{_new_line} = 0;
239             }
240              
241              
242             # Increase the indentation level.
243             sub _over {
244 63     63   88 my ($self) = @_;
245              
246 63         147 ++$self->{_level};
247             }
248              
249              
250             # Decrease the indentation level.
251             sub _back {
252 47     47   670 my ($self) = @_;
253              
254 47 100       1333 --$self->{_level} if($self->{_level} > 0);
255             }
256              
257              
258             # Return a string of spaces according to the current indentation level and the
259             # spaces setting for indenting.
260             sub _indent {
261 245     245   542 my ($self) = @_;
262              
263 245 100       564 if($self->{_new_line}) {
264 170         695 return $self->{space} x ($self->{spaces} * $self->{_level});
265             }
266             else {
267 75         195 return $self->{space};
268             }
269             }
270              
271              
272             # Add a line break, but make sure there are no empty lines.
273             sub _new_line {
274 202     202   909 my ($self) = @_;
275              
276 202 100       588 $self->{_output} .= $self->{break} unless($self->{_new_line});
277 202         433 $self->{_new_line} = 1;
278             }
279              
280              
281             # Have a look at the token that's coming up next.
282             sub _next_token {
283 98     98   136 my ($self) = @_;
284              
285 98 50       103 return @{$self->{_tokens}} ? $self->{_tokens}->[0] : undef;
  98         581  
286             }
287              
288              
289             # Get the next token, removing it from the list of remaining tokens.
290             sub _token {
291 299     299   404 my ($self) = @_;
292              
293 299         300 return shift @{$self->{_tokens}};
  299         1106  
294             }
295              
296              
297             # Check if a token is a known SQL keyword.
298             sub _is_keyword {
299 574     574   730 my ($self, $token) = @_;
300              
301 574         632 return ~~ grep { $_ eq uc($token) } @{$self->{keywords}};
  195754         304223  
  574         1503  
302             }
303              
304              
305             # Add new keywords to highlight.
306             sub add_keywords {
307 1     1 1 2 my $self = shift;
308              
309 1         3 for my $keyword (@_) {
310 1 50       2 push @{$self->{keywords}}, ref($keyword) ? @{$keyword} : $keyword;
  1         6  
  0         0  
311             }
312             }
313              
314              
315             # Add new rules.
316             sub add_rule {
317 7     7 1 814 my ($self, $format, $token) = @_;
318              
319 7   50     44 my $rules = $self->{rules} ||= {};
320 7   100     35 my $group = $rules->{$format} ||= [];
321              
322 7 100       7 push @{$group}, ref($token) ? @{$token} : $token;
  7         21  
  4         22  
323             }
324              
325              
326             # Find custom rule for a token.
327             sub _get_rule {
328 287     287   395 my ($self, $token) = @_;
329              
330 287         283 values %{$self->{rules}}; # Reset iterator.
  287         494  
331              
332 287         377 while(my ($rule, $list) = each %{$self->{rules}}) {
  805         2332  
333 532 100       754 return $rule if(grep { uc($token) eq uc($_) } @$list);
  1412         3865  
334             }
335              
336 273         443 return undef;
337             }
338              
339              
340             sub _process_rule {
341 14     14   22 my ($self, $rule, $token) = @_;
342              
343             my $format = {
344 16     16   41 break => sub { $self->_new_line },
345 1     1   3 over => sub { $self->_over },
346 2     2   5 back => sub { $self->_back },
347 14     14   29 token => sub { $self->_add_token($token) },
348 0     0   0 push => sub { push @{$self->{_level_stack}}, $self->{_level} },
  0         0  
349 0   0 0   0 pop => sub { $self->{_level} = pop(@{$self->{_level_stack}}) || 0 },
350 2     2   3 reset => sub { $self->{_level} = 0; @{$self->{_level_stack}} = (); },
  2         3  
  2         23  
351 14         209 };
352              
353 14         64 for(split /-/, lc $rule) {
354 35 50       88 &{$format->{$_}} if($format->{$_});
  35         71  
355             }
356             }
357              
358              
359             # Check if a token is a constant.
360             sub _is_constant {
361 208     208   394 my ($self, $token) = @_;
362              
363 208   100     1808 return ($token =~ /^\d+$/ or $token =~ /^(['"`]).*\1$/);
364             }
365              
366              
367             # Check if a token is punctuation.
368             sub _is_punctuation {
369 287     287   420 my ($self, $token) = @_;
370              
371 287         1608 return ($token =~ /^[,;.]$/);
372             }
373              
374              
375             1
376              
377             __END__