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