Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions lib/Db/MagicMapper/MagicSearchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,13 @@ class MagicSearchHandler {
*/
private MagicMetadataColumn $columnRoles;

/**
* Reads filters on a path inside an object property and writes their SQL.
*
* @var NestedPathFilter
*/
private NestedPathFilter $nestedPaths;

/**
* Constructor for MagicSearchHandler
*
Expand Down Expand Up @@ -242,6 +249,7 @@ public function __construct(
$this->termParser = new SearchTermParser();
$this->termCompiler = new SearchTermSqlCompiler();
$this->columnRoles = new MagicMetadataColumn();
$this->nestedPaths = new NestedPathFilter();
}//end __construct()

/**
Expand Down Expand Up @@ -1739,6 +1747,25 @@ private function buildObjectFilterConditionsSql(
$reservedParams = $this->getReservedParams();
$properties = $schema->getProperties() ?? [];

[$query, $nestedFilters] = $this->nestedPaths->split(filters: $query, properties: $properties);
foreach ($nestedFilters as $filter) {
$expression = $this->nestedPaths->expression(
column: $this->quoteIdentifier(name: $this->sanitizeColumnName(name: $filter['root']), isPostgres: $isPostgres),
path: $filter['path'],
isPostgres: $isPostgres
);
if ($expression === null) {
$conditions[] = '1=0';
continue;
}

$quote = static fn (string $value): string => (string)$connection->quote($value);
array_push(
$conditions,
...$this->nestedPaths->conditions(expression: $expression, value: $filter['value'], bind: $quote, isPostgres: $isPostgres)
);
}

foreach ($query as $key => $value) {
// Skip reserved params, underscore-prefixed params, and @ metadata params.
if (in_array($key, $reservedParams, true) === true
Expand Down Expand Up @@ -2766,6 +2793,9 @@ private function applyObjectFilters(IQueryBuilder $qb, array $filters, Schema $s

$isPostgres = $this->isPostgresPlatform();

[$filters, $nestedFilters] = $this->nestedPaths->split(filters: $filters, properties: $properties ?? []);
$this->applyNestedPathFilters(qb: $qb, nestedFilters: $nestedFilters, properties: $properties ?? [], isPostgres: $isPostgres);

foreach ($filters as $field => $value) {
// Check if this field exists as a column in the schema.
if (($properties[$field] ?? null) === null) {
Expand Down Expand Up @@ -2999,6 +3029,53 @@ private function applyJsonArrayFilter(IQueryBuilder $qb, string $columnName, mix
$qb->andWhere($orConditions);
}//end applyJsonArrayFilter()

/**
* Filter on fields inside object properties (`dunning.currentStage=2`), on the QueryBuilder path.
*
* A path the JSON column cannot be read at (a segment that is not a plain
* name) matches nothing and is reported as an ignored filter, the way an
* unknown property is.
*
* @param IQueryBuilder $qb Query builder to modify.
* @param list<array{root: string, path: list<string>, value: mixed}> $nestedFilters The path filters from NestedPathFilter::split().
* @param array<string,mixed> $properties The schema's properties.
* @param bool $isPostgres Whether the platform is PostgreSQL.
*
* @return void
*
* @throws EncryptedFieldFilterException When the object property is encrypted.
*
* @spec openspec/changes/nested-property-filters/specs/zoeken-filteren/spec.md#requirement-a-filter-can-name-a-field-inside-an-object-property
*/
private function applyNestedPathFilters(IQueryBuilder $qb, array $nestedFilters, array $properties, bool $isPostgres): void {
foreach ($nestedFilters as $filter) {
if (($properties[$filter['root']]['x-openregister-encrypted'] ?? false) === true) {
throw new EncryptedFieldFilterException(property: $filter['root']);
}

$expression = $this->nestedPaths->expression(
column: 't.' . $this->sanitizeColumnName(name: $filter['root']),
path: $filter['path'],
isPostgres: $isPostgres
);
if ($expression === null) {
$this->ignoredFilters[] = $filter['root'] . '.' . implode('.', $filter['path']);
$qb->andWhere('1 = 0');
continue;
}

$conditions = $this->nestedPaths->conditions(
expression: $expression,
value: $filter['value'],
bind: static fn (string $value): string => (string)$qb->createNamedParameter($value),
isPostgres: $isPostgres
);
foreach ($conditions as $condition) {
$qb->andWhere($condition);
}
}
}//end applyNestedPathFilters()

/**
* Apply filter for object columns (related objects)
*
Expand Down
Loading
Loading