Slice implementations as roles
Matt S Trout [Fri, 20 Apr 2012 10:03:30 +0000 (10:03 +0000)]
lib/Data/Query/Renderer/SQL/Naive.pm
lib/Data/Query/Renderer/SQL/SQLite.pm
lib/Data/Query/Renderer/SQL/Slice/FetchFirst.pm [new file with mode: 0644]
lib/Data/Query/Renderer/SQL/Slice/LimitOffset.pm [new file with mode: 0644]

index 1fdd5ef..c4ac4b3 100644 (file)
@@ -7,7 +7,7 @@ sub intersperse { my $i = shift; my @i = map +($_, $i), @_; pop @i; @i }
 use SQL::ReservedWords;
 use Data::Query::Constants qw(
   DQ_IDENTIFIER DQ_OPERATOR DQ_VALUE DQ_JOIN DQ_ALIAS DQ_ORDER DQ_LITERAL
-  DQ_GROUP
+  DQ_GROUP DQ_SELECT DQ_SLICE
 );
 
 use Moo;
@@ -287,7 +287,7 @@ sub _render_select {
     ($dq->{from}
        ? ($self->_format_keyword('FROM'), @{$self->_render($dq->{from})})
        : ()
-    )
+    ),
   ];
 }
 
@@ -302,8 +302,11 @@ sub _render_alias {
       }
     }
   }
-  return [
-    $self->_render($dq->{from}),
+  my %parenthesise = map +($_ => 1), DQ_SELECT, DQ_SLICE;
+  return [ # XXX not sure this is the right place to detect this
+    ($parenthesise{$dq->{from}{type}}
+      ? [ '(', $self->_render($dq->{from}), ')' ]
+      : $self->_render($dq->{from})),
     $as || ' ',
     $self->_render_identifier({ elements => [ $dq->{to} ] })
   ];
@@ -363,7 +366,9 @@ sub _render_order {
   my @ret = (
     $self->_format_keyword('ORDER BY'),
     $self->_render($dq->{by}),
-    ($dq->{direction} ? $self->_format_keyword($dq->{direction}) : ())
+    (defined $dq->{reverse}
+      ? $self->_format_keyword($dq->{reverse} ? 'DESC' : 'ASC')
+      : ())
   );
   my $from;
   while ($from = $dq->{from}) {
@@ -372,7 +377,9 @@ sub _render_order {
     push @ret, (
       ',',
       $self->_render($dq->{by}),
-      ($dq->{direction} ? $self->_format_keyword($dq->{direction}) : ())
+      (exists $dq->{reverse}
+        ? $self->_format_keyword($dq->{reverse} ? 'DESC' : 'ASC')
+        : ())
     );
   }
   unshift @ret, $self->_render($from) if $from;
index 4dac236..c44b2cb 100644 (file)
@@ -4,15 +4,6 @@ use Moo;
 
 extends 'Data::Query::Renderer::SQL::Naive';
 
-sub _render_slice {
-  my ($self, $dq) = @_;
-  [ ($dq->{from} ? $self->_render($dq->{from}) : ()),
-    $self->_format_keyword('LIMIT'), $self->_render($dq->{limit}),
-    ($dq->{offset}
-      ? ($self->_format_keyword('OFFSET'), $self->_render($dq->{offset}))
-      : ()
-    ),
-  ];
-}
+with 'Data::Query::Renderer::SQL::Slice::LimitOffset';
 
 1;
diff --git a/lib/Data/Query/Renderer/SQL/Slice/FetchFirst.pm b/lib/Data/Query/Renderer/SQL/Slice/FetchFirst.pm
new file mode 100644 (file)
index 0000000..6dd07fe
--- /dev/null
@@ -0,0 +1,155 @@
+package Data::Query::Renderer::SQL::Slice::FetchFirst;
+
+use Data::Query::Constants qw(
+  DQ_SELECT DQ_ALIAS DQ_IDENTIFIER DQ_ORDER DQ_SLICE
+);
+use Moo::Role;
+
+sub _render_slice {
+  my ($self, $dq) = @_;
+  unless ($dq->{offset}) {
+    return [
+      ($dq->{from} ? $self->_render($dq->{from}) : ()),
+      $self->_format_keyword('FETCH FIRST'),
+      sprintf("%i", $dq->{limit}{value}),
+      $self->_format_keyword('ROWS ONLY')
+    ];
+  }
+  unless ($dq->{order_is_stable}) {
+    die "FetchFirst limit style requires a stable order";
+  }
+  die "Slice's inner is not a Select"
+    unless (my $orig_select = $dq->{from})->{type} eq DQ_SELECT;
+  my %alias_map;
+  my $gensym_count;
+  my (@inside_select_list, @outside_select_list);
+  my $default_inside_alias;
+  SELECT: foreach my $s (@{$orig_select->{select}}) {
+    my $name;
+    if ($s->{type} eq DQ_ALIAS) {
+      $name = $s->{to};
+      $s = $s->{from};
+    }
+    my $key;
+    if ($s->{type} eq DQ_IDENTIFIER) {
+      if (@{$s->{elements}} == 2) {
+        $default_inside_alias ||= $s->{elements}[0];
+        if ($s->{elements}[0] eq $default_inside_alias) {
+          $alias_map{join('.',@{$s->{elements}})} = $s;
+          push @inside_select_list, $s;
+          push @outside_select_list, $s;
+          next SELECT;
+        }
+      }
+      $name ||= join('__', @{$s->{elements}});
+      $key = join('.', @{$s->{elements}});
+    } else {
+      die "XXX not implemented yet";
+    }
+    $name ||= 'GENSYM__'.++$gensym_count;
+    push @inside_select_list, +{
+      type => DQ_ALIAS,
+      from => $s,
+      to => $name,
+    };
+    push @outside_select_list, $alias_map{$key} = +{
+      type => DQ_IDENTIFIER,
+      elements => [ $name ]
+    };
+  }
+  my $order = $orig_select->{from};
+  my $order_gensym_count;
+  die "Slice's Select not followed by Order but order_is_stable set"
+    unless $order->{type} eq DQ_ORDER;
+  my (@order_nodes, %order_map);
+  while ($order->{type} eq DQ_ORDER) {
+    my $by = $order->{by};
+    if ($by->{type} eq DQ_IDENTIFIER) {
+      $order_map{$by}
+        = $alias_map{join('.', @{$by->{elements}})}
+          ||= do {
+                my $name = 'ORDER__BY__'.++$order_gensym_count;
+                push @inside_select_list, +{
+                  type => DQ_ALIAS,
+                  from => $by,
+                  to => $name
+                };
+                +{
+                  type => DQ_IDENTIFIER,
+                  elements => [ $name ],
+                };
+              };
+    } else {
+      die "XXX not implemented yet";
+    }
+    push @order_nodes, $order;
+    $order = $order->{from};
+  }
+  my $inside_order = $order;
+  $inside_order = +{
+    type => DQ_ORDER,
+    by => $_->{by},
+    reverse => $_->{reverse},
+    from => $inside_order
+  } for reverse @order_nodes;
+  my $inside_select = +{
+    type => DQ_SELECT,
+    select => \@inside_select_list,
+    from => $inside_order,
+  };
+  my $limit_plus_offset = +{
+    %{$dq->{limit}}, value => $dq->{limit}{value} + $dq->{offset}{value}
+  };
+  $default_inside_alias ||= 'me';
+  my $bridge_from = +{
+    type => DQ_ALIAS,
+    to => $default_inside_alias,
+    from => {
+      type => DQ_SLICE,
+      limit => $limit_plus_offset,
+      from => $inside_select,
+    },
+  };
+  my $outside_order = $bridge_from;
+  $outside_order = +{
+    type => DQ_ORDER,
+    by => $order_map{$_->{by}},
+    reverse => !$_->{reverse},
+    from => $outside_order
+  } for reverse @order_nodes;
+  my $outside_select = +{
+    type => DQ_SELECT,
+    select => (
+      $dq->{preserve_order}
+        ? [ @outside_select_list, @order_map{map $_->{by}, @order_nodes} ]
+        : \@outside_select_list,
+    ),
+    from => $outside_order,
+  };
+  my $final = {
+    type => DQ_SLICE,
+    limit => $dq->{limit},
+    from => $outside_select
+  };
+  if ($dq->{preserve_order}) {
+    $final = {
+      type => DQ_ALIAS,
+      from => $final,
+      to => $default_inside_alias,
+    };
+    $final = +{
+      type => DQ_ORDER,
+      by => $order_map{$_->{by}},
+      reverse => $_->{reverse},
+      from => $final
+    } for reverse @order_nodes;
+    $final = {
+      type => DQ_SELECT,
+      select => \@outside_select_list,
+      from => $final,
+    };
+  }
+  return $self->_render($final);
+}
+
+1;
diff --git a/lib/Data/Query/Renderer/SQL/Slice/LimitOffset.pm b/lib/Data/Query/Renderer/SQL/Slice/LimitOffset.pm
new file mode 100644 (file)
index 0000000..54ef3f7
--- /dev/null
@@ -0,0 +1,15 @@
+package Data::Query::Renderer::SQL::Slice::LimitOffset;
+
+use Moo::Role;
+
+sub _render_slice {
+  my ($self, $dq) = @_;
+  [ ($dq->{from} ? $self->_render($dq->{from}) : ()),
+    $self->_format_keyword('LIMIT'), $self->_render($dq->{limit}),
+    ($dq->{offset}
+      ? ($self->_format_keyword('OFFSET'), $self->_render($dq->{offset}))
+      : ()),
+  ];
+}
+
+1;