From: Rafael Kitover Date: Wed, 10 Jun 2009 11:12:08 +0000 (+0000) Subject: Merge 'trunk' into 'sybase' X-Git-Tag: v0.08112~14^2~142 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=70f524e6b39efe4fb16e2fd75132c118f050df25;hp=8242035e7f824106e902a7667d0e82c4ec442b0b;p=dbsrgits%2FDBIx-Class.git Merge 'trunk' into 'sybase' r5385@hlagh (orig r6528): ribasushi | 2009-06-06 01:45:10 -0700 rename MC test dir r5386@hlagh (orig r6529): ribasushi | 2009-06-06 01:47:47 -0700 TODOified reentrancy counter - this shall be used to optimize MC some day r5387@hlagh (orig r6530): ribasushi | 2009-06-06 01:50:56 -0700 Port another forgotten MC fix r5393@hlagh (orig r6537): ribasushi | 2009-06-07 14:07:55 -0700 Fix for mysql subquery problem r5394@hlagh (orig r6538): ribasushi | 2009-06-07 14:36:43 -0700 Make empty/default inserts use standard SQL r5395@hlagh (orig r6539): ribasushi | 2009-06-07 15:59:21 -0700 Add mysql empty insert SQL override Make SQLAHacks parts loadable at runtime via ensure_class_loaded r5396@hlagh (orig r6540): ribasushi | 2009-06-07 16:03:04 -0700 Make podcoverage happy r5397@hlagh (orig r6541): ribasushi | 2009-06-07 16:24:06 -0700 Fix find_or_new/create to stop returning random rows when default value insert is requested r5398@hlagh (orig r6542): ribasushi | 2009-06-08 02:36:56 -0700 Simply order_by/_virtual_order_by handling r5406@hlagh (orig r6552): ribasushi | 2009-06-08 14:56:41 -0700 duh r5410@hlagh (orig r6556): ash | 2009-06-09 03:20:34 -0700 Addjust bug to show problem with rows => 1 + child rel r5411@hlagh (orig r6557): ribasushi | 2009-06-09 04:12:46 -0700 Require a recent bugfixed Devel::Cycle r5413@hlagh (orig r6559): ash | 2009-06-09 06:07:30 -0700 Make IC::DT extra warning state the column name too r5420@hlagh (orig r6574): ribasushi | 2009-06-09 15:19:48 -0700 AuthorCheck fixes r5424@hlagh (orig r6578): ribasushi | 2009-06-09 15:52:17 -0700 r6522@Thesaurus (orig r6521): ribasushi | 2009-06-05 19:27:55 +0200 New branch to try resultsource related stuff r6545@Thesaurus (orig r6544): ribasushi | 2009-06-08 13:00:54 +0200 First stab at adding resultsources to each join in select - works won-der-ful-ly r6546@Thesaurus (orig r6545): ribasushi | 2009-06-08 13:14:08 +0200 Commit failing test and thoughts on search arg deflation r6576@Thesaurus (orig r6575): ribasushi | 2009-06-10 00:31:55 +0200 Todoify DT in search deflation test until after 0.09 r6577@Thesaurus (orig r6576): ribasushi | 2009-06-10 00:48:07 +0200 Factor out the $ident resolver r5426@hlagh (orig r6580): ribasushi | 2009-06-09 16:21:50 -0700 Move as_query out of the cursor r5427@hlagh (orig r6581): ribasushi | 2009-06-09 16:27:19 -0700 Think before commit r5428@hlagh (orig r6582): ribasushi | 2009-06-10 00:37:19 -0700 Clarify and disable rows/prefetch test - fix is easy, but architecturally unsound - need more time --- diff --git a/Changes b/Changes index 1869f7f..ab51a54 100644 --- a/Changes +++ b/Changes @@ -8,6 +8,9 @@ Revision history for DBIx::Class an exception - fixed corner case when populate() erroneously falls back to create() + - work around braindead mysql when doing subquery counts on + resultsets containing identically named columns from several + tables 0.08103 2009-05-26 19:50:00 (UTC) - Multiple $resultset -> count/update/delete fixes. Now any diff --git a/Makefile.PL b/Makefile.PL index 80de580..ebdf8b0 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -71,6 +71,7 @@ my %force_requires_if_author = ( # t/52cycle.t 'Test::Memory::Cycle' => 0, + 'Devel::Cycle' => 1.10, # t/60core.t 'DateTime::Format::MySQL' => 0, diff --git a/lib/DBIx/Class/InflateColumn/DateTime.pm b/lib/DBIx/Class/InflateColumn/DateTime.pm index 54e66f5..d006e55 100644 --- a/lib/DBIx/Class/InflateColumn/DateTime.pm +++ b/lib/DBIx/Class/InflateColumn/DateTime.pm @@ -116,14 +116,14 @@ sub register_column { my $timezone; if ( defined $info->{extra}{timezone} ) { carp "Putting timezone into extra => { timezone => '...' } has been deprecated, ". - "please put it directly into the columns definition."; + "please put it directly into the '$column' column definition."; $timezone = $info->{extra}{timezone}; } my $locale; if ( defined $info->{extra}{locale} ) { carp "Putting locale into extra => { locale => '...' } has been deprecated, ". - "please put it directly into the columns definition."; + "please put it directly into the '$column' column definition."; $locale = $info->{extra}{locale}; } @@ -139,7 +139,7 @@ sub register_column { if (defined $info->{extra}{floating_tz_ok}) { carp "Putting floating_tz_ok into extra => { floating_tz_ok => 1 } has been deprecated, ". - "please put it directly into the columns definition."; + "please put it directly into the '$column' column definition."; $info{floating_tz_ok} = $info->{extra}{floating_tz_ok}; } diff --git a/lib/DBIx/Class/Relationship/ManyToMany.pm b/lib/DBIx/Class/Relationship/ManyToMany.pm index e953a44..bab7bb1 100644 --- a/lib/DBIx/Class/Relationship/ManyToMany.pm +++ b/lib/DBIx/Class/Relationship/ManyToMany.pm @@ -85,12 +85,12 @@ EOW my $obj; if (ref $_[0]) { if (ref $_[0] eq 'HASH') { - $obj = $f_rel_rs->create($_[0]); + $obj = $f_rel_rs->find_or_create($_[0]); } else { $obj = $_[0]; } } else { - $obj = $f_rel_rs->create({@_}); + $obj = $f_rel_rs->find_or_create({@_}); } my $link_vals = @_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}; diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index 079edc6..7b34a86 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -661,7 +661,6 @@ sub cursor { my ($self) = @_; my $attrs = $self->_resolved_attrs_copy; - $attrs->{_virtual_order_by} = $self->_gen_virtual_order; return $self->{cursor} ||= $self->result_source->storage->select($attrs->{from}, $attrs->{select}, @@ -714,7 +713,6 @@ sub single { } my $attrs = $self->_resolved_attrs_copy; - $attrs->{_virtual_order_by} = $self->_gen_virtual_order; if ($where) { if (defined $attrs->{where}) { @@ -742,15 +740,6 @@ sub single { return (@data ? ($self->_construct_object(@data))[0] : undef); } -# _gen_virtual_order -# -# This is a horrble hack, but seems like the best we can do at this point -# Some limit emulations (Top) require an ordered resultset in order to -# function at all. So supply a PK order to be used if necessary - -sub _gen_virtual_order { - return [ shift->result_source->primary_columns ]; -} # _is_unique_query # @@ -1329,7 +1318,7 @@ sub _rs_update_delete { my $subrs = (ref $self)->new($rsrc, $attrs); - return $self->result_source->storage->subq_update_delete($subrs, $op, $values); + return $self->result_source->storage->_subq_update_delete($subrs, $op, $values); } else { return $rsrc->storage->$op( @@ -1936,7 +1925,10 @@ B: This feature is still experimental. =cut -sub as_query { return shift->cursor->as_query(@_) } +sub as_query { + my $self = shift; + return $self->result_source->storage->as_query($self->_resolved_attrs); +} =head2 find_or_new @@ -1977,8 +1969,10 @@ sub find_or_new { my $self = shift; my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; - my $exists = $self->find($hash, $attrs); - return defined $exists ? $exists : $self->new_result($hash); + if (keys %$hash and my $row = $self->find($hash, $attrs) ) { + return $row; + } + return $self->new_result($hash); } =head2 create @@ -2108,8 +2102,10 @@ sub find_or_create { my $self = shift; my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; - my $exists = $self->find($hash, $attrs); - return defined $exists ? $exists : $self->create($hash); + if (keys %$hash and my $row = $self->find($hash, $attrs) ) { + return $row; + } + return $self->create($hash); } =head2 update_or_create @@ -2435,7 +2431,11 @@ sub _resolve_from { my $attrs = $self->{attrs}; my $from = $attrs->{from} - || [ { $attrs->{alias} => $source->from } ]; + || [ { + -result_source => $source, + -alias => $attrs->{alias}, + $attrs->{alias} => $source->from, + } ]; my $seen = { %{$attrs->{seen_join} || {} } }; @@ -2540,7 +2540,11 @@ sub _resolved_attrs { push( @{ $attrs->{as} }, @$adds ); } - $attrs->{from} ||= [ { $self->{attrs}{alias} => $source->from } ]; + $attrs->{from} ||= [ { + -result_source => $source, + -alias => $self->{attrs}{alias}, + $self->{attrs}{alias} => $source->from, + } ]; if ( exists $attrs->{join} || exists $attrs->{prefetch} ) { my $join = delete $attrs->{join} || {}; @@ -2571,6 +2575,14 @@ sub _resolved_attrs { $attrs->{order_by} = []; } + # If the order_by is otherwise empty - we will use this for TOP limit + # emulation and the like. + # Although this is needed only if the order_by is not defined, it is + # actually cheaper to just populate this rather than properly examining + # order_by (stuf like [ {} ] and the like) + $attrs->{_virtual_order_by} = [ $self->result_source->primary_columns ]; + + my $collapse = $attrs->{collapse} || {}; if ( my $prefetch = delete $attrs->{prefetch} ) { $prefetch = $self->_merge_attr( {}, $prefetch ); @@ -2613,7 +2625,7 @@ sub _joinpath_aliases { my $p = $paths; $p = $p->{$_} ||= {} for @{$j->[0]{-join_path}}; - push @{$p->{-join_aliases} }, $j->[0]{-join_alias}; + push @{$p->{-join_aliases} }, $j->[0]{-alias}; } return $paths; diff --git a/lib/DBIx/Class/ResultSource.pm b/lib/DBIx/Class/ResultSource.pm index cd819a8..c8f7e8d 100644 --- a/lib/DBIx/Class/ResultSource.pm +++ b/lib/DBIx/Class/ResultSource.pm @@ -1120,10 +1120,13 @@ sub _resolve_join { $type = $rel_info->{attrs}{join_type} || ''; $force_left->{force} = 1 if lc($type) eq 'left'; } - return [ { $as => $self->related_source($join)->from, + + my $rel_src = $self->related_source($join); + return [ { $as => $rel_src->from, + -result_source => $rel_src, -join_type => $type, -join_path => [@$jpath, $join], - -join_alias => $as, + -alias => $as, -relation_chain_depth => $seen->{-relation_chain_depth} || 0, }, $self->_resolve_condition($rel_info->{cond}, $as, $alias) ]; diff --git a/lib/DBIx/Class/SQLAHacks.pm b/lib/DBIx/Class/SQLAHacks.pm index c0b9937..a454cd5 100644 --- a/lib/DBIx/Class/SQLAHacks.pm +++ b/lib/DBIx/Class/SQLAHacks.pm @@ -193,6 +193,14 @@ sub insert { my $self = shift; my $table = shift; $table = $self->_quote($table) unless ref($table); + + # SQLA will emit INSERT INTO $table ( ) VALUES ( ) + # which is sadly understood only by MySQL. Change default behavior here, + # until SQLA2 comes with proper dialect support + if (! $_[0] or (ref $_[0] eq 'HASH' and !keys %{$_[0]} ) ) { + return "INSERT INTO ${table} DEFAULT VALUES" + } + $self->SUPER::insert($table, @_); } diff --git a/lib/DBIx/Class/SQLAHacks/MySQL.pm b/lib/DBIx/Class/SQLAHacks/MySQL.pm new file mode 100644 index 0000000..9b4d0be --- /dev/null +++ b/lib/DBIx/Class/SQLAHacks/MySQL.pm @@ -0,0 +1,24 @@ +package # Hide from PAUSE + DBIx::Class::SQLAHacks::MySQL; + +use base qw( DBIx::Class::SQLAHacks ); +use Carp::Clan qw/^DBIx::Class|^SQL::Abstract/; + +# +# MySQL does not understand the standard INSERT INTO $table DEFAULT VALUES +# Adjust SQL here instead +# +sub insert { + my $self = shift; + + my $table = $_[0]; + $table = $self->_quote($table) unless ref($table); + + if (! $_[1] or (ref $_[1] eq 'HASH' and !keys %{$_[1]} ) ) { + return "INSERT INTO ${table} () VALUES ()" + } + + return $self->SUPER::insert (@_); +} + +1; diff --git a/lib/DBIx/Class/SQLAHacks/OracleJoins.pm b/lib/DBIx/Class/SQLAHacks/OracleJoins.pm index f02a913..4b5efba 100644 --- a/lib/DBIx/Class/SQLAHacks/OracleJoins.pm +++ b/lib/DBIx/Class/SQLAHacks/OracleJoins.pm @@ -2,7 +2,7 @@ package # Hide from PAUSE DBIx::Class::SQLAHacks::OracleJoins; use base qw( DBIx::Class::SQLAHacks ); -use Carp::Clan qw/^DBIx::Class/; +use Carp::Clan qw/^DBIx::Class|^SQL::Abstract/; sub select { my ($self, $table, $fields, $where, $order, @rest) = @_; diff --git a/lib/DBIx/Class/Storage/DBI.pm b/lib/DBIx/Class/Storage/DBI.pm index 20b1f1f..52cd456 100644 --- a/lib/DBIx/Class/Storage/DBI.pm +++ b/lib/DBIx/Class/Storage/DBI.pm @@ -7,10 +7,9 @@ use strict; use warnings; use Carp::Clan qw/^DBIx::Class/; use DBI; -use DBIx::Class::SQLAHacks; use DBIx::Class::Storage::DBI::Cursor; use DBIx::Class::Storage::Statistics; -use Scalar::Util qw/blessed weaken/; +use Scalar::Util(); use List::Util(); __PACKAGE__->mk_group_accessors('simple' => @@ -603,6 +602,7 @@ sub sql_maker { my ($self) = @_; unless ($self->_sql_maker) { my $sql_maker_class = $self->sql_maker_class; + $self->ensure_class_loaded ($sql_maker_class); $self->_sql_maker($sql_maker_class->new( $self->_sql_maker_args )); } return $self->_sql_maker; @@ -717,7 +717,7 @@ sub _connect { if($dbh && !$self->unsafe) { my $weak_self = $self; - weaken($weak_self); + Scalar::Util::weaken($weak_self); $dbh->{HandleError} = sub { if ($weak_self) { $weak_self->throw_exception("DBI Exception: $_[0]"); @@ -898,7 +898,7 @@ sub txn_rollback { sub _prep_for_execute { my ($self, $op, $extra_bind, $ident, $args) = @_; - if( blessed($ident) && $ident->isa("DBIx::Class::ResultSource") ) { + if( Scalar::Util::blessed($ident) && $ident->isa("DBIx::Class::ResultSource") ) { $ident = $ident->from(); } @@ -910,6 +910,38 @@ sub _prep_for_execute { return ($sql, \@bind); } +=head2 as_query + +=over 4 + +=item Arguments: $rs_attrs + +=item Return Value: \[ $sql, @bind ] + +=back + +Returns the SQL statement and bind vars that would result from the given +ResultSet attributes (does not actually run a query) + +=cut + +sub as_query { + my ($self, $rs_attr) = @_; + + my $sql_maker = $self->sql_maker; + local $sql_maker->{for}; + + # my ($op, $bind, $ident, $bind_attrs, $select, $cond, $order, $rows, $offset) = $self->_select_args(...); + my @args = $self->_select_args($rs_attr->{from}, $rs_attr->{select}, $rs_attr->{where}, $rs_attr); + + # my ($sql, $bind) = $self->_prep_for_execute($op, $bind, $ident, [ $select, $cond, $order, $rows, $offset ]); + my ($sql, $bind) = $self->_prep_for_execute( + @args[0 .. 2], + [ @args[4 .. $#args] ], + ); + return \[ "($sql)", @{ $bind || [] }]; +} + sub _fix_bind_params { my ($self, @bind) = @_; @@ -947,7 +979,7 @@ sub _query_start { if ( $self->debug ) { @bind = $self->_fix_bind_params(@bind); - + $self->debugobj->query_start( $sql, @bind ); } } @@ -1006,7 +1038,7 @@ sub _execute { sub insert { my ($self, $source, $to_insert) = @_; - + my $ident = $source->from; my $bind_attributes = $self->source_bind_attributes($source); @@ -1108,7 +1140,7 @@ sub delete { my $self = shift @_; my $source = shift @_; - my $bind_attrs = {}; ## If ever it's needed... + my $bind_attrs = $self->source_bind_attributes($source); return $self->_execute('delete' => [], $source, $bind_attrs, @_); } @@ -1120,7 +1152,7 @@ sub delete { # Genarating a single PK column subquery is trivial and supported # by all RDBMS. However if we have a multicolumn PK, things get ugly. # Look at _multipk_update_delete() -sub subq_update_delete { +sub _subq_update_delete { my $self = shift; my ($rs, $op, $values) = @_; @@ -1213,23 +1245,41 @@ sub _select { sub _select_args { my ($self, $ident, $select, $condition, $attrs) = @_; - my $order = $attrs->{order_by}; my $for = delete $attrs->{for}; my $sql_maker = $self->sql_maker; $sql_maker->{for} = $for; - my @in_order_attrs = qw/group_by having _virtual_order_by/; - if (List::Util::first { exists $attrs->{$_} } (@in_order_attrs) ) { - $order = { - ($order - ? (order_by => $order) - : () - ), - ( map { $_ => $attrs->{$_} } (@in_order_attrs) ) - }; + my $order = { map + { $attrs->{$_} ? ( $_ => $attrs->{$_} ) : () } + (qw/order_by group_by having _virtual_order_by/ ) + }; + + + my $bind_attrs = {}; + + my $alias2source = $self->_resolve_ident_sources ($ident); + + for my $alias (keys %$alias2source) { + my $bindtypes = $self->source_bind_attributes ($alias2source->{$alias}) || {}; + for my $col (keys %$bindtypes) { + + my $fqcn = join ('.', $alias, $col); + $bind_attrs->{$fqcn} = $bindtypes->{$col} if $bindtypes->{$col}; + + # so that unqualified searches can be bound too + $bind_attrs->{$col} = $bind_attrs->{$fqcn} if $alias eq 'me'; + } } - my $bind_attrs = {}; ## Future support + + # This would be the point to deflate anything found in $condition + # (and leave $attrs->{bind} intact). Problem is - inflators historically + # expect a row object. And all we have is a resultsource (it is trivial + # to extract deflator coderefs via $alias2source above). + # + # I don't see a way forward other than changing the way deflators are + # invoked, and that's just bad... + my @args = ('select', $attrs->{bind}, $ident, $bind_attrs, $select, $condition, $order); if ($attrs->{software_limit} || $sql_maker->_default_limit_syntax eq "GenericSubQ") { @@ -1245,6 +1295,35 @@ sub _select_args { return @args; } +sub _resolve_ident_sources { + my ($self, $ident) = @_; + + my $alias2source = {}; + + # the reason this is so contrived is that $ident may be a {from} + # structure, specifying multiple tables to join + if ( Scalar::Util::blessed($ident) && $ident->isa("DBIx::Class::ResultSource") ) { + $alias2source->{$ident->alias} = $ident; + } + elsif (ref $ident eq 'ARRAY') { + + for (@$ident) { + my $tabinfo; + if (ref $_ eq 'HASH') { + $tabinfo = $_; + } + if (ref $_ eq 'ARRAY' and ref $_->[0] eq 'HASH') { + $tabinfo = $_->[0]; + } + + $alias2source->{$tabinfo->{-alias}} = $tabinfo->{-result_source} + if ($tabinfo->{-result_source}); + } + } + + return $alias2source; +} + sub _trim_attributes_for_count { my ($self, $source, $attrs) = @_; my %attrs = %$attrs; @@ -1292,7 +1371,7 @@ sub count_grouped { } $sub_attrs->{group_by} ||= [ map { "$attrs->{alias}.$_" } ($source->primary_columns) ]; - $sub_attrs->{select} = $self->_grouped_count_select ($sub_attrs); + $sub_attrs->{select} = $self->_grouped_count_select ($source, $sub_attrs); $attrs->{from} = [{ count_subq => $source->resultset_class->new ($source, $sub_attrs )->as_query @@ -1311,8 +1390,8 @@ sub count_grouped { # choke in various ways. # sub _grouped_count_select { - my ($self, $attrs) = @_; - return $attrs->{group_by}; + my ($self, $source, $rs_args) = @_; + return $rs_args->{group_by}; } sub source_bind_attributes { diff --git a/lib/DBIx/Class/Storage/DBI/Cursor.pm b/lib/DBIx/Class/Storage/DBI/Cursor.pm index 5800113..bd2a20a 100644 --- a/lib/DBIx/Class/Storage/DBI/Cursor.pm +++ b/lib/DBIx/Class/Storage/DBI/Cursor.pm @@ -49,32 +49,6 @@ sub new { return bless ($new, $class); } -=head2 as_query - -=over 4 - -=item Arguments: none - -=item Return Value: \[ $sql, @bind ] - -=back - -Returns the SQL statement and bind vars associated with the invocant. - -=cut - -sub as_query { - my $self = shift; - - my $storage = $self->{storage}; - my $sql_maker = $storage->sql_maker; - local $sql_maker->{for}; - - my @args = $storage->_select_args(@{$self->{args}}); - my ($sql, $bind) = $storage->_prep_for_execute(@args[0 .. 2], [@args[4 .. $#args]]); - return \[ "($sql)", @{ $bind || [] }]; -} - =head2 next =over 4 diff --git a/lib/DBIx/Class/Storage/DBI/mysql.pm b/lib/DBIx/Class/Storage/DBI/mysql.pm index dcdeafe..221548a 100644 --- a/lib/DBIx/Class/Storage/DBI/mysql.pm +++ b/lib/DBIx/Class/Storage/DBI/mysql.pm @@ -5,7 +5,7 @@ use warnings; use base qw/DBIx::Class::Storage::DBI::MultiColumnIn/; -# __PACKAGE__->load_components(qw/PK::Auto/); +__PACKAGE__->sql_maker_class('DBIx::Class::SQLAHacks::MySQL'); sub with_deferred_fk_checks { my ($self, $sub) = @_; @@ -53,10 +53,23 @@ sub lag_behind_master { # MySql can not do subquery update/deletes, only way is slow per-row operations. # This assumes you have set proper transaction isolation and use innodb. -sub subq_update_delete { +sub _subq_update_delete { return shift->_per_row_update_delete (@_); } +# MySql chokes on things like: +# COUNT(*) FROM (SELECT tab1.col, tab2.col FROM tab1 JOIN tab2 ... ) +# claiming that col is a duplicate column (it loses the table specifiers by +# the time it gets to the *). Thus for any subquery count we select only the +# primary keys of the main table in the inner query. This hopefully still +# hits the indexes and keeps mysql happy. +# (mysql does not care if the SELECT and the GROUP BY match) +sub _grouped_count_select { + my ($self, $source, $rs_args) = @_; + my @pcols = map { join '.', $rs_args->{alias}, $_ } ($source->primary_columns); + return @pcols ? \@pcols : $rs_args->{group_by}; +} + 1; =head1 NAME diff --git a/lib/DBIx/Class/Storage/Statistics.pm b/lib/DBIx/Class/Storage/Statistics.pm index 22dcadc..a81f606 100644 --- a/lib/DBIx/Class/Storage/Statistics.pm +++ b/lib/DBIx/Class/Storage/Statistics.pm @@ -89,6 +89,8 @@ Called when a transaction begins. sub txn_begin { my $self = shift; + return if $self->callback; + $self->print("BEGIN WORK\n"); } @@ -100,6 +102,8 @@ Called when a transaction is rolled back. sub txn_rollback { my $self = shift; + return if $self->callback; + $self->print("ROLLBACK\n"); } @@ -111,6 +115,8 @@ Called when a transaction is committed. sub txn_commit { my $self = shift; + return if $self->callback; + $self->print("COMMIT\n"); } @@ -122,6 +128,8 @@ Called when a savepoint is created. sub svp_begin { my ($self, $name) = @_; + return if $self->callback; + $self->print("SAVEPOINT $name\n"); } @@ -133,7 +141,9 @@ Called when a savepoint is released. sub svp_release { my ($self, $name) = @_; - $self->print("RELEASE SAVEPOINT $name\n"); + return if $self->callback; + + $self->print("RELEASE SAVEPOINT $name\n"); } =head2 svp_rollback @@ -144,7 +154,9 @@ Called when rolling back to a savepoint. sub svp_rollback { my ($self, $name) = @_; - $self->print("ROLLBACK TO SAVEPOINT $name\n"); + return if $self->callback; + + $self->print("ROLLBACK TO SAVEPOINT $name\n"); } =head2 query_start diff --git a/t/03podcoverage.t b/t/03podcoverage.t index a086559..fe8516b 100644 --- a/t/03podcoverage.t +++ b/t/03podcoverage.t @@ -116,6 +116,7 @@ my $exceptions = { 'DBIx::Class::Storage::DBI::Pg' => { skip => 1 }, 'DBIx::Class::Storage::DBI::SQLite' => { skip => 1 }, 'DBIx::Class::Storage::DBI::mysql' => { skip => 1 }, + 'DBIx::Class::SQLAHacks::MySQL' => { skip => 1 }, 'SQL::Translator::Parser::DBIx::Class' => { skip => 1 }, 'SQL::Translator::Producer::DBIx::Class::File' => { skip => 1 }, diff --git a/t/18insert_default.t b/t/18insert_default.t new file mode 100644 index 0000000..c3f9369 --- /dev/null +++ b/t/18insert_default.t @@ -0,0 +1,31 @@ +use strict; +use warnings; + +use Test::More; +use lib qw(t/lib); +use DBICTest; + +my $tests = 3; +plan tests => $tests; + +my $schema = DBICTest->init_schema(); +my $rs = $schema->resultset ('Artist'); +my $last_obj = $rs->search ({}, { order_by => { -desc => 'artistid' }, rows => 1})->single; +my $last_id = $last_obj ? $last_obj->artistid : 0; + +my $obj; +eval { $obj = $rs->create ({}) }; +my $err = $@; + +ok ($obj, 'Insert defaults ( $rs->create ({}) )' ); +SKIP: { + skip "Default insert failed: $err", $tests-1 if $err; + + # this should be picked up without calling the DB again + is ($obj->artistid, $last_id + 1, 'Autoinc PK works'); + + # for this we need to refresh + $obj->discard_changes; + is ($obj->rank, 13, 'Default value works'); +} + diff --git a/t/52cycle.t b/t/52cycle.t index 0c1e330..8be1768 100644 --- a/t/52cycle.t +++ b/t/52cycle.t @@ -5,9 +5,9 @@ use Test::More; use lib qw(t/lib); BEGIN { - eval { require Test::Memory::Cycle }; - if ($@) { - plan skip_all => "leak test needs Test::Memory::Cycle"; + eval { require Test::Memory::Cycle; require Devel::Cycle }; + if ($@ or Devel::Cycle->VERSION < 1.10) { + plan skip_all => "leak test needs Test::Memory::Cycle and Devel::Cycle >= 1.10"; } else { plan tests => 1; } diff --git a/t/71mysql.t b/t/71mysql.t index cf02a61..88d6ea2 100644 --- a/t/71mysql.t +++ b/t/71mysql.t @@ -14,7 +14,7 @@ my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MYSQL_${_}" } qw/DSN USER PASS/}; plan skip_all => 'Set $ENV{DBICTEST_MYSQL_DSN}, _USER and _PASS to run this test' unless ($dsn && $user); -plan tests => 23; +plan tests => 19; my $schema = DBICTest::Schema->connect($dsn, $user, $pass); @@ -114,7 +114,7 @@ $schema->populate ('BooksInLibrary', [ # (mysql doesn't seem to like subqueries with equally named columns) # -SKIP: { +{ # try a ->has_many direction (due to a 'multi' accessor the select/group_by group is collapsed) my $owners = $schema->resultset ('Owners')->search ( { 'books.id' => { '!=', undef }}, @@ -122,41 +122,19 @@ SKIP: { ); my $owners2 = $schema->resultset ('Owners')->search ({ id => { -in => $owners->get_column ('me.id')->as_query }}); for ($owners, $owners2) { - lives_ok { is ($_->all, 2, 'Prefetched grouped search returns correct number of rows') } - || skip ('No test due to exception', 1); - lives_ok { is ($_->count, 2, 'Prefetched grouped search returns correct count') } - || skip ('No test due to exception', 1); + is ($_->all, 2, 'Prefetched grouped search returns correct number of rows'); + is ($_->count, 2, 'Prefetched grouped search returns correct count'); } - TODO: { - # try a ->prefetch direction (no select collapse) - my $books = $schema->resultset ('BooksInLibrary')->search ( - { 'owner.name' => 'wiggle' }, - { prefetch => 'owner', distinct => 1 } - ); - - local $TODO = 'MySQL is crazy - there seems to be no way to make this work'; - # error thrown is: - # Duplicate column name 'id' [for Statement " - # SELECT COUNT( * ) - # FROM ( - # SELECT me.id, me.source, me.owner, me.title, me.price, owner.id, owner.name - # FROM books me - # JOIN owners owner ON owner.id = me.owner - # WHERE ( ( owner.name = ? AND source = ? ) ) - # GROUP BY me.id, me.source, me.owner, me.title, me.price, owner.id, owner.name - # ) count_subq - # " with ParamValues: 0='wiggle', 1='Library'] - # - # go fucking figure - - my $books2 = $schema->resultset ('BooksInLibrary')->search ({ id => { -in => $books->get_column ('me.id')->as_query }}); - for ($books, $books2) { - lives_ok { is ($_->all, 1, 'Prefetched grouped search returns correct number of rows') } - || skip ('No test due to exception', 1); - lives_ok { is ($_->count, 1, 'Prefetched grouped search returns correct count') } - || skip ('No test due to exception', 1); - } + # try a ->belongs_to direction (no select collapse) + my $books = $schema->resultset ('BooksInLibrary')->search ( + { 'owner.name' => 'wiggle' }, + { prefetch => 'owner', distinct => 1 } + ); + my $books2 = $schema->resultset ('BooksInLibrary')->search ({ id => { -in => $books->get_column ('me.id')->as_query }}); + for ($books, $books2) { + is ($_->all, 1, 'Prefetched grouped search returns correct number of rows'); + is ($_->count, 1, 'Prefetched grouped search returns correct count'); } } diff --git a/t/86sqlt.t b/t/86sqlt.t index 24d573e..4b89019 100644 --- a/t/86sqlt.t +++ b/t/86sqlt.t @@ -210,7 +210,7 @@ my %fk_constraints = ( 'name' => 'bookmark_fk_link', 'index_name' => 'bookmark_idx_link', 'selftable' => 'bookmark', 'foreigntable' => 'link', 'selfcols' => ['link'], 'foreigncols' => ['id'], - on_delete => '', on_update => '', deferrable => 1, + on_delete => 'SET NULL', on_update => 'CASCADE', deferrable => 1, }, ], # ForceForeign diff --git a/t/89inflate_datetime.t b/t/89inflate_datetime.t index ea7c930..d165c33 100644 --- a/t/89inflate_datetime.t +++ b/t/89inflate_datetime.t @@ -13,7 +13,7 @@ use DBICTest; my $schema = DBICTest->init_schema(); -plan tests => 53; +plan tests => 57; SKIP: { eval { require DateTime::Format::MySQL }; @@ -29,6 +29,18 @@ isa_ok($event->starts_at, 'DateTime', 'DateTime returned'); my $starts = $event->starts_at; is("$starts", '2006-04-25T22:24:33', 'Correct date/time'); +TODO: { + local $TODO = "We can't do this yet before 0.09" if DBIx::Class->VERSION < 0.09; + + ok(my $row = + $schema->resultset('Event')->search({ starts_at => $starts })->single); + is(eval { $row->id }, 1, 'DT in search'); + + ok($row = + $schema->resultset('Event')->search({ starts_at => { '>=' => $starts } })->single); + is(eval { $row->id }, 1, 'DT in search with condition'); +} + # create using DateTime my $created = $schema->resultset('Event')->create({ starts_at => DateTime->new(year=>2006, month=>6, day=>18), diff --git a/t/bind/bindtype_columns.t b/t/bind/bindtype_columns.t index 1462d9b..629185d 100644 --- a/t/bind/bindtype_columns.t +++ b/t/bind/bindtype_columns.t @@ -49,10 +49,7 @@ my $new; is($row->get_column('bytea'), $big_long_string, "Created the blob correctly."); } -TODO: { - local $TODO = - 'Passing bind attributes to $sth->bind_param() should be implemented (it only works in $storage->insert ATM)'; - +{ my $rs = $schema->resultset('BindType')->search({ bytea => $big_long_string }); # search on the bytea column (select) diff --git a/t/lib/DBICTest/AuthorCheck.pm b/t/lib/DBICTest/AuthorCheck.pm index 4d2a6f6..1e58b93 100644 --- a/t/lib/DBICTest/AuthorCheck.pm +++ b/t/lib/DBICTest/AuthorCheck.pm @@ -84,7 +84,7 @@ EOE sub _find_co_root { my @mod_parts = split /::/, (__PACKAGE__ . '.pm'); - my $rel_path = file (@mod_parts); + my $rel_path = join ('/', @mod_parts); # %INC stores paths with / regardless of OS return undef unless ($INC{$rel_path}); @@ -93,7 +93,7 @@ sub _find_co_root { # - do 'cd ..' as many times as necessary to get to t/lib/../.. my $root = dir ($INC{$rel_path}); - for (0 .. @mod_parts + 1) { + for (1 .. @mod_parts + 2) { $root = $root->parent; } diff --git a/t/lib/DBICTest/Schema/Bookmark.pm b/t/lib/DBICTest/Schema/Bookmark.pm index bb32a14..c468936 100644 --- a/t/lib/DBICTest/Schema/Bookmark.pm +++ b/t/lib/DBICTest/Schema/Bookmark.pm @@ -19,6 +19,6 @@ __PACKAGE__->add_columns( ); __PACKAGE__->set_primary_key('id'); -__PACKAGE__->belongs_to(link => 'DBICTest::Schema::Link' ); +__PACKAGE__->belongs_to(link => 'DBICTest::Schema::Link', 'link', { on_delete => 'SET NULL' } ); 1; diff --git a/t/lib/DBICTest/Schema/Link.pm b/t/lib/DBICTest/Schema/Link.pm index bf6d623..19b7aa0 100644 --- a/t/lib/DBICTest/Schema/Link.pm +++ b/t/lib/DBICTest/Schema/Link.pm @@ -25,6 +25,8 @@ __PACKAGE__->add_columns( ); __PACKAGE__->set_primary_key('id'); +__PACKAGE__->has_many ( bookmarks => 'DBICTest::Schema::Bookmark', 'link', { cascade_delete => 0 } ); + use overload '""' => sub { shift->url }, fallback=> 1; 1; diff --git a/t/96multi_create/cd_single.t b/t/multi_create/cd_single.t similarity index 100% rename from t/96multi_create/cd_single.t rename to t/multi_create/cd_single.t diff --git a/t/multi_create/insert_defaults.t b/t/multi_create/insert_defaults.t new file mode 100644 index 0000000..e8054a7 --- /dev/null +++ b/t/multi_create/insert_defaults.t @@ -0,0 +1,45 @@ +use strict; +use warnings; + +use Test::More; +use Test::Exception; +use lib qw(t/lib); +use DBICTest; + +plan tests => 8; + +my $schema = DBICTest->init_schema(); + +# Attempt sequential nested find_or_create with autoinc +# As a side effect re-test nested default create (both the main object and the relation are {}) +my $bookmark_rs = $schema->resultset('Bookmark'); +my $last_bookmark = $bookmark_rs->search ({}, { order_by => { -desc => 'id' }, rows => 1})->single; +my $last_link = $bookmark_rs->search_related ('link', {}, { order_by => { -desc => 'link.id' }, rows => 1})->single; + +# find_or_create a bookmark-link combo with data for a non-existing link +my $o1 = $bookmark_rs->find_or_create ({ link => { url => 'something-weird' } }); +is ($o1->id, $last_bookmark->id + 1, '1st bookmark ID'); +is ($o1->link->id, $last_link->id + 1, '1st related link ID'); + +# find_or_create a bookmark-link combo without any data at all (default insert) +# should extend this test to all available Storage's, and fix them accordingly +my $o2 = $bookmark_rs->find_or_create ({ link => {} }); +is ($o2->id, $last_bookmark->id + 2, '2nd bookmark ID'); +is ($o2->link->id, $last_link->id + 2, '2nd related link ID'); + +# make sure the pre-existing link has only one related bookmark +is ($last_link->bookmarks->count, 1, 'Expecting only 1 bookmark and 1 link, someone mucked with the table!'); + +# find_or_create a bookmark withouyt any data, but supplying an existing link object +# should return $last_bookmark +my $o0 = $bookmark_rs->find_or_create ({ link => $last_link }); +is_deeply ({ $o0->columns}, {$last_bookmark->columns}, 'Correctly identify a row given a relationship'); + +# inject an additional bookmark and repeat the test +# should warn and return the first row +my $o3 = $last_link->create_related ('bookmarks', {}); +is ($o3->id, $last_bookmark->id + 3, '3rd bookmark ID'); + +local $SIG{__WARN__} = sub { warn @_ unless $_[0] =~ /Query returned more than one row/ }; +my $oX = $bookmark_rs->find_or_create ({ link => $last_link }); +is_deeply ({ $oX->columns}, {$last_bookmark->columns}, 'Correctly identify a row given a relationship'); diff --git a/t/multi_create/m2m.t b/t/multi_create/m2m.t new file mode 100644 index 0000000..4275f58 --- /dev/null +++ b/t/multi_create/m2m.t @@ -0,0 +1,30 @@ +use strict; +use warnings; + +use Test::More; +use Test::Exception; +use lib qw(t/lib); +use DBICTest; + +plan tests => 4; + +my $schema = DBICTest->init_schema(); + +lives_ok ( sub { + + my $prod_rs = $schema->resultset ('Producer'); + my $prod_count = $prod_rs->count; + + my $cd = $schema->resultset('CD')->first; + $cd->add_to_producers ({name => 'new m2m producer'}); + + is ($prod_rs->count, $prod_count + 1, 'New producer created'); + ok ($cd->producers->find ({name => 'new m2m producer'}), 'Producer created with correct name'); + + my $cd2 = $schema->resultset('CD')->search ( { cdid => { '!=', $cd->cdid } }, {rows => 1} )->single; # retrieve a cd different from the first + $cd2->add_to_producers ({name => 'new m2m producer'}); # attach to an existing producer + ok ($cd2->producers->find ({name => 'new m2m producer'}), 'Exsiting producer attached to existing cd'); + +}, 'Test far-end find_or_create over many_to_many'); + +1; diff --git a/t/96multi_create/multilev_might_have_PKeqFK.t b/t/multi_create/multilev_might_have_PKeqFK.t similarity index 100% rename from t/96multi_create/multilev_might_have_PKeqFK.t rename to t/multi_create/multilev_might_have_PKeqFK.t diff --git a/t/multi_create/reentrance_count.t b/t/multi_create/reentrance_count.t new file mode 100644 index 0000000..0c0dca8 --- /dev/null +++ b/t/multi_create/reentrance_count.t @@ -0,0 +1,180 @@ +use strict; +use warnings; + +use Test::More; +use Test::Exception; +use lib qw(t/lib); +use DBICTest; + +plan 'no_plan'; + +my $schema = DBICTest->init_schema(); + +my $query_stats; +$schema->storage->debugcb (sub { push @{$query_stats->{$_[0]}}, $_[1] }); +$schema->storage->debug (1); + +TODO: { + local $TODO = 'This is an optimization task, will wait... a while'; + +lives_ok (sub { + undef $query_stats; + $schema->resultset('Artist')->create ({ + name => 'poor artist', + cds => [ + { + title => 'cd1', + year => 2001, + }, + { + title => 'cd2', + year => 2002, + }, + ], + }); + + is ( @{$query_stats->{INSERT} || []}, 3, 'number of inserts during creation of artist with 2 cds' ); + is ( @{$query_stats->{SELECT} || []}, 0, 'number of selects during creation of artist with 2 cds' ) + || $ENV{DBIC_MULTICREATE_DEBUG} && diag join "\n", @{$query_stats->{SELECT} || []}; +}); + + +lives_ok (sub { + undef $query_stats; + $schema->resultset('Artist')->create ({ + name => 'poorer artist', + cds => [ + { + title => 'cd3', + year => 2003, + genre => { name => 'vague genre' }, + }, + { + title => 'cd4', + year => 2004, + genre => { name => 'vague genre' }, + }, + ], + }); + + is ( @{$query_stats->{INSERT} || []}, 4, 'number of inserts during creation of artist with 2 cds, converging on the same genre' ); + is ( @{$query_stats->{SELECT} || []}, 0, 'number of selects during creation of artist with 2 cds, converging on the same genre' ) + || $ENV{DBIC_MULTICREATE_DEBUG} && diag join "\n", @{$query_stats->{SELECT} || []}; +}); + + +lives_ok (sub { + my $genre = $schema->resultset('Genre')->first; + undef $query_stats; + $schema->resultset('Artist')->create ({ + name => 'poorest artist', + cds => [ + { + title => 'cd5', + year => 2005, + genre => $genre, + }, + { + title => 'cd6', + year => 2004, + genre => $genre, + }, + ], + }); + + is ( @{$query_stats->{INSERT} || []}, 3, 'number of inserts during creation of artist with 2 cds, converging on the same existing genre' ); + is ( @{$query_stats->{SELECT} || []}, 0, 'number of selects during creation of artist with 2 cds, converging on the same existing genre' ) + || $ENV{DBIC_MULTICREATE_DEBUG} && diag join "\n", @{$query_stats->{SELECT} || []}; +}); + + +lives_ok (sub { + undef $query_stats; + $schema->resultset('Artist')->create ({ + name => 'poorer than the poorest artist', + cds => [ + { + title => 'cd7', + year => 2007, + cd_to_producer => [ + { + producer => { + name => 'jolly producer', + producer_to_cd => [ + { + cd => { + title => 'cd8', + year => 2008, + artist => { + name => 'poorer than the poorest artist', + }, + }, + }, + ], + }, + }, + ], + }, + ], + }); + + is ( @{$query_stats->{INSERT} || []}, 6, 'number of inserts during creation of artist->cd->producer->cd->same_artist' ); + is ( @{$query_stats->{SELECT} || []}, 0, 'number of selects during creation of artist->cd->producer->cd->same_artist' ) + || $ENV{DBIC_MULTICREATE_DEBUG} && diag join "\n", @{$query_stats->{SELECT} || []}; +}); + +lives_ok (sub { + undef $query_stats; + $schema->resultset ('Artist')->find(1)->create_related (cds => { + title => 'cd9', + year => 2009, + cd_to_producer => [ + { + producer => { + name => 'jolly producer', + producer_to_cd => [ + { + cd => { + title => 'cd10', + year => 2010, + artist => { + name => 'poorer than the poorest artist', + }, + }, + }, + ], + }, + }, + ], + }); + + is ( @{$query_stats->{INSERT} || []}, 4, 'number of inserts during creation of existing_artist->cd->existing_producer->cd->existing_artist2' ); + is ( @{$query_stats->{SELECT} || []}, 0, 'number of selects during creation of existing_artist->cd->existing_producer->cd->existing_artist2' ) + || $ENV{DBIC_MULTICREATE_DEBUG} && diag join "\n", @{$query_stats->{SELECT} || []}; +}); + +lives_ok (sub { + undef $query_stats; + + my $artist = $schema->resultset ('Artist')->first; + my $producer = $schema->resultset ('Producer')->first; + + $schema->resultset ('CD')->create ({ + title => 'cd11', + year => 2011, + artist => $artist, + cd_to_producer => [ + { + producer => $producer, + }, + ], + }); + + is ( @{$query_stats->{INSERT} || []}, 2, 'number of inserts during creation of artist_object->cd->producer_object' ); + is ( @{$query_stats->{SELECT} || []}, 0, 'number of selects during creation of artist_object->cd->producer_object' ) + || $ENV{DBIC_MULTICREATE_DEBUG} && diag join "\n", @{$query_stats->{SELECT} || []}; +}); + +} + +1; diff --git a/t/prefetch/rows_bug.t b/t/prefetch/rows_bug.t index 1457013..21bb6a2 100644 --- a/t/prefetch/rows_bug.t +++ b/t/prefetch/rows_bug.t @@ -7,7 +7,8 @@ use Test::More; use lib qw(t/lib); use DBICTest; -plan $@ ? (skip_all => 'needs DBD::SQLite for testing') : (tests => 2); +plan skip_all => 'fix pending'; +#plan tests => 4; my $schema = DBICTest->init_schema(); my $no_prefetch = $schema->resultset('Artist')->search( @@ -23,61 +24,45 @@ my $use_prefetch = $schema->resultset('Artist')->search( } ); -my $no_prefetch_count = 0; -my $use_prefetch_count = 0; - is($no_prefetch->count, $use_prefetch->count, '$no_prefetch->count == $use_prefetch->count'); +is( + scalar ($no_prefetch->all), + scalar ($use_prefetch->all), + "Amount of returned rows is right" +); + -TODO: { - local $TODO = "This is a difficult bug to fix, workaround is not to use prefetch with rows"; - $no_prefetch_count++ while $no_prefetch->next; - $use_prefetch_count++ while $use_prefetch->next; - is( - $no_prefetch_count, - $use_prefetch_count, - "manual row count confirms consistency" - . " (\$no_prefetch_count == $no_prefetch_count, " - . " \$use_prefetch_count == $use_prefetch_count)" - ); -} -__END__ -The fix is to, when using prefetch, take the query and put it into a subquery -joined to the tables we're prefetching from. This might result in the same -table being joined once in the main subquery and once in the main query. This -may actually resolve other, unknown edgecase bugs. It is also the right way -to do prefetching. Optimizations can come later. +my $artist_many_cds = $schema->resultset('Artist')->search ( {}, { + join => 'cds', + group_by => 'me.artistid', + having => \ 'count(cds.cdid) > 1', +})->first; + + +$no_prefetch = $schema->resultset('Artist')->search( + { artistid => $artist_many_cds->id }, + { rows => 1 } +); -This means that: - $foo_rs->search( - { ... }, - { - prefetch => 'bar', - ... - }, - ); +$use_prefetch = $schema->resultset('Artist')->search( + { artistid => $artist_many_cds->id }, + { + prefetch => 'cds', + rows => 1 + } +); -becomes: - my $temp = $foo_rs->search( - { ... }, - { - join => 'bar', - ... - }, - ); - $foo_rs->storage->schema->resultset('foo')->search( - undef, - { - from => [ - { me => $temp->as_query }, - ], - prefetch => 'bar', - }, - ); +my $prefetch_artist = $use_prefetch->first; +my $normal_artist = $no_prefetch->first; -Problem: - * The prefetch->join change needs to happen ONLY IF there are conditions - that depend on bar being joined. - * How will this work when the $rs is further searched on? Those clauses - need to be added to the subquery, not the outer one. This is particularly - true if rows is added in the attribute later per the Pager. +is( + $prefetch_artist->cds->count, + $normal_artist->cds->count, + "Count of child rel with prefetch + rows => 1 is right" +); +is ( + scalar ($prefetch_artist->cds->all), + scalar ($normal_artist->cds->all), + "Amount of child rel rows with prefetch + rows => 1 is right" +);