Lose a couple of oddball dependencies (while moronizing the tests a bit)
[scpubgit/Q-Branch.git] / lib / SQL / Abstract / Test.pm
CommitLineData
fffe6900 1package SQL::Abstract::Test; # see doc at end of file
2
3use strict;
4use warnings;
5aad8cf3 5use base qw/Test::Builder::Module Exporter/;
fffe6900 6use Data::Dumper;
7use Carp;
4abea32b 8use Test::Builder;
fffe6900 9
e7827ba2 10our @EXPORT_OK = qw/&is_same_sql_bind &is_same_sql &is_same_bind
11 &eq_sql_bind &eq_sql &eq_bind
fffe6900 12 $case_sensitive $sql_differ/;
13
14our $case_sensitive = 0;
e40f5df9 15our $parenthesis_significant = 0;
fffe6900 16our $sql_differ; # keeps track of differing portion between SQLs
5aad8cf3 17our $tb = __PACKAGE__->builder;
fffe6900 18
25823711 19# Parser states for _recurse_parse()
01b64cb7 20use constant PARSE_TOP_LEVEL => 0;
5221d7fc 21use constant PARSE_IN_EXPR => 1;
22use constant PARSE_IN_PARENS => 2;
01b64cb7 23use constant PARSE_RHS => 3;
25823711 24
25# These SQL keywords always signal end of the current expression (except inside
26# of a parenthesized subexpression).
27# Format: A list of strings that will be compiled to extended syntax (ie.
28# /.../x) regexes, without capturing parentheses. They will be automatically
29# anchored to word boundaries to match the whole token).
30my @expression_terminator_sql_keywords = (
1b17d1b0 31 'SELECT',
25823711 32 'FROM',
33 '(?:
34 (?:
35 (?: \b (?: LEFT | RIGHT | FULL ) \s+ )?
36 (?: \b (?: CROSS | INNER | OUTER ) \s+ )?
37 )?
38 JOIN
39 )',
40 'ON',
41 'WHERE',
b9a4fdae 42 'EXISTS',
25823711 43 'GROUP \s+ BY',
44 'HAVING',
45 'ORDER \s+ BY',
46 'LIMIT',
47 'OFFSET',
48 'FOR',
49 'UNION',
50 'INTERSECT',
51 'EXCEPT',
02288357 52 'RETURNING',
25823711 53);
54
01b64cb7 55# These are binary operator keywords always a single LHS and RHS
56# * AND/OR are handled separately as they are N-ary
9e8dab3f 57# * so is NOT as being unary
01b64cb7 58# * BETWEEN without paranthesis around the ANDed arguments (which
59# makes it a non-binary op) is detected and accomodated in
60# _recurse_parse()
b9a4fdae 61my $stuff_around_mathops = qr/[\w\s\`\'\"\)]/;
01b64cb7 62my @binary_op_keywords = (
30d09fa9 63 ( map
b9a4fdae 64 {
65 ' ^ ' . quotemeta ($_) . "(?= \$ | $stuff_around_mathops ) ",
66 " (?<= $stuff_around_mathops)" . quotemeta ($_) . "(?= \$ | $stuff_around_mathops ) ",
67 }
68 (qw/< > != <> = <= >=/)
30d09fa9 69 ),
70 ( map
71 { '\b (?: NOT \s+)?' . $_ . '\b' }
72 (qw/IN BETWEEN LIKE/)
73 ),
01b64cb7 74);
75
76my $tokenizer_re_str = join("\n\t|\n",
9e8dab3f 77 ( map { '\b' . $_ . '\b' } @expression_terminator_sql_keywords, 'AND', 'OR', 'NOT'),
30d09fa9 78 @binary_op_keywords,
01b64cb7 79);
80
b9a4fdae 81my $tokenizer_re = qr/ \s* ( $tokenizer_re_str | \( | \) | \? ) \s* /xi;
01b64cb7 82
35149895 83# All of these keywords allow their parameters to be specified with or without parenthesis without changing the semantics
01b64cb7 84my @unrollable_ops = (
1b17d1b0 85 'ON',
86 'WHERE',
87 'GROUP \s+ BY',
88 'HAVING',
89 'ORDER \s+ BY',
90);
91
fffe6900 92sub is_same_sql_bind {
93 my ($sql1, $bind_ref1, $sql2, $bind_ref2, $msg) = @_;
94
95 # compare
25823711 96 my $same_sql = eq_sql($sql1, $sql2);
fffe6900 97 my $same_bind = eq_bind($bind_ref1, $bind_ref2);
98
a6daa642 99 # call Test::Builder::ok
1a828f61 100 my $ret = $tb->ok($same_sql && $same_bind, $msg);
fffe6900 101
102 # add debugging info
103 if (!$same_sql) {
e7827ba2 104 _sql_differ_diag($sql1, $sql2);
fffe6900 105 }
106 if (!$same_bind) {
e7827ba2 107 _bind_differ_diag($bind_ref1, $bind_ref2);
fffe6900 108 }
1a828f61 109
110 # pass ok() result further
111 return $ret;
fffe6900 112}
113
e7827ba2 114sub is_same_sql {
115 my ($sql1, $sql2, $msg) = @_;
116
117 # compare
118 my $same_sql = eq_sql($sql1, $sql2);
119
120 # call Test::Builder::ok
1a828f61 121 my $ret = $tb->ok($same_sql, $msg);
e7827ba2 122
123 # add debugging info
124 if (!$same_sql) {
125 _sql_differ_diag($sql1, $sql2);
126 }
1a828f61 127
128 # pass ok() result further
129 return $ret;
e7827ba2 130}
131
132sub is_same_bind {
133 my ($bind_ref1, $bind_ref2, $msg) = @_;
134
135 # compare
136 my $same_bind = eq_bind($bind_ref1, $bind_ref2);
137
138 # call Test::Builder::ok
1a828f61 139 my $ret = $tb->ok($same_bind, $msg);
e7827ba2 140
141 # add debugging info
142 if (!$same_bind) {
143 _bind_differ_diag($bind_ref1, $bind_ref2);
144 }
1a828f61 145
146 # pass ok() result further
147 return $ret;
e7827ba2 148}
149
150sub _sql_differ_diag {
151 my ($sql1, $sql2) = @_;
152
153 $tb->diag("SQL expressions differ\n"
154 ." got: $sql1\n"
155 ."expected: $sql2\n"
156 ."differing in :\n$sql_differ\n"
157 );
158}
159
160sub _bind_differ_diag {
161 my ($bind_ref1, $bind_ref2) = @_;
162
163 $tb->diag("BIND values differ\n"
164 ." got: " . Dumper($bind_ref1)
165 ."expected: " . Dumper($bind_ref2)
166 );
167}
168
169sub eq_sql_bind {
170 my ($sql1, $bind_ref1, $sql2, $bind_ref2) = @_;
171
172 return eq_sql($sql1, $sql2) && eq_bind($bind_ref1, $bind_ref2);
173}
174
175
fffe6900 176sub eq_bind {
177 my ($bind_ref1, $bind_ref2) = @_;
fffe6900 178
fdfbbc65 179 local $Data::Dumper::Useqq = 1;
180 local $Data::Dumper::Sortkeys = 1;
181
182 return Dumper($bind_ref1) eq Dumper($bind_ref2);
fffe6900 183}
184
185sub eq_sql {
25823711 186 my ($sql1, $sql2) = @_;
187
188 # parse
189 my $tree1 = parse($sql1);
190 my $tree2 = parse($sql2);
191
1b17d1b0 192 return 1 if _eq_sql($tree1, $tree2);
25823711 193}
194
195sub _eq_sql {
fffe6900 196 my ($left, $right) = @_;
197
939db550 198 # one is defined the other not
199 if ( (defined $left) xor (defined $right) ) {
200 return 0;
201 }
202 # one is undefined, then so is the other
203 elsif (not defined $left) {
d15c14cc 204 return 1;
205 }
1b17d1b0 206 # one is a list, the other is an op with a list
207 elsif (ref $left->[0] xor ref $right->[0]) {
208 $sql_differ = sprintf ("left: %s\nright: %s\n", map { unparse ($_) } ($left, $right) );
fffe6900 209 return 0;
210 }
1b17d1b0 211 # one is a list, so is the other
212 elsif (ref $left->[0]) {
213 for (my $i = 0; $i <= $#$left or $i <= $#$right; $i++ ) {
214 return 0 if (not _eq_sql ($left->[$i], $right->[$i]) );
215 }
216 return 1;
217 }
218 # both are an op-list combo
219 else {
220
e40f5df9 221 # unroll parenthesis if possible/allowed
222 _parenthesis_unroll ($_) for ($left, $right);
1b17d1b0 223
224 # if operators are different
b9a4fdae 225 if ( $left->[0] ne $right->[0] ) {
1b17d1b0 226 $sql_differ = sprintf "OP [$left->[0]] != [$right->[0]] in\nleft: %s\nright: %s\n",
227 unparse($left),
228 unparse($right);
229 return 0;
230 }
231 # elsif operators are identical, compare operands
232 else {
b9a4fdae 233 if ($left->[0] eq 'LITERAL' ) { # unary
01b64cb7 234 (my $l = " $left->[1][0] " ) =~ s/\s+/ /g;
235 (my $r = " $right->[1][0] ") =~ s/\s+/ /g;
1b17d1b0 236 my $eq = $case_sensitive ? $l eq $r : uc($l) eq uc($r);
01b64cb7 237 $sql_differ = "[$l] != [$r]\n" if not $eq;
1b17d1b0 238 return $eq;
239 }
240 else {
241 my $eq = _eq_sql($left->[1], $right->[1]);
242 $sql_differ ||= sprintf ("left: %s\nright: %s\n", map { unparse ($_) } ($left, $right) ) if not $eq;
243 return $eq;
244 }
fffe6900 245 }
246 }
247}
248
fffe6900 249sub parse {
250 my $s = shift;
251
25823711 252 # tokenize string, and remove all optional whitespace
253 my $tokens = [];
254 foreach my $token (split $tokenizer_re, $s) {
b9a4fdae 255 push @$tokens, $token if (length $token) && ($token =~ /\S/);
25823711 256 }
fffe6900 257
25823711 258 my $tree = _recurse_parse($tokens, PARSE_TOP_LEVEL);
fffe6900 259 return $tree;
260}
261
262sub _recurse_parse {
25823711 263 my ($tokens, $state) = @_;
fffe6900 264
265 my $left;
266 while (1) { # left-associative parsing
267
268 my $lookahead = $tokens->[0];
1b17d1b0 269 if ( not defined($lookahead)
270 or
271 ($state == PARSE_IN_PARENS && $lookahead eq ')')
272 or
01b64cb7 273 ($state == PARSE_IN_EXPR && grep { $lookahead =~ /^ $_ $/xi } ('\)', @expression_terminator_sql_keywords ) )
274 or
9e8dab3f 275 ($state == PARSE_RHS && grep { $lookahead =~ /^ $_ $/xi } ('\)', @expression_terminator_sql_keywords, @binary_op_keywords, 'AND', 'OR', 'NOT' ) )
1b17d1b0 276 ) {
1b17d1b0 277 return $left;
1b17d1b0 278 }
fffe6900 279
280 my $token = shift @$tokens;
281
282 # nested expression in ()
b9a4fdae 283 if ($token eq '(' ) {
25823711 284 my $right = _recurse_parse($tokens, PARSE_IN_PARENS);
09abf3a0 285 $token = shift @$tokens or croak "missing closing ')' around block " . unparse ($right);
286 $token eq ')' or croak "unexpected token '$token' terminating block " . unparse ($right);
1b17d1b0 287 $left = $left ? [@$left, [PAREN => [$right] ]]
288 : [PAREN => [$right] ];
fffe6900 289 }
01b64cb7 290 # AND/OR
1b17d1b0 291 elsif ($token =~ /^ (?: OR | AND ) $/xi ) {
292 my $op = uc $token;
25823711 293 my $right = _recurse_parse($tokens, PARSE_IN_EXPR);
1b17d1b0 294
295 # Merge chunks if logic matches
296 if (ref $right and $op eq $right->[0]) {
297 $left = [ (shift @$right ), [$left, map { @$_ } @$right] ];
298 }
299 else {
300 $left = [$op => [$left, $right]];
301 }
fffe6900 302 }
01b64cb7 303 # binary operator keywords
304 elsif (grep { $token =~ /^ $_ $/xi } @binary_op_keywords ) {
305 my $op = uc $token;
306 my $right = _recurse_parse($tokens, PARSE_RHS);
307
b9a4fdae 308 # A between with a simple LITERAL for a 1st RHS argument needs a
01b64cb7 309 # rerun of the search to (hopefully) find the proper AND construct
b9a4fdae 310 if ($op eq 'BETWEEN' and $right->[0] eq 'LITERAL') {
01b64cb7 311 unshift @$tokens, $right->[1][0];
312 $right = _recurse_parse($tokens, PARSE_IN_EXPR);
313 }
314
315 $left = [$op => [$left, $right] ];
316 }
25823711 317 # expression terminator keywords (as they start a new expression)
1b17d1b0 318 elsif (grep { $token =~ /^ $_ $/xi } @expression_terminator_sql_keywords ) {
01b64cb7 319 my $op = uc $token;
25823711 320 my $right = _recurse_parse($tokens, PARSE_IN_EXPR);
1b17d1b0 321 $left = $left ? [@$left, [$op => [$right] ]]
01b64cb7 322 : [[ $op => [$right] ]];
25823711 323 }
9e8dab3f 324 # NOT (last as to allow all other NOT X pieces first)
325 elsif ( $token =~ /^ not $/ix ) {
326 my $op = uc $token;
327 my $right = _recurse_parse ($tokens, PARSE_RHS);
328 $left = $left ? [ @$left, [$op => [$right] ]]
329 : [[ $op => [$right] ]];
330
331 }
b9a4fdae 332 # literal (eat everything on the right until RHS termination)
fffe6900 333 else {
b9a4fdae 334 my $right = _recurse_parse ($tokens, PARSE_RHS);
335 $left = $left ? [$left, [LITERAL => [join ' ', $token, unparse($right)||()] ] ]
336 : [ LITERAL => [join ' ', $token, unparse($right)||()] ];
fffe6900 337 }
338 }
339}
340
e40f5df9 341sub _parenthesis_unroll {
342 my $ast = shift;
343
344 return if $parenthesis_significant;
345 return unless (ref $ast and ref $ast->[1]);
346
347 my $changes;
348 do {
349 my @children;
350 $changes = 0;
351
352 for my $child (@{$ast->[1]}) {
353 if (not ref $child or not $child->[0] eq 'PAREN') {
354 push @children, $child;
355 next;
356 }
357
358 # unroll nested parenthesis
359 while ($child->[1][0][0] eq 'PAREN') {
360 $child = $child->[1][0];
361 $changes++;
362 }
363
364 # if the parenthesis are wrapped around an AND/OR matching the parent AND/OR - open the parenthesis up and merge the list
365 if (
366 ( $ast->[0] eq 'AND' or $ast->[0] eq 'OR')
367 and
368 $child->[1][0][0] eq $ast->[0]
369 ) {
370 push @children, @{$child->[1][0][1]};
371 $changes++;
372 }
fffe6900 373
e40f5df9 374 # if the parent operator explcitly allows it nuke the parenthesis
375 elsif ( grep { $ast->[0] =~ /^ $_ $/xi } @unrollable_ops ) {
376 push @children, $child->[1][0];
377 $changes++;
378 }
379
b9a4fdae 380 # only one LITERAL element in the parenthesis
9e8dab3f 381 elsif (
b9a4fdae 382 @{$child->[1]} == 1 && $child->[1][0][0] eq 'LITERAL'
9e8dab3f 383 ) {
384 push @children, $child->[1][0];
385 $changes++;
386 }
387
b9a4fdae 388 # only one element in the parenthesis which is a binary op with two LITERAL sub-children
e40f5df9 389 elsif (
390 @{$child->[1]} == 1
391 and
392 grep { $child->[1][0][0] =~ /^ $_ $/xi } (@binary_op_keywords)
393 and
b9a4fdae 394 $child->[1][0][1][0][0] eq 'LITERAL'
e40f5df9 395 and
b9a4fdae 396 $child->[1][0][1][1][0] eq 'LITERAL'
e40f5df9 397 ) {
398 push @children, $child->[1][0];
399 $changes++;
400 }
401
402 # otherwise no more mucking for this pass
403 else {
404 push @children, $child;
405 }
406 }
407
408 $ast->[1] = \@children;
409
410 } while ($changes);
411
412}
fffe6900 413
414sub unparse {
415 my $tree = shift;
1b17d1b0 416
417 if (not $tree ) {
418 return '';
419 }
420 elsif (ref $tree->[0]) {
421 return join (" ", map { unparse ($_) } @$tree);
422 }
b9a4fdae 423 elsif ($tree->[0] eq 'LITERAL') {
01b64cb7 424 return $tree->[1][0];
1b17d1b0 425 }
426 elsif ($tree->[0] eq 'PAREN') {
01b64cb7 427 return sprintf '(%s)', join (" ", map {unparse($_)} @{$tree->[1]});
1b17d1b0 428 }
01b64cb7 429 elsif ($tree->[0] eq 'OR' or $tree->[0] eq 'AND' or (grep { $tree->[0] =~ /^ $_ $/xi } @binary_op_keywords ) ) {
1b17d1b0 430 return join (" $tree->[0] ", map {unparse($_)} @{$tree->[1]});
431 }
432 else {
433 return sprintf '%s %s', $tree->[0], unparse ($tree->[1]);
434 }
fffe6900 435}
436
437
4381;
439
440
441__END__
442
443=head1 NAME
444
445SQL::Abstract::Test - Helper function for testing SQL::Abstract
446
447=head1 SYNOPSIS
448
449 use SQL::Abstract;
450 use Test::More;
e7827ba2 451 use SQL::Abstract::Test import => [qw/
452 is_same_sql_bind is_same_sql is_same_bind
453 eq_sql_bind eq_sql eq_bind
454 /];
ec9af79e 455
fffe6900 456 my ($sql, @bind) = SQL::Abstract->new->select(%args);
e7827ba2 457
fffe6900 458 is_same_sql_bind($given_sql, \@given_bind,
459 $expected_sql, \@expected_bind, $test_msg);
460
e7827ba2 461 is_same_sql($given_sql, $expected_sql, $test_msg);
462 is_same_bind(\@given_bind, \@expected_bind, $test_msg);
463
464 my $is_same = eq_sql_bind($given_sql, \@given_bind,
465 $expected_sql, \@expected_bind);
466
467 my $sql_same = eq_sql($given_sql, $expected_sql);
468 my $bind_same = eq_bind(\@given_bind, \@expected_bind);
469
fffe6900 470=head1 DESCRIPTION
471
472This module is only intended for authors of tests on
473L<SQL::Abstract|SQL::Abstract> and related modules;
474it exports functions for comparing two SQL statements
475and their bound values.
476
477The SQL comparison is performed on I<abstract syntax>,
478ignoring differences in spaces or in levels of parentheses.
479Therefore the tests will pass as long as the semantics
480is preserved, even if the surface syntax has changed.
481
ec9af79e 482B<Disclaimer> : the semantic equivalence handling is pretty limited.
483A lot of effort goes into distinguishing significant from
484non-significant parenthesis, including AND/OR operator associativity.
485Currently this module does not support commutativity and more
486intelligent transformations like Morgan laws, etc.
487
488For a good overview of what this test framework is capable of refer
489to C<t/10test.t>
fffe6900 490
491=head1 FUNCTIONS
492
493=head2 is_same_sql_bind
494
495 is_same_sql_bind($given_sql, \@given_bind,
496 $expected_sql, \@expected_bind, $test_msg);
497
498Compares given and expected pairs of C<($sql, \@bind)>, and calls
e7827ba2 499L<Test::Builder/ok> on the result, with C<$test_msg> as message. If the test
500fails, a detailed diagnostic is printed. For clients which use L<Test::More>,
501this is the one of the three functions (L</is_same_sql_bind>, L</is_same_sql>,
502L</is_same_bind>) that needs to be imported.
503
504=head2 is_same_sql
505
506 is_same_sql($given_sql, $expected_sql, $test_msg);
507
508Compares given and expected SQL statements, and calls L<Test::Builder/ok> on
509the result, with C<$test_msg> as message. If the test fails, a detailed
510diagnostic is printed. For clients which use L<Test::More>, this is the one of
511the three functions (L</is_same_sql_bind>, L</is_same_sql>, L</is_same_bind>)
512that needs to be imported.
513
514=head2 is_same_bind
515
516 is_same_bind(\@given_bind, \@expected_bind, $test_msg);
517
518Compares given and expected bind values, and calls L<Test::Builder/ok> on the
519result, with C<$test_msg> as message. If the test fails, a detailed diagnostic
520is printed. For clients which use L<Test::More>, this is the one of the three
521functions (L</is_same_sql_bind>, L</is_same_sql>, L</is_same_bind>) that needs
522to be imported.
523
524=head2 eq_sql_bind
525
526 my $is_same = eq_sql_bind($given_sql, \@given_bind,
527 $expected_sql, \@expected_bind);
528
529Compares given and expected pairs of C<($sql, \@bind)>. Similar to
530L</is_same_sql_bind>, but it just returns a boolean value and does not print
531diagnostics or talk to L<Test::Builder>.
fffe6900 532
533=head2 eq_sql
534
535 my $is_same = eq_sql($given_sql, $expected_sql);
536
e7827ba2 537Compares the abstract syntax of two SQL statements. Similar to L</is_same_sql>,
538but it just returns a boolean value and does not print diagnostics or talk to
539L<Test::Builder>. If the result is false, the global variable L</$sql_differ>
540will contain the SQL portion where a difference was encountered; this is useful
541for printing diagnostics.
fffe6900 542
543=head2 eq_bind
544
545 my $is_same = eq_sql(\@given_bind, \@expected_bind);
546
e7827ba2 547Compares two lists of bind values, taking into account the fact that some of
548the values may be arrayrefs (see L<SQL::Abstract/bindtype>). Similar to
549L</is_same_bind>, but it just returns a boolean value and does not print
550diagnostics or talk to L<Test::Builder>.
fffe6900 551
552=head1 GLOBAL VARIABLES
553
e7827ba2 554=head2 $case_sensitive
fffe6900 555
556If true, SQL comparisons will be case-sensitive. Default is false;
557
e40f5df9 558=head2 $parenthesis_significant
559
560If true, SQL comparison will preserve and report difference in nested
561parenthesis. Useful for testing the C<-nest> modifier. Defaults to false;
562
e7827ba2 563=head2 $sql_differ
fffe6900 564
565When L</eq_sql> returns false, the global variable
566C<$sql_differ> contains the SQL portion
567where a difference was encountered.
568
569
570=head1 SEE ALSO
571
a6daa642 572L<SQL::Abstract>, L<Test::More>, L<Test::Builder>.
fffe6900 573
25823711 574=head1 AUTHORS
fffe6900 575
576Laurent Dami, E<lt>laurent.dami AT etat geneve chE<gt>
577
25823711 578Norbert Buchmuller <norbi@nix.hu>
579
e96c510a 580Peter Rabbitson <ribasushi@cpan.org>
581
fffe6900 582=head1 COPYRIGHT AND LICENSE
583
584Copyright 2008 by Laurent Dami.
585
586This library is free software; you can redistribute it and/or modify
587it under the same terms as Perl itself.