package
DBIx::Class::Storage::DBIHacks;
sub
_prune_unused_joins {
my
(
$self
,
$attrs
) =
@_
;
return
(
$attrs
->{from}, {})
unless
(
ref
$attrs
->{from} eq
'ARRAY'
and
@{
$attrs
->{from}} > 1
and
ref
$attrs
->{from}[0] eq
'HASH'
and
ref
$attrs
->{from}[1] eq
'ARRAY'
and
$self
->_use_join_optimizer
);
my
$orig_aliastypes
=
$self
->_resolve_aliastypes_from_select_args(
$attrs
);
my
$new_aliastypes
= {
%$orig_aliastypes
};
my
@reclassify
=
'joining'
;
push
@reclassify
,
qw(multiplying premultiplied)
if
$attrs
->{_force_prune_multiplying_joins} or
$attrs
->{group_by};
delete
@{
$new_aliastypes
}{
@reclassify
};
my
@newfrom
=
$attrs
->{from}[0];
my
%need_joins
;
for
( @{
$new_aliastypes
}{
grep
{
$_
ne
'premultiplied'
}
keys
%$new_aliastypes
}) {
$need_joins
{
$_
} = 1
for
keys
%$_
;
$need_joins
{
$_
} = 1
for
map
{
values
%$_
}
map
{ @{
$_
->{-parents}} }
values
%$_
;
}
for
my
$j
(@{
$attrs
->{from}}[1..$
push
@newfrom
,
$j
if
(
(!
defined
$j
->[0]{-alias})
||
$need_joins
{
$j
->[0]{-alias}}
);
}
for
my
$ctype
(
@reclassify
) {
$new_aliastypes
->{
$ctype
} = {
map
{
$need_joins
{
$_
} ? (
$_
=>
$orig_aliastypes
->{
$ctype
}{
$_
} ) : () }
keys
%{
$orig_aliastypes
->{
$ctype
}}
}
}
return
( \
@newfrom
,
$new_aliastypes
);
}
sub
_adjust_select_args_for_complex_prefetch {
my
(
$self
,
$attrs
) =
@_
;
$self
->throw_exception (
'Complex prefetches are not supported on resultsets with a custom from attribute'
)
unless
(
ref
$attrs
->{from} eq
'ARRAY'
and
@{
$attrs
->{from}} > 1
and
ref
$attrs
->{from}[0] eq
'HASH'
and
ref
$attrs
->{from}[1] eq
'ARRAY'
);
my
$root_alias
=
$attrs
->{alias};
my
$outer_attrs
= {
%$attrs
};
delete
@{
$outer_attrs
}{
qw(from bind rows offset group_by _grouped_by_distinct having)
};
my
$inner_attrs
= {
%$attrs
,
_simple_passthrough_construction
=> 1 };
delete
@{
$inner_attrs
}{
qw(for collapse select as)
};
delete
$inner_attrs
->{order_by}
if
(
delete
$inner_attrs
->{_order_is_artificial}
or
!
$inner_attrs
->{rows}
);
$outer_attrs
->{
select
} = [ @{
$attrs
->{
select
}} ];
my
(
$root_node
,
$root_node_offset
);
for
my
$i
(0 .. $
my
$node
=
$inner_attrs
->{from}[
$i
];
my
$h
= (
ref
$node
eq
'HASH'
) ?
$node
: (
ref
$node
eq
'ARRAY'
and
ref
$node
->[0] eq
'HASH'
) ?
$node
->[0]
:
next
;
if
( (
$h
->{-alias}||
''
) eq
$root_alias
and
$h
->{-rsrc} ) {
$root_node
=
$h
;
$root_node_offset
=
$i
;
last
;
}
}
$self
->throw_exception (
'Complex prefetches are not supported on resultsets with a custom from attribute'
)
unless
$root_node
;
my
$colinfo
=
$self
->_resolve_column_info(
$inner_attrs
->{from});
my
$selected_root_columns
;
for
my
$i
(0 .. $
my
$sel
=
$outer_attrs
->{
select
}->[
$i
];
next
if
(
$colinfo
->{
$sel
} and
$colinfo
->{
$sel
}{-source_alias} ne
$root_alias
);
if
(
ref
$sel
eq
'HASH'
) {
$sel
->{-as} ||=
$attrs
->{as}[
$i
];
$outer_attrs
->{
select
}->[
$i
] =
join
(
'.'
,
$root_alias
, (
$sel
->{-as} ||
"inner_column_$i"
) );
}
elsif
(!
ref
$sel
and
my
$ci
=
$colinfo
->{
$sel
}) {
$selected_root_columns
->{
$ci
->{-colname}} = 1;
}
push
@{
$inner_attrs
->{
select
}},
$sel
;
push
@{
$inner_attrs
->{as}},
$attrs
->{as}[
$i
];
}
my
$connecting_aliastypes
=
$self
->_resolve_aliastypes_from_select_args({
%$inner_attrs
,
select
=> [],
});
for
(
sort
map
{
keys
%{
$_
->{-seen_columns}||{}} }
map
{
values
%$_
}
values
%$connecting_aliastypes
) {
my
$ci
=
$colinfo
->{
$_
} or
next
;
if
(
$ci
->{-source_alias} eq
$root_alias
and
!
$selected_root_columns
->{
$ci
->{-colname}}++
) {
push
@{
$inner_attrs
->{
select
}},
$ci
->{-fq_colname};
push
@{
$inner_attrs
->{as}},
$ci
->{-fq_colname};
}
}
my
$inner_subq
=
do
{
local
$self
->{_use_join_optimizer} = 1;
(
$inner_attrs
->{from},
my
$inner_aliastypes
) =
$self
->_prune_unused_joins ({
%$inner_attrs
,
_force_prune_multiplying_joins
=> 1
});
if
(
grep
{
$_
ne
$root_alias
}
keys
%{
$inner_aliastypes
->{multiplying} || {} }
and
( !
$inner_aliastypes
->{grouping} or
$inner_attrs
->{_grouped_by_distinct} )
) {
my
$cur_sel
= {
map
{
$_
=> 1 } @{
$inner_attrs
->{
select
}} };
my
$inner_select_with_extras
;
my
@pks
=
map
{
"$root_alias.$_"
}
$root_node
->{-rsrc}->primary_columns
or
$self
->throw_exception(
sprintf
'Unable to perform complex limited prefetch off %s without declared primary key'
,
$root_node
->{-rsrc}->source_name,
);
for
my
$col
(
@pks
) {
push
@{
$inner_select_with_extras
||= [ @{
$inner_attrs
->{
select
}} ] },
$col
unless
$cur_sel
->{
$col
}++;
}
(
$inner_attrs
->{group_by},
$inner_attrs
->{order_by}) =
$self
->_group_over_selection({
%$inner_attrs
,
$inner_select_with_extras
? (
select
=>
$inner_select_with_extras
) : (),
_aliastypes
=>
$inner_aliastypes
,
});
}
$self
->{_use_join_optimizer} = 0;
$self
->_select_args_to_query (
@{
$inner_attrs
}{
qw(from select where)
},
$inner_attrs
,
);
};
my
@orig_from
= @{
$attrs
->{from}};
$outer_attrs
->{from} = \
my
@outer_from
;
if
(
$root_node_offset
) {
@outer_from
=
splice
@orig_from
, 0,
$root_node_offset
;
push
@outer_from
, [
{
-alias
=>
$root_alias
,
-rsrc
=>
$root_node
->{-rsrc},
$root_alias
=>
$inner_subq
,
},
@{
$orig_from
[0]}[1 .. $
];
}
else
{
@outer_from
= {
-alias
=>
$root_alias
,
-rsrc
=>
$root_node
->{-rsrc},
$root_alias
=>
$inner_subq
,
};
}
shift
@orig_from
;
my
$outer_aliastypes
=
$outer_attrs
->{_aliastypes} =
$self
->_resolve_aliastypes_from_select_args({
%$outer_attrs
,
from
=> \
@orig_from
});
my
(
$outer_select_chain
,
@outer_nonselecting_chains
) =
map
{ +{
map
{
$_
=> 1 }
map
{
values
%$_
}
map
{ @{
$_
->{-parents}} }
values
%{
$outer_aliastypes
->{
$_
} || {} }
} }
qw/selecting restricting grouping ordering/
;
my
$may_need_outer_group_by
;
while
(
my
$j
=
shift
@orig_from
) {
my
$alias
=
$j
->[0]{-alias};
if
(
$outer_select_chain
->{
$alias
}
) {
push
@outer_from
,
$j
}
elsif
(first {
$_
->{
$alias
} }
@outer_nonselecting_chains
) {
push
@outer_from
,
$j
;
$may_need_outer_group_by
||=
$outer_aliastypes
->{multiplying}{
$alias
} ? 1 : 0;
}
}
if
(
$may_need_outer_group_by
and
$attrs
->{_grouped_by_distinct} ) {
(
$outer_attrs
->{group_by},
$outer_attrs
->{order_by}) =
$self
->_group_over_selection ({
%$outer_attrs
,
from
=> \
@outer_from
,
});
}
return
$outer_attrs
;
}
sub
_resolve_aliastypes_from_select_args {
my
(
$self
,
$attrs
) =
@_
;
$self
->throw_exception (
'Unable to analyze custom {from}'
)
if
ref
$attrs
->{from} ne
'ARRAY'
;
my
$aliases_by_type
;
my
$alias_list
;
for
my
$node
(@{
$attrs
->{from}}) {
my
$j
=
$node
;
$j
=
$j
->[0]
if
ref
$j
eq
'ARRAY'
;
my
$al
=
$j
->{-alias}
or
next
;
$alias_list
->{
$al
} =
$j
;
$aliases_by_type
->{multiplying}{
$al
} ||= {
-parents
=>
$j
->{-join_path}||[] }
if
ref
(
$node
) eq
'ARRAY'
and !
$j
->{-is_single};
$aliases_by_type
->{premultiplied}{
$al
} ||= {
-parents
=>
$j
->{-join_path}||[] }
if
grep
{
$aliases_by_type
->{multiplying}{
$_
} }
grep
{
$_
ne
$al
}
map
{
values
%$_
}
@{
$j
->{-join_path}||[] }
}
my
$colinfo
=
$self
->_resolve_column_info (
$attrs
->{from});
my
$sql_maker
=
$self
->sql_maker;
local
$sql_maker
->{where_bind};
local
$sql_maker
->{group_bind};
local
$sql_maker
->{having_bind};
local
$sql_maker
->{from_bind};
local
$sql_maker
->{quote_char} =
$sql_maker
->{quote_char};
local
$sql_maker
->{name_sep} =
$sql_maker
->{name_sep};
unless
(
defined
$sql_maker
->{quote_char} and
length
$sql_maker
->{quote_char}) {
$sql_maker
->{quote_char} = [
"\x00"
,
"\xFF"
];
$sql_maker
->{name_sep} =
''
;
}
my
(
$lquote
,
$rquote
,
$sep
) =
map
{
quotemeta
$_
} (
$sql_maker
->_quote_chars,
$sql_maker
->name_sep);
my
$to_scan
= {
restricting
=> [
(
$sql_maker
->_recurse_where (
$attrs
->{where}))[0],
$sql_maker
->_parse_rs_attrs ({
having
=>
$attrs
->{having} }),
],
grouping
=> [
$sql_maker
->_parse_rs_attrs ({
group_by
=>
$attrs
->{group_by} }),
],
joining
=> [
$sql_maker
->_recurse_from (
ref
$attrs
->{from}[0] eq
'ARRAY'
?
$attrs
->{from}[0][0] :
$attrs
->{from}[0],
@{
$attrs
->{from}}[1 .. $
),
],
selecting
=> [
map
{ (
$sql_maker
->_recurse_fields(
$_
))[0] } @{
$attrs
->{
select
}},
],
ordering
=> [
map
{
$_
->[0] }
$self
->_extract_order_criteria (
$attrs
->{order_by},
$sql_maker
),
],
};
for
my
$v
(
values
%$to_scan
) {
my
@nv
;
for
(
@$v
) {
next
if
(
!
defined
$_
or
(
ref
$_
eq
'ARRAY'
and
(
@$_
== 0 or
@$_
== 2 )
)
);
if
(
ref
$_
) {
$self
->throw_exception(
"Unexpected ref in scan-plan: "
. Data::Dumper::Concise::Dumper(
$v
) );
}
push
@nv
,
$_
;
}
$v
= \
@nv
;
}
$to_scan
->{selecting} = [
grep
{
$_
!~ / \A \s* \( \s* SELECT \s+ .+? \s+ FROM \s+ .+? \) \s* \z /xsi
} @{
$to_scan
->{selecting} || [] } ];
for
my
$type
(
keys
%$to_scan
) {
for
my
$piece
(@{
$to_scan
->{
$type
}}) {
if
(
$colinfo
->{
$piece
} and
my
$alias
=
$colinfo
->{
$piece
}{-source_alias}) {
$aliases_by_type
->{
$type
}{
$alias
} ||= {
-parents
=>
$alias_list
->{
$alias
}{-join_path}||[] };
$aliases_by_type
->{
$type
}{
$alias
}{-seen_columns}{
$colinfo
->{
$piece
}{-fq_colname}} =
$piece
;
}
}
}
for
my
$alias
(
keys
%$alias_list
) {
my
$al_re
=
qr/
$lquote $alias $rquote $sep (?: $lquote ([^$rquote]+) $rquote )?
|
\b $alias \. ([^\s\)\($rquote]+)?
/
x;
for
my
$type
(
keys
%$to_scan
) {
for
my
$piece
(@{
$to_scan
->{
$type
}}) {
if
(
my
@matches
=
$piece
=~ /
$al_re
/g) {
$aliases_by_type
->{
$type
}{
$alias
} ||= {
-parents
=>
$alias_list
->{
$alias
}{-join_path}||[] };
$aliases_by_type
->{
$type
}{
$alias
}{-seen_columns}{
"$alias.$_"
} =
"$alias.$_"
for
grep
{
defined
$_
}
@matches
;
}
}
}
}
for
my
$col
(
keys
%$colinfo
) {
next
if
$col
=~ / \. /x;
my
$col_re
=
qr/ $lquote ($col) $rquote /
x;
for
my
$type
(
keys
%$to_scan
) {
for
my
$piece
(@{
$to_scan
->{
$type
}}) {
if
(
my
@matches
=
$piece
=~ /
$col_re
/g) {
my
$alias
=
$colinfo
->{
$col
}{-source_alias};
$aliases_by_type
->{
$type
}{
$alias
} ||= {
-parents
=>
$alias_list
->{
$alias
}{-join_path}||[] };
$aliases_by_type
->{
$type
}{
$alias
}{-seen_columns}{
"$alias.$_"
} =
$_
for
grep
{
defined
$_
}
@matches
;
}
}
}
}
for
my
$j
(
values
%$alias_list
) {
my
$alias
=
$j
->{-alias} or
next
;
$aliases_by_type
->{restricting}{
$alias
} ||= {
-parents
=>
$j
->{-join_path}||[] }
if
(
(not
$j
->{-join_type})
or
(
$j
->{-join_type} !~ /^left (?: \s+ outer)? $/xi)
);
}
for
(
keys
%$aliases_by_type
) {
delete
$aliases_by_type
->{
$_
}
unless
keys
%{
$aliases_by_type
->{
$_
}};
}
return
$aliases_by_type
;
}
sub
_group_over_selection {
my
(
$self
,
$attrs
) =
@_
;
my
$colinfos
=
$self
->_resolve_column_info (
$attrs
->{from});
my
(
@group_by
,
%group_index
);
for
(@{
$attrs
->{
select
}}) {
if
(!
ref
(
$_
) or
ref
(
$_
) ne
'HASH'
) {
push
@group_by
,
$_
;
$group_index
{
$_
}++;
if
(
$colinfos
->{
$_
} and
$_
!~ /\./ ) {
$group_index
{
"$colinfos->{$_}{-source_alias}.$_"
}++;
}
}
}
my
@order_by
=
$self
->_extract_order_criteria(
$attrs
->{order_by})
or
return
(\
@group_by
,
$attrs
->{order_by});
my
(
$leftovers
,
$sql_maker
,
@new_order_by
,
$order_chunks
,
$aliastypes
);
my
$group_already_unique
=
$self
->_columns_comprise_identifying_set(
$colinfos
, \
@group_by
);
for
my
$o_idx
(0 ..
$#order_by
) {
next
if
$order_by
[
$o_idx
][0] =~ /^ (?: min | max ) \s* \( .+ \) $/ix;
my
$chunk_ci
;
if
(
@{
$order_by
[
$o_idx
]} != 1
or
( ! (
$chunk_ci
=
$colinfos
->{
$order_by
[
$o_idx
][0]} ) and
$attrs
->{_aliastypes} )
) {
push
@$leftovers
,
$order_by
[
$o_idx
][0];
}
next
unless
$chunk_ci
;
next
if
$group_index
{
$chunk_ci
->{-fq_colname}};
$aliastypes
||= (
$attrs
->{_aliastypes}
or
$self
->_resolve_aliastypes_from_select_args({
from
=>
$attrs
->{from},
order_by
=>
$attrs
->{order_by},
})
)
if
$group_already_unique
;
if
(
$group_already_unique
and
!
$aliastypes
->{multiplying}{
$chunk_ci
->{-source_alias}}
and
!
$aliastypes
->{premultiplied}{
$chunk_ci
->{-source_alias}}
) {
push
@group_by
,
$chunk_ci
->{-fq_colname};
$group_index
{
$chunk_ci
->{-fq_colname}}++
}
else
{
$sql_maker
||=
$self
->sql_maker;
$order_chunks
||= [
map
{
ref
$_
eq
'ARRAY'
?
$_
: [
$_
] }
$sql_maker
->_order_by_chunks(
$attrs
->{order_by})
];
my
(
$chunk
,
$is_desc
) =
$sql_maker
->_split_order_chunk(
$order_chunks
->[
$o_idx
][0]);
$new_order_by
[
$o_idx
] = \[
sprintf
(
'%s( %s )%s'
,
(
$is_desc
?
'MAX'
:
'MIN'
),
$chunk
,
(
$is_desc
?
' DESC'
:
''
),
),
@ {
$order_chunks
->[
$o_idx
]} [ 1 .. $
];
}
}
$self
->throw_exception (
sprintf
'Unable to programatically derive a required group_by from the supplied '
.
'order_by criteria. To proceed either add an explicit group_by, or '
.
'simplify your order_by to only include plain columns '
.
'(supplied order_by: %s)'
,
join
', '
,
map
{
"'$_'"
}
@$leftovers
,
)
if
$leftovers
;
if
(
@new_order_by
) {
$new_order_by
[
$_
] ||= \
$order_chunks
->[
$_
]
for
( 0 ..
$#$order_chunks
);
}
return
(
\
@group_by
,
(
@new_order_by
? \
@new_order_by
:
$attrs
->{order_by} ),
);
}
sub
_resolve_ident_sources {
my
(
$self
,
$ident
) =
@_
;
my
$alias2source
= {};
if
( blessed
$ident
&&
$ident
->isa(
"DBIx::Class::ResultSource"
) ) {
$alias2source
->{me} =
$ident
;
}
elsif
(
ref
$ident
eq
'ARRAY'
) {
for
(
@$ident
) {
my
$tabinfo
;
if
(
ref
$_
eq
'HASH'
) {
$tabinfo
=
$_
;
}
if
(
ref
$_
eq
'ARRAY'
and
ref
$_
->[0] eq
'HASH'
) {
$tabinfo
=
$_
->[0];
}
$alias2source
->{
$tabinfo
->{-alias}} =
$tabinfo
->{-rsrc}
if
(
$tabinfo
->{-rsrc});
}
}
return
$alias2source
;
}
sub
_resolve_column_info {
my
(
$self
,
$ident
,
$colnames
) =
@_
;
return
{}
if
$colnames
and !
@$colnames
;
my
$alias2src
=
$self
->_resolve_ident_sources(
$ident
);
my
(
%seen_cols
,
@auto_colnames
);
for
my
$alias
(
keys
%$alias2src
) {
my
$rsrc
=
$alias2src
->{
$alias
};
for
my
$colname
(
$rsrc
->columns) {
push
@{
$seen_cols
{
$colname
}},
$alias
;
push
@auto_colnames
,
"$alias.$colname"
unless
$colnames
;
}
}
$colnames
||= [
@auto_colnames
,
grep
{ @{
$seen_cols
{
$_
}} == 1 } (
keys
%seen_cols
),
];
my
(
%return
,
$colinfos
);
foreach
my
$col
(
@$colnames
) {
my
(
$source_alias
,
$colname
) =
$col
=~ m/^ (?: ([^\.]+) \. )? (.+) $/x;
$source_alias
||=
$seen_cols
{
$colname
}[0]
if
(
$seen_cols
{
$colname
} and @{
$seen_cols
{
$colname
}} == 1);
next
unless
$source_alias
;
my
$rsrc
=
$alias2src
->{
$source_alias
}
or
next
;
$return
{
$col
} = {
%{
(
$colinfos
->{
$source_alias
} ||=
$rsrc
->columns_info )->{
$colname
}
||
$self
->throw_exception(
"No such column '$colname' on source "
.
$rsrc
->source_name
);
},
-result_source
=>
$rsrc
,
-source_alias
=>
$source_alias
,
-fq_colname
=>
$col
eq
$colname
?
"$source_alias.$col"
:
$col
,
-colname
=>
$colname
,
};
$return
{
"$source_alias.$colname"
} =
$return
{
$col
}
if
$col
eq
$colname
;
}
return
\
%return
;
}
sub
_inner_join_to_node {
my
(
$self
,
$from
,
$alias
) =
@_
;
my
$switch_branch
=
$self
->_find_join_path_to_node(
$from
,
$alias
);
return
$from
unless
@{
$switch_branch
||[]};
my
@new_from
= (
$from
->[0]);
my
$sw_idx
= {
map
{ (
values
%$_
), 1 }
@$switch_branch
};
for
my
$j
(@{
$from
}[1 ..
$#$from
]) {
my
$jalias
=
$j
->[0]{-alias};
if
(
$sw_idx
->{
$jalias
}) {
my
%attrs
= %{
$j
->[0]};
delete
$attrs
{-join_type};
push
@new_from
, [
\
%attrs
,
@{
$j
}[ 1 ..
$#$j
],
];
}
else
{
push
@new_from
,
$j
;
}
}
return
\
@new_from
;
}
sub
_find_join_path_to_node {
my
(
$self
,
$from
,
$target_alias
) =
@_
;
return
undef
if
(
ref
$from
ne
'ARRAY'
||
ref
$from
->[0] ne
'HASH'
||
!
defined
$from
->[0]{-alias}
);
return
[]
if
$from
->[0]{-alias} eq
$target_alias
;
for
my
$i
(1 ..
$#$from
) {
return
$from
->[
$i
][0]{-join_path}
if
( (
$from
->[
$i
][0]{-alias}||
''
) eq
$target_alias
);
}
return
undef
;
}
sub
_extract_order_criteria {
my
(
$self
,
$order_by
,
$sql_maker
) =
@_
;
my
$parser
=
sub
{
my
(
$sql_maker
,
$order_by
,
$orig_quote_chars
) =
@_
;
return
scalar
$sql_maker
->_order_by_chunks (
$order_by
)
unless
wantarray
;
my
(
$lq
,
$rq
,
$sep
) =
map
{
quotemeta
(
$_
) } (
(
$orig_quote_chars
?
@$orig_quote_chars
:
$sql_maker
->_quote_chars),
$sql_maker
->name_sep
);
my
@chunks
;
for
(
$sql_maker
->_order_by_chunks (
$order_by
) ) {
my
$chunk
=
ref
$_
? [
@$_
] : [
$_
];
(
$chunk
->[0]) =
$sql_maker
->_split_order_chunk(
$chunk
->[0]);
$chunk
->[0] =~ s/^
$lq
(.+?)
$rq
$sep
$lq
(.+?)
$rq
$/
"$1.$2"
/xe
or
$chunk
->[0] =~ s/^
$lq
(.+)
$rq
$/$1/x;
push
@chunks
,
$chunk
;
}
return
@chunks
;
};
if
(
$sql_maker
) {
return
$parser
->(
$sql_maker
,
$order_by
);
}
else
{
$sql_maker
=
$self
->sql_maker;
my
$orig_quote_chars
= [
$sql_maker
->_quote_chars];
local
$sql_maker
->{quote_char};
return
$parser
->(
$sql_maker
,
$order_by
,
$orig_quote_chars
);
}
}
sub
_order_by_is_stable {
my
(
$self
,
$ident
,
$order_by
,
$where
) =
@_
;
my
@cols
= (
(
map
{
$_
->[0] }
$self
->_extract_order_criteria(
$order_by
) ),
(
$where
?
keys
%{
$self
->_extract_fixed_condition_columns(
$where
) } : () ),
) or
return
0;
my
$colinfo
=
$self
->_resolve_column_info(
$ident
, \
@cols
);
return
keys
%$colinfo
?
$self
->_columns_comprise_identifying_set(
$colinfo
, \
@cols
)
: 0
;
}
sub
_columns_comprise_identifying_set {
my
(
$self
,
$colinfo
,
$columns
) =
@_
;
my
$cols_per_src
;
$cols_per_src
-> {
$_
->{-source_alias}} -> {
$_
->{-colname}} =
$_
for
grep
{
defined
$_
} @{
$colinfo
}{
@$columns
};
for
(
values
%$cols_per_src
) {
my
$src
= (
values
%$_
)[0]->{-result_source};
return
1
if
$src
->_identifying_column_set(
$_
);
}
return
0;
}
sub
_extract_colinfo_of_stable_main_source_order_by_portion {
my
(
$self
,
$attrs
) =
@_
;
my
$nodes
=
$self
->_find_join_path_to_node(
$attrs
->{from},
$attrs
->{alias});
return
unless
defined
$nodes
;
my
@ord_cols
=
map
{
$_
->[0] }
(
$self
->_extract_order_criteria(
$attrs
->{order_by}) )
;
return
unless
@ord_cols
;
my
$valid_aliases
= {
map
{
$_
=> 1 } (
$attrs
->{from}[0]{-alias},
map
{
values
%$_
}
@$nodes
,
) };
my
$colinfos
=
$self
->_resolve_column_info(
$attrs
->{from});
my
(
$colinfos_to_return
,
$seen_main_src_cols
);
for
my
$col
(
@ord_cols
) {
my
$colinfo
=
$colinfos
->{
$col
} or
last
;
last
unless
$valid_aliases
->{
$colinfo
->{-source_alias}};
$colinfos_to_return
->{
$col
} =
$colinfo
;
$seen_main_src_cols
->{
$colinfo
->{-colname}} = 1
if
$colinfo
->{-source_alias} eq
$attrs
->{alias};
}
return
unless
$seen_main_src_cols
;
my
$main_src_fixed_cols_from_cond
= [
$attrs
->{where}
? (
map
{
(
$colinfos
->{
$_
} and
$colinfos
->{
$_
}{-source_alias} eq
$attrs
->{alias} )
?
$colinfos
->{
$_
}{-colname}
: ()
}
keys
%{
$self
->_extract_fixed_condition_columns(
$attrs
->{where}) }
)
: ()
];
return
$attrs
->{result_source}->_identifying_column_set([
keys
%$seen_main_src_cols
,
@$main_src_fixed_cols_from_cond
,
]) ?
$colinfos_to_return
: ();
}
sub
_collapse_cond {
my
(
$self
,
$where
,
$where_is_anded_array
) =
@_
;
my
$fin
;
if
(!
$where
) {
return
;
}
elsif
(
$where_is_anded_array
or
ref
$where
eq
'HASH'
) {
my
@pairs
;
my
@pieces
=
$where_is_anded_array
?
@$where
:
$where
;
while
(
@pieces
) {
my
$chunk
=
shift
@pieces
;
if
(
ref
$chunk
eq
'HASH'
) {
for
(
sort
keys
%$chunk
) {
if
(
$_
eq
''
) {
is_literal_value(
$chunk
->{
$_
})
? carp
'Hash-pairs consisting of an empty string with a literal are deprecated, use -and => [ $literal ] instead'
:
$self
->throw_exception(
"Supplying an empty left hand side argument is not supported in hash-pairs"
)
;
}
push
@pairs
,
$_
=>
$chunk
->{
$_
};
}
}
elsif
(
ref
$chunk
eq
'ARRAY'
) {
push
@pairs
,
-or
=>
$chunk
if
@$chunk
;
}
elsif
( !
length
ref
$chunk
) {
$self
->throw_exception(
"Supplying an empty left hand side argument is not supported in array-pairs"
)
if
$where_is_anded_array
and (!
defined
$chunk
or
$chunk
eq
''
);
push
@pairs
,
$chunk
,
shift
@pieces
;
}
else
{
push
@pairs
,
''
,
$chunk
;
}
}
return
unless
@pairs
;
my
@conds
=
$self
->_collapse_cond_unroll_pairs(\
@pairs
)
or
return
;
for
my
$c
(
@conds
) {
if
(
ref
$c
ne
'HASH'
) {
push
@{
$fin
->{-and}},
$c
;
}
else
{
for
my
$col
(
sort
keys
%$c
) {
if
(
$col
=~ /^\-and$/i) {
push
@{
$fin
->{-and}},
ref
$c
->{
$col
} eq
'ARRAY'
? @{
$c
->{
$col
}}
:
ref
$c
->{
$col
} eq
'HASH'
? %{
$c
->{
$col
}}
: {
$col
=>
$c
->{
$col
} }
;
}
elsif
(
$col
=~ /^\-/) {
push
@{
$fin
->{-and}}, {
$col
=>
$c
->{
$col
} };
}
elsif
(
exists
$fin
->{
$col
}) {
$fin
->{
$col
} = [
-and
=>
map
{
(
ref
$_
eq
'ARRAY'
and (
$_
->[0]||
''
) =~ /^\-and$/i )
? @{
$_
}[1..
$#$_
]
:
$_
;
} (
$fin
->{
$col
},
$c
->{
$col
}) ];
}
else
{
$fin
->{
$col
} =
$c
->{
$col
};
}
}
}
}
}
elsif
(
ref
$where
eq
'ARRAY'
) {
my
$fin_idx
;
for
(
my
$i
= 0;
$i
<=
$#$where
;
$i
++ ) {
$self
->throw_exception(
"Supplying an empty left hand side argument is not supported in array-pairs"
)
if
(!
defined
$where
->[
$i
] or !
length
$where
->[
$i
]);
my
$logic_mod
=
lc
( (
$where
->[
$i
] =~ /^(\-(?:and|or))$/i)[0] ||
''
);
if
(
$logic_mod
) {
$i
++;
$self
->throw_exception(
"Unsupported top-level op/arg pair: [ $logic_mod => $where->[$i] ]"
)
unless
ref
$where
->[
$i
] eq
'HASH'
or
ref
$where
->[
$i
] eq
'ARRAY'
;
my
$sub_elt
=
$self
->_collapse_cond({
$logic_mod
=>
$where
->[
$i
] })
or
next
;
my
@keys
=
keys
%$sub_elt
;
if
(
@keys
== 1 and
$keys
[0] !~ /^\-/ ) {
$fin_idx
->{
"COL_$keys[0]_"
. serialize
$sub_elt
} =
$sub_elt
;
}
else
{
$fin_idx
->{
"SER_"
. serialize
$sub_elt
} =
$sub_elt
;
}
}
elsif
(!
length
ref
$where
->[
$i
] ) {
my
$sub_elt
=
$self
->_collapse_cond({ @{
$where
}[
$i
,
$i
+1] })
or
next
;
$fin_idx
->{
"COL_$where->[$i]_"
. serialize
$sub_elt
} =
$sub_elt
;
$i
++;
}
else
{
$fin_idx
->{
"SER_"
. serialize
$where
->[
$i
] } =
$self
->_collapse_cond(
$where
->[
$i
] ) ||
next
;
}
}
if
(!
$fin_idx
) {
return
;
}
elsif
(
keys
%$fin_idx
== 1 ) {
$fin
= (
values
%$fin_idx
)[0];
}
else
{
my
@or
;
for
(
sort
keys
%$fin_idx
) {
if
(
ref
$fin_idx
->{
$_
} eq
'HASH'
and
keys
%{
$fin_idx
->{
$_
}} == 1 ) {
my
(
$l
,
$r
) = %{
$fin_idx
->{
$_
}};
if
(
ref
$r
eq
'ARRAY'
and
(
(
@$r
== 1 and
$l
=~ /^\-and$/i )
or
$l
=~ /^\-or$/i
)
) {
push
@or
,
@$r
}
elsif
(
ref
$r
eq
'HASH'
and
keys
%$r
== 1
and
$l
=~ /^\-(?:and|or)$/i
) {
push
@or
,
%$r
;
}
else
{
push
@or
,
$l
,
$r
;
}
}
else
{
push
@or
,
$fin_idx
->{
$_
};
}
}
$fin
->{-or} = \
@or
;
}
}
else
{
$fin
= {
-and
=> [
$where
] };
}
while
(
$fin
->{-and}
and
@{
$fin
->{-and}} < 2
) {
my
$and
=
delete
$fin
->{-and};
last
if
@$and
== 0;
if
(
ref
$and
->[0] eq
'HASH'
and
!
grep
{
exists
$fin
->{
$_
} }
keys
%{
$and
->[0]}
) {
$fin
= {
%$fin
, %{
$and
->[0]}
};
}
else
{
$fin
->{-and} =
$and
;
last
;
}
}
for
my
$col
(
grep
{
$_
!~ /^\-/ }
keys
%$fin
) {
next
unless
ref
$fin
->{
$col
} eq
'ARRAY'
and (
$fin
->{
$col
}[0]||
''
) =~ /^\-and$/i;
my
$val_bag
= {
map
{
(!
defined
$_
) ? (
UNDEF
=>
undef
)
: ( !
length
ref
$_
or is_plain_value
$_
) ? (
"VAL_$_"
=>
$_
)
: ( (
'SER_'
. serialize
$_
) =>
$_
)
} @{
$fin
->{
$col
}}[1 .. $
if
(
keys
%$val_bag
== 1 ) {
(
$fin
->{
$col
}) =
values
%$val_bag
;
}
else
{
$fin
->{
$col
} = [
-and
=>
map
{
$val_bag
->{
$_
} }
sort
keys
%$val_bag
];
}
}
return
keys
%$fin
?
$fin
: ();
}
sub
_collapse_cond_unroll_pairs {
my
(
$self
,
$pairs
) =
@_
;
my
@conds
;
while
(
@$pairs
) {
my
(
$lhs
,
$rhs
) =
splice
@$pairs
, 0, 2;
if
(
$lhs
eq
''
) {
push
@conds
,
$self
->_collapse_cond(
$rhs
);
}
elsif
(
$lhs
=~ /^\-and$/i ) {
push
@conds
,
$self
->_collapse_cond(
$rhs
, (
ref
$rhs
eq
'ARRAY'
));
}
elsif
(
$lhs
=~ /^\-or$/i ) {
push
@conds
,
$self
->_collapse_cond(
(
ref
$rhs
eq
'HASH'
) ? [
map
{
$_
=>
$rhs
->{
$_
} }
sort
keys
%$rhs
] :
$rhs
);
}
else
{
if
(
ref
$rhs
eq
'HASH'
and !
keys
%$rhs
) {
}
elsif
(
ref
$rhs
eq
'HASH'
and
keys
%$rhs
== 1 and
exists
$rhs
->{-ident}) {
push
@conds
, {
$lhs
=> {
'='
,
$rhs
} };
}
elsif
(
ref
$rhs
eq
'HASH'
and
keys
%$rhs
== 1 and
exists
$rhs
->{-value} and is_plain_value
$rhs
->{-value}) {
push
@conds
, {
$lhs
=>
$rhs
->{-value} };
}
elsif
(
ref
$rhs
eq
'HASH'
and
keys
%$rhs
== 1 and
exists
$rhs
->{
'='
}) {
if
(
length
ref
$rhs
->{
'='
} and is_literal_value
$rhs
->{
'='
} ) {
push
@conds
, {
$lhs
=>
$rhs
};
}
else
{
for
my
$p
(
$self
->_collapse_cond_unroll_pairs([
$lhs
=>
$rhs
->{
'='
} ])) {
if
(
keys
%$p
> 1) {
local
$Data::Dumper::Deepcopy
= 1;
$self
->throw_exception(
"Internal error: unexpected collapse unroll:"
. Data::Dumper::Concise::Dumper {
in
=> {
$lhs
=>
$rhs
},
out
=>
$p
}
);
}
my
(
$l
,
$r
) =
%$p
;
push
@conds
, (
!
length
ref
$r
or
ref
$r
eq
'HASH'
and
keys
%$rhs
== 1 and
exists
$rhs
->{
'='
}
or
is_plain_value(
$r
)
)
? {
$l
=>
$r
}
: {
$l
=> {
'='
=>
$r
} }
;
}
}
}
elsif
(
ref
$rhs
eq
'ARRAY'
) {
if
(!
@$rhs
) {
push
@conds
, {
$lhs
=> [] };
}
elsif
( (
$rhs
->[0]||
''
) =~ /^\-(?:and|or)$/i ) {
$self
->throw_exception(
"Value modifier not followed by any values: $lhs => [ $rhs->[0] ] "
)
if
@$rhs
== 1;
if
(
$rhs
->[0] =~ /^\-and$/i ) {
unshift
@$pairs
,
map
{
$lhs
=>
$_
} @{
$rhs
}[1..
$#$rhs
];
}
elsif
(
@$rhs
== 2) {
unshift
@$pairs
,
$lhs
=>
$rhs
->[1];
}
else
{
push
@conds
, {
$lhs
=> [ @{
$rhs
}[1..
$#$rhs
] ] };
}
}
elsif
(
@$rhs
== 1) {
unshift
@$pairs
,
$lhs
=>
$rhs
->[0];
}
else
{
push
@conds
, {
$lhs
=>
$rhs
};
}
}
elsif
(
ref
$rhs
eq
'HASH'
and
(
my
(
$subop
) =
keys
%$rhs
) == 1
and
length
ref
((
values
%$rhs
)[0])
and
my
$vref
= is_plain_value( (
values
%$rhs
)[0] )
) {
push
@conds
, {
$lhs
=> {
$subop
=>
$$vref
} }
}
else
{
push
@conds
, {
$lhs
=>
$rhs
};
}
}
}
return
@conds
;
}
sub
_extract_fixed_condition_columns {
my
(
$self
,
$where
,
$consider_nulls
) =
@_
;
my
$where_hash
=
$self
->_collapse_cond(
$_
[1]);
my
$res
= {};
my
(
$c
,
$v
);
for
$c
(
keys
%$where_hash
) {
my
$vals
;
if
(!
defined
(
$v
=
$where_hash
->{
$c
}) ) {
$vals
->{UNDEF} =
$v
if
$consider_nulls
}
elsif
(
ref
$v
eq
'HASH'
and
keys
%$v
== 1
) {
if
(
exists
$v
->{-value}) {
if
(
defined
$v
->{-value}) {
$vals
->{
"VAL_$v->{-value}"
} =
$v
->{-value}
}
elsif
(
$consider_nulls
) {
$vals
->{UNDEF} =
$v
->{-value};
}
}
elsif
(
length
ref
$v
->{
'='
}
and
(
(
ref
$v
->{
'='
} eq
'HASH'
and
keys
%{
$v
->{
'='
}} == 1 and
exists
$v
->{
'='
}{-ident} )
or
is_literal_value(
$v
->{
'='
})
)
) {
$vals
->{
'SER_'
. serialize
$v
->{
'='
} } =
$v
->{
'='
};
}
}
elsif
(
!
length
ref
$v
or
is_plain_value (
$v
)
) {
$vals
->{
"VAL_$v"
} =
$v
;
}
elsif
(
ref
$v
eq
'ARRAY'
and (
$v
->[0]||
''
) eq
'-and'
) {
for
( @{
$v
}[1..
$#$v
] ) {
my
$subval
=
$self
->_extract_fixed_condition_columns({
$c
=>
$_
},
'consider nulls'
);
next
unless
exists
$subval
->{
$c
};
$vals
->{
!
defined
$subval
->{
$c
} ?
'UNDEF'
: ( !
length
ref
$subval
->{
$c
} or is_plain_value
$subval
->{
$c
} ) ?
"VAL_$subval->{$c}"
: (
'SER_'
. serialize
$subval
->{
$c
} )
} =
$subval
->{
$c
};
}
}
if
(
keys
%$vals
== 1) {
(
$res
->{
$c
}) = (
values
%$vals
)
unless
!
$consider_nulls
and
exists
$vals
->{UNDEF};
}
elsif
(
keys
%$vals
> 1) {
$res
->{
$c
} = UNRESOLVABLE_CONDITION;
}
}
$res
;
}
1;