Bug 43241: Fix datetime/boolean conversion in q= queries with DBIC operators

_parse_dbic_query maps field names but did not convert values. When
datetime or boolean filters are nested inside DBIC operators (-and,
-or) in the q= parameter, attributes_from_api (which normally handles
the conversion via _recursive_fixup) cannot reach them because it only
iterates top-level keys.

This patch fixes the issue in two places:

1. _parse_dbic_query (Query.pm): When a key maps to a known column
   (on the primary table or a related object), call _recursive_fixup
   to convert RFC3339 timestamps to MySQL format and booleans to 0/1.
   Related object instances are cached on the result_set to avoid
   repeated prefetch_whitelist lookups during recursive parsing.

2. Objects.pm: Move attributes_from_api to process named query params
   before merging with q= params. This avoids double-processing since
   q= values are already fixed up by _parse_dbic_query.

Test plan:
1. Apply the regression tests from the previous commit
2. Run:
   $ prove t/Koha/REST/Plugin/Query.t
   $ prove t/db_dependent/Koha/REST/Plugin/Objects.t
=> FAIL: Tests fail!
3. Apply this patch
4. Repeat step 2
=> SUCCESS: Tests pass!
5. Sign off :-D

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
This commit is contained in:
Tomás Cohen Arazi 2026-08-06 10:45:31 -03:00 committed by Pedro Amorim
parent 811e57c441
commit 450e6bf178
No known key found for this signature in database
GPG Key ID: 06B75BA7F3894D55
2 changed files with 70 additions and 6 deletions

View File

@ -262,6 +262,7 @@ controller, and thus shouldn't be called twice in it.
# Apply the mapping function to the passed params
if ( defined $filtered_params ) {
$filtered_params = $c->build_query_params( $filtered_params, $reserved_params );
$filtered_params = $result_set->attributes_from_api($filtered_params);
}
if ($query_params) {
@ -271,10 +272,6 @@ controller, and thus shouldn't be called twice in it.
);
}
if ( defined $filtered_params ) {
$filtered_params = $result_set->attributes_from_api($filtered_params);
}
$c->dbic_validate_operators( { filtered_params => $filtered_params } );
# Generate the resultset

View File

@ -769,8 +769,25 @@ sub _parse_dbic_query {
$parsed_hash->{$key} = _from_api_param( $q_params->{$key}, $result_set );
next;
}
my $k = _from_api_param( $key, $result_set );
$parsed_hash->{$k} = _parse_dbic_query( $q_params->{$key}, $result_set );
my $k = _from_api_param( $key, $result_set );
my $value = $q_params->{$key};
# Apply datetime/boolean fixup when the key maps to a known column.
# This is needed because values inside q= queries bypass attributes_from_api
# (which normally calls _recursive_fixup) when nested within DBIC operators
# like -and or -or. We resolve the column info here - handling both the
# primary table (me.col or bare col) and related objects (relation.col).
if ( $k !~ /^-/ ) {
my ( $ko, $col ) = _resolve_column_info( $key, $result_set );
if ( $ko && $col ) {
my $columns_info = $ko->_result->result_source->columns_info;
if ( exists $columns_info->{$col} ) {
$value = $ko->_recursive_fixup( $col, $value, $columns_info->{$col} );
}
}
}
$parsed_hash->{$k} = ref($value) ? _parse_dbic_query( $value, $result_set ) : $value;
}
return $parsed_hash;
} elsif ( reftype($q_params) && reftype($q_params) eq 'ARRAY' ) {
@ -781,6 +798,56 @@ sub _parse_dbic_query {
}
}
=head3 _resolve_column_info
my ( $ko, $col ) = _resolve_column_info( $api_key, $result_set );
Given an API field key (e.g. "timestamp", "me.timestamp",
"librarian.expiry_date", or "patron.holds.item.creation_date") and a
result_set, walks the relation chain to resolve the Koha::Object that
owns the leaf column. Returns ($ko, $col) where $ko is the resolved
Koha::Object instance and $col is the DBIC column name, or () if
resolution fails.
Related object instances are cached on $result_set->{_related_objects}
to avoid repeated prefetch_whitelist lookups.
=cut
sub _resolve_column_info {
my ( $key, $result_set ) = @_;
my @parts = split /\./, $key;
# Strip "me" prefix - it refers to the primary table
shift @parts if @parts > 1 && $parts[0] eq 'me';
# The last part is the column (in API naming)
my $api_col = pop @parts;
# Walk the relation chain. prefetch_whitelist returns singular
# Koha::Object class names (e.g. "Koha::Patron"), which we
# instantiate to access their prefetch_whitelist and from_api_mapping.
my $ko = $result_set->{_singular_object} ||= $result_set->object_class->new();
my @path;
for my $rel (@parts) {
push @path, $rel;
my $cache_key = join( '.', @path );
unless ( exists $result_set->{_related_objects}{$cache_key} ) {
my $class = $ko->prefetch_whitelist->{$rel};
$result_set->{_related_objects}{$cache_key} = $class ? $class->new() : undef;
}
$ko = $result_set->{_related_objects}{$cache_key};
return () unless $ko;
}
# Resolve the column name through from_api_mapping
my $mapping = $ko->from_api_mapping;
my $col = ( $mapping && defined $mapping->{$api_col} ) ? $mapping->{$api_col} : $api_col;
return ( $ko, $col );
}
=head3 _validate_operator
=cut