+ $flattened = _collapse_indirect_filters($flattened);
+
+ my $all_filters = { %filters, %{ $params{filters} || {} } };
+ my $all_methods = { %methods, %{ $params{methods} || {} } };
+ my $all_complex = { %complex_methods, %{ $params{complex_methods} || {} } };
+
+ my @result;
+ for (my $i = 0; $i < scalar @$flattened; $i += 2) {
+ my (@args, @filters, $method);
+
+ my ($key, $value) = ($flattened->[$i], $flattened->[$i+1]);
+ my ($type, $op) = $key =~ m{:(.+)::(.+)};
+
+ my $is_multi = $key =~ s/:multi//;
+ my $is_any = $key =~ s/:any//;
+ my @value_tokens = $is_multi || $is_any ? parse_line('\s+', 0, $value) : ($value);
+
+ ($key, $method) = split m{::}, $key, 2;
+ ($key, @filters) = split m{:}, $key;
+
+ my $orig_key = $key;
+
+ for my $value_token (@value_tokens) {
+ $key = $orig_key;
+
+ $value_token = _apply($value_token, $_, $all_filters) for @filters;
+ $value_token = _apply($value_token, $method, $all_methods) if $method && exists $all_methods->{$method};
+ ($key, $value_token) = _apply_complex($key, $value_token, $method, $all_complex) if $method && exists $all_complex->{$method};
+ ($key, $value_token) = _dispatch_custom_filters($params{class}, $with_objects, $key, $value_token) if $params{class};
+ ($key, $value_token) = _apply_value_filters($key, $value_token, $type, $op);
+
+ push @args, $key, $value_token;
+ }
+
+ next unless defined $key;
+
+ push @result, $is_multi ? (and => [ @args ]) : $is_any ? (or => [ @args ]) : @args;
+ }
+ return \@result;
+}
+
+sub _apply_value_filters {
+ my ($key, $value, $type, $op) = @_;
+
+ return ($key, $value) unless $key && $value && $type && $op && (ref($value) eq 'HASH');
+
+ if (($type eq 'date') && ($op eq 'le')) {
+ my $date = delete $value->{le};
+ $value->{lt} = $date->add(days => 1);
+ }
+
+ return ($key, $value);
+}
+
+sub _dispatch_custom_filters {
+ my ($class, $with_objects, $key, $value) = @_;
+
+ # the key should by now have no filters left
+ # if it has, catch it here:
+ die 'unrecognized filters' if $key =~ /:/;
+
+ my @tokens = split /\./, $key;
+ my $curr_class = $class->object_class;
+
+ # our key will consist of dot-delimited tokens
+ # like this: order.part.unit.name
+ # each of these tokens except the last one is one of:
+ # - a relationship in the parent object
+ # - a custom filter
+ #
+ # the last token must be
+ # - a custom filter
+ # - a column in the parent object
+ #
+ # find first token which is not a relationship,
+ # so we can pass the rest on
+ my $i = 0;
+ while ($i < $#tokens) {
+ eval {
+ $curr_class = $curr_class->meta->relationship($tokens[$i])->class;
+ ++$i;
+ } or do {
+ last;
+ }
+ }
+
+ my $manager = $curr_class->meta->convention_manager->auto_manager_class_name;
+ my $obj_path = join '.', @tokens[0..$i-1];
+ my $obj_prefix = join '.', @tokens[0..$i-1], '';
+ my $key_token = $tokens[$i];
+ my @additional_tokens = @tokens[$i+1..$#tokens];
+
+ if ($manager->can('filter')) {
+ ($key, $value, my $obj) = $manager->filter($key_token, $value, $obj_prefix, $obj_path, @additional_tokens);
+ _add_uniq($with_objects, $obj) if $obj;
+ } else {
+ _add_uniq($with_objects, $obj_path) if $obj_path;
+ }
+
+ return ($key, $value);
+}
+
+sub _add_uniq {
+ my ($array, $what) = @_;
+
+ $array //= [];
+ @$array = (uniq @$array, listify($what));
+}
+
+sub _collapse_indirect_filters {
+ my ($flattened) = @_;
+
+ die 'flattened filter array length is uneven, should be possible to use as hash' if @$flattened % 2;
+
+ my (%keys_to_delete, %keys_to_move, @collapsed);
+
+ # search keys matching /::$/;
+ for (my $i = 0; $i < scalar @$flattened; $i += 2) {
+ my ($key, $value) = ($flattened->[$i], $flattened->[$i+1]);