File Coverage

blib/lib/Class/DBI/SQL/Transformer/Quotify.pm
Criterion Covered Total %
statement 54 73 73.9
branch 12 14 85.7
condition 6 19 31.5
subroutine 6 8 75.0
pod n/a
total 78 114 68.4


line stmt bran cond sub pod time code
1             package Class::DBI::SQL::Transformer::Quotify;
2              
3 2     2   54593 use warnings;
  2         6  
  2         79  
4 2     2   12 use strict;
  2         4  
  2         91  
5              
6 2     2   14 use base qw/Class::DBI::SQL::Transformer/;
  2         15  
  2         2245  
7             our $VERSION = '0.02';
8              
9             sub _expand_table {
10 3     3   6 my $self = shift;
11 3         6 my $s = shift;
12 3 100       16 my ($class, $alias) = split /=/, defined($s)?$s:'', 2;
13 3         11 my $caller = $self->{_caller};
14 3 100       19 my $table = $class ? $class->table : $caller->table;
15 3   33     73 $self->{cmap}{ $alias || $table } = $class || ref $caller || $caller;
      66        
16 3   50     16 ($alias ||= "") &&= " ".$caller->db_Main->quote_identifier($alias);
      33        
17 3         9 return $caller->db_Main->quote_identifier($table) . $alias;
18             }
19              
20             sub _expand_join {
21 0     0   0 my $self = shift;
22 0         0 my $joins = shift;
23 0         0 my @table = split /\s+/, $joins;
24              
25 0         0 my $caller = $self->{_caller};
26 0         0 my %tojoin = map { $table[$_] => $table[ $_ + 1 ] } 0 .. $#table - 1;
  0         0  
27 0         0 my @sql;
28 0         0 while (my ($t1, $t2) = each %tojoin) {
29 0   0     0 my ($c1, $c2) = map $self->{cmap}{$_}
30             || $caller->_croak("Don't understand table '$_' in JOIN"), ($t1, $t2);
31              
32             my $join_col = sub {
33 0     0   0 my ($c1, $c2) = @_;
34 0         0 my $meta = $c1->meta_info('has_a');
35 0         0 my ($col) = grep $meta->{$_}->foreign_class eq $c2, keys %$meta;
36 0         0 $col;
37 0         0 };
38              
39 0   0     0 my $col = $join_col->($c1 => $c2) || do {
40             ($c1, $c2) = ($c2, $c1);
41             ($t1, $t2) = ($t2, $t1);
42             $join_col->($c1 => $c2);
43             };
44              
45 0 0       0 $caller->_croak("Don't know how to join $c1 to $c2") unless $col;
46 0         0 push @sql, sprintf " %s = %s ",
47             $caller->db_Main->quote_identifier($t1, $col),
48             $caller->db_Main->quote_identifier($t2, $c2->primary_column);
49             }
50 0         0 return join " AND ", @sql;
51             }
52              
53             sub _backtickify_arg {
54 360     360   474 my $self = shift;
55 360         596 my $caller = $self->{_caller};
56 360   50     928 my $char = $caller->db_Main->get_info(29) || q{"}; # SQL_IDENTIFIER_QUOTE_CHAR
57 360 100       18655 return $_[0] if $_[0] =~ /^$char[^$char]*$char$/; # return if already quoted
58 297         920 my @cols = $_[1]
59 486         790 ? @{$_[1]} # use what's given us (in the recursion cases)
60             # or (the initial case) use all cols, sorted longest to shortest
61             # This is necessary so that 'foo bar' gets processed before 'foo',
62             # so that if you have "foo bar" it doesn't become "`foo` bar"
63 324 100       1046 : sort { length $b <=> length $a } map { "$_" } $caller->all_columns
  243         13557  
64             ;
65 324 100       881 return $_[0] unless @cols;
66 287         490 my $c = shift @cols; # process first col
67 287         766 my $quoted = $caller->db_Main->quote_identifier($c);
68 287         15854 $_[0] =~ s/\b(?
69             # Recurse on all the pieces w/the remaining columns to process.
70             # Note the the quoted ones will just return right way.
71 287         3665 my @s = map { $self->_backtickify_arg($_,\@cols) } split /($quoted)/, $_[0];
  324         866  
72 287         551 $_[0] = join '', @s;
73 287         1025 return $_[0];
74             }
75              
76             sub _do_transformation {
77 59     59   67687 my $me = shift;
78 59         837 my $sql = $me->{_sql};
79 59         87 my @args = @{ $me->{_args} };
  59         144  
80 59         106 my $caller = $me->{_caller};
81              
82             # Each entry in @args is a SQL fragment. This will bugger with fragments that
83             # contain strings that match column names but are not supposed to be column names.
84 59         170 $me->_backtickify_arg($_) for @args;
85              
86 59         140 $sql =~ s/__TABLE(?:\((.+?)\))?__/$me->_expand_table($1)/eg;
  3         11  
87 59         207 $sql =~ s/__JOIN\((.+?)\)__/$me->_expand_join($1)/eg;
  0         0  
88 59         111 $sql =~ s/__ESSENTIAL__/join ", ", map { $caller->db_Main->quote_identifier($_) } $caller->_essential/eg;
  1         10  
  9         457  
89 59         141 $sql =~
90 2         10 s/__ESSENTIAL\((.+?)\)__/join ", ", map $caller->db_Main->quote_identifier($1,$_), $caller->_essential/eg;
91 59 100       1369 if ($sql =~ /__IDENTIFIER__/) {
92 1         13 my $key_sql = join " AND ", map $caller->db_Main->quote_identifier($_).'=?', $caller->primary_columns;
93 1         95 $sql =~ s/__IDENTIFIER__/$key_sql/g;
94             }
95              
96 59         134 $me->{_transformed_sql} = $sql;
97 59         159 $me->{_transformed_args} = [@args];
98 59         96 $me->{_transformed} = 1;
99 59         1021 return 1;
100             }
101              
102             1;
103              
104             =pod
105              
106             =head1 NAME
107              
108             Class::DBI::SQL::Transformer::Quotify - Quote column and table names in Class::DBI-generated SQL
109              
110             =head1 VERSION
111              
112             Version 0.02
113              
114             =head1 SYNOPSIS
115              
116             package Foo;
117             use base qw/Class::DBI/;
118             __PACKAGE__->connection('DBI:Mock:', '', '');
119             __PACKAGE__->sql_transformer_class('Class::DBI::SQL::Transformer::Quotify');
120             __PACKAGE__->table('table name');
121             __PACKAGE__->columns( Essential => 'my id', 'my name' );
122             package main;
123             my $row = Foo->retrieve( 3 );
124              
125             =head1 DESCRIPTION
126              
127             This is an attempt to solve the problem of spaces and/or reserved words in table and/or column names. Normally, Class::DBI does not quote these, so it results in sql such as the following (which clearly will error out):
128              
129             SELECT my id, my name
130             FROM table name
131             WHERE my id = ?
132              
133             This is implemented by subclassing L and notifying L via its C attribute. Note that some of the methods are completely replaced.
134              
135             =head1 BACKGROUND/EVOLUTION
136              
137             I first came upon L, which worked great, except the naming of the schema was so bad I hit an edge case that needed fixing first, which got me looking under the hood: L
138              
139             Since that version of Class::DBI::Plugin::Backtickify, Class::DBI (as of v3.0.8) had refactored the Class::DBI::SQL::Transformer class and introduced the Class::DBIsql_transformer_class() method. Which is why this module has the namespace it does instead of Class::DBI::Plugin:: and why I didn't just submit a patch for Backtickify.
140              
141             Drawing heavily from Backtickify, i generalized it to this module by using L::quote_identifier() instead of a hardcoded backtick.
142              
143             This potentially is (at least a partial) solution (or workaround) for Class::DBI RT ticket 7715 I: L
144              
145             In the course of investigation, also reported this Class::DBI issue, which this module also resolves: L
146              
147             =head1 AUTHOR
148              
149             David Westbrook (CPAN: davidrw), C<< >>
150              
151             =head1 BUGS
152              
153             Please report any bugs or feature requests to C, or through
154             the web interface at L. I will be notified, and then you'll
155             automatically be notified of progress on your bug as I make changes.
156              
157             =head1 SUPPORT
158              
159             You can find documentation for this module with the perldoc command.
160              
161             perldoc Class::DBI::SQL::Transformer::Quotify
162              
163             You can also look for information at:
164              
165             =over 4
166              
167             =item * RT: CPAN's request tracker
168              
169             L
170              
171             =item * AnnoCPAN: Annotated CPAN documentation
172              
173             L
174              
175             =item * CPAN Ratings
176              
177             L
178              
179             =item * Search CPAN
180              
181             L
182              
183             =back
184              
185             =head1 SEE ALSO
186              
187             L, L, L, L
188              
189             =head1 ACKNOWLEDGEMENTS
190              
191             David Baird for the groundwork of Class::DBI::Plugin::Backtickify
192              
193             =head1 COPYRIGHT & LICENSE
194              
195             Copyright 2008 David Westbrook, all rights reserved.
196              
197             This program is free software; you can redistribute it and/or modify it
198             under the same terms as Perl itself.
199              
200             =cut
201