# $Id: SQL.pm 319 2007-01-22 21:06:50Z btrott $ package Data::ObjectDriver::SQL; use strict; use warnings; use base qw( Class::Accessor::Fast ); __PACKAGE__->mk_accessors(qw( select select_map select_map_reverse from joins where bind limit offset group order having where_values )); sub new { my $class = shift; my $stmt = $class->SUPER::new(@_); $stmt->select([]); $stmt->select_map({}); $stmt->select_map_reverse({}); $stmt->bind([]); $stmt->from([]); $stmt->where([]); $stmt->where_values({}); $stmt->having([]); $stmt->joins([]); $stmt; } sub add_select { my $stmt = shift; my($term, $col) = @_; $col ||= $term; push @{ $stmt->select }, $term; $stmt->select_map->{$term} = $col; $stmt->select_map_reverse->{$col} = $term; } sub add_join { my $stmt = shift; my($table, $joins) = @_; push @{ $stmt->joins }, { table => $table, joins => ref($joins) eq 'ARRAY' ? $joins : [ $joins ], }; } sub as_sql { my $stmt = shift; my $sql = ''; if (@{ $stmt->select }) { $sql .= 'SELECT '; $sql .= join(', ', map { my $alias = $stmt->select_map->{$_}; $alias && /(?:^|\.)\Q$alias\E$/ ? $_ : "$_ $alias"; } @{ $stmt->select }) . "\n"; } $sql .= 'FROM '; ## Add any explicit JOIN statements before the non-joined tables. if ($stmt->joins && @{ $stmt->joins }) { my $initial_table_written = 0; for my $j (@{ $stmt->joins }) { my($table, $joins) = map { $j->{$_} } qw( table joins ); $sql .= $table unless $initial_table_written++; for my $join (@{ $j->{joins} }) { $sql .= ' ' . uc($join->{type}) . ' JOIN ' . $join->{table} . ' ON ' . $join->{condition}; } } $sql .= ', ' if @{ $stmt->from }; } $sql .= join(', ', @{ $stmt->from }) . "\n"; $sql .= $stmt->as_sql_where; $sql .= $stmt->as_aggregate('group'); $sql .= $stmt->as_sql_having; $sql .= $stmt->as_aggregate('order'); $sql .= $stmt->as_limit; $sql; } sub as_limit { my $stmt = shift; my $n = $stmt->limit or return ''; die "Non-numerics in limit clause ($n)" if $n =~ /\D/; return sprintf "LIMIT %d%s\n", $n, ($stmt->offset ? " OFFSET " . int($stmt->offset) : ""); } sub as_aggregate { my $stmt = shift; my($set) = @_; if (my $attribute = $stmt->$set()) { my $elements = (ref($attribute) eq 'ARRAY') ? $attribute : [ $attribute ]; return uc($set) . ' BY ' . join(', ', map { $_->{column} . ($_->{desc} ? (' ' . $_->{desc}) : '') } @$elements) . "\n"; } return ''; } sub as_sql_where { my $stmt = shift; $stmt->where && @{ $stmt->where } ? 'WHERE ' . join(' AND ', @{ $stmt->where }) . "\n" : ''; } sub as_sql_having { my $stmt = shift; $stmt->having && @{ $stmt->having } ? 'HAVING ' . join(' AND ', @{ $stmt->having }) . "\n" : ''; } sub add_where { my $stmt = shift; ## xxx Need to support old range and transform behaviors. my($col, $val) = @_; Carp::croak("Invalid/unsafe column name $col") unless $col =~ /^[\w\.]+$/; my($term, $bind) = $stmt->_mk_term($col, $val); push @{ $stmt->{where} }, "($term)"; push @{ $stmt->{bind} }, @$bind; $stmt->where_values->{$col} = $val; } sub has_where { my $stmt = shift; my($col, $val) = @_; # TODO: should check if the value is same with $val? exists $stmt->where_values->{$col}; } sub add_having { my $stmt = shift; my($col, $val) = @_; # Carp::croak("Invalid/unsafe column name $col") unless $col =~ /^[\w\.]+$/; if (my $orig = $stmt->select_map_reverse->{$col}) { $col = $orig; } my($term, $bind) = $stmt->_mk_term($col, $val); push @{ $stmt->{having} }, "($term)"; push @{ $stmt->{bind} }, @$bind; } sub _mk_term { my $stmt = shift; my($col, $val) = @_; my $term = ''; my @bind; if (ref($val) eq 'ARRAY') { if (ref $val->[0] or $val->[0] eq '-and') { my $logic = 'OR'; my @values = @$val; if ($val->[0] eq '-and') { $logic = 'AND'; shift @values; } my @terms; for my $v (@values) { my($term, $bind) = $stmt->_mk_term($col, $v); push @terms, "($term)"; push @bind, @$bind; } $term = join " $logic ", @terms; } else { $term = "$col IN (".join(',', ('?') x scalar @$val).')'; @bind = @$val; } } elsif (ref($val) eq 'HASH') { my $c = $val->{column} || $col; $term = "$c $val->{op} ?"; push @bind, $val->{value}; } elsif (ref($val) eq 'SCALAR') { $term = "$col $$val"; } else { $term = "$col = ?"; push @bind, $val; } ($term, \@bind); } 1; __END__ =head1 NAME Data::ObjectDriver::SQL - an SQL statement =head1 SYNOPSIS my $sql = Data::ObjectDriver::SQL->new(); $sql->select([ 'id', 'name', 'bucket_id', 'note_id' ]); $sql->from([ 'foo' ]); $sql->add_where('name', 'fred'); $sql->add_where('bucket_id', { op => '!=', value => 47 }); $sql->add_where('note_id', \'IS NULL'); $sql->limit(1); my $sth = $dbh->prepare($sql->as_sql); $sth->execute(@{ $sql->{bind} }); my @values = $sth->selectrow_array(); my $obj = SomeObject->new(); $obj->set_columns(...); =head1 DESCRIPTION I represents an SQL statement. SQL statements are used internally to C object drivers to convert database operations (C, C, etc) into database operations, but sometimes you just gotta use SQL. =head1 ATTRIBUTES I sports several data attributes that represent the parts of the modeled SQL statement. These attributes all have accessor and mutator methods. Note that some attributes have more convenient methods of modification (for example, C for the C attribute). =head2 C query. =head2 C (hashref) The map of database column names to object fields in a C list to column names. =head2 C (hashref) The map of object fields to database column names in a C query. Note if you perform a C query. The requested object member will be indicated to be C<$term> in the statement's C and C attributes. C<$term> is optional, and defaults to the same value as C<$column>. =head2 C<$sql-Eadd_join($table, \@joins)> Adds the join statement indicated by C<$table> and C<\@joins> to the list of C table references for the statement. The structure for the set of joins are as described for the C attribute member above. =head2 C<$sql-Eadd_where($column, $value)> Adds a condition on the value of the database column C<$column> to the statement's C clause. A record will be tested against the below conditions according to what type of data structure C<$value> is: =over 4 =item * a scalar The value of C<$column> must equal C<$value>. =item * a reference to a scalar The value of C<$column> must evaluate true against the SQL given in C<$$value>. For example, if C<$$value> were C, C<$column> must be C for a record to pass. =item * a hashref The value of C<$column> must compare against the condition represented by C<$value>, which can contain the members: =over 4 =item * C The value with which to compare (required). =item * C The SQL operator with which to compare C and the value of C<$column> (required). =item * C The column name for the comparison. If this is present, it overrides the column name C<$column>, allowing you to build more complex conditions like C<((foo = 1 AND bar = 2) OR (baz = 3))>. =back For example, if C were C and C were C, a record's C<$column> column would have to be C to match. =item * an arrayref of scalars The value of C<$column> may equal any of the members of C<@$value>. The generated SQL performs the comparison with as an C expression. =item * an arrayref of (mostly) references The value of C<$column> must compare against I of the expressions represented in C<@$value>. Each member of the list can be any of the structures described here as possible forms of C<$value>. If the first member of the C<@$value> array is the scalar string C<-and>, I subsequent members of <@$value> must be met for the record to match. Note this is not very useful unless contained as one option of a larger C alternation. =back All individual conditions specified with C must be true for a record to be a result of the query. Beware that you can create a circular reference that will recursively generate an infinite SQL statement (for example, by specifying a arrayref C<$value> that itself contains C<$value>). As C evaluates your expressions before storing the conditions in the C attribute as a generated SQL string, this will occur when calling C, not C. So don't do that. =head2 C<$sql-Ehas_where($column, [$value])> Returns whether a where clause for the column C<$column> was added to the statement with the C method. The C<$value> argument is currently ignored. =head2 C<$sql-Eadd_having($column, $value)> Adds an expression to the C portion of the statement's C clause. The expression compares C<$column> using C<$value>, which can be any of the structures described above for the C method. =head2 C<$sql-Eas_sql()> Returns the SQL fully representing the SQL statement C<$sql>. =head2 C<$sql-Eas_sql_having()> Returns the SQL representing the C portion of C<$sql>'s C clause. =head2 C<$sql-Eas_sql_where()> Returns the SQL representing C<$sql>'s C clause. =head2 C<$sql-Eas_limit()> Returns the SQL for the C clause of the statement. =head2 C<$sql-Eas_aggregate($set)> Returns the SQL representing the aggregation clause of type C<$set> for the SQL statement C<$sql>. Reasonable values of C<$set> are C and C. =head1 DIAGNOSTICS =over 4 =item * C> The column name you specified to C contained characters that are not allowed in database column names. Only word characters and periods are allowed. Perhaps you didn't filter punctuation out of a generated column name correctly. =back =head1 BUGS AND LIMITATIONS I does not provide the functionality for turning SQL statements into instances of object classes. =head1 SEE ALSO =head1 LICENSE I is free software; you may redistribute it and/or modify it under the same terms as Perl itself. =head1 AUTHOR & COPYRIGHT Except where otherwise noted, I is Copyright 2005-2006 Six Apart, cpan@sixapart.com. All rights reserved. =cut