Skip to content

Commit 038dea0

Browse files
authored
Merge pull request #77 from itk-dev/feature/semver-filter-upper-bound
fix: SemverFilter must respect value2 with directional operators
2 parents 25ab300 + 1fe33d5 commit 038dea0

4 files changed

Lines changed: 90 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
- [#77](https://github.com/itk-dev/devops_itksites/pull/77)
11+
Fix SemverFilter: respect value2 with directional operators
12+
1013
- [#75](https://github.com/itk-dev/devops_itksites/pull/75)
1114
Add semver-aware filter on every admin version column, make version
1215
column semver sortable

src/Form/Type/Admin/SemverFilter.php

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,23 @@ public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto,
3939
return;
4040
}
4141

42-
$isRange = SemverFilterType::COMPARISON_BETWEEN === $comparison
43-
|| SemverFilterType::COMPARISON_BETWEEN_EXCLUSIVE === $comparison;
44-
if ($isRange && '' === $value2) {
42+
// When the user fills the upper-bound field together with a directional
43+
// operator (>, >=, <, <=), treat the filter as a range. The operator's
44+
// inclusivity carries over (>= / <= → inclusive, > / < → exclusive),
45+
// so the natural reading "from X to Y" works regardless of which side
46+
// the user picked. Range operators (between / between_exclusive)
47+
// require both values and behave the same way. = and != are exact
48+
// matches, so value2 is ignored.
49+
$isExplicitRange = in_array($comparison, [SemverFilterType::COMPARISON_BETWEEN, SemverFilterType::COMPARISON_BETWEEN_EXCLUSIVE], true);
50+
$autoRange = '' !== $value2 && in_array($comparison, [
51+
SemverFilterType::COMPARISON_GT,
52+
SemverFilterType::COMPARISON_GTE,
53+
SemverFilterType::COMPARISON_LT,
54+
SemverFilterType::COMPARISON_LTE,
55+
], true);
56+
$isRange = $isExplicitRange || $autoRange;
57+
58+
if ($isExplicitRange && '' === $value2) {
4559
return;
4660
}
4761

@@ -70,9 +84,19 @@ public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto,
7084
// references its argument five times) and PDO would complain about
7185
// bound-variable count.
7286
if ($isRange) {
73-
[$lowerOp, $upperOp] = SemverFilterType::COMPARISON_BETWEEN === $comparison
74-
? ['>=', '<=']
75-
: ['>', '<'];
87+
$inclusive = in_array($comparison, [
88+
SemverFilterType::COMPARISON_BETWEEN,
89+
SemverFilterType::COMPARISON_GTE,
90+
SemverFilterType::COMPARISON_LTE,
91+
], true);
92+
[$lowerOp, $upperOp] = $inclusive ? ['>=', '<='] : ['>', '<'];
93+
94+
// Sort the two values numerically so the user can enter them in any
95+
// order — "< 11.3.0" with value2 = "10.0.0" still produces a sane
96+
// range, not an unsatisfiable WHERE.
97+
$a = self::toSemverNumeric($value);
98+
$b = self::toSemverNumeric($value2);
99+
[$min, $max] = $a <= $b ? [$a, $b] : [$b, $a];
76100

77101
$queryBuilder
78102
->andWhere(sprintf(
@@ -83,8 +107,8 @@ public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto,
83107
$parameter,
84108
$upperOp,
85109
))
86-
->setParameter($parameter.'_min', self::toSemverNumeric($value))
87-
->setParameter($parameter.'_max', self::toSemverNumeric($value2))
110+
->setParameter($parameter.'_min', $min)
111+
->setParameter($parameter.'_max', $max)
88112
;
89113

90114
return;

src/Form/Type/Admin/SemverFilterType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
4646
])
4747
->add('value2', TextType::class, [
4848
'required' => false,
49-
'attr' => ['placeholder' => 'upper bound (when between)'],
49+
'attr' => ['placeholder' => 'upper bound (optional, makes it a range)'],
5050
])
5151
;
5252
}

tests/Form/Type/Admin/SemverFilterTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,60 @@ public function testApplyShortCircuitsRangeOnInvalidUpperBound(): void
102102
self::assertStringContainsString('1 = 0', $qb->getDQL(), 'Invalid upper-bound input must produce a 0-row query');
103103
}
104104

105+
/**
106+
* Regression: `>= 11.0.0` with value2=11.2.0 must produce a range,
107+
* not silently ignore the upper bound (which previously let 11.3.5
108+
* leak through).
109+
*/
110+
public function testApplyAutoPromotesGteWithValue2ToInclusiveRange(): void
111+
{
112+
$qb = $this->makeInstallationQueryBuilder();
113+
114+
$this->apply($qb, '>=', '11.0.0', '11.2.0');
115+
116+
$dql = $qb->getDQL();
117+
self::assertStringContainsString('SEMVER_NUMERIC(entity.frameworkVersion) >= :frameworkVersion_0_min', $dql);
118+
self::assertStringContainsString('SEMVER_NUMERIC(entity.frameworkVersion) <= :frameworkVersion_0_max', $dql);
119+
self::assertSame(11_000_000_000_000, $qb->getParameter('frameworkVersion_0_min')?->getValue());
120+
self::assertSame(11_000_200_000_000, $qb->getParameter('frameworkVersion_0_max')?->getValue());
121+
}
122+
123+
public function testApplyAutoPromotesGtWithValue2ToExclusiveRange(): void
124+
{
125+
$qb = $this->makeInstallationQueryBuilder();
126+
127+
$this->apply($qb, '>', '11.0.0', '11.2.0');
128+
129+
$dql = $qb->getDQL();
130+
self::assertStringContainsString('SEMVER_NUMERIC(entity.frameworkVersion) > :frameworkVersion_0_min', $dql);
131+
self::assertStringContainsString('SEMVER_NUMERIC(entity.frameworkVersion) < :frameworkVersion_0_max', $dql);
132+
}
133+
134+
/**
135+
* Auto-promote sorts the two values so users can enter them in either
136+
* order — "< 11.2.0" with value2 = "11.0.0" still means [11.0.0, 11.2.0].
137+
*/
138+
public function testApplyAutoPromoteSortsValuesNumerically(): void
139+
{
140+
$qb = $this->makeInstallationQueryBuilder();
141+
142+
$this->apply($qb, '<', '11.2.0', '11.0.0');
143+
144+
self::assertSame(11_000_000_000_000, $qb->getParameter('frameworkVersion_0_min')?->getValue());
145+
self::assertSame(11_000_200_000_000, $qb->getParameter('frameworkVersion_0_max')?->getValue());
146+
}
147+
148+
public function testApplyIgnoresValue2ForEqualsOperator(): void
149+
{
150+
$qb = $this->makeInstallationQueryBuilder();
151+
152+
$this->apply($qb, '=', '11.0.0', '11.2.0');
153+
154+
$dql = $qb->getDQL();
155+
self::assertStringNotContainsString('_min', $dql, '= operator with value2 must remain a single-value match');
156+
self::assertSame(11_000_000_000_000, $qb->getParameter('frameworkVersion_0')?->getValue());
157+
}
158+
105159
/**
106160
* @return iterable<string, array{string, string, int}>
107161
*/

0 commit comments

Comments
 (0)