diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -42,13 +42,16 @@ ConnectionTest.cpp DriverTest.cpp ExpressionsTest.cpp + MissingTableTest.cpp QuerySchemaTest.cpp KDbTest.cpp LINK_LIBRARIES kdbtestutils ) +target_compile_definitions(MissingTableTest PRIVATE -DFILES_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" ) + if(NOT WIN32) #TODO enable for Windows when headers_test.sh is ported e.g. to python add_subdirectory(headers) endif() diff --git a/autotests/KDbTestUtils.h b/autotests/KDbTestUtils.h --- a/autotests/KDbTestUtils.h +++ b/autotests/KDbTestUtils.h @@ -22,11 +22,13 @@ #include "kdbtestutils_export.h" -#include -#include +#include +#include #include #include -#include + +#include +#include Q_DECLARE_METATYPE(KDbField::TypeGroup) Q_DECLARE_METATYPE(KDbField::Type) @@ -110,9 +112,30 @@ KDBTEST_METHOD_DECL(testDriverManager, (), ()); KDBTEST_METHOD_DECL(testSqliteDriver, (), ()); - KDBTEST_METHOD_DECL(testConnect, (const KDbConnectionData &cdata), (cdata)); + + //! Connects to a database + //! @since 3.2 + KDBTEST_METHOD_DECL(testConnect, + (const KDbConnectionData &cdata, + const KDbConnectionOptions &options = KDbConnectionOptions()), + (cdata, options)); + KDBTEST_METHOD_DECL(testUse, (), ()); + //! Convenience method that performs testConnect and testUse in one go + //! @since 3.2 + KDBTEST_METHOD_DECL(testConnectAndUse, + (const KDbConnectionData &cdata, + const KDbConnectionOptions &options = KDbConnectionOptions()), + (cdata, options)); + + //! Overload of testConnectAndUse for file-based databases + //! @since 3.2 + KDBTEST_METHOD_DECL(testConnectAndUse, + (const QString &path, + const KDbConnectionOptions &options = KDbConnectionOptions()), + (path, options)); + //! Creates database with name @a dbName //! Does not use the database. //! @todo don't hardcode SQLite here diff --git a/autotests/KDbTestUtils.cpp b/autotests/KDbTestUtils.cpp --- a/autotests/KDbTestUtils.cpp +++ b/autotests/KDbTestUtils.cpp @@ -110,23 +110,24 @@ QVERIFY2(mimeTypes.contains(KDb::defaultFileBasedDriverMimeType()), "SQLite's MIME types should include the default file based one"); } -void KDbTestUtils::testConnectInternal(const KDbConnectionData &cdata) +void KDbTestUtils::testConnectInternal(const KDbConnectionData &cdata, + const KDbConnectionOptions &options) { //qDebug() << cdata; if (!driver) { //! @todo don't hardcode SQLite here KDB_VERIFY(manager.resultable(), driver = manager.driver("org.kde.kdb.sqlite"), "Driver not found"); } - KDbConnectionOptions connOptions; + KDbConnectionOptions connOptionsOverride(options); QStringList extraSqliteExtensionPaths; extraSqliteExtensionPaths << SQLITE_LOCAL_ICU_EXTENSION_PATH; - connOptions.insert("extraSqliteExtensionPaths", extraSqliteExtensionPaths); + connOptionsOverride.insert("extraSqliteExtensionPaths", extraSqliteExtensionPaths); connection.reset(); // remove previous connection if present const int connCount = driver->connections().count(); - connection.reset(driver->createConnection(cdata, connOptions)); + connection.reset(driver->createConnection(cdata, connOptionsOverride)); KDB_VERIFY(driver, !connection.isNull(), "Failed to create connection"); QVERIFY2(cdata.driverId().isEmpty(), "Connection data has filled driver ID"); QCOMPARE(connection->data().driverId(), driver->metaData()->id()); @@ -155,6 +156,29 @@ KDB_VERIFY(connection, connection->isDatabaseUsed(), "Database not used after call to useDatabase()"); } +void KDbTestUtils::testConnectAndUseInternal(const KDbConnectionData &cdata, + const KDbConnectionOptions &options) +{ + if (!testConnect(cdata, options) || !connection) { + qWarning() << driver->result(); + QFAIL("testConnect"); + } + if (!testUse() || !connection->isDatabaseUsed()) { + qWarning() << connection->result(); + bool result = testDisconnect(); + Q_UNUSED(result); + QFAIL("testUse"); + } +} + +void KDbTestUtils::testConnectAndUseInternal(const QString &path, + const KDbConnectionOptions &options) +{ + KDbConnectionData cdata; + cdata.setDatabaseName(path); + testConnectAndUseInternal(cdata, options); +} + void KDbTestUtils::testCreateDbInternal(const QString &dbName) { //open connection diff --git a/autotests/MissingTableTest.cpp b/autotests/MissingTableTest.cpp new file mode 100644 --- /dev/null +++ b/autotests/MissingTableTest.cpp @@ -0,0 +1,89 @@ +/* This file is part of the KDE project + Copyright (C) 2018 Jarosław Staniek + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. +*/ + +#include +#include +#include + +class MissingTableTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + //! Tests if the list of tables skips name for which physical table is missing. + //! The missingTableTest.kexi file has "persons" table deleted. + void testListTables(); + void cleanupTestCase(); + void cleanup(); + +private: + //! Opens database needed for tests + bool openDatabase(const QString &path); + + KDbTestUtils m_utils; +}; + +void MissingTableTest::initTestCase() +{ +} + +void MissingTableTest::init() +{ + QString dir(QFile::decodeName(FILES_DATA_DIR)); + QVERIFY(openDatabase(dir + "/missingTableTest.kexi")); +} + + +bool MissingTableTest::openDatabase(const QString &path) +{ + KDbConnectionOptions options; + options.setReadOnly(true); + return m_utils.testConnectAndUse(path, options); +} + +void MissingTableTest::testListTables() +{ + const bool alsoSystemTables = true; + bool ok; + QStringList foundTableNames = m_utils.connection->tableNames(alsoSystemTables, &ok); + QVERIFY(ok); + + // call again with ok == nullptr + QCOMPARE(foundTableNames, m_utils.connection->tableNames(alsoSystemTables)); + + // make sure missing table is not present + std::sort(foundTableNames.begin(), foundTableNames.end()); + const QStringList expectedTables( + { "cars", "kexi__db", "kexi__fields", "kexi__objectdata", "kexi__objects" }); + QCOMPARE(foundTableNames, expectedTables); +} + +void MissingTableTest::cleanup() +{ + QVERIFY(m_utils.testDisconnect()); +} + +void MissingTableTest::cleanupTestCase() +{ +} + +QTEST_GUILESS_MAIN(MissingTableTest) + +#include "MissingTableTest.moc" diff --git a/autotests/data/missingTableTest.kexi b/autotests/data/missingTableTest.kexi new file mode 100644 index 0000000000000000000000000000000000000000..628414b59ffb618ebd9229e28bcf87c1267e90a6 GIT binary patch literal 11264 zc%1E8Pj4GV6rb5yd+oSU6@sy%B+67#RlAK{Xa!M(kcbnvL`j@9b}3a9S!?e&*|PR5 zv+JZ5;^1)Mz&GFm2fhR1f`s@0NR_ArcO)t#Zd`g{X4ZB#w)^jb2$`Q`?|Z-Z_RVCx zv-4(NN_RGFpWr%W4b#Urfes)9?_&%Afc%pHCy4Qn9?~IrF(AnPJ{A!IdMflW^b|5U z4&yIz@4t+w4r&o2l}y4;#y6`DshP|ZpXka?VYytu<>mZF0SgAFZ40jz%Z1y89lX7> zwz<4>AFmhgFJOAqBn+=LeX`H5cZ;R9+r`4_0&Y-?IJit6`kCXHZlo?>hM$*YD<6}G zcBN9ciDP+TIdZDiumz|6ifDhRk$S~%y9DRk{09xQO|wB{+d^?jbKgJc-7OfGk(%w< zv>7IAwCVZGw41&crm5?5$5WI=>SoLF`wdWLoeY5tWy+iS4hO_>@yfR8Gn6rnb(;`9S&WvWHu3UkgqflsCeL!ly zcS=Dc8tIyFnpU3qJC7V{8Xjso@P|`AQeLgwJp&r4Y!-Hwhq`B(zB#CC&+MeI_WC*S zJmBJ185i$vY{;gCpJ-YrUaKl@;Lm(a9~Y0l{`{GtprG*ogqZ&t`W~R)_?Lo$!WqGw z76tL-A`l7bibzUD?rGs_8}ro2<4M{)2%Z82OoYd&RdG5@3JJ%FB| zN9a5B1!{0bK|$fg$VJi8qRhwnhD4;}m4@AH&7UL5KEW=>39A$)BiuR>i@`XEO>nDi z9j&d-=ojK}I*2$85?+gmGX_)&u5Tc2O$OE^NC-+R@~=sa^OHeClK`3fWO<*w207V} zAA}e4KZ1S&=x_7~`W5}mMFj^rH9KsSB zSEsa6n~|m&gF4?k+_Ftyo&PB)oL`9fuR{jVq4rq&Ow{JU2VnITgBVexlaCsy`FVJ} z9;#?m9a{B5qH(H@u^Y1N;qfV(w!DzcDK); zaJfC?EZJ*N4oF$NSpsMIk{NQAY_R@+4bWHGFWM#-6#i4_T#Xs2Oa>lb46PI`kFb+< zqNgQBWmi3;NU$uajY^6}WkX>PtGi|l-)nUfvtU_^8Zrs&gi{;WWW6<4it$n1loruP i_c=+RdphfLLph#QTg(ZxjLJq|HXBv@e+7l}i+=#Xs1Qv6 literal 0 Hc$@result(); + KDbConnectionOptions options; + options.setReadOnly(true); + if (!m_utils.testConnectAndUse(path, options)) { return false; } m_parser.reset(new KDbParser(m_utils.connection.data())); @@ -63,12 +62,6 @@ return false; } #endif - if (!m_utils.testUse() || !m_utils.connection->isDatabaseUsed()) { - qWarning() << m_utils.connection->result(); - bool result = m_utils.testDisconnect(); - Q_UNUSED(result); - return false; - } return true; } diff --git a/src/KDbConnection.h b/src/KDbConnection.h --- a/src/KDbConnection.h +++ b/src/KDbConnection.h @@ -1,5 +1,5 @@ /* This file is part of the KDE project - Copyright (C) 2003-2017 Jarosław Staniek + Copyright (C) 2003-2018 Jarosław Staniek This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public @@ -1002,6 +1002,32 @@ @return true on success. */ virtual bool drv_getServerVersion(KDbServerVersionInfo* version) = 0; + /** + * LOW LEVEL METHOD. Obtains a list containing names of all physical + * tables of this connection and returns it. + * + * @a ok must not be @c nullptr. + * + * Default implementation covers functionality of SQL backends. It executes low-level SQL + * defined by KDbDriverBehavior::GET_TABLE_NAMES_SQL string. On failure of execution or if + * KDbDriverBehavior::GET_TABLE_NAMES_SQL is empty, @a ok is set to @c false. On success @a ok + * is set to @c true. Returning empty list is not an error. + * + * If the database driver is not able to offer such a list, do not reimplement this method, it + * will just always return false and users of KDb will need to take this into account. + * + * To reimplement the method, set @a ok to @c true only on successfull obtaining of table names, + * and to @c false otherwise. + * + * This method is used by tableNames() to filter out tables names that have been found in + * project's metadata but lack related physical tables. + * + * @since 3.2 + * + * @see tableNames() + */ + virtual QStringList drv_getTableNames(bool *ok); + /*! LOW LEVEL METHOD. For implementation: returns true if table with name @a tableName exists in the database. @return @c false if it does not exist or @c cancelled if error occurred. diff --git a/src/KDbConnection.cpp b/src/KDbConnection.cpp --- a/src/KDbConnection.cpp +++ b/src/KDbConnection.cpp @@ -1006,18 +1006,34 @@ QStringList KDbConnection::tableNames(bool alsoSystemTables, bool* ok) { + QStringList result; bool success; - QStringList list = objectNames(KDb::TableObjectType, &success); - if (ok) { - *ok = success; + if (!ok) { + ok = &success; } - if (!success) { + QStringList list = objectNames(KDb::TableObjectType, ok); + if (!*ok) { m_result.prependMessage(tr("Could not retrieve list of table names.")); + return QStringList(); } - if (alsoSystemTables && success) { + if (alsoSystemTables) { list += kdbSystemTableNames(); } - return list; + const QStringList physicalTableNames = drv_getTableNames(ok); + if (!*ok) { + m_result.prependMessage(tr("Could not retrieve list of physical table names.")); + return QStringList(); + } + QSet physicalTableNamesSet; + for (const QString &name : physicalTableNames) { + physicalTableNamesSet.insert(name.toLower()); + } + for (const QString &name : list) { + if (physicalTableNamesSet.contains(name.toLower())) { + result += name; + } + } + return result; } tristate KDbConnection::containsTable(const QString &tableName) @@ -1826,6 +1842,35 @@ return dropQuery(qs); } +QStringList KDbConnection::drv_getTableNames(bool *ok) +{ + Q_ASSERT(ok); + QStringList tableNames; + const KDbEscapedString sql(d->driver->behavior()->GET_TABLE_NAMES_SQL); + if (sql.isEmpty()) { + *ok = false; + return QStringList(); + } + QSharedPointer result = prepareSql(sql); + if (!result) { + *ok = false; + return QStringList(); + } + Q_FOREVER { + QSharedPointer record = result->fetchRecord(); + if (!record) { + if (result->lastResult().isError()) { + *ok = false; + return QStringList(); + } + break; + } + tableNames.append(record->stringValue(0)); + } + *ok = true; + return tableNames; +} + bool KDbConnection::drv_createTable(const KDbTableSchema& tableSchema) { const KDbNativeStatementBuilder builder(this, KDb::DriverEscaping); diff --git a/src/KDbConnectionProxy.h b/src/KDbConnectionProxy.h --- a/src/KDbConnectionProxy.h +++ b/src/KDbConnectionProxy.h @@ -294,6 +294,11 @@ bool drv_getServerVersion(KDbServerVersionInfo* version) override; + /** + * @since 3.2 + */ + QStringList drv_getTableNames(bool *ok) override; + tristate drv_containsTable(const QString &tableName) override; bool drv_createTable(const KDbTableSchema& tableSchema) override; diff --git a/src/KDbConnectionProxy.cpp b/src/KDbConnectionProxy.cpp --- a/src/KDbConnectionProxy.cpp +++ b/src/KDbConnectionProxy.cpp @@ -591,6 +591,12 @@ return d->connection->drv_getServerVersion(version); } +QStringList KDbConnectionProxy::drv_getTableNames(bool *ok) +{ + Q_ASSERT(ok); + return d->connection->drv_getTableNames(ok); +} + tristate KDbConnectionProxy::drv_containsTable(const QString &tableName) { return d->connection->drv_containsTable(tableName); diff --git a/src/KDbDriverBehavior.h b/src/KDbDriverBehavior.h --- a/src/KDbDriverBehavior.h +++ b/src/KDbDriverBehavior.h @@ -1,5 +1,5 @@ /* This file is part of the KDE project - Copyright (C) 2003-2017 Jarosław Staniek + Copyright (C) 2003-2018 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public @@ -219,6 +219,15 @@ expressions. */ QString RANDOM_FUNCTION; + /** + * SQL statement used to obtain list of physical table names. + * Used by default implementation of KDbConnection::drv_getTableNames(). Empty by default. + * If empty, default implementation of KDbConnection::drv_getTableNames() fails. + * + * @since 3.2 + */ + KDbEscapedString GET_TABLE_NAMES_SQL; + private: void initInternalProperties(); friend class KDbDriver; diff --git a/src/drivers/mysql/MysqlDriver.cpp b/src/drivers/mysql/MysqlDriver.cpp --- a/src/drivers/mysql/MysqlDriver.cpp +++ b/src/drivers/mysql/MysqlDriver.cpp @@ -21,11 +21,10 @@ */ #include "MysqlDriver.h" -#include "MysqlConnection.h" #include "KDbDriverBehavior.h" #include "KDbExpression.h" -#include "KDbField.h" -#include "KDb.h" +#include "KDbPreparedStatement.h" +#include "MysqlConnection.h" #include @@ -54,6 +53,8 @@ //! @todo add configuration option beh->TEXT_TYPE_MAX_LENGTH = 255; beh->RANDOM_FUNCTION = QLatin1String("RAND"); + beh->GET_TABLE_NAMES_SQL = KDbEscapedString("SHOW TABLES"); + initDriverSpecificKeywords(keywords); //predefined properties diff --git a/src/drivers/postgresql/PostgresqlDriver.cpp b/src/drivers/postgresql/PostgresqlDriver.cpp --- a/src/drivers/postgresql/PostgresqlDriver.cpp +++ b/src/drivers/postgresql/PostgresqlDriver.cpp @@ -57,6 +57,9 @@ beh->BOOLEAN_TRUE_LITERAL = QLatin1String("TRUE"); beh->BOOLEAN_FALSE_LITERAL = QLatin1String("FALSE"); beh->USE_TEMPORARY_DATABASE_FOR_CONNECTION_IF_NEEDED = true; + beh->GET_TABLE_NAMES_SQL = KDbEscapedString( + "SELECT table_name FROM information_schema.tables WHERE " + "table_type='BASE TABLE' AND table_schema NOT IN ('pg_catalog', 'information_schema')"); initDriverSpecificKeywords(m_keywords); initPgsqlToKDbMap(); diff --git a/src/drivers/sqlite/SqliteDriver.cpp b/src/drivers/sqlite/SqliteDriver.cpp --- a/src/drivers/sqlite/SqliteDriver.cpp +++ b/src/drivers/sqlite/SqliteDriver.cpp @@ -69,6 +69,8 @@ beh->CONNECTION_REQUIRED_TO_CHECK_DB_EXISTENCE = false; beh->CONNECTION_REQUIRED_TO_CREATE_DB = false; beh->CONNECTION_REQUIRED_TO_DROP_DB = false; + beh->GET_TABLE_NAMES_SQL + = KDbEscapedString("SELECT name FROM sqlite_master WHERE type='table'"); initDriverSpecificKeywords(keywords);