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
21 changes: 17 additions & 4 deletions lib/features/workspace/grid_filter_engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class _PredicateNode extends _FilterAstNode {
this.inValues = const [],
this.betweenMin,
this.betweenMax,
this.likeRegex,
});

final int colIndex;
Expand All @@ -134,6 +135,10 @@ class _PredicateNode extends _FilterAstNode {
final String? betweenMin;
final String? betweenMax;

/// Compiled once at parse time for LIKE / ILIKE (and NOT variants), so the
/// per-row scan does not rebuild the same RegExp for every row.
final RegExp? likeRegex;

@override
bool evaluate(List<String> row, List<String> lowerColumns) {
if (colIndex < 0 || colIndex >= row.length) return false;
Expand Down Expand Up @@ -172,21 +177,25 @@ class _PredicateNode extends _FilterAstNode {

// LIKE / NOT LIKE
if (upperOp == 'LIKE') {
final regex = _likeToRegExp(targetValue, caseSensitive: true);
final regex =
likeRegex ?? _likeToRegExp(targetValue, caseSensitive: true);
return regex.hasMatch(cellValue);
}
if (upperOp == 'NOT LIKE') {
final regex = _likeToRegExp(targetValue, caseSensitive: true);
final regex =
likeRegex ?? _likeToRegExp(targetValue, caseSensitive: true);
return !regex.hasMatch(cellValue);
}

// ILIKE / NOT ILIKE
if (upperOp == 'ILIKE') {
final regex = _likeToRegExp(targetValue, caseSensitive: false);
final regex =
likeRegex ?? _likeToRegExp(targetValue, caseSensitive: false);
return regex.hasMatch(cellValue);
}
if (upperOp == 'NOT ILIKE') {
final regex = _likeToRegExp(targetValue, caseSensitive: false);
final regex =
likeRegex ?? _likeToRegExp(targetValue, caseSensitive: false);
return !regex.hasMatch(cellValue);
}

Expand Down Expand Up @@ -497,6 +506,10 @@ abstract final class _FilterLexer {
colIndex: colIdx,
op: op,
targetValue: patVal.value,
likeRegex: _PredicateNode._likeToRegExp(
patVal.value,
caseSensitive: likeType == 'LIKE',
),
),
consumedChars: (lPos - start) + patVal.consumedChars,
);
Expand Down
60 changes: 60 additions & 0 deletions test/features/workspace/data_grid_engines_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,66 @@ void main() {
expect(resIlike, equals([1]));
});

test('LIKE is case-sensitive while ILIKE ignores case', () {
List<int> run(String f) => GridFilterEngine.filterRowIndices(
filterText: f,
columns: columns,
rows: rows,
);
expect(run("status LIKE 'act%'"), isEmpty);
expect(run("status ILIKE 'act%'"), equals([0, 2]));
});

test('NOT LIKE / NOT ILIKE invert the match', () {
List<int> run(String f) => GridFilterEngine.filterRowIndices(
filterText: f,
columns: columns,
rows: rows,
);
expect(run("status NOT LIKE 'ACT%'"), equals([1, 3]));
expect(run("status NOT ILIKE '%pend%'"), equals([0, 2, 3]));
expect(run("status NOT LIKE 'act%'"), equals([0, 1, 2, 3]));
});

test('LIKE supports _ wildcard and treats regex metacharacters literally',
() {
final specialRows = [
['a.c'],
['abc'],
['a+c'],
['(x)'],
];
List<int> run(String f) => GridFilterEngine.filterRowIndices(
filterText: f,
columns: ['v'],
rows: specialRows,
);
expect(run("v LIKE 'a_c'"), equals([0, 1, 2]));
expect(run("v LIKE 'a.c'"), equals([0]));
expect(run("v LIKE 'a+c'"), equals([2]));
expect(run("v LIKE '(x)'"), equals([3]));
});

test('LIKE inside AND / OR gives the same result on every row', () {
final many = [
for (var i = 0; i < 2000; i++) ['$i', i.isEven ? 'ACTIVE' : 'PENDING'],
];
final res = GridFilterEngine.filterRowIndices(
filterText: "status LIKE 'ACT%' AND id LIKE '%9' OR status ILIKE 'pen%' AND id LIKE '1%'",
columns: ['id', 'status'],
rows: many,
);
final expected = [
for (var i = 0; i < many.length; i++)
if ((many[i][1].startsWith('ACT') && many[i][0].endsWith('9')) ||
(many[i][1].toLowerCase().startsWith('pen') &&
many[i][0].startsWith('1')))
i,
];
expect(res, equals(expected));
expect(res, isNotEmpty);
});

test('filters with IN and NOT IN list of literals', () {
final resIn = GridFilterEngine.filterRowIndices(
filterText: "status IN ('PENDING', 'CANCELLED')",
Expand Down
Loading