The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

use strict;
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
);
}
}
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