1package SQL::Maker::Select; 2use strict; 3use warnings; 4use utf8; 5use SQL::Maker::Condition; 6use SQL::Maker::Util; 7use Class::Accessor::Lite ( 8 new => 0, 9 wo => [qw/distinct for_update/], 10 rw => [qw/prefix/], 11 ro => [qw/quote_char name_sep new_line strict/], 12); 13use Scalar::Util (); 14 15sub offset { 16 if (@_==1) { 17 return $_[0]->{offset}; 18 } else { 19 $_[0]->{offset} = $_[1]; 20 return $_[0]; 21 } 22} 23 24sub limit { 25 if (@_==1) { 26 $_[0]->{limit}; 27 } else { 28 $_[0]->{limit} = $_[1]; 29 return $_[0]; 30 } 31} 32 33sub new { 34 my $class = shift; 35 my %args = @_ == 1 ? %{$_[0]} : @_; 36 my $self = bless { 37 select => +[], 38 distinct => 0, 39 select_map => +{}, 40 select_map_reverse => +{}, 41 from => +[], 42 joins => +[], 43 index_hint => +{}, 44 group_by => +[], 45 order_by => +[], 46 prefix => 'SELECT ', 47 new_line => "\n", 48 strict => 0, 49 %args 50 }, $class; 51 52 return $self; 53} 54 55sub new_condition { 56 my $self = shift; 57 58 SQL::Maker::Condition->new( 59 quote_char => $self->{quote_char}, 60 name_sep => $self->{name_sep}, 61 strict => $self->{strict}, 62 ); 63} 64 65sub bind { 66 my $self = shift; 67 my @bind; 68 push @bind, @{$self->{subqueries}} if $self->{subqueries}; 69 push @bind, $self->{where}->bind if $self->{where}; 70 push @bind, $self->{having}->bind if $self->{having}; 71 return wantarray ? @bind : \@bind; 72} 73 74sub add_select { 75 my ($self, $term, $col) = @_; 76 77 $col ||= $term; 78 push @{ $self->{select} }, $term; 79 $self->{select_map}->{$term} = $col; 80 $self->{select_map_reverse}->{$col} = $term; 81 return $self; 82} 83 84sub add_from { 85 my ($self, $table, $alias) = @_; 86 if ( Scalar::Util::blessed( $table ) and $table->can('as_sql') ) { 87 push @{ $self->{subqueries} }, $table->bind; 88 push @{$self->{from}}, [ \do{ '(' . $table->as_sql . ')' }, $alias ]; 89 } 90 else { 91 push @{$self->{from}}, [$table, $alias]; 92 } 93 return $self; 94} 95 96sub add_join { 97 my ($self, $table_ref, $joins) = @_; 98 my ($table, $alias) = ref($table_ref) eq 'ARRAY' ? @$table_ref : ($table_ref); 99 100 if ( Scalar::Util::blessed( $table ) and $table->can('as_sql') ) { 101 push @{ $self->{subqueries} }, $table->bind; 102 $table = \do{ '(' . $table->as_sql . ')' }; 103 } 104 105 push @{ $self->{joins} }, { 106 table => [ $table, $alias ], 107 joins => $joins, 108 }; 109 return $self; 110} 111 112sub add_index_hint { 113 my ($self, $table, $hint) = @_; 114 115 my ($type, $list); 116 117 if (ref $hint eq 'HASH') { 118 # { type => '...', list => ['foo'] } 119 $type = $hint->{type} || 'USE'; 120 $list = ref($hint->{list}) eq 'ARRAY' ? $hint->{list} : [ $hint->{list} ]; 121 } else { 122 # ['foo, 'bar'] or just 'foo' 123 $type = 'USE'; 124 $list = ref($hint) eq 'ARRAY' ? $hint : [ $hint ]; 125 } 126 127 $self->{index_hint}->{$table} = { 128 type => $type, 129 list => $list, 130 }; 131 132 return $self; 133} 134 135sub _quote { 136 my ($self, $label) = @_; 137 138 return $$label if ref $label eq 'SCALAR'; 139 SQL::Maker::Util::quote_identifier($label, $self->{quote_char}, $self->{name_sep}) 140} 141 142sub as_sql { 143 my $self = shift; 144 my $sql = ''; 145 my $new_line = $self->new_line; 146 147 if (@{ $self->{select} }) { 148 $sql .= $self->{prefix}; 149 $sql .= 'DISTINCT ' if $self->{distinct}; 150 $sql .= join(', ', map { 151 my $alias = $self->{select_map}->{$_}; 152 if (!$alias) { 153 $self->_quote($_) 154 } elsif ($alias && $_ =~ /(?:^|\.)\Q$alias\E$/) { 155 $self->_quote($_) 156 } else { 157 $self->_quote($_) . ' AS ' . $self->_quote($alias) 158 } 159 } @{ $self->{select} }) . $new_line; 160 } 161 162 $sql .= 'FROM '; 163 164 ## Add any explicit JOIN statements before the non-joined tables. 165 if ($self->{joins} && @{ $self->{joins} }) { 166 my $initial_table_written = 0; 167 for my $j (@{ $self->{joins} }) { 168 my ($table, $join) = map { $j->{$_} } qw( table joins ); 169 $table = $self->_add_index_hint(@$table); ## index hint handling 170 $sql .= $table unless $initial_table_written++; 171 $sql .= ' ' . uc($join->{type}) if $join->{type}; 172 $sql .= ' JOIN ' . $self->_quote($join->{table}); 173 $sql .= ' ' . $self->_quote($join->{alias}) if $join->{alias}; 174 175 if ( defined $join->{condition} ) { 176 if (ref $join->{condition} && ref $join->{condition} eq 'ARRAY') { 177 $sql .= ' USING ('. join(', ', map { $self->_quote($_) } @{ $join->{condition} }) . ')'; 178 } 179 elsif (ref $join->{condition} && ref $join->{condition} eq 'HASH') { 180 my @conds; 181 for my $key (keys %{ $join->{condition} }) { 182 push @conds, $self->_quote($key) . ' = ' . $self->_quote($join->{condition}{$key}); 183 } 184 $sql .= ' ON ' . join(' AND ', @conds); 185 } 186 else { 187 $sql .= ' ON ' . $join->{condition}; 188 } 189 } 190 } 191 $sql .= ', ' if @{ $self->{from} }; 192 } 193 194 if ($self->{from} && @{ $self->{from} }) { 195 $sql .= join ', ', 196 map { $self->_add_index_hint($_->[0], $_->[1]) } 197 @{ $self->{from} }; 198 } 199 200 $sql .= $new_line; 201 $sql .= $self->as_sql_where() if $self->{where}; 202 203 $sql .= $self->as_sql_group_by if $self->{group_by}; 204 $sql .= $self->as_sql_having if $self->{having}; 205 $sql .= $self->as_sql_order_by if $self->{order_by}; 206 207 $sql .= $self->as_sql_limit if defined $self->{limit}; 208 209 $sql .= $self->as_sql_for_update; 210 $sql =~ s/${new_line}+$//; 211 212 return $sql; 213} 214 215sub as_sql_limit { 216 my $self = shift; 217 218 my $n = $self->{limit}; 219 return '' unless defined $n; 220 221 die "Non-numerics in limit clause ($n)" if $n =~ /\D/; 222 return sprintf "LIMIT %d%s" . $self->new_line, $n, 223 ($self->{offset} ? " OFFSET " . int($self->{offset}) : ""); 224} 225 226sub add_order_by { 227 my ($self, $col, $type) = @_; 228 push @{$self->{order_by}}, [$col, $type]; 229 return $self; 230} 231 232sub as_sql_order_by { 233 my ($self) = @_; 234 235 my @attrs = @{$self->{order_by}}; 236 return '' unless @attrs; 237 238 return 'ORDER BY ' 239 . join(', ', map { 240 my ($col, $type) = @$_; 241 if (ref $col) { 242 $$col 243 } else { 244 $type ? $self->_quote($col) . " $type" : $self->_quote($col) 245 } 246 } @attrs) 247 . $self->new_line; 248} 249 250sub add_group_by { 251 my ($self, $group, $order) = @_; 252 push @{$self->{group_by}}, $order ? $self->_quote($group) . " $order" : $self->_quote($group); 253 return $self; 254} 255 256sub as_sql_group_by { 257 my ($self,) = @_; 258 259 my $elems = $self->{group_by}; 260 261 return '' if @$elems == 0; 262 263 return 'GROUP BY ' 264 . join(', ', @$elems) 265 . $self->new_line; 266} 267 268sub set_where { 269 my ($self, $where) = @_; 270 $self->{where} = $where; 271 return $self; 272} 273 274sub add_where { 275 my ($self, $col, $val) = @_; 276 277 $self->{where} ||= $self->new_condition(); 278 $self->{where}->add($col, $val); 279 return $self; 280} 281 282sub add_where_raw { 283 my ($self, $term, $bind) = @_; 284 285 $self->{where} ||= $self->new_condition(); 286 $self->{where}->add_raw($term, $bind); 287 return $self; 288} 289 290sub as_sql_where { 291 my $self = shift; 292 293 my $where = $self->{where}->as_sql(undef, sub { $self->_quote($_[0]) }); 294 $where ? "WHERE $where" . $self->new_line : ''; 295} 296 297sub as_sql_having { 298 my $self = shift; 299 if ($self->{having}) { 300 'HAVING ' . $self->{having}->as_sql . $self->new_line; 301 } else { 302 '' 303 } 304} 305 306sub add_having { 307 my ($self, $col, $val) = @_; 308 309 if (my $orig = $self->{select_map_reverse}->{$col}) { 310 $col = $orig; 311 } 312 313 $self->{having} ||= $self->new_condition(); 314 $self->{having}->add($col, $val); 315 return $self; 316} 317 318sub as_sql_for_update { 319 my $self = shift; 320 $self->{for_update} ? ' FOR UPDATE' : ''; 321} 322 323sub _add_index_hint { 324 my ($self, $tbl_name, $alias) = @_; 325 my $quoted = $alias ? $self->_quote($tbl_name) . ' ' . $self->_quote($alias) : $self->_quote($tbl_name); 326 my $hint = $self->{index_hint}->{$tbl_name}; 327 return $quoted unless $hint && ref($hint) eq 'HASH'; 328 if ($hint->{list} && @{ $hint->{list} }) { 329 return $quoted . ' ' . uc($hint->{type} || 'USE') . ' INDEX (' . 330 join (',', map { $self->_quote($_) } @{ $hint->{list} }) . 331 ')'; 332 } 333 return $quoted; 334} 335 336 3371; 338__END__ 339 340=head1 NAME 341 342SQL::Maker::Select - dynamic SQL generator 343 344=head1 SYNOPSIS 345 346 my $sql = SQL::Maker::Select->new() 347 ->add_select('foo') 348 ->add_select('bar') 349 ->add_select('baz') 350 ->add_from('table_name') 351 ->as_sql; 352 # => "SELECT foo, bar, baz FROM table_name" 353 354=head1 DESCRIPTION 355 356=head1 METHODS 357 358=over 4 359 360=item C<< my $sql = $stmt->as_sql(); >> 361 362Render the SQL string. 363 364=item C<< my @bind = $stmt->bind(); >> 365 366Get the bind variables. 367 368=item C<< $stmt->add_select('*') >> 369 370=item C<< $stmt->add_select($col => $alias) >> 371 372=item C<< $stmt->add_select(\'COUNT(*)' => 'cnt') >> 373 374Add a new select term. It's automatically quoted. 375 376=item C<< $stmt->add_from($table :Str | $select :SQL::Maker::Select) : SQL::Maker::Select >> 377 378Add a new FROM clause. You can specify the table name or an instance of L<SQL::Maker::Select> for a sub-query. 379 380I<Return:> $stmt itself. 381 382=item C<< $stmt->add_join(user => {type => 'inner', table => 'config', condition => 'user.user_id = config.user_id'}); >> 383 384=item C<< $stmt->add_join(user => {type => 'inner', table => 'config', condition => {'user.user_id' => 'config.user_id'}); >> 385 386=item C<< $stmt->add_join(user => {type => 'inner', table => 'config', condition => ['user_id']}); >> 387 388Add a new JOIN clause. If you pass an arrayref for 'condition' then it uses 'USING'. If 'type' is omitted 389it falls back to plain JOIN. 390 391 my $stmt = SQL::Maker::Select->new(); 392 $stmt->add_join( 393 user => { 394 type => 'inner', 395 table => 'config', 396 condition => 'user.user_id = config.user_id', 397 } 398 ); 399 $stmt->as_sql(); 400 # => 'FROM user INNER JOIN config ON user.user_id = config.user_id' 401 402 my $stmt = SQL::Maker::Select->new(quote_char => '`', name_sep => '.'); 403 $stmt->add_join( 404 user => { 405 type => 'inner', 406 table => 'config', 407 condition => {'user.user_id' => 'config.user_id'}, 408 } 409 ); 410 $stmt->as_sql(); 411 # => 'FROM `user` INNER JOIN `config` ON `user`.`user_id` = `config`.`user_id`' 412 413 my $stmt = SQL::Maker::Select->new(); 414 $stmt->add_select('name'); 415 $stmt->add_join( 416 user => { 417 type => 'inner', 418 table => 'config', 419 condition => ['user_id'], 420 } 421 ); 422 $stmt->as_sql(); 423 # => 'SELECT name FROM user INNER JOIN config USING (user_id)' 424 425 my $subquery = SQL::Maker::Select->new(); 426 $subquery->add_select('*'); 427 $subquery->add_from( 'foo' ); 428 $subquery->add_where( 'hoge' => 'fuga' ); 429 my $stmt = SQL::Maker::Select->new(); 430 $stmt->add_join( 431 [ $subquery, 'bar' ] => { 432 type => 'inner', 433 table => 'baz', 434 alias => 'b1', 435 condition => 'bar.baz_id = b1.baz_id' 436 }, 437 ); 438 $stmt->as_sql; 439 # => "FROM (SELECT * FROM foo WHERE (hoge = ?)) bar INNER JOIN baz b1 ON bar.baz_id = b1.baz_id"; 440 441=item C<< $stmt->add_index_hint(foo => {type => 'USE', list => ['index_hint']}); >> 442 443=item C<< $stmt->add_index_hint(foo => 'index_hint'); >> 444 445=item C<< $stmt->add_index_hint(foo => ['index_hint']); >> 446 447 my $stmt = SQL::Maker::Select->new(); 448 $stmt->add_select('name'); 449 $stmt->add_from('user'); 450 $stmt->add_index_hint(user => {type => 'USE', list => ['index_hint']}); 451 $stmt->as_sql(); 452 # => "SELECT name FROM user USE INDEX (index_hint)" 453 454=item C<< $stmt->add_where('foo_id' => 'bar'); >> 455 456Add a new WHERE clause. 457 458 my $stmt = SQL::Maker::Select->new() 459 ->add_select('c') 460 ->add_from('foo') 461 ->add_where('name' => 'john') 462 ->add_where('type' => {IN => [qw/1 2 3/]}) 463 ->as_sql(); 464 # => "SELECT c FROM foo WHERE (name = ?) AND (type IN (?, ?, ?))" 465 466=item C<< $stmt->add_where_raw('id = ?', [1]) >> 467 468Add a new WHERE clause from raw placeholder string and bind variables. 469 470 my $stmt = SQL::Maker::Select->new() 471 ->add_select('c') 472 ->add_from('foo') 473 ->add_where_raw('EXISTS(SELECT * FROM bar WHERE name = ?)' => ['john']) 474 ->add_where_raw('type IS NOT NULL') 475 ->as_sql(); 476 # => "SELECT c FROM foo WHERE (EXISTS(SELECT * FROM bar WHERE name = ?)) AND (type IS NOT NULL)" 477 478 479=item C<< $stmt->set_where($condition) >> 480 481Set the WHERE clause. 482 483$condition should be instance of L<SQL::Maker::Condition>. 484 485 my $cond1 = SQL::Maker::Condition->new() 486 ->add("name" => "john"); 487 my $cond2 = SQL::Maker::Condition->new() 488 ->add("type" => {IN => [qw/1 2 3/]}); 489 my $stmt = SQL::Maker::Select->new() 490 ->add_select('c') 491 ->add_from('foo') 492 ->set_where($cond1 & $cond2) 493 ->as_sql(); 494 # => "SELECT c FROM foo WHERE ((name = ?)) AND ((type IN (?, ?, ?)))" 495 496=item C<< $stmt->add_order_by('foo'); >> 497 498=item C<< $stmt->add_order_by({'foo' => 'DESC'}); >> 499 500Add a new ORDER BY clause. 501 502 my $stmt = SQL::Maker::Select->new() 503 ->add_select('c') 504 ->add_from('foo') 505 ->add_order_by('name' => 'DESC') 506 ->add_order_by('id') 507 ->as_sql(); 508 # => "SELECT c FROM foo ORDER BY name DESC, id" 509 510=item C<< $stmt->add_group_by('foo'); >> 511 512Add a new GROUP BY clause. 513 514 my $stmt = SQL::Maker::Select->new() 515 ->add_select('c') 516 ->add_from('foo') 517 ->add_group_by('id') 518 ->as_sql(); 519 # => "SELECT c FROM foo GROUP BY id" 520 521 my $stmt = SQL::Maker::Select->new() 522 ->add_select('c') 523 ->add_from('foo') 524 ->add_group_by('id' => 'DESC') 525 ->as_sql(); 526 # => "SELECT c FROM foo GROUP BY id DESC" 527 528=item C<< $stmt->limit(30) >> 529 530=item C<< $stmt->offset(5) >> 531 532Add LIMIT and OFFSET. 533 534 my $stmt = SQL::Maker::Select->new() 535 ->add_select('c') 536 ->add_from('foo') 537 ->limit(30) 538 ->offset(5) 539 ->as_sql(); 540 # => "SELECT c FROM foo LIMIT 30 OFFSET 5" 541 542=item C<< $stmt->add_having(cnt => 2) >> 543 544Add a HAVING clause. 545 546 my $stmt = SQL::Maker::Select->new() 547 ->add_from('foo') 548 ->add_select(\'COUNT(*)' => 'cnt') 549 ->add_having(cnt => 2) 550 ->as_sql(); 551 # => "SELECT COUNT(*) AS cnt FROM foo HAVING (COUNT(*) = ?)" 552 553=back 554 555=head1 SEE ALSO 556 557L<Data::ObjectDriver::SQL> 558 559