order stability testing
Matt S Trout [Fri, 20 Apr 2012 09:17:45 +0000 (09:17 +0000)]
lib/DBIx/Class/ResultSet.pm
lib/DBIx/Class/SQLMaker.pm
lib/DBIx/Class/SQLMaker/Converter.pm
lib/DBIx/Class/SQLMaker/SQLite.pm
lib/DBIx/Class/Storage/DBIHacks.pm
t/sqlmaker/limit_dialects/fetch_first.t

index 43c06fb..ed2a3f3 100644 (file)
@@ -1371,11 +1371,9 @@ sub _construct_objects {
       my @order_by_dq = $self->_extract_by_from_order_by(
         $conv->_order_by_to_dq($attrs->{order_by})
       );
-      my @ord_cols = map {
-        $_->{type} eq DQ_IDENTIFIER
-          ? join('.',@{$_->{elements}})
-          : '__TOTALLY_BOGUS_DUDE__'
-      } @order_by_dq;
+      my @ord_cols = map { join('.',@{$_->{elements}}) }
+                       grep { $_->{type} eq DQ_IDENTIFIER }
+                         @order_by_dq;
 
       my $colinfos = $st->_resolve_column_info($attrs->{from}, \@ord_cols);
 
index 83f5da8..88ede1e 100644 (file)
@@ -49,6 +49,12 @@ use Moo;
 
 has limit_dialect => (is => 'rw', trigger => sub { shift->clear_renderer });
 
+has limit_requires_order_by_stability_check
+  => (is => 'rw', default => sub { 0 });
+
+has limit_enforces_order_by_stability
+  => (is => 'rw', default => sub { 0 });
+
 # for when I need a normalized l/r pair
 sub _quote_chars {
   map
@@ -57,7 +63,9 @@ sub _quote_chars {
   ;
 }
 
-sub converter_class { 'DBIx::Class::SQLMaker::Converter' }
+sub _build_converter_class {
+  Module::Runtime::use_module('DBIx::Class::SQLMaker::Converter')
+}
 
 # FIXME when we bring in the storage weaklink, check its schema
 # weaklink and channel through $schema->throw_exception
@@ -129,7 +137,39 @@ sub select {
     $limit = $self->__max_int;
   }
 
-  my ($sql, @bind) = $self->next::method ($table, $fields, $where, $rs_attrs->{order_by}, { %{$rs_attrs}, limit => $limit, offset => $offset } );
+  my %final_attrs = (%{$rs_attrs}, limit => $limit, offset => $offset);
+
+  if ($offset and $self->limit_requires_order_by_stability_check) {
+     my $source = $rs_attrs->{_rsroot_rsrc};
+    unless (
+      $final_attrs{order_is_stable}
+      = $source->schema->storage
+               ->_order_by_is_stable(
+                   @final_attrs{qw(from order_by where)}
+                 )
+    ) {
+      if ($self->limit_enforces_order_by_stability) {
+        if ($self->converter->_order_by_to_dq($final_attrs{order_by})) {
+          $self->throw_exception(
+            'Current limit/offset implementation requires a stable order for offset'
+          );
+        }
+        if (my $ident_cols = $source->_identifying_column_set) {
+          $final_attrs{order_by} = [
+            map "$final_attrs{alias}.$_", @$ident_cols
+          ];
+          $final_attrs{order_is_stable} = 1;
+        } else {
+          $self->throw_exception(sprintf(
+            'Unable to auto-construct stable order criteria for "skimming type" 
+limit '
+          . "dialect based on source '%s'", $source->name) );
+        }
+      }
+    }
+  }
+
+  my ($sql, @bind) = $self->next::method ($table, $fields, $where, $final_attrs{order_by}, \%final_attrs );
 
   $sql .= $self->_lock_select ($rs_attrs->{for})
     if $rs_attrs->{for};
index d927c75..82fd8b9 100644 (file)
@@ -3,7 +3,6 @@ package DBIx::Class::SQLMaker::Converter;
 use Data::Query::Constants qw(DQ_ALIAS DQ_GROUP DQ_WHERE DQ_JOIN DQ_SLICE);
 use Moo;
 
-require SQL::Abstract::Converter; # XXX Moo bug caused by the local
 extends 'SQL::Abstract::Converter';
 
 around _select_to_dq => sub {
@@ -27,6 +26,9 @@ around _select_to_dq => sub {
         })
       : ()
     ),
+    ($attrs->{order_is_stable}
+      ? (order_is_stable => 1)
+      : ())
   };
 };
 
index f905b46..ce719ba 100644 (file)
@@ -3,7 +3,9 @@ package # Hide from PAUSE
 
 use base qw( DBIx::Class::SQLMaker );
 
-sub renderer_class { 'Data::Query::Renderer::SQL::SQLite' }
+sub _build_renderer_class {
+  Module::Runtime::use_module('Data::Query::Renderer::SQL::SQLite')
+}
 
 #
 # SQLite does not understand SELECT ... FOR UPDATE
index a5ce102..633836a 100644 (file)
@@ -17,7 +17,7 @@ use List::Util 'first';
 use Scalar::Util 'blessed';
 use Sub::Name 'subname';
 use Data::Query::Constants qw(
-  DQ_ALIAS DQ_JOIN DQ_IDENTIFIER DQ_ORDER DQ_LITERAL
+  DQ_ALIAS DQ_JOIN DQ_IDENTIFIER DQ_ORDER DQ_LITERAL DQ_OPERATOR
 );
 use namespace::clean;
 
@@ -705,9 +705,27 @@ sub _extract_order_criteria {
 sub _order_by_is_stable {
   my ($self, $ident, $order_by, $where) = @_;
 
+  my @ident_dq;
+  my $conv = $self->sql_maker->converter;
+
+  $self->_scan_identifiers(
+    sub { push @ident_dq, $_[0] }, $conv->_order_by_to_dq($order_by)
+  );
+
+  if ($where) {
+    # old _extract_fixed_condition_columns logic
+    $self->_scan_nodes({
+      DQ_OPERATOR ,=> sub {
+        if (($_[0]->{operator}{'SQL.Naive'}||'') eq '=') {
+          push @ident_dq,
+            grep { $_->{type} eq DQ_IDENTIFIER } @{$_[0]->{args}};
+        }
+      }
+    }, $conv->_where_to_dq($where));
+  }
+
   my $colinfo = $self->_resolve_column_info($ident, [
-    (map { $_->[0] } $self->_extract_order_criteria($order_by)),
-    $where ? @{$self->_extract_fixed_condition_columns($where)} :(),
+    map { join('.', @{$_->{elements}}) } @ident_dq
   ]);
 
   return undef unless keys %$colinfo;
@@ -723,41 +741,4 @@ sub _order_by_is_stable {
   return undef;
 }
 
-# returns an arrayref of column names which *definitely* have som
-# sort of non-nullable equality requested in the given condition
-# specification. This is used to figure out if a resultset is
-# constrained to a column which is part of a unique constraint,
-# which in turn allows us to better predict how ordering will behave
-# etc.
-#
-# this is a rudimentary, incomplete, and error-prone extractor
-# however this is OK - it is conservative, and if we can not find
-# something that is in fact there - the stack will recover gracefully
-# Also - DQ and the mst it rode in on will save us all RSN!!!
-sub _extract_fixed_condition_columns {
-  my ($self, $where, $nested) = @_;
-
-  return unless ref $where eq 'HASH';
-
-  my @cols;
-  for my $lhs (keys %$where) {
-    if ($lhs =~ /^\-and$/i) {
-      push @cols, ref $where->{$lhs} eq 'ARRAY'
-        ? ( map { $self->_extract_fixed_condition_columns($_, 1) } @{$where->{$lhs}} )
-        : $self->_extract_fixed_condition_columns($where->{$lhs}, 1)
-      ;
-    }
-    elsif ($lhs !~ /^\-/) {
-      my $val = $where->{$lhs};
-
-      push @cols, $lhs if (defined $val and (
-        ! ref $val
-          or
-        (ref $val eq 'HASH' and keys %$val == 1 and defined $val->{'='})
-      ));
-    }
-  }
-  return $nested ? @cols : \@cols;
-}
-
 1;
index 302201c..b7dc463 100644 (file)
@@ -10,7 +10,14 @@ my $schema = DBICTest->init_schema;
 
 # based on toplimit.t
 delete $schema->storage->_sql_maker->{_cached_syntax};
-$schema->storage->_sql_maker->limit_dialect ('FetchFirst');
+$schema->storage->_sql_maker->renderer_class(
+  Moo::Role->create_class_with_roles(qw(
+    Data::Query::Renderer::SQL::Naive
+    Data::Query::Renderer::SQL::Slice::FetchFirst
+  ))
+);
+$schema->storage->_sql_maker->limit_requires_order_by_stability_check(1);
+$schema->storage->_sql_maker->limit_enforces_order_by_stability(1);
 
 my $books_45_and_owners = $schema->resultset ('BooksInLibrary')->search ({}, {
   prefetch => 'owner', rows => 2, offset => 3,