Merge branch 'q'
Matt S Trout [Thu, 21 Jan 2021 20:33:30 +0000 (20:33 +0000)]
28 files changed:
Changes
examples/bangdbic.pl [new file with mode: 0644]
examples/sqla2passthrough.pl [new file with mode: 0644]
lib/DBIx/Class/SQLMaker/Role/SQLA2Passthrough.pm [new file with mode: 0644]
lib/SQL/Abstract.pm
lib/SQL/Abstract/Formatter.pm [new file with mode: 0644]
lib/SQL/Abstract/Parts.pm [new file with mode: 0644]
lib/SQL/Abstract/Plugin/BangOverrides.pm [new file with mode: 0644]
lib/SQL/Abstract/Plugin/ExtraClauses.pm [new file with mode: 0644]
lib/SQL/Abstract/Reference.pm [new file with mode: 0644]
lib/SQL/Abstract/Role/Plugin.pm [new file with mode: 0644]
lib/SQL/Abstract/Test.pm
maint/inplace [new file with mode: 0755]
maint/lib/Chunkstrumenter.pm [new file with mode: 0644]
maint/podregen [new file with mode: 0755]
maint/sqlacexpr [new file with mode: 0755]
maint/sqlaexpr [new file with mode: 0755]
t/00new.t
t/01generate.t
t/02where.t
t/04modifiers.t
t/05in_between.t
t/06order_by.t
t/08special_ops.t
t/21op_ident.t
t/24order_by_chunks.t [new file with mode: 0644]
t/80extra_clauses.t [new file with mode: 0644]
xt/91podcoverage.t

diff --git a/Changes b/Changes
index 25ffbdb..a65608d 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,5 +1,18 @@
 Revision history for SQL::Abstract
 
+  - Collapse custom join conditions back to something DBIC might understand
+
+1.90_03 - 2019-10-13
+   - Add proof of concept DBIx::Class::SQLMaker::Role::SQLA2Passthrough
+   - _where_field_IN/BETWEEN are documented as subclassable; feature restored
+
+1.90_02 - 2019-10-12
+    - fix DBIC ident op expander compat wrapper to handle call as unop
+
+1.90_01 - 2019-10-09
+    - Complete overhaul of the internals, see the SQL::Abstract::Reference
+      docs to understand the new implementation's affordances.
+
 1.87 - 2020-06-02
     - Add runtime dependency on Test::Deep and Test::Builder::Module for
       SQL::Abstract::Test (RT#131623)
diff --git a/examples/bangdbic.pl b/examples/bangdbic.pl
new file mode 100644 (file)
index 0000000..72815d1
--- /dev/null
@@ -0,0 +1,56 @@
+use strict;
+use warnings;
+{
+  package MySchema;
+  use Object::Tap;
+  use base qw(DBIx::Class::Schema);
+  use DBIx::Class::ResultSource::Table;
+  __PACKAGE__->register_source(
+    Foo => DBIx::Class::ResultSource::Table->new({ name => 'foo' })
+             ->$_tap(add_columns => qw(x y z))
+  );
+  __PACKAGE__->register_source(
+    Bar => DBIx::Class::ResultSource::Table->new({ name => 'bar' })
+             ->$_tap(add_columns => qw(a b c))
+  );
+}
+
+my $s = MySchema->connect('dbi:SQLite:dbname=:memory:');
+
+my $rs = $s->resultset('Foo')->search({ z => 1 });
+
+warn ${$rs->as_query}->[0]."\n";
+
+$s->storage->ensure_connected;
+
+$s->storage
+  ->sql_maker->plugin('+ExtraClauses')->plugin('+BangOverrides');
+
+my $rs2 = $s->resultset('Foo')->search({
+  -op => [ '=', { -ident => 'outer.y' }, { -ident => 'me.x' } ]
+});
+
+warn ${$rs2->as_query}->[0]."\n";
+
+my $rs3 = $rs2->search({}, {
+  '!from' => sub { my ($sqla, $from) = @_;
+    my $base = $sqla->expand_expr({ -old_from => $from });
+    return [ $base, -join => [ 'wub', on => [ 'me.z' => 'wub.z' ] ] ];
+  }
+});
+
+warn ${$rs3->as_query}->[0]."\n";
+
+my $rs4 = $rs3->search({}, {
+  '!with' => [ [ qw(wub x y z) ], $s->resultset('Bar')->as_query ],
+});
+
+warn ${$rs4->as_query}->[0]."\n";
+
+my $rs5 = $rs->search({}, { select => [ { -coalesce => [ { -ident => 'x' }, { -value => 7 } ] } ] });
+
+warn ${$rs5->as_query}->[0]."\n";
+
+my $rs6 = $rs->search({}, { '!select' => [ { -coalesce => [ { -ident => 'x' }, { -value => 7 } ] } ] });
+
+warn ${$rs6->as_query}->[0]."\n";
diff --git a/examples/sqla2passthrough.pl b/examples/sqla2passthrough.pl
new file mode 100644 (file)
index 0000000..8a6d051
--- /dev/null
@@ -0,0 +1,67 @@
+use strict;
+use warnings;
+use Devel::Dwarn;
+use With::Roles;
+{
+  package MySchema;
+  use Object::Tap;
+  use base qw(DBIx::Class::Schema);
+  use DBIx::Class::ResultSource::Table;
+  __PACKAGE__->register_source(
+    Foo => DBIx::Class::ResultSource::Table->new({ name => 'foo' })
+             ->$_tap(add_columns => qw(x y z))
+  );
+  __PACKAGE__->register_source(
+    Bar => DBIx::Class::ResultSource::Table->new({ name => 'bar' })
+             ->$_tap(add_columns => qw(x y1 y2 z))
+  );
+}
+{
+  package MyScratchpad;
+  use DBIx::Class::SQLMaker::Role::SQLA2Passthrough qw(on);
+  MySchema->source('Foo')->add_relationship(bars => 'Bar' => on {
+    +{ 'foreign.x' => 'self.x',
+       'foreign.y1' => { '<=', 'self.y' },
+       'foreign.y2' => { '>=', 'self.y' },
+    };
+  });
+}
+
+my $s = MySchema->connect('dbi:SQLite:dbname=:memory:');
+::Dwarn([ $s->source('Foo')->columns ]);
+
+my $rs = $s->resultset('Foo')->search({ z => 1 });
+
+::Dwarn(${$rs->as_query}->[0]);
+
+$s->storage->ensure_connected;
+
+$s->storage
+  ->sql_maker
+  ->with::roles('DBIx::Class::SQLMaker::Role::SQLA2Passthrough')
+  ->plugin('+ExtraClauses')
+  ->plugin('+BangOverrides');
+
+warn ref($s->storage->sql_maker);
+
+my $rs2 = $s->resultset('Foo')->search({
+  -op => [ '=', { -ident => 'outer.x' }, { -ident => 'me.y' } ]
+}, {
+  'select' => [ 'me.x', { -ident => 'me.z' } ],
+  '!with' => [ outer => $rs->get_column('x')->as_query ],
+});
+
+::Dwarn(${$rs2->as_query}->[0]);
+
+my $rs3 = $s->resultset('Foo')
+            ->search({}, { prefetch => 'bars' });
+
+::Dwarn(${$rs3->as_query}->[0]);
+
+$s->source('Foo')->result_class('DBIx::Class::Core');
+$s->source('Foo')->set_primary_key('x');
+
+my $rs4 = $s->resultset('Foo')->new_result({ x => 1, y => 2 })
+            ->search_related('bars');
+
+::Dwarn(${$rs4->as_query}->[0]);
diff --git a/lib/DBIx/Class/SQLMaker/Role/SQLA2Passthrough.pm b/lib/DBIx/Class/SQLMaker/Role/SQLA2Passthrough.pm
new file mode 100644 (file)
index 0000000..7ff513a
--- /dev/null
@@ -0,0 +1,209 @@
+package DBIx::Class::SQLMaker::Role::SQLA2Passthrough;
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+our @EXPORT = qw(on);
+
+sub on (&) {
+  my ($on) = @_;
+  sub {
+    my ($args) = @_;
+    $args->{self_resultsource}
+         ->schema->storage->sql_maker
+         ->expand_join_condition(
+             $on->($args),
+             $args
+           );
+  }
+}
+
+use Role::Tiny;
+
+around select => sub {
+  my ($orig, $self, $table, $fields, $where, $rs_attrs, $limit, $offset) = @_;
+
+  $fields = \[ $self->render_expr({ -list => [
+    grep defined,
+    map +(ref($_) eq 'HASH'
+          ? do {
+              my %f = %$_;
+              my $as = delete $f{-as};
+              my ($f, $rhs) = %f;
+              my $func = +{ ($f =~ /^-/ ? $f : "-${f}") => $rhs };
+              ($as
+                ? +{ -op => [ 'as', $func, { -ident => [ $as ] } ] }
+                : $func)
+            }
+          : $_), ref($fields) eq 'ARRAY' ? @$fields : $fields
+  ] }, -ident) ];
+
+  if (my $gb = $rs_attrs->{group_by}) {
+    $rs_attrs = {
+      %$rs_attrs,
+      group_by => \[ $self->render_expr({ -list => $gb }, -ident) ]
+    };
+  }
+  $self->$orig($table, $fields, $where, $rs_attrs, $limit, $offset);
+};
+
+sub expand_join_condition {
+  my ($self, $cond, $args) = @_;
+  my ($type, %known) = do {
+    if (my $obj = $args->{self_result_object}) {
+      (self => $obj->get_columns)
+    } elsif (my $val = $args->{foreign_values}) {
+      (foreign => %$val)
+    } else {
+      ('')
+    }
+  };
+  my $maybe = $type ? 1 : 0;
+  my $outside;
+  my $wrap = sub {
+    my ($orig) = @_;
+    $outside = $orig;
+    sub {
+      my $res = $orig->(@_);
+      my ($name, $col) = @{$res->{-ident}};
+      if ($name eq 'self' or $name eq 'foreign') {
+        if ($type eq $name) {
+          $maybe = 0 unless exists $known{$col};
+        }
+        return { -ident => [ $args->{"${name}_alias"}, $col ] };
+      }
+      return $res;
+    };
+  };
+  my $sqla = $self->clone->wrap_op_expander(ident => $wrap);
+  my $aqt = $sqla->expand_expr($cond, -ident);
+  return $aqt unless $maybe;
+  my $inner_wrap = sub {
+    my $res = $outside->(@_);
+    my ($name, $col) = @{$res->{-ident}};
+    if ($name eq 'self' or $name eq 'foreign') {
+      if ($type eq $name) {
+        return { -bind => [ $args->{"${name}_alias"}.'.'.$col, $known{$col} ] };
+      }
+      return { -ident => [ $args->{"${name}_alias"}, $col ] };
+    }
+    return $res;
+  };
+  $sqla->op_expander(ident => $inner_wrap);
+  my $inner_aqt = $self->_collapsify($sqla->expand_expr($cond, -ident));
+  return ($aqt, $inner_aqt);
+}
+
+sub _collapsify {
+  my ($self, $aqt) = @_;
+  return $aqt unless my @opargs = @{$aqt->{-op}};
+  my ($logop, @args) = @opargs;
+  return $aqt unless $logop eq 'and';
+  my %collapsed = map {
+    my $q = $_;
+    return $aqt unless my @opargs = @{$q->{-op}};
+    my ($op, $lhs, @rest) = @opargs;
+    return $aqt unless my @ident = @{$lhs->{-ident}};
+    (join('.', @ident), { $op => \@rest });
+  } @args;
+  return \%collapsed;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+DBIx::Class::SQLMaker::Role::SQLA2Passthrough - A test of future possibilities
+
+=head1 SYNOPSIS
+
+=over 4
+
+=item * select and group_by options are processed using the richer SQLA2 code
+
+=item * expand_join_condition is provided to more easily express rich joins
+
+=back
+
+See C<examples/sqla2passthrough.pl> for a small amount of running code.
+
+=head1 SETUP
+
+  (on_connect_call => sub {
+     my ($storage) = @_;
+     $storage->sql_maker
+             ->with::roles('DBIx::Class::SQLMaker::Role::SQLA2Passthrough');
+  })
+
+=head2 expand_join_condition
+
+  __PACKAGE__->has_many(minions => 'Blah::Person' => sub {
+    my ($args) = @_;
+    $args->{self_resultsource}
+         ->schema->storage->sql_maker
+         ->expand_join_condition(
+             $args
+           );
+  });
+
+=head2 on
+
+  __PACKAGE__->has_many(minions => 'Blah::Person' => on {
+    { 'self.group_id' => 'foreign.group_id',
+      'self.rank' => { '>', 'foreign.rank' } }
+  });
+
+Or with ParameterizedJoinHack,
+
+  __PACKAGE__->parameterized_has_many(
+      priority_tasks => 'MySchema::Result::Task',
+      [['min_priority'] => sub {
+          my $args = shift;
+          return +{
+              "$args->{foreign_alias}.owner_id" => {
+                  -ident => "$args->{self_alias}.id",
+              },
+              "$args->{foreign_alias}.priority" => {
+                  '>=' => $_{min_priority},
+              },
+          };
+      }],
+  );
+
+becomes
+
+  __PACKAGE__->parameterized_has_many(
+      priority_tasks => 'MySchema::Result::Task',
+      [['min_priority'] => on {
+        { 'foreign.owner_id' => 'self.id',
+          'foreign.priority' => { '>=', { -value => $_{min_priority} } } }
+      }]
+  );
+
+Note that foreign/self can appear in such a condition on either side, BUT
+if you want L<DBIx::Class> to be able to use a join-less version you must
+ensure that the LHS is all foreign columns, i.e.
+
+  on {
+    +{
+      'foreign.x' => 'self.x',
+      'self.y' => { -between => [ 'foreign.y1', 'foreign.y2' ] }
+    }
+  }
+
+is completely valid but DBIC will insist on doing a JOIN even if you
+have a fully populated row object to call C<search_related> on - to avoid
+the spurious JOIN, you must specify it with explicit LHS foreign cols as:
+
+  on {
+    +{
+      'foreign.x' => 'self.x',
+      'foreign.y1' => { '<=', 'self.y' },
+      'foreign.y2' => { '>=', 'self.y' },
+    }
+  }
+
+=cut
index 112643e..30b3ffc 100644 (file)
@@ -2,12 +2,13 @@ package SQL::Abstract; # see doc at end of file
 
 use strict;
 use warnings;
+use Module::Runtime ();
 use Carp ();
 use List::Util ();
 use Scalar::Util ();
 
 use Exporter 'import';
-our @EXPORT_OK = qw(is_plain_value is_literal_value);
+our @EXPORT_OK = qw(is_plain_value is_literal_value is_undef_value);
 
 BEGIN {
   if ($] < 5.009_005) {
@@ -27,7 +28,7 @@ BEGIN {
 # GLOBALS
 #======================================================================
 
-our $VERSION  = '1.87';
+our $VERSION  = '1.90_03';
 
 # This would confuse some packagers
 $VERSION = eval $VERSION if $VERSION =~ /_/; # numify for warning-free dev releases
@@ -37,22 +38,11 @@ our $AUTOLOAD;
 # special operators (-in, -between). May be extended/overridden by user.
 # See section WHERE: BUILTIN SPECIAL OPERATORS below for implementation
 my @BUILTIN_SPECIAL_OPS = (
-  {regex => qr/^ (?: not \s )? between $/ix, handler => '_where_field_BETWEEN'},
-  {regex => qr/^ (?: not \s )? in      $/ix, handler => '_where_field_IN'},
-  {regex => qr/^ ident                 $/ix, handler => '_where_op_IDENT'},
-  {regex => qr/^ value                 $/ix, handler => '_where_op_VALUE'},
-  {regex => qr/^ is (?: \s+ not )?     $/ix, handler => '_where_field_IS'},
-);
-
-# unaryish operators - key maps to handler
-my @BUILTIN_UNARY_OPS = (
-  # the digits are backcompat stuff
-  { regex => qr/^ and  (?: [_\s]? \d+ )? $/xi, handler => '_where_op_ANDOR' },
-  { regex => qr/^ or   (?: [_\s]? \d+ )? $/xi, handler => '_where_op_ANDOR' },
-  { regex => qr/^ nest (?: [_\s]? \d+ )? $/xi, handler => '_where_op_NEST' },
-  { regex => qr/^ (?: not \s )? bool     $/xi, handler => '_where_op_BOOL' },
-  { regex => qr/^ ident                  $/xi, handler => '_where_op_IDENT' },
-  { regex => qr/^ value                  $/xi, handler => '_where_op_VALUE' },
+  {regex => qr/^ (?: not \s )? between $/ix, handler => sub { die "NOPE" }},
+  {regex => qr/^ is (?: \s+ not )?     $/ix, handler => sub { die "NOPE" }},
+  {regex => qr/^ (?: not \s )? in      $/ix, handler => sub { die "NOPE" }},
+  {regex => qr/^ ident                 $/ix, handler => sub { die "NOPE" }},
+  {regex => qr/^ value                 $/ix, handler => sub { die "NOPE" }},
 );
 
 #======================================================================
@@ -81,6 +71,15 @@ sub is_literal_value ($) {
   : undef;
 }
 
+sub is_undef_value ($) {
+  !defined($_[0])
+  or (
+    ref($_[0]) eq 'HASH'
+    and exists $_[0]->{-value}
+    and not defined $_[0]->{-value}
+  );
+}
+
 # FIXME XSify - this can be done so much more efficiently
 sub is_plain_value ($) {
   no strict 'refs';
@@ -138,6 +137,70 @@ sub is_plain_value ($) {
 # NEW
 #======================================================================
 
+our %Defaults = (
+  expand => {
+    bool => '_expand_bool',
+    row => '_expand_row',
+    op => '_expand_op',
+    func => '_expand_func',
+    values => '_expand_values',
+    list => '_expand_list',
+  },
+  expand_op => {
+    (map +($_ => __PACKAGE__->make_binop_expander('_expand_between')),
+      qw(between not_between)),
+    (map +($_ => __PACKAGE__->make_binop_expander('_expand_in')),
+      qw(in not_in)),
+    (map +($_ => '_expand_op_andor'), ('and', 'or')),
+    (map +($_ => '_expand_op_is'), ('is', 'is_not')),
+    (map +($_ => __PACKAGE__->make_unop_expander("_expand_${_}")),
+      qw(ident value nest)),
+    bind => __PACKAGE__->make_unop_expander(sub { +{ -bind => $_[2] } }),
+  },
+  render => {
+    (map +($_, "_render_$_"),
+      qw(op func bind ident literal row values keyword)),
+  },
+  render_op => {
+    (map +($_ => '_render_op_between'), 'between', 'not_between'),
+    (map +($_ => '_render_op_in'), 'in', 'not_in'),
+    (map +($_ => '_render_unop_postfix'),
+      'is_null', 'is_not_null', 'asc', 'desc',
+    ),
+    (not => '_render_unop_paren'),
+    (map +($_ => '_render_op_andor'), qw(and or)),
+    ',' => '_render_op_multop',
+  },
+  clauses_of => {
+    delete => [ qw(target where returning) ],
+    update => [ qw(target set where returning) ],
+    insert => [ qw(target fields from returning) ],
+    select => [ qw(select from where order_by) ],
+  },
+  expand_clause => {
+    'delete.from' => '_expand_delete_clause_target',
+    'update.update' => '_expand_update_clause_target',
+    'insert.into' => '_expand_insert_clause_target',
+    'insert.values' => '_expand_insert_clause_from',
+  },
+  render_clause => {
+    'delete.target' => '_render_delete_clause_target',
+    'update.target' => '_render_update_clause_target',
+    'insert.target' => '_render_insert_clause_target',
+    'insert.fields' => '_render_insert_clause_fields',
+    'insert.from' => '_render_insert_clause_from',
+  },
+);
+
+foreach my $stmt (keys %{$Defaults{clauses_of}}) {
+  $Defaults{expand}{$stmt} = '_expand_statement';
+  $Defaults{render}{$stmt} = '_render_statement';
+  foreach my $clause (@{$Defaults{clauses_of}{$stmt}}) {
+    $Defaults{expand_clause}{"${stmt}.${clause}"}
+      = "_expand_${stmt}_clause_${clause}";
+  }
+}
+
 sub new {
   my $self = shift;
   my $class = ref($self) || $self;
@@ -160,8 +223,8 @@ sub new {
   $opt{equality_op}   = qr/^( \Q$opt{cmp}\E | \= )$/ix;
   $opt{inequality_op} = qr/^( != | <> )$/ix;
 
-  $opt{like_op}       = qr/^ (is\s+)? r?like $/xi;
-  $opt{not_like_op}   = qr/^ (is\s+)? not \s+ r?like $/xi;
+  $opt{like_op}       = qr/^ (is_)?r?like $/xi;
+  $opt{not_like_op}   = qr/^ (is_)?not_r?like $/xi;
 
   # SQL booleans
   $opt{sqltrue}  ||= '1=1';
@@ -169,12 +232,9 @@ sub new {
 
   # special operators
   $opt{special_ops} ||= [];
-  # regexes are applied in order, thus push after user-defines
-  push @{$opt{special_ops}}, @BUILTIN_SPECIAL_OPS;
 
   # unary operators
   $opt{unary_ops} ||= [];
-  push @{$opt{unary_ops}}, @BUILTIN_UNARY_OPS;
 
   # rudimentary sanity-check for user supplied bits treated as functions/operators
   # If a purported  function matches this regular expression, an exception is thrown.
@@ -190,167 +250,383 @@ sub new {
     ^ \s* go \s
   /xmi;
 
-  return bless \%opt, $class;
-}
+  $opt{expand_unary} = {};
 
-
-sub _assert_pass_injection_guard {
-  if ($_[1] =~ $_[0]->{injection_guard}) {
-    my $class = ref $_[0];
-    puke "Possible SQL injection attempt '$_[1]'. If this is indeed a part of the "
-     . "desired SQL use literal SQL ( \'...' or \[ '...' ] ) or supply your own "
-     . "{injection_guard} attribute to ${class}->new()"
+  foreach my $name (sort keys %Defaults) {
+    $opt{$name} = { %{$Defaults{$name}}, %{$opt{$name}||{}} };
   }
-}
-
 
-#======================================================================
-# INSERT methods
-#======================================================================
+  if ($class ne __PACKAGE__) {
 
-sub insert {
-  my $self    = shift;
-  my $table   = $self->_table(shift);
-  my $data    = shift || return;
-  my $options = shift;
+    # check for overriden methods
 
-  my $method       = $self->_METHOD_FOR_refkind("_insert", $data);
-  my ($sql, @bind) = $self->$method($data);
-  $sql = join " ", $self->_sqlcase('insert into'), $table, $sql;
+    foreach my $type (qw(insert update delete)) {
+      my $method = "_${type}_returning";
+      if (__PACKAGE__->can($method) ne $class->can($method)) {
+        my $clause = "${type}.returning";
+        $opt{expand_clause}{$clause} = sub { $_[2] },
+        $opt{render_clause}{$clause}
+          = sub { [ $_[0]->$method($_[3]) ] };
+      }
+    }
+    if (__PACKAGE__->can('_table') ne $class->can('_table')) {
+      $opt{expand_clause}{'select.from'} = sub {
+        return +{ -literal => [ $_[0]->_table($_[2]) ] };
+      };
+    }
+    if (__PACKAGE__->can('_order_by') ne $class->can('_order_by')) {
+      $opt{expand_clause}{'select.order_by'} = sub { $_[2] };
+      $opt{render_clause}{'select.order_by'} = sub {
+        [ $_[0]->_order_by($_[2]) ];
+      };
+    }
+    if (__PACKAGE__->can('_select_fields') ne $class->can('_select_fields')) {
+      $opt{expand_clause}{'select.select'} = sub { $_[2] };
+      $opt{render_clause}{'select.select'} = sub {
+        my @super = $_[0]->_select_fields($_[2]);
+        my $effort = [
+          ref($super[0]) eq 'HASH'
+            ? $_[0]->render_expr($super[0])
+            : @super
+        ];
+        return $_[0]->join_query_parts(
+          ' ', { -keyword => 'select' }, $effort
+        );
+      };
+    }
+    foreach my $type (qw(in between)) {
+      my $meth = "_where_field_".uc($type);
+      if (__PACKAGE__->can($meth) ne $class->can($meth)) {
+        my $exp = sub {
+          my ($self, $op, $v, $k) = @_;
+          $op = join ' ', split '_', $op;
+          return +{ -literal => [
+            $self->$meth($k, $op, $v)
+          ] };
+        };
+        $opt{expand_op}{$_} = $exp for $type, "not_${type}";
+      }
+    }
+    if ($class->isa('DBIx::Class::SQLMaker')) {
+      $opt{warn_once_on_nest} = 1;
+      $opt{disable_old_special_ops} = 1;
+      $opt{render_clause}{'select.where'} = sub {
+        my ($sql, @bind) = $_[0]->where($_[2]);
+        s/\A\s+//, s/\s+\Z// for $sql;
+        return [ $sql, @bind ];
+      };
+      $opt{expand_op}{ident} = $class->make_unop_expander(sub {
+        my ($self, undef, $body) = @_;
+        $body = $body->from if Scalar::Util::blessed($body);
+        $self->_expand_ident(ident => $body);
+      });
+    }
+    if ($class->isa('SQL::Abstract::More')) {
+      my $orig = $opt{expand_op}{or};
+      $opt{expand_op}{or} = sub {
+        my ($self, $logop, $v, $k) = @_;
+        if ($k and ref($v) eq 'ARRAY') {
+          my ($type, $val) = @$v;
+          my $op;
+          if (
+            ref($type) eq 'HASH' and ref($val) eq 'HASH'
+            and keys %$type == 1 and keys %$val == 1
+            and (keys %$type)[0] eq (keys %$val)[0]
+          ) {
+            ($op) = keys %$type;
+            ($type) = values %$type;
+            ($val) = values %$val;
+          }
+          if ($self->is_bind_value_with_type(my $v = [ $type, $val ])) {
+            return $self->expand_expr(
+              { $k, map +($op ? { $op => $_ } : $_), { -bind => $v } }
+            );
+          }
+        }
+        return $self->$orig($logop, $v, $k);
+      };
+      $opt{render}{bind} = sub {
+        return [ '?', map +(ref($_->[0]) ? $_ : $_->[1]), $_[2] ]
+      };
+    }
+  }
 
-  if ($options->{returning}) {
-    my ($s, @b) = $self->_insert_returning($options);
-    $sql .= $s;
-    push @bind, @b;
+  if ($opt{lazy_join_sql_parts}) {
+    my $mod = Module::Runtime::use_module('SQL::Abstract::Parts');
+    $opt{join_sql_parts} ||= sub { $mod->new(@_) };
   }
 
-  return wantarray ? ($sql, @bind) : $sql;
-}
+  $opt{join_sql_parts} ||= sub { join $_[0], @_[1..$#_] };
 
-# So that subclasses can override INSERT ... RETURNING separately from
-# UPDATE and DELETE (e.g. DBIx::Class::SQLMaker::Oracle does this)
-sub _insert_returning { shift->_returning(@_) }
+  return bless \%opt, $class;
+}
 
-sub _returning {
-  my ($self, $options) = @_;
+sub _ext_rw {
+  my ($self, $name, $key, $value) = @_;
+  return $self->{$name}{$key} unless @_ > 3;
+  $self->{$name}{$key} = $value;
+  return $self;
+}
 
-  my $f = $options->{returning};
+sub make_unop_expander {
+  my (undef, $exp) = @_;
+  sub {
+    my ($self, $name, $body, $k) = @_;
+    return $self->_expand_hashpair_cmp($k, { "-${name}" => $body })
+      if defined($k);
+    return $self->$exp($name, $body);
+  }
+}
 
-  my $fieldlist = $self->_SWITCH_refkind($f, {
-    ARRAYREF     => sub {join ', ', map { $self->_quote($_) } @$f;},
-    SCALAR       => sub {$self->_quote($f)},
-    SCALARREF    => sub {$$f},
-  });
-  return $self->_sqlcase(' returning ') . $fieldlist;
+sub make_binop_expander {
+  my (undef, $exp) = @_;
+  sub {
+    my ($self, $name, $body, $k) = @_;
+    $k = shift @{$body = [ @$body ]} unless defined $k;
+    $k = ref($k) ? $k : { -ident => $k };
+    return $self->$exp($name, $body, $k);
+  }
 }
 
-sub _insert_HASHREF { # explicit list of fields and then values
-  my ($self, $data) = @_;
+sub plugin {
+  my ($self, $plugin, @args) = @_;
+  unless (ref $plugin) {
+    $plugin =~ s/\A\+/${\__PACKAGE__}::Plugin::/;
+    require(join('/', split '::', $plugin).'.pm');
+  }
+  $plugin->apply_to($self, @args);
+  return $self;
+}
 
-  my @fields = sort keys %$data;
+BEGIN {
+  foreach my $type (qw(
+    expand op_expand render op_render clause_expand clause_render
+  )) {
+    my $name = join '_', reverse split '_', $type;
+    my $singular = "${type}er";
+
+    eval qq{sub ${singular} {
+      my \$self = shift;
+      return \$self->_ext_rw('${name}', \@_) if \@_ == 1;
+      return \$self->${singular}s(\@_)
+    }; 1 } or die "Method builder failed for ${singular}: $@";
+    eval qq{sub wrap_${singular} {
+      shift->wrap_${singular}s(\@_)
+    }; 1 } or die "Method builder failed for wrap_${singular}: $@";
+
+    eval qq{sub ${singular}s {
+      my (\$self, \@args) = \@_;
+      while (my (\$this_key, \$this_value) = splice(\@args, 0, 2)) {
+        \$self->_ext_rw('${name}', \$this_key, \$this_value);
+      }
+      return \$self;
+    }; 1 } or die "Method builder failed for ${singular}s: $@";
+    eval qq{sub wrap_${singular}s {
+      my (\$self, \@args) = \@_;
+      while (my (\$this_key, \$this_builder) = splice(\@args, 0, 2)) {
+        my \$orig = \$self->_ext_rw('${name}', \$this_key);
+        \$self->_ext_rw(
+          '${name}', \$this_key,
+           \$this_builder->(\$orig, '${name}', \$this_key),
+        );
+      }
+      return \$self;
+    }; 1 } or die "Method builder failed for wrap_${singular}s: $@";
+    eval qq{sub ${singular}_list { sort keys %{\$_[0]->{\$name}} }; 1; }
+     or die "Method builder failed for ${singular}_list: $@";
+  }
+  foreach my $singular (qw(unop_expander binop_expander)) {
+    eval qq{sub ${singular} { shift->${singular}s(\@_) }; 1 }
+      or die "Method builder failed for ${singular}: $@";
+    eval qq{sub ${singular}s {
+      my (\$self, \@args) = \@_;
+      while (my (\$this_key, \$this_value) = splice(\@args, 0, 2)) {
+        \$self->_ext_rw(
+           expand_op => \$this_key,
+           \$self->make_${singular}(\$this_value),
+        );
+      }
+      return \$self;
+    }; 1 } or die "Method builder failed for ${singular}s: $@";
+  }
+}
 
-  my ($sql, @bind) = $self->_insert_values($data);
+#sub register_op { $_[0]->{is_op}{$_[1]} = 1; $_[0] }
 
-  # assemble SQL
-  $_ = $self->_quote($_) foreach @fields;
-  $sql = "( ".join(", ", @fields).") ".$sql;
+sub statement_list { sort keys %{$_[0]->{clauses_of}} }
 
-  return ($sql, @bind);
+sub clauses_of {
+  my ($self, $of, @clauses) = @_;
+  unless (@clauses) {
+    return @{$self->{clauses_of}{$of}||[]};
+  }
+  if (ref($clauses[0]) eq 'CODE') {
+    @clauses = $self->${\($clauses[0])}(@{$self->{clauses_of}{$of}||[]});
+  }
+  $self->{clauses_of}{$of} = \@clauses;
+  return $self;
 }
 
-sub _insert_ARRAYREF { # just generate values(?,?) part (no list of fields)
-  my ($self, $data) = @_;
+sub clone {
+  my ($self) = @_;
+  bless(
+    {
+      (map +($_ => (
+        ref($self->{$_}) eq 'HASH'
+          ? { %{$self->{$_}} }
+          : $self->{$_}
+      )), keys %$self),
+    },
+    ref($self)
+  );
+}
 
-  # no names (arrayref) so can't generate bindtype
-  $self->{bindtype} ne 'columns'
-    or belch "can't do 'columns' bindtype when called with arrayref";
+sub sqltrue { +{ -literal => [ $_[0]->{sqltrue} ] } }
+sub sqlfalse { +{ -literal => [ $_[0]->{sqlfalse} ] } }
 
-  my (@values, @all_bind);
-  foreach my $value (@$data) {
-    my ($values, @bind) = $self->_insert_value(undef, $value);
-    push @values, $values;
-    push @all_bind, @bind;
+sub _assert_pass_injection_guard {
+  if ($_[1] =~ $_[0]->{injection_guard}) {
+    my $class = ref $_[0];
+    puke "Possible SQL injection attempt '$_[1]'. If this is indeed a part of the "
+     . "desired SQL use literal SQL ( \'...' or \[ '...' ] ) or supply your own "
+     . "{injection_guard} attribute to ${class}->new()"
   }
-  my $sql = $self->_sqlcase('values')." ( ".join(", ", @values)." )";
-  return ($sql, @all_bind);
 }
 
-sub _insert_ARRAYREFREF { # literal SQL with bind
-  my ($self, $data) = @_;
 
-  my ($sql, @bind) = @${$data};
-  $self->_assert_bindval_matches_bindtype(@bind);
+#======================================================================
+# INSERT methods
+#======================================================================
+
+sub insert {
+  my ($self, $table, $data, $options) = @_;
 
-  return ($sql, @bind);
+  my $stmt = do {
+    if (ref($table) eq 'HASH') {
+      $table;
+    } else {
+      my %clauses = (target => $table, values => $data, %{$options||{}});
+      \%clauses;
+    }
+  };
+  my @rendered = $self->render_statement({ -insert => $stmt });
+  return wantarray ? @rendered : $rendered[0];
 }
 
+sub _expand_insert_clause_target {
+  +(target => $_[0]->expand_expr($_[2], -ident));
+}
 
-sub _insert_SCALARREF { # literal SQL without bind
-  my ($self, $data) = @_;
+sub _expand_insert_clause_fields {
+  return +{ -row => [
+    $_[0]->expand_expr({ -list => $_[2] }, -ident)
+  ] } if ref($_[2]) eq 'ARRAY';
+  return $_[2]; # should maybe still expand somewhat?
+}
 
-  return ($$data);
+sub _expand_insert_clause_from {
+  my ($self, undef, $data) = @_;
+  if (ref($data) eq 'HASH' and (keys(%$data))[0] =~ /^-/) {
+    return $self->expand_expr($data);
+  }
+  my ($f_aqt, $v_aqt) = $self->_expand_insert_values($data);
+  return (
+    from => { -values => [ $v_aqt ] },
+    ($f_aqt ? (fields => $f_aqt) : ()),
+  );
+}
+
+sub _expand_insert_clause_returning {
+  +(returning => $_[0]->expand_expr({ -list => $_[2] }, -ident));
 }
 
-sub _insert_values {
+sub _expand_insert_values {
   my ($self, $data) = @_;
+  if (is_literal_value($data)) {
+    (undef, $self->expand_expr($data));
+  } else {
+    my ($fields, $values) = (
+      ref($data) eq 'HASH' ?
+        ([ sort keys %$data ], [ @{$data}{sort keys %$data} ])
+        : ([], $data)
+    );
 
-  my (@values, @all_bind);
-  foreach my $column (sort keys %$data) {
-    my ($values, @bind) = $self->_insert_value($column, $data->{$column});
-    push @values, $values;
-    push @all_bind, @bind;
+    # no names (arrayref) means can't generate bindtype
+    !($fields) && $self->{bindtype} eq 'columns'
+      && belch "can't do 'columns' bindtype when called with arrayref";
+
+    +(
+      (@$fields
+        ? $self->expand_expr({ -row => $fields }, -ident)
+        : undef
+      ),
+      +{ -row => [
+        map {
+         local our $Cur_Col_Meta = $fields->[$_];
+         $self->_expand_insert_value($values->[$_])
+         } 0..$#$values
+      ] },
+    );
   }
-  my $sql = $self->_sqlcase('values')." ( ".join(", ", @values)." )";
-  return ($sql, @all_bind);
 }
 
-sub _insert_value {
-  my ($self, $column, $v) = @_;
+sub _render_insert_clause_fields {
+  return $_[0]->render_aqt($_[2]);
+}
 
-  my (@values, @all_bind);
-  $self->_SWITCH_refkind($v, {
+sub _render_insert_clause_target {
+  my ($self, undef, $from) = @_;
+  $self->join_query_parts(' ', { -keyword => 'insert into' }, $from);
+}
 
-    ARRAYREF => sub {
-      if ($self->{array_datatypes}) { # if array datatype are activated
-        push @values, '?';
-        push @all_bind, $self->_bindtype($column, $v);
-      }
-      else {                  # else literal SQL with bind
-        my ($sql, @bind) = @$v;
-        $self->_assert_bindval_matches_bindtype(@bind);
-        push @values, $sql;
-        push @all_bind, @bind;
-      }
-    },
+sub _render_insert_clause_from {
+  return $_[0]->render_aqt($_[2], 1);
+}
 
-    ARRAYREFREF => sub {        # literal SQL with bind
-      my ($sql, @bind) = @${$v};
-      $self->_assert_bindval_matches_bindtype(@bind);
-      push @values, $sql;
-      push @all_bind, @bind;
-    },
+# So that subclasses can override INSERT ... RETURNING separately from
+# UPDATE and DELETE (e.g. DBIx::Class::SQLMaker::Oracle does this)
+sub _insert_returning { shift->_returning(@_) }
 
-    # THINK: anything useful to do with a HASHREF ?
-    HASHREF => sub {       # (nothing, but old SQLA passed it through)
-      #TODO in SQLA >= 2.0 it will die instead
-      belch "HASH ref as bind value in insert is not supported";
-      push @values, '?';
-      push @all_bind, $self->_bindtype($column, $v);
-    },
+sub _redispatch_returning {
+  my ($self, $type, undef, $returning) = @_;
+  [ $self->${\"_${type}_returning"}({ returning => $returning }) ];
+}
 
-    SCALARREF => sub {          # literal SQL without bind
-      push @values, $$v;
-    },
+sub _returning {
+  my ($self, $options) = @_;
 
-    SCALAR_or_UNDEF => sub {
-      push @values, '?';
-      push @all_bind, $self->_bindtype($column, $v);
-    },
+  my $f = $options->{returning};
 
-  });
+  my ($sql, @bind) = @{ $self->render_aqt(
+    $self->expand_expr({ -list => $f }, -ident)
+  ) };
+  my $rsql = $self->_sqlcase(' returning ').$sql;
+  return wantarray ? ($rsql, @bind) : $rsql;
+}
+
+sub _expand_insert_value {
+  my ($self, $v) = @_;
+
+  my $k = our $Cur_Col_Meta;
 
-  my $sql = join(", ", @values);
-  return ($sql, @all_bind);
+  if (ref($v) eq 'ARRAY') {
+    if ($self->{array_datatypes}) {
+      return +{ -bind => [ $k, $v ] };
+    }
+    my ($sql, @bind) = @$v;
+    $self->_assert_bindval_matches_bindtype(@bind);
+    return +{ -literal => $v };
+  }
+  if (ref($v) eq 'HASH') {
+    if (grep !/^-/, keys %$v) {
+      belch "HASH ref as bind value in insert is not supported";
+      return +{ -bind => [ $k, $v ] };
+    }
+  }
+  if (!defined($v)) {
+    return +{ -bind => [ $k, undef ] };
+  }
+  return $self->expand_expr($v);
 }
 
 
@@ -359,91 +635,78 @@ sub _insert_value {
 # UPDATE methods
 #======================================================================
 
-
 sub update {
-  my $self    = shift;
-  my $table   = $self->_table(shift);
-  my $data    = shift || return;
-  my $where   = shift;
-  my $options = shift;
-
-  # first build the 'SET' part of the sql statement
-  puke "Unsupported data type specified to \$sql->update"
-    unless ref $data eq 'HASH';
+  my ($self, $table, $set, $where, $options) = @_;
 
-  my ($sql, @all_bind) = $self->_update_set_values($data);
-  $sql = $self->_sqlcase('update ') . $table . $self->_sqlcase(' set ')
-          . $sql;
-
-  if ($where) {
-    my($where_sql, @where_bind) = $self->where($where);
-    $sql .= $where_sql;
-    push @all_bind, @where_bind;
-  }
-
-  if ($options->{returning}) {
-    my ($returning_sql, @returning_bind) = $self->_update_returning($options);
-    $sql .= $returning_sql;
-    push @all_bind, @returning_bind;
-  }
+  my $stmt = do {
+    if (ref($table) eq 'HASH') {
+      $table
+    } else {
+      my %clauses;
+      @clauses{qw(target set where)} = ($table, $set, $where);
+      puke "Unsupported data type specified to \$sql->update"
+        unless ref($clauses{set}) eq 'HASH';
+      @clauses{keys %$options} = values %$options;
+      \%clauses;
+    }
+  };
+  my @rendered = $self->render_statement({ -update => $stmt });
+  return wantarray ? @rendered : $rendered[0];
+}
 
-  return wantarray ? ($sql, @all_bind) : $sql;
+sub _render_update_clause_target {
+  my ($self, undef, $target) = @_;
+  $self->join_query_parts(' ', { -keyword => 'update' }, $target);
 }
 
 sub _update_set_values {
   my ($self, $data) = @_;
 
-  my (@set, @all_bind);
-  for my $k (sort keys %$data) {
-    my $v = $data->{$k};
-    my $r = ref $v;
-    my $label = $self->_quote($k);
-
-    $self->_SWITCH_refkind($v, {
-      ARRAYREF => sub {
-        if ($self->{array_datatypes}) { # array datatype
-          push @set, "$label = ?";
-          push @all_bind, $self->_bindtype($k, $v);
-        }
-        else {                          # literal SQL with bind
-          my ($sql, @bind) = @$v;
-          $self->_assert_bindval_matches_bindtype(@bind);
-          push @set, "$label = $sql";
-          push @all_bind, @bind;
-        }
-      },
-      ARRAYREFREF => sub { # literal SQL with bind
-        my ($sql, @bind) = @${$v};
-        $self->_assert_bindval_matches_bindtype(@bind);
-        push @set, "$label = $sql";
-        push @all_bind, @bind;
-      },
-      SCALARREF => sub {  # literal SQL without bind
-        push @set, "$label = $$v";
-      },
-      HASHREF => sub {
-        my ($op, $arg, @rest) = %$v;
+  return @{ $self->render_aqt(
+    $self->_expand_update_set_values(undef, $data),
+  ) };
+}
 
-        puke 'Operator calls in update must be in the form { -op => $arg }'
-          if (@rest or not $op =~ /^\-(.+)/);
+sub _expand_update_set_values {
+  my ($self, undef, $data) = @_;
+  $self->expand_expr({ -list => [
+    map {
+      my ($k, $set) = @$_;
+      $set = { -bind => $_ } unless defined $set;
+      +{ -op => [ '=', { -ident => $k }, $set ] };
+    }
+    map {
+      my $k = $_;
+      my $v = $data->{$k};
+      (ref($v) eq 'ARRAY'
+        ? ($self->{array_datatypes}
+            ? [ $k, +{ -bind => [ $k, $v ] } ]
+            : [ $k, +{ -literal => $v } ])
+        : do {
+            local our $Cur_Col_Meta = $k;
+            [ $k, $self->_expand_expr($v) ]
+          }
+      );
+    } sort keys %$data
+  ] });
+}
 
-        local $self->{_nested_func_lhs} = $k;
-        my ($sql, @bind) = $self->_where_unary_op($1, $arg);
+sub _expand_update_clause_target {
+  my ($self, undef, $target) = @_;
+  +(target => $self->expand_expr({ -list => $target }, -ident));
+}
 
-        push @set, "$label = $sql";
-        push @all_bind, @bind;
-      },
-      SCALAR_or_UNDEF => sub {
-        push @set, "$label = ?";
-        push @all_bind, $self->_bindtype($k, $v);
-      },
-    });
-  }
+sub _expand_update_clause_set {
+  return $_[2] if ref($_[2]) eq 'HASH' and ($_[2]->{-op}||[''])->[0] eq ',';
+  +(set => $_[0]->_expand_update_set_values($_[1], $_[2]));
+}
 
-  # generate sql
-  my $sql = join ', ', @set;
+sub _expand_update_clause_where {
+  +(where => $_[0]->expand_expr($_[2]));
+}
 
-  return ($sql, @all_bind);
+sub _expand_update_clause_returning {
+  +(returning => $_[0]->expand_expr({ -list => $_[2] }, -ident));
 }
 
 # So that subclasses can override UPDATE ... RETURNING separately from
@@ -456,60 +719,131 @@ sub _update_returning { shift->_returning(@_) }
 # SELECT
 #======================================================================
 
-
 sub select {
-  my $self   = shift;
-  my $table  = $self->_table(shift);
-  my $fields = shift || '*';
-  my $where  = shift;
-  my $order  = shift;
+  my ($self, @args) = @_;
+  my $stmt = do {
+    if (ref(my $sel = $args[0]) eq 'HASH') {
+      $sel
+    } else {
+      my %clauses;
+      @clauses{qw(from select where order_by)} = @args;
 
-  my ($fields_sql, @bind) = $self->_select_fields($fields);
+      # This oddity is to literalify since historically SQLA doesn't quote
+      # a single identifier argument, so we convert it into a literal
 
-  my ($where_sql, @where_bind) = $self->where($where, $order);
-  push @bind, @where_bind;
+      $clauses{select} = { -literal => [ $clauses{select}||'*' ] }
+        unless ref($clauses{select});
+      \%clauses;
+    }
+  };
 
-  my $sql = join(' ', $self->_sqlcase('select'), $fields_sql,
-                      $self->_sqlcase('from'),   $table)
-          . $where_sql;
+  my @rendered = $self->render_statement({ -select => $stmt });
+  return wantarray ? @rendered : $rendered[0];
+}
 
-  return wantarray ? ($sql, @bind) : $sql;
+sub _expand_select_clause_select {
+  my ($self, undef, $select) = @_;
+  +(select => $self->expand_expr({ -list => $select }, -ident));
+}
+
+sub _expand_select_clause_from {
+  my ($self, undef, $from) = @_;
+  +(from => $self->expand_expr({ -list => $from }, -ident));
+}
+
+sub _expand_select_clause_where {
+  my ($self, undef, $where) = @_;
+
+  my $sqla = do {
+    if (my $conv = $self->{convert}) {
+      my $_wrap = sub {
+        my $orig = shift;
+        sub {
+          my $self = shift;
+          +{ -func => [
+            $conv,
+            $self->$orig(@_)
+          ] };
+        };
+      };
+      $self->clone
+           ->wrap_expander(bind => $_wrap)
+           ->wrap_op_expanders(map +($_ => $_wrap), qw(ident value))
+           ->wrap_expander(func => sub {
+               my $orig = shift;
+               sub {
+                 my ($self, $type, $thing) = @_;
+                 if (ref($thing) eq 'ARRAY' and $thing->[0] eq $conv
+                     and @$thing == 2 and ref($thing->[1]) eq 'HASH'
+                     and (
+                       $thing->[1]{-ident}
+                       or $thing->[1]{-value}
+                       or $thing->[1]{-bind})
+                     ) {
+                   return { -func => $thing }; # already went through our expander
+                 }
+                 return $self->$orig($type, $thing);
+               }
+             });
+    } else {
+      $self;
+    }
+  };
+
+  return +(where => $sqla->expand_expr($where));
+}
+
+sub _expand_select_clause_order_by {
+  my ($self, undef, $order_by) = @_;
+  +(order_by => $self->_expand_order_by($order_by));
 }
 
 sub _select_fields {
   my ($self, $fields) = @_;
-  return ref $fields eq 'ARRAY' ? join ', ', map { $self->_quote($_) } @$fields
-                                : $fields;
+  return $fields unless ref($fields);
+  my ($sql, @bind) = @{ $self->render_aqt(
+    $self->expand_expr({ -list => $fields }, '-ident')
+  ) };
+  return wantarray ? ($sql, @bind) : $sql;
 }
 
 #======================================================================
 # DELETE
 #======================================================================
 
-
 sub delete {
-  my $self    = shift;
-  my $table   = $self->_table(shift);
-  my $where   = shift;
-  my $options = shift;
-
-  my($where_sql, @bind) = $self->where($where);
-  my $sql = $self->_sqlcase('delete from ') . $table . $where_sql;
+  my ($self, $table, $where, $options) = @_;
 
-  if ($options->{returning}) {
-    my ($returning_sql, @returning_bind) = $self->_delete_returning($options);
-    $sql .= $returning_sql;
-    push @bind, @returning_bind;
-  }
-
-  return wantarray ? ($sql, @bind) : $sql;
+  my $stmt = do {
+    if (ref($table) eq 'HASH') {
+      $table;
+    } else {
+      my %clauses = (target => $table, where => $where, %{$options||{}});
+      \%clauses;
+    }
+  };
+  my @rendered = $self->render_statement({ -delete => $stmt });
+  return wantarray ? @rendered : $rendered[0];
 }
 
 # So that subclasses can override DELETE ... RETURNING separately from
 # INSERT and UPDATE
 sub _delete_returning { shift->_returning(@_) }
 
+sub _expand_delete_clause_target {
+  +(target => $_[0]->expand_expr({ -list => $_[2] }, -ident));
+}
+
+sub _expand_delete_clause_where { +(where => $_[0]->expand_expr($_[2])); }
+
+sub _expand_delete_clause_returning {
+  +(returning => $_[0]->expand_expr({ -list => $_[2] }, -ident));
+}
 
+sub _render_delete_clause_target {
+   my ($self, undef, $from) = @_;
+   $self->join_query_parts(' ', { -keyword => 'delete from' }, $from);
+}
 
 #======================================================================
 # WHERE: entry point
@@ -521,8 +855,12 @@ sub _delete_returning { shift->_returning(@_) }
 sub where {
   my ($self, $where, $order) = @_;
 
+  local $self->{convert_where} = $self->{convert};
+
   # where ?
-  my ($sql, @bind) = $self->_recurse_where($where);
+  my ($sql, @bind) = defined($where)
+   ? $self->_recurse_where($where)
+   : (undef);
   $sql = (defined $sql and length $sql) ? $self->_sqlcase(' where ') . "( $sql )" : '';
 
   # order by?
@@ -535,758 +873,832 @@ sub where {
   return wantarray ? ($sql, @bind) : $sql;
 }
 
+{ our $Default_Scalar_To = -value }
 
-sub _recurse_where {
-  my ($self, $where, $logic) = @_;
-
-  # dispatch on appropriate method according to refkind of $where
-  my $method = $self->_METHOD_FOR_refkind("_where", $where);
-
-  my ($sql, @bind) =  $self->$method($where, $logic);
-
-  # DBIx::Class used to call _recurse_where in scalar context
-  # something else might too...
-  if (wantarray) {
-    return ($sql, @bind);
-  }
-  else {
-    belch "Calling _recurse_where in scalar context is deprecated and will go away before 2.0";
-    return $sql;
-  }
+sub expand_expr {
+  my ($self, $expr, $default_scalar_to) = @_;
+  local our $Default_Scalar_To = $default_scalar_to if $default_scalar_to;
+  $self->_expand_expr($expr);
 }
 
-
-
-#======================================================================
-# WHERE: top-level ARRAYREF
-#======================================================================
-
-
-sub _where_ARRAYREF {
-  my ($self, $where, $logic) = @_;
-
-  $logic = uc($logic || $self->{logic});
-  $logic eq 'AND' or $logic eq 'OR' or puke "unknown logic: $logic";
-
-  my @clauses = @$where;
-
-  my (@sql_clauses, @all_bind);
-  # need to use while() so can shift() for pairs
-  while (@clauses) {
-    my $el = shift @clauses;
-
-    $el = undef if (defined $el and ! length $el);
-
-    # switch according to kind of $el and get corresponding ($sql, @bind)
-    my ($sql, @bind) = $self->_SWITCH_refkind($el, {
-
-      # skip empty elements, otherwise get invalid trailing AND stuff
-      ARRAYREF  => sub {$self->_recurse_where($el)        if @$el},
-
-      ARRAYREFREF => sub {
-        my ($s, @b) = @$$el;
-        $self->_assert_bindval_matches_bindtype(@b);
-        ($s, @b);
-      },
-
-      HASHREF   => sub {$self->_recurse_where($el, 'and') if %$el},
-
-      SCALARREF => sub { ($$el);                                 },
-
-      SCALAR    => sub {
-        # top-level arrayref with scalars, recurse in pairs
-        $self->_recurse_where({$el => shift(@clauses)})
-      },
-
-      UNDEF     => sub {puke "Supplying an empty left hand side argument is not supported in array-pairs" },
-    });
-
-    if ($sql) {
-      push @sql_clauses, $sql;
-      push @all_bind, @bind;
-    }
+sub render_aqt {
+  my ($self, $aqt, $top_level) = @_;
+  my ($k, $v, @rest) = %$aqt;
+  die "No" if @rest;
+  die "Not a node type: $k" unless $k =~ s/^-//;
+  if (my $meth = $self->{render}{$k}) {
+    local our $Render_Top_Level = $top_level;
+    return $self->$meth($k, $v)||[];
   }
-
-  return $self->_join_sql_clauses($logic, \@sql_clauses, \@all_bind);
+  die "notreached: $k";
 }
 
-#======================================================================
-# WHERE: top-level ARRAYREFREF
-#======================================================================
-
-sub _where_ARRAYREFREF {
-    my ($self, $where) = @_;
-    my ($sql, @bind) = @$$where;
-    $self->_assert_bindval_matches_bindtype(@bind);
-    return ($sql, @bind);
+sub render_expr {
+  my ($self, $expr, $default_scalar_to) = @_;
+  return @{ $self->render_aqt(
+    $self->expand_expr($expr, $default_scalar_to)
+  ) };
 }
 
-#======================================================================
-# WHERE: top-level HASHREF
-#======================================================================
+sub render_statement {
+  my ($self, $expr, $default_scalar_to) = @_;
+  @{$self->render_aqt(
+    $self->expand_expr($expr, $default_scalar_to), 1
+  )};
+}
 
-sub _where_HASHREF {
-  my ($self, $where) = @_;
-  my (@sql_clauses, @all_bind);
-
-  for my $k (sort keys %$where) {
-    my $v = $where->{$k};
-
-    # ($k => $v) is either a special unary op or a regular hashpair
-    my ($sql, @bind) = do {
-      if ($k =~ /^-./) {
-        # put the operator in canonical form
-        my $op = $k;
-        $op = substr $op, 1;  # remove initial dash
-        $op =~ s/^\s+|\s+$//g;# remove leading/trailing space
-        $op =~ s/\s+/ /g;     # compress whitespace
-
-        # so that -not_foo works correctly
-        $op =~ s/^not_/NOT /i;
-
-        $self->_debug("Unary OP(-$op) within hashref, recursing...");
-        my ($s, @b) = $self->_where_unary_op($op, $v);
-
-        # top level vs nested
-        # we assume that handled unary ops will take care of their ()s
-        $s = "($s)" unless (
-          List::Util::first {$op =~ $_->{regex}} @{$self->{unary_ops}}
-            or
-          ( defined $self->{_nested_func_lhs} and $self->{_nested_func_lhs} eq $k )
-        );
-        ($s, @b);
-      }
-      else {
-        if (! length $k) {
-          if (is_literal_value ($v) ) {
-            belch 'Hash-pairs consisting of an empty string with a literal are deprecated, and will be removed in 2.0: use -and => [ $literal ] instead';
-          }
-          else {
-            puke "Supplying an empty left hand side argument is not supported in hash-pairs";
-          }
+sub _expand_statement {
+  my ($self, $type, $args) = @_;
+  my $ec = $self->{expand_clause};
+  if ($args->{_}) {
+    $args = { %$args };
+    $args->{$type} = delete $args->{_}
+  }
+  my %has_clause = map +($_ => 1), @{$self->{clauses_of}{$type}};
+  return +{ "-${type}" => +{
+    map {
+      my $val = $args->{$_};
+      if (defined($val) and my $exp = $ec->{"${type}.$_"}) {
+        if ((my (@exp) = $self->$exp($_ => $val)) == 1) {
+          ($_ => $exp[0])
+        } else {
+          @exp
         }
+      } elsif ($has_clause{$_}) {
+        ($_ => $self->expand_expr($val))
+      } else {
+        ($_ => $val)
+      }
+    } sort keys %$args
+  } };
+}
 
-        my $method = $self->_METHOD_FOR_refkind("_where_hashpair", $v);
-        $self->$method($k, $v);
+sub _render_statement {
+  my ($self, $type, $args) = @_;
+  my @parts;
+  foreach my $clause (@{$self->{clauses_of}{$type}}) {
+    next unless my $clause_expr = $args->{$clause};
+    my $part = do {
+      if (my $rdr = $self->{render_clause}{"${type}.${clause}"}) {
+        $self->$rdr($clause, $clause_expr, $args);
+      } else {
+        my $r = $self->render_aqt($clause_expr, 1);
+        next unless defined $r->[0] and length $r->[0];
+        $self->join_query_parts(' ',
+          { -keyword => $clause },
+          $r
+        );
       }
     };
-
-    push @sql_clauses, $sql;
-    push @all_bind, @bind;
+    push @parts, $part;
   }
-
-  return $self->_join_sql_clauses('and', \@sql_clauses, \@all_bind);
+  my $q = $self->join_query_parts(' ', @parts);
+  return $self->join_query_parts('',
+    (our $Render_Top_Level ? $q : ('(', $q, ')'))
+  );
 }
 
-sub _where_unary_op {
-  my ($self, $op, $rhs) = @_;
-
-  # top level special ops are illegal in general
-  # this includes the -ident/-value ops (dual purpose unary and special)
-  puke "Illegal use of top-level '-$op'"
-    if ! defined $self->{_nested_func_lhs} and List::Util::first { $op =~ $_->{regex} } @{$self->{special_ops}};
-
-  if (my $op_entry = List::Util::first { $op =~ $_->{regex} } @{$self->{unary_ops}}) {
-    my $handler = $op_entry->{handler};
+sub _normalize_op {
+  my ($self, $raw) = @_;
+  my $op = lc $raw;
+  return $op if grep $_->{$op}, @{$self}{qw(expand_op render_op)};
+  s/^-(?=.)//, s/\s+/_/g for $op;
+  $op;
+}
 
-    if (not ref $handler) {
-      if ($op =~ s/ [_\s]? \d+ $//x ) {
-        belch 'Use of [and|or|nest]_N modifiers is deprecated and will be removed in SQLA v2.0. '
-            . "You probably wanted ...-and => [ -$op => COND1, -$op => COND2 ... ]";
-      }
-      return $self->$handler($op, $rhs);
+sub _expand_expr {
+  my ($self, $expr) = @_;
+  our $Expand_Depth ||= 0; local $Expand_Depth = $Expand_Depth + 1;
+  return undef unless defined($expr);
+  if (ref($expr) eq 'HASH') {
+    return undef unless my $kc = keys %$expr;
+    if ($kc > 1) {
+      return $self->_expand_logop(and => $expr);
     }
-    elsif (ref $handler eq 'CODE') {
-      return $handler->($self, $op, $rhs);
+    my ($key, $value) = %$expr;
+    if ($key =~ /^-/ and $key =~ s/ [_\s]? \d+ $//x ) {
+      belch 'Use of [and|or|nest]_N modifiers is deprecated and will be removed in SQLA v2.0. '
+          . "You probably wanted ...-and => [ $key => COND1, $key => COND2 ... ]";
     }
-    else {
-      puke "Illegal handler for operator $op - expecting a method name or a coderef";
+    return $self->_expand_hashpair($key, $value);
+  }
+  if (ref($expr) eq 'ARRAY') {
+    return $self->_expand_logop(lc($self->{logic}), $expr);
+  }
+  if (my $literal = is_literal_value($expr)) {
+    return +{ -literal => $literal };
+  }
+  if (!ref($expr) or Scalar::Util::blessed($expr)) {
+    return $self->_expand_scalar($expr);
+  }
+  die "notreached";
+}
+
+sub _expand_hashpair {
+  my ($self, $k, $v) = @_;
+  unless (defined($k) and length($k)) {
+    if (defined($k) and my $literal = is_literal_value($v)) {
+      belch 'Hash-pairs consisting of an empty string with a literal are deprecated, and will be removed in 2.0: use -and => [ $literal ] instead';
+      return { -literal => $literal };
     }
+    puke "Supplying an empty left hand side argument is not supported";
   }
+  if ($k =~ /^-./) {
+    return $self->_expand_hashpair_op($k, $v);
+  } elsif ($k =~ /^\W+$/) {
+    my ($lhs, @rhs) = ref($v) eq 'ARRAY' ? @$v : $v;
+    return $self->_expand_op(
+      -op, [ $k, $self->expand_expr($lhs, -ident), @rhs ]
+    );
+  }
+  return $self->_expand_hashpair_ident($k, $v);
+}
 
-  $self->_debug("Generic unary OP: $op - recursing as function");
+sub _expand_hashpair_ident {
+  my ($self, $k, $v) = @_;
 
-  $self->_assert_pass_injection_guard($op);
+  local our $Cur_Col_Meta = $k;
 
-  my ($sql, @bind) = $self->_SWITCH_refkind($rhs, {
-    SCALAR =>   sub {
-      puke "Illegal use of top-level '-$op'"
-        unless defined $self->{_nested_func_lhs};
+  # hash with multiple or no elements is andor
 
-      return (
-        $self->_convert('?'),
-        $self->_bindtype($self->{_nested_func_lhs}, $rhs)
-      );
-    },
-    FALLBACK => sub {
-      $self->_recurse_where($rhs)
-    },
-  });
+  if (ref($v) eq 'HASH' and keys %$v != 1) {
+    return $self->_expand_logop(and => $v, $k);
+  }
 
-  $sql = sprintf('%s %s',
-    $self->_sqlcase($op),
-    $sql,
-  );
+  # undef needs to be re-sent with cmp to achieve IS/IS NOT NULL
 
-  return ($sql, @bind);
-}
+  if (is_undef_value($v)) {
+    return $self->_expand_hashpair_cmp($k => undef);
+  }
 
-sub _where_op_ANDOR {
-  my ($self, $op, $v) = @_;
+  # scalars and objects get expanded as whatever requested or values
 
-  $self->_SWITCH_refkind($v, {
-    ARRAYREF => sub {
-      return $self->_where_ARRAYREF($v, $op);
-    },
+  if (!ref($v) or Scalar::Util::blessed($v)) {
+    return $self->_expand_hashpair_scalar($k, $v);
+  }
 
-    HASHREF => sub {
-      return ($op =~ /^or/i)
-        ? $self->_where_ARRAYREF([ map { $_ => $v->{$_} } (sort keys %$v) ], $op)
-        : $self->_where_HASHREF($v);
-    },
+  # single key hashref is a hashtriple
 
-    SCALARREF  => sub {
-      puke "-$op => \\\$scalar makes little sense, use " .
-        ($op =~ /^or/i
-          ? '[ \$scalar, \%rest_of_conditions ] instead'
-          : '-and => [ \$scalar, \%rest_of_conditions ] instead'
-        );
-    },
+  if (ref($v) eq 'HASH') {
+    return $self->_expand_hashtriple($k, %$v);
+  }
 
-    ARRAYREFREF => sub {
-      puke "-$op => \\[...] makes little sense, use " .
-        ($op =~ /^or/i
-          ? '[ \[...], \%rest_of_conditions ] instead'
-          : '-and => [ \[...], \%rest_of_conditions ] instead'
-        );
-    },
+  # arrayref needs re-engineering over the elements
 
-    SCALAR => sub { # permissively interpreted as SQL
-      puke "-$op => \$value makes little sense, use -bool => \$value instead";
-    },
+  if (ref($v) eq 'ARRAY') {
+    return $self->sqlfalse unless @$v;
+    $self->_debug("ARRAY($k) means distribute over elements");
+    my $logic = lc(
+      ($v->[0]||'') =~ /^-(and|or)$/i
+        ? (shift(@{$v = [ @$v ]}), $1)
+        : lc($self->{logic} || 'OR')
+    );
+    return $self->_expand_logop(
+      $logic => $v, $k
+    );
+  }
 
-    UNDEF => sub {
-      puke "-$op => undef not supported";
-    },
-   });
+  if (my $literal = is_literal_value($v)) {
+    unless (length $k) {
+      belch 'Hash-pairs consisting of an empty string with a literal are deprecated, and will be removed in 2.0: use -and => [ $literal ] instead';
+      return \$literal;
+    }
+    my ($sql, @bind) = @$literal;
+    if ($self->{bindtype} eq 'columns') {
+      for (@bind) {
+        $self->_assert_bindval_matches_bindtype($_);
+      }
+    }
+    return +{ -literal => [ $self->_quote($k).' '.$sql, @bind ] };
+  }
+  die "notreached";
 }
 
-sub _where_op_NEST {
-  my ($self, $op, $v) = @_;
-
-  $self->_SWITCH_refkind($v, {
+sub _expand_scalar {
+  my ($self, $expr) = @_;
 
-    SCALAR => sub { # permissively interpreted as SQL
-      belch "literal SQL should be -nest => \\'scalar' "
-          . "instead of -nest => 'scalar' ";
-      return ($v);
-    },
-
-    UNDEF => sub {
-      puke "-$op => undef not supported";
-    },
+  return $self->_expand_expr({ (our $Default_Scalar_To) => $expr });
+}
 
-    FALLBACK => sub {
-      $self->_recurse_where($v);
-    },
+sub _expand_hashpair_scalar {
+  my ($self, $k, $v) = @_;
 
-   });
+  return $self->_expand_hashpair_cmp(
+    $k, $self->_expand_scalar($v),
+  );
 }
 
+sub _expand_hashpair_op {
+  my ($self, $k, $v) = @_;
 
-sub _where_op_BOOL {
-  my ($self, $op, $v) = @_;
+  $self->_assert_pass_injection_guard($k =~ /\A-(.*)\Z/s);
 
-  my ($s, @b) = $self->_SWITCH_refkind($v, {
-    SCALAR => sub { # interpreted as SQL column
-      $self->_convert($self->_quote($v));
-    },
+  my $op = $self->_normalize_op($k);
 
-    UNDEF => sub {
-      puke "-$op => undef not supported";
-    },
+  my $wsop = join(' ', split '_', $op);
 
-    FALLBACK => sub {
-      $self->_recurse_where($v);
-    },
-  });
+  my $is_special = List::Util::first { $wsop =~ $_->{regex} }
+                     @{$self->{special_ops}};
 
-  $s = "(NOT $s)" if $op =~ /^not/i;
-  ($s, @b);
-}
+  { # Old SQLA compat
 
+    # the old special op system requires illegality for top-level use
 
-sub _where_op_IDENT {
-  my $self = shift;
-  my ($op, $rhs) = splice @_, -2;
-  if (! defined $rhs or length ref $rhs) {
-    puke "-$op requires a single plain scalar argument (a quotable identifier)";
+    if (
+      (our $Expand_Depth) == 1
+      and (
+        $is_special
+        or (
+          $self->{disable_old_special_ops}
+          and List::Util::first { $wsop =~ $_->{regex} } @BUILTIN_SPECIAL_OPS
+        )
+      )
+    ) {
+      puke "Illegal use of top-level '-$wsop'"
+    }
   }
 
-  # in case we are called as a top level special op (no '=')
-  my $lhs = shift;
-
-  $_ = $self->_convert($self->_quote($_)) for ($lhs, $rhs);
-
-  return $lhs
-    ? "$lhs = $rhs"
-    : $rhs
-  ;
-}
+  if (my $exp = $self->{expand}{$op}||$self->{expand_op}{$op}) {
+    return $self->$exp($op, $v);
+  }
 
-sub _where_op_VALUE {
-  my $self = shift;
-  my ($op, $rhs) = splice @_, -2;
+  if ($self->{render}{$op}) {
+    return { "-${op}" => $v };
+  }
 
-  # in case we are called as a top level special op (no '=')
-  my $lhs = shift;
+  # Ops prefixed with -not_ get converted
 
-  # special-case NULL
-  if (! defined $rhs) {
-    return defined $lhs
-      ? $self->_convert($self->_quote($lhs)) . ' IS NULL'
-      : undef
-    ;
+  if (my ($rest) = $op =~/^not_(.*)$/) {
+    return +{ -op => [
+      'not',
+      $self->_expand_expr({ "-${rest}", $v })
+    ] };
   }
 
-  my @bind =
-    $self->_bindtype(
-      (defined $lhs ? $lhs : $self->{_nested_func_lhs}),
-      $rhs,
-    )
-  ;
+  { # Old SQLA compat
 
-  return $lhs
-    ? (
-      $self->_convert($self->_quote($lhs)) . ' = ' . $self->_convert('?'),
-      @bind
-    )
-    : (
-      $self->_convert('?'),
-      @bind,
-    )
-  ;
-}
+    # the old unary op system means we should touch nothing and let it work
 
-sub _where_hashpair_ARRAYREF {
-  my ($self, $k, $v) = @_;
+    my $op = join(' ', split '_', $op);
 
-  if (@$v) {
-    my @v = @$v; # need copy because of shift below
-    $self->_debug("ARRAY($k) means distribute over elements");
+    if (my $us = List::Util::first { $op =~ $_->{regex} } @{$self->{unary_ops}}) {
+      return { -op => [ $op, $v ] };
+    }
+  }
 
-    # put apart first element if it is an operator (-and, -or)
-    my $op = (
-       (defined $v[0] && $v[0] =~ /^ - (?: AND|OR ) $/ix)
-         ? shift @v
-         : ''
-    );
-    my @distributed = map { {$k =>  $_} } @v;
+  my $type = $is_special || $self->{render_op}{$op} ? -op : -func;
 
-    if ($op) {
-      $self->_debug("OP($op) reinjected into the distributed array");
-      unshift @distributed, $op;
-    }
+  if ($self->{restore_old_unop_handling}) {
 
-    my $logic = $op ? substr($op, 1) : '';
+    # Old SQLA compat
 
-    return $self->_recurse_where(\@distributed, $logic);
-  }
-  else {
-    $self->_debug("empty ARRAY($k) means 0=1");
-    return ($self->{sqlfalse});
+    if (
+      ref($v) eq 'HASH'
+      and keys %$v == 1
+      and (keys %$v)[0] =~ /^-/
+      and not $self->{render_op}{$op}
+      and not $is_special
+    ) {
+      $type = -func;
+    } else {
+      $type = -op;
+    }
   }
-}
 
-sub _where_hashpair_HASHREF {
-  my ($self, $k, $v, $logic) = @_;
-  $logic ||= 'and';
-
-  local $self->{_nested_func_lhs} = defined $self->{_nested_func_lhs}
-    ? $self->{_nested_func_lhs}
-    : $k
-  ;
+  if ($type eq -func and ref($v) eq 'ARRAY') {
+    return $self->_expand_expr({ -func => [ $op, @$v ] });
+  }
 
-  my ($all_sql, @all_bind);
+  return $self->_expand_expr({ $type => [ $op, $v ] });
+}
 
-  for my $orig_op (sort keys %$v) {
-    my $val = $v->{$orig_op};
+sub _expand_hashpair_cmp {
+  my ($self, $k, $v) = @_;
+  $self->_expand_hashtriple($k, $self->{cmp}, $v);
+}
 
-    # put the operator in canonical form
-    my $op = $orig_op;
+sub _expand_hashtriple {
+  my ($self, $k, $vk, $vv) = @_;
 
-    # FIXME - we need to phase out dash-less ops
-    $op =~ s/^-//;        # remove possible initial dash
-    $op =~ s/^\s+|\s+$//g;# remove leading/trailing space
-    $op =~ s/\s+/ /g;     # compress whitespace
+  my $ik = $self->_expand_expr({ -ident => $k });
 
-    $self->_assert_pass_injection_guard($op);
+  my $op = $self->_normalize_op($vk);
+  $self->_assert_pass_injection_guard($op);
 
-    # fixup is_not
-    $op =~ s/^is_not/IS NOT/i;
+  if ($op =~ s/ _? \d+ $//x ) {
+    return $self->_expand_expr($k, { $vk, $vv });
+  }
+  if (my $x = $self->{expand_op}{$op}) {
+    local our $Cur_Col_Meta = $k;
+    return $self->$x($op, $vv, $k);
+  }
+  { # Old SQLA compat
 
-    # so that -not_foo works correctly
-    $op =~ s/^not_/NOT /i;
+    my $op = join(' ', split '_', $op);
 
-    # another retarded special case: foo => { $op => { -value => undef } }
-    if (ref $val eq 'HASH' and keys %$val == 1 and exists $val->{-value} and ! defined $val->{-value} ) {
-      $val = undef;
+    if (my $us = List::Util::first { $op =~ $_->{regex} } @{$self->{special_ops}}) {
+      return { -op => [ $op, $ik, $vv ] };
     }
-
-    my ($sql, @bind);
-
-    # CASE: col-value logic modifiers
-    if ($orig_op =~ /^ \- (and|or) $/xi) {
-      ($sql, @bind) = $self->_where_hashpair_HASHREF($k, $val, $1);
+    if (my $us = List::Util::first { $op =~ $_->{regex} } @{$self->{unary_ops}}) {
+      return { -op => [
+        $self->{cmp},
+        $ik,
+        { -op => [ $op, $vv ] }
+      ] };
     }
-    # CASE: special operators like -in or -between
-    elsif (my $special_op = List::Util::first { $op =~ $_->{regex} } @{$self->{special_ops}}) {
-      my $handler = $special_op->{handler};
-      if (! $handler) {
-        puke "No handler supplied for special operator $orig_op";
-      }
-      elsif (not ref $handler) {
-        ($sql, @bind) = $self->$handler($k, $op, $val);
-      }
-      elsif (ref $handler eq 'CODE') {
-        ($sql, @bind) = $handler->($self, $k, $op, $val);
-      }
-      else {
-        puke "Illegal handler for special operator $orig_op - expecting a method name or a coderef";
+  }
+  if (ref($vv) eq 'ARRAY') {
+    my @raw = @$vv;
+    my $logic = (defined($raw[0]) and $raw[0] =~ /^-(and|or)$/i)
+      ? (shift(@raw), lc $1) : 'or';
+    my @values = map +{ $vk => $_ }, @raw;
+    if (
+      $op =~ $self->{inequality_op}
+      or $op =~ $self->{not_like_op}
+    ) {
+      if (lc($logic) eq 'or' and @values > 1) {
+        belch "A multi-element arrayref as an argument to the inequality op '${\uc(join ' ', split '_', $op)}' "
+            . 'is technically equivalent to an always-true 1=1 (you probably wanted '
+            . "to say ...{ \$inequality_op => [ -and => \@values ] }... instead)"
+        ;
       }
     }
-    else {
-      $self->_SWITCH_refkind($val, {
-
-        ARRAYREF => sub {       # CASE: col => {op => \@vals}
-          ($sql, @bind) = $self->_where_field_op_ARRAYREF($k, $op, $val);
-        },
-
-        ARRAYREFREF => sub {    # CASE: col => {op => \[$sql, @bind]} (literal SQL with bind)
-          my ($sub_sql, @sub_bind) = @$$val;
-          $self->_assert_bindval_matches_bindtype(@sub_bind);
-          $sql  = join ' ', $self->_convert($self->_quote($k)),
-                            $self->_sqlcase($op),
-                            $sub_sql;
-          @bind = @sub_bind;
-        },
-
-        UNDEF => sub {          # CASE: col => {op => undef} : sql "IS (NOT)? NULL"
-          my $is =
-            $op =~ /^not$/i               ? 'is not'  # legacy
-          : $op =~ $self->{equality_op}   ? 'is'
-          : $op =~ $self->{like_op}       ? belch("Supplying an undefined argument to '@{[ uc $op]}' is deprecated") && 'is'
-          : $op =~ $self->{inequality_op} ? 'is not'
-          : $op =~ $self->{not_like_op}   ? belch("Supplying an undefined argument to '@{[ uc $op]}' is deprecated") && 'is not'
-          : puke "unexpected operator '$orig_op' with undef operand";
+    unless (@values) {
+      # try to DWIM on equality operators
+      return ($self->_dwim_op_to_is($op,
+        "Supplying an empty arrayref to '%s' is deprecated",
+        "operator '%s' applied on an empty array (field '$k')"
+      ) ? $self->sqlfalse : $self->sqltrue);
+    }
+    return $self->_expand_logop($logic => \@values, $k);
+  }
+  if (is_undef_value($vv)) {
+    my $is = ($self->_dwim_op_to_is($op,
+      "Supplying an undefined argument to '%s' is deprecated",
+      "unexpected operator '%s' with undef operand",
+    ) ? 'is' : 'is not');
 
-          $sql = $self->_quote($k) . $self->_sqlcase(" $is null");
-        },
+    return $self->_expand_hashpair($k => { $is, undef });
+  }
+  local our $Cur_Col_Meta = $k;
+  return +{ -op => [
+    $op,
+    $ik,
+    $self->_expand_expr($vv)
+  ] };
+}
 
-        FALLBACK => sub {       # CASE: col => {op/func => $stuff}
-          ($sql, @bind) = $self->_where_unary_op($op, $val);
+sub _dwim_op_to_is {
+  my ($self, $raw, $empty, $fail) = @_;
 
-          $sql = join(' ',
-            $self->_convert($self->_quote($k)),
-            $self->{_nested_func_lhs} eq $k ? $sql : "($sql)",  # top level vs nested
-          );
-        },
-      });
-    }
+  my $op = $self->_normalize_op($raw);
 
-    ($all_sql) = (defined $all_sql and $all_sql) ? $self->_join_sql_clauses($logic, [$all_sql, $sql], []) : $sql;
-    push @all_bind, @bind;
+  if ($op =~ /^not$/i) {
+    return 0;
   }
-  return ($all_sql, @all_bind);
+  if ($op =~ $self->{equality_op}) {
+    return 1;
+  }
+  if ($op =~ $self->{like_op}) {
+    belch(sprintf $empty, uc(join ' ', split '_', $op));
+    return 1;
+  }
+  if ($op =~ $self->{inequality_op}) {
+    return 0;
+  }
+  if ($op =~ $self->{not_like_op}) {
+    belch(sprintf $empty, uc(join ' ', split '_', $op));
+    return 0;
+  }
+  puke(sprintf $fail, $op);
 }
 
-sub _where_field_IS {
-  my ($self, $k, $op, $v) = @_;
-
-  my ($s) = $self->_SWITCH_refkind($v, {
-    UNDEF => sub {
-      join ' ',
-        $self->_convert($self->_quote($k)),
-        map { $self->_sqlcase($_)} ($op, 'null')
-    },
-    FALLBACK => sub {
-      puke "$op can only take undef as argument";
-    },
-  });
+sub _expand_func {
+  my ($self, undef, $args) = @_;
+  my ($func, @args) = @$args;
+  return +{ -func => [ $func, map $self->expand_expr($_), @args ] };
+}
 
-  $s;
+sub _expand_ident {
+  my ($self, undef, $body) = @_;
+  unless (defined($body) or (ref($body) and ref($body) eq 'ARRAY')) {
+    puke "-ident requires a single plain scalar argument (a quotable identifier) or an arrayref of identifier parts";
+  }
+  my ($sep) = map +(defined() ? $_ : '.') , $self->{name_sep};
+  my @parts = map +($sep
+                     ? map split(/\Q${sep}\E/, $_), @$_
+                     : @$_
+                   ), ref($body) ? $body : [ $body ];
+  return { -ident => $parts[-1] } if $self->{_dequalify_idents};
+  unless ($self->{quote_char}) {
+    $self->_assert_pass_injection_guard($_) for @parts;
+  }
+  return +{ -ident => \@parts };
 }
 
-sub _where_field_op_ARRAYREF {
-  my ($self, $k, $op, $vals) = @_;
+sub _expand_value {
+  +{ -bind => [ our $Cur_Col_Meta, $_[2] ] };
+}
 
-  my @vals = @$vals;  #always work on a copy
+sub _expand_row {
+  my ($self, undef, $args) = @_;
+  +{ -row => [ map $self->expand_expr($_), @$args ] };
+}
 
-  if (@vals) {
-    $self->_debug(sprintf '%s means multiple elements: [ %s ]',
-      $vals,
-      join(', ', map { defined $_ ? "'$_'" : 'NULL' } @vals ),
-    );
+sub _expand_op {
+  my ($self, undef, $args) = @_;
+  my ($op, @opargs) = @$args;
+  if (my $exp = $self->{expand_op}{$op}) {
+    return $self->$exp($op, \@opargs);
+  }
+  if (List::Util::first { $op =~ $_->{regex} } @{$self->{unary_ops}}) {
+    return { -op => [ $op, @opargs ] };
+  }
+  +{ -op => [ $op, map $self->expand_expr($_), @opargs ] };
+}
 
-    # see if the first element is an -and/-or op
-    my $logic;
-    if (defined $vals[0] && $vals[0] =~ /^ - (AND|OR) $/ix) {
-      $logic = uc $1;
-      shift @vals;
-    }
+sub _expand_bool {
+  my ($self, undef, $v) = @_;
+  if (ref($v)) {
+    return $self->_expand_expr($v);
+  }
+  puke "-bool => undef not supported" unless defined($v);
+  return $self->_expand_expr({ -ident => $v });
+}
 
-    # a long standing API wart - an attempt to change this behavior during
-    # the 1.50 series failed *spectacularly*. Warn instead and leave the
-    # behavior as is
-    if (
-      @vals > 1
-        and
-      (!$logic or $logic eq 'OR')
-        and
-      ($op =~ $self->{inequality_op} or $op =~ $self->{not_like_op})
-    ) {
-      my $o = uc($op);
-      belch "A multi-element arrayref as an argument to the inequality op '$o' "
-          . 'is technically equivalent to an always-true 1=1 (you probably wanted '
-          . "to say ...{ \$inequality_op => [ -and => \@values ] }... instead)"
-      ;
-    }
+sub _expand_list {
+  my ($self, undef, $expr) = @_;
+  return { -op => [
+    ',', map $self->expand_expr($_),
+          @{$expr->{-op}}[1..$#{$expr->{-op}}]
+  ] } if ref($expr) eq 'HASH' and ($expr->{-op}||[''])->[0] eq ',';
+  return +{ -op => [ ',',
+    map $self->expand_expr($_),
+      ref($expr) eq 'ARRAY' ? @$expr : $expr
+  ] };
+}
 
-    # distribute $op over each remaining member of @vals, append logic if exists
-    return $self->_recurse_where([map { {$k => {$op, $_}} } @vals], $logic);
+sub _expand_logop {
+  my ($self, $logop, $v, $k) = @_;
+  $self->${\$self->{expand_op}{$logop}}($logop, $v, $k);
+}
 
+sub _expand_op_andor {
+  my ($self, $logop, $v, $k) = @_;
+  if (defined $k) {
+    $v = [ map +{ $k, $_ },
+             (ref($v) eq 'HASH')
+              ? (map +{ $_ => $v->{$_} }, sort keys %$v)
+              : @$v,
+         ];
   }
-  else {
-    # try to DWIM on equality operators
-    return
-      $op =~ $self->{equality_op}   ? $self->{sqlfalse}
-    : $op =~ $self->{like_op}       ? belch("Supplying an empty arrayref to '@{[ uc $op]}' is deprecated") && $self->{sqlfalse}
-    : $op =~ $self->{inequality_op} ? $self->{sqltrue}
-    : $op =~ $self->{not_like_op}   ? belch("Supplying an empty arrayref to '@{[ uc $op]}' is deprecated") && $self->{sqltrue}
-    : puke "operator '$op' applied on an empty array (field '$k')";
+  if (ref($v) eq 'HASH') {
+    return undef unless keys %$v;
+    return +{ -op => [
+      $logop,
+      map $self->_expand_expr({ $_ => $v->{$_} }),
+        sort keys %$v
+    ] };
+  }
+  if (ref($v) eq 'ARRAY') {
+    $logop eq 'and' or $logop eq 'or' or puke "unknown logic: $logop";
+
+    my @expr = grep {
+      (ref($_) eq 'ARRAY' and @$_)
+      or (ref($_) eq 'HASH' and %$_)
+      or 1
+    } @$v;
+
+    my @res;
+
+    while (my ($el) = splice @expr, 0, 1) {
+      puke "Supplying an empty left hand side argument is not supported in array-pairs"
+        unless defined($el) and length($el);
+      my $elref = ref($el);
+      if (!$elref) {
+        local our $Expand_Depth = 0;
+        push(@res, grep defined, $self->_expand_expr({ $el, shift(@expr) }));
+      } elsif ($elref eq 'ARRAY') {
+        push(@res, grep defined, $self->_expand_expr($el)) if @$el;
+      } elsif (my $l = is_literal_value($el)) {
+        push @res, { -literal => $l };
+      } elsif ($elref eq 'HASH') {
+        local our $Expand_Depth = 0;
+        push @res, grep defined, $self->_expand_expr($el) if %$el;
+      } else {
+        die "notreached";
+      }
+    }
+    # ???
+    # return $res[0] if @res == 1;
+    return { -op => [ $logop, @res ] };
   }
+  die "notreached";
 }
 
-
-sub _where_hashpair_SCALARREF {
-  my ($self, $k, $v) = @_;
-  $self->_debug("SCALAR($k) means literal SQL: $$v");
-  my $sql = $self->_quote($k) . " " . $$v;
-  return ($sql);
+sub _expand_op_is {
+  my ($self, $op, $vv, $k) = @_;
+  ($k, $vv) = @$vv unless defined $k;
+  puke "$op can only take undef as argument"
+    if defined($vv)
+       and not (
+         ref($vv) eq 'HASH'
+         and exists($vv->{-value})
+         and !defined($vv->{-value})
+       );
+  return +{ -op => [ $op.'_null', $self->expand_expr($k, -ident) ] };
 }
 
-# literal SQL with bind
-sub _where_hashpair_ARRAYREFREF {
-  my ($self, $k, $v) = @_;
-  $self->_debug("REF($k) means literal SQL: @${$v}");
-  my ($sql, @bind) = @$$v;
-  $self->_assert_bindval_matches_bindtype(@bind);
-  $sql  = $self->_quote($k) . " " . $sql;
-  return ($sql, @bind );
+sub _expand_between {
+  my ($self, $op, $vv, $k) = @_;
+  my @rhs = map $self->_expand_expr($_),
+              ref($vv) eq 'ARRAY' ? @$vv : $vv;
+  unless (
+    (@rhs == 1 and ref($rhs[0]) eq 'HASH' and $rhs[0]->{-literal})
+    or
+    (@rhs == 2 and defined($rhs[0]) and defined($rhs[1]))
+  ) {
+    puke "Operator '${\uc($op)}' requires either an arrayref with two defined values or expressions, or a single literal scalarref/arrayref-ref";
+  }
+  return +{ -op => [
+    $op,
+    $self->expand_expr($k),
+    map $self->expand_expr($_, -value), @rhs
+  ] }
 }
 
-# literal SQL without bind
-sub _where_hashpair_SCALAR {
-  my ($self, $k, $v) = @_;
-  $self->_debug("NOREF($k) means simple key=val: $k $self->{cmp} $v");
-  my $sql = join ' ', $self->_convert($self->_quote($k)),
-                      $self->_sqlcase($self->{cmp}),
-                      $self->_convert('?');
-  my @bind =  $self->_bindtype($k, $v);
-  return ($sql, @bind);
+sub _expand_in {
+  my ($self, $op, $vv, $k) = @_;
+  if (my $literal = is_literal_value($vv)) {
+    my ($sql, @bind) = @$literal;
+    my $opened_sql = $self->_open_outer_paren($sql);
+    return +{ -op => [
+      $op, $self->expand_expr($k, -ident),
+      { -literal => [ $opened_sql, @bind ] }
+    ] };
+  }
+  my $undef_err =
+    'SQL::Abstract before v1.75 used to generate incorrect SQL when the '
+  . "-${\uc($op)} operator was given an undef-containing list: !!!AUDIT YOUR CODE "
+  . 'AND DATA!!! (the upcoming Data::Query-based version of SQL::Abstract '
+  . 'will emit the logically correct SQL instead of raising this exception)'
+  ;
+  puke("Argument passed to the '${\uc($op)}' operator can not be undefined")
+    if !defined($vv);
+  my @rhs = map $self->expand_expr($_, -value),
+              map { defined($_) ? $_: puke($undef_err) }
+                (ref($vv) eq 'ARRAY' ? @$vv : $vv);
+  return $self->${\($op =~ /^not/ ? 'sqltrue' : 'sqlfalse')} unless @rhs;
+
+  return +{ -op => [
+    $op,
+    $self->expand_expr($k, -ident),
+    @rhs
+  ] };
 }
 
+sub _expand_nest {
+  my ($self, undef, $v) = @_;
+  # DBIx::Class requires a nest warning to be emitted once but the private
+  # method it overrode to do so no longer exists
+  if ($self->{warn_once_on_nest}) {
+    unless (our $Nest_Warned) {
+      belch(
+        "-nest in search conditions is deprecated, you most probably wanted:\n"
+        .q|{..., -and => [ \%cond0, \@cond1, \'cond2', \[ 'cond3', [ col => bind ] ], etc. ], ... }|
+      );
+      $Nest_Warned = 1;
+    }
+  }
+  return $self->_expand_expr($v);
+}
 
-sub _where_hashpair_UNDEF {
-  my ($self, $k, $v) = @_;
-  $self->_debug("UNDEF($k) means IS NULL");
-  my $sql = $self->_quote($k) . $self->_sqlcase(' is null');
-  return ($sql);
+sub _expand_values {
+  my ($self, undef, $values) = @_;
+  return { -values => [
+    map +(
+      ref($_) eq 'HASH'
+        ? $self->expand_expr($_)
+        : +{ -row => [ map $self->expand_expr($_), @$_ ] }
+    ), ref($values) eq 'ARRAY' ? @$values : $values
+  ] };
 }
 
-#======================================================================
-# WHERE: TOP-LEVEL OTHERS (SCALARREF, SCALAR, UNDEF)
-#======================================================================
+sub _recurse_where {
+  my ($self, $where) = @_;
 
+  # Special case: top level simple string treated as literal
 
-sub _where_SCALARREF {
-  my ($self, $where) = @_;
+  my $where_exp = (ref($where)
+                    ? $self->_expand_select_clause_where(undef, $where)
+                    : { -literal => [ $where ] });
 
-  # literal sql
-  $self->_debug("SCALAR(*top) means literal SQL: $$where");
-  return ($$where);
-}
+  # dispatch expanded expression
 
+  my ($sql, @bind) = defined($where_exp) ? @{ $self->render_aqt($where_exp) || [] } : ();
+  # DBIx::Class used to call _recurse_where in scalar context
+  # something else might too...
+  if (wantarray) {
+    return ($sql, @bind);
+  }
+  else {
+    belch "Calling _recurse_where in scalar context is deprecated and will go away before 2.0";
+    return $sql;
+  }
+}
 
-sub _where_SCALAR {
-  my ($self, $where) = @_;
+sub _render_ident {
+  my ($self, undef, $ident) = @_;
 
-  # literal sql
-  $self->_debug("NOREF(*top) means literal SQL: $where");
-  return ($where);
+  return [ $self->_quote($ident) ];
 }
 
+sub _render_row {
+  my ($self, undef, $values) = @_;
+  return $self->join_query_parts('',
+    '(',
+    $self->_render_op(undef, [ ',', @$values ]),
+    ')'
+  );
+}
 
-sub _where_UNDEF {
-  my ($self) = @_;
-  return ();
+sub _render_func {
+  my ($self, undef, $rest) = @_;
+  my ($func, @args) = @$rest;
+  return $self->join_query_parts('',
+    $self->_sqlcase($func),
+    $self->join_query_parts('',
+      '(',
+      $self->join_query_parts(', ', @args),
+      ')'
+    ),
+  );
 }
 
+sub _render_bind {
+  my ($self, undef, $bind) = @_;
+  return [ '?', $self->_bindtype(@$bind) ];
+}
 
-#======================================================================
-# WHERE: BUILTIN SPECIAL OPERATORS (-in, -between)
-#======================================================================
+sub _render_literal {
+  my ($self, undef, $literal) = @_;
+  $self->_assert_bindval_matches_bindtype(@{$literal}[1..$#$literal]);
+  return $literal;
+}
 
+sub _render_keyword {
+  my ($self, undef, $keyword) = @_;
+  return [ $self->_sqlcase(
+    ref($keyword) ? $$keyword : join ' ', split '_', $keyword
+  ) ];
+}
 
-sub _where_field_BETWEEN {
-  my ($self, $k, $op, $vals) = @_;
+sub _render_op {
+  my ($self, undef, $v) = @_;
+  my ($op, @args) = @$v;
+  if (my $r = $self->{render_op}{$op}) {
+    return $self->$r($op, \@args);
+  }
 
-  my ($label, $and, $placeholder);
-  $label       = $self->_convert($self->_quote($k));
-  $and         = ' ' . $self->_sqlcase('and') . ' ';
-  $placeholder = $self->_convert('?');
-  $op               = $self->_sqlcase($op);
+  { # Old SQLA compat
 
-  my $invalid_args = "Operator '$op' requires either an arrayref with two defined values or expressions, or a single literal scalarref/arrayref-ref";
+    my $op = join(' ', split '_', $op);
 
-  my ($clause, @bind) = $self->_SWITCH_refkind($vals, {
-    ARRAYREFREF => sub {
-      my ($s, @b) = @$$vals;
-      $self->_assert_bindval_matches_bindtype(@b);
-      ($s, @b);
-    },
-    SCALARREF => sub {
-      return $$vals;
-    },
-    ARRAYREF => sub {
-      puke $invalid_args if @$vals != 2;
-
-      my (@all_sql, @all_bind);
-      foreach my $val (@$vals) {
-        my ($sql, @bind) = $self->_SWITCH_refkind($val, {
-           SCALAR => sub {
-             return ($placeholder, $self->_bindtype($k, $val) );
-           },
-           SCALARREF => sub {
-             return $$val;
-           },
-           ARRAYREFREF => sub {
-             my ($sql, @bind) = @$$val;
-             $self->_assert_bindval_matches_bindtype(@bind);
-             return ($sql, @bind);
-           },
-           HASHREF => sub {
-             my ($func, $arg, @rest) = %$val;
-             puke "Only simple { -func => arg } functions accepted as sub-arguments to BETWEEN"
-               if (@rest or $func !~ /^ \- (.+)/x);
-             $self->_where_unary_op($1 => $arg);
-           },
-           FALLBACK => sub {
-             puke $invalid_args,
-           },
-        });
-        push @all_sql, $sql;
-        push @all_bind, @bind;
-      }
+    my $ss = List::Util::first { $op =~ $_->{regex} } @{$self->{special_ops}};
+    if ($ss and @args > 1) {
+      puke "Special op '${op}' requires first value to be identifier"
+        unless my ($ident) = map $_->{-ident}, grep ref($_) eq 'HASH', $args[0];
+      my $k = join(($self->{name_sep}||'.'), @$ident);
+      local our $Expand_Depth = 1;
+      return [ $self->${\($ss->{handler})}($k, $op, $args[1]) ];
+    }
+    if (my $us = List::Util::first { $op =~ $_->{regex} } @{$self->{unary_ops}}) {
+      return [ $self->${\($us->{handler})}($op, $args[0]) ];
+    }
+    if ($ss) {
+      return $self->_render_unop_paren($op, \@args);
+    }
+  }
+  if (@args == 1) {
+    return $self->_render_unop_prefix($op, \@args);
+  } else {
+    return $self->_render_op_multop($op, \@args);
+  }
+  die "notreached";
+}
 
-      return (
-        (join $and, @all_sql),
-        @all_bind
-      );
-    },
-    FALLBACK => sub {
-      puke $invalid_args,
-    },
-  });
 
-  my $sql = "( $label $op $clause )";
-  return ($sql, @bind)
+sub _render_op_between {
+  my ($self, $op, $args) = @_;
+  my ($left, $low, $high) = @$args;
+  my @rh = do {
+    if (@$args == 2) {
+      puke "Single arg to between must be a literal"
+        unless $low->{-literal};
+      $low;
+    } else {
+      +($low, { -keyword => 'and' }, $high);
+    }
+  };
+  return $self->join_query_parts(' ',
+    '(', $left, { -keyword => $op }, @rh, ')',
+  );
 }
 
+sub _render_op_in {
+  my ($self, $op, $args) = @_;
+  my ($lhs, @rhs) = @$args;
+
+  return $self->join_query_parts(' ',
+    $lhs,
+    { -keyword => $op },
+    $self->join_query_parts(' ',
+      '(',
+      $self->join_query_parts(', ', @rhs),
+      ')'
+    ),
+  );
+}
 
-sub _where_field_IN {
-  my ($self, $k, $op, $vals) = @_;
+sub _render_op_andor {
+  my ($self, $op, $args) = @_;
+  return undef unless @$args;
+  return $self->join_query_parts('', $args->[0]) if @$args == 1;
+  my $inner = $self->_render_op_multop($op, $args);
+  return undef unless defined($inner->[0]) and length($inner->[0]);
+  return $self->join_query_parts(' ',
+    '(', $inner, ')'
+  );
+}
 
-  # backwards compatibility: if scalar, force into an arrayref
-  $vals = [$vals] if defined $vals && ! ref $vals;
-
-  my ($label)       = $self->_convert($self->_quote($k));
-  my ($placeholder) = $self->_convert('?');
-  $op               = $self->_sqlcase($op);
-
-  my ($sql, @bind) = $self->_SWITCH_refkind($vals, {
-    ARRAYREF => sub {     # list of choices
-      if (@$vals) { # nonempty list
-        my (@all_sql, @all_bind);
-
-        for my $val (@$vals) {
-          my ($sql, @bind) = $self->_SWITCH_refkind($val, {
-            SCALAR => sub {
-              return ($placeholder, $val);
-            },
-            SCALARREF => sub {
-              return $$val;
-            },
-            ARRAYREFREF => sub {
-              my ($sql, @bind) = @$$val;
-              $self->_assert_bindval_matches_bindtype(@bind);
-              return ($sql, @bind);
-            },
-            HASHREF => sub {
-              my ($func, $arg, @rest) = %$val;
-              puke "Only simple { -func => arg } functions accepted as sub-arguments to IN"
-                if (@rest or $func !~ /^ \- (.+)/x);
-              $self->_where_unary_op($1 => $arg);
-            },
-            UNDEF => sub {
-              puke(
-                'SQL::Abstract before v1.75 used to generate incorrect SQL when the '
-              . "-$op operator was given an undef-containing list: !!!AUDIT YOUR CODE "
-              . 'AND DATA!!! (the upcoming Data::Query-based version of SQL::Abstract '
-              . 'will emit the logically correct SQL instead of raising this exception)'
-              );
-            },
-          });
-          push @all_sql, $sql;
-          push @all_bind, @bind;
-        }
+sub _render_op_multop {
+  my ($self, $op, $args) = @_;
+  my @parts = @$args;
+  return undef unless @parts;
+  return $self->render_aqt($parts[0]) if @parts == 1;
+  my $join = ($op eq ','
+                ? ', '
+                : { -keyword => " ${op} " }
+             );
+  return $self->join_query_parts($join, @parts);
+}
 
-        return (
-          sprintf('%s %s ( %s )',
-            $label,
-            $op,
-            join(', ', @all_sql)
-          ),
-          $self->_bindtype($k, @all_bind),
-        );
-      }
-      else { # empty list: some databases won't understand "IN ()", so DWIM
-        my $sql = ($op =~ /\bnot\b/i) ? $self->{sqltrue} : $self->{sqlfalse};
-        return ($sql);
-      }
-    },
+sub _render_values {
+  my ($self, undef, $values) = @_;
+  my $inner = $self->join_query_parts(' ',
+    { -keyword => 'values' },
+    $self->join_query_parts(', ',
+      ref($values) eq 'ARRAY' ? @$values : $values
+    ),
+  );
+  return $self->join_query_parts('',
+    (our $Render_Top_Level ? $inner : ('(', $inner, ')'))
+  );
+}
 
-    SCALARREF => sub {  # literal SQL
-      my $sql = $self->_open_outer_paren($$vals);
-      return ("$label $op ( $sql )");
-    },
-    ARRAYREFREF => sub {  # literal SQL with bind
-      my ($sql, @bind) = @$$vals;
-      $self->_assert_bindval_matches_bindtype(@bind);
-      $sql = $self->_open_outer_paren($sql);
-      return ("$label $op ( $sql )", @bind);
-    },
+sub join_query_parts {
+  my ($self, $join, @parts) = @_;
+  if (ref($join) eq 'HASH') {
+    $join = $self->render_aqt($join)->[0];
+  }
+  my @final = map +(
+    ref($_) eq 'HASH'
+      ? $self->render_aqt($_)
+      : ((ref($_) eq 'ARRAY') ? $_ : [ $_ ])
+  ), @parts;
+  return [
+    $self->{join_sql_parts}->(
+      $join, grep defined && length, map $_->[0], @final
+    ),
+    (map @{$_}[1..$#$_], @final),
+  ];
+}
 
-    UNDEF => sub {
-      puke "Argument passed to the '$op' operator can not be undefined";
-    },
+sub _render_unop_paren {
+  my ($self, $op, $v) = @_;
+  return $self->join_query_parts('',
+    '(', $self->_render_unop_prefix($op, $v), ')'
+  );
+}
 
-    FALLBACK => sub {
-      puke "special op $op requires an arrayref (or scalarref/arrayref-ref)";
-    },
-  });
+sub _render_unop_prefix {
+  my ($self, $op, $v) = @_;
+  my $op_sql = $self->{restore_old_unop_handling}
+                 ? $self->_sqlcase($op)
+                 : { -keyword => $op };
+  return $self->join_query_parts(' ',
+    ($self->{restore_old_unop_handling}
+      ? $self->_sqlcase($op)
+      : { -keyword => \$op }),
+    $v->[0]
+  );
+}
 
-  return ($sql, @bind);
+sub _render_unop_postfix {
+  my ($self, $op, $v) = @_;
+  return $self->join_query_parts(' ',
+    $v->[0], { -keyword => $op },
+  );
 }
 
 # Some databases (SQLite) treat col IN (1, 2) different from
@@ -1319,90 +1731,108 @@ sub _open_outer_paren {
   $sql;
 }
 
+sub _where_field_IN {
+  my ($self, $k, $op, $vals) = @_;
+  @{$self->_render_op_in(
+    $op,
+    [
+      $self->expand_expr($k, -ident),
+      map $self->expand_expr($_, -value),
+        ref($vals) eq 'ARRAY' ? @$vals : $vals
+    ]
+  )};
+}
+
+sub _where_field_BETWEEN {
+  my ($self, $k, $op, $vals) = @_;
+  @{$self->_render_op_between(
+    $op,
+    [ $self->expand_expr($k, -ident), ref($vals) eq 'ARRAY' ? @$vals : $vals ]
+  )};
+}
 
 #======================================================================
 # ORDER BY
 #======================================================================
 
-sub _order_by {
+sub _expand_order_by {
   my ($self, $arg) = @_;
 
-  my (@sql, @bind);
-  for my $c ($self->_order_by_chunks($arg) ) {
-    $self->_SWITCH_refkind($c, {
-      SCALAR => sub { push @sql, $c },
-      ARRAYREF => sub { push @sql, shift @$c; push @bind, @$c },
-    });
-  }
-
-  my $sql = @sql
-    ? sprintf('%s %s',
-        $self->_sqlcase(' order by'),
-        join(', ', @sql)
-      )
-    : ''
-  ;
-
-  return wantarray ? ($sql, @bind) : $sql;
+  return unless defined($arg) and not (ref($arg) eq 'ARRAY' and !@$arg);
+
+  return $self->expand_expr({ -list => $arg })
+    if ref($arg) eq 'HASH' and ($arg->{-op}||[''])->[0] eq ',';
+
+  my $expander = sub {
+    my ($self, $dir, $expr) = @_;
+    my @to_expand = ref($expr) eq 'ARRAY' ? @$expr : $expr;
+    foreach my $arg (@to_expand) {
+      if (
+        ref($arg) eq 'HASH'
+        and keys %$arg > 1
+        and grep /^-(asc|desc)$/, keys %$arg
+      ) {
+        puke "ordering direction hash passed to order by must have exactly one key (-asc or -desc)";
+      }
+    }
+    my @exp = map +(
+                defined($dir) ? { -op => [ $dir =~ /^-?(.*)$/ ,=> $_ ] } : $_
+              ),
+                map $self->expand_expr($_, -ident),
+                map ref($_) eq 'ARRAY' ? @$_ : $_, @to_expand;
+    return undef unless @exp;
+    return undef if @exp == 1 and not defined($exp[0]);
+    return +{ -op => [ ',', @exp ] };
+  };
+
+  local @{$self->{expand}}{qw(asc desc)} = (($expander) x 2);
+
+  return $self->$expander(undef, $arg);
 }
 
-sub _order_by_chunks {
+sub _order_by {
   my ($self, $arg) = @_;
 
-  return $self->_SWITCH_refkind($arg, {
-
-    ARRAYREF => sub {
-      map { $self->_order_by_chunks($_ ) } @$arg;
-    },
-
-    ARRAYREFREF => sub {
-      my ($s, @b) = @$$arg;
-      $self->_assert_bindval_matches_bindtype(@b);
-      [ $s, @b ];
-    },
+  return '' unless defined(my $expanded = $self->_expand_order_by($arg));
 
-    SCALAR    => sub {$self->_quote($arg)},
+  my ($sql, @bind) = @{ $self->render_aqt($expanded) };
 
-    UNDEF     => sub {return () },
+  return '' unless length($sql);
 
-    SCALARREF => sub {$$arg}, # literal SQL, no quoting
+  my $final_sql = $self->_sqlcase(' order by ').$sql;
 
-    HASHREF   => sub {
-      # get first pair in hash
-      my ($key, $val, @rest) = %$arg;
+  return $final_sql unless wantarray;
 
-      return () unless $key;
+  return ($final_sql, @bind);
+}
 
-      if (@rest or not $key =~ /^-(desc|asc)/i) {
-        puke "hash passed to _order_by must have exactly one key (-desc or -asc)";
-      }
+# _order_by no longer needs to call this so doesn't but DBIC uses it.
 
-      my $direction = $1;
+sub _order_by_chunks {
+  my ($self, $arg) = @_;
 
-      my @ret;
-      for my $c ($self->_order_by_chunks($val)) {
-        my ($sql, @bind);
+  return () unless defined(my $expanded = $self->_expand_order_by($arg));
 
-        $self->_SWITCH_refkind($c, {
-          SCALAR => sub {
-            $sql = $c;
-          },
-          ARRAYREF => sub {
-            ($sql, @bind) = @$c;
-          },
-        });
+  my @res = $self->_chunkify_order_by($expanded);
+  (ref() ? $_->[0] : $_) .= '' for @res;
+  return @res;
+}
 
-        $sql = $sql . ' ' . $self->_sqlcase($direction);
+sub _chunkify_order_by {
+  my ($self, $expanded) = @_;
 
-        push @ret, [ $sql, @bind];
-      }
+  return grep length, @{ $self->render_aqt($expanded) }
+    if $expanded->{-ident} or @{$expanded->{-literal}||[]} == 1;
 
-      return @ret;
-    },
-  });
+  for ($expanded) {
+    if (ref() eq 'HASH' and $_->{-op} and $_->{-op}[0] eq ',') {
+      my ($comma, @list) = @{$_->{-op}};
+      return map $self->_chunkify_order_by($_), @list;
+    }
+    return $self->render_aqt($_);
+  }
 }
 
-
 #======================================================================
 # DATASOURCE (FOR NOW, JUST PLAIN TABLE OR LIST OF TABLES)
 #======================================================================
@@ -1410,11 +1840,9 @@ sub _order_by_chunks {
 sub _table  {
   my $self = shift;
   my $from = shift;
-  $self->_SWITCH_refkind($from, {
-    ARRAYREF     => sub {join ', ', map { $self->_quote($_) } @$from;},
-    SCALAR       => sub {$self->_quote($from)},
-    SCALARREF    => sub {$$from},
-  });
+  $self->render_aqt(
+    $self->expand_expr({ -list => $from }, -ident)
+  )->[0];
 }
 
 
@@ -1428,9 +1856,16 @@ sub _quote {
 
   return '' unless defined $_[1];
   return ${$_[1]} if ref($_[1]) eq 'SCALAR';
+  puke 'Identifier cannot be hashref' if ref($_[1]) eq 'HASH';
 
-  $_[0]->{quote_char} or
-    ($_[0]->_assert_pass_injection_guard($_[1]), return $_[1]);
+  unless ($_[0]->{quote_char}) {
+    if (ref($_[1]) eq 'ARRAY') {
+      return join($_[0]->{name_sep}||'.', @{$_[1]});
+    } else {
+      $_[0]->_assert_pass_injection_guard($_[1]);
+      return $_[1];
+    }
+  }
 
   my $qref = ref $_[0]->{quote_char};
   my ($l, $r) =
@@ -1441,9 +1876,21 @@ sub _quote {
   my $esc = $_[0]->{escape_char} || $r;
 
   # parts containing * are naturally unquoted
-  return join($_[0]->{name_sep}||'', map
-    +( $_ eq '*' ? $_ : do { (my $n = $_) =~ s/(\Q$esc\E|\Q$r\E)/$esc$1/g; $l . $n . $r } ),
-    ( $_[0]->{name_sep} ? split (/\Q$_[0]->{name_sep}\E/, $_[1] ) : $_[1] )
+  return join(
+    $_[0]->{name_sep}||'',
+    map +(
+      $_ eq '*'
+        ? $_
+        : do { (my $n = $_) =~ s/(\Q$esc\E|\Q$r\E)/$esc$1/g; $l . $n . $r }
+    ),
+    (ref($_[1]) eq 'ARRAY'
+      ? @{$_[1]}
+      : (
+          $_[0]->{name_sep}
+            ? split (/\Q$_[0]->{name_sep}\E/, $_[1] )
+            : $_[1]
+        )
+    )
   );
 }
 
@@ -1451,8 +1898,11 @@ sub _quote {
 # Conversion, if applicable
 sub _convert {
   #my ($self, $arg) = @_;
-  if ($_[0]->{convert}) {
-    return $_[0]->_sqlcase($_[0]->{convert}) .'(' . $_[1] . ')';
+  if (my $conv = $_[0]->{convert_where}) {
+    return @{ $_[0]->join_query_parts('',
+      $_[0]->_sqlcase($conv),
+      '(' , $_[1] , ')'
+    ) };
   }
   return $_[1];
 }
@@ -1481,23 +1931,6 @@ sub _assert_bindval_matches_bindtype {
   }
 }
 
-sub _join_sql_clauses {
-  my ($self, $logic, $clauses_aref, $bind_aref) = @_;
-
-  if (@$clauses_aref > 1) {
-    my $join  = " " . $self->_sqlcase($logic) . " ";
-    my $sql = '( ' . join($join, @$clauses_aref) . ' )';
-    return ($sql, @$bind_aref);
-  }
-  elsif (@$clauses_aref) {
-    return ($clauses_aref->[0], @$bind_aref); # no parentheses
-  }
-  else {
-    return (); # if no SQL, ignore @$bind_aref
-  }
-}
-
-
 # Fix SQL case, if so requested
 sub _sqlcase {
   # LDNOTE: if $self->{case} is true, then it contains 'lower', so we
@@ -1505,7 +1938,6 @@ sub _sqlcase {
   return $_[0]->{case} ? $_[1] : uc($_[1]);
 }
 
-
 #======================================================================
 # DISPATCHING FROM REFKIND
 #======================================================================
@@ -1688,6 +2120,7 @@ sub AUTOLOAD {
     # This allows us to check for a local, then _form, attr
     my $self = shift;
     my($name) = $AUTOLOAD =~ /.*::(.+)/;
+    puke "AUTOLOAD invoked for method name ${name} and allow_autoload option not set" unless $self->{allow_autoload};
     return $self->generate($name, @_);
 }
 
@@ -2326,6 +2759,10 @@ module:
 On failure returns C<undef>, on success returns an B<array> reference
 containing the unpacked version of the supplied literal SQL and bind values.
 
+=head2 is_undef_value
+
+Tests for undef, whether expanded or not.
+
 =head1 WHERE CLAUSES
 
 =head2 Introduction
@@ -2981,7 +3418,9 @@ forms. Examples:
 
 
 
-=head1 SPECIAL OPERATORS
+=head1 OLD EXTENSION SYSTEM
+
+=head2 SPECIAL OPERATORS
 
   my $sqlmaker = SQL::Abstract->new(special_ops => [
      {
@@ -3066,7 +3505,7 @@ of the MATCH .. AGAINST syntax for MySQL
   ]);
 
 
-=head1 UNARY OPERATORS
+=head2 UNARY OPERATORS
 
   my $sqlmaker = SQL::Abstract->new(unary_ops => [
      {
@@ -3118,6 +3557,186 @@ When supplied with a coderef, it is called as:
 
 =back
 
+=head1 NEW METHODS (EXPERIMENTAL)
+
+See L<SQL::Abstract::Reference> for the C<expr> versus C<aqt> concept and
+an explanation of what the below extensions are extending.
+
+=head2 plugin
+
+  $sqla->plugin('+Foo');
+
+Enables plugin SQL::Abstract::Plugin::Foo.
+
+=head2 render_expr
+
+  my ($sql, @bind) = $sqla->render_expr($expr);
+
+=head2 render_statement
+
+Use this if you may be rendering a top level statement so e.g. a SELECT
+query doesn't get wrapped in parens
+
+  my ($sql, @bind) = $sqla->render_statement($expr);
+
+=head2 expand_expr
+
+Expression expansion with optional default for scalars.
+
+  my $aqt = $self->expand_expr($expr);
+  my $aqt = $self->expand_expr($expr, -ident);
+
+=head2 render_aqt
+
+Top level means avoid parens on statement AQT.
+
+  my $res = $self->render_aqt($aqt, $top_level);
+  my ($sql, @bind) = @$res;
+
+=head2 join_query_parts
+
+Similar to join() but will render hashrefs as nodes for both join and parts,
+and treats arrayref as a nested C<[ $join, @parts ]> structure.
+
+  my $part = $self->join_query_parts($join, @parts);
+
+=head1 NEW EXTENSION SYSTEM
+
+=head2 clone
+
+  my $sqla2 = $sqla->clone;
+
+Performs a semi-shallow copy such that extension methods won't leak state
+but excessive depth is avoided.
+
+=head2 expander
+
+=head2 expanders
+
+=head2 op_expander
+
+=head2 op_expanders
+
+=head2 clause_expander
+
+=head2 clause_expanders
+
+  $sqla->expander('name' => sub { ... });
+  $sqla->expanders('name1' => sub { ... }, 'name2' => sub { ... });
+
+=head2 expander_list
+
+=head2 op_expander_list
+
+=head2 clause_expander_list
+
+  my @names = $sqla->expander_list;
+
+=head2 wrap_expander
+
+=head2 wrap_expanders
+
+=head2 wrap_op_expander
+
+=head2 wrap_op_expanders
+
+=head2 wrap_clause_expander
+
+=head2 wrap_clause_expanders
+
+  $sqla->wrap_expander('name' => sub { my ($orig) = @_; sub { ... } });
+  $sqla->wrap_expanders(
+    'name1' => sub { my ($orig1) = @_; sub { ... } },
+    'name2' => sub { my ($orig2) = @_; sub { ... } },
+  );
+
+=head2 renderer
+
+=head2 renderers
+
+=head2 op_renderer
+
+=head2 op_renderers
+
+=head2 clause_renderer
+
+=head2 clause_renderers
+
+  $sqla->renderer('name' => sub { ... });
+  $sqla->renderers('name1' => sub { ... }, 'name2' => sub { ... });
+
+=head2 renderer_list
+
+=head2 op_renderer_list
+
+=head2 clause_renderer_list
+
+  my @names = $sqla->renderer_list;
+
+=head2 wrap_renderer
+
+=head2 wrap_renderers
+
+=head2 wrap_op_renderer
+
+=head2 wrap_op_renderers
+
+=head2 wrap_clause_renderer
+
+=head2 wrap_clause_renderers
+
+  $sqla->wrap_renderer('name' => sub { my ($orig) = @_; sub { ... } });
+  $sqla->wrap_renderers(
+    'name1' => sub { my ($orig1) = @_; sub { ... } },
+    'name2' => sub { my ($orig2) = @_; sub { ... } },
+  );
+
+=head2 clauses_of
+
+  my @clauses = $sqla->clauses_of('select');
+  $sqla->clauses_of(select => \@new_clauses);
+  $sqla->clauses_of(select => sub {
+    my (undef, @old_clauses) = @_;
+    ...
+    return @new_clauses;
+  });
+
+=head2 statement_list
+
+  my @list = $sqla->statement_list;
+
+=head2 make_unop_expander
+
+  my $exp = $sqla->make_unop_expander(sub { ... });
+
+If the op is found as a binop, assumes it wants a default comparison, so
+the inner expander sub can reliably operate as
+
+  sub { my ($self, $name, $body) = @_; ... }
+
+=head2 make_binop_expander
+
+  my $exp = $sqla->make_binop_expander(sub { ... });
+
+If the op is found as a unop, assumes the value will be an arrayref with the
+LHS as the first entry, and converts that to an ident node if it's a simple
+scalar. So the inner expander sub looks like
+
+  sub {
+    my ($self, $name, $body, $k) = @_;
+    { -blah => [ map $self->expand_expr($_), $k, $body ] }
+  }
+
+=head2 unop_expander
+
+=head2 unop_expanders
+
+=head2 binop_expander
+
+=head2 binop_expanders
+
+The above methods operate exactly like the op_ versions but wrap the coderef
+using the appropriate make_ method first.
 
 =head1 PERFORMANCE
 
diff --git a/lib/SQL/Abstract/Formatter.pm b/lib/SQL/Abstract/Formatter.pm
new file mode 100644 (file)
index 0000000..47694f0
--- /dev/null
@@ -0,0 +1,80 @@
+package SQL::Abstract::Formatter;
+
+require SQL::Abstract::Parts; # it loads us too, don't cross the streams
+
+use Moo;
+
+has indent_by => (is => 'ro', default => '  ');
+has max_width => (is => 'ro', default => 78);
+
+sub _join {
+  shift;
+  return SQL::Abstract::Parts::stringify(\@_);
+}
+
+sub format {
+  my ($self, $join, @parts) = @_;
+  $self->_fold_sql('', '', @{$self->_simplify($join, @parts)});
+}
+
+sub _simplify {
+  my ($self, $join, @parts) = @_;
+  return '' unless @parts;
+  return $parts[0] if @parts == 1 and !ref($parts[0]);
+  return $self->_simplify(@{$parts[0]}) if @parts == 1;
+  return [ $join, map ref() ? $self->_simplify(@$_) : $_, @parts ];
+}
+
+sub _fold_sql {
+  my ($self, $indent0, $indent, $join, @parts) = @_;
+  my @res;
+  my $w = $self->max_width;
+  my $join_len = 0;
+  (s/, \z/,\n/ and $join_len = 1)
+    or s/\a /\n/
+    or $_ = "\n"
+      for my $line_join = $join;
+  my ($nl_pre, $nl_post) = split "\n", $line_join;
+  my $line_orig = my $line = $indent0;
+  my $next_indent = $indent.$self->indent_by;
+  my $line_proto = $indent.$nl_post;
+  PART: foreach my $idx (0..$#parts) {
+    my $p = $parts[$idx];
+#::DwarnT STARTPART => $p, \@res, $line, $line_orig;
+    my $pre = ($line ne $line_orig ? $join : '');
+    my $j_part = $pre.(my $j = ref($p) ? $self->_join(@$p) : $p);
+    if (length($j_part) + length($line) + $join_len <= $w) {
+      $line .= $j_part;
+      next PART;
+    }
+    my $innerdent = @res
+                      ? $next_indent
+                      : $indent0.$self->indent_by;
+    if (ref($p) and $p->[1] eq '(' and $p->[-1] eq ')') {
+      my $already = !($line eq $indent0 or $line eq $line_orig);
+      push @res, $line.($already ? $join : '').'('."\n";
+      my (undef, undef, $inner) = @$p;
+      my $folded = $self->_fold_sql($innerdent, $innerdent, @$inner);
+      $folded =~ s/\n\z//;
+      push @res, $folded."\n";
+      $line_orig = $line
+         = $indent0.')'.($idx == $#parts ? '' : $join);
+      next PART;
+    }
+    if ($line ne $line_orig) {
+      push @res, $line.($idx == $#parts ? '' : $nl_pre)."\n";
+    }
+    if (length($line = $line_proto.$j) <= $w) {
+      next PART;
+    }
+    my $folded = $self->_fold_sql($line_proto, $innerdent, @$p);
+    $folded =~ s/\n\z//;
+    push @res, $folded.($idx == $#parts ? '' : $nl_pre)."\n";
+    $line_orig = $line = $idx == $#parts ? '' : $line_proto;
+  } continue {
+#::DwarnT ENDPART => $parts[$idx], \@res, $line, $line_orig;
+  }
+  return join '', @res, $line;
+}
+
+1;
diff --git a/lib/SQL/Abstract/Parts.pm b/lib/SQL/Abstract/Parts.pm
new file mode 100644 (file)
index 0000000..e4a2033
--- /dev/null
@@ -0,0 +1,37 @@
+package SQL::Abstract::Parts;
+
+use Module::Runtime ();
+use Scalar::Util ();
+use strict;
+use warnings;
+
+use overload '""' => 'stringify', fallback => 1;
+
+sub new {
+  my ($proto, $join, @parts) = @_;
+  bless([
+    $join, map Scalar::Util::blessed($_) ? [ @$_ ] : $_, @parts
+  ], ref($proto) || $proto);
+}
+
+sub stringify {
+  my ($self) = @_;
+  my ($join, @parts) = @$self;
+  return join($join, map +(ref() ? stringify($_) : $_), @parts);
+}
+
+sub to_array { return @{$_[0]} }
+
+sub formatter {
+  my ($self, %opts) = @_;
+  Module::Runtime::use_module('SQL::Abstract::Formatter')
+    ->new(%opts)
+}
+
+sub format {
+  my ($self, %opts) = @_;
+  $self->formatter(%opts)
+       ->format($self->to_array);
+}
+
+1;
diff --git a/lib/SQL/Abstract/Plugin/BangOverrides.pm b/lib/SQL/Abstract/Plugin/BangOverrides.pm
new file mode 100644 (file)
index 0000000..0f8348f
--- /dev/null
@@ -0,0 +1,92 @@
+package SQL::Abstract::Plugin::BangOverrides;
+
+use Moo;
+
+with 'SQL::Abstract::Role::Plugin';
+
+sub register_extensions {
+  my ($self, $sqla) = @_;
+  foreach my $stmt ($sqla->statement_list) {
+    $sqla->wrap_expander($stmt => sub {
+      my ($orig) = @_;
+      sub {
+        my ($self, $name, $args) = @_;
+        my %args = (
+          %$args,
+          (ref($args->{order_by}) eq 'HASH'
+            ? %{$args->{order_by}}
+            : ())
+        );
+        my %overrides;
+        foreach my $clause (map /^!(.*)$/, keys %args) {
+          my $override = delete $args{"!${clause}"};
+          $overrides{$clause} = (
+            ref($override) eq 'CODE'
+              ? $self->$override($args{$clause})
+              : $override
+          );
+        }
+        $self->$orig($name, { %$args, %overrides });
+      }
+    });
+  }
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+SQL::Abstract::Plugin::BangOverrides
+
+=head2 SYNOPSIS
+
+  $sqla->plugin('+BangOverrides');
+  ...
+  profit();
+
+=head1 METHODS
+
+=head2 register_extensions
+
+Wraps all currently existing clause based statements such that when a clause
+of '!name' is encountered, if its value is a coderef, it's called with the
+original value of the 'name' clause and expected to return a replacement, and
+if not, it's simply used as a direct replacement.
+
+This allows for passing data through existing systems that attempt to have
+their own handling for thing but whose capabilities are now superceded by
+L<SQL::Abstract>, and is primarily useful to provide access to experimental
+feature bundles such as L<SQL::Abstract::Plugin::ExtraClauses>.
+
+As an example of such a thing, given an appropriate DBIC setup
+(see C<examples/bangdbic.pl>):
+
+  $s->storage->sqlmaker->plugin('+ExtraClauses')->plugin('+BangOverrides');
+
+  my $rs2 = $s->resultset('Foo')->search({
+    -op => [ '=', { -ident => 'outer.y' }, { -ident => 'me.x' } ]
+  });
+  # (SELECT me.x, me.y, me.z FROM foo me WHERE ( outer.y = me.x ))
+
+  my $rs3 = $rs2->search({}, {
+    '!from' => sub { my ($sqla, $from) = @_;
+      my $base = $sqla->expand_expr({ -old_from => $from });
+      return [ $base, -join => [ 'wub', on => [ 'me.z' => 'wub.z' ] ] ];
+    }
+  });
+  # (SELECT me.x, me.y, me.z FROM foo me JOIN wub ON me.z = wub.z WHERE ( outer.y = me.x ))
+
+  my $rs4 = $rs3->search({}, {
+    '!with' => [ [ qw(wub x y z) ], $s->resultset('Bar')->as_query ],
+  });
+  # (WITH wub(x, y, z) AS (SELECT me.a, me.b, me.c FROM bar me) SELECT me.x, me.y, me.z FROM foo me JOIN wub ON me.z = wub.z WHERE ( outer.y = me.x ))
+
+  my $rs5 = $rs->search({}, { select => [ { -coalesce => [ { -ident => 'x' }, { -value => 7 } ] } ] });
+  # (SELECT -COALESCE( -IDENT( x ), -VALUE( 7 ) ) FROM foo me WHERE ( z = ? ))
+
+  my $rs6 = $rs->search({}, { '!select' => [ { -coalesce => [ { -ident => 'x' }, { -value => 7 } ] } ] });
+  # (SELECT COALESCE(x, ?) FROM foo me WHERE ( z = ? ))
+
+=cut
diff --git a/lib/SQL/Abstract/Plugin/ExtraClauses.pm b/lib/SQL/Abstract/Plugin/ExtraClauses.pm
new file mode 100644 (file)
index 0000000..79912bf
--- /dev/null
@@ -0,0 +1,944 @@
+package SQL::Abstract::Plugin::ExtraClauses;
+
+use Moo;
+
+with 'SQL::Abstract::Role::Plugin';
+
+sub register_extensions {
+  my ($self, $sqla) = @_;
+
+  my @clauses = $sqla->clauses_of('select');
+  my @before_setop;
+  CLAUSE: foreach my $idx (0..$#clauses) {
+    if ($clauses[$idx] eq 'order_by') {
+      @before_setop = @clauses[0..$idx-1];
+      splice(@clauses, $idx, 0, qw(setop group_by having));
+      last CLAUSE;
+    }
+  }
+
+  die "Huh?" unless @before_setop;
+  $sqla->clauses_of(select => @clauses);
+
+  $sqla->clauses_of(update => sub {
+    my ($self, @clauses) = @_;
+    splice(@clauses, 2, 0, 'from');
+    @clauses;
+  });
+
+  $sqla->clauses_of(delete => sub {
+    my ($self, @clauses) = @_;
+    splice(@clauses, 1, 0, 'using');
+    @clauses;
+  });
+
+  $self->register(
+    (map +(
+      "${_}er" => [
+        do {
+          my $x = $_;
+          (map +($_ => "_${x}_${_}"), qw(join from_list alias))
+        }
+       ]
+    ), qw(expand render)),
+    binop_expander => [ as => '_expand_op_as' ],
+    renderer => [ as => '_render_as' ],
+    expander => [ cast => '_expand_cast' ],
+    clause_expanders => [
+      'select.group_by'
+        => sub { $_[0]->expand_expr({ -list => $_[2] }, -ident) },
+      'select.having'
+        => sub { $_[0]->expand_expr($_[2]) },
+      'update.from' => '_expand_from_list',
+      "update.target", '_expand_update_clause_target',
+      "update.update", '_expand_update_clause_target',
+      'delete.using' => '_expand_from_list',
+      'insert.rowvalues' => sub {
+        +(from => $_[0]->expand_expr({ -values => $_[2] }));
+      },
+      'insert.select' => sub {
+        +(from => $_[0]->expand_expr({ -select => $_[2] }));
+      },
+    ],
+  );
+
+  $sqla->expander(old_from => $sqla->clause_expander('select.from'));
+  $sqla->wrap_clause_expander('select.from', sub {
+    my ($orig) = @_;
+    sub {
+      my ($sqla, undef, $args) = @_;
+      if (ref($args) eq 'HASH') {
+        return $self->_expand_from_list(undef, $args);
+      }
+      if (
+        ref($args) eq 'ARRAY'
+        and grep { !ref($_) and $_ =~ /^-/ } @$args
+      ) {
+        return $self->_expand_from_list(undef, $args);
+      }
+      return $sqla->$orig(undef, $args);
+    }
+  });
+
+  # set ops
+  $sqla->wrap_expander(select => sub {
+    $self->cb('_expand_select', $_[0], \@before_setop);
+  });
+
+  $self->register(
+    clause_renderer => [
+      'select.setop' => sub { $_[0]->render_aqt($_[2]) }
+    ],
+    expander => [
+      map +($_ => '_expand_setop', "${_}_all" => '_expand_setop'), qw(union intersect except) ],
+    renderer => [ map +($_ => '_render_setop'), qw(union intersect except) ],
+  );
+
+  my $setop_expander = $self->cb('_expand_clause_setop');
+
+  $sqla->clause_expanders(
+    map +($_ => $setop_expander),
+      map "select.${_}",
+        map +($_, "${_}_all", "${_}_distinct"),
+          qw(union intersect except)
+  );
+
+  foreach my $stmt (qw(select insert update delete)) {
+    $sqla->clauses_of($stmt => 'with', $sqla->clauses_of($stmt));
+    $self->register(
+      clause_expanders => [
+        "${stmt}.with" => '_expand_with',
+        "${stmt}.with_recursive" => '_expand_with',
+      ],
+      clause_renderer => [ "${stmt}.with" => '_render_with' ],
+    );
+  }
+
+  return $sqla;
+}
+
+sub _expand_select {
+  my ($self, $orig, $before_setop, @args) = @_;
+  my $exp = $self->sqla->$orig(@args);
+  return $exp unless my $setop = (my $sel = $exp->{-select})->{setop};
+  if (my @keys = grep $sel->{$_}, @$before_setop) {
+    my %inner; @inner{@keys} = delete @{$sel}{@keys};
+    unshift @{(values(%$setop))[0]{queries}},
+      { -select => \%inner };
+  }
+  return $exp;
+}
+
+sub _expand_from_list {
+  my ($self, undef, $args) = @_;
+  if (ref($args) eq 'HASH') {
+    return $args if $args->{-from_list};
+    return { -from_list => [ $self->expand_expr($args) ] };
+  }
+  my @list;
+  my @args = ref($args) eq 'ARRAY' ? @$args : ($args);
+  while (my $entry = shift @args) {
+    if (!ref($entry) and $entry =~ /^-(.*)/) {
+      if ($1 eq 'as') {
+        $list[-1] = $self->expand_expr({ -as => [
+          $list[-1], map +(ref($_) eq 'ARRAY' ? @$_ : $_), shift(@args)
+        ]});
+        next;
+      }
+      $entry = { $entry => shift @args };
+    }
+    my $aqt = $self->expand_expr($entry, -ident);
+    if ($aqt->{-join} and not $aqt->{-join}{from}) {
+      $aqt->{-join}{from} = pop @list;
+    }
+    push @list, $aqt;
+  }
+  return $list[0] if @list == 1;
+  return { -from_list => \@list };
+}
+
+sub _expand_join {
+  my ($self, undef, $args) = @_;
+  my %proto = (
+    ref($args) eq 'HASH'
+      ? %$args
+      : (to => @$args)
+  );
+  if (my $as = delete $proto{as}) {
+    $proto{to} = $self->expand_expr(
+                   { -as => [ { -from_list => $proto{to} }, $as ] }
+                 );
+  }
+  if (defined($proto{using}) and ref(my $using = $proto{using}) ne 'HASH') {
+    $proto{using} = [
+      map [ $self->expand_expr($_, -ident) ],
+        ref($using) eq 'ARRAY' ? @$using: $using
+    ];
+  }
+  my %ret = (
+    type => delete $proto{type},
+    to => $self->expand_expr({ -from_list => delete $proto{to} }, -ident)
+  );
+  %ret = (%ret,
+    map +($_ => $self->expand_expr($proto{$_}, -ident)),
+      sort keys %proto
+  );
+  return +{ -join => \%ret };
+}
+
+sub _render_from_list {
+  my ($self, undef, $list) = @_;
+  return $self->join_query_parts(', ', @$list);
+}
+
+sub _render_join {
+  my ($self, undef, $args) = @_;
+
+  my @parts = (
+    $args->{from},
+    { -keyword => join '_', ($args->{type}||()), 'join' },
+    (map +($_->{-ident} || $_->{-as}
+      ? $_
+      : ('(', $self->render_aqt($_, 1), ')')),
+        map +(@{$_->{-from_list}||[]} == 1 ? $_->{-from_list}[0] : $_),
+          $args->{to}
+    ),
+    ($args->{on} ? (
+      { -keyword => 'on' },
+      $args->{on},
+    ) : ()),
+    ($args->{using} ? (
+      { -keyword => 'using' },
+      '(', $args->{using}, ')',
+    ) : ()),
+  );
+  return $self->join_query_parts(' ', @parts);
+}
+
+sub _expand_op_as {
+  my ($self, undef, $vv, $k) = @_;
+  my @vv = (ref($vv) eq 'ARRAY' ? @$vv : $vv);
+  my $ik = $self->expand_expr($k, -ident);
+  return +{ -as => [ $ik, $self->expand_expr($vv[0], -ident) ] }
+    if @vv == 1 and ref($vv[0]) eq 'HASH';
+
+  my @as = map $self->expand_expr($_, -ident), @vv;
+  return { -as => [ $ik, $self->expand_expr({ -alias => \@as }) ] };
+}
+
+sub _render_as {
+  my ($self, undef, $args) = @_;
+  my ($thing, $alias) = @$args;
+  return $self->join_query_parts(
+    ' ',
+    $thing,
+    { -keyword => 'as' },
+    $alias,
+  );
+}
+
+sub _render_alias {
+  my ($self, undef, $args) = @_;
+  my ($as, @cols) = @$args;
+  return (@cols
+    ? $self->join_query_parts('',
+         $as,
+         '(',
+         $self->join_query_parts(
+           ', ',
+           @cols
+         ),
+         ')',
+      )
+    : $self->render_aqt($as)
+  );
+}
+
+sub _expand_update_clause_target {
+  my ($self, undef, $target) = @_;
+  +(target => $self->_expand_from_list(undef, $target));
+}
+
+sub _expand_cast {
+  my ($self, undef, $thing) = @_;
+  return { -func => [ cast => $thing ] } if ref($thing) eq 'HASH';
+  my ($cast, $to) = @{$thing};
+  +{ -func => [ cast => { -as => [
+    $self->expand_expr($cast),
+    $self->expand_expr($to, -ident),
+  ] } ] };
+}
+
+sub _expand_alias {
+  my ($self, undef, $args) = @_;
+  if (ref($args) eq 'HASH' and my $alias = $args->{-alias}) {
+    $args = $alias;
+  }
+  my @parts = map $self->expand_expr($_, -ident),
+                ref($args) eq 'ARRAY' ? @{$args} : $args;
+  return $parts[0] if @parts == 1;
+  return { -alias => \@parts };
+}
+
+sub _expand_with {
+  my ($self, $name, $with) = @_;
+  my (undef, $type) = split '_', $name;
+  if (ref($with) eq 'HASH') {
+    return +{
+      %$with,
+      queries => [
+        map +[
+          $self->expand_expr({ -alias => $_->[0] }, -ident),
+          $self->expand_expr($_->[1]),
+        ], @{$with->{queries}}
+      ]
+    }
+  }
+  my @with = @$with;
+  my @exp;
+  while (my ($alias, $query) = splice @with, 0, 2) {
+    push @exp, [
+      $self->expand_expr({ -alias => $alias }, -ident),
+      $self->expand_expr($query)
+    ];
+  }
+  return +(with => { ($type ? (type => $type) : ()), queries => \@exp });
+}
+
+sub _render_with {
+  my ($self, undef, $with) = @_;
+  my $q_part = $self->join_query_parts(', ',
+    map {
+      my ($alias, $query) = @$_;
+      $self->join_query_parts(' ',
+          $alias,
+          { -keyword => 'as' },
+          $query,
+      )
+    } @{$with->{queries}}
+  );
+  return $self->join_query_parts(' ',
+    { -keyword => join '_', 'with', ($with->{type}||'') },
+    $q_part,
+  );
+}
+
+sub _expand_setop {
+  my ($self, $setop, $args) = @_;
+  my $is_all = $setop =~ s/_all$//;
+  +{ "-${setop}" => {
+       ($is_all ? (type => 'all') : ()),
+       (ref($args) eq 'ARRAY'
+          ? (queries => [ map $self->expand_expr($_), @$args ])
+          : (
+              %$args,
+              queries => [ map $self->expand_expr($_), @{$args->{queries}} ]
+            )
+       ),
+  } };
+}
+
+sub _render_setop {
+  my ($self, $setop, $args) = @_;
+  $self->join_query_parts(
+    { -keyword => ' '.join('_', $setop, ($args->{type}||())).' ' },
+    @{$args->{queries}}
+  );
+}
+
+sub _expand_clause_setop {
+  my ($self, $setop, $args) = @_;
+  my ($op, $type) = split '_', $setop;
+  +(setop => $self->expand_expr({
+    "-${op}" => {
+      ($type ? (type => $type) : ()),
+      queries => (ref($args) eq 'ARRAY' ? $args : [ $args ])
+    }
+  }));
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+SQL::Abstract::ExtraClauses - new/experimental additions to L<SQL::Abstract>
+
+=head1 SYNOPSIS
+
+  my $sqla = SQL::Abstract->new;
+  SQL::Abstract::ExtraClauses->apply_to($sqla);
+
+=head1 WARNING
+
+This module is basically a nursery for things that seem like a good idea
+to live in until we figure out if we were right about that.
+
+=head1 METHODS
+
+=head2 apply_to
+
+Applies the plugin to an L<SQL::Abstract> object.
+
+=head2 register_extensions
+
+Registers the extensions described below
+
+=head2 cb
+
+For plugin authors, creates a callback to call a method on the plugin.
+
+=head2 register
+
+For plugin authors, registers callbacks more easily.
+
+=head2 sqla
+
+Available only during plugin callback executions, contains the currently
+active L<SQL::Abstract> object.
+
+=head1 NODE TYPES
+
+=head2 alias
+
+Represents a table alias. Expands name and column names with ident as default.
+
+  # expr
+  { -alias => [ 't', 'x', 'y', 'z' ] }
+
+  # aqt
+  { -alias => [
+      { -ident => [ 't' ] }, { -ident => [ 'x' ] },
+      { -ident => [ 'y' ] }, { -ident => [ 'z' ] },
+  ] }
+
+  # query
+  t(x, y, z)
+  []
+
+=head2 as
+
+Represents an sql AS. LHS is expanded with ident as default, RHS is treated
+as a list of arguments for the alias node.
+
+  # expr
+  { foo => { -as => 'bar' } }
+
+  # aqt
+  { -as => [ { -ident => [ 'foo' ] }, { -ident => [ 'bar' ] } ] }
+
+  # query
+  foo AS bar
+  []
+
+  # expr
+  { -as => [ { -select => { _ => 'blah' } }, 't', 'blah' ] }
+
+  # aqt
+  { -as => [
+      { -select =>
+          { select => { -op => [ ',', { -ident => [ 'blah' ] } ] } }
+      },
+      { -alias => [ { -ident => [ 't' ] }, { -ident => [ 'blah' ] } ] },
+  ] }
+
+  # query
+  (SELECT blah) AS t(blah)
+  []
+
+=head2 cast
+
+  # expr
+  { -cast => [ { -ident => 'birthday' }, 'date' ] }
+
+  # aqt
+  { -func => [
+      'cast', {
+        -as => [ { -ident => [ 'birthday' ] }, { -ident => [ 'date' ] } ]
+      },
+  ] }
+
+  # query
+  CAST(birthday AS date)
+  []
+
+=head2 join
+
+If given an arrayref, pretends it was given a hashref with the first
+element of the arrayref as the value for 'to' and the remaining pairs copied.
+
+Given a hashref, the 'as' key is if presented expanded to wrap the 'to'.
+
+If present the 'using' key is expanded as a list of idents.
+
+Known keys are: 'from' (the left hand side), 'type' ('left', 'right', or
+nothing), 'to' (the right hand side), 'on' and 'using'.
+
+  # expr
+  { -join => {
+      from => 'lft',
+      on => { 'lft.bloo' => { '>' => 'rgt.blee' } },
+      to => 'rgt',
+      type => 'left',
+  } }
+
+  # aqt
+  { -join => {
+      from => { -ident => [ 'lft' ] },
+      on => { -op => [
+          '>', { -ident => [ 'lft', 'bloo' ] },
+          { -ident => [ 'rgt', 'blee' ] },
+      ] },
+      to => { -ident => [ 'rgt' ] },
+      type => 'left',
+  } }
+
+  # query
+  lft LEFT JOIN rgt ON lft.bloo > rgt.blee
+  []
+
+=head2 from_list
+
+List of components of the FROM clause; -foo type elements indicate a pair
+with the next element; this is easiest if I show you:
+
+  # expr
+  { -from_list => [
+      't1', -as => 'table_one', -join =>
+      [ 't2', 'on', { 'table_one.x' => 't2.x' } ],
+  ] }
+
+  # aqt
+  { -join => {
+      from =>
+        {
+          -as => [ { -ident => [ 't1' ] }, { -ident => [ 'table_one' ] } ]
+        },
+      on => { -op => [
+          '=', { -ident => [ 'table_one', 'x' ] },
+          { -ident => [ 't2', 'x' ] },
+      ] },
+      to => { -ident => [ 't2' ] },
+      type => undef,
+  } }
+
+  # query
+  t1 AS table_one JOIN t2 ON table_one.x = t2.x
+  []
+
+Or with using:
+
+  # expr
+  { -from_list =>
+      [ 't1', -as => 'table_one', -join => [ 't2', 'using', [ 'x' ] ] ]
+  }
+
+  # aqt
+  { -join => {
+      from =>
+        {
+          -as => [ { -ident => [ 't1' ] }, { -ident => [ 'table_one' ] } ]
+        },
+      to => { -ident => [ 't2' ] },
+      type => undef,
+      using =>
+        { -op => [ 'or', { -op => [ 'or', { -ident => [ 'x' ] } ] } ] },
+  } }
+
+  # query
+  t1 AS table_one JOIN t2 USING ( x )
+  []
+
+With oddities:
+
+  # expr
+  { -from_list => [
+      'x', -join =>
+      [ [ 'y', -join => [ 'z', 'type', 'left' ] ], 'type', 'left' ],
+  ] }
+
+  # aqt
+  { -join => {
+      from => { -ident => [ 'x' ] },
+      to => { -join => {
+          from => { -ident => [ 'y' ] },
+          to => { -ident => [ 'z' ] },
+          type => 'left',
+      } },
+      type => 'left',
+  } }
+
+  # query
+  x LEFT JOIN ( y LEFT JOIN z )
+  []
+
+=head2 setops
+
+Expanders are provided for union, union_all, intersect, intersect_all,
+except and except_all, and each takes an arrayref of queries:
+
+  # expr
+  { -union => [
+      { -select => { _ => { -value => 1 } } },
+      { -select => { _ => { -value => 2 } } },
+  ] }
+
+  # aqt
+  { -union => { queries => [
+        { -select =>
+            { select => { -op => [ ',', { -bind => [ undef, 1 ] } ] } }
+        },
+        { -select =>
+            { select => { -op => [ ',', { -bind => [ undef, 2 ] } ] } }
+        },
+  ] } }
+
+  # query
+  (SELECT ?) UNION (SELECT ?)
+  [ 1, 2 ]
+
+  # expr
+  { -union_all => [
+      { -select => { _ => { -value => 1 } } },
+      { -select => { _ => { -value => 2 } } },
+      { -select => { _ => { -value => 1 } } },
+  ] }
+
+  # aqt
+  { -union => {
+      queries => [
+        { -select =>
+            { select => { -op => [ ',', { -bind => [ undef, 1 ] } ] } }
+        },
+        { -select =>
+            { select => { -op => [ ',', { -bind => [ undef, 2 ] } ] } }
+        },
+        { -select =>
+            { select => { -op => [ ',', { -bind => [ undef, 1 ] } ] } }
+        },
+      ],
+      type => 'all',
+  } }
+
+  # query
+  (SELECT ?) UNION ALL (SELECT ?) UNION ALL (SELECT ?)
+  [ 1, 2, 1 ]
+
+=head1 STATEMENT EXTENSIONS
+
+=head2 group by clause for select
+
+Expanded as a list with an ident default:
+
+  # expr
+  { -select => { group_by => [ 'foo', 'bar' ] } }
+
+  # aqt
+  { -select => { group_by =>
+        {
+          -op => [ ',', { -ident => [ 'foo' ] }, { -ident => [ 'bar' ] } ]
+        }
+  } }
+
+  # query
+  GROUP BY foo, bar
+  []
+
+=head2 having clause for select
+
+Basic expr, just like where, given having is pretty much post-group-by
+where clause:
+
+  # expr
+  { -select =>
+      { having => { '>' => [ { -count => { -ident => 'foo' } }, 3 ] } }
+  }
+
+  # aqt
+  { -select => { having => { -op => [
+          '>', { -func => [ 'count', { -ident => [ 'foo' ] } ] },
+          { -bind => [ undef, 3 ] },
+  ] } } }
+
+  # query
+  HAVING COUNT(foo) > ?
+  [ 3 ]
+
+=head2 setop clauses
+
+If a select query contains a clause matching any of the setop node types,
+clauses that appear before the setop would in the resulting query are
+gathered together and moved into an inner select node:
+
+  # expr
+  { -select => {
+      _ => '*',
+      from => 'foo',
+      order_by => 'baz',
+      union =>
+        {
+          -select => { _ => '*', from => 'bar', where => { thing => 1 } }
+        },
+      where => { thing => 1 },
+  } }
+
+  # aqt
+  { -select => {
+      order_by => { -op => [ ',', { -ident => [ 'baz' ] } ] },
+      setop => { -union => { queries => [
+            { -select => {
+                from => { -ident => [ 'foo' ] },
+                select => { -op => [ ',', { -ident => [ '*' ] } ] },
+                where => { -op => [
+                    '=', { -ident => [ 'thing' ] },
+                    { -bind => [ 'thing', 1 ] },
+                ] },
+            } },     ] },
+            { -select => {
+                from => { -ident => [ 'bar' ] },
+                select => { -op => [ ',', { -ident => [ '*' ] } ] },
+                where => { -op => [
+                    '=', { -ident => [ 'thing' ] },
+                    { -bind => [ 'thing', 1 ] },
+            } },
+      ] } },
+  } }
+
+  # query
+  (SELECT * FROM foo WHERE thing = ?) UNION (
+    SELECT * FROM bar WHERE thing = ?
+  )
+  ORDER BY baz
+  [ 1, 1 ]
+
+=head2 update from clause
+
+Some databases allow an additional FROM clause to reference other tables
+for the data to update; this clause is expanded as a normal from list, check
+your database for what is and isn't allowed in practice.
+
+  # expr
+  { -update => {
+      _ => 'employees',
+      from => 'accounts',
+      set => { sales_count => { sales_count => { '+' => \1 } } },
+      where => {
+        'accounts.name' => { '=' => \"'Acme Corporation'" },
+        'employees.id' => { -ident => 'accounts.sales_person' },
+      },
+  } }
+
+  # aqt
+  { -update => {
+      from => { -ident => [ 'accounts' ] },
+      set => { -op => [
+          ',', { -op => [
+              '=', { -ident => [ 'sales_count' ] }, { -op => [
+                  '+', { -ident => [ 'sales_count' ] },
+                  { -literal => [ 1 ] },
+              ] },
+          ] },
+      ] },
+      target => { -ident => [ 'employees' ] },
+      where => { -op => [
+          'and', { -op => [
+              '=', { -ident => [ 'accounts', 'name' ] },
+              { -literal => [ "'Acme Corporation'" ] },
+          ] }, { -op => [
+              '=', { -ident => [ 'employees', 'id' ] },
+              { -ident => [ 'accounts', 'sales_person' ] },
+          ] },
+      ] },
+  } }
+
+  # query
+  UPDATE employees SET sales_count = sales_count + 1 FROM accounts
+  WHERE (
+    accounts.name = 'Acme Corporation'
+    AND employees.id = accounts.sales_person
+  )
+  []
+
+=head2 delete using clause
+
+Some databases allow an additional USING clause to reference other tables
+for the data to update; this clause is expanded as a normal from list, check
+your database for what is and isn't allowed in practice.
+
+  # expr
+  { -delete => {
+      from => 'x',
+      using => 'y',
+      where => { 'x.id' => { -ident => 'y.x_id' } },
+  } }
+
+  # aqt
+  { -delete => {
+      target => { -op => [ ',', { -ident => [ 'x' ] } ] },
+      using => { -ident => [ 'y' ] },
+      where => { -op => [
+          '=', { -ident => [ 'x', 'id' ] },
+          { -ident => [ 'y', 'x_id' ] },
+      ] },
+  } }
+
+  # query
+  DELETE FROM x USING y WHERE x.id = y.x_id
+  []
+
+=head2 insert rowvalues and select clauses
+
+rowvalues and select are shorthand for
+
+  { from => { -select ... } }
+
+and
+
+  { from => { -values ... } }
+
+respectively:
+
+  # expr
+  { -insert =>
+      { into => 'numbers', rowvalues => [ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] ] }
+  }
+
+  # aqt
+  { -insert => {
+      from => { -values => [
+          { -row =>
+              [ { -bind => [ undef, 1 ] }, { -bind => [ undef, 2 ] } ]
+          },
+          { -row =>
+              [ { -bind => [ undef, 3 ] }, { -bind => [ undef, 4 ] } ]
+          },
+          { -row =>
+              [ { -bind => [ undef, 5 ] }, { -bind => [ undef, 6 ] } ]
+          },
+      ] },
+      target => { -ident => [ 'numbers' ] },
+  } }
+
+  # query
+  INSERT INTO numbers VALUES (?, ?), (?, ?), (?, ?)
+  [ 1, 2, 3, 4, 5, 6 ]
+
+  # expr
+  { -insert =>
+      { into => 'numbers', select => { _ => '*', from => 'old_numbers' } }
+  }
+
+  # aqt
+  { -insert => {
+      from => { -select => {
+          from => { -ident => [ 'old_numbers' ] },
+          select => { -op => [ ',', { -ident => [ '*' ] } ] },
+      } },
+      target => { -ident => [ 'numbers' ] },
+  } }
+
+  # query
+  INSERT INTO numbers SELECT * FROM old_numbers
+  []
+
+=head2 with and with_recursive clauses
+
+These clauses are available on select/insert/update/delete queries; check
+your database for applicability (e.g. mysql supports all four but mariadb
+only select).
+
+The value should be an arrayref of name/query pairs:
+
+  # expr
+  { -select => {
+      from => 'foo',
+      select => '*',
+      with => [ 'foo', { -select => { select => \1 } } ],
+  } }
+
+  # aqt
+  { -select => {
+      from => { -ident => [ 'foo' ] },
+      select => { -op => [ ',', { -ident => [ '*' ] } ] },
+      with => { queries => [ [
+            { -ident => [ 'foo' ] }, { -select =>
+                { select => { -op => [ ',', { -literal => [ 1 ] } ] } }
+            },
+      ] ] },
+  } }
+
+  # query
+  WITH foo AS (SELECT 1) SELECT * FROM foo
+  []
+
+A more complete example (designed for mariadb, (ab)using the fact that
+mysqloids materialise subselects in FROM into an unindexed temp table to
+circumvent the restriction that you can't select from the table you're
+currently updating:
+
+  # expr
+  { -update => {
+      _ => [
+        'tree_table', -join => {
+          as => 'tree',
+          on => { 'tree.id' => 'tree_with_path.id' },
+          to => { -select => {
+              from => 'tree_with_path',
+              select => '*',
+              with_recursive => [
+                [ 'tree_with_path', 'id', 'parent_id', 'path' ],
+                { -select => {
+                    _ => [
+                      'id', 'parent_id', { -as => [
+                          { -cast => { -as => [ 'id', 'char', 255 ] } },
+                          'path',
+                      ] } ],
+                    from => 'tree_table',
+                    union_all => { -select => {
+                        _ => [
+                          't.id', 't.parent_id', { -as => [
+                              { -concat => [ 'r.path', \"'/'", 't.id' ] },
+                              'path',
+                          ] },
+                        ],
+                        from => [
+                          'tree_table', -as => 't', -join => {
+                            as => 'r',
+                            on => { 't.parent_id' => 'r.id' },
+                            to => 'tree_with_path',
+                          },
+                        ],
+                    } },
+                    where => { parent_id => undef },
+                } },
+              ],
+          } },
+        },
+      ],
+      set => { path => { -ident => [ 'tree', 'path' ] } },
+  } }
+
+  # query
+  UPDATE
+    tree_table JOIN
+    (
+      WITH RECURSIVE
+        tree_with_path(id, parent_id, path) AS (
+          (
+            SELECT id, parent_id, CAST(id AS char(255)) AS path
+            FROM tree_table WHERE parent_id IS NULL
+          ) UNION ALL (
+            SELECT t.id, t.parent_id, CONCAT(r.path, '/', t.id) AS path
+            FROM
+              tree_table AS t JOIN tree_with_path AS r ON
+              t.parent_id = r.id
+          )
+        )
+      SELECT * FROM tree_with_path
+    ) AS tree
+    ON tree.id = tree_with_path.id
+  SET path = tree.path
+  []
+
+=cut
diff --git a/lib/SQL/Abstract/Reference.pm b/lib/SQL/Abstract/Reference.pm
new file mode 100644 (file)
index 0000000..257fd73
--- /dev/null
@@ -0,0 +1,1151 @@
+package SQL::Abstract::Reference;
+
+1;
+
+__END__
+=head1 NAME
+
+SQL::Abstract::Reference - Reference documentation for L<SQL::Abstract>
+
+=head1 TERMS
+
+=head2 Expression (expr)
+
+The DWIM structure that's passed to most methods by default is referred to
+as expression syntax. If you see a variable with C<expr> in the name, or a
+comment before a code block saying C<# expr>, this is what's being described.
+
+=head2 Abstract Query Tree (aqt)
+
+The explicit structure that an expression is converted into before it's
+rendered into SQL is referred to as an abstract query tree. If you see a
+variable with C<aqt> in the name, or a comment before a code block saying
+C<# aqt>, this is what's being described.
+
+=head2 SQL and Bind Values (query)
+
+The final result of L<SQL::Abstract> rendering is generally an SQL statement
+plus bind values for passing to DBI, ala:
+
+  my ($sql, @bind) = $sqla->some_method(@args);
+  my @hashes = @{$dbh->do($sql, { Slice => {} }, @bind)};
+
+If you see a comment before a code block saying C<# query>, the SQL + bind
+array is what's being described.
+
+=head2 Expander
+
+An expander subroutine is written as:
+
+  sub {
+    my ($sqla, $name, $value, $k) = @_;
+    ...
+    return $aqt;
+  }
+
+$name is the expr node type for node expanders, the op name for op
+expanders, and the clause name for clause expanders.
+
+$value is the body of the thing being expanded
+
+If an op expander is being called as the binary operator in a L</hashtriple>
+expression, $k will be the hash key to be used as the left hand side
+identifier.
+
+This can trivially be converted to an C<ident> type AQT node with:
+
+  my $ident = $sqla->expand_expr({ -ident => $k });
+
+=head2 Renderer
+
+A renderer subroutine looks like:
+
+  sub {
+    my ($sqla, $type, $value) = @_;
+    ...
+    $sqla->join_query_parts($join, @parts);
+  }
+
+and can be registered on a per-type, per-op or per-clause basis.
+
+=head1 AQT node types
+
+An AQT node consists of a hashref with a single key, whose name is C<-type>
+where 'type' is the node type, and whose value is the data for the node.
+
+The following is an explanation of the built-in AQT type renderers;
+additional renderers can be registered as part of the extension system.
+
+=head2 literal
+
+  # expr
+  { -literal => [ 'SPANG(?, ?)', 1, 27 ] }
+
+  # query
+  SPANG(?, ?)
+  [ 1, 27 ]
+
+=head2 ident
+
+  # expr
+  { -ident => 'foo' }
+
+  # query
+  foo
+  []
+
+  # expr
+  { -ident => [ 'foo', 'bar' ] }
+
+  # query
+  foo.bar
+  []
+
+=head2 bind
+
+  # expr
+  { -bind => [ 'colname', 'value' ] }
+
+  # query
+  ?
+  [ 'value' ]
+
+=head2 row
+
+  # expr
+  {
+    -row => [ { -bind => [ 'r', 1 ] }, { -ident => [ 'clown', 'car' ] } ]
+  }
+
+  # query
+  (?, clown.car)
+  [ 1 ]
+
+=head2 func
+
+  # expr
+  {
+    -func => [ 'foo', { -ident => [ 'bar' ] }, { -bind => [ undef, 7 ] } ]
+  }
+
+  # query
+  FOO(bar, ?)
+  [ 7 ]
+
+=head2 op
+
+Standard binop:
+
+  # expr
+  { -op => [
+      '=', { -ident => [ 'bomb', 'status' ] },
+      { -value => 'unexploded' },
+  ] }
+
+  # query
+  bomb.status = ?
+  [ 'unexploded' ]
+
+
+Prefix unop:
+
+  # expr
+  { -op => [ '-', { -ident => 'foo' } ] }
+
+  # query
+  - foo
+  []
+
+Not as special case parenthesised unop:
+
+  # expr
+  { -op => [ 'not', { -ident => 'explosive' } ] }
+
+  # query
+  (NOT explosive)
+  []
+
+Postfix unop: (is_null, is_not_null, asc, desc)
+
+  # expr
+  { -op => [ 'is_null', { -ident => [ 'bobby' ] } ] }
+
+  # query
+  bobby IS NULL
+  []
+
+AND and OR:
+
+  # expr
+  { -op =>
+      [ 'and', { -ident => 'x' }, { -ident => 'y' }, { -ident => 'z' } ]
+  }
+
+  # query
+  ( x AND y AND z )
+  []
+
+IN (and NOT IN):
+
+  # expr
+  { -op => [
+      'in', { -ident => 'card' }, { -bind => [ 'card', 3 ] },
+      { -bind => [ 'card', 'J' ] },
+  ] }
+
+  # query
+  card IN ( ?, ? )
+  [ 3, 'J' ]
+
+BETWEEN (and NOT BETWEEN):
+
+  # expr
+  { -op => [
+      'between', { -ident => 'pints' }, { -bind => [ 'pints', 2 ] },
+      { -bind => [ 'pints', 4 ] },
+  ] }
+
+  # query
+  ( pints BETWEEN ? AND ? )
+  [ 2, 4 ]
+
+Comma (use -row for parens):
+
+  # expr
+  { -op => [ ',', { -literal => [ 1 ] }, { -literal => [ 2 ] } ] }
+
+  # query
+  1, 2
+  []
+
+=head2 values
+
+  # expr
+  { -values =>
+      { -row => [ { -bind => [ undef, 1 ] }, { -bind => [ undef, 2 ] } ] }
+  }
+
+  # query
+  VALUES (?, ?)
+  [ 1, 2 ]
+
+  # expr
+  { -values => [
+      { -row => [ { -literal => [ 1 ] }, { -literal => [ 2 ] } ] },
+      { -row => [ { -literal => [ 3 ] }, { -literal => [ 4 ] } ] },
+  ] }
+
+  # query
+  VALUES (1, 2), (3, 4)
+  []
+
+=head2 keyword
+
+  # expr
+  { -keyword => 'insert_into' }
+
+  # query
+  INSERT INTO
+  []
+
+=head2 statement types
+
+AQT node types are also provided for C<select>, C<insert>, C<update> and
+C<delete>. These types are handled by the clauses system as discussed later.
+
+=head1 Expressions
+
+=head2 node expr
+
+The simplest expression is just an AQT node:
+
+  # expr
+  { -ident => [ 'foo', 'bar' ] }
+
+  # aqt
+  { -ident => [ 'foo', 'bar' ] }
+
+  # query
+  foo.bar
+  []
+
+However, even in the case of an AQT node, the node value will be expanded if
+an expander has been registered for that node type:
+
+  # expr
+  { -ident => 'foo.bar' }
+
+  # aqt
+  { -ident => [ 'foo', 'bar' ] }
+
+  # query
+  foo.bar
+  []
+
+=head2 identifier hashpair types
+
+=head3 hashtriple
+
+  # expr
+  { id => { op => 'value' } }
+
+  # aqt
+  { -op =>
+      [ 'op', { -ident => [ 'id' ] }, { -bind => [ 'id', 'value' ] } ]
+  }
+
+  # query
+  id OP ?
+  [ 'value' ]
+
+If the value is undef, attempts to convert equality and like ops to IS NULL,
+and inequality and not like to IS NOT NULL:
+
+  # expr
+  { id => { '!=' => undef } }
+
+  # aqt
+  { -op => [ 'is_not_null', { -ident => [ 'id' ] } ] }
+
+  # query
+  id IS NOT NULL
+  []
+
+=head3 identifier hashpair w/simple value
+
+Equivalent to a hashtriple with an op of '='.
+
+  # expr
+  { id => 'value' }
+
+  # aqt
+  {
+    -op => [ '=', { -ident => [ 'id' ] }, { -bind => [ 'id', 'value' ] } ]
+  }
+
+  # query
+  id = ?
+  [ 'value' ]
+
+(an object value will also follow this code path)
+
+=head3 identifier hashpair w/undef RHS
+
+Converted to IS NULL :
+
+  # expr
+  { id => undef }
+
+  # aqt
+  { -op => [ 'is_null', { -ident => [ 'id' ] } ] }
+
+  # query
+  id IS NULL
+  []
+
+(equivalent to the -is operator) :
+
+  # expr
+  { id => { -is => undef } }
+
+  # aqt
+  { -op => [ 'is_null', { -ident => [ 'id' ] } ] }
+
+  # query
+  id IS NULL
+  []
+
+=head3 identifier hashpair w/literal RHS
+
+Directly appended to the key, remember you need to provide an operator:
+
+  # expr
+  { id => \"= dont_try_this_at_home" }
+
+  # aqt
+  { -literal => [ 'id = dont_try_this_at_home' ] }
+
+  # query
+  id = dont_try_this_at_home
+  []
+
+  # expr
+  { id => \[
+        "= seriously(?, ?, ?, ?)",
+        "use",
+        "-ident",
+        "and",
+        "-func",
+      ]
+  }
+
+  # aqt
+  { -literal =>
+      [ 'id = seriously(?, ?, ?, ?)', 'use', -ident => 'and', '-func' ]
+  }
+
+  # query
+  id = seriously(?, ?, ?, ?)
+  [ 'use', -ident => 'and', '-func' ]
+
+(you may absolutely use this when there's no built-in expression type for
+what you need and registering a custom one would be more hassle than it's
+worth, but, y'know, do try and avoid it)
+
+=head3 identifier hashpair w/arrayref value
+
+Becomes equivalent to a -or over an arrayref of hashrefs with the identifier
+as key and the member of the original arrayref as the value:
+
+  # expr
+  { id => [ 3, 4, { '>' => 12 } ] }
+
+  # aqt
+  { -op => [
+      'or',
+      { -op => [ '=', { -ident => [ 'id' ] }, { -bind => [ 'id', 3 ] } ] },
+      { -op => [ '=', { -ident => [ 'id' ] }, { -bind => [ 'id', 4 ] } ] },
+      {
+        -op => [ '>', { -ident => [ 'id' ] }, { -bind => [ 'id', 12 ] } ]
+      },
+  ] }
+
+  # query
+  ( id = ? OR id = ? OR id > ? )
+  [ 3, 4, 12 ]
+
+  # expr
+  { -or => [ { id => 3 }, { id => 4 }, { id => { '>' => 12 } } ] }
+
+  # aqt
+  { -op => [
+      'or',
+      { -op => [ '=', { -ident => [ 'id' ] }, { -bind => [ 'id', 3 ] } ] },
+      { -op => [ '=', { -ident => [ 'id' ] }, { -bind => [ 'id', 4 ] } ] },
+      {
+        -op => [ '>', { -ident => [ 'id' ] }, { -bind => [ 'id', 12 ] } ]
+      },
+  ] }
+
+  # query
+  ( id = ? OR id = ? OR id > ? )
+  [ 3, 4, 12 ]
+
+Special Case: If the first element of the arrayref is -or or -and, that's
+used as the top level logic op:
+
+  # expr
+  { id => [ -and => { '>' => 3 }, { '<' => 6 } ] }
+
+  # aqt
+  { -op => [
+      'and',
+      { -op => [ '>', { -ident => [ 'id' ] }, { -bind => [ 'id', 3 ] } ] },
+      { -op => [ '<', { -ident => [ 'id' ] }, { -bind => [ 'id', 6 ] } ] },
+  ] }
+
+  # query
+  ( id > ? AND id < ? )
+  [ 3, 6 ]
+
+=head3 identifier hashpair w/hashref value
+
+Becomes equivalent to a -and over an arrayref of hashtriples constructed
+with the identifier as the key and each key/value pair of the original
+hashref as the value:
+
+  # expr
+  { id => { '<' => 4, '>' => 3 } }
+
+  # aqt
+  { -op => [
+      'and',
+      { -op => [ '<', { -ident => [ 'id' ] }, { -bind => [ 'id', 4 ] } ] },
+      { -op => [ '>', { -ident => [ 'id' ] }, { -bind => [ 'id', 3 ] } ] },
+  ] }
+
+  # query
+  ( id < ? AND id > ? )
+  [ 4, 3 ]
+
+is sugar for:
+
+  # expr
+  { -and => [ { id => { '<' => 4 } }, { id => { '>' => 3 } } ] }
+
+  # aqt
+  { -op => [
+      'and',
+      { -op => [ '<', { -ident => [ 'id' ] }, { -bind => [ 'id', 4 ] } ] },
+      { -op => [ '>', { -ident => [ 'id' ] }, { -bind => [ 'id', 3 ] } ] },
+  ] }
+
+  # query
+  ( id < ? AND id > ? )
+  [ 4, 3 ]
+
+=head2 operator hashpair types
+
+A hashpair whose key begins with a -, or whose key consists entirely of
+nonword characters (thereby covering '=', '>', pg json ops, etc.) is
+processed as an operator hashpair.
+
+=head3 operator hashpair w/node type
+
+If a node type expander is registered for the key, the hashpair is
+treated as a L</node expr>.
+
+=head3 operator hashpair w/registered op
+
+If an expander is registered for the op name, that's run and the
+result returned:
+
+  # expr
+  { -in => [ 'foo', 1, 2, 3 ] }
+
+  # aqt
+  { -op => [
+      'in', { -ident => [ 'foo' ] }, { -bind => [ undef, 1 ] },
+      { -bind => [ undef, 2 ] }, { -bind => [ undef, 3 ] },
+  ] }
+
+  # query
+  foo IN ( ?, ?, ? )
+  [ 1, 2, 3 ]
+
+=head3 operator hashpair w/not prefix
+
+If the op name starts -not_ this is stripped and turned into a -not
+wrapper around the result:
+
+  # expr
+  { -not_ident => 'foo' }
+
+  # aqt
+  { -op => [ 'not', { -ident => [ 'foo' ] } ] }
+
+  # query
+  (NOT foo)
+  []
+
+is equivalent to:
+
+  # expr
+  { -not => { -ident => 'foo' } }
+
+  # aqt
+  { -op => [ 'not', { -ident => [ 'foo' ] } ] }
+
+  # query
+  (NOT foo)
+  []
+
+=head3 operator hashpair with unknown op
+
+If the C<unknown_unop_always_func> option is set (which is recommended but
+defaults to off for backwards compatibility reasons), an unknown op
+expands into a C<-func> node:
+
+  # expr
+  { -count => { -ident => '*' } }
+
+  # aqt
+  { -func => [ 'count', { -ident => [ '*' ] } ] }
+
+  # query
+  COUNT(*)
+  []
+
+If not, an unknown op will expand into a C<-op> node.
+
+=head2 hashref expr
+
+A hashref with more than one pair becomes a C<-and> over its hashpairs, i.e.
+
+  # expr
+  { x => 1, y => 2 }
+
+  # aqt
+  { -op => [
+      'and',
+      { -op => [ '=', { -ident => [ 'x' ] }, { -bind => [ 'x', 1 ] } ] },
+      { -op => [ '=', { -ident => [ 'y' ] }, { -bind => [ 'y', 2 ] } ] },
+  ] }
+
+  # query
+  ( x = ? AND y = ? )
+  [ 1, 2 ]
+
+is short hand for:
+
+  # expr
+  { -and => [ { x => 1 }, { y => 2 } ] }
+
+  # aqt
+  { -op => [
+      'and',
+      { -op => [ '=', { -ident => [ 'x' ] }, { -bind => [ 'x', 1 ] } ] },
+      { -op => [ '=', { -ident => [ 'y' ] }, { -bind => [ 'y', 2 ] } ] },
+  ] }
+
+  # query
+  ( x = ? AND y = ? )
+  [ 1, 2 ]
+
+=head2 arrayref expr
+
+An arrayref becomes a C<-or> over its contents. Arrayrefs, hashrefs and
+literals are all expanded and added to the clauses of the C<-or>. If the
+arrayref contains a scalar it's treated as the key of a hashpair and the
+next element as the value.
+
+  # expr
+  [ { x => 1 }, [ { y => 2 }, { z => 3 } ], 'key', 'value', \"lit()" ]
+
+  # aqt
+  { -op => [
+      'or',
+      { -op => [ '=', { -ident => [ 'x' ] }, { -bind => [ 'x', 1 ] } ] },
+      { -op => [
+          'or', {
+            -op => [ '=', { -ident => [ 'y' ] }, { -bind => [ 'y', 2 ] } ]
+          }, {
+            -op => [ '=', { -ident => [ 'z' ] }, { -bind => [ 'z', 3 ] } ]
+          },
+      ] }, { -op =>
+          [
+            '=', { -ident => [ 'key' ] },
+            { -bind => [ 'key', 'value' ] },
+          ]
+      },
+      { -literal => [ 'lit()' ] },
+  ] }
+
+  # query
+  ( x = ? OR ( y = ? OR z = ? ) OR key = ? OR lit() )
+  [ 1, 2, 3, 'value' ]
+
+=head1 Default Expanders
+
+=head2 bool
+
+Turns the old -bool syntax into the value expression, i.e.
+
+  # expr
+  { -bool => { -ident => 'foo' } }
+
+  # aqt
+  { -ident => [ 'foo' ] }
+
+  # query
+  foo
+  []
+
+behaves the same way as the now-directly-supported
+
+  # expr
+  { -ident => 'foo' }
+
+  # aqt
+  { -ident => [ 'foo' ] }
+
+  # query
+  foo
+  []
+
+=head2 row
+
+Expands the elements of the value arrayref:
+
+  # expr
+  { -row => [ 1, { -ident => 'foo' }, 2, 3 ] }
+
+  # aqt
+  { -row => [
+      { -bind => [ undef, 1 ] }, { -ident => [ 'foo' ] },
+      { -bind => [ undef, 2 ] }, { -bind => [ undef, 3 ] },
+  ] }
+
+  # query
+  (?, foo, ?, ?)
+  [ 1, 2, 3 ]
+
+=head2 op
+
+If an expander is registered for the op name, delegates to the expander; if
+not, expands the argument values:
+
+  # expr
+  { -op => [ 'ident', 'foo.bar' ] }
+
+  # aqt
+  { -ident => [ 'foo', 'bar' ] }
+
+  # query
+  foo.bar
+  []
+
+  # expr
+  { -op => [ '=', { -ident => 'foo' }, 3 ] }
+
+  # aqt
+  { -op => [ '=', { -ident => [ 'foo' ] }, { -bind => [ undef, 3 ] } ] }
+
+  # query
+  foo = ?
+  [ 3 ]
+
+=head2 func
+
+Expands the argument values:
+
+  # expr
+  { -func => [ 'coalesce', { -ident => 'thing' }, 'fallback' ] }
+
+  # aqt
+  { -func => [
+      'coalesce', { -ident => [ 'thing' ] },
+      { -bind => [ undef, 'fallback' ] },
+  ] }
+
+  # query
+  COALESCE(thing, ?)
+  [ 'fallback' ]
+
+=head2 values
+
+A hashref value is expanded as an expression:
+
+  # expr
+  { -values => { -row => [ 1, 2 ] } }
+
+  # aqt
+  { -values => [
+      { -row => [ { -bind => [ undef, 1 ] }, { -bind => [ undef, 2 ] } ] }
+  ] }
+
+  # query
+  VALUES (?, ?)
+  [ 1, 2 ]
+
+An arrayref value's elements are either expressions or arrayrefs to be
+treated as rows:
+
+  # expr
+  { -values => [ { -row => [ 1, 2 ] }, [ 3, 4 ] ] }
+
+  # aqt
+  { -values => [
+      { -row => [ { -bind => [ undef, 1 ] }, { -bind => [ undef, 2 ] } ] },
+      { -row => [ { -bind => [ undef, 3 ] }, { -bind => [ undef, 4 ] } ] },
+  ] }
+
+  # query
+  VALUES (?, ?), (?, ?)
+  [ 1, 2, 3, 4 ]
+
+=head2 list
+
+Expects a value or an arrayref of values, expands them, and returns just
+the expanded aqt for a single entry or a comma operator for multiple:
+
+  # expr
+  { -list => [ { -ident => 'foo' } ] }
+
+  # aqt
+  { -op => [ ',', { -ident => [ 'foo' ] } ] }
+
+  # query
+  foo
+  []
+
+  # expr
+  { -list => [ { -ident => 'foo' }, { -ident => 'bar' } ] }
+
+  # aqt
+  { -op => [ ',', { -ident => [ 'foo' ] }, { -ident => [ 'bar' ] } ] }
+
+  # query
+  foo, bar
+  []
+
+=head2 between op
+
+The RHS of between must either be a pair of exprs/plain values, or a single
+literal expr:
+
+  # expr
+  { -between => [ 'size', 3, { -ident => 'max_size' } ] }
+
+  # aqt
+  { -op => [
+      'between', { -ident => [ 'size' ] }, { -bind => [ undef, 3 ] },
+      { -ident => [ 'max_size' ] },
+  ] }
+
+  # query
+  ( size BETWEEN ? AND max_size )
+  [ 3 ]
+
+  # expr
+  { size => { -between => [ 3, { -ident => 'max_size' } ] } }
+
+  # aqt
+  { -op => [
+      'between', { -ident => [ 'size' ] }, { -bind => [ 'size', 3 ] },
+      { -ident => [ 'max_size' ] },
+  ] }
+
+  # query
+  ( size BETWEEN ? AND max_size )
+  [ 3 ]
+
+  # expr
+  { size => { -between => \"3 AND 7" } }
+
+  # aqt
+  { -op =>
+      [
+        'between', { -ident => [ 'size' ] },
+        { -literal => [ '3 AND 7' ] },
+      ]
+  }
+
+  # query
+  ( size BETWEEN 3 AND 7 )
+  []
+
+not_between is also expanded:
+
+  # expr
+  { size => { -not_between => [ 3, 7 ] } }
+
+  # aqt
+  { -op => [
+      'not_between', { -ident => [ 'size' ] },
+      { -bind => [ 'size', 3 ] }, { -bind => [ 'size', 7 ] },
+  ] }
+
+  # query
+  ( size NOT BETWEEN ? AND ? )
+  [ 3, 7 ]
+
+=head2 in op
+
+The RHS of in/not_in is either an expr/value or an arrayref of
+exprs/values:
+
+  # expr
+  { foo => { -in => [ 1, 2 ] } }
+
+  # aqt
+  { -op => [
+      'in', { -ident => [ 'foo' ] }, { -bind => [ 'foo', 1 ] },
+      { -bind => [ 'foo', 2 ] },
+  ] }
+
+  # query
+  foo IN ( ?, ? )
+  [ 1, 2 ]
+
+  # expr
+  { bar => { -not_in => \"(1, 2)" } }
+
+  # aqt
+  { -op =>
+      [ 'not_in', { -ident => [ 'bar' ] }, { -literal => [ '1, 2' ] } ]
+  }
+
+  # query
+  bar NOT IN ( 1, 2 )
+  []
+
+A non-trivial LHS is expanded with ident as the default rather than value:
+
+  # expr
+  { -in => [
+      { -row => [ 'x', 'y' ] }, { -row => [ 1, 2 ] },
+      { -row => [ 3, 4 ] },
+  ] }
+
+  # aqt
+  { -op => [
+      'in', { -row => [ { -ident => [ 'x' ] }, { -ident => [ 'y' ] } ] },
+      { -row => [ { -bind => [ undef, 1 ] }, { -bind => [ undef, 2 ] } ] },
+      { -row => [ { -bind => [ undef, 3 ] }, { -bind => [ undef, 4 ] } ] },
+  ] }
+
+  # query
+  (x, y) IN ( (?, ?), (?, ?) )
+  [ 1, 2, 3, 4 ]
+
+=head2 and/or ops
+
+expands the same way as a plain arrayref/hashref expression but with the
+logic type set to the op name.
+
+=head2 is op
+
+Expands is and is_not to null checks, RHS value must be undef:
+
+  # expr
+  { -is => [ 'foo', undef ] }
+
+  # aqt
+  { -op => [ 'is_null', { -ident => [ 'foo' ] } ] }
+
+  # query
+  foo IS NULL
+  []
+
+  # expr
+  { bar => { -is_not => undef } }
+
+  # aqt
+  { -op => [ 'is_not_null', { -ident => [ 'bar' ] } ] }
+
+  # query
+  bar IS NOT NULL
+  []
+
+=head2 ident op
+
+Expands a string ident to an arrayref by splitting on the configured
+separator, almost always '.':
+
+  # expr
+  { -ident => 'foo.bar' }
+
+  # aqt
+  { -ident => [ 'foo', 'bar' ] }
+
+  # query
+  foo.bar
+  []
+
+=head2 value op
+
+Expands to a bind node with the currently applicable column name if known:
+
+  # expr
+  { foo => { '=' => { -value => 3 } } }
+
+  # aqt
+  { -op => [ '=', { -ident => [ 'foo' ] }, { -bind => [ 'foo', 3 ] } ] }
+
+  # query
+  foo = ?
+  [ 3 ]
+
+=head1 Query Types
+
+=head2 select
+
+A select node accepts select, from, where and order_by clauses.
+
+The select clause is expanded as a list expression with a -ident default:
+
+  # expr
+  { -select => { _ => [ 'foo', 'bar', { -count => 'baz' } ] } }
+
+  # aqt
+  { -select => { select => { -op => [
+          ',', { -ident => [ 'foo' ] }, { -ident => [ 'bar' ] },
+          { -func => [ 'count', { -ident => [ 'baz' ] } ] },
+  ] } } }
+
+  # query
+  SELECT foo, bar, COUNT(baz)
+  []
+
+The from clause is expanded as a list expression with a -ident default:
+
+  # expr
+  { -select => {
+      from => [ 'schema1.table1', { -ident => [ 'schema2', 'table2' ] } ]
+  } }
+
+  # aqt
+  { -select => { from => { -from_list => [
+          { -ident => [ 'schema1', 'table1' ] },
+          { -ident => [ 'schema2', 'table2' ] },
+  ] } } }
+
+  # query
+  FROM schema1.table1, schema2.table2
+  []
+
+The where clause is expanded as a plain expression:
+
+  # expr
+  { -select => { where => { foo => 3 } } }
+
+  # aqt
+  { -select => { where => {
+        -op => [ '=', { -ident => [ 'foo' ] }, { -bind => [ 'foo', 3 ] } ]
+  } } }
+
+  # query
+  WHERE foo = ?
+  [ 3 ]
+
+The order_by clause expands as a list expression at top level, but a hashref
+element may be either an expr or a hashpair with key -asc or -desc to indicate
+an order by direction:
+
+  # expr
+  { -select =>
+      { order_by => [ 'foo', { -desc => 'bar' }, { -max => 'baz' } ] }
+  }
+
+  # aqt
+  { -select => { order_by => { -op => [
+          ',', { -ident => [ 'foo' ] }, {
+            -op => [ ',', { -op => [ 'desc', { -ident => [ 'bar' ] } ] } ]
+          }, { -func => [ 'max', { -ident => [ 'baz' ] } ] },
+  ] } } }
+
+  # query
+  ORDER BY foo, bar DESC, MAX(baz)
+  []
+
+=head2
+
+An insert node accepts an into/target clause, a fields clause, a values/from
+clause, and a returning clause.
+
+The target clause is expanded with an ident default.
+
+The fields clause is expanded as a list expression if an arrayref, and
+otherwise passed through.
+
+The from clause may either be an expr, a literal, an arrayref of column
+values, or a hashref mapping colum names to values.
+
+The returning clause is expanded as a list expr with an ident default.
+
+  # expr
+  { -insert => {
+      into => 'foo',
+      returning => 'id',
+      values => { bar => 'yay', baz => 'argh' },
+  } }
+
+  # aqt
+  { -insert => {
+      fields =>
+        { -row => [ { -ident => [ 'bar' ] }, { -ident => [ 'baz' ] } ] },
+      from => { -values => [ { -row => [
+              { -bind => [ 'bar', 'yay' ] },
+              { -bind => [ 'baz', 'argh' ] },
+      ] } ] },
+      returning => { -op => [ ',', { -ident => [ 'id' ] } ] },
+      target => { -ident => [ 'foo' ] },
+  } }
+
+  # query
+  INSERT INTO foo (bar, baz) VALUES (?, ?) RETURNING id
+  [ 'yay', 'argh' ]
+
+  # expr
+  { -insert => {
+      fields => [ 'bar', 'baz' ],
+      from => { -select => { _ => [ 'bar', 'baz' ], from => 'other' } },
+      into => 'foo',
+  } }
+
+  # aqt
+  { -insert => {
+      fields => { -row => [ { -op =>
+              [ ',', { -ident => [ 'bar' ] }, { -ident => [ 'baz' ] } ]
+      } ] },
+      from => { -select => {
+          from => { -ident => [ 'other' ] },
+          select => { -op =>
+              [ ',', { -ident => [ 'bar' ] }, { -ident => [ 'baz' ] } ]
+          },
+      } },
+      target => { -ident => [ 'foo' ] },
+  } }
+
+  # query
+  INSERT INTO foo (bar, baz) SELECT bar, baz FROM other
+  []
+
+=head2 update
+
+An update node accepts update/target (either may be used at expansion time),
+set, where, and returning clauses.
+
+The target clause is expanded with an ident default.
+
+The set clause (if not already a list expr) is expanded as a hashref where
+the keys are identifiers to be set and the values are exprs/values.
+
+The where clauses is expanded as a normal expr.
+
+The returning clause is expanded as a list expr with an ident default.
+
+  # expr
+  { -update => {
+      _ => 'foo',
+      returning => [ 'id', 'baz' ],
+      set => { bar => 3, baz => { baz => { '+' => 1 } } },
+      where => { -not => { -ident => 'quux' } },
+  } }
+
+  # aqt
+  { -update => {
+      returning =>
+        {
+          -op => [ ',', { -ident => [ 'id' ] }, { -ident => [ 'baz' ] } ]
+        },
+      set => { -op => [
+          ',', { -op =>
+              [ '=', { -ident => [ 'bar' ] }, { -bind => [ 'bar', 3 ] } ]
+          }, { -op => [
+              '=', { -ident => [ 'baz' ] }, { -op => [
+                  '+', { -ident => [ 'baz' ] },
+                  { -bind => [ 'baz', 1 ] },
+              ] },
+          ] },
+      ] },
+      target => { -ident => [ 'foo' ] },
+      where => { -op => [ 'not', { -ident => [ 'quux' ] } ] },
+  } }
+
+  # query
+  UPDATE foo SET bar = ?, baz = baz + ? WHERE (NOT quux) RETURNING id, baz
+  [ 3, 1 ]
+
+=head2 delete
+
+delete accepts from/target, where, and returning clauses.
+
+The target clause is expanded with an ident default.
+
+The where clauses is expanded as a normal expr.
+
+The returning clause is expanded as a list expr with an ident default.
+
+  # expr
+  { -delete => {
+      from => 'foo',
+      returning => 'id',
+      where => { bar => { '<' => 10 } },
+  } }
+
+  # aqt
+  { -delete => {
+      returning => { -op => [ ',', { -ident => [ 'id' ] } ] },
+      target => { -op => [ ',', { -ident => [ 'foo' ] } ] },
+      where => { -op =>
+          [ '<', { -ident => [ 'bar' ] }, { -bind => [ 'bar', 10 ] } ]
+      },
+  } }
+
+  # query
+  DELETE FROM foo WHERE bar < ? RETURNING id
+  [ 10 ]
+
+=cut
diff --git a/lib/SQL/Abstract/Role/Plugin.pm b/lib/SQL/Abstract/Role/Plugin.pm
new file mode 100644 (file)
index 0000000..47c325e
--- /dev/null
@@ -0,0 +1,72 @@
+package SQL::Abstract::Role::Plugin;
+
+use Moo::Role;
+
+has sqla => (
+  is => 'ro', init_arg => undef,
+  handles => [ qw(
+    expand_expr render_aqt join_query_parts
+  ) ],
+);
+
+sub cb {
+  my ($self, $method, @args) = @_;
+  return sub {
+    local $self->{sqla} = shift;
+    $self->$method(@args, @_)
+  };
+}
+
+sub register {
+  my ($self, @pairs) = @_;
+  my $sqla = $self->sqla;
+  while (my ($method, $cases) = splice(@pairs, 0, 2)) {
+    my @cases = @$cases;
+    while (my ($name, $case) = splice(@cases, 0, 2)) {
+      $sqla->$method($name, $self->cb($case));
+    }
+  }
+  return $self;
+}
+
+sub apply_to {
+  my ($self, $sqla) = @_;
+  $self = $self->new unless ref($self);
+  local $self->{sqla} = $sqla;
+  $self->register_extensions($sqla);
+}
+
+requires 'register_extensions';
+
+1;
+
+__END__
+
+=head1 NAME
+
+SQL::Abstract::Role::Plugin - helpful methods for plugin authors
+
+=head1 METHODS
+
+=head2 apply_to
+
+Applies the plugin to an L<SQL::Abstract> object.
+
+=head2 register_extensions
+
+Provided by the plugin, registers its extensions to the sqla object.
+
+=head2 cb
+
+Creates a callback to call a method on the plugin.
+
+=head2 register
+
+Calls methods on the sqla object with arguments wrapped as callbacks.
+
+=head2 sqla
+
+Available only during plugin callback executions, contains the currently
+active L<SQL::Abstract> object.
+
+=cut
index bb0fcf7..4f12e06 100644 (file)
@@ -7,6 +7,42 @@ use Test::Builder;
 use Test::Deep ();
 use SQL::Abstract::Tree;
 
+{
+  my $class;
+  if ($class = $ENV{SQL_ABSTRACT_TEST_AGAINST}) {
+    my $mod = join('/', split '::', $class).".pm";
+    require $mod;
+    eval qq{sub SQL::Abstract () { "\Q${class}\E" }; 1}
+      or die "Failed to create const sub for ${class}: $@";
+  }
+  if ($ENV{SQL_ABSTRACT_TEST_EXPAND_STABILITY}) {
+    $class ||= do { require SQL::Abstract; 'SQL::Abstract' };
+    my $orig = $class->can('expand_expr');
+    require Data::Dumper::Concise;
+    my $wrapped = sub {
+      my ($self, @args) = @_;
+      my $e1 = $self->$orig(@args);
+      return $e1 if our $Stab_Check_Rec;
+      local $Stab_Check_Rec = 1;
+      my $e2 = $self->$orig($e1);
+      my ($d1, $d2) = map Data::Dumper::Concise::Dumper($_), $e1, $e2;
+      (our $tb)->is_eq(
+        $d2, $d1,
+        'expand_expr stability ok'
+      ) or do {
+        require Path::Tiny;
+        Path::Tiny->new('e1')->spew($d1);
+        Path::Tiny->new('e2')->spew($d2);
+        system('diff -u e1 e2 1>&2');
+        die "Differences between e1 and e2, bailing out";
+      };
+      return $e1;
+    };
+    no strict 'refs'; no warnings 'redefine';
+    *{"${class}::expand_expr"} = $wrapped;
+  }
+}
+
 our @EXPORT_OK = qw(
   is_same_sql_bind is_same_sql is_same_bind
   eq_sql_bind eq_sql eq_bind dumper diag_where
@@ -124,6 +160,12 @@ sub _sql_differ_diag {
   my $sql2 = shift || '';
 
   my $tb = $tb || __PACKAGE__->builder;
+
+  if (my $profile = $ENV{SQL_ABSTRACT_TEST_TREE_PROFILE}) {
+    my $sqlat = SQL::Abstract::Tree->new(profile => $profile);
+    $_ = $sqlat->format($_) for ($sql1, $sql2);
+  }
+
   $tb->${\($tb->in_todo ? 'note' : 'diag')} (
        "SQL expressions differ\n"
       ." got: $sql1\n"
diff --git a/maint/inplace b/maint/inplace
new file mode 100755 (executable)
index 0000000..6596e46
--- /dev/null
@@ -0,0 +1,17 @@
+#!/usr/bin/env perl
+
+use strictures 2;
+use autodie;
+
+my ($cmd, $file, @args) = @ARGV;
+
+my $input = do { local (@ARGV, $/) = $file; <> };
+
+close STDOUT;
+open STDOUT, '>', $file;
+
+open $out, '|-', $cmd, @args;
+
+print $out $input;
+
+close $out;
diff --git a/maint/lib/Chunkstrumenter.pm b/maint/lib/Chunkstrumenter.pm
new file mode 100644 (file)
index 0000000..e97c18b
--- /dev/null
@@ -0,0 +1,23 @@
+package Chunkstrumenter;
+
+use strictures 2;
+use Class::Method::Modifiers qw(install_modifier);
+use Data::Dumper::Concise;
+use Context::Preserve;
+
+require SQL::Abstract;
+
+open my $log_fh, '>>', 'chunkstrumenter.log';
+
+install_modifier 'SQL::Abstract', around => '_order_by_chunks' => sub {
+  my ($orig, $self) = (shift, shift);
+  my @args = @_;
+  preserve_context { $self->$orig(@args) }
+    after => sub {
+      my $dumped = Dumper([ $self->{quote_char}, \@args, \@_ ]);
+      $dumped =~ s/\n\Z/,\n/;
+      print $log_fh $dumped;
+    };
+};
+
+1;
diff --git a/maint/podregen b/maint/podregen
new file mode 100755 (executable)
index 0000000..cbb5ed2
--- /dev/null
@@ -0,0 +1,61 @@
+#!/usr/bin/env perl
+
+use lib 'lib';
+use strictures 2;
+use Data::Dumper::Compact;
+use Devel::DDCWarn;
+use SQL::Abstract::Formatter;
+use SQL::Abstract;
+
+my $ddc = Data::Dumper::Compact->new(max_width => 72);
+my $sqla = SQL::Abstract->new(
+  lazy_join_sql_parts => 1,
+)->plugin('+ExtraClauses');
+my $sqlaf = SQL::Abstract::Formatter->new(max_width => 72);
+
+while (1) {
+  my $line = <STDIN>;
+  exit 0 unless defined $line;
+  print $line;
+  last if $line =~ /\A__END__/;
+}
+
+my $slurp = do { local $/; <STDIN> };
+
+my ($expr_re, $aqt_re, $query_re) =
+  map qr/(?sm:(.*?)( +)(# ${_}\n)(?:\n|(.*?)\n\n))/, qw(expr aqt query);
+
+sub reformat {
+  my ($thing, $indent) = @_;
+  my $thing_ddc = $ddc->dump($thing);
+  $thing_ddc =~ s/^/$indent/mg;
+  return $thing_ddc;
+}
+
+sub seval { eval('+('.$_[0].')') or die "seval: $_[0]: $@" }
+
+while ($slurp =~ m/\G$expr_re/gc) {
+  my ($pre, $indent, $type, $expr_str) = ($1, $2, $3, $4);
+  print $pre.$indent.$type;
+  print reformat(my $expr = seval($expr_str), $indent);
+  print "\n";
+  die unless $slurp =~ m/\G$query_re/g;
+  my ($qpre, $qindent, $qtype) = ($1, $2, $3);
+  if ($qpre =~ s/\A$aqt_re//) {
+    my ($apre, $aindent, $atype) = ($1, $2, $3);
+    print $apre.$aindent.$atype;
+    print reformat($sqla->expand_expr($expr), $aindent);
+    print "\n";
+  }
+  print $qpre.$qindent.$qtype;
+  my ($sql, @bind) = $sqla->render_statement($expr);
+  my $fsql = (ref($sql) ? $sqlaf->format(@$sql) : $sql);
+  s/^/$indent/mg, s/\n+\Z// for $fsql;
+  print $fsql."\n";
+  print reformat(\@bind, $qindent);
+  print "\n";
+}
+
+$slurp =~ /\G(.*)$/sm;
+
+print $1;
diff --git a/maint/sqlacexpr b/maint/sqlacexpr
new file mode 100755 (executable)
index 0000000..9bcb6c6
--- /dev/null
@@ -0,0 +1,22 @@
+#!/usr/bin/env perl
+
+use lib 'lib';
+use strictures 2;
+use SQL::Abstract;
+use Devel::DDCWarn;
+
+warn $ARGV[1]."\n";
+
+my $sqlac = SQL::Abstract->new(
+  unknown_unop_always_func => 1,
+  lazy_join_sql_parts => 1,
+)->plugin('+ExtraClauses');
+
+my @args = ($ARGV[1] =~ /^\.\// ? do $ARGV[1] : eval '+('.$ARGV[1].')');
+
+die $@ if $@;
+
+my ($q, @bind) = $sqlac->${\$ARGV[0]}(@args);
+
+print STDERR +(ref($q) ? $q->format : $q)."\n";
+Dwarn [ @bind ];
diff --git a/maint/sqlaexpr b/maint/sqlaexpr
new file mode 100755 (executable)
index 0000000..f94a371
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/env perl
+
+use lib 'lib';
+use SQL::Abstract;
+use Devel::Dwarn;
+
+warn $ARGV[1];
+
+my @args = eval '('.$ARGV[1].')';
+
+die $@ if $@;
+
+Dwarn([ SQL::Abstract->new->${\$ARGV[0]}(@args) ]);
index f6aecfe..7cb7103 100644 (file)
--- a/t/00new.t
+++ b/t/00new.t
@@ -2,8 +2,9 @@ use strict;
 use warnings;
 use Test::More;
 use Test::Warn;
+use Test::Exception;
 
-use SQL::Abstract::Test import => ['is_same_sql'];
+use SQL::Abstract::Test import => [ qw(is_same_sql dumper) ];
 use SQL::Abstract;
 
 my @handle_tests = (
@@ -90,13 +91,15 @@ my @handle_tests = (
 for (@handle_tests) {
   my $sqla  = SQL::Abstract->new($_->{args});
   my $stmt;
-  warnings_exist {
-    $stmt = $sqla->select(
-      'test',
-      '*',
-      $_->{where} || { a => 4, b => 0}
-    );
-  } $_->{warns} || [];
+  lives_ok(sub {
+    (warnings_exist {
+      $stmt = $sqla->select(
+        'test',
+        '*',
+        $_->{where} || { a => 4, b => 0}
+      );
+    } $_->{warns} || []) || diag dumper($_);
+  }) or diag dumper({ %$_, threw => $@ });
 
   is_same_sql($stmt, $_->{stmt});
 }
index ebe3aad..85a667a 100644 (file)
@@ -16,7 +16,6 @@ use SQL::Abstract;
 #
 #################
 
-
 my @tests = (
       {
               func   => 'select',
@@ -75,6 +74,14 @@ my @tests = (
               bind   => ['boom']
       },
       {
+              # this is maybe wrong but a single arg doesn't get quoted
+              func   => 'select',
+              args   => ['test', 'id', { a => { '!=', 'boom' } }],
+              stmt   => 'SELECT id FROM test WHERE ( a != ? )',
+              stmt_q => 'SELECT id FROM `test` WHERE ( `a` != ? )',
+              bind   => ['boom']
+      },
+      {
               func   => 'update',
               args   => ['test', {a => 'boom'}, {a => undef}],
               stmt   => 'UPDATE test SET a = ? WHERE ( a IS NULL )',
@@ -83,6 +90,13 @@ my @tests = (
       },
       {
               func   => 'update',
+              args   => ['test', {a => undef }, {a => 'boom'}],
+              stmt   => 'UPDATE test SET a = ? WHERE ( a = ? )',
+              stmt_q => 'UPDATE `test` SET `a` = ? WHERE ( `a` = ? )',
+              bind   => [undef,'boom']
+      },
+      {
+              func   => 'update',
               args   => ['test', {a => 'boom'}, { a => {'!=', "bang" }} ],
               stmt   => 'UPDATE test SET a = ? WHERE ( a != ? )',
               stmt_q => 'UPDATE `test` SET `a` = ? WHERE ( `a` != ? )',
@@ -440,6 +454,14 @@ my @tests = (
               func   => 'update',
               new    => {bindtype => 'columns'},
               args   => ['test', {a => 1, b => \["to_date(?, 'MM/DD/YY')", [{dummy => 1} => '02/02/02']], c => { -lower => 'foo' }}, {a => {'between', [1,2]}}],
+              stmt   => "UPDATE test SET a = ?, b = to_date(?, 'MM/DD/YY'), c = LOWER(?) WHERE ( a BETWEEN ? AND ? )",
+              stmt_q => "UPDATE `test` SET `a` = ?, `b` = to_date(?, 'MM/DD/YY'), `c` = LOWER(?) WHERE ( `a` BETWEEN ? AND ? )",
+              bind   => [[a => '1'], [{dummy => 1} => '02/02/02'], [c => 'foo'], [a => '1'], [a => '2']],
+      },
+      {
+              func   => 'update',
+              new    => {bindtype => 'columns',restore_old_unop_handling => 1},
+              args   => ['test', {a => 1, b => \["to_date(?, 'MM/DD/YY')", [{dummy => 1} => '02/02/02']], c => { -lower => 'foo' }}, {a => {'between', [1,2]}}],
               stmt   => "UPDATE test SET a = ?, b = to_date(?, 'MM/DD/YY'), c = LOWER ? WHERE ( a BETWEEN ? AND ? )",
               stmt_q => "UPDATE `test` SET `a` = ?, `b` = to_date(?, 'MM/DD/YY'), `c` = LOWER ? WHERE ( `a` BETWEEN ? AND ? )",
               bind   => [[a => '1'], [{dummy => 1} => '02/02/02'], [c => 'foo'], [a => '1'], [a => '2']],
@@ -523,6 +545,14 @@ my @tests = (
               func   => 'select',
               new    => {bindtype => 'columns'},
               args   => ['test', '*', [ Y => { '=' => { -max => { -LENGTH => { -min => 'x' } } } } ] ],
+              stmt   => 'SELECT * FROM test WHERE ( Y = ( MAX( LENGTH( MIN(?) ) ) ) )',
+              stmt_q => 'SELECT * FROM `test` WHERE ( `Y` = ( MAX( LENGTH( MIN(?) ) ) ) )',
+              bind   => [[Y => 'x']],
+      },
+      {
+              func   => 'select',
+              new    => {bindtype => 'columns',restore_old_unop_handling => 1},
+              args   => ['test', '*', [ Y => { '=' => { -max => { -LENGTH => { -min => 'x' } } } } ] ],
               stmt   => 'SELECT * FROM test WHERE ( Y = ( MAX( LENGTH( MIN ? ) ) ) )',
               stmt_q => 'SELECT * FROM `test` WHERE ( `Y` = ( MAX( LENGTH( MIN ? ) ) ) )',
               bind   => [[Y => 'x']],
@@ -616,6 +646,13 @@ my @tests = (
               stmt_q => 'DELETE FROM `test` WHERE ( `requestor` IS NULL ) RETURNING `id`, `created_at`',
               bind   => []
       },
+      {
+              func   => 'delete',
+              args   => ['test', \[ undef ] ],
+              stmt   => 'DELETE FROM test',
+              stmt_q => 'DELETE FROM `test`',
+              bind   => []
+      },
 );
 
 # check is( not) => undef
@@ -726,9 +763,6 @@ for my $lhs (undef, '') {
     [ foo => "bar", $lhs => \["baz"], bizz => "buzz" ],
     [ $lhs => \"baz" ],
     [ $lhs => \["baz"] ],
-
-    # except for this one, that is automagically arrayified
-    { foo => "bar", -or => { $lhs => \"baz" }, bizz => "buzz" },
   ) {
     push @tests, {
       func => 'where',
@@ -741,6 +775,7 @@ for my $lhs (undef, '') {
 ## deprecations - sorta worked, likely abused by folks
   for my $where_arg (
     # the arrayref forms of this never worked and throw above
+    { foo => "bar", -or => { $lhs => \"baz" }, bizz => "buzz" },
     { foo => "bar", -and => { $lhs => \"baz" }, bizz => "buzz" },
     { foo => "bar", $lhs => \"baz", bizz => "buzz" },
     { foo => "bar", $lhs => \["baz"], bizz => "buzz" },
@@ -850,17 +885,26 @@ for my $t (@tests) {
       ) || diag dumper({ args => $t->{args}, result => $stmt });
     }
     else {
-      warnings_like(
-        sub { $cref->() },
-        $t->{warns} || [],
-      ) || diag dumper({ args => $t->{args}, result => $stmt });
+      lives_ok(sub {
+        alarm(1); local $SIG{ALRM} = sub {
+          no warnings 'redefine';
+          my $orig = Carp->can('caller_info');
+          local *Carp::caller_info = sub { return if $_[0] > 20; &$orig };
+          print STDERR "ARGH ($SQL::Abstract::Default_Scalar_To): ".Carp::longmess();
+          die "timed out";
+        };
+        warnings_like(
+          sub { $cref->() },
+          $t->{warns} || [],
+        ) || diag dumper({ args => $t->{args}, result => $stmt });
+      }) || diag dumper({ args => $t->{args}, result => $stmt, threw => $@ });
 
       is_same_sql_bind(
         $stmt,
         \@bind,
         $quoted ? $t->{stmt_q}: $t->{stmt},
         $t->{bind}
-      );
+      ) || diag dumper({ args => $t->{args}, result => $stmt });;
     }
   }
 }
index 229770b..4c21045 100644 (file)
@@ -2,7 +2,8 @@ use strict;
 use warnings;
 use Test::More;
 use Test::Warn;
-use SQL::Abstract::Test import => [qw(is_same_sql_bind diag_where) ];
+use Test::Exception;
+use SQL::Abstract::Test import => [qw(is_same_sql_bind diag_where dumper) ];
 
 use SQL::Abstract;
 
@@ -10,6 +11,12 @@ my $not_stringifiable = bless {}, 'SQLA::NotStringifiable';
 
 my @handle_tests = (
     {
+        where => 'foo',
+        order => [],
+        stmt => ' WHERE foo',
+        bind => [],
+    },
+    {
         where => {
             requestor => 'inna',
             worker => ['nwiger', 'rcwe', 'sfz'],
@@ -116,6 +123,14 @@ my @handle_tests = (
 
     {
         where => {
+            requestor => [undef, ''],
+        },
+        stmt => " WHERE ( requestor IS NULL OR requestor = ? )",
+        bind => [''],
+    },
+
+    {
+        where => {
             priority  => [ {'>', 3}, {'<', 1} ],
             requestor => { '!=', undef },
         },
@@ -328,12 +343,12 @@ my @handle_tests = (
 # Op against random functions (these two are oracle-specific)
    {
        where => { timestamp => { '!=' => { -trunc => { -year => \'sysdate' } } } },
-       stmt => " WHERE ( timestamp != TRUNC (YEAR sysdate) )",
+       stmt => " WHERE ( timestamp != TRUNC(YEAR(sysdate)) )",
        bind => [],
    },
    {
        where => { timestamp => { '>=' => { -to_date => '2009-12-21 00:00:00' } } },
-       stmt => " WHERE ( timestamp >= TO_DATE ? )",
+       stmt => " WHERE ( timestamp >= TO_DATE(?) )",
        bind => ['2009-12-21 00:00:00'],
    },
 
@@ -391,17 +406,34 @@ my @handle_tests = (
         stmt  => " WHERE ( 0 ) ",
         bind => [ ],
     },
+    {
+        where => { artistid => {} },
+        stmt => '',
+        bind => [ ],
+    },
+    {
+        where => [ -and => [ {}, [] ], -or => [ {}, [] ] ],
+        stmt => '',
+        bind => [ ],
+    },
+    {
+        where => { '=' => \'bozz' },
+        stmt => 'WHERE = bozz',
+        bind => [ ],
+    },
 );
 
 for my $case (@handle_tests) {
     my $sql = SQL::Abstract->new;
     my ($stmt, @bind);
-    warnings_exist {
-      ($stmt, @bind) = $sql->where($case->{where}, $case->{order});
-    } $case->{warns} || [];
+    lives_ok {
+      warnings_like {
+        ($stmt, @bind) = $sql->where($case->{where}, $case->{order});
+      } $case->{warns} || [];
+    };
 
     is_same_sql_bind($stmt, \@bind, $case->{stmt}, $case->{bind})
-      || diag_where ( $case->{where} );
+      || do { diag_where ( $case->{where} ); diag dumper($sql->_expand_expr($case->{where})) };
 }
 
 done_testing;
index 702c299..027b5d9 100644 (file)
@@ -387,13 +387,15 @@ for my $case (@and_or_tests) {
     my $where_copy = dclone($case->{where});
 
     warnings_are {
-      my ($stmt, @bind) = $sql->where($case->{where});
-      is_same_sql_bind(
-        $stmt,
-        \@bind,
-        $case->{stmt},
-        $case->{bind},
-      ) || diag_where( $case->{where} );
+      lives_ok {
+        my ($stmt, @bind) = $sql->where($case->{where});
+        is_same_sql_bind(
+          $stmt,
+          \@bind,
+          $case->{stmt},
+          $case->{bind},
+        ) || (diag_where ( $case->{where} ), diag dumper ([ EXP => $sql->_expand_expr($case->{where}) ]));
+      } || (diag_where ( $case->{where} ), diag dumper ([ EXP => $sql->_expand_expr($case->{where}) ]));
     } [], 'No warnings within and-or tests';
 
     is_deeply ($case->{where}, $where_copy, 'Where conditions unchanged');
@@ -414,7 +416,7 @@ for my $case (@nest_tests) {
         \@bind,
         $case->{stmt},
         $case->{bind},
-      ) || diag_where ( $case->{where} );
+      ) || (diag_where ( $case->{where} ), diag dumper ([ EXP => $sql->_expand_expr($case->{where}) ]));
     });
   }
 }
@@ -428,7 +430,7 @@ for my $case (@numbered_mods) {
     local $SIG{__WARN__} = sub { push @w, @_ };
 
     my $sql = SQL::Abstract->new($case->{args} || {});
-    {
+    lives_ok {
       my ($old_s, @old_b) = $sql->where($case->{backcompat});
       my ($new_s, @new_b) = $sql->where($case->{correct});
       is_same_sql_bind(
index 14c8f4e..f32c57a 100644 (file)
@@ -93,6 +93,26 @@ my @in_between_tests = (
       ] },
     },
     stmt => "WHERE (
+          ( start0 BETWEEN ? AND UPPER(?)         )
+      AND ( start1 BETWEEN ? AND ?                )
+      AND ( start2 BETWEEN lower(x) AND upper(y)  )
+      AND ( start3 BETWEEN lower(x) AND upper(?)  )
+    )",
+    bind => [1, 2, 1, 2, 'stuff'],
+    test => '-between POD test',
+  },
+  {
+    args => { restore_old_unop_handling => 1 },
+    where => {
+      start0 => { -between => [ 1, { -upper => 2 } ] },
+      start1 => { -between => \["? AND ?", 1, 2] },
+      start2 => { -between => \"lower(x) AND upper(y)" },
+      start3 => { -between => [
+        \"lower(x)",
+        \["upper(?)", 'stuff' ],
+      ] },
+    },
+    stmt => "WHERE (
           ( start0 BETWEEN ? AND UPPER ?          )
       AND ( start1 BETWEEN ? AND ?                )
       AND ( start2 BETWEEN lower(x) AND upper(y)  )
@@ -113,6 +133,32 @@ my @in_between_tests = (
       ] },
     },
     stmt => "WHERE (
+          ( start0 BETWEEN ? AND UPPER(?)         )
+      AND ( start1 BETWEEN ? AND ?                )
+      AND ( start2 BETWEEN lower(x) AND upper(y)  )
+      AND ( start3 BETWEEN lower(x) AND upper(?)  )
+    )",
+    bind => [
+      [ start0 => 1 ],
+      [ start0 => 2 ],
+      [ start1 => 1 ],
+      [ start1 => 2 ],
+      [ start3 => 'stuff' ],
+    ],
+    test => '-between POD test',
+  },
+  {
+    args => { restore_old_unop_handling => 1, bindtype => 'columns' },
+    where => {
+      start0 => { -between => [ 1, { -upper => 2 } ] },
+      start1 => { -between => \["? AND ?", [ start1 => 1], [start1 => 2] ] },
+      start2 => { -between => \"lower(x) AND upper(y)" },
+      start3 => { -between => [
+        \"lower(x)",
+        \["upper(?)", [ start3 => 'stuff'] ],
+      ] },
+    },
+    stmt => "WHERE (
           ( start0 BETWEEN ? AND UPPER ?          )
       AND ( start1 BETWEEN ? AND ?                )
       AND ( start2 BETWEEN lower(x) AND upper(y)  )
@@ -204,6 +250,13 @@ my @in_between_tests = (
 
   {
     where => { x => { -in => [ \['LOWER(?)', 'A' ], \'LOWER(b)', { -lower => 'c' } ] } },
+    stmt => " WHERE ( x IN ( LOWER(?), LOWER(b), LOWER(?) ) )",
+    bind => [qw/A c/],
+    test => '-in with an array of function array refs with args',
+  },
+  {
+    args => { restore_old_unop_handling => 1 },
+    where => { x => { -in => [ \['LOWER(?)', 'A' ], \'LOWER(b)', { -lower => 'c' } ] } },
     stmt => " WHERE ( x IN ( LOWER(?), LOWER(b), LOWER ? ) )",
     bind => [qw/A c/],
     test => '-in with an array of function array refs with args',
@@ -279,15 +332,59 @@ my @in_between_tests = (
   },
 
   {
-    where => { -in => [42] },
-    throws => qr/Illegal use of top-level '-in'/,
+    where => { -in => [ 'bob', 4, 2 ] },
+    stmt => ' WHERE (bob IN (?, ?))',
+    bind => [ 4, 2 ],
     test => 'Top level -in',
   },
+# This works but then SQL::Abstract::Tree breaks - something for a later commit
+#  {
+#    where => { -in => [ { -list => [ qw(x y) ] }, { -list => [ 1, 3 ] }, { -list => [ 2, 4 ] } ] },
+#    stmt => ' WHERE ((x, y) IN ((?, ?), (?, ?))',
+#    bind => [ 1, 3, 2, 4 ],
+#    test => 'Top level -in with list args',
+#  },
   {
     where => { -between => [42, 69] },
-    throws => qr/Illegal use of top-level '-between'/,
-    test => 'Top level -between',
+    throws => qr/Fatal: Operator 'BETWEEN' requires/,
+    test => 'Top level -between with broken args',
   },
+  {
+    where => {
+      -between => [
+        { -op => [ '+', { -ident => 'foo' }, 2 ] },
+        3, 4
+      ],
+    },
+    stmt => ' WHERE (foo + ? BETWEEN ? AND ?)',
+    bind => [ 2, 3, 4 ],
+    test => 'Top level -between with useful LHS',
+  },
+  {
+    where => {
+      -in => [
+        { -row => [ 'x', 'y' ] },
+        { -row => [ 1, 2 ] },
+        { -row => [ 3, 4 ] },
+      ],
+    },
+    stmt => ' WHERE (x, y) IN ((?, ?), (?, ?))',
+    bind => [ 1..4 ],
+    test => 'Complex top-level -in',
+  },
+  {
+    where => { -is => [ 'bob', undef ] },
+    stmt => ' WHERE bob IS NULL',
+    bind => [],
+    test => 'Top level -is ok',
+  },
+  {
+    where => { -op => [ in => x => 1, 2, 3 ] },
+    stmt => ' WHERE x IN (?, ?, ?)',
+    bind => [ 1, 2, 3 ],
+    test => 'Raw -op passes through correctly'
+  },
+
 );
 
 for my $case (@in_between_tests) {
@@ -305,17 +402,19 @@ for my $case (@in_between_tests) {
     }
     else {
       my ($stmt, @bind);
-      warnings_are {
-        ($stmt, @bind) = $sql->where($case->{where});
-      } [], "$label gives no warnings";
+      lives_ok {
+        warnings_are {
+          ($stmt, @bind) = $sql->where($case->{where});
+        } [], "$label gives no warnings";
 
-      is_same_sql_bind(
-        $stmt,
-        \@bind,
-        $case->{stmt},
-        $case->{bind},
-        "$label generates correct SQL and bind",
-      ) || diag_where ( $case->{where} );
+        is_same_sql_bind(
+          $stmt,
+          \@bind,
+          $case->{stmt},
+          $case->{bind},
+          "$label generates correct SQL and bind",
+        ) || diag dumper ({ where => $case->{where}, exp => $sql->_expand_expr($case->{where}) });
+      } || diag dumper ({ where => $case->{where}, exp => $sql->_expand_expr($case->{where}) });
     }
   }
 }
index 7d1213e..0d340ae 100644 (file)
@@ -58,6 +58,11 @@ my @cases =
     expects => '',
     expects_quoted => '',
    },
+   {
+    given => [ {} ],
+    expects => '',
+    expects_quoted => '',
+   },
 
    {
     given => [{-desc => [ qw/colA colB/ ] }],
@@ -134,6 +139,12 @@ throws_ok (
 );
 
 throws_ok (
+  sub { $sql->_order_by([ {-desc => 'colA', -asc => 'colB' } ]) },
+  qr/hash passed .+ must have exactly one key/,
+  'Undeterministic order exception',
+);
+
+throws_ok (
   sub { $sql->_order_by({-desc => [ qw/colA colB/ ], -asc => [ qw/colC colD/ ] }) },
   qr/hash passed .+ must have exactly one key/,
   'Undeterministic order exception',
index d989a79..c4b303e 100644 (file)
@@ -33,6 +33,29 @@ my $sqlmaker = SQL::Abstract->new(special_ops => [
      }
    },
 
+  # PRIOR op from DBIx::Class::SQLMaker::Oracle
+
+  {
+    regex => qr/^prior$/i,
+    handler => sub {
+      my ($self, $lhs, $op, $rhs) = @_;
+      my ($sql, @bind) = $self->_recurse_where ($rhs);
+
+      $sql = sprintf ('%s = %s %s ',
+        $self->_convert($self->_quote($lhs)),
+        $self->_sqlcase ($op),
+        $sql
+      );
+
+      return ($sql, @bind);
+    },
+  },
+
+], unary_ops => [
+  # unary op from Mojo::Pg
+  {regex => qr/^json$/i,
+   handler => sub { '?', { json => $_[2] } }
+  },
 ]);
 
 my @tests = (
@@ -50,6 +73,28 @@ my @tests = (
     bind  => [],
   },
 
+  #3
+  { where => { foo => { -json => { bar => 'baz' } } },
+    stmt => "WHERE foo = ?",
+    bind => [ { json => { bar => 'baz' } } ],
+  },
+
+  #4
+  { where => { foo => { '@>' => { -json => { bar => 'baz' } } } },
+    stmt => "WHERE foo @> ?",
+    bind => [ { json => { bar => 'baz' } } ],
+  },
+
+  # Verify inconsistent behaviour from DBIx::Class:SQLMaker::Oracle works
+  # (unary use of special op is not equivalent to special op + =)
+  {
+    where => {
+      foo_id => { '=' => { '-prior' => { -ident => 'bar_id' } } },
+      baz_id => { '-prior' => { -ident => 'quux_id' } },
+    },
+    stmt        => ' WHERE ( baz_id = PRIOR quux_id AND foo_id = ( PRIOR bar_id ) )',
+    bind        => [],
+  },
 );
 
 for (@tests) {
index a0e566b..d73b11c 100644 (file)
@@ -17,6 +17,16 @@ for my $q ('', '"') {
     $sql_maker->where({ foo => { -ident => undef } })
   } qr/-ident requires a single plain scalar argument/;
 
+  throws_ok {
+    local $sql_maker->{disable_old_special_ops} = 1;
+    $sql_maker->where({'-or' => [{'-ident' => 'foo'},'foo']})
+  } qr/Illegal.*top-level/;
+
+  throws_ok {
+    local $sql_maker->{disable_old_special_ops} = 1;
+    $sql_maker->where({'-or' => [{'-ident' => 'foo'},{'=' => \'bozz'}]})
+  } qr/Illegal.*top-level/;
+
   my ($sql, @bind) = $sql_maker->select('artist', '*', { 'artist.name' => { -ident => 'artist.pseudonym' } } );
   is_same_sql_bind (
     $sql,
@@ -41,6 +51,18 @@ for my $q ('', '"') {
     ",
     [],
   );
+
+  ($sql) = $sql_maker->select(
+    \(my $from = 'foo JOIN bar ON foo.bar_id = bar.id'),
+    [ { -ident => [ 'foo', 'name' ] }, { -ident => [ 'bar', '*' ] } ]
+  );
+
+  is_same_sql_bind(
+    $sql,
+    undef,
+    "SELECT ${q}foo${q}.${q}name${q}, ${q}bar${q}.*
+     FROM $from"
+  );
 }
 
 done_testing;
diff --git a/t/24order_by_chunks.t b/t/24order_by_chunks.t
new file mode 100644 (file)
index 0000000..4a40d67
--- /dev/null
@@ -0,0 +1,1971 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Exception;
+use Data::Dumper::Concise;
+
+use SQL::Abstract;
+
+use SQL::Abstract::Test import => ['is_same_sql_bind'];
+my @cases = (
+  [
+    undef,
+    [
+      \"colA DESC"
+    ],
+    [
+      "colA DESC"
+    ]
+  ],
+  [
+    "`",
+    [
+      \"colA DESC"
+    ],
+    [
+      "colA DESC"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colA"
+    ],
+    [
+      "colA"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colA"
+    ],
+    [
+      "`colA`"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colA DESC"
+    ],
+    [
+      "colA DESC"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colA DESC"
+    ],
+    [
+      "`colA DESC`"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colA"
+    ],
+    [
+      "colA"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colB"
+    ],
+    [
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        "colA",
+        "colB"
+      ]
+    ],
+    [
+      "colA",
+      "colB"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colA"
+    ],
+    [
+      "`colA`"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colB"
+    ],
+    [
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        "colA",
+        "colB"
+      ]
+    ],
+    [
+      "`colA`",
+      "`colB`"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colA ASC"
+    ],
+    [
+      "colA ASC"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colB DESC"
+    ],
+    [
+      "colB DESC"
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        "colA ASC",
+        "colB DESC"
+      ]
+    ],
+    [
+      "colA ASC",
+      "colB DESC"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colA ASC"
+    ],
+    [
+      "`colA ASC`"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colB DESC"
+    ],
+    [
+      "`colB DESC`"
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        "colA ASC",
+        "colB DESC"
+      ]
+    ],
+    [
+      "`colA ASC`",
+      "`colB DESC`"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colA"
+    ],
+    [
+      "colA"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-asc" => "colA"
+      }
+    ],
+    [
+      [
+        "colA ASC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colA"
+    ],
+    [
+      "`colA`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-asc" => "colA"
+      }
+    ],
+    [
+      [
+        "`colA` ASC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colB"
+    ],
+    [
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-desc" => "colB"
+      }
+    ],
+    [
+      [
+        "colB DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colB"
+    ],
+    [
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-desc" => "colB"
+      }
+    ],
+    [
+      [
+        "`colB` DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colA"
+    ],
+    [
+      "colA"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-asc" => "colA"
+      }
+    ],
+    [
+      [
+        "colA ASC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colB"
+    ],
+    [
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-desc" => "colB"
+      }
+    ],
+    [
+      [
+        "colB DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        {
+          "-asc" => "colA"
+        },
+        {
+          "-desc" => "colB"
+        }
+      ]
+    ],
+    [
+      [
+        "colA ASC"
+      ],
+      [
+        "colB DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colA"
+    ],
+    [
+      "`colA`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-asc" => "colA"
+      }
+    ],
+    [
+      [
+        "`colA` ASC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colB"
+    ],
+    [
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-desc" => "colB"
+      }
+    ],
+    [
+      [
+        "`colB` DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        {
+          "-asc" => "colA"
+        },
+        {
+          "-desc" => "colB"
+        }
+      ]
+    ],
+    [
+      [
+        "`colA` ASC"
+      ],
+      [
+        "`colB` DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colA"
+    ],
+    [
+      "colA"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colB"
+    ],
+    [
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-desc" => "colB"
+      }
+    ],
+    [
+      [
+        "colB DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        "colA",
+        {
+          "-desc" => "colB"
+        }
+      ]
+    ],
+    [
+      "colA",
+      [
+        "colB DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colA"
+    ],
+    [
+      "`colA`"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colB"
+    ],
+    [
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-desc" => "colB"
+      }
+    ],
+    [
+      [
+        "`colB` DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        "colA",
+        {
+          "-desc" => "colB"
+        }
+      ]
+    ],
+    [
+      "`colA`",
+      [
+        "`colB` DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colA"
+    ],
+    [
+      "colA"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colB"
+    ],
+    [
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        "colA",
+        "colB"
+      ]
+    ],
+    [
+      "colA",
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-desc" => [
+          "colA",
+          "colB"
+        ]
+      }
+    ],
+    [
+      [
+        "colA DESC"
+      ],
+      [
+        "colB DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        {
+          "-desc" => [
+            "colA",
+            "colB"
+          ]
+        }
+      ]
+    ],
+    [
+      [
+        "colA DESC"
+      ],
+      [
+        "colB DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colA"
+    ],
+    [
+      "`colA`"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colB"
+    ],
+    [
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        "colA",
+        "colB"
+      ]
+    ],
+    [
+      "`colA`",
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-desc" => [
+          "colA",
+          "colB"
+        ]
+      }
+    ],
+    [
+      [
+        "`colA` DESC"
+      ],
+      [
+        "`colB` DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        {
+          "-desc" => [
+            "colA",
+            "colB"
+          ]
+        }
+      ]
+    ],
+    [
+      [
+        "`colA` DESC"
+      ],
+      [
+        "`colB` DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colA"
+    ],
+    [
+      "colA"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colB"
+    ],
+    [
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        "colA",
+        "colB"
+      ]
+    ],
+    [
+      "colA",
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-desc" => [
+          "colA",
+          "colB"
+        ]
+      }
+    ],
+    [
+      [
+        "colA DESC"
+      ],
+      [
+        "colB DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colC"
+    ],
+    [
+      "colC"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-asc" => "colC"
+      }
+    ],
+    [
+      [
+        "colC ASC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        {
+          "-desc" => [
+            "colA",
+            "colB"
+          ]
+        },
+        {
+          "-asc" => "colC"
+        }
+      ]
+    ],
+    [
+      [
+        "colA DESC"
+      ],
+      [
+        "colB DESC"
+      ],
+      [
+        "colC ASC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colA"
+    ],
+    [
+      "`colA`"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colB"
+    ],
+    [
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        "colA",
+        "colB"
+      ]
+    ],
+    [
+      "`colA`",
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-desc" => [
+          "colA",
+          "colB"
+        ]
+      }
+    ],
+    [
+      [
+        "`colA` DESC"
+      ],
+      [
+        "`colB` DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colC"
+    ],
+    [
+      "`colC`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-asc" => "colC"
+      }
+    ],
+    [
+      [
+        "`colC` ASC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        {
+          "-desc" => [
+            "colA",
+            "colB"
+          ]
+        },
+        {
+          "-asc" => "colC"
+        }
+      ]
+    ],
+    [
+      [
+        "`colA` DESC"
+      ],
+      [
+        "`colB` DESC"
+      ],
+      [
+        "`colC` ASC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colA"
+    ],
+    [
+      "colA"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colB"
+    ],
+    [
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        "colA",
+        "colB"
+      ]
+    ],
+    [
+      "colA",
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-desc" => [
+          "colA",
+          "colB"
+        ]
+      }
+    ],
+    [
+      [
+        "colA DESC"
+      ],
+      [
+        "colB DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colC"
+    ],
+    [
+      "colC"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colD"
+    ],
+    [
+      "colD"
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        "colC",
+        "colD"
+      ]
+    ],
+    [
+      "colC",
+      "colD"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-asc" => [
+          "colC",
+          "colD"
+        ]
+      }
+    ],
+    [
+      [
+        "colC ASC"
+      ],
+      [
+        "colD ASC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        {
+          "-desc" => [
+            "colA",
+            "colB"
+          ]
+        },
+        {
+          "-asc" => [
+            "colC",
+            "colD"
+          ]
+        }
+      ]
+    ],
+    [
+      [
+        "colA DESC"
+      ],
+      [
+        "colB DESC"
+      ],
+      [
+        "colC ASC"
+      ],
+      [
+        "colD ASC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colA"
+    ],
+    [
+      "`colA`"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colB"
+    ],
+    [
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        "colA",
+        "colB"
+      ]
+    ],
+    [
+      "`colA`",
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-desc" => [
+          "colA",
+          "colB"
+        ]
+      }
+    ],
+    [
+      [
+        "`colA` DESC"
+      ],
+      [
+        "`colB` DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colC"
+    ],
+    [
+      "`colC`"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colD"
+    ],
+    [
+      "`colD`"
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        "colC",
+        "colD"
+      ]
+    ],
+    [
+      "`colC`",
+      "`colD`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-asc" => [
+          "colC",
+          "colD"
+        ]
+      }
+    ],
+    [
+      [
+        "`colC` ASC"
+      ],
+      [
+        "`colD` ASC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        {
+          "-desc" => [
+            "colA",
+            "colB"
+          ]
+        },
+        {
+          "-asc" => [
+            "colC",
+            "colD"
+          ]
+        }
+      ]
+    ],
+    [
+      [
+        "`colA` DESC"
+      ],
+      [
+        "`colB` DESC"
+      ],
+      [
+        "`colC` ASC"
+      ],
+      [
+        "`colD` ASC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colA"
+    ],
+    [
+      "colA"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colB"
+    ],
+    [
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        "colA",
+        "colB"
+      ]
+    ],
+    [
+      "colA",
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-desc" => [
+          "colA",
+          "colB"
+        ]
+      }
+    ],
+    [
+      [
+        "colA DESC"
+      ],
+      [
+        "colB DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colC"
+    ],
+    [
+      "colC"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-desc" => "colC"
+      }
+    ],
+    [
+      [
+        "colC DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        {
+          "-desc" => [
+            "colA",
+            "colB"
+          ]
+        },
+        {
+          "-desc" => "colC"
+        }
+      ]
+    ],
+    [
+      [
+        "colA DESC"
+      ],
+      [
+        "colB DESC"
+      ],
+      [
+        "colC DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colA"
+    ],
+    [
+      "`colA`"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colB"
+    ],
+    [
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        "colA",
+        "colB"
+      ]
+    ],
+    [
+      "`colA`",
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-desc" => [
+          "colA",
+          "colB"
+        ]
+      }
+    ],
+    [
+      [
+        "`colA` DESC"
+      ],
+      [
+        "`colB` DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colC"
+    ],
+    [
+      "`colC`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-desc" => "colC"
+      }
+    ],
+    [
+      [
+        "`colC` DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        {
+          "-desc" => [
+            "colA",
+            "colB"
+          ]
+        },
+        {
+          "-desc" => "colC"
+        }
+      ]
+    ],
+    [
+      [
+        "`colA` DESC"
+      ],
+      [
+        "`colB` DESC"
+      ],
+      [
+        "`colC` DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colA"
+    ],
+    [
+      "colA"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-asc" => "colA"
+      }
+    ],
+    [
+      [
+        "colA ASC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colB"
+    ],
+    [
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        "colB"
+      ]
+    ],
+    [
+      "colB"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-desc" => [
+          "colB"
+        ]
+      }
+    ],
+    [
+      [
+        "colB DESC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      "colC"
+    ],
+    [
+      "colC"
+    ]
+  ],
+  [
+    undef,
+    [
+      "colD"
+    ],
+    [
+      "colD"
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        "colC",
+        "colD"
+      ]
+    ],
+    [
+      "colC",
+      "colD"
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-asc" => [
+          "colC",
+          "colD"
+        ]
+      }
+    ],
+    [
+      [
+        "colC ASC"
+      ],
+      [
+        "colD ASC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        {
+          "-asc" => "colA"
+        },
+        {
+          "-desc" => [
+            "colB"
+          ]
+        },
+        {
+          "-asc" => [
+            "colC",
+            "colD"
+          ]
+        }
+      ]
+    ],
+    [
+      [
+        "colA ASC"
+      ],
+      [
+        "colB DESC"
+      ],
+      [
+        "colC ASC"
+      ],
+      [
+        "colD ASC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colA"
+    ],
+    [
+      "`colA`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-asc" => "colA"
+      }
+    ],
+    [
+      [
+        "`colA` ASC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colB"
+    ],
+    [
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        "colB"
+      ]
+    ],
+    [
+      "`colB`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-desc" => [
+          "colB"
+        ]
+      }
+    ],
+    [
+      [
+        "`colB` DESC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      "colC"
+    ],
+    [
+      "`colC`"
+    ]
+  ],
+  [
+    "`",
+    [
+      "colD"
+    ],
+    [
+      "`colD`"
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        "colC",
+        "colD"
+      ]
+    ],
+    [
+      "`colC`",
+      "`colD`"
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-asc" => [
+          "colC",
+          "colD"
+        ]
+      }
+    ],
+    [
+      [
+        "`colC` ASC"
+      ],
+      [
+        "`colD` ASC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        {
+          "-asc" => "colA"
+        },
+        {
+          "-desc" => [
+            "colB"
+          ]
+        },
+        {
+          "-asc" => [
+            "colC",
+            "colD"
+          ]
+        }
+      ]
+    ],
+    [
+      [
+        "`colA` ASC"
+      ],
+      [
+        "`colB` DESC"
+      ],
+      [
+        "`colC` ASC"
+      ],
+      [
+        "`colD` ASC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      \[
+          "colA LIKE ?",
+          "test"
+        ]
+    ],
+    [
+      [
+        "colA LIKE ?",
+        "test"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-desc" => \[
+            "colA LIKE ?",
+            "test"
+          ]
+      }
+    ],
+    [
+      [
+        "colA LIKE ? DESC",
+        "test"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      \[
+          "colA LIKE ?",
+          "test"
+        ]
+    ],
+    [
+      [
+        "colA LIKE ?",
+        "test"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-desc" => \[
+            "colA LIKE ?",
+            "test"
+          ]
+      }
+    ],
+    [
+      [
+        "colA LIKE ? DESC",
+        "test"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      \[
+          "colA LIKE ? DESC",
+          "test"
+        ]
+    ],
+    [
+      [
+        "colA LIKE ? DESC",
+        "test"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      \[
+          "colA LIKE ? DESC",
+          "test"
+        ]
+    ],
+    [
+      [
+        "colA LIKE ? DESC",
+        "test"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      \[
+          "colA"
+        ]
+    ],
+    [
+      [
+        "colA"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-asc" => \[
+            "colA"
+          ]
+      }
+    ],
+    [
+      [
+        "colA ASC"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      \[
+          "colB LIKE ?",
+          "test"
+        ]
+    ],
+    [
+      [
+        "colB LIKE ?",
+        "test"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-desc" => \[
+            "colB LIKE ?",
+            "test"
+          ]
+      }
+    ],
+    [
+      [
+        "colB LIKE ? DESC",
+        "test"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      \[
+          "colC LIKE ?",
+          "tost"
+        ]
+    ],
+    [
+      [
+        "colC LIKE ?",
+        "tost"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      {
+        "-asc" => \[
+            "colC LIKE ?",
+            "tost"
+          ]
+      }
+    ],
+    [
+      [
+        "colC LIKE ? ASC",
+        "tost"
+      ]
+    ]
+  ],
+  [
+    undef,
+    [
+      [
+        {
+          "-asc" => \[
+              "colA"
+            ]
+        },
+        {
+          "-desc" => \[
+              "colB LIKE ?",
+              "test"
+            ]
+        },
+        {
+          "-asc" => \[
+              "colC LIKE ?",
+              "tost"
+            ]
+        }
+      ]
+    ],
+    [
+      [
+        "colA ASC"
+      ],
+      [
+        "colB LIKE ? DESC",
+        "test"
+      ],
+      [
+        "colC LIKE ? ASC",
+        "tost"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      \[
+          "colA"
+        ]
+    ],
+    [
+      [
+        "colA"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-asc" => \[
+            "colA"
+          ]
+      }
+    ],
+    [
+      [
+        "colA ASC"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      \[
+          "colB LIKE ?",
+          "test"
+        ]
+    ],
+    [
+      [
+        "colB LIKE ?",
+        "test"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-desc" => \[
+            "colB LIKE ?",
+            "test"
+          ]
+      }
+    ],
+    [
+      [
+        "colB LIKE ? DESC",
+        "test"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      \[
+          "colC LIKE ?",
+          "tost"
+        ]
+    ],
+    [
+      [
+        "colC LIKE ?",
+        "tost"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      {
+        "-asc" => \[
+            "colC LIKE ?",
+            "tost"
+          ]
+      }
+    ],
+    [
+      [
+        "colC LIKE ? ASC",
+        "tost"
+      ]
+    ]
+  ],
+  [
+    "`",
+    [
+      [
+        {
+          "-asc" => \[
+              "colA"
+            ]
+        },
+        {
+          "-desc" => \[
+              "colB LIKE ?",
+              "test"
+            ]
+        },
+        {
+          "-asc" => \[
+              "colC LIKE ?",
+              "tost"
+            ]
+        }
+      ]
+    ],
+    [
+      [
+        "colA ASC"
+      ],
+      [
+        "colB LIKE ? DESC",
+        "test"
+      ],
+      [
+        "colC LIKE ? ASC",
+        "tost"
+      ]
+    ],
+  ],
+  [
+    undef,
+    [{}],
+    [],
+  ],
+);
+
+for my $case (@cases) {
+  my ($quote, $expr, $out) = @$case;
+  my $sqla = SQL::Abstract->new({ quote_char => $quote });
+
+  if (
+    @$expr == 1
+    and ref($expr->[0]) eq 'REF'
+    and ref(${$expr->[0]}) eq 'ARRAY'
+    and @${$expr->[0]} == 1
+  ) {
+    # \[ 'foo' ] is exactly equivalent to \'foo' and the new code knows that
+    $out = $out->[0];
+  }
+
+  my @chunks = $sqla->_order_by_chunks($expr);
+
+  unless (is(Dumper(\@chunks), Dumper($out))) {
+    diag("Above failure from expr: ".Dumper($expr));
+  }
+}
+
+done_testing;
diff --git a/t/80extra_clauses.t b/t/80extra_clauses.t
new file mode 100644 (file)
index 0000000..2f60558
--- /dev/null
@@ -0,0 +1,475 @@
+use strict;
+use warnings;
+use Test::More;
+use SQL::Abstract::Test import => [ qw(is_same_sql_bind is_same_sql) ];
+use SQL::Abstract;
+
+my $sqlac = SQL::Abstract->new->plugin('+ExtraClauses');
+
+is_deeply(
+  [ $sqlac->statement_list ],
+  [ sort qw(select update insert delete) ],
+);
+
+my ($sql, @bind) = $sqlac->select({
+  select => [ qw(artist.id artist.name), { -json_agg => 'cd' } ],
+  from => [
+    { artists => { -as => 'artist' } },
+    -join => [ cds => as => 'cd' => on => { 'cd.artist_id' => 'artist.id' } ],
+  ],
+  where => { 'artist.genres', => { '@>', { -value => [ 'Rock' ] } } },
+  order_by => 'artist.name',
+  group_by => 'artist.id',
+  having => { '>' => [ { -count => 'cd.id' }, 3 ] }
+});
+
+is_same_sql_bind(
+  $sql, \@bind,
+  q{
+    SELECT artist.id, artist.name, JSON_AGG(cd)
+    FROM artists AS artist JOIN cds AS cd ON cd.artist_id = artist.id
+    WHERE artist.genres @> ?
+    GROUP BY artist.id
+    HAVING COUNT(cd.id) > ?
+    ORDER BY artist.name
+  },
+  [ [ 'Rock' ], 3 ]
+);
+
+($sql) = $sqlac->select({
+  select => [ 'a' ],
+  from => [ { -values => [ [ 1, 2 ], [ 3, 4 ] ] }, -as => [ qw(t a b) ] ],
+});
+
+is_same_sql($sql, q{SELECT a FROM (VALUES (1, 2), (3, 4)) AS t(a,b)});
+
+($sql) = $sqlac->update({
+  update => 'employees',
+  set => { sales_count => { sales_count => { '+', \1 } } },
+  from => 'accounts',
+  where => {
+    'accounts.name' => { '=' => \"'Acme Corporation'" },
+    'employees.id' => { -ident => 'accounts.sales_person' },
+  }
+});
+
+is_same_sql(
+  $sql,
+  q{UPDATE employees SET sales_count = sales_count + 1 FROM accounts
+    WHERE accounts.name = 'Acme Corporation'
+    AND employees.id = accounts.sales_person
+  }
+);
+
+($sql) = $sqlac->update({
+  update => [ qw(tab1 tab2) ],
+  set => {
+    'tab1.column1' => { -ident => 'value1' },
+    'tab1.column2' => { -ident => 'value2' },
+  },
+  where => { 'tab1.id' => { -ident => 'tab2.id' } },
+});
+
+is_same_sql(
+  $sql,
+  q{UPDATE tab1, tab2 SET tab1.column1 = value1, tab1.column2 = value2
+     WHERE tab1.id = tab2.id}
+);
+
+is_same_sql(
+  $sqlac->delete({
+    from => 'x',
+    using => 'y',
+    where => { 'x.id' => { -ident => 'y.x_id' } }
+  }),
+  q{DELETE FROM x USING y WHERE x.id = y.x_id}
+);
+
+is_same_sql(
+  $sqlac->select({
+    select => [ 'x.*', 'y.*' ],
+    from => [ 'x', -join => [ 'y', using => 'y_id' ] ],
+  }),
+  q{SELECT x.*, y.* FROM x JOIN y USING (y_id)},
+);
+
+is_same_sql(
+  $sqlac->select({
+    select => 'x.*',
+    from => [ { -select => { select => '*', from => 'y' } }, -as => 'x' ],
+  }),
+  q{SELECT x.* FROM (SELECT * FROM y) AS x},
+);
+
+is_same_sql(
+  $sqlac->insert({
+    into => 'foo',
+    select => { select => '*', from => 'bar' }
+  }),
+  q{INSERT INTO foo SELECT * FROM bar}
+);
+
+($sql, @bind) = $sqlac->insert({
+  into => 'eh',
+  rowvalues => [ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] ]
+});
+
+is_same_sql_bind(
+  $sql, \@bind,
+  q{INSERT INTO eh VALUES (?, ?), (?, ?), (?, ?)},
+  [ 1..6 ],
+);
+
+is_same_sql(
+  $sqlac->select({
+    select => '*',
+    from => 'foo',
+    where => { -not_exists => {
+      -select => {
+        select => \1,
+        from => 'bar',
+        where => { 'foo.id' => { -ident => 'bar.foo_id' } }
+      },
+    } },
+  }),
+  q{SELECT * FROM foo
+    WHERE NOT EXISTS (SELECT 1 FROM bar WHERE foo.id = bar.foo_id)},
+);
+
+is_same_sql(
+  $sqlac->select({
+    select => '*',
+    from => 'foo',
+    where => { id => {
+      '=' => { -select => { select => { -max => 'id' }, from => 'foo' } }
+    } },
+  }),
+  q{SELECT * FROM foo WHERE id = (SELECT MAX(id) FROM foo)},
+);
+
+{
+  my $sqlac = $sqlac->clone
+                    ->clauses_of(
+                        select => (
+                          $sqlac->clauses_of('select'),
+                          qw(limit offset),
+                        )
+                      );
+
+  ($sql, @bind) = $sqlac->select({
+    select => '*',
+    from => 'foo',
+    limit => 10,
+    offset => 20,
+  });
+
+  is_same_sql_bind(
+    $sql, \@bind,
+    q{SELECT * FROM foo LIMIT ? OFFSET ?}, [ 10, 20 ]
+  );
+}
+
+$sql = $sqlac->select({
+  select => { -as => [ \1, 'x' ] },
+  union => { -select => { select => { -as => [ \2, 'x' ] } } },
+  order_by => { -desc => 'x' },
+});
+
+is_same_sql(
+  $sql,
+  q{(SELECT 1 AS x) UNION (SELECT 2 AS x) ORDER BY x DESC},
+);
+
+$sql = $sqlac->select({
+  select => '*',
+  from => 'foo',
+  except => { -select => { select => '*', from => 'foo_exclusions' } }
+});
+
+is_same_sql(
+  $sql,
+  q{(SELECT * FROM foo) EXCEPT (SELECT * FROM foo_exclusions)},
+);
+
+$sql = $sqlac->select({
+  with => [ foo => { -select => { select => \1 } } ],
+  select => '*',
+  from => 'foo'
+});
+
+is_same_sql(
+  $sql,
+  q{WITH foo AS (SELECT 1) SELECT * FROM foo},
+);
+
+$sql = $sqlac->update({
+  _ => [ 'tree_table', -join => {
+      to => { -select => {
+        with_recursive => [
+          [ tree_with_path => qw(id parent_id path) ],
+          { -select => {
+              _ => [
+                qw(id parent_id),
+                { -as => [
+                  { -cast => { -as => [ id => char => 255 ] } },
+                  'path'
+                ] },
+              ],
+              from => 'tree_table',
+              where => { parent_id => undef },
+              union_all => {
+                -select => {
+                  _ => [ qw(t.id t.parent_id),
+                         { -as => [
+                             { -concat => [ 'r.path', \q{'/'}, 't.id' ] },
+                             'path',
+                         ] },
+                       ],
+                  from => [
+                    tree_table => -as => t =>
+                    -join => {
+                      to => 'tree_with_path',
+                      as => 'r',
+                      on => { 't.parent_id' => 'r.id' },
+                    },
+                  ],
+               } },
+          } },
+        ],
+        select => '*',
+        from => 'tree_with_path'
+      } },
+      as => 'tree',
+      on => { 'tree.id' => 'tree_with_path.id' },
+  } ],
+  set => { path => { -ident => [ qw(tree path) ] } },
+});
+
+is_same_sql(
+  $sql,
+  q{
+    UPDATE tree_table JOIN (
+      WITH RECURSIVE tree_with_path(id, parent_id, path) AS (
+        (
+          SELECT id, parent_id, CAST(id AS char(255)) AS path
+          FROM tree_table
+          WHERE parent_id IS NULL
+        )
+        UNION ALL
+        (
+           SELECT t.id, t.parent_id, CONCAT(r.path, '/', t.id) AS path
+           FROM tree_table AS t
+           JOIN tree_with_path AS r ON t.parent_id = r.id
+        )
+      )
+      SELECT * FROM tree_with_path
+    ) AS tree
+    ON tree.id = tree_with_path.id
+    SET path = tree.path
+  },
+);
+
+
+($sql, @bind) = $sqlac->insert({
+  with => [
+    faculty => {
+      -select => {
+        _ => [qw /p.person p.email/],
+        from => [ person => -as => 'p' ],
+        where => {
+          'p.person_type' => 'faculty',
+          'p.person_status' => { '!=' => 'pending' },
+          'p.default_license_id' => undef,
+        },
+      },
+    },
+    grandfather => {
+      -insert => {
+        into => 'license',
+        fields => [ qw(kind expires_on valid_from) ],
+        select => {
+          select => [\(qw('grandfather' '2017-06-30' '2016-07-01'))],
+          from => 'faculty',
+        },
+        returning => 'license_id',
+      }
+    },
+  ],
+  into => 'license_person',
+  fields => [ qw(person_id license_id) ],
+  select => {
+    _ => ['person_id', 'license_id'],
+    from => ['grandfather'],
+    where => {
+      'a.index' => { -ident => 'b.index' },
+    },
+  },
+});
+
+is_same_sql_bind(
+  $sql, \@bind,
+  q{
+    WITH faculty AS (
+      SELECT p.person, p.email FROM person AS p
+      WHERE (
+        p.default_license_id IS NULL
+        AND p.person_status != ?
+        AND p.person_type = ?
+      )
+    ), grandfather AS (
+      INSERT INTO license (kind, expires_on, valid_from)
+      SELECT 'grandfather', '2017-06-30', '2016-07-01'
+        FROM faculty RETURNING license_id
+    ) INSERT INTO license_person (person_id, license_id)
+      SELECT person_id, license_id FROM grandfather WHERE a.index = b.index
+  },
+  [ qw(pending faculty) ],
+);
+
+
+($sql, @bind) = $sqlac->delete({
+  with => [
+    instructors => {
+      -select => {
+        _ => [qw/p.person_id email default_license_id/],
+        from => [
+          person => -as => 'p',
+          -join => {
+            to => 'license_person',
+            as => 'lp',
+            on => { 'lp.person_id' => 'p.person_id' },
+          },
+          -join => {
+            to => 'license',
+            as => 'l',
+            on => { 'l.license_id' => 'lp.license_id' },
+          },
+        ],
+        where => {
+          'p.person_type' => 'faculty',
+          'p.person_status' => { '!=' => 'pending' },
+          'l.kind' => 'pending',
+        },
+        group_by => [qw/ p.person_id /],
+        having => { '>' => [ { -count => 'l.license_id' }, 1 ] }
+      },
+    },
+    deletable_licenses => {
+      -select => {
+        _ => [qw/lp.ctid lp.person_id lp.license_id/],
+        from => [
+          instructors => -as => 'i',
+          -join => {
+            to => 'license_person',
+            as => 'lp',
+            on => { 'lp.person_id' => 'i.person_id' },
+          },
+          -join => {
+            to => 'license',
+            as => 'l',
+            on => { 'l.license_id' => 'lp.license_id' },
+          },
+        ],
+        where => {
+          'lp.license_id' => {
+            '<>' => {-ident => 'i.default_license_id'}
+          },
+          'l.kind' => 'pending',
+        },
+      },
+    },
+  ],
+  from => 'license_person',
+  where => {
+    ctid => { -in =>
+      {
+        -select => {
+          _ => ['ctid'],
+          from => 'deletable_licenses',
+        }
+      }
+    }
+  }
+});
+
+is_same_sql_bind(
+  $sql, \@bind,
+  q{
+    with instructors as (
+      select p.person_id, email, default_license_id
+      from person as p
+      join license_person as lp on lp.person_id = p.person_id
+      join license as l on l.license_id = lp.license_id
+      where l.kind = ?
+      AND p.person_status != ?
+      AND p.person_type = ?
+      group by p.person_id
+      having COUNT(l.license_id) > ?),
+    deletable_licenses as (
+      select lp.ctid, lp.person_id, lp.license_id
+      from instructors as i
+      join license_person as lp on lp.person_id = i.person_id
+      join license as l on l.license_id = lp.license_id
+      where l.kind = ?
+      and lp.license_id <> i.default_license_id
+    )
+    delete from license_person
+    where ctid IN (
+      (select ctid from deletable_licenses)
+    )
+  },
+  [qw(
+    pending pending faculty 1 pending
+    )]
+);
+
+($sql, @bind) = $sqlac->update({
+  _ => ['survey'],
+  set => {
+    license_id => { -ident => 'info.default_license_id' },
+  },
+  from => [
+    -select => {
+      select => [qw( s.survey_id p.default_license_id p.person_id)],
+      from => [
+        person => -as => 'p',
+        -join => {
+          to => 'class',
+          as => 'c',
+          on => { 'c.faculty_id' => 'p.person_id' },
+        },
+        -join => {
+          to => 'survey',
+          as => 's',
+          on => { 's.class_id' => 'c.class_id' },
+        },
+      ],
+      where => { 'p.institution_id' => { -value => 15031 } },
+    },
+    -as => 'info',
+  ],
+  where => {
+    'info.survey_id' => { -ident => 'survey.survey_id' },
+  }
+});
+
+is_same_sql_bind(
+  $sql, \@bind,
+  q{
+    update survey
+    set license_id=info.default_license_id
+    from (
+      select s.survey_id, p.default_license_id, p.person_id
+      from person AS p
+      join class AS c on c.faculty_id = p.person_id
+      join survey AS s on s.class_id = c.class_id
+      where p.institution_id = ?
+    ) AS info
+    where info.survey_id=survey.survey_id
+  },
+  [qw(
+    15031
+    )]
+);
+
+done_testing;
index 52f172b..657f28e 100644 (file)
@@ -25,6 +25,8 @@ my $exceptions = {
     )]},
     'SQL::Abstract::Tree' => { ignore => [qw(BUILDARGS)] },
     'SQL::Abstract::Test' => { skip => 1 },
+    'SQL::Abstract::Formatter' => { skip => 1 },
+    'SQL::Abstract::Parts' => { skip => 1 },
     'DBIx::Class::Storage::Debug::PrettyPrint' => { skip => 1 },
 };