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