Skip to content
Open
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
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ target_sources(${PROJECT_NAME}
src/FileDialog.h
src/ColumnDisplayFormatDialog.h
src/FilterLineEdit.h
src/GlobalFilter.h
src/ForeignKeyEditorDelegate.h
src/PlotDock.h
src/FindReplaceDialog.h
Expand Down Expand Up @@ -197,6 +198,7 @@ target_sources(${PROJECT_NAME}
src/FileDialog.cpp
src/ColumnDisplayFormatDialog.cpp
src/FilterLineEdit.cpp
src/GlobalFilter.cpp
src/ForeignKeyEditorDelegate.cpp
src/PlotDock.cpp
src/FindReplaceDialog.cpp
Expand Down
93 changes: 93 additions & 0 deletions src/GlobalFilter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#include "GlobalFilter.h"

#include <QXmlStreamReader>
#include <QXmlStreamWriter>

namespace GlobalFilter
{

static std::vector<QString> legacyTokenize(const QString& value)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
const QStringList values = value.trimmed().split(" ", QString::SkipEmptyParts);
#else
const QStringList values = value.trimmed().split(" ", Qt::SkipEmptyParts);
#endif
return {values.cbegin(), values.cend()};
}

std::vector<QString> tokenize(const QString& value)
{
std::vector<QString> filters;
int position = 0;

while(position < value.size())
{
while(position < value.size() && value.at(position) == ' ')
++position;
if(position == value.size())
break;

const int termStart = position;
if(value.at(termStart) != '/')
{
while(position < value.size() && value.at(position) != ' ')
++position;
filters.push_back(value.mid(termStart, position - termStart));
continue;
}

bool foundClosingSlash = false;
for(position = termStart + 1; position < value.size(); ++position)
{
if(value.at(position) != '/')
continue;

int backslashes = 0;
for(int previous = position - 1; previous >= termStart && value.at(previous) == '\\'; --previous)
++backslashes;
const bool unescaped = backslashes % 2 == 0;
const bool atTermEnd = position + 1 == value.size() || value.at(position + 1) == ' ';
if(unescaped && !atTermEnd)
return legacyTokenize(value);
if(unescaped)
{
filters.push_back(value.mid(termStart, position - termStart + 1));
++position;
foundClosingSlash = true;
break;
}
}

if(!foundClosingSlash)
return legacyTokenize(value);
}

return filters;
}

void load(std::vector<QString>& filters, QXmlStreamReader& xml)
{
while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != QT_UNICODE_LITERAL("global_filter"))
{
if(xml.name() == QT_UNICODE_LITERAL("filter"))
{
filters.push_back(xml.attributes().value("value").toString());
xml.skipCurrentElement();
}
}
}

void save(const std::vector<QString>& filters, QXmlStreamWriter& xml)
{
xml.writeStartElement("global_filter");
for(const auto& value : filters)
{
xml.writeStartElement("filter");
xml.writeAttribute("value", value);
xml.writeEndElement();
}
xml.writeEndElement();
}

}
19 changes: 19 additions & 0 deletions src/GlobalFilter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef GLOBALFILTER_H
#define GLOBALFILTER_H

#include <QString>
#include <vector>

class QXmlStreamReader;
class QXmlStreamWriter;

namespace GlobalFilter
{

std::vector<QString> tokenize(const QString& value);
void load(std::vector<QString>& filters, QXmlStreamReader& xml);
void save(const std::vector<QString>& filters, QXmlStreamWriter& xml);

}

#endif // GLOBALFILTER_H
20 changes: 3 additions & 17 deletions src/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "SqlUiLexer.h"
#include "FileDialog.h"
#include "FindReplaceDialog.h"
#include "GlobalFilter.h"
#include "RunSql.h"
#include "ExtendedTableWidget.h"
#include "Data.h"
Expand Down Expand Up @@ -2772,15 +2773,7 @@ static void loadBrowseDataTableSettings(BrowseDataTableSettings& settings, sqlb:
settings.plotYAxes[1][y2AxisName] = y2AxisSettings;
}
} else if(xml.name() == QT_UNICODE_LITERAL("global_filter")) {
while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != QT_UNICODE_LITERAL("global_filter"))
{
if(xml.name() == QT_UNICODE_LITERAL("filter"))
{
QString value = xml.attributes().value("value").toString();
settings.globalFilters.push_back(value);
xml.skipCurrentElement();
}
}
GlobalFilter::load(settings.globalFilters, xml);
}
}
}
Expand Down Expand Up @@ -3173,14 +3166,7 @@ static void saveBrowseDataTableSettings(const BrowseDataTableSettings& object, s
xml.writeEndElement();
}
xml.writeEndElement();
xml.writeStartElement("global_filter");
for(const auto& v : object.globalFilters)
{
xml.writeStartElement("filter");
xml.writeAttribute("value", v);
xml.writeEndElement();
}
xml.writeEndElement();
GlobalFilter::save(object.globalFilters, xml);
}

void MainWindow::saveProject(const QString& currentFilename)
Expand Down
10 changes: 2 additions & 8 deletions src/TableBrowser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "DbStructureModel.h"
#include "ExportDataDialog.h"
#include "FilterTableHeader.h"
#include "GlobalFilter.h"
#include "TableBrowser.h"
#include "Settings.h"
#include "sqlitedb.h"
Expand Down Expand Up @@ -129,14 +130,7 @@ TableBrowser::TableBrowser(DBBrowserDB* _db, QWidget* parent) :
emit prepareForFilter();
});
connect(ui->editGlobalFilter, &FilterLineEdit::delayedTextChanged, this, [this](const QString& value) {
// Split up filter values
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
QStringList values = value.trimmed().split(" ", QString::SkipEmptyParts);
#else
QStringList values = value.trimmed().split(" ", Qt::SkipEmptyParts);
#endif
std::vector<QString> filters;
std::copy(values.begin(), values.end(), std::back_inserter(filters));
const std::vector<QString> filters = GlobalFilter::tokenize(value);

ui->actionClearFilters->setEnabled(m_model->filterCount() > 0 || !ui->editGlobalFilter->text().isEmpty());

Expand Down
33 changes: 33 additions & 0 deletions src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,39 @@ add_executable(test-regex ${TESTREGEX_HDR} ${TESTREGEX_SRC})
target_link_libraries(test-regex ${QT_MAJOR}::Test ${QT_MAJOR}::Widgets ${QT5_COMPAT})
add_test(NAME test-regex COMMAND $<TARGET_FILE:test-regex>)

# test global filter

set(TESTGLOBALFILTER_SRC
TestGlobalFilter.cpp
GlobalFilterTestSupport.cpp
../GlobalFilter.cpp
../CondFormat.cpp
../Data.cpp
../Settings.cpp
../sql/Query.cpp
../sql/ObjectIdentifier.cpp
)

set(TESTGLOBALFILTER_HDR
TestGlobalFilter.h
../GlobalFilter.h
../CondFormat.h
../Data.h
../Settings.h
../sql/Query.h
../sql/ObjectIdentifier.h
)

add_executable(test-global-filter ${TESTGLOBALFILTER_HDR} ${TESTGLOBALFILTER_SRC})
target_link_libraries(test-global-filter ${QT_MAJOR}::Test ${QT_MAJOR}::Widgets ${LIBSQLITE_NAME} ${QT5_COMPAT})
add_test(NAME test-global-filter COMMAND $<TARGET_FILE:test-global-filter>)
if(WIN32)
# SQLite and SQLCipher are built as DLLs in the CMake prefix by the Windows CI jobs.
# Keep those prefixes on PATH while CTest launches this executable.
string(REPLACE ";" "\\;" TESTGLOBALFILTER_PATH "${CMAKE_PREFIX_PATH};$ENV{PATH}")
set_tests_properties(test-global-filter PROPERTIES ENVIRONMENT "PATH=${TESTGLOBALFILTER_PATH}")
endif()

# test cache

set(TESTCACHE_SRC
Expand Down
11 changes: 11 additions & 0 deletions src/tests/GlobalFilterTestSupport.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#include "../sqlitedb.h"

namespace sqlb
{

QString escapeString(const QString& literal)
{
return QString::fromStdString(escapeString(literal.toStdString()));
}

}
141 changes: 141 additions & 0 deletions src/tests/TestGlobalFilter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#include "TestGlobalFilter.h"

#include "../CondFormat.h"
#include "../GlobalFilter.h"
#include "../sql/Query.h"

#include <QRegularExpression>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include <QtTest/QtTest>
#include <sqlite3.h>

QTEST_APPLESS_MAIN(TestGlobalFilter)

namespace
{

QStringList asStringList(const std::vector<QString>& values)
{
QStringList result;
for(const auto& value : values)
result.push_back(value);
return result;
}

void regexp(sqlite3_context* context, int argc, sqlite3_value** argv)
{
if(argc != 2 || sqlite3_value_type(argv[0]) == SQLITE_NULL || sqlite3_value_type(argv[1]) == SQLITE_NULL)
{
sqlite3_result_int(context, 0);
return;
}

const auto pattern = reinterpret_cast<const char*>(sqlite3_value_text(argv[0]));
const auto value = reinterpret_cast<const char*>(sqlite3_value_text(argv[1]));
sqlite3_result_int(context, QRegularExpression(QString::fromUtf8(pattern)).match(QString::fromUtf8(value)).hasMatch());
}

QStringList executeIssueQuery(const std::vector<QString>& filters)
{
sqlb::Query query(sqlb::ObjectIdentifier("main", "SomeCities"));
query.setColumnNames({"Name"});
for(const auto& filter : filters)
query.globalWhere().push_back(CondFormat::filterToSqlCondition(filter));

sqlite3* db = nullptr;
if(sqlite3_open(":memory:", &db) != SQLITE_OK)
qFatal("Could not open the SQLite fixture");
if(sqlite3_create_function(db, "regexp", 2, SQLITE_UTF8, nullptr, regexp, nullptr, nullptr) != SQLITE_OK)
qFatal("Could not register REGEXP for the SQLite fixture");
if(sqlite3_exec(db,
"CREATE TABLE SomeCities(Name TEXT);"
"INSERT INTO SomeCities VALUES ('New York'), ('Hawaii'), ('San Francisco');",
nullptr, nullptr, nullptr) != SQLITE_OK)
qFatal("Could not create the SQLite fixture");

QStringList rows;
char* error = nullptr;
const std::string sql = query.buildQuery(false) + " ORDER BY Name";
const int result = sqlite3_exec(db, sql.c_str(), [](void* data, int count, char** values, char**) {
if(count > 0 && values[0])
static_cast<QStringList*>(data)->push_back(QString::fromUtf8(values[0]));
return 0;
}, &rows, &error);
const QString errorMessage = error ? QString::fromUtf8(error) : QString();
sqlite3_free(error);
sqlite3_close(db);
if(result != SQLITE_OK)
qFatal("Issue fixture query failed: %s", qPrintable(errorMessage));
return rows;
}

}

void TestGlobalFilter::tokenizes_data()
{
QTest::addColumn<QString>("input");
QTest::addColumn<QStringList>("expected");

QTest::newRow("regex-with-space") << "/New York/" << QStringList{"/New York/"};
QTest::newRow("ordinary-and-terms") << "foo bar" << QStringList{"foo", "bar"};
QTest::newRow("mixed") << "foo /New York/ bar" << QStringList{"foo", "/New York/", "bar"};
QTest::newRow("multiple-regexes") << "/New York/ /San Francisco/" << QStringList{"/New York/", "/San Francisco/"};
QTest::newRow("later-unmatched-slash-falls-back") << "/New York/ /San Francisco" << QStringList{"/New", "York/", "/San", "Francisco"};
QTest::newRow("escaped-slash") << R"(/New \/ York/)" << QStringList{R"(/New \/ York/)"};
QTest::newRow("unmatched-slash") << "/New York" << QStringList{"/New", "York"};
QTest::newRow("repeated-spaces") << " foo /New York/ bar " << QStringList{"foo", "/New York/", "bar"};
QTest::newRow("closing-slash-needs-boundary") << "/foo/xyz bar" << QStringList{"/foo/xyz", "bar"};
QTest::newRow("slash-literal-before-regex-falls-back") << "/usr/bin /New York/" << QStringList{"/usr/bin", "/New", "York/"};
QTest::newRow("empty-regex") << "//" << QStringList{"//"};
QTest::newRow("tabs-are-not-separators") << "alpha\tbeta gamma" << QStringList{"alpha\tbeta", "gamma"};
QTest::newRow("even-backslash-parity") << R"(/a\\/ b)" << QStringList{R"(/a\\/)", "b"};
QTest::newRow("odd-backslash-parity") << R"(/a\/ b/)" << QStringList{R"(/a\/ b/)"};
}

void TestGlobalFilter::tokenizes()
{
QFETCH(QString, input);
QFETCH(QStringList, expected);
QCOMPARE(asStringList(GlobalFilter::tokenize(input)), expected);
}

void TestGlobalFilter::issueFixtureReturnsNewYork()
{
QCOMPARE(executeIssueQuery(GlobalFilter::tokenize("/New York/")), QStringList{"New York"});
}

void TestGlobalFilter::ordinaryTermsRemainAndConnected()
{
sqlb::Query query(sqlb::ObjectIdentifier("main", "SomeCities"));
query.setColumnNames({"Name"});
const auto filters = GlobalFilter::tokenize("New York");
QCOMPARE(filters.size(), size_t{2});
for(const auto& filter : filters)
query.globalWhere().push_back(CondFormat::filterToSqlCondition(filter));
QVERIFY(QString::fromStdString(query.buildQuery(false)).contains(") AND ("));
}

void TestGlobalFilter::projectRoundTripPreservesRegexTerm()
{
QByteArray project;
QXmlStreamWriter writer(&project);
writer.writeStartDocument();
writer.writeStartElement("table");
GlobalFilter::save(GlobalFilter::tokenize("/New York/"), writer);
writer.writeEndElement();
writer.writeEndDocument();

std::vector<QString> restored;
QXmlStreamReader reader(project);
while(!reader.atEnd())
{
reader.readNext();
if(reader.isStartElement() && reader.name() == QT_UNICODE_LITERAL("global_filter"))
GlobalFilter::load(restored, reader);
}

QVERIFY(!reader.hasError());
QCOMPARE(asStringList(restored), QStringList{"/New York/"});
QCOMPARE(executeIssueQuery(restored), QStringList{"New York"});
}
Loading
Loading