Skip to content

Fix literal wildcards in text filters - #4609

Open
floze-the-genius wants to merge 1 commit into
MerginMaps:masterfrom
floze-the-genius:bugfix/text-filter-literal-wildcards
Open

Fix literal wildcards in text filters#4609
floze-the-genius wants to merge 1 commit into
MerginMaps:masterfrom
floze-the-genius:bugfix/text-filter-literal-wildcards

Conversation

@floze-the-genius

Copy link
Copy Markdown

Summary

  • escape user-entered !, %, and _ before substituting text filter values into LIKE expressions
  • add an explicit ESCAPE '!' clause so % and _ are matched literally while the surrounding %...% still performs substring matching
  • add OGR/GeoPackage regression coverage for percent, underscore, the escape character itself, and combined %_ input

Root cause

Text filter values were quoted for SQL string safety, but SQL LIKE metacharacters were left unchanged. As a result, % matched any sequence and _ matched any single character instead of the characters users typed.

Closes #4517.

Validation

  • project astyle check on all changed C++/header files
  • git diff --check
  • SQLite verification of the generated LIKE ... ESCAPE '!' patterns for %, _, !, and %_
  • regression test uses a temporary copy of the project's real roads.gpkg through the OGR provider

The full Mergin Maps native test binary was not built locally because this host does not have the repository's Qt/QGIS/vcpkg toolchain.

AI assistance disclosure

OpenAI Codex assisted with issue triage, implementation, regression-test drafting, and validation. The contribution was reviewed against the repository policies and the generated SQL behavior was independently checked locally.

@floze-the-genius
floze-the-genius marked this pull request as ready for review July 21, 2026 19:18
Escape LIKE metacharacters in user-entered text and cover percent, underscore, escape-character, and combined inputs against an OGR GeoPackage layer.

Assisted-by: OpenAI Codex
@floze-the-genius
floze-the-genius force-pushed the bugfix/text-filter-literal-wildcards branch from 9b8a549 to 0b9a6d0 Compare August 9, 2026 17:37
@floze-the-genius

Copy link
Copy Markdown
Author

@Withalion I exact-rebased this onto current master 4c38f41 (new head 0b9a6d0; range-diff is identical). astyle 3.4.13 leaves all changed files unchanged, git diff --check passes, and the SQLite ESCAPE matrix for %, , !, and % gives the expected literal-match counts. I could not run the full Qt/QGIS native binary on this host, and the repository workflows are still push-only for forks (#4624 is the draft that addresses this), so native CI remains a maintainer-side gate. Could you review when convenient?

@Withalion Withalion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned several times that you are not able to build the application on your host, what host do you have? The build infra is available for major OSes. If you haven't build the app how do you know it works? Have you run your tests?

Comment on lines +237 to +241
textValue.replace( QStringLiteral( "!" ), QStringLiteral( "!!" ) );
textValue.replace( QStringLiteral( "%" ), QStringLiteral( "!%" ) );
textValue.replace( QStringLiteral( "_" ), QStringLiteral( "!_" ) );
expressionCopy.replace( QStringLiteral( "@@value@@" ), textValue );
expressionCopy.append( QStringLiteral( " ESCAPE '!'" ) );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create new function in InputUtils which will do these steps and just call it from here. Use backslash as the escape character instead. Also are these all the wildcards you can get?

mController->processFilters( filterValues );

const QString expected = QStringLiteral( "(\"Condition\" LIKE '%great%')" );
const QString expected = QStringLiteral( "(\"Condition\" LIKE '%great%' ESCAPE '!')" );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are not escaping anything here

Comment on lines +582 to +595
QTemporaryDir tempDir;
QVERIFY( tempDir.isValid() );
const QString layerPath = tempDir.filePath( QStringLiteral( "roads.gpkg" ) );
QVERIFY( QFile::copy( TestUtils::testDataDir() + QStringLiteral( "/filtering/roads.gpkg" ), layerPath ) );

const QString fieldName = QStringLiteral( "Condition" );
QgsVectorLayer *layer = new QgsVectorLayer(
layerPath + QStringLiteral( "|layername=roads" ),
QStringLiteral( "text-filter-wildcards" ),
QStringLiteral( "ogr" )
);
QVERIFY( layer );
QVERIFY( layer->isValid() );
QgsProject::instance()->addMapLayer( layer );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's wrong with the approach in other tests?

Comment on lines +564 to +574
void TestFilterController::testTextFilterWildcards_data()
{
QTest::addColumn<QString>( "searchValue" );
QTest::addColumn<QString>( "expectedPattern" );
QTest::addColumn<qlonglong>( "expectedCount" );

QTest::newRow( "percent" ) << QStringLiteral( "%" ) << QStringLiteral( "%!%%" ) << 2LL;
QTest::newRow( "underscore" ) << QStringLiteral( "_" ) << QStringLiteral( "%!_%" ) << 2LL;
QTest::newRow( "escape-character" ) << QStringLiteral( "!" ) << QStringLiteral( "%!!%" ) << 1LL;
QTest::newRow( "combined-wildcards" ) << QStringLiteral( "%_" ) << QStringLiteral( "%!%!_%" ) << 1LL;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't use this, just use the expected values inline

Comment on lines +597 to +604
const QStringList values =
{
QStringLiteral( "plain" ),
QStringLiteral( "percent%only" ),
QStringLiteral( "under_score" ),
QStringLiteral( "bang!mark" ),
QStringLiteral( "both%_chars" )
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use values that could match values in condition

Comment on lines +605 to +608
for ( const QString &value : values )
{
QVERIFY( TestUtils::addFeatureToLayer( layer, fieldName, value ) );
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't add more features to the layer there already are some, use them

QVERIFY( !filterId.isEmpty() );

QVariantMap filterValues;
filterValues[filterId] = QVariantList{ searchValue };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apply the filter values one by one and check the results after each one

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Text filter: % and _ treated as SQL wildcards instead of literal characters

2 participants