Skip to content

Commit c65a49e

Browse files
Copilotanidotnet
andcommitted
Refactor to use FlattenableFilter pattern - keep geometry validation in spatial module
Co-authored-by: anidotnet <[email protected]>
1 parent dcc99ea commit c65a49e

File tree

4 files changed

+67
-55
lines changed

4 files changed

+67
-55
lines changed

packages/nitrite/lib/src/collection/operations/find_optimizer.dart

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ class FindOptimizer {
1717

1818
FindPlan _createFilterPlan(
1919
Iterable<IndexDescriptor> indexDescriptors, Filter filter) {
20-
if (filter is AndFilter) {
21-
var filters = _flattenAndFilter(filter);
20+
if (filter is FlattenableFilter) {
21+
var filters = _flattenFilter(filter);
2222
return _createAndPlan(indexDescriptors, filters);
2323
} else if (filter is OrFilter) {
2424
return _createOrPlan(indexDescriptors, filter.filters);
@@ -28,11 +28,11 @@ class FindOptimizer {
2828
}
2929
}
3030

31-
List<Filter> _flattenAndFilter(AndFilter andFilter) {
31+
List<Filter> _flattenFilter(FlattenableFilter flattenableFilter) {
3232
var flattenedFilters = <Filter>[];
33-
for (var filter in andFilter.filters) {
34-
if (filter is AndFilter) {
35-
flattenedFilters.addAll(_flattenAndFilter(filter));
33+
for (var filter in flattenableFilter.getFilters()) {
34+
if (filter is FlattenableFilter) {
35+
flattenedFilters.addAll(_flattenFilter(filter));
3636
} else {
3737
flattenedFilters.add(filter);
3838
}
@@ -241,13 +241,6 @@ class FindOptimizer {
241241
if (filter != findPlan.byIdFilter) {
242242
columnScanFilters.add(filter);
243243
}
244-
} else if (filter is IndexOnlyFilter &&
245-
filter.needsPostIndexValidation()) {
246-
// Some index-only filters (like spatial filters) need post-index validation
247-
// because the index can return false positives. For example, R-Tree spatial
248-
// indexes store only bounding boxes, so they may return documents whose
249-
// bounding boxes overlap but actual geometries don't intersect.
250-
columnScanFilters.add(filter);
251244
}
252245
}
253246

packages/nitrite/lib/src/filters/filter.dart

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,16 @@ abstract class Filter {
175175
}
176176
}
177177

178+
/// Represents a filter which can be flattened or consists of multiple constituent filters.
179+
///
180+
/// This interface allows filters to be decomposed into multiple sub-filters during query
181+
/// optimization. For example, spatial filters can be split into an index scan filter
182+
/// (for bounding box checks) and a validation filter (for actual geometry checks).
183+
abstract class FlattenableFilter {
184+
/// Returns the list of constituent filters that make up this filter.
185+
List<Filter> getFilters();
186+
}
187+
178188
/// An abstract class representing a filter for Nitrite database.
179189
abstract class NitriteFilter extends Filter {
180190
/// Gets the [NitriteConfig] instance.
@@ -330,17 +340,6 @@ abstract class IndexOnlyFilter extends ComparableFilter {
330340

331341
/// Checks if `other` filter can be grouped together with this filter.
332342
bool canBeGrouped(IndexOnlyFilter other);
333-
334-
/// Indicates whether this filter requires post-index validation.
335-
///
336-
/// Some index-only filters (like spatial filters) use the index for
337-
/// preliminary filtering but need a second pass to validate the actual
338-
/// condition. For example, R-Tree spatial indexes store only bounding boxes,
339-
/// so they may return false positives that need to be filtered out.
340-
///
341-
/// Returns `true` if this filter needs to be applied again after index scan
342-
/// to validate results. Defaults to `false`.
343-
bool needsPostIndexValidation() => false;
344343
}
345344

346345
/// @nodoc

packages/nitrite/lib/src/filters/filter_impl.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class OrFilter extends LogicalFilter {
7070
}
7171

7272
/// @nodoc
73-
class AndFilter extends LogicalFilter {
73+
class AndFilter extends LogicalFilter implements FlattenableFilter {
7474
AndFilter(List<Filter> filters) : super(filters) {
7575
for (int i = 1; i < filters.length; i++) {
7676
if (filters[i] is TextFilter) {
@@ -91,6 +91,9 @@ class AndFilter extends LogicalFilter {
9191
return true;
9292
}
9393

94+
@override
95+
List<Filter> getFilters() => filters;
96+
9497
@override
9598
String toString() {
9699
StringBuffer buffer = StringBuffer();

packages/nitrite_spatial/lib/src/filter.dart

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,30 @@ abstract class SpatialFilter extends IndexOnlyFilter {
1717

1818
@override
1919
bool apply(Document doc) {
20-
// This method validates the actual geometry intersection/containment
21-
// after the R-Tree index has filtered candidates based on bounding boxes.
22-
// The R-Tree only stores bounding boxes, so it can return false positives.
23-
// This second-stage check ensures we only return documents whose geometries
24-
// actually satisfy the spatial relationship.
20+
return false;
21+
}
22+
23+
@override
24+
String supportedIndexType() {
25+
return spatialIndex;
26+
}
27+
28+
@override
29+
bool canBeGrouped(IndexOnlyFilter other) {
30+
return other is SpatialFilter && other.field == field;
31+
}
32+
}
33+
34+
/// A non-index filter that validates actual geometry relationships.
35+
/// This filter is used as the second stage of spatial filtering after
36+
/// the R-Tree index has filtered candidates based on bounding boxes.
37+
class _GeometryValidationFilter extends FieldBasedFilter {
38+
final bool Function(Geometry, Geometry) _validator;
39+
40+
_GeometryValidationFilter(super.field, super.value, this._validator);
41+
42+
@override
43+
bool apply(Document doc) {
2544
var fieldValue = doc.get(field);
2645
if (fieldValue == null) {
2746
return false;
@@ -42,28 +61,12 @@ abstract class SpatialFilter extends IndexOnlyFilter {
4261
return false;
4362
}
4463

45-
return applyGeometryFilter(documentGeometry!);
46-
}
47-
48-
/// Subclasses must implement this to define the specific spatial relationship check
49-
bool applyGeometryFilter(Geometry documentGeometry);
50-
51-
@override
52-
String supportedIndexType() {
53-
return spatialIndex;
64+
return _validator(documentGeometry!, value as Geometry);
5465
}
55-
56-
@override
57-
bool canBeGrouped(IndexOnlyFilter other) {
58-
return other is SpatialFilter && other.field == field;
59-
}
60-
61-
@override
62-
bool needsPostIndexValidation() => true;
6366
}
6467

6568
///@nodoc
66-
class WithinFilter extends SpatialFilter {
69+
class WithinFilter extends SpatialFilter implements FlattenableFilter {
6770
WithinFilter(super.field, super.value);
6871

6972
@override
@@ -73,9 +76,16 @@ class WithinFilter extends SpatialFilter {
7376
}
7477

7578
@override
76-
bool applyGeometryFilter(Geometry documentGeometry) {
77-
// Check if the document geometry is within the filter geometry
78-
return documentGeometry.within(value);
79+
List<Filter> getFilters() {
80+
// Return two filters: one for index scan (this), one for validation
81+
return [
82+
this,
83+
_GeometryValidationFilter(
84+
field,
85+
value,
86+
(docGeom, filterGeom) => docGeom.within(filterGeom),
87+
),
88+
];
7989
}
8090

8191
@override
@@ -85,7 +95,7 @@ class WithinFilter extends SpatialFilter {
8595
}
8696

8797
///@nodoc
88-
class IntersectsFilter extends SpatialFilter {
98+
class IntersectsFilter extends SpatialFilter implements FlattenableFilter {
8999
IntersectsFilter(super.field, super.value);
90100

91101
@override
@@ -95,9 +105,16 @@ class IntersectsFilter extends SpatialFilter {
95105
}
96106

97107
@override
98-
bool applyGeometryFilter(Geometry documentGeometry) {
99-
// Check if the document geometry intersects the filter geometry
100-
return documentGeometry.intersects(value);
108+
List<Filter> getFilters() {
109+
// Return two filters: one for index scan (this), one for validation
110+
return [
111+
this,
112+
_GeometryValidationFilter(
113+
field,
114+
value,
115+
(docGeom, filterGeom) => docGeom.intersects(filterGeom),
116+
),
117+
];
101118
}
102119

103120
@override

0 commit comments

Comments
 (0)