diff --git a/autotests/parser/SqlParserTest.cpp b/autotests/parser/SqlParserTest.cpp index 766e101b..4245f235 100644 --- a/autotests/parser/SqlParserTest.cpp +++ b/autotests/parser/SqlParserTest.cpp @@ -1,372 +1,374 @@ /* This file is part of the KDE project Copyright (C) 2012-2017 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 "SqlParserTest.h" #include #include #include #include #include #include Q_DECLARE_METATYPE(KDbEscapedString) QTEST_GUILESS_MAIN(SqlParserTest) void SqlParserTest::initTestCase() { QString dir(QFile::decodeName(OUTPUT_DIR)); QString fname("errors.txt"); m_errorFile.setFileName(dir + QDir::separator() + fname); QVERIFY2(m_errorFile.open(QFile::WriteOnly | QFile::Text), qPrintable(QString("Cannot open %1 file").arg(m_errorFile.fileName()))); m_errorStream.setDevice(&m_errorFile); } bool SqlParserTest::openDatabase(const QString &path) { KDbConnectionData cdata; cdata.setDatabaseName(path); if (!m_utils.testConnect(cdata) || !m_utils.connection) { qWarning() << m_utils.driver->result(); return false; } m_parser.reset(new KDbParser(m_utils.connection.data())); #if 0 if (m_conn->databaseExists(dbName)) { if (!m_conn->dropDatabase(dbName)) { m_conn->disconnect(); return false; } qDebug() << "Database" << dbName << "dropped."; } if (!m_conn->createDatabase(dbName)) { qDebug() << m_conn->result(); m_conn->disconnect(); 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; } static void eatComment(QString* string) { if (!string->startsWith("--")) { return; } int i = 0; for (; i < string->length() && string->at(i) == '-'; ++i) ; QString result = string->mid(i).trimmed(); *string = result; } static void eatEndLines(QString* string) { if (!string->endsWith("--")) { return; } int i = string->length() - 1; for (; i >= 0 && string->at(i) == '-'; --i) ; *string = string->left(i+1).trimmed(); } static void eatEndComment(QString* string) { int pos = string->indexOf("; --"); if (pos == -1) { return; } string->truncate(pos); *string = string->trimmed() + ';'; } void SqlParserTest::testParse_data() { QTest::addColumn("fname"); QTest::addColumn("lineNum"); QTest::addColumn("sql"); QTest::addColumn("expectError"); QString dir(QFile::decodeName(FILES_DATA_DIR)); QString fname("statements.txt"); QFile input(dir + QDir::separator() + fname); bool ok = input.open(QFile::ReadOnly | QFile::Text); QVERIFY2(ok, qPrintable(QString("Could not open data file %1").arg(input.fileName()))); QTextStream in(&input); QString category; QString testName; bool expectError = false; int lineNum = 1; QString dbPath; bool clearTestName = false; for (; !in.atEnd(); ++lineNum) { QString line(in.readLine()); if (line.startsWith("--")) { // comment eatComment(&line); eatEndLines(&line); if (line.startsWith("TODO:")) { continue; } else if (line.startsWith("CATEGORY: ")) { if (clearTestName) { expectError = false; clearTestName = false; testName.clear(); } category = line.mid(QString("CATEGORY: ").length()).trimmed(); //qDebug() << "CATEGORY:" << category; } else if (line == "QUIT") { break; } else if (line.startsWith("SQLITEFILE: ")) { if (clearTestName) { expectError = false; clearTestName = false; testName.clear(); } ok = dbPath.isEmpty(); QVERIFY2(ok, qPrintable(QString("Error at line %1: SQLite file was already specified (%2)") .arg(lineNum).arg(dbPath))); dbPath = line.mid(QString("SQLITEFILE: ").length()).trimmed(); dbPath = dir + QDir::separator() + dbPath; ok = openDatabase(dbPath); QVERIFY2(ok, qPrintable(QString("Error at line %1: Could not open SQLite file %2") .arg(lineNum).arg(dbPath))); } else if (line.startsWith("ERROR: ")) { if (clearTestName) { clearTestName = false; testName.clear(); } expectError = true; testName = line.mid(QString("ERROR: ").length()).trimmed(); } else { if (clearTestName) { expectError = false; clearTestName = false; testName.clear(); } if (!testName.isEmpty()) { testName.append(" "); } testName.append(line); } } else { eatEndComment(&line); KDbEscapedString sql(line.trimmed()); clearTestName = true; if (sql.isEmpty()) { expectError = false; continue; } ok = !dbPath.isEmpty(); QVERIFY2(ok, qPrintable(QString("Error at line %1: SQLite file was not specified, " "could not execute statement").arg(lineNum))); QTest::newRow(qPrintable(QString("file %1:%2, category '%3', test '%4', sql '%5'%6") .arg(fname).arg(lineNum).arg(category).arg(testName).arg(sql.toString()) .arg(expectError ? ", error expected" : ""))) << fname << lineNum << sql << expectError; } } input.close(); } void SqlParserTest::testParse() { QFETCH(QString, fname); QFETCH(int, lineNum); QFETCH(KDbEscapedString, sql); QFETCH(bool, expectError); QString message; if (!sql.endsWith(';')) { message = QString("%1:%2: Missing ';' at the end of line").arg(fname).arg(lineNum); m_errorStream << fname << ':' << lineNum << ' ' << message << endl; QVERIFY2(sql.endsWith(';'), qPrintable(message)); } sql.chop(1); //qDebug() << "SQL:" << sql.toString() << expectError; // 1. Parse KDbParser *parser = m_parser.data(); bool ok = parser->parse(sql); QScopedPointer query(parser->query()); + QCOMPARE(parser->query(), nullptr); // second call should always return nullptr ok = ok && query; if (ok) { // sucess, so error cannot be expected ok = !expectError; message = "Unexpected success of parsing SQL statement"; if (!ok) { m_errorStream << fname << ':' << lineNum << ' ' << message << endl; - if (parser->query()) { - const KDbConnectionAndQuerySchema connQuery(parser->connection(), *parser->query()); + if (query) { + const KDbConnectionAndQuerySchema connQuery(parser->connection(), *query); qDebug() << connQuery; m_errorStream << KDbUtils::debugString(connQuery) << endl; } } QVERIFY2(ok, qPrintable(message)); } else { // failure, so error should be expected ok = expectError; message = QString("%1; Failed to parse SQL Statement:\n\"%2\"\n %3^\n") .arg(KDbUtils::debugString(parser->error()), sql.toString(), QString(parser->error().position() - 1, QChar(' '))); if (ok) { qDebug() << parser->error(); } else { m_errorStream << fname << ':' << lineNum << message << endl; } QVERIFY2(ok, qPrintable(message)); } //! @todo support more drivers if (query) { // 2. Build native SQL for SQLite QList params; KDbNativeStatementBuilder builder(m_utils.connection.data(), KDb::DriverEscaping); KDbEscapedString querySql; ok = builder.generateSelectStatement(&querySql, query.data(), params); QVERIFY2(ok, "Failed to generate native SQLite SQL statement from query"); //! @todo compare with template } if (query) { // 3. Build KDbSQL QList params; KDbNativeStatementBuilder builder(m_utils.connection.data(), KDb::KDbEscaping); KDbEscapedString querySql; ok = builder.generateSelectStatement(&querySql, query.data(), params); QVERIFY2(ok, "Failed to generate KDbSQL statement from query"); //! @todo compare with template // 3.1. Parse the generated KDbSQL again ok = parser->parse(querySql); QScopedPointer secondQuery(parser->query()); + QCOMPARE(parser->query(), nullptr); // second call should always return nullptr ok = ok && secondQuery; QVERIFY2(ok, "Failed to parse generated KDbSQL statement again"); // 3.2. Compare the original query from step #1 with this query ok = *query == *secondQuery; QVERIFY2(ok, "Original query differs from repeatedly parsed query"); } } void SqlParserTest::testTokens() { KDbToken t = KDbToken::LEFT; //qDebug() << t << t.toChar() << t.value() << t.isValid(); t = '+'; //qDebug() << t << t.toChar() << t.value() << t.isValid(); t = KDbToken(); //qDebug() << t << t.toChar() << t.value() << t.isValid(); QCOMPARE(KDbToken::SQL_TYPE.value(), 258); QCOMPARE(KDbToken::AS.value(), 259); QCOMPARE(KDbToken::AS_EMPTY.value(), 260); QCOMPARE(KDbToken::ASC.value(), 261); QCOMPARE(KDbToken::AUTO_INCREMENT.value(), 262); QCOMPARE(KDbToken::BIT.value(), 263); QCOMPARE(KDbToken::BITWISE_SHIFT_LEFT.value(), 264); QCOMPARE(KDbToken::BITWISE_SHIFT_RIGHT.value(), 265); QCOMPARE(KDbToken::BY.value(), 266); QCOMPARE(KDbToken::CHARACTER_STRING_LITERAL.value(), 267); QCOMPARE(KDbToken::CONCATENATION.value(), 268); QCOMPARE(KDbToken::CREATE.value(), 269); QCOMPARE(KDbToken::DESC.value(), 270); QCOMPARE(KDbToken::DISTINCT.value(), 271); QCOMPARE(KDbToken::DOUBLE_QUOTED_STRING.value(), 272); QCOMPARE(KDbToken::FROM.value(), 273); QCOMPARE(KDbToken::JOIN.value(), 274); QCOMPARE(KDbToken::KEY.value(), 275); QCOMPARE(KDbToken::LEFT.value(), 276); QCOMPARE(KDbToken::LESS_OR_EQUAL.value(), 277); QCOMPARE(KDbToken::GREATER_OR_EQUAL.value(), 278); QCOMPARE(KDbToken::SQL_NULL.value(), 279); QCOMPARE(KDbToken::SQL_IS.value(), 280); QCOMPARE(KDbToken::SQL_IS_NULL.value(), 281); QCOMPARE(KDbToken::SQL_IS_NOT_NULL.value(), 282); QCOMPARE(KDbToken::ORDER.value(), 283); QCOMPARE(KDbToken::PRIMARY.value(), 284); QCOMPARE(KDbToken::SELECT.value(), 285); QCOMPARE(KDbToken::INTEGER_CONST.value(), 286); QCOMPARE(KDbToken::REAL_CONST.value(), 287); QCOMPARE(KDbToken::RIGHT.value(), 288); QCOMPARE(KDbToken::SQL_ON.value(), 289); QCOMPARE(KDbToken::DATE_CONST.value(), 290); QCOMPARE(KDbToken::DATETIME_CONST.value(), 291); QCOMPARE(KDbToken::TIME_CONST.value(), 292); QCOMPARE(KDbToken::TABLE.value(), 293); QCOMPARE(KDbToken::IDENTIFIER.value(), 294); QCOMPARE(KDbToken::IDENTIFIER_DOT_ASTERISK.value(), 295); QCOMPARE(KDbToken::QUERY_PARAMETER.value(), 296); QCOMPARE(KDbToken::VARCHAR.value(), 297); QCOMPARE(KDbToken::WHERE.value(), 298); QCOMPARE(KDbToken::SQL.value(), 299); QCOMPARE(KDbToken::SQL_TRUE.value(), 300); QCOMPARE(KDbToken::SQL_FALSE.value(), 301); QCOMPARE(KDbToken::UNION.value(), 302); QCOMPARE(KDbToken::SCAN_ERROR.value(), 303); QCOMPARE(KDbToken::AND.value(), 304); QCOMPARE(KDbToken::BETWEEN.value(), 305); QCOMPARE(KDbToken::NOT_BETWEEN.value(), 306); QCOMPARE(KDbToken::EXCEPT.value(), 307); QCOMPARE(KDbToken::SQL_IN.value(), 308); QCOMPARE(KDbToken::INTERSECT.value(), 309); QCOMPARE(KDbToken::LIKE.value(), 310); QCOMPARE(KDbToken::ILIKE.value(), 311); QCOMPARE(KDbToken::NOT_LIKE.value(), 312); QCOMPARE(KDbToken::NOT.value(), 313); QCOMPARE(KDbToken::NOT_EQUAL.value(), 314); QCOMPARE(KDbToken::NOT_EQUAL2.value(), 315); QCOMPARE(KDbToken::OR.value(), 316); QCOMPARE(KDbToken::SIMILAR_TO.value(), 317); QCOMPARE(KDbToken::NOT_SIMILAR_TO.value(), 318); QCOMPARE(KDbToken::XOR.value(), 319); QCOMPARE(KDbToken::UMINUS.value(), 320); //! @todo add extra tokens: BETWEEN_AND, NOT_BETWEEN_AND } void SqlParserTest::cleanupTestCase() { QVERIFY(m_utils.testDisconnect()); m_errorFile.close(); #if 0 if (!m_conn->dropDatabase()) { qDebug() << m_conn->result(); } qDebug() << "Database" << m_conn->data().databaseName() << "dropped."; #endif } diff --git a/src/KDbConnection.cpp b/src/KDbConnection.cpp index 2bb874bd..e53bb332 100644 --- a/src/KDbConnection.cpp +++ b/src/KDbConnection.cpp @@ -1,3494 +1,3502 @@ /* This file is part of the KDE project Copyright (C) 2003-2017 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 License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KDbConnection.h" #include "KDbConnection_p.h" #include "KDbCursor.h" #include "KDbDriverBehavior.h" #include "KDbDriverMetaData.h" #include "KDbDriver_p.h" #include "KDbLookupFieldSchema.h" #include "KDbNativeStatementBuilder.h" #include "KDbQuerySchema.h" #include "KDbQuerySchema_p.h" #include "KDbRecordData.h" #include "KDbRecordEditBuffer.h" #include "KDbRelationship.h" #include "KDbSqlRecord.h" #include "KDbSqlResult.h" #include "KDbTableOrQuerySchema.h" #include "KDbTableSchemaChangeListener.h" #include "KDbTransactionData.h" #include "KDbTransactionGuard.h" #include "kdb_debug.h" #include #include #include /*! Version number of extended table schema. List of changes: * 2: (Kexi 2.5.0) Added maxLengthIsDefault property (type: bool, if true, KDbField::maxLengthStrategy() == KDbField::DefaultMaxLength) * 1: (Kexi 1.x) Initial version */ #define KDB_EXTENDED_TABLE_SCHEMA_VERSION 2 KDbConnectionInternal::KDbConnectionInternal(KDbConnection *conn) : connection(conn) { } class CursorDeleter { public: explicit CursorDeleter(KDbCursor *cursor) { delete cursor; } }; //================================================ class Q_DECL_HIDDEN KDbConnectionOptions::Private { public: Private() : connection(nullptr) {} Private(const Private &other) { copy(other); } #define KDbConnectionOptionsPrivateArgs(o) std::tie(o.connection) void copy(const Private &other) { KDbConnectionOptionsPrivateArgs((*this)) = KDbConnectionOptionsPrivateArgs(other); } bool operator==(const Private &other) const { return KDbConnectionOptionsPrivateArgs((*this)) == KDbConnectionOptionsPrivateArgs(other); } KDbConnection *connection; }; KDbConnectionOptions::KDbConnectionOptions() : d(new Private) { KDbUtils::PropertySet::insert("readOnly", false, tr("Read only", "Read only connection")); } KDbConnectionOptions::KDbConnectionOptions(const KDbConnectionOptions &other) : KDbUtils::PropertySet(other) , d(new Private(*other.d)) { } KDbConnectionOptions::~KDbConnectionOptions() { delete d; } KDbConnectionOptions& KDbConnectionOptions::operator=(const KDbConnectionOptions &other) { if (this != &other) { KDbUtils::PropertySet::operator=(other); d->copy(*other.d); } return *this; } bool KDbConnectionOptions::operator==(const KDbConnectionOptions &other) const { return KDbUtils::PropertySet::operator==(other) && *d == *other.d; } bool KDbConnectionOptions::isReadOnly() const { return property("readOnly").value().toBool(); } void KDbConnectionOptions::insert(const QByteArray &name, const QVariant &value, const QString &caption) { if (name == "readOnly") { setReadOnly(value.toBool()); return; } QString realCaption; if (property(name).caption().isEmpty()) { // don't allow to change the caption realCaption = caption; } KDbUtils::PropertySet::insert(name, value, realCaption); } void KDbConnectionOptions::setCaption(const QByteArray &name, const QString &caption) { if (name == "readOnly") { return; } KDbUtils::PropertySet::setCaption(name, caption); } void KDbConnectionOptions::setValue(const QByteArray &name, const QVariant &value) { if (name == "readOnly") { setReadOnly(value.toBool()); return; } KDbUtils::PropertySet::setValue(name, value); } void KDbConnectionOptions::remove(const QByteArray &name) { if (name == "readOnly") { return; } KDbUtils::PropertySet::remove(name); } void KDbConnectionOptions::setReadOnly(bool set) { if (d->connection && d->connection->isConnected()) { return; //sanity } KDbUtils::PropertySet::setValue("readOnly", set); } void KDbConnectionOptions::setConnection(KDbConnection *connection) { d->connection = connection; } //================================================ KDbConnectionPrivate::KDbConnectionPrivate(KDbConnection* const conn, KDbDriver *drv, const KDbConnectionData& _connData, const KDbConnectionOptions &_options) : conn(conn) , connData(_connData) , options(_options) , driver(drv) , dbProperties(conn) { options.setConnection(conn); } KDbConnectionPrivate::~KDbConnectionPrivate() { options.setConnection(nullptr); deleteAllCursors(); delete m_parser; qDeleteAll(tableSchemaChangeListeners); qDeleteAll(obsoleteQueries); } void KDbConnectionPrivate::deleteAllCursors() { QSet cursorsToDelete(cursors); cursors.clear(); for(KDbCursor* c : cursorsToDelete) { CursorDeleter deleter(c); } } void KDbConnectionPrivate::errorInvalidDBContents(const QString& details) { conn->m_result = KDbResult(ERR_INVALID_DATABASE_CONTENTS, KDbConnection::tr("Invalid database contents. %1").arg(details)); } QString KDbConnectionPrivate::strItIsASystemObject() const { return KDbConnection::tr("It is a system object."); } void KDbConnectionPrivate::setupKDbSystemSchema() { if (!m_internalKDbTables.isEmpty()) { return; //already set up } { KDbInternalTableSchema *t_objects = new KDbInternalTableSchema(QLatin1String("kexi__objects")); t_objects->addField(new KDbField(QLatin1String("o_id"), KDbField::Integer, KDbField::PrimaryKey | KDbField::AutoInc, KDbField::Unsigned)); t_objects->addField(new KDbField(QLatin1String("o_type"), KDbField::Byte, nullptr, KDbField::Unsigned)); t_objects->addField(new KDbField(QLatin1String("o_name"), KDbField::Text)); t_objects->addField(new KDbField(QLatin1String("o_caption"), KDbField::Text)); t_objects->addField(new KDbField(QLatin1String("o_desc"), KDbField::LongText)); //kdbDebug() << *t_objects; insertTable(t_objects); } { KDbInternalTableSchema *t_objectdata = new KDbInternalTableSchema(QLatin1String("kexi__objectdata")); t_objectdata->addField(new KDbField(QLatin1String("o_id"), KDbField::Integer, KDbField::NotNull, KDbField::Unsigned)); t_objectdata->addField(new KDbField(QLatin1String("o_data"), KDbField::LongText)); t_objectdata->addField(new KDbField(QLatin1String("o_sub_id"), KDbField::Text)); insertTable(t_objectdata); } { KDbInternalTableSchema *t_fields = new KDbInternalTableSchema(QLatin1String("kexi__fields")); t_fields->addField(new KDbField(QLatin1String("t_id"), KDbField::Integer, nullptr, KDbField::Unsigned)); t_fields->addField(new KDbField(QLatin1String("f_type"), KDbField::Byte, nullptr, KDbField::Unsigned)); t_fields->addField(new KDbField(QLatin1String("f_name"), KDbField::Text)); t_fields->addField(new KDbField(QLatin1String("f_length"), KDbField::Integer)); t_fields->addField(new KDbField(QLatin1String("f_precision"), KDbField::Integer)); t_fields->addField(new KDbField(QLatin1String("f_constraints"), KDbField::Integer)); t_fields->addField(new KDbField(QLatin1String("f_options"), KDbField::Integer)); t_fields->addField(new KDbField(QLatin1String("f_default"), KDbField::Text)); //these are additional properties: t_fields->addField(new KDbField(QLatin1String("f_order"), KDbField::Integer)); t_fields->addField(new KDbField(QLatin1String("f_caption"), KDbField::Text)); t_fields->addField(new KDbField(QLatin1String("f_help"), KDbField::LongText)); insertTable(t_fields); } { KDbInternalTableSchema *t_db = new KDbInternalTableSchema(QLatin1String("kexi__db")); t_db->addField(new KDbField(QLatin1String("db_property"), KDbField::Text, KDbField::NoConstraints, KDbField::NoOptions, 32)); t_db->addField(new KDbField(QLatin1String("db_value"), KDbField::LongText)); insertTable(t_db); } } void KDbConnectionPrivate::insertTable(KDbTableSchema* tableSchema) { KDbInternalTableSchema* internalTable = dynamic_cast(tableSchema); if (internalTable) { m_internalKDbTables.insert(internalTable); } else { m_tables.insert(tableSchema->id(), tableSchema); } m_tablesByName.insert(tableSchema->name(), tableSchema); } void KDbConnectionPrivate::removeTable(int id) { QScopedPointer toDelete(m_tables.take(id)); if (!toDelete) { kdbWarning() << "Could not find table to delete with id=" << id; return; } KDbTableSchemaChangeListener::unregisterForChanges(conn, toDelete.data()); const int count = m_tablesByName.remove(toDelete->name()); Q_ASSERT_X(count == 1, "KDbConnectionPrivate::removeTable", "Table to remove not found"); } void KDbConnectionPrivate::takeTable(KDbTableSchema* tableSchema) { if (m_tables.isEmpty()) { return; } m_tables.take(tableSchema->id()); m_tablesByName.take(tableSchema->name()); } void KDbConnectionPrivate::renameTable(KDbTableSchema* tableSchema, const QString& newName) { m_tablesByName.take(tableSchema->name()); tableSchema->setName(newName); m_tablesByName.insert(tableSchema->name(), tableSchema); } void KDbConnectionPrivate::changeTableId(KDbTableSchema* tableSchema, int newId) { m_tables.take(tableSchema->id()); m_tables.insert(newId, tableSchema); } void KDbConnectionPrivate::clearTables() { m_tablesByName.clear(); qDeleteAll(m_internalKDbTables); m_internalKDbTables.clear(); QHash tablesToDelete(m_tables); m_tables.clear(); qDeleteAll(tablesToDelete); } void KDbConnectionPrivate::insertQuery(KDbQuerySchema* query) { m_queries.insert(query->id(), query); m_queriesByName.insert(query->name(), query); } void KDbConnectionPrivate::removeQuery(KDbQuerySchema* querySchema) { m_queriesByName.remove(querySchema->name()); m_queries.remove(querySchema->id()); delete querySchema; } void KDbConnectionPrivate::setQueryObsolete(KDbQuerySchema* query) { obsoleteQueries.insert(query); m_queriesByName.take(query->name()); m_queries.take(query->id()); } void KDbConnectionPrivate::clearQueries() { qDeleteAll(m_queries); m_queries.clear(); } KDbTableSchema* KDbConnectionPrivate::setupTableSchema(KDbTableSchema *table) { Q_ASSERT(table); QScopedPointer newTable(table); KDbCursor *cursor; if (!(cursor = conn->executeQuery( KDbEscapedString("SELECT t_id, f_type, f_name, f_length, f_precision, f_constraints, " "f_options, f_default, f_order, f_caption, f_help " "FROM kexi__fields WHERE t_id=%1 ORDER BY f_order") .arg(driver->valueToSql(KDbField::Integer, table->id()))))) { return nullptr; } if (!cursor->moveFirst()) { if (!cursor->result().isError() && cursor->eof()) { conn->m_result = KDbResult(tr("Table has no fields defined.")); } conn->deleteCursor(cursor); return nullptr; } // For each field: load its schema KDbRecordData fieldData; bool ok = true; while (!cursor->eof()) { // kdbDebug()<<"@@@ f_name=="<value(2).asCString(); if (!cursor->storeCurrentRecord(&fieldData)) { ok = false; break; } KDbField *f = conn->setupField(fieldData); if (!f || !table->addField(f)) { ok = false; break; } cursor->moveNext(); } if (!ok) {//error: conn->deleteCursor(cursor); return nullptr; } if (!conn->deleteCursor(cursor)) { return nullptr; } if (!conn->loadExtendedTableSchemaData(table)) { return nullptr; } //store locally: insertTable(table); return newTable.take(); } KDbQuerySchema* KDbConnectionPrivate::setupQuerySchema(KDbQuerySchema *query) { Q_ASSERT(query); QScopedPointer newQuery(query); QString sql; if (!conn->loadDataBlock(query->id(), &sql, QLatin1String("sql"))) { conn->m_result = KDbResult( ERR_OBJECT_NOT_FOUND, tr("Could not find definition for query \"%1\". Deleting this query is recommended.") .arg(query->name())); return nullptr; } + const QString queryName(query->name()); if (!parser()->parse(KDbEscapedString(sql), query)) { + newQuery.take(); // query is destroyed by the parser conn->m_result = KDbResult( ERR_SQL_PARSE_ERROR, tr("

Could not load definition for query \"%1\". " "SQL statement for this query is invalid:
%2

\n" "

This query can be edited only in Text View.

") - .arg(query->name(), sql)); + .arg(queryName, sql)); return nullptr; } insertQuery(query); return newQuery.take(); } KDbQuerySchemaFieldsExpanded *KDbConnectionPrivate::fieldsExpanded(const KDbQuerySchema *query) { return m_fieldsExpandedCache[query]; } void KDbConnectionPrivate::insertFieldsExpanded(const KDbQuerySchema *query, KDbQuerySchemaFieldsExpanded *cache) { m_fieldsExpandedCache.insert(query, cache); } +void KDbConnectionPrivate::removeFieldsExpanded(const KDbQuerySchema *query) +{ + kdbDebug() << "**CACHE REMOVE**" << query; + m_fieldsExpandedCache.remove(query); +} + //================================================ namespace { //! @internal static: list of internal KDb system table names class SystemTables : public QStringList { public: SystemTables() : QStringList({ QLatin1String("kexi__objects"), QLatin1String("kexi__objectdata"), QLatin1String("kexi__fields"), QLatin1String("kexi__db")}) {} }; } Q_GLOBAL_STATIC(SystemTables, g_kdbSystemTableNames) KDbConnection::KDbConnection(KDbDriver *driver, const KDbConnectionData& connData, const KDbConnectionOptions &options) : d(new KDbConnectionPrivate(this, driver, connData, options)) { if (d->connData.driverId().isEmpty()) { d->connData.setDriverId(d->driver->metaData()->id()); } } void KDbConnection::destroy() { disconnect(); //do not allow the driver to touch me: I will kill myself. d->driver->d->connections.remove(this); } KDbConnection::~KDbConnection() { KDbConnectionPrivate *thisD = d; d = nullptr; // make sure d is nullptr before destructing delete thisD; } KDbConnectionData KDbConnection::data() const { return d->connData; } KDbDriver* KDbConnection::driver() const { return d->driver; } bool KDbConnection::connect() { clearResult(); if (d->isConnected) { m_result = KDbResult(ERR_ALREADY_CONNECTED, tr("Connection already established.")); return false; } d->serverVersion.clear(); if (!(d->isConnected = drv_connect())) { if (m_result.code() == ERR_NONE) { m_result.setCode(ERR_OTHER); } m_result.setMessage(d->driver->metaData()->isFileBased() ? tr("Could not open \"%1\" project file.") .arg(QDir::fromNativeSeparators(QFileInfo(d->connData.databaseName()).fileName())) : tr("Could not connect to \"%1\" database server.") .arg(d->connData.toUserVisibleString())); } if (d->isConnected && !d->driver->behavior()->USING_DATABASE_REQUIRED_TO_CONNECT) { if (!drv_getServerVersion(&d->serverVersion)) return false; } return d->isConnected; } bool KDbConnection::isDatabaseUsed() const { return !d->usedDatabase.isEmpty() && d->isConnected && drv_isDatabaseUsed(); } void KDbConnection::clearResult() { KDbResultable::clearResult(); } bool KDbConnection::disconnect() { clearResult(); if (!d->isConnected) return true; if (!closeDatabase()) return false; bool ok = drv_disconnect(); if (ok) d->isConnected = false; return ok; } bool KDbConnection::isConnected() const { return d->isConnected; } bool KDbConnection::checkConnected() { if (d->isConnected) { clearResult(); return true; } m_result = KDbResult(ERR_NO_CONNECTION, tr("Not connected to the database server.")); return false; } bool KDbConnection::checkIsDatabaseUsed() { if (isDatabaseUsed()) { clearResult(); return true; } m_result = KDbResult(ERR_NO_DB_USED, tr("Currently no database is used.")); return false; } QStringList KDbConnection::databaseNames(bool also_system_db) { //kdbDebug() << also_system_db; if (!checkConnected()) return QStringList(); QString tmpdbName; //some engines need to have opened any database before executing "create database" if (!useTemporaryDatabaseIfNeeded(&tmpdbName)) return QStringList(); QStringList list; bool ret = drv_getDatabasesList(&list); if (!tmpdbName.isEmpty()) { //whatever result is - now we have to close temporary opened database: if (!closeDatabase()) return QStringList(); } if (!ret) return QStringList(); if (also_system_db) return list; //filter system databases: for (QMutableListIterator it(list); it.hasNext();) { if (d->driver->isSystemDatabaseName(it.next())) { it.remove(); } } return list; } bool KDbConnection::drv_getDatabasesList(QStringList* list) { list->clear(); return true; } bool KDbConnection::drv_databaseExists(const QString &dbName, bool ignoreErrors) { QStringList list = databaseNames(true);//also system if (m_result.isError()) { return false; } if (list.indexOf(dbName) == -1) { if (!ignoreErrors) m_result = KDbResult(ERR_OBJECT_NOT_FOUND, tr("The database \"%1\" does not exist.").arg(dbName)); return false; } return true; } bool KDbConnection::databaseExists(const QString &dbName, bool ignoreErrors) { // kdbDebug() << dbName << ignoreErrors; if (d->driver->behavior()->CONNECTION_REQUIRED_TO_CHECK_DB_EXISTENCE && !checkConnected()) return false; clearResult(); if (d->driver->metaData()->isFileBased()) { //for file-based db: file must exists and be accessible QFileInfo file(d->connData.databaseName()); if (!file.exists() || (!file.isFile() && !file.isSymLink())) { if (!ignoreErrors) m_result = KDbResult(ERR_OBJECT_NOT_FOUND, tr("The database file \"%1\" does not exist.") .arg(QDir::fromNativeSeparators(QFileInfo(d->connData.databaseName()).fileName()))); return false; } if (!file.isReadable()) { if (!ignoreErrors) m_result = KDbResult(ERR_ACCESS_RIGHTS, tr("Database file \"%1\" is not readable.") .arg(QDir::fromNativeSeparators(QFileInfo(d->connData.databaseName()).fileName()))); return false; } if (!d->options.isReadOnly() && !file.isWritable()) { if (!ignoreErrors) m_result = KDbResult(ERR_ACCESS_RIGHTS, tr("Database file \"%1\" is not writable.") .arg(QDir::fromNativeSeparators(QFileInfo(d->connData.databaseName()).fileName()))); return false; } return true; } QString tmpdbName; //some engines need to have opened any database before executing "create database" const bool orig_skipDatabaseExistsCheckInUseDatabase = d->skipDatabaseExistsCheckInUseDatabase; d->skipDatabaseExistsCheckInUseDatabase = true; bool ret = useTemporaryDatabaseIfNeeded(&tmpdbName); d->skipDatabaseExistsCheckInUseDatabase = orig_skipDatabaseExistsCheckInUseDatabase; if (!ret) return false; ret = drv_databaseExists(dbName, ignoreErrors); if (!tmpdbName.isEmpty()) { //whatever result is - now we have to close temporary opened database: if (!closeDatabase()) return false; } return ret; } #define createDatabase_CLOSE \ { if (!closeDatabase()) { \ m_result = KDbResult(KDbConnection::tr("Database \"%1\" has been created but " \ "could not be closed after creation.").arg(dbName)); \ return false; \ } } #define createDatabase_ERROR \ { createDatabase_CLOSE; return false; } bool KDbConnection::createDatabase(const QString &dbName) { if (d->driver->behavior()->CONNECTION_REQUIRED_TO_CREATE_DB && !checkConnected()) return false; if (databaseExists(dbName)) { m_result = KDbResult(ERR_OBJECT_EXISTS, tr("Database \"%1\" already exists.").arg(dbName)); return false; } if (d->driver->isSystemDatabaseName(dbName)) { m_result = KDbResult(ERR_SYSTEM_NAME_RESERVED, tr("Could not create database \"%1\". This name is reserved for system database.").arg(dbName)); return false; } if (d->driver->metaData()->isFileBased()) { //update connection data if filename differs if (QFileInfo(dbName).isAbsolute()) { d->connData.setDatabaseName(dbName); } else { d->connData.setDatabaseName( QFileInfo(d->connData.databaseName()).absolutePath() + QDir::separator() + QFileInfo(dbName).fileName()); } } QString tmpdbName; //some engines need to have opened any database before executing "create database" if (!useTemporaryDatabaseIfNeeded(&tmpdbName)) return false; //low-level create if (!drv_createDatabase(dbName)) { m_result.prependMessage(tr("Error creating database \"%1\" on the server.").arg(dbName)); (void)closeDatabase();//sanity return false; } if (!tmpdbName.isEmpty()) { //whatever result is - now we have to close temporary opened database: if (!closeDatabase()) return false; } if (!tmpdbName.isEmpty() || !d->driver->behavior()->IS_DB_OPEN_AFTER_CREATE) { //db need to be opened if (!useDatabase(dbName, false/*not yet kexi compatible!*/)) { m_result = KDbResult(tr("Database \"%1\" has been created but could not be opened.").arg(dbName)); return false; } } else { //just for the rule d->usedDatabase = dbName; d->isConnected = true; } KDbTransaction trans; if (d->driver->transactionsSupported()) { trans = beginTransaction(); if (!trans.isActive()) return false; } //-create system tables schema objects d->setupKDbSystemSchema(); //-physically create internal KDb tables foreach(KDbInternalTableSchema* t, d->internalKDbTables()) { if (!drv_createTable(*t)) createDatabase_ERROR; } //-insert KDb version info: // (for compatibility with Kexi expect the legacy kexidb_major_ver/kexidb_minor_ver values) KDbTableSchema *table = d->table(QLatin1String("kexi__db")); if (!table) createDatabase_ERROR; if (!insertRecord(table, QLatin1String("kexidb_major_ver"), KDb::version().major()) || !insertRecord(table, QLatin1String("kexidb_minor_ver"), KDb::version().minor())) createDatabase_ERROR; if (trans.isActive() && !commitTransaction(trans)) createDatabase_ERROR; createDatabase_CLOSE; return true; } #undef createDatabase_CLOSE #undef createDatabase_ERROR bool KDbConnection::useDatabase(const QString &dbName, bool kexiCompatible, bool *cancelled, KDbMessageHandler* msgHandler) { if (cancelled) *cancelled = false; //kdbDebug() << dbName << kexiCompatible; if (!checkConnected()) return false; QString my_dbName; if (dbName.isEmpty()) my_dbName = d->connData.databaseName(); else my_dbName = dbName; if (my_dbName.isEmpty()) return false; if (d->usedDatabase == my_dbName) return true; //already used if (!d->skipDatabaseExistsCheckInUseDatabase) { if (!databaseExists(my_dbName, false /*don't ignore errors*/)) return false; //database must exist } if (!d->usedDatabase.isEmpty() && !closeDatabase()) //close db if already used return false; d->usedDatabase.clear(); if (!drv_useDatabase(my_dbName, cancelled, msgHandler)) { if (cancelled && *cancelled) return false; QString msg(tr("Opening database \"%1\" failed.").arg(my_dbName)); m_result.prependMessage(msg); return false; } if (d->serverVersion.isNull() && d->driver->behavior()->USING_DATABASE_REQUIRED_TO_CONNECT) { // get version just now, it was not possible earlier if (!drv_getServerVersion(&d->serverVersion)) return false; } //-create system tables schema objects d->setupKDbSystemSchema(); if (kexiCompatible && my_dbName.compare(anyAvailableDatabaseName(), Qt::CaseInsensitive) != 0) { //-get global database information bool ok; const int major = d->dbProperties.value(QLatin1String("kexidb_major_ver")).toInt(&ok); if (!ok) { m_result = d->dbProperties.result(); return false; } const int minor = d->dbProperties.value(QLatin1String("kexidb_minor_ver")).toInt(&ok); if (!ok) { m_result = d->dbProperties.result(); return false; } d->databaseVersion.setMajor(major); d->databaseVersion.setMinor(minor); } d->usedDatabase = my_dbName; return true; } bool KDbConnection::closeDatabase() { if (d->usedDatabase.isEmpty()) return true; //no db used if (!checkConnected()) return true; bool ret = true; /*! @todo (js) add CLEVER algorithm here for nested transactions */ if (d->driver->transactionsSupported()) { //rollback all transactions d->dontRemoveTransactions = true; //lock! foreach(const KDbTransaction& tr, d->transactions) { if (!rollbackTransaction(tr)) {//rollback as much as you can, don't stop on prev. errors ret = false; } else { kdbDebug() << "transaction rolled back!"; kdbDebug() << "trans.refcount==" << (tr.m_data ? QString::number(tr.m_data->refcount()) : QLatin1String("(null)")); } } d->dontRemoveTransactions = false; //unlock! d->transactions.clear(); //free trans. data } //delete own cursors: d->deleteAllCursors(); //delete own schemas d->clearTables(); d->clearQueries(); if (!drv_closeDatabase()) return false; d->usedDatabase.clear(); return ret; } QString KDbConnection::currentDatabase() const { return d->usedDatabase; } bool KDbConnection::useTemporaryDatabaseIfNeeded(QString* name) { if (d->driver->behavior()->USE_TEMPORARY_DATABASE_FOR_CONNECTION_IF_NEEDED && !isDatabaseUsed()) { //we have no db used, but it is required by engine to have used any! *name = anyAvailableDatabaseName(); if (name->isEmpty()) { m_result = KDbResult(ERR_NO_DB_USED, tr("Could not find any database for temporary connection.")); return false; } const bool orig_skipDatabaseExistsCheckInUseDatabase = d->skipDatabaseExistsCheckInUseDatabase; d->skipDatabaseExistsCheckInUseDatabase = true; bool ret = useDatabase(*name, false); d->skipDatabaseExistsCheckInUseDatabase = orig_skipDatabaseExistsCheckInUseDatabase; if (!ret) { m_result = KDbResult(m_result.code(), tr("Error during starting temporary connection using \"%1\" database name.").arg(*name)); return false; } } return true; } bool KDbConnection::dropDatabase(const QString &dbName) { if (d->driver->behavior()->CONNECTION_REQUIRED_TO_DROP_DB && !checkConnected()) return false; QString dbToDrop; if (dbName.isEmpty() && d->usedDatabase.isEmpty()) { if (!d->driver->metaData()->isFileBased() || (d->driver->metaData()->isFileBased() && d->connData.databaseName().isEmpty())) { m_result = KDbResult(ERR_NO_NAME_SPECIFIED, tr("Could not delete database. Name is not specified.")); return false; } //this is a file driver so reuse previously passed filename dbToDrop = d->connData.databaseName(); } else { if (dbName.isEmpty()) { dbToDrop = d->usedDatabase; } else { if (d->driver->metaData()->isFileBased()) //lets get full path dbToDrop = QFileInfo(dbName).absoluteFilePath(); else dbToDrop = dbName; } } if (dbToDrop.isEmpty()) { m_result = KDbResult(ERR_NO_NAME_SPECIFIED, tr("Could not delete database. Name is not specified.")); return false; } if (d->driver->isSystemDatabaseName(dbToDrop)) { m_result = KDbResult(ERR_SYSTEM_NAME_RESERVED, tr("Could not delete system database \"%1\".").arg(dbToDrop)); return false; } if (isDatabaseUsed() && d->usedDatabase == dbToDrop) { //we need to close database because cannot drop used this database if (!closeDatabase()) return false; } QString tmpdbName; //some engines need to have opened any database before executing "drop database" if (!useTemporaryDatabaseIfNeeded(&tmpdbName)) return false; //ok, now we have access to dropping bool ret = drv_dropDatabase(dbToDrop); if (!tmpdbName.isEmpty()) { //whatever result is - now we have to close temporary opened database: if (!closeDatabase()) return false; } return ret; } QStringList KDbConnection::objectNames(int objectType, bool* ok) { if (!checkIsDatabaseUsed()) { if (ok) { *ok = false; } return QStringList(); } KDbEscapedString sql; if (objectType == KDb::AnyObjectType) { sql = "SELECT o_name FROM kexi__objects ORDER BY o_id"; } else { sql = KDbEscapedString("SELECT o_name FROM kexi__objects WHERE o_type=%1" " ORDER BY o_id").arg(d->driver->valueToSql(KDbField::Integer, objectType)); } QStringList list; const bool success = queryStringListInternal(&sql, &list, nullptr, nullptr, 0, KDb::isIdentifier); if (ok) { *ok = success; } if (!success) { m_result.prependMessage(tr("Could not retrieve list of object names.")); } return list; } QStringList KDbConnection::tableNames(bool alsoSystemTables, bool* ok) { bool success; QStringList list = objectNames(KDb::TableObjectType, &success); if (ok) { *ok = success; } if (!success) { m_result.prependMessage(tr("Could not retrieve list of table names.")); } if (alsoSystemTables && success) { list += kdbSystemTableNames(); } return list; } tristate KDbConnection::containsTable(const QString &tableName) { return drv_containsTable(tableName); } QStringList KDbConnection::kdbSystemTableNames() { return *g_kdbSystemTableNames; } KDbServerVersionInfo KDbConnection::serverVersion() const { return isConnected() ? d->serverVersion : KDbServerVersionInfo(); } KDbVersionInfo KDbConnection::databaseVersion() const { return isDatabaseUsed() ? d->databaseVersion : KDbVersionInfo(); } KDbProperties KDbConnection::databaseProperties() const { return d->dbProperties; } QList KDbConnection::tableIds(bool* ok) { return objectIds(KDb::TableObjectType, ok); } QList KDbConnection::queryIds(bool* ok) { return objectIds(KDb::QueryObjectType, ok); } QList KDbConnection::objectIds(int objectType, bool* ok) { if (!checkIsDatabaseUsed()) return QList(); KDbEscapedString sql; if (objectType == KDb::AnyObjectType) sql = "SELECT o_id, o_name FROM kexi__objects ORDER BY o_id"; else sql = "SELECT o_id, o_name FROM kexi__objects WHERE o_type=" + QByteArray::number(objectType) + " ORDER BY o_id"; KDbCursor *c = executeQuery(sql); if (!c) { if (ok) { *ok = false; } m_result.prependMessage(tr("Could not retrieve list of object identifiers.")); return QList(); } QList list; for (c->moveFirst(); !c->eof(); c->moveNext()) { QString tname = c->value(1).toString(); //kexi__objects.o_name if (KDb::isIdentifier(tname)) { list.append(c->value(0).toInt()); //kexi__objects.o_id } } deleteCursor(c); if (ok) { *ok = true; } return list; } //yeah, it is very efficient: #define C_A(a) , const QVariant& c ## a #define V_A0 d->driver->valueToSql( tableSchema->field(0), c0 ) #define V_A(a) + ',' + d->driver->valueToSql( \ tableSchema->field(a) ? tableSchema->field(a)->type() : KDbField::Text, c ## a ) // kdbDebug() << "******** " << QString("INSERT INTO ") + // escapeIdentifier(tableSchema->name()) + // " VALUES (" + vals + ")"; QSharedPointer KDbConnection::insertRecordInternal(const QString &tableSchemaName, KDbFieldList *fields, const KDbEscapedString &sql) { QSharedPointer res; if (!drv_beforeInsert(tableSchemaName,fields )) { return res; } res = prepareSql(sql); if (!res || res->lastResult().isError()) { res.clear(); return res; } if (!drv_afterInsert(tableSchemaName, fields)) { res.clear(); return res; } { // Fetching is needed to perform real execution at least for some backends. // Also we are not expecting record but let's delete if there's any. QSharedPointer record = res->fetchRecord(); Q_UNUSED(record) } if (res->lastResult().isError()) { res.clear(); } return res; } #define C_INS_REC(args, vals) \ QSharedPointer KDbConnection::insertRecord(KDbTableSchema* tableSchema args) { \ return insertRecordInternal(tableSchema->name(), tableSchema, \ KDbEscapedString("INSERT INTO ") + escapeIdentifier(tableSchema->name()) \ + " (" \ + tableSchema->sqlFieldsList(this) \ + ") VALUES (" + vals + ')'); \ } #define C_INS_REC_ALL \ C_INS_REC( C_A(0), V_A0 ) \ C_INS_REC( C_A(0) C_A(1), V_A0 V_A(1) ) \ C_INS_REC( C_A(0) C_A(1) C_A(2), V_A0 V_A(1) V_A(2) ) \ C_INS_REC( C_A(0) C_A(1) C_A(2) C_A(3), V_A0 V_A(1) V_A(2) V_A(3) ) \ C_INS_REC( C_A(0) C_A(1) C_A(2) C_A(3) C_A(4), V_A0 V_A(1) V_A(2) V_A(3) V_A(4) ) \ C_INS_REC( C_A(0) C_A(1) C_A(2) C_A(3) C_A(4) C_A(5), V_A0 V_A(1) V_A(2) V_A(3) V_A(4) V_A(5) ) \ C_INS_REC( C_A(0) C_A(1) C_A(2) C_A(3) C_A(4) C_A(5) C_A(6), V_A0 V_A(1) V_A(2) V_A(3) V_A(4) V_A(5) V_A(6) ) \ C_INS_REC( C_A(0) C_A(1) C_A(2) C_A(3) C_A(4) C_A(5) C_A(6) C_A(7), V_A0 V_A(1) V_A(2) V_A(3) V_A(4) V_A(5) V_A(6) V_A(7) ) C_INS_REC_ALL #undef V_A0 #undef V_A #undef C_INS_REC #define V_A0 value += d->driver->valueToSql( it.next(), c0 ); #define V_A( a ) value += (',' + d->driver->valueToSql( it.next(), c ## a )); #define C_INS_REC(args, vals) \ QSharedPointer KDbConnection::insertRecord(KDbFieldList* fields args) \ { \ KDbEscapedString value; \ const KDbField::List *flist = fields->fields(); \ QListIterator it(*flist); \ vals \ it.toFront(); \ QString tableName((it.hasNext() && it.peekNext()->table()) ? it.next()->table()->name() : QLatin1String("??")); \ return insertRecordInternal(tableName, fields, \ KDbEscapedString(QLatin1String("INSERT INTO ") + escapeIdentifier(tableName)) \ + " (" + fields->sqlFieldsList(this) \ + ") VALUES (" + value + ')'); \ } C_INS_REC_ALL #undef C_A #undef V_A #undef V_ALAST #undef C_INS_REC #undef C_INS_REC_ALL QSharedPointer KDbConnection::insertRecord(KDbTableSchema *tableSchema, const QList &values) { // Each SQL identifier needs to be escaped in the generated query. QSharedPointer res; const KDbField::List *flist = tableSchema->fields(); if (flist->isEmpty()) { return res; } KDbField::ListIterator fieldsIt(flist->constBegin()); QList::ConstIterator it = values.constBegin(); KDbEscapedString sql; sql.reserve(4096); while (fieldsIt != flist->constEnd() && (it != values.end())) { KDbField *f = *fieldsIt; if (sql.isEmpty()) { sql = KDbEscapedString("INSERT INTO ") + escapeIdentifier(tableSchema->name()) + " VALUES ("; } else { sql += ','; } sql += d->driver->valueToSql(f, *it); // kdbDebug() << "val" << i++ << ": " << d->driver->valueToSql( f, *it ); ++it; ++fieldsIt; } sql += ')'; m_result.setSql(sql); res = insertRecordInternal(tableSchema->name(), tableSchema, sql); return res; } QSharedPointer KDbConnection::insertRecord(KDbFieldList *fields, const QList &values) { // Each SQL identifier needs to be escaped in the generated query. QSharedPointer res; const KDbField::List *flist = fields->fields(); if (flist->isEmpty()) { return res; } KDbField::ListIterator fieldsIt(flist->constBegin()); KDbEscapedString sql; sql.reserve(4096); QList::ConstIterator it = values.constBegin(); const QString tableName(flist->first()->table()->name()); while (fieldsIt != flist->constEnd() && it != values.constEnd()) { KDbField *f = *fieldsIt; if (sql.isEmpty()) { sql = KDbEscapedString("INSERT INTO ") + escapeIdentifier(tableName) + '(' + fields->sqlFieldsList(this) + ") VALUES ("; } else { sql += ','; } sql += d->driver->valueToSql(f, *it); // kdbDebug() << "val" << i++ << ": " << d->driver->valueToSql( f, *it ); ++it; ++fieldsIt; if (fieldsIt == flist->constEnd()) break; } sql += ')'; m_result.setSql(sql); res = insertRecordInternal(tableName, fields, sql); return res; } inline static bool checkSql(const KDbEscapedString& sql, KDbResult* result) { Q_ASSERT(result); if (!sql.isValid()) { *result = KDbResult(ERR_SQL_EXECUTION_ERROR, KDbConnection::tr("SQL statement for execution is invalid or empty.")); result->setErrorSql(sql); //remember for error handling return false; } return true; } QSharedPointer KDbConnection::prepareSql(const KDbEscapedString& sql) { m_result.setSql(sql); return QSharedPointer(drv_prepareSql(sql)); } bool KDbConnection::executeSql(const KDbEscapedString& sql) { m_result.setSql(sql); if (!checkSql(sql, &m_result)) { return false; } if (!drv_executeSql(sql)) { m_result.setMessage(QString()); //clear as this could be most probably just "Unknown error" string. m_result.setErrorSql(sql); m_result.prependMessage(ERR_SQL_EXECUTION_ERROR, tr("Error while executing SQL statement.")); kdbWarning() << m_result; return false; } return true; } KDbField* KDbConnection::findSystemFieldName(const KDbFieldList& fieldlist) { for (KDbField::ListIterator it(fieldlist.fieldsIterator()); it != fieldlist.fieldsIteratorConstEnd(); ++it) { if (d->driver->isSystemFieldName((*it)->name())) return *it; } return nullptr; } //! Creates a KDbField list for kexi__fields, for sanity. Used by createTable() static KDbFieldList* createFieldListForKexi__Fields(KDbTableSchema *kexi__fieldsSchema) { if (!kexi__fieldsSchema) return nullptr; return kexi__fieldsSchema->subList( QList() << "t_id" << "f_type" << "f_name" << "f_length" << "f_precision" << "f_constraints" << "f_options" << "f_default" << "f_order" << "f_caption" << "f_help" ); } static QVariant buildLengthValue(const KDbField &f) { if (f.isFPNumericType()) { return f.scale(); } return f.maxLength(); } //! builds a list of values for field's @a f properties. Used by createTable(). static void buildValuesForKexi__Fields(QList& vals, KDbField* f) { const KDbField::Type type = f->type(); // cache: evaluating type of expressions can be expensive vals.clear(); vals << QVariant(f->table()->id()) << QVariant(type) << QVariant(f->name()) << buildLengthValue(*f) << QVariant(KDbField::isFPNumericType(type) ? f->precision() : 0) << QVariant(f->constraints()) << QVariant(f->options()) // KDb::variantToString() is needed here because the value can be of any QVariant type, // depending on f->type() << (f->defaultValue().isNull() ? QVariant() : QVariant(KDb::variantToString(f->defaultValue()))) << QVariant(f->order()) << QVariant(f->caption()) << QVariant(f->description()); } bool KDbConnection::storeMainFieldSchema(KDbField *field) { if (!field || !field->table()) return false; KDbFieldList *fl = createFieldListForKexi__Fields(d->table(QLatin1String("kexi__fields"))); if (!fl) return false; QList vals; buildValuesForKexi__Fields(vals, field); QList::ConstIterator valsIt = vals.constBegin(); bool first = true; KDbEscapedString sql("UPDATE kexi__fields SET "); foreach(KDbField *f, *fl->fields()) { sql.append((first ? QString() : QLatin1String(", ")) + f->name() + QLatin1Char('=') + d->driver->valueToSql(f, *valsIt)); if (first) first = false; ++valsIt; } delete fl; sql.append(KDbEscapedString(" WHERE t_id=%1 AND f_name=%2") .arg(d->driver->valueToSql(KDbField::Integer, field->table()->id())) .arg(escapeString(field->name()))); return executeSql(sql); } #define createTable_ERR \ { kdbDebug() << "ERROR!"; \ m_result.prependMessage(KDbConnection::tr("Creating table failed.")); \ rollbackAutoCommitTransaction(tg.transaction()); \ return false; } bool KDbConnection::createTable(KDbTableSchema* tableSchema, CreateTableOptions options) { if (!tableSchema || !checkIsDatabaseUsed()) return false; //check if there are any fields if (tableSchema->fieldCount() < 1) { clearResult(); m_result = KDbResult(ERR_CANNOT_CREATE_EMPTY_OBJECT, tr("Could not create table without fields.")); return false; } KDbInternalTableSchema* internalTable = dynamic_cast(tableSchema); const QString tableName(tableSchema->name()); if (!internalTable) { if (d->driver->isSystemObjectName(tableName)) { clearResult(); m_result = KDbResult(ERR_SYSTEM_NAME_RESERVED, tr("System name \"%1\" cannot be used as table name.") .arg(tableSchema->name())); return false; } KDbField *sys_field = findSystemFieldName(*tableSchema); if (sys_field) { clearResult(); m_result = KDbResult(ERR_SYSTEM_NAME_RESERVED, tr("System name \"%1\" cannot be used as one of fields in \"%2\" table.") .arg(sys_field->name(), tableName)); return false; } } bool previousSchemaStillKept = false; KDbTableSchema *existingTable = nullptr; if (options & CreateTableOption::DropDestination) { //get previous table (do not retrieve, though) existingTable = this->tableSchema(tableName); if (existingTable) { if (existingTable == tableSchema) { clearResult(); m_result = KDbResult(ERR_OBJECT_EXISTS, tr("Could not create the same table \"%1\" twice.").arg(tableSchema->name())); return false; } //! @todo (js) update any structure (e.g. queries) that depend on this table! if (existingTable->id() > 0) tableSchema->setId(existingTable->id()); //copy id from existing table previousSchemaStillKept = true; if (!dropTableInternal(existingTable, false /*alsoRemoveSchema*/)) return false; } } else { if (!internalTable && this->tableSchema(tableSchema->name())) { clearResult(); m_result = KDbResult(ERR_OBJECT_EXISTS, tr("Table \"%1\" already exists.").arg(tableSchema->name())); return false; } } KDbTransactionGuard tg; if (!beginAutoCommitTransaction(&tg)) return false; if (internalTable) { if (!drv_containsTable(internalTable->name())) { // internal table may exist if (!drv_createTable(*tableSchema)) { createTable_ERR; } } } else { if (!drv_createTable(*tableSchema)) { createTable_ERR; } } //add the object data to kexi__* tables if (!internalTable) { //update kexi__objects if (!storeNewObjectData(tableSchema)) createTable_ERR; KDbTableSchema *ts = d->table(QLatin1String("kexi__fields")); if (!ts) return false; //for sanity: remove field info (if any) for this table id if (!KDb::deleteRecords(this, *ts, QLatin1String("t_id"), tableSchema->id())) return false; KDbFieldList *fl = createFieldListForKexi__Fields(ts); if (!fl) return false; foreach(KDbField *f, *tableSchema->fields()) { QList vals; buildValuesForKexi__Fields(vals, f); if (!insertRecord(fl, vals)) createTable_ERR; } delete fl; if (!storeExtendedTableSchemaData(tableSchema)) createTable_ERR; } bool res = commitAutoCommitTransaction(tg.transaction()); if (res) { if (!internalTable) { if (previousSchemaStillKept) { //remove previous table schema d->removeTable(tableSchema->id()); } } //store locally d->insertTable(tableSchema); //ok, this table is not created by the connection tableSchema->setConnection(this); } return res; } KDbTableSchema *KDbConnection::copyTable(const KDbTableSchema &tableSchema, const KDbObject &newData) { clearResult(); if (this->tableSchema(tableSchema.name()) != &tableSchema) { m_result = KDbResult(ERR_OBJECT_NOT_FOUND, tr("Table \"%1\" does not exist.").arg(tableSchema.name())); return nullptr; } KDbTableSchema *copiedTable = new KDbTableSchema(tableSchema, false /* !copyId*/); // copy name, caption, description copiedTable->setName(newData.name()); copiedTable->setCaption(newData.caption()); copiedTable->setDescription(newData.description()); // copy the structure and data if (!createTable(copiedTable, CreateTableOptions(CreateTableOption::Default) & ~CreateTableOptions(CreateTableOption::DropDestination))) { delete copiedTable; return nullptr; } if (!drv_copyTableData(tableSchema, *copiedTable)) { dropTable(copiedTable); delete copiedTable; return nullptr; } return copiedTable; } KDbTableSchema *KDbConnection::copyTable(const QString &tableName, const KDbObject &newData) { clearResult(); KDbTableSchema* ts = tableSchema(tableName); if (!ts) { m_result = KDbResult(ERR_OBJECT_NOT_FOUND, tr("Table \"%1\" does not exist.").arg(tableName)); return nullptr; } return copyTable(*ts, newData); } bool KDbConnection::drv_copyTableData(const KDbTableSchema &tableSchema, const KDbTableSchema &destinationTableSchema) { KDbEscapedString sql = KDbEscapedString("INSERT INTO %1 SELECT * FROM %2") .arg(escapeIdentifier(destinationTableSchema.name())) .arg(escapeIdentifier(tableSchema.name())); return executeSql(sql); } bool KDbConnection::removeObject(int objId) { clearResult(); //remove table schema from kexi__* tables KDbTableSchema *kexi__objects = d->table(QLatin1String("kexi__objects")); KDbTableSchema *kexi__objectdata = d->table(QLatin1String("kexi__objectdata")); if (!kexi__objects || !kexi__objectdata || !KDb::deleteRecords(this, *kexi__objects, QLatin1String("o_id"), objId) //schema entry || !KDb::deleteRecords(this, *kexi__objectdata, QLatin1String("o_id"), objId)) //data blocks { m_result = KDbResult(ERR_DELETE_SERVER_ERROR, tr("Could not delete object's data.")); return false; } return true; } bool KDbConnection::drv_dropTable(const QString& tableName) { return executeSql(KDbEscapedString("DROP TABLE %1").arg(escapeIdentifier(tableName))); } tristate KDbConnection::dropTable(KDbTableSchema* tableSchema) { return dropTableInternal(tableSchema, true); } tristate KDbConnection::dropTableInternal(KDbTableSchema* tableSchema, bool alsoRemoveSchema) { // Each SQL identifier needs to be escaped in the generated query. clearResult(); if (!tableSchema) return false; //be sure that we handle the correct KDbTableSchema object: if (tableSchema->id() < 0 || this->tableSchema(tableSchema->name()) != tableSchema || this->tableSchema(tableSchema->id()) != tableSchema) { m_result = KDbResult(ERR_OBJECT_NOT_FOUND, tr("Could not delete table \"%1\". %2") .arg(tr("Unexpected name or identifier."), tableSchema->name())); return false; } tristate res = KDbTableSchemaChangeListener::closeListeners(this, tableSchema); if (true != res) return res; //sanity checks: if (d->driver->isSystemObjectName(tableSchema->name())) { m_result = KDbResult(ERR_SYSTEM_NAME_RESERVED, tr("Could not delete table \"%1\". %2") .arg(tableSchema->name(), d->strItIsASystemObject())); return false; } KDbTransactionGuard tg; if (!beginAutoCommitTransaction(&tg)) return false; //for sanity we're checking if this table exists physically const tristate result = drv_containsTable(tableSchema->name()); if (~result) { return cancelled; } if (result == true) { if (!drv_dropTable(tableSchema->name())) return false; } KDbTableSchema *ts = d->table(QLatin1String("kexi__fields")); if (!ts || !KDb::deleteRecords(this, *ts, QLatin1String("t_id"), tableSchema->id())) //field entries return false; //remove table schema from kexi__objects table if (!removeObject(tableSchema->id())) { return false; } if (alsoRemoveSchema) { //! @todo js: update any structure (e.g. queries) that depend on this table! tristate res = removeDataBlock(tableSchema->id(), QLatin1String("extended_schema")); if (!res) return false; d->removeTable(tableSchema->id()); } return commitAutoCommitTransaction(tg.transaction()); } tristate KDbConnection::dropTable(const QString& tableName) { clearResult(); KDbTableSchema* ts = tableSchema(tableName); if (!ts) { m_result = KDbResult(ERR_OBJECT_NOT_FOUND, tr("Table \"%1\" does not exist.").arg(tableName)); return false; } return dropTable(ts); } tristate KDbConnection::alterTable(KDbTableSchema* tableSchema, KDbTableSchema* newTableSchema) { clearResult(); tristate res = KDbTableSchemaChangeListener::closeListeners(this, tableSchema); if (true != res) return res; if (tableSchema == newTableSchema) { m_result = KDbResult(ERR_OBJECT_THE_SAME, tr("Could not alter table \"%1\" using the same table as destination.") .arg(tableSchema->name())); return false; } //! @todo (js) implement real altering //! @todo (js) update any structure (e.g. query) that depend on this table! bool ok = true; bool empty; #if 0 //! @todo uncomment: empty = isEmpty(tableSchema, ok) && ok; #else empty = true; #endif if (empty) { ok = createTable(newTableSchema, KDbConnection::CreateTableOption::Default | KDbConnection::CreateTableOption::DropDestination); } return ok; } bool KDbConnection::alterTableName(KDbTableSchema* tableSchema, const QString& newName, AlterTableNameOptions options) { clearResult(); if (tableSchema != this->tableSchema(tableSchema->id())) { m_result = KDbResult(ERR_OBJECT_NOT_FOUND, tr("Unknown table \"%1\".").arg(tableSchema->name())); return false; } if (newName.isEmpty() || !KDb::isIdentifier(newName)) { m_result = KDbResult(ERR_INVALID_IDENTIFIER, tr("Invalid table name \"%1\".").arg(newName)); return false; } const QString oldTableName = tableSchema->name(); const QString newTableName = newName.trimmed(); if (oldTableName.trimmed() == newTableName) { m_result = KDbResult(ERR_OBJECT_THE_SAME, tr("Could not rename table \"%1\" using the same name.") .arg(newTableName)); return false; } //! @todo alter table name for server DB backends! //! @todo what about objects (queries/forms) that use old name? KDbTableSchema *tableToReplace = this->tableSchema(newName); const bool destTableExists = tableToReplace != nullptr; const int origID = destTableExists ? tableToReplace->id() : -1; //will be reused in the new table if (!(options & AlterTableNameOption::DropDestination) && destTableExists) { m_result = KDbResult(ERR_OBJECT_EXISTS, tr("Could not rename table \"%1\" to \"%2\". Table \"%3\" already exists.") .arg(tableSchema->name(), newName, newName)); return false; } //helper: #define alterTableName_ERR \ tableSchema->setName(oldTableName) //restore old name KDbTransactionGuard tg; if (!beginAutoCommitTransaction(&tg)) return false; // drop the table replaced (with schema) if (destTableExists) { if (!dropTable(newName)) { return false; } // the new table owns the previous table's id: if (!executeSql( KDbEscapedString("UPDATE kexi__objects SET o_id=%1 WHERE o_id=%2 AND o_type=%3") .arg(d->driver->valueToSql(KDbField::Integer, origID)) .arg(d->driver->valueToSql(KDbField::Integer, tableSchema->id())) .arg(d->driver->valueToSql(KDbField::Integer, int(KDb::TableObjectType))))) { return false; } if (!executeSql(KDbEscapedString("UPDATE kexi__fields SET t_id=%1 WHERE t_id=%2") .arg(d->driver->valueToSql(KDbField::Integer, origID)) .arg(d->driver->valueToSql(KDbField::Integer, tableSchema->id())))) { return false; } //maintain table ID d->changeTableId(tableSchema, origID); tableSchema->setId(origID); } if (!drv_alterTableName(tableSchema, newTableName)) { alterTableName_ERR; return false; } // Update kexi__objects //! @todo if (!executeSql(KDbEscapedString("UPDATE kexi__objects SET o_name=%1 WHERE o_id=%2") .arg(escapeString(tableSchema->name())) .arg(d->driver->valueToSql(KDbField::Integer, tableSchema->id())))) { alterTableName_ERR; return false; } //! @todo what about caption? //restore old name: it will be changed soon! tableSchema->setName(oldTableName); if (!commitAutoCommitTransaction(tg.transaction())) { alterTableName_ERR; return false; } //update tableSchema: d->renameTable(tableSchema, newTableName); return true; } bool KDbConnection::drv_alterTableName(KDbTableSchema* tableSchema, const QString& newName) { const QString oldTableName = tableSchema->name(); tableSchema->setName(newName); if (!executeSql(KDbEscapedString("ALTER TABLE %1 RENAME TO %2") .arg(KDbEscapedString(escapeIdentifier(oldTableName)), KDbEscapedString(escapeIdentifier(newName))))) { tableSchema->setName(oldTableName); //restore old name return false; } return true; } bool KDbConnection::dropQuery(KDbQuerySchema* querySchema) { clearResult(); if (!querySchema) return false; KDbTransactionGuard tg; if (!beginAutoCommitTransaction(&tg)) return false; //remove query schema from kexi__objects table if (!removeObject(querySchema->id())) { return false; } //! @todo update any structure that depend on this table! d->removeQuery(querySchema); return commitAutoCommitTransaction(tg.transaction()); } bool KDbConnection::dropQuery(const QString& queryName) { clearResult(); KDbQuerySchema* qs = querySchema(queryName); if (!qs) { m_result = KDbResult(ERR_OBJECT_NOT_FOUND, tr("Query \"%1\" does not exist.").arg(queryName)); return false; } return dropQuery(qs); } bool KDbConnection::drv_createTable(const KDbTableSchema& tableSchema) { const KDbNativeStatementBuilder builder(this, KDb::DriverEscaping); KDbEscapedString sql; if (!builder.generateCreateTableStatement(&sql,tableSchema)) { return false; } //kdbDebug() << "******** " << sql; return executeSql(sql); } bool KDbConnection::drv_createTable(const QString& tableName) { KDbTableSchema *ts = tableSchema(tableName); if (!ts) return false; return drv_createTable(*ts); } bool KDbConnection::beginAutoCommitTransaction(KDbTransactionGuard* tg) { if ((d->driver->behavior()->features & KDbDriver::IgnoreTransactions) || !d->autoCommit) { tg->setTransaction(KDbTransaction()); return true; } // commit current transaction (if present) for drivers // that allow single transaction per connection if (d->driver->behavior()->features & KDbDriver::SingleTransactions) { if (d->defaultTransactionStartedInside) //only commit internally started transaction if (!commitTransaction(d->default_trans, KDbTransaction::CommitOption::IgnoreInactive)) { tg->setTransaction(KDbTransaction()); return false; //we have a real error } d->defaultTransactionStartedInside = d->default_trans.isNull(); if (!d->defaultTransactionStartedInside) { tg->setTransaction(d->default_trans); tg->doNothing(); return true; //reuse externally started transaction } } else if (!(d->driver->behavior()->features & KDbDriver::MultipleTransactions)) { tg->setTransaction(KDbTransaction()); return true; //no trans. supported at all - just return } tg->setTransaction(beginTransaction()); return !m_result.isError(); } bool KDbConnection::commitAutoCommitTransaction(const KDbTransaction& trans) { if (d->driver->behavior()->features & KDbDriver::IgnoreTransactions) return true; if (trans.isNull() || !d->driver->transactionsSupported()) return true; if (d->driver->behavior()->features & KDbDriver::SingleTransactions) { if (!d->defaultTransactionStartedInside) //only commit internally started transaction return true; //give up } return commitTransaction(trans, KDbTransaction::CommitOption::IgnoreInactive); } bool KDbConnection::rollbackAutoCommitTransaction(const KDbTransaction& trans) { if (trans.isNull() || !d->driver->transactionsSupported()) return true; return rollbackTransaction(trans); } #define SET_ERR_TRANS_NOT_SUPP \ { m_result = KDbResult(ERR_UNSUPPORTED_DRV_FEATURE, \ KDbConnection::tr("Transactions are not supported for \"%1\" driver.").arg( d->driver->metaData()->name() )); } #define SET_BEGIN_TR_ERROR \ { if (!m_result.isError()) \ m_result = KDbResult(ERR_ROLLBACK_OR_COMMIT_TRANSACTION, \ KDbConnection::tr("Begin transaction failed.")); } KDbTransaction KDbConnection::beginTransaction() { if (!checkIsDatabaseUsed()) return KDbTransaction(); KDbTransaction trans; if (d->driver->behavior()->features & KDbDriver::IgnoreTransactions) { //we're creating dummy transaction data here, //so it will look like active trans.m_data = new KDbTransactionData(this); d->transactions.append(trans); return trans; } if (d->driver->behavior()->features & KDbDriver::SingleTransactions) { if (d->default_trans.isActive()) { m_result = KDbResult(ERR_TRANSACTION_ACTIVE, tr("Transaction already started.")); return KDbTransaction(); } if (!(trans.m_data = drv_beginTransaction())) { SET_BEGIN_TR_ERROR; return KDbTransaction(); } d->default_trans = trans; d->transactions.append(trans); return d->default_trans; } if (d->driver->behavior()->features & KDbDriver::MultipleTransactions) { if (!(trans.m_data = drv_beginTransaction())) { SET_BEGIN_TR_ERROR; return KDbTransaction(); } d->transactions.append(trans); return trans; } SET_ERR_TRANS_NOT_SUPP; return KDbTransaction(); } bool KDbConnection::commitTransaction(const KDbTransaction trans, KDbTransaction::CommitOptions options) { if (!isDatabaseUsed()) return false; if (!d->driver->transactionsSupported() && !(d->driver->behavior()->features & KDbDriver::IgnoreTransactions)) { SET_ERR_TRANS_NOT_SUPP; return false; } KDbTransaction t = trans; if (!t.isActive()) { //try default tr. if (!d->default_trans.isActive()) { if (options & KDbTransaction::CommitOption::IgnoreInactive) { return true; } clearResult(); m_result = KDbResult(ERR_NO_TRANSACTION_ACTIVE, tr("Transaction not started.")); return false; } t = d->default_trans; d->default_trans = KDbTransaction(); //now: no default tr. } bool ret = true; if (!(d->driver->behavior()->features & KDbDriver::IgnoreTransactions)) ret = drv_commitTransaction(t.m_data); if (t.m_data) t.m_data->setActive(false); //now this transaction if inactive if (!d->dontRemoveTransactions) //true=transaction obj will be later removed from list d->transactions.removeAt(d->transactions.indexOf(t)); if (!ret && !m_result.isError()) m_result = KDbResult(ERR_ROLLBACK_OR_COMMIT_TRANSACTION, tr("Error on commit transaction.")); return ret; } bool KDbConnection::rollbackTransaction(const KDbTransaction trans, KDbTransaction::CommitOptions options) { if (!isDatabaseUsed()) return false; if (!d->driver->transactionsSupported() && !(d->driver->behavior()->features & KDbDriver::IgnoreTransactions)) { SET_ERR_TRANS_NOT_SUPP; return false; } KDbTransaction t = trans; if (!t.isActive()) { //try default tr. if (!d->default_trans.isActive()) { if (options & KDbTransaction::CommitOption::IgnoreInactive) { return true; } clearResult(); m_result = KDbResult(ERR_NO_TRANSACTION_ACTIVE, tr("Transaction not started.")); return false; } t = d->default_trans; d->default_trans = KDbTransaction(); //now: no default tr. } bool ret = true; if (!(d->driver->behavior()->features & KDbDriver::IgnoreTransactions)) ret = drv_rollbackTransaction(t.m_data); if (t.m_data) t.m_data->setActive(false); //now this transaction if inactive if (!d->dontRemoveTransactions) //true=transaction obj will be later removed from list d->transactions.removeAt(d->transactions.indexOf(t)); if (!ret && !m_result.isError()) m_result = KDbResult(ERR_ROLLBACK_OR_COMMIT_TRANSACTION, tr("Error on rollback transaction.")); return ret; } #undef SET_ERR_TRANS_NOT_SUPP #undef SET_BEGIN_TR_ERROR /*bool KDbConnection::duringTransaction() { return drv_duringTransaction(); }*/ KDbTransaction KDbConnection::defaultTransaction() const { return d->default_trans; } void KDbConnection::setDefaultTransaction(const KDbTransaction& trans) { if (!isDatabaseUsed()) return; if (!(d->driver->behavior()->features & KDbDriver::IgnoreTransactions) && (!trans.isActive() || !d->driver->transactionsSupported())) { return; } d->default_trans = trans; } QList KDbConnection::transactions() { return d->transactions; } bool KDbConnection::autoCommit() const { return d->autoCommit; } bool KDbConnection::setAutoCommit(bool on) { if (d->autoCommit == on || d->driver->behavior()->features & KDbDriver::IgnoreTransactions) return true; if (!drv_setAutoCommit(on)) return false; d->autoCommit = on; return true; } KDbTransactionData* KDbConnection::drv_beginTransaction() { if (!executeSql(KDbEscapedString("BEGIN"))) return nullptr; return new KDbTransactionData(this); } bool KDbConnection::drv_commitTransaction(KDbTransactionData *) { return executeSql(KDbEscapedString("COMMIT")); } bool KDbConnection::drv_rollbackTransaction(KDbTransactionData *) { return executeSql(KDbEscapedString("ROLLBACK")); } bool KDbConnection::drv_setAutoCommit(bool /*on*/) { return true; } KDbCursor* KDbConnection::executeQuery(const KDbEscapedString& sql, KDbCursor::Options options) { if (sql.isEmpty()) return nullptr; KDbCursor *c = prepareQuery(sql, options); if (!c) return nullptr; if (!c->open()) {//err - kill that m_result = c->result(); CursorDeleter deleter(c); return nullptr; } return c; } KDbCursor* KDbConnection::executeQuery(KDbQuerySchema* query, const QList& params, KDbCursor::Options options) { KDbCursor *c = prepareQuery(query, params, options); if (!c) return nullptr; if (!c->open()) {//err - kill that m_result = c->result(); CursorDeleter deleter(c); return nullptr; } return c; } KDbCursor* KDbConnection::executeQuery(KDbQuerySchema* query, KDbCursor::Options options) { return executeQuery(query, QList(), options); } KDbCursor* KDbConnection::executeQuery(KDbTableSchema* table, KDbCursor::Options options) { return executeQuery(table->query(), options); } KDbCursor* KDbConnection::prepareQuery(KDbTableSchema* table, KDbCursor::Options options) { return prepareQuery(table->query(), options); } KDbCursor* KDbConnection::prepareQuery(KDbQuerySchema* query, const QList& params, KDbCursor::Options options) { KDbCursor* cursor = prepareQuery(query, options); if (cursor) cursor->setQueryParameters(params); return cursor; } bool KDbConnection::deleteCursor(KDbCursor *cursor) { if (!cursor) return false; if (cursor->connection() != this) {//illegal call kdbWarning() << "Could not delete the cursor not owned by the same connection!"; return false; } const bool ret = cursor->close(); CursorDeleter deleter(cursor); return ret; } //! @todo IMPORTANT: fix KDbConnection::setupObjectData() after refactoring bool KDbConnection::setupObjectData(const KDbRecordData &data, KDbObject *object) { if (data.count() < 5) { kdbWarning() << "Aborting, object data should have at least 5 elements, found" << data.count(); return false; } bool ok; const int id = data[0].toInt(&ok); if (!ok) return false; object->setId(id); const QString name(data[2].toString()); if (!KDb::isIdentifier(name)) { m_result = KDbResult(ERR_INVALID_IDENTIFIER, tr("Invalid object name \"%1\".").arg(name)); return false; } object->setName(name); object->setCaption(data[3].toString()); object->setDescription(data[4].toString()); // kdbDebug()<<"@@@ KDbConnection::setupObjectData() == " << sdata.schemaDataDebugString(); return true; } tristate KDbConnection::loadObjectData(int type, int id, KDbObject* object) { KDbRecordData data; if (type == KDb::AnyObjectType) { if (true != querySingleRecord(KDbEscapedString("SELECT o_id, o_type, o_name, o_caption, " "o_desc FROM kexi__objects WHERE o_id=%1") .arg(d->driver->valueToSql(KDbField::Integer, id)), &data)) { return cancelled; } } else { if (true != querySingleRecord(KDbEscapedString("SELECT o_id, o_type, o_name, o_caption, o_desc " "FROM kexi__objects WHERE o_type=%1 AND o_id=%1") .arg(d->driver->valueToSql(KDbField::Integer, type)) .arg(d->driver->valueToSql(KDbField::Integer, id)), &data)) { return cancelled; } } return setupObjectData(data, object); } tristate KDbConnection::loadObjectData(int type, const QString& name, KDbObject* object) { KDbRecordData data; if (true != querySingleRecord( KDbEscapedString("SELECT o_id, o_type, o_name, o_caption, o_desc " "FROM kexi__objects WHERE o_type=%1 AND o_name=%2") .arg(d->driver->valueToSql(KDbField::Integer, type)) .arg(escapeString(name)), &data)) { return cancelled; } return setupObjectData(data, object); } bool KDbConnection::storeObjectDataInternal(KDbObject* object, bool newObject) { KDbTableSchema *ts = d->table(QLatin1String("kexi__objects")); if (!ts) return false; if (newObject) { int existingID; if (true == querySingleNumber( KDbEscapedString("SELECT o_id FROM kexi__objects WHERE o_type=%1 AND o_name=%2") .arg(d->driver->valueToSql(KDbField::Integer, object->type())) .arg(escapeString(object->name())), &existingID)) { //we already have stored an object data with the same name and type: //just update it's properties as it would be existing object object->setId(existingID); newObject = false; } } if (newObject) { if (object->id() <= 0) {//get new ID QScopedPointer fl(ts->subList( QList() << "o_type" << "o_name" << "o_caption" << "o_desc")); if (!fl) { return false; } QSharedPointer result = insertRecord(fl.data(), QVariant(object->type()), QVariant(object->name()), QVariant(object->caption()), QVariant(object->description())); if (!result) { return false; } //fetch newly assigned ID //! @todo safe to cast it? quint64 obj_id = KDb::lastInsertedAutoIncValue(result, QLatin1String("o_id"), *ts); //kdbDebug() << "NEW obj_id == " << obj_id; if (obj_id == std::numeric_limits::max()) { return false; } object->setId(obj_id); return true; } else { QScopedPointer fl(ts->subList( QList() << "o_id" << "o_type" << "o_name" << "o_caption" << "o_desc")); return fl && insertRecord(fl.data(), QVariant(object->id()), QVariant(object->type()), QVariant(object->name()), QVariant(object->caption()), QVariant(object->description())); } } //existing object: return executeSql( KDbEscapedString("UPDATE kexi__objects SET o_type=%2, o_caption=%3, o_desc=%4 WHERE o_id=%1") .arg(d->driver->valueToSql(KDbField::Integer, object->id())) .arg(d->driver->valueToSql(KDbField::Integer, object->type())) .arg(escapeString(object->caption())) .arg(escapeString(object->description()))); } bool KDbConnection::storeObjectData(KDbObject* object) { return storeObjectDataInternal(object, false); } bool KDbConnection::storeNewObjectData(KDbObject* object) { return storeObjectDataInternal(object, true); } QString KDbConnection::escapeIdentifier(const QString& id, KDb::IdentifierEscapingType escapingType) const { return escapingType == KDb::KDbEscaping ? KDb::escapeIdentifier(id) : escapeIdentifier(id); } KDbCursor* KDbConnection::executeQueryInternal(const KDbEscapedString& sql, KDbQuerySchema* query, const QList* params) { Q_ASSERT(!sql.isEmpty() || query); clearResult(); if (!sql.isEmpty()) { return executeQuery(sql); } if (!query) { return nullptr; } if (params) { return executeQuery(query, *params); } return executeQuery(query); } tristate KDbConnection::querySingleRecordInternal(KDbRecordData *data, const KDbEscapedString *sql, KDbQuerySchema *query, const QList *params, QueryRecordOptions options) { Q_ASSERT(sql || query); if (sql) { //! @todo does not work with non-SQL data sources m_result.setSql(d->driver->addLimitTo1(*sql, options & QueryRecordOption::AddLimitTo1)); } KDbCursor *cursor = executeQueryInternal(m_result.sql(), query, params); if (!cursor) { kdbWarning() << "!querySingleRecordInternal() " << m_result.sql(); return false; } if (!cursor->moveFirst() || cursor->eof() || !cursor->storeCurrentRecord(data)) { const tristate result = cursor->result().isError() ? tristate(false) : tristate(cancelled); // kdbDebug() << "!cursor->moveFirst() || cursor->eof() || cursor->storeCurrentRecord(data) // " // "m_result.sql()=" << m_result.sql(); m_result = cursor->result(); deleteCursor(cursor); return result; } return deleteCursor(cursor); } tristate KDbConnection::querySingleRecord(const KDbEscapedString &sql, KDbRecordData *data, QueryRecordOptions options) { return querySingleRecordInternal(data, &sql, nullptr, nullptr, options); } tristate KDbConnection::querySingleRecord(KDbQuerySchema *query, KDbRecordData *data, QueryRecordOptions options) { return querySingleRecordInternal(data, nullptr, query, nullptr, options); } tristate KDbConnection::querySingleRecord(KDbQuerySchema *query, KDbRecordData *data, const QList ¶ms, QueryRecordOptions options) { return querySingleRecordInternal(data, nullptr, query, ¶ms, options); } bool KDbConnection::checkIfColumnExists(KDbCursor *cursor, int column) { if (column >= cursor->fieldCount()) { m_result = KDbResult(ERR_CURSOR_RECORD_FETCHING, tr("Column \"%1\" does not exist in the query.").arg(column)); return false; } return true; } tristate KDbConnection::querySingleStringInternal(const KDbEscapedString *sql, QString *value, KDbQuerySchema *query, const QList *params, int column, QueryRecordOptions options) { Q_ASSERT(sql || query); if (sql) { //! @todo does not work with non-SQL data sources m_result.setSql(d->driver->addLimitTo1(*sql, options & QueryRecordOption::AddLimitTo1)); } KDbCursor *cursor = executeQueryInternal(m_result.sql(), query, params); if (!cursor) { kdbWarning() << "!querySingleStringInternal()" << m_result.sql(); return false; } if (!cursor->moveFirst() || cursor->eof()) { const tristate result = cursor->result().isError() ? tristate(false) : tristate(cancelled); // kdbDebug() << "!cursor->moveFirst() || cursor->eof()" << m_result.sql(); deleteCursor(cursor); return result; } if (!checkIfColumnExists(cursor, column)) { deleteCursor(cursor); return false; } if (value) { *value = cursor->value(column).toString(); } return deleteCursor(cursor); } tristate KDbConnection::querySingleString(const KDbEscapedString &sql, QString *value, int column, QueryRecordOptions options) { return querySingleStringInternal(&sql, value, nullptr, nullptr, column, options); } tristate KDbConnection::querySingleString(KDbQuerySchema *query, QString *value, int column, QueryRecordOptions options) { return querySingleStringInternal(nullptr, value, query, nullptr, column, options); } tristate KDbConnection::querySingleString(KDbQuerySchema *query, QString *value, const QList ¶ms, int column, QueryRecordOptions options) { return querySingleStringInternal(nullptr, value, query, ¶ms, column, options); } tristate KDbConnection::querySingleNumberInternal(const KDbEscapedString *sql, int *number, KDbQuerySchema *query, const QList *params, int column, QueryRecordOptions options) { QString str; const tristate result = querySingleStringInternal(sql, &str, query, params, column, options); if (result != true) return result; bool ok; const int _number = str.toInt(&ok); if (!ok) return false; if (number) { *number = _number; } return true; } tristate KDbConnection::querySingleNumber(const KDbEscapedString &sql, int *number, int column, QueryRecordOptions options) { return querySingleNumberInternal(&sql, number, nullptr, nullptr, column, options); } tristate KDbConnection::querySingleNumber(KDbQuerySchema *query, int *number, int column, QueryRecordOptions options) { return querySingleNumberInternal(nullptr, number, query, nullptr, column, options); } tristate KDbConnection::querySingleNumber(KDbQuerySchema *query, int *number, const QList ¶ms, int column, QueryRecordOptions options) { return querySingleNumberInternal(nullptr, number, query, ¶ms, column, options); } bool KDbConnection::queryStringListInternal(const KDbEscapedString *sql, QStringList *list, KDbQuerySchema *query, const QList *params, int column, bool (*filterFunction)(const QString &)) { if (sql) { m_result.setSql(*sql); } KDbCursor *cursor = executeQueryInternal(m_result.sql(), query, params); if (!cursor) { kdbWarning() << "!queryStringListInternal() " << m_result.sql(); return false; } cursor->moveFirst(); if (cursor->result().isError()) { m_result = cursor->result(); deleteCursor(cursor); return false; } if (!cursor->eof() && !checkIfColumnExists(cursor, column)) { deleteCursor(cursor); return false; } if (list) { list->clear(); } QStringList listResult; while (!cursor->eof()) { const QString str(cursor->value(column).toString()); if (!filterFunction || filterFunction(str)) { listResult.append(str); } if (!cursor->moveNext() && cursor->result().isError()) { m_result = cursor->result(); deleteCursor(cursor); return false; } } if (list) { *list = listResult; } return deleteCursor(cursor); } bool KDbConnection::queryStringList(const KDbEscapedString& sql, QStringList* list, int column) { return queryStringListInternal(&sql, list, nullptr, nullptr, column, nullptr); } bool KDbConnection::queryStringList(KDbQuerySchema* query, QStringList* list, int column) { return queryStringListInternal(nullptr, list, query, nullptr, column, nullptr); } bool KDbConnection::queryStringList(KDbQuerySchema* query, QStringList* list, const QList& params, int column) { return queryStringListInternal(nullptr, list, query, ¶ms, column, nullptr); } tristate KDbConnection::resultExists(const KDbEscapedString &sql, QueryRecordOptions options) { // optimization if (d->driver->behavior()->SELECT_1_SUBQUERY_SUPPORTED) { // this is at least for sqlite if ((options & QueryRecordOption::AddLimitTo1) && sql.left(6).toUpper() == "SELECT") { m_result.setSql(d->driver->addLimitTo1("SELECT 1 FROM (" + sql + ')')); } else { m_result.setSql(sql); } } else { if ((options & QueryRecordOption::AddLimitTo1) && sql.startsWith("SELECT")) { m_result.setSql(d->driver->addLimitTo1(sql)); } else { m_result.setSql(sql); } } KDbCursor *cursor = executeQuery(m_result.sql()); if (!cursor) { kdbWarning() << "!executeQuery()" << m_result.sql(); return cancelled; } if (!cursor->moveFirst() || cursor->eof()) { //kdbWarning() << "!cursor->moveFirst() || cursor->eof()" << m_result.sql(); m_result = cursor->result(); deleteCursor(cursor); return m_result.isError() ? cancelled : tristate(false); } return deleteCursor(cursor) ? tristate(true) : cancelled; } tristate KDbConnection::isEmpty(KDbTableSchema* table) { const KDbNativeStatementBuilder builder(this, KDb::DriverEscaping); KDbEscapedString sql; if (!builder.generateSelectStatement(&sql, table)) { return cancelled; } const tristate result = resultExists(sql); if (~result) { return cancelled; } return result == false; } //! Used by addFieldPropertyToExtendedTableSchemaData() static void createExtendedTableSchemaMainElementIfNeeded( QDomDocument* doc, QDomElement* extendedTableSchemaMainEl, bool* extendedTableSchemaStringIsEmpty) { if (!*extendedTableSchemaStringIsEmpty) return; //init document *extendedTableSchemaMainEl = doc->createElement(QLatin1String("EXTENDED_TABLE_SCHEMA")); doc->appendChild(*extendedTableSchemaMainEl); extendedTableSchemaMainEl->setAttribute(QLatin1String("version"), QString::number(KDB_EXTENDED_TABLE_SCHEMA_VERSION)); *extendedTableSchemaStringIsEmpty = false; } //! Used by addFieldPropertyToExtendedTableSchemaData() static void createExtendedTableSchemaFieldElementIfNeeded(QDomDocument* doc, QDomElement* extendedTableSchemaMainEl, const QString& fieldName, QDomElement* extendedTableSchemaFieldEl, bool append = true) { if (!extendedTableSchemaFieldEl->isNull()) return; *extendedTableSchemaFieldEl = doc->createElement(QLatin1String("field")); if (append) extendedTableSchemaMainEl->appendChild(*extendedTableSchemaFieldEl); extendedTableSchemaFieldEl->setAttribute(QLatin1String("name"), fieldName); } /*! @internal used by storeExtendedTableSchemaData() Creates DOM node for @a propertyName and @a propertyValue. Creates enclosing EXTENDED_TABLE_SCHEMA element if EXTENDED_TABLE_SCHEMA is true. Updates extendedTableSchemaStringIsEmpty and extendedTableSchemaMainEl afterwards. If extendedTableSchemaFieldEl is null, creates element (with optional "custom" attribute is @a custom is false). */ static void addFieldPropertyToExtendedTableSchemaData( const KDbField& f, const QByteArray &propertyName, const QVariant& propertyValue, QDomDocument* doc, QDomElement* extendedTableSchemaMainEl, QDomElement* extendedTableSchemaFieldEl, bool* extendedTableSchemaStringIsEmpty, bool custom = false) { createExtendedTableSchemaMainElementIfNeeded(doc, extendedTableSchemaMainEl, extendedTableSchemaStringIsEmpty); createExtendedTableSchemaFieldElementIfNeeded( doc, extendedTableSchemaMainEl, f.name(), extendedTableSchemaFieldEl); //create QDomElement extendedTableSchemaFieldPropertyEl = doc->createElement(QLatin1String("property")); extendedTableSchemaFieldEl->appendChild(extendedTableSchemaFieldPropertyEl); if (custom) extendedTableSchemaFieldPropertyEl.setAttribute(QLatin1String("custom"), QLatin1String("true")); extendedTableSchemaFieldPropertyEl.setAttribute(QLatin1String("name"), QLatin1String(propertyName)); QDomElement extendedTableSchemaFieldPropertyValueEl; switch (propertyValue.type()) { case QVariant::String: extendedTableSchemaFieldPropertyValueEl = doc->createElement(QLatin1String("string")); break; case QVariant::ByteArray: extendedTableSchemaFieldPropertyValueEl = doc->createElement(QLatin1String("cstring")); break; case QVariant::Int: case QVariant::Double: case QVariant::UInt: case QVariant::LongLong: case QVariant::ULongLong: extendedTableSchemaFieldPropertyValueEl = doc->createElement(QLatin1String("number")); break; case QVariant::Bool: extendedTableSchemaFieldPropertyValueEl = doc->createElement(QLatin1String("bool")); break; default: //! @todo add more QVariant types kdbCritical() << "addFieldPropertyToExtendedTableSchemaData(): impl. error"; } extendedTableSchemaFieldPropertyEl.appendChild(extendedTableSchemaFieldPropertyValueEl); extendedTableSchemaFieldPropertyValueEl.appendChild( doc->createTextNode(propertyValue.toString())); } bool KDbConnection::storeExtendedTableSchemaData(KDbTableSchema* tableSchema) { //! @todo future: save in older versions if neeed QDomDocument doc(QLatin1String("EXTENDED_TABLE_SCHEMA")); QDomElement extendedTableSchemaMainEl; bool extendedTableSchemaStringIsEmpty = true; //for each field: foreach(KDbField* f, *tableSchema->fields()) { QDomElement extendedTableSchemaFieldEl; const KDbField::Type type = f->type(); // cache: evaluating type of expressions can be expensive if (f->visibleDecimalPlaces() >= 0/*nondefault*/ && KDb::supportsVisibleDecimalPlacesProperty(type)) { addFieldPropertyToExtendedTableSchemaData( *f, "visibleDecimalPlaces", f->visibleDecimalPlaces(), &doc, &extendedTableSchemaMainEl, &extendedTableSchemaFieldEl, &extendedTableSchemaStringIsEmpty); } if (type == KDbField::Text) { if (f->maxLengthStrategy() == KDbField::DefaultMaxLength) { addFieldPropertyToExtendedTableSchemaData( *f, "maxLengthIsDefault", true, &doc, &extendedTableSchemaMainEl, &extendedTableSchemaFieldEl, &extendedTableSchemaStringIsEmpty); } } // boolean field with "not null" // add custom properties const KDbField::CustomPropertiesMap customProperties(f->customProperties()); for (KDbField::CustomPropertiesMap::ConstIterator itCustom = customProperties.constBegin(); itCustom != customProperties.constEnd(); ++itCustom) { addFieldPropertyToExtendedTableSchemaData( *f, itCustom.key(), itCustom.value(), &doc, &extendedTableSchemaMainEl, &extendedTableSchemaFieldEl, &extendedTableSchemaStringIsEmpty, /*custom*/true); } // save lookup table specification, if present KDbLookupFieldSchema *lookupFieldSchema = tableSchema->lookupFieldSchema(*f); if (lookupFieldSchema) { createExtendedTableSchemaFieldElementIfNeeded( &doc, &extendedTableSchemaMainEl, f->name(), &extendedTableSchemaFieldEl, false/* !append */); lookupFieldSchema->saveToDom(&doc, &extendedTableSchemaFieldEl); if (extendedTableSchemaFieldEl.hasChildNodes()) { // this element provides the definition, so let's append it now createExtendedTableSchemaMainElementIfNeeded(&doc, &extendedTableSchemaMainEl, &extendedTableSchemaStringIsEmpty); extendedTableSchemaMainEl.appendChild(extendedTableSchemaFieldEl); } } } // Store extended schema information (see ExtendedTableSchemaInformation in Kexi Wiki) if (extendedTableSchemaStringIsEmpty) { #ifdef KDB_DEBUG_GUI KDb::alterTableActionDebugGUI(QLatin1String("** Extended table schema REMOVED.")); #endif if (!removeDataBlock(tableSchema->id(), QLatin1String("extended_schema"))) return false; } else { #ifdef KDB_DEBUG_GUI KDb::alterTableActionDebugGUI( QLatin1String("** Extended table schema set to:\n") + doc.toString(4)); #endif if (!storeDataBlock(tableSchema->id(), doc.toString(1), QLatin1String("extended_schema"))) return false; } return true; } bool KDbConnection::loadExtendedTableSchemaData(KDbTableSchema* tableSchema) { #define loadExtendedTableSchemaData_ERR \ { m_result = KDbResult(tr("Error while loading extended table schema.", \ "Extended schema for a table: loading error")); \ return false; } #define loadExtendedTableSchemaData_ERR2(details) \ { m_result = KDbResult(details); \ m_result.setMessageTitle(tr("Error while loading extended table schema.", \ "Extended schema for a table: loading error")); \ return false; } #define loadExtendedTableSchemaData_ERR3(data) \ { m_result = KDbResult(tr("Invalid XML data: %1").arg(data.left(1024))); \ m_result.setMessageTitle(tr("Error while loading extended table schema.", \ "Extended schema for a table: loading error")); \ return false; } // Load extended schema information, if present (see ExtendedTableSchemaInformation in Kexi Wiki) QString extendedTableSchemaString; tristate res = loadDataBlock(tableSchema->id(), &extendedTableSchemaString, QLatin1String("extended_schema")); if (!res) loadExtendedTableSchemaData_ERR; // extendedTableSchemaString will be just empty if there is no such data block if (extendedTableSchemaString.isEmpty()) return true; QDomDocument doc; QString errorMsg; int errorLine, errorColumn; if (!doc.setContent(extendedTableSchemaString, &errorMsg, &errorLine, &errorColumn)) { loadExtendedTableSchemaData_ERR2( tr("Error in XML data: \"%1\" in line %2, column %3.\nXML data: %4") .arg(errorMsg).arg(errorLine).arg(errorColumn).arg(extendedTableSchemaString.left(1024))); } //! @todo look at the current format version (KDB_EXTENDED_TABLE_SCHEMA_VERSION) if (doc.doctype().name() != QLatin1String("EXTENDED_TABLE_SCHEMA")) loadExtendedTableSchemaData_ERR3(extendedTableSchemaString); QDomElement docEl = doc.documentElement(); if (docEl.tagName() != QLatin1String("EXTENDED_TABLE_SCHEMA")) loadExtendedTableSchemaData_ERR3(extendedTableSchemaString); for (QDomNode n = docEl.firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement fieldEl = n.toElement(); if (fieldEl.tagName() == QLatin1String("field")) { KDbField *f = tableSchema->field(fieldEl.attribute(QLatin1String("name"))); if (f) { //set properties of the field: //! @todo more properties for (QDomNode propNode = fieldEl.firstChild(); !propNode.isNull(); propNode = propNode.nextSibling()) { const QDomElement propEl = propNode.toElement(); bool ok; int intValue; if (propEl.tagName() == QLatin1String("property")) { QByteArray propertyName = propEl.attribute(QLatin1String("name")).toLatin1(); if (propEl.attribute(QLatin1String("custom")) == QLatin1String("true")) { //custom property const QVariant v(KDb::loadPropertyValueFromDom(propEl.firstChild(), &ok)); if (ok) { f->setCustomProperty(propertyName, v); } } else if (propertyName == "visibleDecimalPlaces") { if (KDb::supportsVisibleDecimalPlacesProperty(f->type())) { intValue = KDb::loadIntPropertyValueFromDom(propEl.firstChild(), &ok); if (ok) f->setVisibleDecimalPlaces(intValue); } } else if (propertyName == "maxLengthIsDefault") { if (f->type() == KDbField::Text) { const bool maxLengthIsDefault = KDb::loadPropertyValueFromDom(propEl.firstChild(), &ok).toBool(); if (ok) { f->setMaxLengthStrategy( maxLengthIsDefault ? KDbField::DefaultMaxLength : KDbField::DefinedMaxLength); } } } //! @todo more properties... } else if (propEl.tagName() == QLatin1String("lookup-column")) { KDbLookupFieldSchema *lookupFieldSchema = KDbLookupFieldSchema::loadFromDom(propEl); if (lookupFieldSchema) { kdbDebug() << f->name() << *lookupFieldSchema; tableSchema->setLookupFieldSchema(f->name(), lookupFieldSchema); } } } } else { kdbWarning() << "no such field:" << fieldEl.attribute(QLatin1String("name")) << "in table:" << tableSchema->name(); } } } return true; } KDbField* KDbConnection::setupField(const KDbRecordData &data) { bool ok = true; int f_int_type = data.at(1).toInt(&ok); if (f_int_type <= KDbField::InvalidType || f_int_type > KDbField::LastType) ok = false; if (!ok) return nullptr; KDbField::Type f_type = (KDbField::Type)f_int_type; const int f_len = qMax(0, data.at(3).toInt(&ok)); // defined limit if (!ok) { return nullptr; } //! @todo load maxLengthStrategy info to see if the maxLength is the default int f_prec = data.at(4).toInt(&ok); if (!ok) return nullptr; KDbField::Constraints f_constr = (KDbField::Constraints)data.at(5).toInt(&ok); if (!ok) return nullptr; KDbField::Options f_opts = (KDbField::Options)data.at(6).toInt(&ok); if (!ok) return nullptr; QString name(data.at(2).toString()); if (!KDb::isIdentifier(name)) { name = KDb::stringToIdentifier(name); } KDbField *f = new KDbField( name, f_type, f_constr, f_opts, f_len, f_prec); QVariant defaultVariant = data.at(7); if (defaultVariant.isValid()) { defaultVariant = KDb::stringToVariant(defaultVariant.toString(), KDbField::variantType(f_type), &ok); if (ok) { f->setDefaultValue(defaultVariant); } else { kdbWarning() << "problem with KDb::stringToVariant(" << defaultVariant << ')'; ok = true; //problem with defaultValue is not critical } } f->setCaption(data.at(9).toString()); f->setDescription(data.at(10).toString()); return f; } KDbTableSchema* KDbConnection::tableSchema(const QString& tableName) { KDbTableSchema *t = d->table(tableName); if (t || tableName.isEmpty()) { return t; } //not found: retrieve schema QScopedPointer newTable(new KDbTableSchema); clearResult(); if (true != loadObjectData(KDb::TableObjectType, tableName, newTable.data())) { return nullptr; } return d->setupTableSchema(newTable.take()); } KDbTableSchema* KDbConnection::tableSchema(int tableId) { KDbTableSchema *t = d->table(tableId); if (t) return t; //not found: retrieve schema QScopedPointer newTable(new KDbTableSchema); clearResult(); if (true != loadObjectData(KDb::TableObjectType, tableId, newTable.data())) { return nullptr; } return d->setupTableSchema(newTable.take()); } tristate KDbConnection::loadDataBlock(int objectID, QString* dataString, const QString& dataID) { if (objectID <= 0) return false; return querySingleString( KDbEscapedString("SELECT o_data FROM kexi__objectdata WHERE o_id=%1 AND ") .arg(d->driver->valueToSql(KDbField::Integer, objectID)) + KDbEscapedString(KDb::sqlWhere(d->driver, KDbField::Text, QLatin1String("o_sub_id"), dataID.isEmpty() ? QVariant() : QVariant(dataID))), dataString); } bool KDbConnection::storeDataBlock(int objectID, const QString &dataString, const QString& dataID) { if (objectID <= 0) return false; KDbEscapedString sql( KDbEscapedString("SELECT kexi__objectdata.o_id FROM kexi__objectdata WHERE o_id=%1") .arg(d->driver->valueToSql(KDbField::Integer, objectID))); KDbEscapedString sql_sub(KDb::sqlWhere(d->driver, KDbField::Text, QLatin1String("o_sub_id"), dataID.isEmpty() ? QVariant() : QVariant(dataID))); const tristate result = resultExists(sql + " AND " + sql_sub); if (~result) { return false; } if (result == true) { return executeSql(KDbEscapedString("UPDATE kexi__objectdata SET o_data=%1 WHERE o_id=%2 AND ") .arg(d->driver->valueToSql(KDbField::LongText, dataString)) .arg(d->driver->valueToSql(KDbField::Integer, objectID)) + sql_sub); } return executeSql( KDbEscapedString("INSERT INTO kexi__objectdata (o_id, o_data, o_sub_id) VALUES (") + KDbEscapedString::number(objectID) + ',' + d->driver->valueToSql(KDbField::LongText, dataString) + ',' + d->driver->valueToSql(KDbField::Text, dataID) + ')'); } bool KDbConnection::copyDataBlock(int sourceObjectID, int destObjectID, const QString &dataID) { if (sourceObjectID <= 0 || destObjectID <= 0) return false; if (sourceObjectID == destObjectID) return true; if (!removeDataBlock(destObjectID, dataID)) // remove before copying return false; KDbEscapedString sql = KDbEscapedString( "INSERT INTO kexi__objectdata SELECT %1, t.o_data, t.o_sub_id " "FROM kexi__objectdata AS t WHERE o_id=%2") .arg(d->driver->valueToSql(KDbField::Integer, destObjectID)) .arg(d->driver->valueToSql(KDbField::Integer, sourceObjectID)); if (!dataID.isEmpty()) { sql += KDbEscapedString(" AND ") + KDb::sqlWhere(d->driver, KDbField::Text, QLatin1String("o_sub_id"), dataID); } return executeSql(sql); } bool KDbConnection::removeDataBlock(int objectID, const QString& dataID) { if (objectID <= 0) return false; if (dataID.isEmpty()) return KDb::deleteRecords(this, QLatin1String("kexi__objectdata"), QLatin1String("o_id"), QString::number(objectID)); else return KDb::deleteRecords(this, QLatin1String("kexi__objectdata"), QLatin1String("o_id"), KDbField::Integer, objectID, QLatin1String("o_sub_id"), KDbField::Text, dataID); } KDbQuerySchema* KDbConnection::querySchema(const QString& aQueryName) { QString queryName = aQueryName.toLower(); KDbQuerySchema *q = d->query(queryName); if (q || queryName.isEmpty()) { return q; } //not found: retrieve schema QScopedPointer newQuery(new KDbQuerySchema); clearResult(); if (true != loadObjectData(KDb::QueryObjectType, aQueryName, newQuery.data())) { return nullptr; } return d->setupQuerySchema(newQuery.take()); } KDbQuerySchema* KDbConnection::querySchema(int queryId) { KDbQuerySchema *q = d->query(queryId); if (q) return q; //not found: retrieve schema QScopedPointer newQuery(new KDbQuerySchema); clearResult(); if (true != loadObjectData(KDb::QueryObjectType, queryId, newQuery.data())) { return nullptr; } return d->setupQuerySchema(newQuery.take()); } bool KDbConnection::setQuerySchemaObsolete(const QString& queryName) { KDbQuerySchema* oldQuery = querySchema(queryName); if (!oldQuery) return false; d->setQueryObsolete(oldQuery); return true; } QString KDbConnection::escapeIdentifier(const QString& id) const { return d->driver->escapeIdentifier(id); } bool KDbConnection::isInternalTableSchema(const QString& tableName) { KDbTableSchema* schema = d->table(tableName); return (schema && schema->isInternal()) // these are here for compatiblility because we're no longer instantiate // them but can exist in projects created with previous Kexi versions: || tableName == QLatin1String("kexi__final") || tableName == QLatin1String("kexi__useractions"); } void KDbConnection::removeMe(KDbTableSchema *table) { if (table && d) { d->takeTable(table); } } QString KDbConnection::anyAvailableDatabaseName() { if (!d->availableDatabaseName.isEmpty()) { return d->availableDatabaseName; } return d->driver->behavior()->ALWAYS_AVAILABLE_DATABASE_NAME; } void KDbConnection::setAvailableDatabaseName(const QString& dbName) { d->availableDatabaseName = dbName; } //! @internal used in updateRecord(), insertRecord(), inline static void updateRecordDataWithNewValues( KDbConnection *conn, KDbQuerySchema* query, KDbRecordData* data, const KDbRecordEditBuffer::DbHash& b, QHash* columnsOrderExpanded) { *columnsOrderExpanded = query->columnsOrder(conn, KDbQuerySchema::ColumnsOrderMode::ExpandedList); QHash::ConstIterator columnsOrderExpandedIt; for (KDbRecordEditBuffer::DbHash::ConstIterator it = b.constBegin();it != b.constEnd();++it) { columnsOrderExpandedIt = columnsOrderExpanded->constFind(it.key()); if (columnsOrderExpandedIt == columnsOrderExpanded->constEnd()) { kdbWarning() << "(KDbConnection) \"now also assign new value in memory\" step" "- could not find item" << it.key()->aliasOrName(); continue; } (*data)[ columnsOrderExpandedIt.value() ] = it.value(); } } bool KDbConnection::updateRecord(KDbQuerySchema* query, KDbRecordData* data, KDbRecordEditBuffer* buf, bool useRecordId) { // Each SQL identifier needs to be escaped in the generated query. // kdbDebug() << *query; clearResult(); //--get PKEY if (buf->dbBuffer().isEmpty()) { kdbDebug() << " -- NO CHANGES DATA!"; return true; } KDbTableSchema *mt = query->masterTable(); if (!mt) { kdbWarning() << " -- NO MASTER TABLE!"; m_result = KDbResult(ERR_UPDATE_NO_MASTER_TABLE, tr("Could not update record because there is no master table defined.")); return false; } KDbIndexSchema *pkey = (mt->primaryKey() && !mt->primaryKey()->fields()->isEmpty()) ? mt->primaryKey() : nullptr; if (!useRecordId && !pkey) { kdbWarning() << " -- NO MASTER TABLE's PKEY!"; m_result = KDbResult(ERR_UPDATE_NO_MASTER_TABLES_PKEY, tr("Could not update record because master table has no primary key defined.")); //! @todo perhaps we can try to update without using PKEY? return false; } //update the record: KDbEscapedString sql; sql.reserve(4096); sql = KDbEscapedString("UPDATE ") + escapeIdentifier(mt->name()) + " SET "; KDbEscapedString sqlset, sqlwhere; sqlset.reserve(1024); sqlwhere.reserve(1024); KDbRecordEditBuffer::DbHash b = buf->dbBuffer(); //gather the fields which are updated ( have values in KDbRecordEditBuffer) KDbFieldList affectedFields; for (KDbRecordEditBuffer::DbHash::ConstIterator it = b.constBegin();it != b.constEnd();++it) { if (it.key()->field()->table() != mt) continue; // skip values for fields outside of the master table (e.g. a "visible value" of the lookup field) if (!sqlset.isEmpty()) sqlset += ','; KDbField* currentField = it.key()->field(); const bool affectedFieldsAddOk = affectedFields.addField(currentField); Q_ASSERT(affectedFieldsAddOk); sqlset += KDbEscapedString(escapeIdentifier(currentField->name())) + '=' + d->driver->valueToSql(currentField, it.value()); } if (pkey) { const QVector pkeyFieldsOrder(query->pkeyFieldsOrder(this)); //kdbDebug() << pkey->fieldCount() << " ? " << query->pkeyFieldCount(); if (pkey->fieldCount() != query->pkeyFieldCount(this)) { //sanity check kdbWarning() << " -- NO ENTIRE MASTER TABLE's PKEY SPECIFIED!"; m_result = KDbResult(ERR_UPDATE_NO_ENTIRE_MASTER_TABLES_PKEY, tr("Could not update record because it does not contain entire primary key of master table.")); return false; } if (!pkey->fields()->isEmpty()) { int i = 0; foreach(KDbField *f, *pkey->fields()) { if (!sqlwhere.isEmpty()) sqlwhere += " AND "; QVariant val(data->at(pkeyFieldsOrder.at(i))); if (val.isNull() || !val.isValid()) { m_result = KDbResult(ERR_UPDATE_NULL_PKEY_FIELD, tr("Primary key's field \"%1\" cannot be empty.").arg(f->name())); //js todo: pass the field's name somewhere! return false; } sqlwhere += KDbEscapedString(escapeIdentifier(f->name())) + '=' + d->driver->valueToSql(f, val); i++; } } } else { //use RecordId sqlwhere = KDbEscapedString(escapeIdentifier(d->driver->behavior()->ROW_ID_FIELD_NAME)) + '=' + d->driver->valueToSql(KDbField::BigInteger, (*data)[data->size() - 1]); } sql += (sqlset + " WHERE " + sqlwhere); //kdbDebug() << " -- SQL == " << ((sql.length() > 400) ? (sql.left(400) + "[.....]") : sql); // preprocessing before update if (!drv_beforeUpdate(mt->name(), &affectedFields)) return false; bool res = executeSql(sql); // postprocessing after update if (!drv_afterUpdate(mt->name(), &affectedFields)) return false; if (!res) { m_result = KDbResult(ERR_UPDATE_SERVER_ERROR, tr("Record updating on the server failed.")); return false; } //success: now also assign new values in memory: QHash columnsOrderExpanded; updateRecordDataWithNewValues(this, query, data, b, &columnsOrderExpanded); return true; } bool KDbConnection::insertRecord(KDbQuerySchema* query, KDbRecordData* data, KDbRecordEditBuffer* buf, bool getRecordId) { // Each SQL identifier needs to be escaped in the generated query. clearResult(); //--get PKEY /*disabled: there may be empty records (with autoinc) if (buf.dbBuffer().isEmpty()) { kdbDebug() << " -- NO CHANGES DATA!"; return true; }*/ KDbTableSchema *mt = query->masterTable(); if (!mt) { kdbWarning() << " -- NO MASTER TABLE!"; m_result = KDbResult(ERR_INSERT_NO_MASTER_TABLE, tr("Could not insert record because there is no master table specified.")); return false; } KDbIndexSchema *pkey = (mt->primaryKey() && !mt->primaryKey()->fields()->isEmpty()) ? mt->primaryKey() : nullptr; if (!getRecordId && !pkey) { kdbWarning() << " -- WARNING: NO MASTER TABLE's PKEY"; } KDbEscapedString sqlcols, sqlvals; sqlcols.reserve(1024); sqlvals.reserve(1024); //insert the record: KDbEscapedString sql; sql.reserve(4096); sql = KDbEscapedString("INSERT INTO ") + escapeIdentifier(mt->name()) + " ("; KDbRecordEditBuffer::DbHash b = buf->dbBuffer(); // add default values, if available (for any column without value explicitly set) const KDbQueryColumnInfo::Vector fieldsExpanded( query->fieldsExpanded(this, KDbQuerySchema::FieldsExpandedMode::Unique)); int fieldsExpandedCount = fieldsExpanded.count(); for (int i = 0; i < fieldsExpandedCount; i++) { KDbQueryColumnInfo *ci = fieldsExpanded.at(i); if (ci->field() && KDb::isDefaultValueAllowed(*ci->field()) && !ci->field()->defaultValue().isNull() && !b.contains(ci)) { //kdbDebug() << "adding default value" << ci->field->defaultValue().toString() << "for column" << ci->field->name(); b.insert(ci, ci->field()->defaultValue()); } } //collect fields which have values in KDbRecordEditBuffer KDbFieldList affectedFields; if (b.isEmpty()) { // empty record inserting requested: if (!getRecordId && !pkey) { kdbWarning() << "MASTER TABLE's PKEY REQUIRED FOR INSERTING EMPTY RECORDS: INSERT CANCELLED"; m_result = KDbResult(ERR_INSERT_NO_MASTER_TABLES_PKEY, tr("Could not insert record because master table has no primary key specified.")); return false; } if (pkey) { const QVector pkeyFieldsOrder(query->pkeyFieldsOrder(this)); // kdbDebug() << pkey->fieldCount() << " ? " << query->pkeyFieldCount(); if (pkey->fieldCount() != query->pkeyFieldCount(this)) { // sanity check kdbWarning() << "NO ENTIRE MASTER TABLE's PKEY SPECIFIED!"; m_result = KDbResult(ERR_INSERT_NO_ENTIRE_MASTER_TABLES_PKEY, tr("Could not insert record because it does not contain " "entire master table's primary key.")); return false; } } //at least one value is needed for VALUES section: find it and set to NULL: KDbField *anyField = mt->anyNonPKField(); if (!anyField) { if (!pkey) { kdbWarning() << "WARNING: NO FIELD AVAILABLE TO SET IT TO NULL"; return false; } else { //try to set NULL in pkey field (could not work for every SQL engine!) anyField = pkey->fields()->first(); } } sqlcols += escapeIdentifier(anyField->name()); sqlvals += d->driver->valueToSql(anyField, QVariant()/*NULL*/); const bool affectedFieldsAddOk = affectedFields.addField(anyField); Q_ASSERT(affectedFieldsAddOk); } else { // non-empty record inserting requested: for (KDbRecordEditBuffer::DbHash::ConstIterator it = b.constBegin();it != b.constEnd();++it) { if (it.key()->field()->table() != mt) continue; // skip values for fields outside of the master table (e.g. a "visible value" of the lookup field) if (!sqlcols.isEmpty()) { sqlcols += ','; sqlvals += ','; } KDbField* currentField = it.key()->field(); const bool affectedFieldsAddOk = affectedFields.addField(currentField); Q_ASSERT(affectedFieldsAddOk); sqlcols += escapeIdentifier(currentField->name()); sqlvals += d->driver->valueToSql(currentField, it.value()); } } sql += (sqlcols + ") VALUES (" + sqlvals + ')'); // kdbDebug() << " -- SQL == " << sql; // low-level insert QSharedPointer result = insertRecordInternal(mt->name(), &affectedFields, sql); if (!result) { m_result = KDbResult(ERR_INSERT_SERVER_ERROR, tr("Record inserting on the server failed.")); return false; } //success: now also assign a new value in memory: QHash columnsOrderExpanded; updateRecordDataWithNewValues(this, query, data, b, &columnsOrderExpanded); //fetch autoincremented values KDbQueryColumnInfo::List *aif_list = query->autoIncrementFields(this); quint64 recordId = 0; if (pkey && !aif_list->isEmpty()) { //! @todo now only if PKEY is present, this should also work when there's no PKEY KDbQueryColumnInfo *id_columnInfo = aif_list->first(); //! @todo safe to cast it? quint64 last_id = KDb::lastInsertedAutoIncValue(result, id_columnInfo->field()->name(), id_columnInfo->field()->table()->name(), &recordId); if (last_id == std::numeric_limits::max()) { //! @todo show error //! @todo remove just inserted record. How? Using ROLLBACK? return false; } KDbRecordData aif_data; KDbEscapedString getAutoIncForInsertedValue("SELECT " + query->autoIncrementSqlFieldsList(this) + " FROM " + escapeIdentifier(id_columnInfo->field()->table()->name()) + " WHERE " + escapeIdentifier(id_columnInfo->field()->name()) + '=' + QByteArray::number(last_id)); if (true != querySingleRecord(getAutoIncForInsertedValue, &aif_data)) { //! @todo show error return false; } int i = 0; foreach(KDbQueryColumnInfo *ci, *aif_list) { // kdbDebug() << "AUTOINCREMENTED FIELD" << fi->field->name() << "==" << aif_data[i].toInt(); ((*data)[ columnsOrderExpanded.value(ci)] = aif_data.value(i)).convert(ci->field()->variantType()); //cast to get proper type i++; } } else { recordId = result->lastInsertRecordId(); // kdbDebug() << "new recordId ==" << recordId; if (d->driver->behavior()->ROW_ID_FIELD_RETURNS_LAST_AUTOINCREMENTED_VALUE) { kdbWarning() << "d->driver->behavior()->ROW_ID_FIELD_RETURNS_LAST_AUTOINCREMENTED_VALUE"; return false; } } if (getRecordId && /*sanity check*/data->size() > fieldsExpanded.size()) { // kdbDebug() << "new ROWID ==" << ROWID; (*data)[data->size() - 1] = recordId; } return true; } bool KDbConnection::deleteRecord(KDbQuerySchema* query, KDbRecordData* data, bool useRecordId) { // Each SQL identifier needs to be escaped in the generated query. clearResult(); KDbTableSchema *mt = query->masterTable(); if (!mt) { kdbWarning() << " -- NO MASTER TABLE!"; m_result = KDbResult(ERR_DELETE_NO_MASTER_TABLE, tr("Could not delete record because there is no master table specified.")); return false; } KDbIndexSchema *pkey = (mt->primaryKey() && !mt->primaryKey()->fields()->isEmpty()) ? mt->primaryKey() : nullptr; //! @todo allow to delete from a table without pkey if (!useRecordId && !pkey) { kdbWarning() << " -- WARNING: NO MASTER TABLE's PKEY"; m_result = KDbResult(ERR_DELETE_NO_MASTER_TABLES_PKEY, tr("Could not delete record because there is no primary key for master table specified.")); return false; } //update the record: KDbEscapedString sql; sql.reserve(4096); sql = KDbEscapedString("DELETE FROM ") + escapeIdentifier(mt->name()) + " WHERE "; KDbEscapedString sqlwhere; sqlwhere.reserve(1024); if (pkey) { const QVector pkeyFieldsOrder(query->pkeyFieldsOrder(this)); //kdbDebug() << pkey->fieldCount() << " ? " << query->pkeyFieldCount(); if (pkey->fieldCount() != query->pkeyFieldCount(this)) { //sanity check kdbWarning() << " -- NO ENTIRE MASTER TABLE's PKEY SPECIFIED!"; m_result = KDbResult(ERR_DELETE_NO_ENTIRE_MASTER_TABLES_PKEY, tr("Could not delete record because it does not contain entire master table's primary key.")); return false; } int i = 0; foreach(KDbField *f, *pkey->fields()) { if (!sqlwhere.isEmpty()) sqlwhere += " AND "; QVariant val(data->at(pkeyFieldsOrder.at(i))); if (val.isNull() || !val.isValid()) { m_result = KDbResult(ERR_DELETE_NULL_PKEY_FIELD, tr("Primary key's field \"%1\" cannot be empty.").arg(f->name())); //js todo: pass the field's name somewhere! return false; } sqlwhere += KDbEscapedString(escapeIdentifier(f->name())) + '=' + d->driver->valueToSql(f, val); i++; } } else {//use RecordId sqlwhere = KDbEscapedString(escapeIdentifier(d->driver->behavior()->ROW_ID_FIELD_NAME)) + '=' + d->driver->valueToSql(KDbField::BigInteger, (*data)[data->size() - 1]); } sql += sqlwhere; //kdbDebug() << " -- SQL == " << sql; if (!executeSql(sql)) { m_result = KDbResult(ERR_DELETE_SERVER_ERROR, tr("Record deletion on the server failed.")); return false; } return true; } bool KDbConnection::deleteAllRecords(KDbQuerySchema* query) { clearResult(); KDbTableSchema *mt = query->masterTable(); if (!mt) { kdbWarning() << " -- NO MASTER TABLE!"; return false; } KDbIndexSchema *pkey = mt->primaryKey(); if (!pkey || pkey->fields()->isEmpty()) { kdbWarning() << "-- WARNING: NO MASTER TABLE's PKEY"; } KDbEscapedString sql = KDbEscapedString("DELETE FROM ") + escapeIdentifier(mt->name()); //kdbDebug() << "-- SQL == " << sql; if (!executeSql(sql)) { m_result = KDbResult(ERR_DELETE_SERVER_ERROR, tr("Record deletion on the server failed.")); return false; } return true; } int KDbConnection::recordCount(const KDbEscapedString& sql) { int count = -1; //will be changed only on success of querySingleNumber() const tristate result = querySingleNumber( KDbEscapedString("SELECT COUNT() FROM (") + sql + ") AS kdb__subquery", &count); if (~result) { count = 0; } return count; } int KDbConnection::recordCount(const KDbTableSchema& tableSchema) { //! @todo does not work with non-SQL data sources int count = -1; // will be changed only on success of querySingleNumber() const tristate result = querySingleNumber(KDbEscapedString("SELECT COUNT(*) FROM ") + tableSchema.connection()->escapeIdentifier(tableSchema.name()), &count); if (~result) { count = 0; } return count; } int KDbConnection::recordCount(KDbQuerySchema* querySchema, const QList& params) { //! @todo does not work with non-SQL data sources int count = -1; //will be changed only on success of querySingleNumber() KDbNativeStatementBuilder builder(this, KDb::DriverEscaping); KDbEscapedString subSql; if (!builder.generateSelectStatement(&subSql, querySchema, params)) { return -1; } const tristate result = querySingleNumber( KDbEscapedString("SELECT COUNT(*) FROM (") + subSql + ") AS kdb__subquery", &count); if (~result) { count = 0; } return count; } int KDbConnection::recordCount(KDbTableOrQuerySchema* tableOrQuery, const QList& params) { if (tableOrQuery) { if (tableOrQuery->table()) return recordCount(*tableOrQuery->table()); if (tableOrQuery->query()) return recordCount(tableOrQuery->query(), params); } return -1; } KDbConnectionOptions* KDbConnection::options() { return &d->options; } void KDbConnection::addCursor(KDbCursor* cursor) { d->cursors.insert(cursor); } void KDbConnection::takeCursor(KDbCursor* cursor) { if (d && !d->cursors.isEmpty()) { // checking because this may be called from ~KDbConnection() d->cursors.remove(cursor); } } KDbPreparedStatement KDbConnection::prepareStatement(KDbPreparedStatement::Type type, KDbFieldList* fields, const QStringList& whereFieldNames) { //! @todo move to ConnectionInterface just like we moved execute() and prepare() to KDbPreparedStatementInterface... KDbPreparedStatementInterface *iface = prepareStatementInternal(); if (!iface) return KDbPreparedStatement(); return KDbPreparedStatement(iface, type, fields, whereFieldNames); } KDbEscapedString KDbConnection::recentSqlString() const { return result().errorSql().isEmpty() ? m_result.sql() : result().errorSql(); } KDbEscapedString KDbConnection::escapeString(const QString& str) const { return d->driver->escapeString(str); } //! @todo extraMessages #if 0 static const char *extraMessages[] = { QT_TRANSLATE_NOOP("KDbConnection", "Unknown error.") }; #endif diff --git a/src/KDbConnection_p.h b/src/KDbConnection_p.h index b0652023..df52d3d1 100644 --- a/src/KDbConnection_p.h +++ b/src/KDbConnection_p.h @@ -1,210 +1,213 @@ /* This file is part of the KDE project Copyright (C) 2005 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 License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KDB_CONNECTION_P_H #define KDB_CONNECTION_P_H #include "KDbConnectionData.h" #include "KDbConnection.h" #include "KDbConnectionOptions.h" #include "kdb_export.h" #include "KDbParser.h" #include "KDbProperties.h" #include "KDbQuerySchema_p.h" #include "KDbVersionInfo.h" //! Interface for accessing connection's internal result, for use by drivers. class KDB_EXPORT KDbConnectionInternal { public: explicit KDbConnectionInternal(KDbConnection *conn); KDbConnection* const connection; private: Q_DISABLE_COPY(KDbConnectionInternal) }; class KDbConnectionPrivate { Q_DECLARE_TR_FUNCTIONS(KDbConnectionPrivate) public: KDbConnectionPrivate(KDbConnection* const conn, KDbDriver *drv, const KDbConnectionData& _connData, const KDbConnectionOptions &_options); ~KDbConnectionPrivate(); void deleteAllCursors(); void errorInvalidDBContents(const QString& details); QString strItIsASystemObject() const; inline KDbParser *parser() { return m_parser ? m_parser : (m_parser = new KDbParser(conn)); } inline KDbTableSchema* table(const QString& name) const { return m_tablesByName.value(name); } inline KDbTableSchema* table(int id) const { return m_tables.value(id); } //! used just for removing system KDbTableSchema objects on db close. inline QSet internalKDbTables() const { return m_internalKDbTables; } /*! Allocates all needed table KDb system objects for kexi__* KDb library's system tables schema. These objects are used internally in this connection and are added to list of tables (by name, not by id because these have no ids). */ void setupKDbSystemSchema(); void insertTable(KDbTableSchema* tableSchema); /*! Removes table schema having identifier @a id from internal structures and destroys it. Does not make any change at the backend. */ void removeTable(int id); void takeTable(KDbTableSchema* tableSchema); void renameTable(KDbTableSchema* tableSchema, const QString& newName); void changeTableId(KDbTableSchema* tableSchema, int newId); void clearTables(); inline KDbQuerySchema* query(const QString& name) const { return m_queriesByName.value(name); } inline KDbQuerySchema* query(int id) const { return m_queries.value(id); } void insertQuery(KDbQuerySchema* query); /*! Removes @a querySchema from internal structures and destroys it. Does not make any change at the backend. */ void removeQuery(KDbQuerySchema* querySchema); void setQueryObsolete(KDbQuerySchema* query); void clearQueries(); /*! @return a full table schema for a table retrieved using 'kexi__*' system tables. Connection keeps ownership of the returned object. Used internally by tableSchema() methods. On failure deletes @a table and returns @c nullptr. */ KDbTableSchema* setupTableSchema(KDbTableSchema *table) Q_REQUIRED_RESULT; /*! @return a full query schema for a query using 'kexi__*' system tables. Connection keeps ownership of the returned object. Used internally by querySchema() methods. On failure deletes @a query and returns @c nullptr. */ KDbQuerySchema* setupQuerySchema(KDbQuerySchema *query) Q_REQUIRED_RESULT; //! @return cached fields expanded information for @a query KDbQuerySchemaFieldsExpanded *fieldsExpanded(const KDbQuerySchema *query); //! Inserts cached fields expanded information for @a query void insertFieldsExpanded(const KDbQuerySchema *query, KDbQuerySchemaFieldsExpanded *cache); + //! Removes cached fields expanded information for @a query + void removeFieldsExpanded(const KDbQuerySchema *query); + KDbConnection* const conn; //!< The @a KDbConnection instance this @a KDbConnectionPrivate belongs to. KDbConnectionData connData; //!< the @a KDbConnectionData used within that connection. //! True for read only connection. Used especially for file-based drivers. KDbConnectionOptions options; //!< The driver this @a KDbConnection instance uses. KDbDriver * const driver; /*! Default transaction handle. If transactions are supported: Any operation on database (e.g. inserts) that is started without specifying transaction context, will be performed in the context of this transaction. */ KDbTransaction default_trans; QList transactions; QHash* > tableSchemaChangeListeners; QHash* > queryTableSchemaChangeListeners; //! Used in KDbConnection::setQuerySchemaObsolete( const QString& queryName ) //! to collect obsolete queries. THese are deleted on connection deleting. QSet obsoleteQueries; //! server version information for this connection. KDbServerVersionInfo serverVersion; //! Database version information for this connection. KDbVersionInfo databaseVersion; KDbParser *m_parser = nullptr; //! cursors created for this connection QSet cursors; //! Database properties KDbProperties dbProperties; QString availableDatabaseName; //!< used by anyAvailableDatabaseName() QString usedDatabase; //!< database name that is opened now (the currentDatabase() name) //! true if rollbackTransaction() and commitTransaction() shouldn't remove //! the transaction object from 'transactions' list; used by closeDatabase() bool dontRemoveTransactions = false; //! used to avoid endless recursion between useDatabase() and databaseExists() //! when useTemporaryDatabaseIfNeeded() works bool skipDatabaseExistsCheckInUseDatabase = false; /*! Used when single transactions are only supported (KDbDriver::SingleTransactions). True value means default KDbTransaction has been started inside connection object (by beginAutoCommitTransaction()), otherwise default transaction has been started outside of the object (e.g. before createTable()), so we shouldn't autocommit the transaction in commitAutoCommitTransaction(). Also, beginAutoCommitTransaction() doesn't restarts transaction if default_trans_started_inside is false. Such behavior allows user to execute a sequence of actions like CREATE TABLE...; INSERT DATA...; within a single transaction and commit it or rollback by hand. */ bool defaultTransactionStartedInside = false; bool isConnected = false; bool autoCommit = true; bool insideCloseDatabase = false; //!< helper: true while closeDatabase() is executed private: //! Table schemas retrieved on demand with tableSchema() QHash m_tables; QHash m_tablesByName; //! used just for removing system KDbTableSchema objects on db close. QSet m_internalKDbTables; //! Query schemas retrieved on demand with querySchema() QHash m_queries; QHash m_queriesByName; KDbUtils::AutodeletedHash m_fieldsExpandedCache; Q_DISABLE_COPY(KDbConnectionPrivate) }; #endif diff --git a/src/KDbNativeStatementBuilder.cpp b/src/KDbNativeStatementBuilder.cpp index 72f6a24a..e05b307a 100644 --- a/src/KDbNativeStatementBuilder.cpp +++ b/src/KDbNativeStatementBuilder.cpp @@ -1,518 +1,533 @@ /* This file is part of the KDE project - Copyright (C) 2003-2016 Jarosław Staniek + Copyright (C) 2003-2017 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 License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KDbNativeStatementBuilder.h" #include "KDbConnection.h" #include "kdb_debug.h" #include "KDbDriverBehavior.h" #include "KDbDriver_p.h" #include "KDbExpression.h" #include "KDbLookupFieldSchema.h" #include "KDbOrderByColumn.h" #include "KDbQueryAsterisk.h" #include "KDbQuerySchema.h" #include "KDbQuerySchemaParameter.h" #include "KDbRelationship.h" KDbSelectStatementOptions::~KDbSelectStatementOptions() { } //================================================ class Q_DECL_HIDDEN KDbNativeStatementBuilder::Private { public: Private() {} //! @todo use equivalent of QPointer KDbConnection *connection; KDb::IdentifierEscapingType dialect; private: Q_DISABLE_COPY(Private) }; //================================================ KDbNativeStatementBuilder::KDbNativeStatementBuilder(KDbConnection *connection, KDb::IdentifierEscapingType dialect) : d(new Private) { d->connection = connection; d->dialect = dialect; } KDbNativeStatementBuilder::~KDbNativeStatementBuilder() { delete d; } static bool selectStatementInternal(KDbEscapedString *target, KDbConnection *connection, KDb::IdentifierEscapingType dialect, KDbQuerySchema* querySchema, const KDbSelectStatementOptions& options, const QList& parameters) { Q_ASSERT(target); Q_ASSERT(querySchema); //"SELECT FROM ..." is theoretically allowed " //if (querySchema.fieldCount()<1) // return QString(); // Each SQL identifier needs to be escaped in the generated query. const KDbDriver *driver = dialect == KDb::DriverEscaping ? connection->driver() : nullptr; if (!querySchema->statement().isEmpty()) { //! @todo replace with KDbNativeQuerySchema? It shouldn't be here. *target = querySchema->statement(); return true; } //! @todo looking at singleTable is visually nice but a field name can conflict //! with function or variable name... int number = 0; - bool singleTable = querySchema->tables()->count() <= 1; + QList* tables = querySchema->tables(); + bool singleTable = tables->count() <= 1; if (singleTable) { //make sure we will have single table: foreach(KDbField *f, *querySchema->fields()) { if (querySchema->isColumnVisible(number) && f->table() && f->table()->lookupFieldSchema(*f)) { //uups, no, there's at least one left join singleTable = false; break; } number++; } } KDbEscapedString sql; //final sql string sql.reserve(4096); KDbEscapedString s_additional_joins; //additional joins needed for lookup fields KDbEscapedString s_additional_fields; //additional fields to append to the fields list int internalUniqueTableAliasNumber = 0; //used to build internalUniqueTableAliases int internalUniqueQueryAliasNumber = 0; //used to build internalUniqueQueryAliases number = 0; QList subqueries_for_lookup_data; // subqueries will be added to FROM section const QString kdb_subquery_prefix = QStringLiteral("__kdb_subquery_"); KDbQuerySchemaParameterValueListIterator paramValuesIt(parameters); KDbQuerySchemaParameterValueListIterator *paramValuesItPtr = parameters.isEmpty() ? nullptr : ¶mValuesIt; foreach(KDbField *f, *querySchema->fields()) { if (querySchema->isColumnVisible(number)) { if (!sql.isEmpty()) sql += ", "; if (f->isQueryAsterisk()) { KDbQueryAsterisk *asterisk = static_cast(f); if (!singleTable && asterisk->isSingleTableAsterisk()) { //single-table * sql.append(KDb::escapeIdentifier(driver, asterisk->table()->name())).append(".*"); - } - else { //all-tables * (or simplified table.* when there's only one table) - sql += '*'; + } else { + /* All-tables asterisk + NOTE: do not output in this form because there can be extra tables + automatically added for obtaining lookup data what changes number of fields. + Reliable solution to that: for tables T1..Tn output T1.*,..Tn.* + Example for Northwind: + - instead of: SELECT * FROM orders LEFT OUTER JOIN + customers ON orders.customerid=customers.customerid + - use this: SELECT orders.*, customers.contactname FROM orders LEFT OUTER JOIN + customers ON orders.customerid=customers.customerid + */ + KDbEscapedString s_tables; + for (KDbTableSchema *table : *tables) { + if (!s_tables.isEmpty()) { + s_tables += ", "; + } + s_tables.append(KDb::escapeIdentifier(driver, table->name()) + QLatin1String(".*")); + } + sql += s_tables; } } else { if (f->isExpression()) { sql += f->expression().toString(driver, paramValuesItPtr); } else { if (!f->table()) {//sanity check return false; } QString tableName; int tablePosition = querySchema->tableBoundToColumn(number); if (tablePosition >= 0) { tableName = KDb::iifNotEmpty(querySchema->tableAlias(tablePosition), f->table()->name()); } if (options.addVisibleLookupColumns()) { // try to find table/alias name harder if (tableName.isEmpty()) { tableName = querySchema->tableAlias(f->table()->name()); } if (tableName.isEmpty()) { tableName = f->table()->name(); } } if (!singleTable && !tableName.isEmpty()) { sql.append(KDb::escapeIdentifier(driver, tableName)).append('.'); } sql += KDb::escapeIdentifier(driver, f->name()); } const QString aliasString(querySchema->columnAlias(number)); if (!aliasString.isEmpty()) { sql.append(" AS ").append(aliasString); } //! @todo add option that allows to omit "AS" keyword } KDbLookupFieldSchema *lookupFieldSchema = (options.addVisibleLookupColumns() && f->table()) ? f->table()->lookupFieldSchema(*f) : nullptr; if (lookupFieldSchema && lookupFieldSchema->boundColumn() >= 0) { // Lookup field schema found // Now we also need to fetch "visible" value from the lookup table, not only the value of binding. // -> build LEFT OUTER JOIN clause for this purpose (LEFT, not INNER because the binding can be broken) // "LEFT OUTER JOIN lookupTable ON thisTable.thisField=lookupTable.boundField" KDbLookupFieldSchemaRecordSource recordSource = lookupFieldSchema->recordSource(); if (recordSource.type() == KDbLookupFieldSchemaRecordSource::Type::Table) { KDbTableSchema *lookupTable = connection->tableSchema(recordSource.name()); KDbFieldList* visibleColumns = nullptr; KDbField *boundField = nullptr; if (lookupTable && lookupFieldSchema->boundColumn() < lookupTable->fieldCount() && (visibleColumns = lookupTable->subList(lookupFieldSchema->visibleColumns())) && (boundField = lookupTable->field(lookupFieldSchema->boundColumn()))) { //add LEFT OUTER JOIN if (!s_additional_joins.isEmpty()) s_additional_joins += ' '; const QString internalUniqueTableAlias( QLatin1String("__kdb_") + lookupTable->name() + QLatin1Char('_') + QString::number(internalUniqueTableAliasNumber++)); s_additional_joins += KDbEscapedString("LEFT OUTER JOIN %1 AS %2 ON %3.%4=%5.%6") .arg(KDb::escapeIdentifier(driver, lookupTable->name())) .arg(internalUniqueTableAlias) .arg(KDb::escapeIdentifier(driver, querySchema->tableAliasOrName(f->table()->name()))) .arg(KDb::escapeIdentifier(driver, f->name())) .arg(internalUniqueTableAlias) .arg(KDb::escapeIdentifier(driver, boundField->name())); //add visibleField to the list of SELECTed fields //if it is not yet present there if (!s_additional_fields.isEmpty()) s_additional_fields += ", "; //! @todo Add lookup schema option for separator other than ' ' or even option for placeholders like "Name ? ?" //! @todo Add possibility for joining the values at client side. s_additional_fields += visibleColumns->sqlFieldsList( connection, QLatin1String(" || ' ' || "), internalUniqueTableAlias, dialect); } delete visibleColumns; } else if (recordSource.type() == KDbLookupFieldSchemaRecordSource::Type::Query) { KDbQuerySchema *lookupQuery = connection->querySchema(recordSource.name()); if (!lookupQuery) { kdbWarning() << "!lookupQuery"; return false; } const KDbQueryColumnInfo::Vector fieldsExpanded( lookupQuery->fieldsExpanded(connection)); if (lookupFieldSchema->boundColumn() >= fieldsExpanded.count()) { kdbWarning() << "lookupFieldSchema->boundColumn() >= fieldsExpanded.count()"; return false; } KDbQueryColumnInfo *boundColumnInfo = fieldsExpanded.at(lookupFieldSchema->boundColumn()); if (!boundColumnInfo) { kdbWarning() << "!boundColumnInfo"; return false; } KDbField *boundField = boundColumnInfo->field(); if (!boundField) { kdbWarning() << "!boundField"; return false; } //add LEFT OUTER JOIN if (!s_additional_joins.isEmpty()) s_additional_joins += ' '; KDbEscapedString internalUniqueQueryAlias(KDb::escapeIdentifier( driver, kdb_subquery_prefix + lookupQuery->name() + QLatin1Char('_') + QString::number(internalUniqueQueryAliasNumber++))); KDbNativeStatementBuilder builder(connection, dialect); KDbEscapedString subSql; if (!builder.generateSelectStatement(&subSql, lookupQuery, options, parameters)) { return false; } s_additional_joins += KDbEscapedString("LEFT OUTER JOIN (%1) AS %2 ON %3.%4=%5.%6") .arg(subSql) .arg(internalUniqueQueryAlias) .arg(KDb::escapeIdentifier(driver, f->table()->name())) .arg(KDb::escapeIdentifier(driver, f->name())) .arg(internalUniqueQueryAlias) .arg(KDb::escapeIdentifier(driver, boundColumnInfo->aliasOrName())); if (!s_additional_fields.isEmpty()) s_additional_fields += ", "; const QList visibleColumns(lookupFieldSchema->visibleColumns()); KDbEscapedString expression; foreach(int visibleColumnIndex, visibleColumns) { //! @todo Add lookup schema option for separator other than ' ' or even option for placeholders like "Name ? ?" //! @todo Add possibility for joining the values at client side. if (fieldsExpanded.count() <= visibleColumnIndex) { kdbWarning() << "fieldsExpanded.count() <= (*visibleColumnsIt) : " << fieldsExpanded.count() << " <= " << visibleColumnIndex; return false; } if (!expression.isEmpty()) expression += " || ' ' || "; expression += ( internalUniqueQueryAlias + '.' + KDb::escapeIdentifier(driver, fieldsExpanded.value(visibleColumnIndex)->aliasOrName()) ); } s_additional_fields += expression; } else { kdbWarning() << "unsupported record source type" << recordSource.typeName(); return false; } } } number++; } //add lookup fields if (!s_additional_fields.isEmpty()) sql += (", " + s_additional_fields); if (driver && options.alsoRetrieveRecordId()) { //append rowid column + //! @todo Check if the rowid isn't already part of regular SELECT columns, if so, don't add KDbEscapedString s; if (!sql.isEmpty()) s = ", "; if (querySchema->masterTable()) s += KDbEscapedString(querySchema->tableAliasOrName(querySchema->masterTable()->name())) + '.'; s += KDbDriverPrivate::behavior(driver)->ROW_ID_FIELD_NAME; sql += s; } if (sql.isEmpty()) { sql.prepend("SELECT"); // "SELECT FROM ..." case } else { sql.prepend("SELECT "); } - QList* tables = querySchema->tables(); - if ((tables && !tables->isEmpty()) || !subqueries_for_lookup_data.isEmpty()) { + if (!tables->isEmpty() || !subqueries_for_lookup_data.isEmpty()) { sql += " FROM "; KDbEscapedString s_from; - if (tables) { - number = 0; - foreach(KDbTableSchema *table, *tables) { - if (!s_from.isEmpty()) - s_from += ", "; - s_from += KDb::escapeIdentifier(driver, table->name()); - const QString aliasString(querySchema->tableAlias(number)); - if (!aliasString.isEmpty()) - s_from.append(" AS ").append(aliasString); - number++; - } + number = 0; + foreach(KDbTableSchema *table, *tables) { + if (!s_from.isEmpty()) + s_from += ", "; + s_from += KDb::escapeIdentifier(driver, table->name()); + const QString aliasString(querySchema->tableAlias(number)); + if (!aliasString.isEmpty()) + s_from.append(" AS ").append(aliasString); + number++; } // add subqueries for lookup data int subqueries_for_lookup_data_counter = 0; foreach(KDbQuerySchema* subQuery, subqueries_for_lookup_data) { if (!s_from.isEmpty()) s_from += ", "; KDbEscapedString subSql; if (!selectStatementInternal(&subSql, connection, dialect, subQuery, options, parameters)) { return false; } s_from += '(' + subSql + ") AS " + KDb::escapeIdentifier( driver, kdb_subquery_prefix + QString::number(subqueries_for_lookup_data_counter++)); } sql += s_from; } KDbEscapedString s_where; s_where.reserve(4096); //JOINS if (!s_additional_joins.isEmpty()) { sql += ' ' + s_additional_joins + ' '; } //! @todo: we're using WHERE for joins now; use INNER/LEFT/RIGHT JOIN later //WHERE bool wasWhere = false; //for later use foreach(KDbRelationship *rel, *querySchema->relationships()) { if (s_where.isEmpty()) { wasWhere = true; } else s_where += " AND "; KDbEscapedString s_where_sub; foreach(const KDbField::Pair &pair, *rel->fieldPairs()) { if (!s_where_sub.isEmpty()) s_where_sub += " AND "; s_where_sub += KDbEscapedString(KDb::escapeIdentifier(driver, pair.first->table()->name())) + '.' + KDb::escapeIdentifier(driver, pair.first->name()) + " = " + KDb::escapeIdentifier(driver, pair.second->table()->name()) + '.' + KDb::escapeIdentifier(driver, pair.second->name()); } if (rel->fieldPairs()->count() > 1) { s_where_sub.prepend('('); s_where_sub += ')'; } s_where += s_where_sub; } //EXPLICITLY SPECIFIED WHERE EXPRESSION if (!querySchema->whereExpression().isNull()) { if (wasWhere) { //! @todo () are not always needed s_where = '(' + s_where + ") AND (" + querySchema->whereExpression().toString(driver, paramValuesItPtr) + ')'; } else { s_where = querySchema->whereExpression().toString(driver, paramValuesItPtr); } } if (!s_where.isEmpty()) sql += " WHERE " + s_where; //! @todo (js) add other sql parts //(use wasWhere here) // ORDER BY KDbEscapedString orderByString(querySchema->orderByColumnList()->toSqlString( !singleTable /*includeTableName*/, connection, dialect)); const QVector pkeyFieldsOrder(querySchema->pkeyFieldsOrder(connection)); if (dialect == KDb::DriverEscaping && orderByString.isEmpty() && !pkeyFieldsOrder.isEmpty()) { // Native only: add automatic ORDER BY if there is no explicitly defined one // (especially helps when there are complex JOINs) KDbOrderByColumnList automaticPKOrderBy; const KDbQueryColumnInfo::Vector fieldsExpanded(querySchema->fieldsExpanded(connection)); foreach(int pkeyFieldsIndex, pkeyFieldsOrder) { if (pkeyFieldsIndex < 0) // no field mentioned in this query continue; if (pkeyFieldsIndex >= fieldsExpanded.count()) { kdbWarning() << "ORDER BY: (*it) >= fieldsExpanded.count() - " << pkeyFieldsIndex << " >= " << fieldsExpanded.count(); continue; } KDbQueryColumnInfo *ci = fieldsExpanded[ pkeyFieldsIndex ]; automaticPKOrderBy.appendColumn(ci); } orderByString = automaticPKOrderBy.toSqlString(!singleTable /*includeTableName*/, connection, dialect); } if (!orderByString.isEmpty()) sql += (" ORDER BY " + orderByString); //kdbDebug() << sql; *target = sql; return true; } bool KDbNativeStatementBuilder::generateSelectStatement(KDbEscapedString *target, KDbQuerySchema* querySchema, const KDbSelectStatementOptions& options, const QList& parameters) const { return selectStatementInternal(target, d->connection, d->dialect, querySchema, options, parameters); } bool KDbNativeStatementBuilder::generateSelectStatement(KDbEscapedString *target, KDbQuerySchema* querySchema, const QList& parameters) const { return selectStatementInternal(target, d->connection, d->dialect, querySchema, KDbSelectStatementOptions(), parameters); } bool KDbNativeStatementBuilder::generateSelectStatement(KDbEscapedString *target, KDbTableSchema* tableSchema, const KDbSelectStatementOptions& options) const { return generateSelectStatement(target, tableSchema->query(), options); } bool KDbNativeStatementBuilder::generateCreateTableStatement(KDbEscapedString *target, const KDbTableSchema& tableSchema) const { if (!target) { return false; } // Each SQL identifier needs to be escaped in the generated query. const KDbDriver *driver = d->dialect == KDb::DriverEscaping ? d->connection->driver() : nullptr; KDbEscapedString sql; sql.reserve(4096); sql = KDbEscapedString("CREATE TABLE ") + KDb::escapeIdentifier(driver, tableSchema.name()) + " ("; bool first = true; for (const KDbField *field : *tableSchema.fields()) { if (first) first = false; else sql += ", "; KDbEscapedString v = KDbEscapedString(KDb::escapeIdentifier(driver, field->name())) + ' '; const bool autoinc = field->isAutoIncrement(); const bool pk = field->isPrimaryKey() || (autoinc && driver && driver->behavior()->AUTO_INCREMENT_REQUIRES_PK); //! @todo warning: ^^^^^ this allows only one autonumber per table when AUTO_INCREMENT_REQUIRES_PK==true! const KDbField::Type type = field->type(); // cache: evaluating type of expressions can be expensive if (autoinc && d->connection->driver()->behavior()->SPECIAL_AUTO_INCREMENT_DEF) { if (pk) v.append(d->connection->driver()->behavior()->AUTO_INCREMENT_TYPE).append(' ') .append(d->connection->driver()->behavior()->AUTO_INCREMENT_PK_FIELD_OPTION); else v.append(d->connection->driver()->behavior()->AUTO_INCREMENT_TYPE).append(' ') .append(d->connection->driver()->behavior()->AUTO_INCREMENT_FIELD_OPTION); } else { if (autoinc && !d->connection->driver()->behavior()->AUTO_INCREMENT_TYPE.isEmpty()) v += d->connection->driver()->behavior()->AUTO_INCREMENT_TYPE; else v += d->connection->driver()->sqlTypeName(type, *field); if (KDbField::isIntegerType(type) && field->isUnsigned()) { v.append(' ').append(d->connection->driver()->behavior()->UNSIGNED_TYPE_KEYWORD); } if (KDbField::isFPNumericType(type) && field->precision() > 0) { if (field->scale() > 0) v += QString::fromLatin1("(%1,%2)").arg(field->precision()).arg(field->scale()); else v += QString::fromLatin1("(%1)").arg(field->precision()); } else if (type == KDbField::Text) { int realMaxLen; if (d->connection->driver()->behavior()->TEXT_TYPE_MAX_LENGTH == 0) { realMaxLen = field->maxLength(); // allow to skip (N) } else { // max length specified by driver if (field->maxLength() == 0) { // as long as possible realMaxLen = d->connection->driver()->behavior()->TEXT_TYPE_MAX_LENGTH; } else { // not longer than specified by driver realMaxLen = qMin(d->connection->driver()->behavior()->TEXT_TYPE_MAX_LENGTH, field->maxLength()); } } if (realMaxLen > 0) { v += QString::fromLatin1("(%1)").arg(realMaxLen); } } if (autoinc) { v.append(' ').append(pk ? d->connection->driver()->behavior()->AUTO_INCREMENT_PK_FIELD_OPTION : d->connection->driver()->behavior()->AUTO_INCREMENT_FIELD_OPTION); } else { //! @todo here is automatically a single-field key created if (pk) v += " PRIMARY KEY"; } if (!pk && field->isUniqueKey()) v += " UNIQUE"; ///@todo IS this ok for all engines?: if (!autoinc && !field->isPrimaryKey() && field->isNotNull()) if (!autoinc && !pk && field->isNotNull()) v += " NOT NULL"; //only add not null option if no autocommit is set if (d->connection->driver()->supportsDefaultValue(*field) && field->defaultValue().isValid()) { KDbEscapedString valToSql(d->connection->driver()->valueToSql(field, field->defaultValue())); if (!valToSql.isEmpty()) //for sanity v += " DEFAULT " + valToSql; } } sql += v; } sql += ')'; *target = sql; return true; } diff --git a/src/KDbQuerySchema.cpp b/src/KDbQuerySchema.cpp index 2e0f742d..fa9f4bd4 100644 --- a/src/KDbQuerySchema.cpp +++ b/src/KDbQuerySchema.cpp @@ -1,1387 +1,1388 @@ /* This file is part of the KDE project Copyright (C) 2003-2017 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 "KDbQuerySchema.h" #include "KDbQuerySchema_p.h" #include "KDbQueryAsterisk.h" #include "KDbConnection.h" #include "KDbConnection_p.h" #include "kdb_debug.h" #include "KDbLookupFieldSchema.h" #include "KDbOrderByColumn.h" #include "KDbParser_p.h" #include "KDbQuerySchemaParameter.h" #include "KDbRelationship.h" QString escapeIdentifier(const QString& name, KDbConnection *conn, KDb::IdentifierEscapingType escapingType) { switch (escapingType) { case KDb::DriverEscaping: if (conn) return conn->escapeIdentifier(name); break; case KDb::KDbEscaping: return KDb::escapeIdentifier(name); } return QLatin1Char('"') + name + QLatin1Char('"'); } KDbQuerySchema::KDbQuerySchema() : KDbFieldList(false)//fields are not owned by KDbQuerySchema object , KDbObject(KDb::QueryObjectType) , d(new KDbQuerySchemaPrivate(this)) { } KDbQuerySchema::KDbQuerySchema(KDbTableSchema *tableSchema) : KDbFieldList(false)//fields are not owned by KDbQuerySchema object , KDbObject(KDb::QueryObjectType) , d(new KDbQuerySchemaPrivate(this)) { if (tableSchema) { d->masterTable = tableSchema; /*if (!d->masterTable) { kdbWarning() << "!d->masterTable"; m_name.clear(); return; }*/ addTable(d->masterTable); //defaults: //inherit name from a table setName(d->masterTable->name()); //inherit caption from a table setCaption(d->masterTable->caption()); // add explicit field list to avoid problems (e.g. with fields added outside of the app): foreach(KDbField* f, *d->masterTable->fields()) { addField(f); } } } KDbQuerySchema::KDbQuerySchema(const KDbQuerySchema& querySchema, KDbConnection *conn) : KDbFieldList(querySchema, false /* !deepCopyFields */) , KDbObject(querySchema) , d(new KDbQuerySchemaPrivate(this, querySchema.d)) { //only deep copy query asterisks foreach(KDbField* f, *querySchema.fields()) { KDbField *copiedField; if (dynamic_cast(f)) { copiedField = f->copy(); if (static_cast(f->parent()) == &querySchema) { copiedField->setParent(this); } } else { copiedField = f; } addField(copiedField); } // this deep copy must be after the 'd' initialization because fieldsExpanded() is used there d->orderByColumnList = new KDbOrderByColumnList(*querySchema.d->orderByColumnList, conn, const_cast(&querySchema), this); } KDbQuerySchema::~KDbQuerySchema() { delete d; } void KDbQuerySchema::clear() { KDbFieldList::clear(); KDbObject::clear(); d->clear(); } /*virtual*/ bool KDbQuerySchema::insertField(int position, KDbField *field) { return insertFieldInternal(position, field, -1/*don't bind*/, true); } bool KDbQuerySchema::insertInvisibleField(int position, KDbField *field) { return insertFieldInternal(position, field, -1/*don't bind*/, false); } bool KDbQuerySchema::insertField(int position, KDbField *field, int bindToTable) { return insertFieldInternal(position, field, bindToTable, true); } bool KDbQuerySchema::insertInvisibleField(int position, KDbField *field, int bindToTable) { return insertFieldInternal(position, field, bindToTable, false); } bool KDbQuerySchema::insertFieldInternal(int position, KDbField *field, int bindToTable, bool visible) { if (!field) { kdbWarning() << "!field"; return false; } if (position > fieldCount()) { kdbWarning() << "position" << position << "out of range"; return false; } if (!field->isQueryAsterisk() && !field->isExpression() && !field->table()) { kdbWarning() << "field" << field->name() << "must contain table information!"; return false; } if (fieldCount() >= d->visibility.size()) { d->visibility.resize(d->visibility.size()*2); d->tablesBoundToColumns.resize(d->tablesBoundToColumns.size()*2); } d->clearCachedData(); if (!KDbFieldList::insertField(position, field)) { return false; } if (field->isQueryAsterisk()) { d->asterisks.append(field); //if this is single-table asterisk, //add a table to list if doesn't exist there: if (field->table() && !d->tables.contains(field->table())) d->tables.append(field->table()); } else if (field->table()) { //add a table to list if doesn't exist there: if (!d->tables.contains(field->table())) d->tables.append(field->table()); } //update visibility //--move bits to make a place for a new one for (int i = fieldCount() - 1; i > position; i--) d->visibility.setBit(i, d->visibility.testBit(i - 1)); d->visibility.setBit(position, visible); //bind to table if (bindToTable < -1 || bindToTable > d->tables.count()) { kdbWarning() << "bindToTable" << bindToTable << "out of range"; bindToTable = -1; } //--move items to make a place for a new one for (int i = fieldCount() - 1; i > position; i--) d->tablesBoundToColumns[i] = d->tablesBoundToColumns[i-1]; d->tablesBoundToColumns[position] = bindToTable; kdbDebug() << "bound to table" << bindToTable; if (bindToTable == -1) kdbDebug() << " "; else kdbDebug() << " name=" << d->tables.at(bindToTable)->name() << " alias=" << tableAlias(bindToTable); QString s; for (int i = 0; i < fieldCount();i++) s += (QString::number(d->tablesBoundToColumns[i]) + QLatin1Char(' ')); kdbDebug() << "tablesBoundToColumns == [" << s << "]"; if (field->isExpression()) d->regenerateExprAliases = true; return true; } int KDbQuerySchema::tableBoundToColumn(int columnPosition) const { int res = d->tablesBoundToColumns.value(columnPosition, -99); if (res == -99) { kdbWarning() << "columnPosition" << columnPosition << "out of range"; return -1; } return res; } bool KDbQuerySchema::addField(KDbField* field) { return insertField(fieldCount(), field); } bool KDbQuerySchema::addField(KDbField* field, int bindToTable) { return insertField(fieldCount(), field, bindToTable); } bool KDbQuerySchema::addInvisibleField(KDbField* field) { return insertInvisibleField(fieldCount(), field); } bool KDbQuerySchema::addInvisibleField(KDbField* field, int bindToTable) { return insertInvisibleField(fieldCount(), field, bindToTable); } bool KDbQuerySchema::removeField(KDbField *field) { int indexOfAsterisk = -1; if (field->isQueryAsterisk()) { indexOfAsterisk = d->asterisks.indexOf(field); } if (!KDbFieldList::removeField(field)) { return false; } d->clearCachedData(); if (indexOfAsterisk >= 0) { //kdbDebug() << "d->asterisks.removeAt:" << field; //field->debug(); d->asterisks.removeAt(indexOfAsterisk); //this will destroy this asterisk } //! @todo should we also remove table for this field or asterisk? return true; } bool KDbQuerySchema::addExpressionInternal(const KDbExpression& expr, bool visible) { KDbField *field = new KDbField(this, expr); bool ok; if (visible) { ok = addField(field); } else { ok = addInvisibleField(field); } if (!ok) { delete field; } + d->ownedExpressionFields.append(field); return ok; } bool KDbQuerySchema::addExpression(const KDbExpression& expr) { return addExpressionInternal(expr, true); } bool KDbQuerySchema::addInvisibleExpression(const KDbExpression& expr) { return addExpressionInternal(expr, false); } bool KDbQuerySchema::isColumnVisible(int position) const { return (position < fieldCount()) ? d->visibility.testBit(position) : false; } void KDbQuerySchema::setColumnVisible(int position, bool visible) { if (position < fieldCount()) d->visibility.setBit(position, visible); } bool KDbQuerySchema::addAsteriskInternal(KDbQueryAsterisk *asterisk, bool visible) { if (!asterisk) { return false; } //make unique name asterisk->setName((asterisk->table() ? (asterisk->table()->name() + QLatin1String(".*")) : QString(QLatin1Char('*'))) + QString::number(asterisks()->count())); return visible ? addField(asterisk) : addInvisibleField(asterisk); } bool KDbQuerySchema::addAsterisk(KDbQueryAsterisk *asterisk) { return addAsteriskInternal(asterisk, true); } bool KDbQuerySchema::addInvisibleAsterisk(KDbQueryAsterisk *asterisk) { return addAsteriskInternal(asterisk, false); } QDebug operator<<(QDebug dbg, const KDbConnectionAndQuerySchema &connectionAndSchema) { KDbConnection* conn = std::get<0>(connectionAndSchema); const KDbQuerySchema& query = std::get<1>(connectionAndSchema); //fields KDbTableSchema *mt = query.masterTable(); dbg.nospace() << "QUERY"; dbg.space() << static_cast(query) << '\n'; dbg.nospace() << " - MASTERTABLE=" << (mt ? mt->name() : QLatin1String("")) << "\n - COLUMNS:\n"; if (query.fieldCount() > 0) dbg.nospace() << static_cast(query) << '\n'; else dbg.nospace() << "\n"; if (query.fieldCount() == 0) dbg.nospace() << " - NO FIELDS\n"; else dbg.nospace() << " - FIELDS EXPANDED ("; int fieldsExpandedCount = 0; bool first; if (query.fieldCount() > 0) { const KDbQueryColumnInfo::Vector fe(query.fieldsExpanded(conn)); fieldsExpandedCount = fe.size(); dbg.nospace() << fieldsExpandedCount << "):\n"; first = true; for (int i = 0; i < fieldsExpandedCount; i++) { KDbQueryColumnInfo *ci = fe[i]; if (first) first = false; else dbg.nospace() << ",\n"; dbg.nospace() << *ci; } dbg.nospace() << '\n'; } //it's safer to delete fieldsExpanded for now // (debugString() could be called before all fields are added) //bindings dbg.nospace() << " - BINDINGS:\n"; bool bindingsExist = false; for (int i = 0; i < query.fieldCount(); i++) { const int tablePos = query.tableBoundToColumn(i); if (tablePos >= 0) { const QString tAlias(query.tableAlias(tablePos)); if (!tAlias.isEmpty()) { bindingsExist = true; dbg.space() << "FIELD"; dbg.space() << static_cast(query).field(i)->name(); dbg.space() << "USES ALIAS"; dbg.space() << tAlias; dbg.space() << "OF TABLE"; dbg.space() << query.tables()->at(tablePos)->name() << '\n'; } } } if (!bindingsExist) { dbg.nospace() << "\n"; } //tables dbg.nospace() << " - TABLES:\n"; first = true; foreach(KDbTableSchema *table, *query.tables()) { if (first) first = false; else dbg.nospace() << ","; dbg.space() << table->name(); } if (query.tables()->isEmpty()) dbg.nospace() << ""; //aliases dbg.nospace() << "\n - COLUMN ALIASES:\n"; if (query.columnAliasesCount() == 0) { dbg.nospace() << "\n"; } else { int i = -1; foreach(KDbField *f, *query.fields()) { i++; const QString alias(query.columnAlias(i)); if (!alias.isEmpty()) { dbg.nospace() << QString::fromLatin1("FIELD #%1:").arg(i); dbg.space() << (f->name().isEmpty() ? QLatin1String("") : f->name()) << " -> " << alias << '\n'; } } } dbg.nospace() << "- TABLE ALIASES:\n"; if (query.tableAliasesCount() == 0) { dbg.nospace() << "\n"; } else { int i = -1; foreach(KDbTableSchema* table, *query.tables()) { i++; const QString alias(query.tableAlias(i)); if (!alias.isEmpty()) { dbg.nospace() << QString::fromLatin1("table #%1:").arg(i); dbg.space() << (table->name().isEmpty() ? QLatin1String("") : table->name()) << " -> " << alias << '\n'; } } } if (!query.whereExpression().isNull()) { dbg.nospace() << " - WHERE EXPRESSION:\n" << query.whereExpression() << '\n'; } dbg.nospace() << qPrintable(QString::fromLatin1(" - ORDER BY (%1):\n").arg(query.orderByColumnList()->count())); if (query.orderByColumnList()->isEmpty()) { dbg.nospace() << "\n"; } else { dbg.nospace() << *query.orderByColumnList(); } return dbg.nospace(); } KDbTableSchema* KDbQuerySchema::masterTable() const { if (d->masterTable) return d->masterTable; if (d->tables.isEmpty()) return nullptr; //try to find master table if there's only one table (with possible aliasses) QString tableNameLower; int num = -1; foreach(KDbTableSchema *table, d->tables) { num++; if (!tableNameLower.isEmpty() && table->name().toLower() != tableNameLower) { //two or more different tables return nullptr; } tableNameLower = tableAlias(num); } return d->tables.first(); } void KDbQuerySchema::setMasterTable(KDbTableSchema *table) { if (table) d->masterTable = table; } QList* KDbQuerySchema::tables() const { return &d->tables; } void KDbQuerySchema::addTable(KDbTableSchema *table, const QString& alias) { kdbDebug() << (void *)table << "alias=" << alias; if (!table) return; // only append table if: it has alias or it has no alias but there is no such table on the list if (alias.isEmpty() && d->tables.contains(table)) { int num = -1; foreach(KDbTableSchema *t, d->tables) { num++; if (0 == t->name().compare(table->name(), Qt::CaseInsensitive)) { if (tableAlias(num).isEmpty()) { kdbDebug() << "table" << table->name() << "without alias already added"; return; } } } } d->tables.append(table); if (!alias.isEmpty()) setTableAlias(d->tables.count() - 1, alias); } void KDbQuerySchema::removeTable(KDbTableSchema *table) { if (!table) return; if (d->masterTable == table) d->masterTable = nullptr; d->tables.removeAt(d->tables.indexOf(table)); //! @todo remove fields! } KDbTableSchema* KDbQuerySchema::table(const QString& tableName) const { //! @todo maybe use tables_byname? foreach(KDbTableSchema *table, d->tables) { if (0 == table->name().compare(tableName, Qt::CaseInsensitive)) { return table; } } return nullptr; } bool KDbQuerySchema::contains(KDbTableSchema *table) const { return d->tables.contains(table); } KDbField* KDbQuerySchema::findTableField(const QString &tableOrTableAndFieldName) const { QString tableName, fieldName; if (!KDb::splitToTableAndFieldParts(tableOrTableAndFieldName, &tableName, &fieldName, KDb::SetFieldNameIfNoTableName)) { return nullptr; } if (tableName.isEmpty()) { foreach(KDbTableSchema *table, d->tables) { if (table->field(fieldName)) return table->field(fieldName); } return nullptr; } KDbTableSchema *tableSchema = table(tableName); if (!tableSchema) return nullptr; return tableSchema->field(fieldName); } int KDbQuerySchema::columnAliasesCount() const { return d->columnAliasesCount(); } QString KDbQuerySchema::columnAlias(int position) const { return d->columnAlias(position); } bool KDbQuerySchema::hasColumnAlias(int position) const { return d->hasColumnAlias(position); } bool KDbQuerySchema::setColumnAlias(int position, const QString& alias) { if (position >= fieldCount()) { kdbWarning() << "position" << position << "out of range!"; return false; } const QString fixedAlias(alias.trimmed()); KDbField *f = KDbFieldList::field(position); if (f->captionOrName().isEmpty() && fixedAlias.isEmpty()) { kdbWarning() << "position" << position << "could not remove alias when no name is specified for expression column!"; return false; } return d->setColumnAlias(position, fixedAlias); } int KDbQuerySchema::tableAliasesCount() const { return d->tableAliases.count(); } QString KDbQuerySchema::tableAlias(int position) const { return d->tableAliases.value(position); } QString KDbQuerySchema::tableAlias(const QString& tableName) const { const int pos = tablePosition(tableName); if (pos == -1) { return QString(); } return d->tableAliases.value(pos); } QString KDbQuerySchema::tableAliasOrName(const QString& tableName) const { const int pos = tablePosition(tableName); if (pos == -1) { return QString(); } return KDb::iifNotEmpty(d->tableAliases.value(pos), tableName); } int KDbQuerySchema::tablePositionForAlias(const QString& name) const { return d->tablePositionForAlias(name); } int KDbQuerySchema::tablePosition(const QString& tableName) const { int num = -1; foreach(KDbTableSchema* table, d->tables) { num++; if (0 == table->name().compare(tableName, Qt::CaseInsensitive)) { return num; } } return -1; } QList KDbQuerySchema::tablePositions(const QString& tableName) const { QList result; int num = -1; foreach(KDbTableSchema* table, d->tables) { num++; if (0 == table->name().compare(tableName, Qt::CaseInsensitive)) { result += num; } } return result; } bool KDbQuerySchema::hasTableAlias(int position) const { return d->tableAliases.contains(position); } bool KDbQuerySchema::hasTableAlias(const QString &name) const { return d->tablePositionForAlias(name) != -1; } int KDbQuerySchema::columnPositionForAlias(const QString& name) const { return d->columnPositionForAlias(name); } bool KDbQuerySchema::hasColumnAlias(const QString &name) const { return d->columnPositionForAlias(name) != -1; } bool KDbQuerySchema::setTableAlias(int position, const QString& alias) { if (position >= d->tables.count()) { kdbWarning() << "position" << position << "out of range!"; return false; } const QString fixedAlias(alias.trimmed()); if (fixedAlias.isEmpty()) { const QString oldAlias(d->tableAliases.take(position)); if (!oldAlias.isEmpty()) { d->removeTablePositionForAlias(oldAlias); } return true; } return d->setTableAlias(position, fixedAlias); } QList* KDbQuerySchema::relationships() const { return &d->relations; } KDbField::List* KDbQuerySchema::asterisks() const { return &d->asterisks; } KDbEscapedString KDbQuerySchema::statement() const { return d->sql; } void KDbQuerySchema::setStatement(const KDbEscapedString& sql) { d->sql = sql; } const KDbField* KDbQuerySchema::field(KDbConnection *conn, const QString& identifier, ExpandMode mode) const { KDbQueryColumnInfo *ci = columnInfo(conn, identifier, mode); return ci ? ci->field() : nullptr; } KDbField* KDbQuerySchema::field(KDbConnection *conn, const QString& identifier, ExpandMode mode) { return const_cast( static_cast(this)->field(conn, identifier, mode)); } KDbField* KDbQuerySchema::field(int id) { return KDbFieldList::field(id); } const KDbField* KDbQuerySchema::field(int id) const { return KDbFieldList::field(id); } KDbQueryColumnInfo *KDbQuerySchema::columnInfo(KDbConnection *conn, const QString &identifier, ExpandMode mode) const { const KDbQuerySchemaFieldsExpanded *cache = computeFieldsExpanded(conn); return mode == ExpandMode::Expanded ? cache->columnInfosByNameExpanded.value(identifier) : cache->columnInfosByName.value(identifier); } KDbQueryColumnInfo::Vector KDbQuerySchema::fieldsExpandedInternal( KDbConnection *conn, FieldsExpandedMode mode, bool onlyVisible) const { if (!conn) { kdbWarning() << "Connection required"; return KDbQueryColumnInfo::Vector(); } KDbQuerySchemaFieldsExpanded *cache = computeFieldsExpanded(conn); const KDbQueryColumnInfo::Vector *realFieldsExpanded = onlyVisible ? &cache->visibleFieldsExpanded : &cache->fieldsExpanded; if (mode == FieldsExpandedMode::WithInternalFields || mode == FieldsExpandedMode::WithInternalFieldsAndRecordId) { //a ref to a proper pointer (as we cache the vector for two cases) KDbQueryColumnInfo::Vector& tmpFieldsExpandedWithInternal = (mode == FieldsExpandedMode::WithInternalFields) ? (onlyVisible ? cache->visibleFieldsExpandedWithInternal : cache->fieldsExpandedWithInternal) : (onlyVisible ? cache->visibleFieldsExpandedWithInternalAndRecordId : cache->fieldsExpandedWithInternalAndRecordId); //special case if (tmpFieldsExpandedWithInternal.isEmpty()) { //glue expanded and internal fields and cache it const int internalFieldCount = cache->internalFields.size(); const int fieldsExpandedVectorSize = realFieldsExpanded->size(); const int size = fieldsExpandedVectorSize + internalFieldCount + ((mode == FieldsExpandedMode::WithInternalFieldsAndRecordId) ? 1 : 0) /*ROWID*/; tmpFieldsExpandedWithInternal.resize(size); for (int i = 0; i < fieldsExpandedVectorSize; ++i) { tmpFieldsExpandedWithInternal[i] = realFieldsExpanded->at(i); } if (internalFieldCount > 0) { for (int i = 0; i < internalFieldCount; ++i) { KDbQueryColumnInfo *info = cache->internalFields[i]; tmpFieldsExpandedWithInternal[fieldsExpandedVectorSize + i] = info; } } if (mode == FieldsExpandedMode::WithInternalFieldsAndRecordId) { if (!d->fakeRecordIdField) { d->fakeRecordIdField = new KDbField(QLatin1String("rowID"), KDbField::BigInteger); d->fakeRecordIdCol = new KDbQueryColumnInfo(d->fakeRecordIdField, QString(), true); } tmpFieldsExpandedWithInternal[fieldsExpandedVectorSize + internalFieldCount] = d->fakeRecordIdCol; } } return tmpFieldsExpandedWithInternal; } if (mode == FieldsExpandedMode::Default) { return *realFieldsExpanded; } //mode == Unique: QSet columnsAlreadyFound; const int fieldsExpandedCount(realFieldsExpanded->count()); KDbQueryColumnInfo::Vector result(fieldsExpandedCount); //initial size is set //compute unique list int uniqueListCount = 0; for (int i = 0; i < fieldsExpandedCount; i++) { KDbQueryColumnInfo *ci = realFieldsExpanded->at(i); if (!columnsAlreadyFound.contains(ci->aliasOrName())) { columnsAlreadyFound.insert(ci->aliasOrName()); result[uniqueListCount++] = ci; } } result.resize(uniqueListCount); //update result size return result; } KDbQueryColumnInfo::Vector KDbQuerySchema::internalFields(KDbConnection *conn) const { KDbQuerySchemaFieldsExpanded *cache = computeFieldsExpanded(conn); return cache->internalFields; } KDbQueryColumnInfo* KDbQuerySchema::expandedOrInternalField(KDbConnection *conn, int index) const { return fieldsExpanded(conn, FieldsExpandedMode::WithInternalFields).value(index); } inline static QString lookupColumnKey(KDbField *foreignField, KDbField* field) { QString res; if (field->table()) // can be 0 for anonymous fields built as joined multiple visible columns res = field->table()->name() + QLatin1Char('.'); return res + field->name() + QLatin1Char('_') + foreignField->table()->name() + QLatin1Char('.') + foreignField->name(); } KDbQuerySchemaFieldsExpanded *KDbQuerySchema::computeFieldsExpanded(KDbConnection *conn) const { KDbQuerySchemaFieldsExpanded *cache = conn->d->fieldsExpanded(this); if (cache) { return cache; } cache = new KDbQuerySchemaFieldsExpanded; QScopedPointer guard(cache); //collect all fields in a list (not a vector yet, because we do not know its size) KDbQueryColumnInfo::List list; //temporary KDbQueryColumnInfo::List lookup_list; //temporary, for collecting additional fields related to lookup fields QHash columnInfosOutsideAsterisks; //helper for filling d->columnInfosByName int i = 0; int numberOfColumnsWithMultipleVisibleFields = 0; //used to find an unique name for anonymous field int fieldPosition = -1; for (KDbField *f : *fields()) { fieldPosition++; if (f->isQueryAsterisk()) { if (static_cast(f)->isSingleTableAsterisk()) { const KDbField::List *ast_fields = static_cast(f)->table()->fields(); foreach(KDbField *ast_f, *ast_fields) { KDbQueryColumnInfo *ci = new KDbQueryColumnInfo(ast_f, QString()/*no field for asterisk!*/, isColumnVisible(fieldPosition)); list.append(ci); kdbDebug() << "caching (unexpanded) columns order:" << *ci << "at position" << fieldPosition; cache->columnsOrder.insert(ci, fieldPosition); } } else {//all-tables asterisk: iterate through table list foreach(KDbTableSchema *table, d->tables) { //add all fields from this table const KDbField::List *tab_fields = table->fields(); foreach(KDbField *tab_f, *tab_fields) { //! @todo (js): perhaps not all fields should be appended here // d->detailedVisibility += isFieldVisible(fieldPosition); // list.append(tab_f); KDbQueryColumnInfo *ci = new KDbQueryColumnInfo(tab_f, QString()/*no field for asterisk!*/, isColumnVisible(fieldPosition)); list.append(ci); kdbDebug() << "caching (unexpanded) columns order:" << *ci << "at position" << fieldPosition; cache->columnsOrder.insert(ci, fieldPosition); } } } } else { //a single field KDbQueryColumnInfo *ci = new KDbQueryColumnInfo(f, columnAlias(fieldPosition), isColumnVisible(fieldPosition)); list.append(ci); columnInfosOutsideAsterisks.insert(ci, true); kdbDebug() << "caching (unexpanded) column's order:" << *ci << "at position" << fieldPosition; cache->columnsOrder.insert(ci, fieldPosition); cache->columnsOrderWithoutAsterisks.insert(ci, fieldPosition); //handle lookup field schema KDbLookupFieldSchema *lookupFieldSchema = f->table() ? f->table()->lookupFieldSchema(*f) : nullptr; if (!lookupFieldSchema || lookupFieldSchema->boundColumn() < 0) continue; // Lookup field schema found: // Now we also need to fetch "visible" value from the lookup table, not only the value of binding. // -> build LEFT OUTER JOIN clause for this purpose (LEFT, not INNER because the binding can be broken) // "LEFT OUTER JOIN lookupTable ON thisTable.thisField=lookupTable.boundField" KDbLookupFieldSchemaRecordSource recordSource = lookupFieldSchema->recordSource(); if (recordSource.type() == KDbLookupFieldSchemaRecordSource::Type::Table) { KDbTableSchema *lookupTable = conn->tableSchema(recordSource.name()); KDbFieldList* visibleColumns = nullptr; KDbField *boundField = nullptr; if (lookupTable && lookupFieldSchema->boundColumn() < lookupTable->fieldCount() && (visibleColumns = lookupTable->subList(lookupFieldSchema->visibleColumns())) && (boundField = lookupTable->field(lookupFieldSchema->boundColumn()))) { KDbField *visibleColumn = nullptr; // for single visible column, just add it as-is if (visibleColumns->fieldCount() == 1) { visibleColumn = visibleColumns->fields()->first(); } else { // for multiple visible columns, build an expression column // (the expression object will be owned by column info) visibleColumn = new KDbField(); visibleColumn->setName( QString::fromLatin1("[multiple_visible_fields_%1]") .arg(++numberOfColumnsWithMultipleVisibleFields)); visibleColumn->setExpression( KDbConstExpression(KDbToken::CHARACTER_STRING_LITERAL, QVariant()/*not important*/)); - cache->ownedVisibleColumns.append(visibleColumn); // remember to delete later + cache->ownedVisibleFields.append(visibleColumn); // remember to delete later } lookup_list.append( new KDbQueryColumnInfo(visibleColumn, QString(), true/*visible*/, ci/*foreign*/)); /* //add visibleField to the list of SELECTed fields if it is not yes present there if (!findTableField( visibleField->table()->name()+"."+visibleField->name() )) { if (!table( visibleField->table()->name() )) { } if (!sql.isEmpty()) sql += QString::fromLatin1(", "); sql += (escapeIdentifier(visibleField->table()->name(), drvEscaping) + "." + escapeIdentifier(visibleField->name(), drvEscaping)); }*/ } delete visibleColumns; } else if (recordSource.type() == KDbLookupFieldSchemaRecordSource::Type::Query) { KDbQuerySchema *lookupQuery = conn->querySchema(recordSource.name()); if (!lookupQuery) continue; const KDbQueryColumnInfo::Vector lookupQueryFieldsExpanded( lookupQuery->fieldsExpanded(conn)); if (lookupFieldSchema->boundColumn() >= lookupQueryFieldsExpanded.count()) continue; KDbQueryColumnInfo *boundColumnInfo = nullptr; if (!(boundColumnInfo = lookupQueryFieldsExpanded.value(lookupFieldSchema->boundColumn()))) continue; KDbField *boundField = boundColumnInfo->field(); if (!boundField) continue; const QList visibleColumns(lookupFieldSchema->visibleColumns()); bool ok = true; // all indices in visibleColumns should be in [0..lookupQueryFieldsExpanded.size()-1] foreach(int visibleColumn, visibleColumns) { if (visibleColumn >= lookupQueryFieldsExpanded.count()) { ok = false; break; } } if (!ok) continue; KDbField *visibleColumn = nullptr; // for single visible column, just add it as-is if (visibleColumns.count() == 1) { visibleColumn = lookupQueryFieldsExpanded.value(visibleColumns.first())->field(); } else { // for multiple visible columns, build an expression column // (the expression object will be owned by column info) visibleColumn = new KDbField(); visibleColumn->setName( QString::fromLatin1("[multiple_visible_fields_%1]") .arg(++numberOfColumnsWithMultipleVisibleFields)); visibleColumn->setExpression( KDbConstExpression(KDbToken::CHARACTER_STRING_LITERAL, QVariant()/*not important*/)); - cache->ownedVisibleColumns.append(visibleColumn); // remember to delete later + cache->ownedVisibleFields.append(visibleColumn); // remember to delete later } lookup_list.append( new KDbQueryColumnInfo(visibleColumn, QString(), true/*visible*/, ci/*foreign*/)); /* //add visibleField to the list of SELECTed fields if it is not yes present there if (!findTableField( visibleField->table()->name()+"."+visibleField->name() )) { if (!table( visibleField->table()->name() )) { } if (!sql.isEmpty()) sql += QString::fromLatin1(", "); sql += (escapeIdentifier(visibleField->table()->name(), drvEscaping) + "." + escapeIdentifier(visibleField->name(), drvEscaping)); }*/ } } } //prepare clean vector for expanded list, and a map for order information cache->fieldsExpanded.resize(list.count()); cache->visibleFieldsExpanded.resize(list.count()); /*fill (based on prepared 'list' and 'lookup_list'): -the vector -the map -"fields by name" dictionary */ i = -1; int visibleIndex = -1; foreach(KDbQueryColumnInfo* ci, list) { i++; cache->fieldsExpanded[i] = ci; if (ci->isVisible()) { ++visibleIndex; cache->visibleFieldsExpanded[visibleIndex] = ci; } cache->columnsOrderExpanded.insert(ci, i); //remember field by name/alias/table.name if there's no such string yet in d->columnInfosByNameExpanded if (!ci->alias().isEmpty()) { //store alias and table.alias if (!cache->columnInfosByNameExpanded.contains(ci->alias())) { cache->columnInfosByNameExpanded.insert(ci->alias(), ci); } QString tableAndAlias(ci->alias()); if (ci->field()->table()) tableAndAlias.prepend(ci->field()->table()->name() + QLatin1Char('.')); if (!cache->columnInfosByNameExpanded.contains(tableAndAlias)) { cache->columnInfosByNameExpanded.insert(tableAndAlias, ci); } //the same for "unexpanded" list if (columnInfosOutsideAsterisks.contains(ci)) { if (!cache->columnInfosByName.contains(ci->alias())) { cache->columnInfosByName.insert(ci->alias(), ci); } if (!cache->columnInfosByName.contains(tableAndAlias)) { cache->columnInfosByName.insert(tableAndAlias, ci); } } } else { //no alias: store name and table.name if (!cache->columnInfosByNameExpanded.contains(ci->field()->name())) { cache->columnInfosByNameExpanded.insert(ci->field()->name(), ci); } QString tableAndName(ci->field()->name()); if (ci->field()->table()) tableAndName.prepend(ci->field()->table()->name() + QLatin1Char('.')); if (!cache->columnInfosByNameExpanded.contains(tableAndName)) { cache->columnInfosByNameExpanded.insert(tableAndName, ci); } //the same for "unexpanded" list if (columnInfosOutsideAsterisks.contains(ci)) { if (!cache->columnInfosByName.contains(ci->field()->name())) { cache->columnInfosByName.insert(ci->field()->name(), ci); } if (!cache->columnInfosByName.contains(tableAndName)) { cache->columnInfosByName.insert(tableAndName, ci); } } } } cache->visibleFieldsExpanded.resize(visibleIndex + 1); //remove duplicates for lookup fields QHash lookup_dict; //used to fight duplicates and to update KDbQueryColumnInfo::indexForVisibleLookupValue() // (a mapping from table.name string to int* lookupFieldIndex i = 0; for (QMutableListIterator it(lookup_list); it.hasNext();) { KDbQueryColumnInfo* ci = it.next(); const QString key(lookupColumnKey(ci->foreignColumn()->field(), ci->field())); if (lookup_dict.contains(key)) { // this table.field is already fetched by this query it.remove(); delete ci; } else { lookup_dict.insert(key, i); i++; } } //create internal expanded list with lookup fields cache->internalFields.resize(lookup_list.count()); i = -1; foreach(KDbQueryColumnInfo *ci, lookup_list) { i++; //add it to the internal list cache->internalFields[i] = ci; cache->columnsOrderExpanded.insert(ci, list.count() + i); } //update KDbQueryColumnInfo::indexForVisibleLookupValue() cache for columns numberOfColumnsWithMultipleVisibleFields = 0; for (i = 0; i < cache->fieldsExpanded.size(); i++) { KDbQueryColumnInfo* ci = cache->fieldsExpanded[i]; //! @todo KDbQuerySchema itself will also support lookup fields... KDbLookupFieldSchema *lookupFieldSchema = ci->field()->table() ? ci->field()->table()->lookupFieldSchema(*ci->field()) : nullptr; if (!lookupFieldSchema || lookupFieldSchema->boundColumn() < 0) continue; const KDbLookupFieldSchemaRecordSource recordSource = lookupFieldSchema->recordSource(); if (recordSource.type() == KDbLookupFieldSchemaRecordSource::Type::Table) { KDbTableSchema *lookupTable = conn->tableSchema(recordSource.name()); KDbFieldList* visibleColumns = nullptr; if (lookupTable && lookupFieldSchema->boundColumn() < lookupTable->fieldCount() && (visibleColumns = lookupTable->subList(lookupFieldSchema->visibleColumns()))) { // for single visible column, just add it as-is if (visibleColumns->fieldCount() == 1) { KDbField *visibleColumn = visibleColumns->fields()->first(); const QString key(lookupColumnKey(ci->field(), visibleColumn)); int index = lookup_dict.value(key, -99); if (index != -99) ci->setIndexForVisibleLookupValue(cache->fieldsExpanded.size() + index); } else { const QString key(QString::fromLatin1("[multiple_visible_fields_%1]_%2.%3") .arg(++numberOfColumnsWithMultipleVisibleFields) .arg(ci->field()->table()->name(), ci->field()->name())); int index = lookup_dict.value(key, -99); if (index != -99) ci->setIndexForVisibleLookupValue(cache->fieldsExpanded.size() + index); } } delete visibleColumns; } else if (recordSource.type() == KDbLookupFieldSchemaRecordSource::Type::Query) { KDbQuerySchema *lookupQuery = conn->querySchema(recordSource.name()); if (!lookupQuery) continue; const KDbQueryColumnInfo::Vector lookupQueryFieldsExpanded( lookupQuery->fieldsExpanded(conn)); if (lookupFieldSchema->boundColumn() >= lookupQueryFieldsExpanded.count()) continue; KDbQueryColumnInfo *boundColumnInfo = nullptr; if (!(boundColumnInfo = lookupQueryFieldsExpanded.value(lookupFieldSchema->boundColumn()))) continue; KDbField *boundField = boundColumnInfo->field(); if (!boundField) continue; const QList visibleColumns(lookupFieldSchema->visibleColumns()); // for single visible column, just add it as-is if (visibleColumns.count() == 1) { if (lookupQueryFieldsExpanded.count() > visibleColumns.first()) { // sanity check KDbField *visibleColumn = lookupQueryFieldsExpanded.at(visibleColumns.first())->field(); const QString key(lookupColumnKey(ci->field(), visibleColumn)); int index = lookup_dict.value(key, -99); if (index != -99) ci->setIndexForVisibleLookupValue(cache->fieldsExpanded.size() + index); } } else { const QString key(QString::fromLatin1("[multiple_visible_fields_%1]_%2.%3") .arg(++numberOfColumnsWithMultipleVisibleFields) .arg(ci->field()->table()->name(), ci->field()->name())); int index = lookup_dict.value(key, -99); if (index != -99) ci->setIndexForVisibleLookupValue(cache->fieldsExpanded.size() + index); } } else { kdbWarning() << "unsupported record source type" << recordSource.typeName(); } } if (d->recentConnection != conn) { if (d->recentConnection) { // connection changed: remove old cache - d->recentConnection->d->insertFieldsExpanded(this, nullptr); + d->recentConnection->d->removeFieldsExpanded(this); } d->recentConnection = conn; } conn->d->insertFieldsExpanded(this, guard.take()); return cache; } QHash KDbQuerySchema::columnsOrder(KDbConnection *conn, ColumnsOrderMode mode) const { KDbQuerySchemaFieldsExpanded *cache = computeFieldsExpanded(conn); if (mode == ColumnsOrderMode::UnexpandedList) { return cache->columnsOrder; } else if (mode == ColumnsOrderMode::UnexpandedListWithoutAsterisks) { return cache->columnsOrderWithoutAsterisks; } return cache->columnsOrderExpanded; } QVector KDbQuerySchema::pkeyFieldsOrder(KDbConnection *conn) const { if (d->pkeyFieldsOrder) return *d->pkeyFieldsOrder; KDbTableSchema *tbl = masterTable(); if (!tbl || !tbl->primaryKey()) return QVector(); //get order of PKEY fields (e.g. for records updating or inserting ) KDbIndexSchema *pkey = tbl->primaryKey(); kdbDebug() << *pkey; d->pkeyFieldsOrder = new QVector(pkey->fieldCount(), -1); d->pkeyFieldCount = 0; const KDbQueryColumnInfo::Vector fieldsExpanded(this->fieldsExpanded(conn)); const int fCount = fieldsExpanded.count(); for (int i = 0; i < fCount; i++) { const KDbQueryColumnInfo *fi = fieldsExpanded[i]; const int fieldIndex = fi->field()->table() == tbl ? pkey->indexOf(*fi->field()) : -1; if (fieldIndex != -1/* field found in PK */ && d->pkeyFieldsOrder->at(fieldIndex) == -1 /* first time */) { kdbDebug() << "FIELD" << fi->field()->name() << "IS IN PKEY AT POSITION #" << fieldIndex; (*d->pkeyFieldsOrder)[fieldIndex] = i; d->pkeyFieldCount++; } } kdbDebug() << d->pkeyFieldCount << " OUT OF " << pkey->fieldCount() << " PKEY'S FIELDS FOUND IN QUERY " << name(); return *d->pkeyFieldsOrder; } int KDbQuerySchema::pkeyFieldCount(KDbConnection *conn) { (void)pkeyFieldsOrder(conn); /* rebuild information */ return d->pkeyFieldCount; } KDbRelationship* KDbQuerySchema::addRelationship(KDbField *field1, KDbField *field2) { //@todo: find existing global db relationships KDbRelationship *r = new KDbRelationship(this, field1, field2); if (r->isEmpty()) { delete r; return nullptr; } d->relations.append(r); return r; } KDbQueryColumnInfo::List* KDbQuerySchema::autoIncrementFields(KDbConnection *conn) const { if (!d->autoincFields) { d->autoincFields = new KDbQueryColumnInfo::List(); } KDbTableSchema *mt = masterTable(); if (!mt) { kdbWarning() << "no master table!"; return d->autoincFields; } if (d->autoincFields->isEmpty()) {//no cache const KDbQueryColumnInfo::Vector fieldsExpanded(this->fieldsExpanded(conn)); for (int i = 0; i < fieldsExpanded.count(); i++) { KDbQueryColumnInfo *ci = fieldsExpanded[i]; if (ci->field()->table() == mt && ci->field()->isAutoIncrement()) { d->autoincFields->append(ci); } } } return d->autoincFields; } // static KDbEscapedString KDbQuerySchema::sqlColumnsList(const KDbQueryColumnInfo::List &infolist, KDbConnection *conn, KDb::IdentifierEscapingType escapingType) { KDbEscapedString result; result.reserve(256); bool start = true; foreach(KDbQueryColumnInfo* ci, infolist) { if (!start) result += ","; else start = false; result += escapeIdentifier(ci->field()->name(), conn, escapingType); } return result; } KDbEscapedString KDbQuerySchema::autoIncrementSqlFieldsList(KDbConnection *conn) const { // QWeakPointer driverWeakPointer // = DriverManagerInternal::self()->driverWeakPointer(*conn->driver()); if ( /*d->lastUsedDriverForAutoIncrementSQLFieldsList != driverWeakPointer ||*/ d->autoIncrementSqlFieldsList.isEmpty()) { d->autoIncrementSqlFieldsList = KDbQuerySchema::sqlColumnsList(*autoIncrementFields(conn), conn); //d->lastUsedDriverForAutoIncrementSQLFieldsList = driverWeakPointer; } return d->autoIncrementSqlFieldsList; } static void setResult(const KDbParseInfoInternal &parseInfo, QString *errorMessage, QString *errorDescription) { if (errorMessage) { *errorMessage = parseInfo.errorMessage(); } if (errorDescription) { *errorDescription = parseInfo.errorDescription(); } } bool KDbQuerySchema::setWhereExpression(const KDbExpression &expr, QString *errorMessage, QString *errorDescription) { KDbExpression newWhereExpr = expr.clone(); KDbParseInfoInternal parseInfo(this); QString tempErrorMessage; QString tempErrorDescription; QString *errorMessagePointer = errorMessage ? errorMessage : &tempErrorMessage; QString *errorDescriptionPointer = errorDescription ? errorDescription : &tempErrorDescription; if (!newWhereExpr.validate(&parseInfo)) { setResult(parseInfo, errorMessagePointer, errorDescription); kdbWarning() << "message=" << *errorMessagePointer << "description=" << *errorDescriptionPointer; kdbWarning() << newWhereExpr; d->whereExpr = KDbExpression(); return false; } errorMessagePointer->clear(); errorDescriptionPointer->clear(); KDbQuerySchemaPrivate::setWhereExpressionInternal(this, newWhereExpr); return true; } bool KDbQuerySchema::addToWhereExpression(KDbField *field, const QVariant &value, KDbToken relation, QString *errorMessage, QString *errorDescription) { KDbToken token; if (value.isNull()) { token = KDbToken::SQL_NULL; } else { const KDbField::Type type = field->type(); // cache: evaluating type of expressions can be expensive if (KDbField::isIntegerType(type)) { token = KDbToken::INTEGER_CONST; } else if (KDbField::isFPNumericType(type)) { token = KDbToken::REAL_CONST; } else { token = KDbToken::CHARACTER_STRING_LITERAL; } //! @todo date, time } KDbBinaryExpression newExpr( KDbConstExpression(token, value), relation, KDbVariableExpression((field->table() ? (field->table()->name() + QLatin1Char('.')) : QString()) + field->name()) ); const KDbExpression origWhereExpr = d->whereExpr; if (!d->whereExpr.isNull()) { newExpr = KDbBinaryExpression( d->whereExpr, KDbToken::AND, newExpr ); } const bool result = setWhereExpression(newExpr, errorMessage, errorDescription); if (!result) { // revert, setWhereExpression() cleared it d->whereExpr = origWhereExpr; } return result; } /* void KDbQuerySchema::addToWhereExpression(KDbField *field, const QVariant& value) switch (value.type()) { case Int: case UInt: case Bool: case LongLong: case ULongLong: token = INTEGER_CONST; break; case Double: token = REAL_CONST; break; default: token = CHARACTER_STRING_LITERAL; } //! @todo date, time */ KDbExpression KDbQuerySchema::whereExpression() const { return d->whereExpr; } void KDbQuerySchema::setOrderByColumnList(const KDbOrderByColumnList& list) { delete d->orderByColumnList; d->orderByColumnList = new KDbOrderByColumnList(list, nullptr, nullptr, nullptr); // all field names should be found, exit otherwise ..........? } KDbOrderByColumnList* KDbQuerySchema::orderByColumnList() { return d->orderByColumnList; } const KDbOrderByColumnList* KDbQuerySchema::orderByColumnList() const { return d->orderByColumnList; } QList KDbQuerySchema::parameters(KDbConnection *conn) const { QList params; const KDbQueryColumnInfo::Vector fieldsExpanded(this->fieldsExpanded(conn)); for (int i = 0; i < fieldsExpanded.count(); ++i) { KDbQueryColumnInfo *ci = fieldsExpanded[i]; if (!ci->field()->expression().isNull()) { ci->field()->expression().getQueryParameters(¶ms); } } KDbExpression where = whereExpression(); if (!where.isNull()) { where.getQueryParameters(¶ms); } return params; } bool KDbQuerySchema::validate(QString *errorMessage, QString *errorDescription) { KDbParseInfoInternal parseInfo(this); foreach(KDbField* f, *fields()) { if (f->isExpression()) { if (!f->expression().validate(&parseInfo)) { setResult(parseInfo, errorMessage, errorDescription); return false; } } } if (!whereExpression().validate(&parseInfo)) { setResult(parseInfo, errorMessage, errorDescription); return false; } return true; } diff --git a/src/KDbQuerySchema_p.cpp b/src/KDbQuerySchema_p.cpp index 22a78c86..c26fbcbb 100644 --- a/src/KDbQuerySchema_p.cpp +++ b/src/KDbQuerySchema_p.cpp @@ -1,159 +1,159 @@ /* This file is part of the KDE project Copyright (C) 2003-2016 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 "KDbQuerySchema_p.h" #include "KDbConnection.h" #include "KDbConnection_p.h" #include "KDbOrderByColumn.h" #include "kdb_debug.h" KDbQuerySchemaPrivate::KDbQuerySchemaPrivate(KDbQuerySchema* q, KDbQuerySchemaPrivate* copy) : query(q) , masterTable(nullptr) , fakeRecordIdField(nullptr) , fakeRecordIdCol(nullptr) , maxIndexWithAlias(-1) , visibility(64) , orderByColumnList(nullptr) , autoincFields(nullptr) , pkeyFieldsOrder(nullptr) , pkeyFieldCount(0) , tablesBoundToColumns(64, -1) // will be resized if needed , regenerateExprAliases(false) { visibility.fill(false); if (copy) { // deep copy *this = *copy; // orderByColumnList = nullptr; autoincFields = nullptr; autoIncrementSqlFieldsList.clear(); pkeyFieldsOrder = nullptr; fakeRecordIdCol = nullptr; fakeRecordIdField = nullptr; // if (!copy->whereExpr.isNull()) { whereExpr = copy->whereExpr.clone(); } // "*this = *copy" causes copying pointers; pull of them without destroying, // will be deep-copied in the KDbQuerySchema ctor. asterisks.setAutoDelete(false); asterisks.clear(); asterisks.setAutoDelete(true); } else { orderByColumnList = new KDbOrderByColumnList; } } KDbQuerySchemaPrivate::~KDbQuerySchemaPrivate() { if (recentConnection) { - recentConnection->d->insertFieldsExpanded(query, nullptr); + recentConnection->d->removeFieldsExpanded(query); } delete orderByColumnList; delete autoincFields; delete pkeyFieldsOrder; delete fakeRecordIdCol; delete fakeRecordIdField; } void KDbQuerySchemaPrivate::clear() { columnAliases.clear(); tableAliases.clear(); asterisks.clear(); relations.clear(); masterTable = nullptr; tables.clear(); clearCachedData(); delete pkeyFieldsOrder; pkeyFieldsOrder = nullptr; visibility.fill(false); tablesBoundToColumns = QVector(64, -1); // will be resized if needed tablePositionsForAliases.clear(); columnPositionsForAliases.clear(); } void KDbQuerySchemaPrivate::clearCachedData() { if (orderByColumnList) { orderByColumnList->clear(); } if (recentConnection) { - recentConnection->d->insertFieldsExpanded(query, nullptr); + recentConnection->d->removeFieldsExpanded(query); } delete autoincFields; autoincFields = nullptr; autoIncrementSqlFieldsList.clear(); } bool KDbQuerySchemaPrivate::setColumnAlias(int position, const QString& alias) { if (alias.isEmpty()) { columnAliases.remove(position); maxIndexWithAlias = -1; return true; } return setColumnAliasInternal(position, alias); } void KDbQuerySchemaPrivate::tryRegenerateExprAliases() { if (!regenerateExprAliases) return; //regenerate missing aliases for experessions int colNum = 0; //used to generate a name QString columnAlias; int p = -1; foreach(KDbField* f, *query->fields()) { p++; if (f->isExpression() && columnAliases.value(p).isEmpty()) { //missing do { //find 1st unused colNum++; columnAlias = tr("expr%1", "short for 'expression' word, it will expand " "to 'expr1', 'expr2', etc. Please use ONLY latin " "letters and DON'T use '.'").arg(colNum); columnAlias = KDb::stringToIdentifier(columnAlias); // sanity fix, translators make mistakes! } while (-1 != tablePositionForAlias(columnAlias)); (void)setColumnAliasInternal(p, columnAlias); } } regenerateExprAliases = false; } bool KDbQuerySchemaPrivate::setColumnAliasInternal(int position, const QString& alias) { const int currentPos = columnPositionsForAliases.value(alias.toLower(), -1); if (currentPos == position) { return true; // already set } if (currentPos == -1) { columnAliases.insert(position, alias.toLower()); columnPositionsForAliases.insert(alias.toLower(), position); maxIndexWithAlias = qMax(maxIndexWithAlias, position); return true; } kdbWarning() << "Alias" << alias << "for already set for column" << currentPos << ", cannot set to a new column. Remove old alias first."; return false; } diff --git a/src/KDbQuerySchema_p.h b/src/KDbQuerySchema_p.h index 2556a8f0..7755879b 100644 --- a/src/KDbQuerySchema_p.h +++ b/src/KDbQuerySchema_p.h @@ -1,244 +1,247 @@ /* This file is part of the KDE project Copyright (C) 2003-2016 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. */ #ifndef KDB_QUERYSCHEMA_P_H #define KDB_QUERYSCHEMA_P_H #include "KDbDriver.h" #include "KDbExpression.h" #include "KDbQuerySchema.h" #include #include class KDbConnection; //! @internal class KDbQuerySchemaPrivate { Q_DECLARE_TR_FUNCTIONS(KDbQuerySchema) public: explicit KDbQuerySchemaPrivate(KDbQuerySchema* q, KDbQuerySchemaPrivate* copy = nullptr); ~KDbQuerySchemaPrivate(); //! @return a new query that's associated with @a conn. Used internally, e.g. by the parser. //! Uses an internal KDbQuerySchema(KDbConnection*) ctor. static KDbQuerySchema* createQuery(KDbConnection *conn); void clear(); void clearCachedData(); bool setColumnAlias(int position, const QString& alias); inline bool setTableAlias(int position, const QString& alias) { if (tablePositionsForAliases.contains(alias.toLower())) { return false; } tableAliases.insert(position, alias.toLower()); tablePositionsForAliases.insert(alias.toLower(), position); return true; } inline int columnAliasesCount() { tryRegenerateExprAliases(); return columnAliases.count(); } inline QString columnAlias(int position) { tryRegenerateExprAliases(); return columnAliases.value(position); } inline bool hasColumnAlias(int position) { tryRegenerateExprAliases(); return columnAliases.contains(position); } inline void removeTablePositionForAlias(const QString& alias) { tablePositionsForAliases.remove(alias.toLower()); } inline int tablePositionForAlias(const QString& alias) const { return tablePositionsForAliases.value(alias.toLower(), -1); } inline int columnPositionForAlias(const QString& alias) const { return columnPositionsForAliases.value(alias.toLower(), -1); } //! Accessor for buildSelectQuery() static void setWhereExpressionInternal(KDbQuerySchema *query, const KDbExpression &expr) { query->d->whereExpr = expr; } KDbQuerySchema *query; /*! Master table of the query. Can be @c nullptr. Any data modifications can be performed if we know master table. If null, query's records cannot be modified. */ KDbTableSchema *masterTable; /*! List of tables used in this query */ QList tables; KDbField *fakeRecordIdField; //! used to mark a place for record Id KDbQueryColumnInfo *fakeRecordIdCol; //! used to mark a place for record Id protected: void tryRegenerateExprAliases(); bool setColumnAliasInternal(int position, const QString& alias); /*! Used to mapping columns to its aliases for this query */ QHash columnAliases; /*! Collects table positions for aliases: used in tablePositionForAlias(). */ QHash tablePositionsForAliases; /*! Collects column positions for aliases: used in columnPositionForAlias(). */ QHash columnPositionsForAliases; public: /*! Used to mapping tables to its aliases for this query */ QHash tableAliases; /*! Helper used with aliases */ int maxIndexWithAlias; /*! Used to store visibility flag for every field */ QBitArray visibility; /*! List of asterisks defined for this query */ KDbField::List asterisks; /*! A list of fields for ORDER BY section. @see KDbQuerySchema::orderByColumnList(). */ KDbOrderByColumnList* orderByColumnList; /*! A cache for autoIncrementFields(). */ KDbQueryColumnInfo::List *autoincFields; /*! A cache for autoIncrementSqlFieldsList(). */ KDbEscapedString autoIncrementSqlFieldsList; QWeakPointer lastUsedDriverForAutoIncrementSQLFieldsList; /*! order of PKEY fields (e.g. for updateRecord() ) */ QVector *pkeyFieldsOrder; /*! number of PKEY fields within the query */ int pkeyFieldCount; /*! Forced (predefined) raw SQL statement */ KDbEscapedString sql; /*! Relationships defined for this query. */ QList relations; /*! Information about columns bound to tables. Used if table is used in FROM section more than once (using table aliases). This list is updated by insertField(int position, KDbField *field, int bindToTable, bool visible), using bindToTable parameter. Example: for this statement: SELECT t1.a, othertable.x, t2.b FROM table t1, table t2, othertable; tablesBoundToColumns list looks like this: [ 0, -1, 1 ] - first column is bound to table 0 "t1" - second coulmn is not specially bound (othertable.x isn't ambiguous) - third column is bound to table 1 "t2" */ QVector tablesBoundToColumns; /*! WHERE expression */ KDbExpression whereExpr; /*! Set by insertField(): true, if aliases for expression columns should be generated on next columnAlias() call. */ bool regenerateExprAliases; //! Points to connection recently used for caching //! @todo use equivalent of QPointer KDbConnection *recentConnection = nullptr; + + //! Owned fields created by KDbQuerySchema::addExpressionInternal() + KDbField::List ownedExpressionFields; }; //! Information about expanded fields for a single query schema, used for caching class KDbQuerySchemaFieldsExpanded { public: inline KDbQuerySchemaFieldsExpanded() { } inline ~KDbQuerySchemaFieldsExpanded() { qDeleteAll(fieldsExpanded); qDeleteAll(internalFields); } /*! Temporary field vector for using in fieldsExpanded() */ KDbQueryColumnInfo::Vector fieldsExpanded; /*! Like fieldsExpanded but only visible column infos; infos are not owned. */ KDbQueryColumnInfo::Vector visibleFieldsExpanded; /*! Temporary field vector containing internal fields used for lookup columns. */ KDbQueryColumnInfo::Vector internalFields; /*! Temporary, used to cache sum of expanded fields and internal fields (+record Id) used for lookup columns. Contains not auto-deleted items.*/ KDbQueryColumnInfo::Vector fieldsExpandedWithInternalAndRecordId; /*! Like fieldsExpandedWithInternalAndRecordId but only contains visible column infos; infos are not owned.*/ KDbQueryColumnInfo::Vector visibleFieldsExpandedWithInternalAndRecordId; /*! Temporary, used to cache sum of expanded fields and internal fields used for lookup columns. Contains not auto-deleted items.*/ KDbQueryColumnInfo::Vector fieldsExpandedWithInternal; /*! Like fieldsExpandedWithInternal but only contains visible column infos; infos are not owned.*/ KDbQueryColumnInfo::Vector visibleFieldsExpandedWithInternal; /*! A hash for fast lookup of query columns' order (unexpanded version). */ QHash columnsOrder; /*! A hash for fast lookup of query columns' order (unexpanded version without asterisks). */ QHash columnsOrderWithoutAsterisks; /*! A hash for fast lookup of query columns' order. This is exactly opposite information compared to vector returned by fieldsExpanded() */ QHash columnsOrderExpanded; QHash columnInfosByNameExpanded; QHash columnInfosByName; //!< Same as columnInfosByNameExpanded but asterisks are skipped - //! field schemas created for multiple joined columns like a||' '||b||' '||c - KDbField::List ownedVisibleColumns; + //! Fields created for multiple joined columns like a||' '||b||' '||c + KDbField::List ownedVisibleFields; }; //! @return identifier string @a name escaped using @a conn connection and type @a escapingType QString escapeIdentifier(const QString& name, KDbConnection *conn, KDb::IdentifierEscapingType escapingType); #endif diff --git a/src/parser/KDbParser.cpp b/src/parser/KDbParser.cpp index e9f9b50e..f4ec5d82 100644 --- a/src/parser/KDbParser.cpp +++ b/src/parser/KDbParser.cpp @@ -1,231 +1,226 @@ /* This file is part of the KDE project Copyright (C) 2003 Lucijan Busch Copyright (C) 2004-2017 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 "KDbParser.h" #include "KDbParser_p.h" #include "generated/sqlparser.h" #include "KDbConnection.h" #include "KDbTableSchema.h" #include bool parseData(); //! Cache class ParserStatic { public: ParserStatic() : statementTypeStrings({ QLatin1String("None"), QLatin1String("Select"), QLatin1String("CreateTable"), QLatin1String("AlterTable"), QLatin1String("Insert"), QLatin1String("Update"), QLatin1String("Delete")}) { } const std::vector statementTypeStrings; private: Q_DISABLE_COPY(ParserStatic) }; Q_GLOBAL_STATIC(ParserStatic, KDb_parserStatic) KDbParser::KDbParser(KDbConnection *connection) : d(new KDbParserPrivate) { d->connection = connection; } KDbParser::~KDbParser() { delete d; } KDbParser::StatementType KDbParser::statementType() const { return d->statementType; } QString KDbParser::statementTypeString() const { Q_ASSERT(size_t(d->statementType) < sizeof(KDb_parserStatic->statementTypeStrings)); return KDb_parserStatic->statementTypeStrings[d->statementType]; } KDbTableSchema *KDbParser::table() { KDbTableSchema *t = d->table; d->table = nullptr; return t; } -const KDbTableSchema *KDbParser::table() const -{ - return const_cast(this)->table(); -} - KDbQuerySchema *KDbParser::query() { KDbQuerySchema *s = d->query; d->query = nullptr; return s; } KDbConnection *KDbParser::connection() { return d->connection; } const KDbConnection *KDbParser::connection() const { return d->connection; } KDbParserError KDbParser::error() const { return d->error; } KDbEscapedString KDbParser::statement() const { return d->sql; } void KDbParser::init() { if (d->initialized) return; // nothing to do d->initialized = true; } bool KDbParser::parse(const KDbEscapedString &sql, KDbQuerySchema *query) { init(); reset(); d->sql = sql; d->query = query; KDbParser *oldParser = globalParser; KDbField *oldField = globalField; globalParser = this; globalField = nullptr; bool res = parseData(); globalParser = oldParser; globalField = oldField; if (query) { // if existing query was supplied to parse() nullptr should be returned by query() d->query = nullptr; } return res; } void KDbParser::reset() { d->reset(); } //------------------------------------- class Q_DECL_HIDDEN KDbParserError::Private { public: Private() {} Private(const Private &other) { copy(other); } #define KDbParserErrorPrivateArgs(o) std::tie(o.type, o.message, o.token, o.position) void copy(const Private &other) { KDbParserErrorPrivateArgs((*this)) = KDbParserErrorPrivateArgs(other); } bool operator==(const Private &other) const { return KDbParserErrorPrivateArgs((*this)) == KDbParserErrorPrivateArgs(other); } QString type; QString message; QByteArray token; int position = -1; }; KDbParserError::KDbParserError() : d(new Private) { } KDbParserError::KDbParserError(const QString &type, const QString &message, const QByteArray &token, int position) : d(new Private) { d->type = type; d->message = message; d->token = token; d->position = position; } KDbParserError::KDbParserError(const KDbParserError &other) : d(new Private(*other.d)) { *d = *other.d; } KDbParserError::~KDbParserError() { delete d; } KDbParserError& KDbParserError::operator=(const KDbParserError &other) { if (this != &other) { d->copy(*other.d); } return *this; } bool KDbParserError::operator==(const KDbParserError &other) const { return *d == *other.d; } QString KDbParserError::type() const { return d->type; } QString KDbParserError::message() const { return d->message; } int KDbParserError::position() const { return d->position; } QDebug operator<<(QDebug dbg, const KDbParserError& error) { if (error.type().isEmpty() && error.message().isEmpty()) { return dbg.space() << "KDb:KDbParserError: None"; } return dbg.space() << "KDb:KDbParserError: type=" << error.type() << "message=" << error.message() << "pos=" << error.position() << ")"; } diff --git a/src/parser/KDbParser.h b/src/parser/KDbParser.h index dba9063e..19f88537 100644 --- a/src/parser/KDbParser.h +++ b/src/parser/KDbParser.h @@ -1,209 +1,205 @@ /* This file is part of the KDE project Copyright (C) 2003 Lucijan Busch Copyright (C) 2004-2017 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. */ #ifndef KDB_PARSER_H #define KDB_PARSER_H #include "kdb_export.h" #include #include class KDbConnection; class KDbQuerySchema; class KDbTableSchema; class KDbEscapedString; /** * Provides detailed error description about KDbParser. * * @todo Make it explicitly shared using SDC * @todo change type to enum */ class KDB_EXPORT KDbParserError { public: /** * Empty constructor. */ KDbParserError(); /** * Constructor. * * @param type The error type. * @param message A description of the error. * @param token Token where the Error happend. * @param position The position where the error happened. */ KDbParserError(const QString &type, const QString &message, const QByteArray &token, int position); /** * Copy constructor. */ KDbParserError(const KDbParserError &other); ~KDbParserError(); KDbParserError& operator=(const KDbParserError &other); bool operator==(const KDbParserError &other) const; inline bool operator!=(const KDbParserError &other) const { return !operator==(other); } /** * @return the error type. */ QString type() const; /** * @return translated error message. */ QString message() const; /** * @return (character) position where the error happened. */ int position() const; private: class Private; Private * const d; }; class KDbParserPrivate; //!< @internal /** * A parser tool for SQL statements. * * The KDbParser class offers functionality of a SQL parser for database-backend-independent * KDbSQL dialect. Schema objects such as KDbQuerySchema that are created after successful parsing * can be then used for running the queries on actual data or used for further modification. * * @todo Add examples * @todo Support more types than the SELECT */ class KDB_EXPORT KDbParser { Q_DECLARE_TR_FUNCTIONS(KDbParser) public: /** * The type of the statement. */ enum StatementType { NoType, //!< No statement type specified or detected Select, //!< Query-statement CreateTable, //!< Create a new table AlterTable, //!< Alter schema of an existing table Insert, //!< Insert new records Update, //!< Update existing records Delete //!< Delete existing records }; /** * Constructs an new parser object. * @a connection is used to obtain context, for example wildcards "T.*" resolution * is possible only if information about table T is available. */ explicit KDbParser(KDbConnection *connection); ~KDbParser(); /** * @brief Clears the parser's status and runs the parsing for a raw SQL statement * * If parsing of @a sql results in a proper query and @a query is present, it will be set to * representation of the parsed query. * @since 3.1 */ bool parse(const KDbEscapedString &sql, KDbQuerySchema *query = nullptr); /** * Reset the parser's status (table, query, error, statement, statement type). */ void reset(); /** * @return the resulting statement type * NoType is returned if parsing failed or it has not been yet performed or reset() was called. */ StatementType statementType() const; /** * @return the resulting statement type as string. It is not translated. */ QString statementTypeString() const; /** * @return a pointer to a query schema if 'CREATE TABLE ...' statement was parsed * or @c nullptr for any other statements or on error. * @note A proper table schema is returned only once for each successful parse() call, * and the object is owned by the caller. In all other cases @c nullptr is returned. * * @todo Implement this */ - KDbTableSchema *table(); - - //! @overload - //! @since 3.1 - const KDbTableSchema *table() const; + KDbTableSchema *table() Q_REQUIRED_RESULT; /** * @return a pointer to a new query schema created by parsing 'SELECT ...' statement * or @c nullptr for any other statements or on error. * If existing query was supplied to parse() @c nullptr is returned. * @note A proper query schema is returned only once for each successful parse() call, * and the object is owned by the caller. In all other cases nullptr is returned. */ - KDbQuerySchema *query(); + KDbQuerySchema *query() Q_REQUIRED_RESULT; /** * @return a pointer to the used database connection or @c nullptr if it was not set. */ KDbConnection *connection(); //! @overload //! @since 3.1 const KDbConnection *connection() const; /** * @return detailed information about last error. * If no error occurred KDbParserError::type() is empty. */ KDbParserError error() const; /** * @return the statement passed on the most recent call of parse(). */ KDbEscapedString statement() const; private: void init(); friend class KDbParserPrivate; KDbParserPrivate * const d; //!< @internal d-pointer class. Q_DISABLE_COPY(KDbParser) }; //! Sends information about parser error @a error to debug output @a dbg. KDB_EXPORT QDebug operator<<(QDebug dbg, const KDbParserError& error); #endif diff --git a/src/tools/KDbUtils.h b/src/tools/KDbUtils.h index 8d6cdd6d..27bd6ab0 100644 --- a/src/tools/KDbUtils.h +++ b/src/tools/KDbUtils.h @@ -1,466 +1,478 @@ /* This file is part of the KDE project Copyright (C) 2003-2016 Jarosław Staniek Portions of kstandarddirs.cpp: Copyright (C) 1999 Sirtaj Singh Kang Copyright (C) 1999,2007 Stephan Kulow Copyright (C) 1999 Waldo Bastian Copyright (C) 2009 David Faure This program 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 program 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 program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KDB_TOOLS_UTILS_H #define KDB_TOOLS_UTILS_H #include "kdb_export.h" #include "config-kdb.h" #include #include #include #include namespace KDbUtils { //! @return true if @a o has parent @a par (checks recursively) KDB_EXPORT bool hasParent(QObject* par, QObject* o); //! @return parent object of @a o that is of type @a type or @c nullptr if no such parent template inline type findParent(QObject* o, const char* className = nullptr) { if (!o) return nullptr; while ((o = o->parent())) { if (::qobject_cast< type >(o) && (!className || o->inherits(className))) return ::qobject_cast< type >(o); } return nullptr; } //! QDateTime - a hack needed because QVariant(QTime) has broken isNull() KDB_EXPORT QDateTime stringToHackedQTime(const QString& s); /*! Serializes @a map to the array pointed by @a array. KDbUtils::deserializeMap() can be used to deserialize this array back to map. Does nothing if @a array is @c nullptr. */ KDB_EXPORT void serializeMap(const QMap& map, QByteArray *array); /*! Serializes @a map to the string pointed by @a string. KDbUtils::deserializeMap() can be used to deserialize this array back to map. Does nothing if @a string is @c nullptr. */ KDB_EXPORT void serializeMap(const QMap& map, QString *string); /*! @return a map deserialized from a byte array @a array. @a array need to contain data previously serialized using KexiUtils::serializeMap(). */ KDB_EXPORT QMap deserializeMap(const QByteArray& array); /*! @return a map deserialized from @a string. @a string need to contain data previously serialized using KexiUtils::serializeMap(). */ KDB_EXPORT QMap deserializeMap(const QString& string); /*! @return a valid filename converted from @a string by: - replacing \\, /, :, *, ?, ", <, >, |, \n \\t characters with a space - simplifing whitespace by removing redundant space characters using QString::simplified() Do not pass full paths here, but only filename strings. */ KDB_EXPORT QString stringToFileName(const QString& string); /*! Performs a simple @a string encryption using rot47-like algorithm. Each character's unicode value is increased by 47 + i (where i is index of the character). The resulting string still contains readable characters but some of them can be non-ASCII. Does nothing if @a string is @c nullptr. @note Do not use this for data that can be accessed by attackers! */ KDB_EXPORT void simpleCrypt(QString *string); /*! Performs a simple @a string decryption using rot47-like algorithm, using opposite operations to KexiUtils::simpleCrypt(). @return true on success and false on failure. Failue means that one or more characters have unicode numbers smaller than value of 47 + i. On failure @a string is not altered. Does nothing and returns @c false if @a string is @c nullptr. */ KDB_EXPORT bool simpleDecrypt(QString *string); //! @internal KDB_EXPORT QString pointerToStringInternal(void* pointer, int size); //! @internal KDB_EXPORT void* stringToPointerInternal(const QString& string, int size); //! @return a pointer @a pointer safely serialized to string template QString pointerToString(type *pointer) { return pointerToStringInternal(pointer, sizeof(type*)); } //! @return a pointer of type @a type safely deserialized from @a string template type* stringToPointer(const QString& string) { return static_cast(stringToPointerInternal(string, sizeof(type*))); } //! @return value converted to text, squeezed to reasonable length, useful for debugging //! If the value is not a byte array or string, or if it's not longer than 1024 characters, //! @a value is returned. //! @since 3.1 KDB_EXPORT QVariant squeezedValue(const QVariant &value); //! @short Autodeleting hash template class AutodeletedHash : public QHash { public: //! Creates autodeleting hash as a copy of @a other. //! Auto-deletion is not enabled as it would cause double deletion for items. //! If you enable auto-deletion on here, make sure you disable it in the @a other hash. AutodeletedHash(const AutodeletedHash& other) : QHash(other), m_autoDelete(false) {} //! Creates empty autodeleting hash. //! Auto-deletion is enabled by default. AutodeletedHash(bool autoDelete = true) : QHash(), m_autoDelete(autoDelete) {} ~AutodeletedHash() { if (m_autoDelete) { qDeleteAll(*this); } } void setAutoDelete(bool set) { m_autoDelete = set; } bool autoDelete() const { return m_autoDelete; } void clear() { if (m_autoDelete) { qDeleteAll(*this); } QHash::clear(); } typename QHash::iterator erase(typename QHash::iterator pos) { typename QHash::iterator it = QHash::erase(pos); if (m_autoDelete) { delete it.value(); it.value() = 0; return it; } } typename QHash::iterator insert(const Key &key, const T &value) { if (m_autoDelete) { T &oldValue = QHash::operator[](key); if (oldValue && oldValue != value) { // only delete if differs delete oldValue; } } return QHash::insert(key, value); } + int remove(const Key &key) { + if (m_autoDelete) { + const QList values(QHash::values(key)); + const int result = QHash::remove(key); + for (T item : values) { + delete item; + } + return result; + } else { + return QHash::remove(key); + } + } // note: no need to override insertMulti(), unite(), take(), they do not replace items private: bool m_autoDelete; }; //! @short Autodeleting list template class AutodeletedList : public QList { public: //! Creates autodeleting list as a copy of @a other. //! Auto-deletion is not enabled as it would cause double deletion for items. //! If you enable auto-deletion on here, make sure you disable it in the @a other list. AutodeletedList(const AutodeletedList& other) : QList(other), m_autoDelete(false) {} //! Creates empty autodeleting list. //! Auto-deletion is enabled by default. AutodeletedList(bool autoDelete = true) : QList(), m_autoDelete(autoDelete) {} ~AutodeletedList() { if (m_autoDelete) qDeleteAll(*this); } void setAutoDelete(bool set) { m_autoDelete = set; } bool autoDelete() const { return m_autoDelete; } void removeAt(int i) { T item = QList::takeAt(i); if (m_autoDelete) delete item; } void removeFirst() { T item = QList::takeFirst(); if (m_autoDelete) delete item; } void removeLast() { T item = QList::takeLast(); if (m_autoDelete) delete item; } void replace(int i, const T& value) { T item = QList::takeAt(i); insert(i, value); if (m_autoDelete) delete item; } void insert(int i, const T& value) { QList::insert(i, value); } typename QList::iterator erase(typename QList::iterator pos) { T item = *pos; typename QList::iterator res = QList::erase(pos); if (m_autoDelete) delete item; return res; } typename QList::iterator erase( typename QList::iterator afirst, typename QList::iterator alast) { if (!m_autoDelete) return QList::erase(afirst, alast); while (afirst != alast) { T item = *afirst; afirst = QList::erase(afirst); delete item; } return alast; } void pop_back() { removeLast(); } void pop_front() { removeFirst(); } int removeAll(const T& value) { if (!m_autoDelete) return QList::removeAll(value); typename QList::iterator it(QList::begin()); int removedCount = 0; while (it != QList::end()) { if (*it == value) { T item = *it; it = QList::erase(it); delete item; removedCount++; } else ++it; } return removedCount; } void clear() { if (!m_autoDelete) return QList::clear(); while (!QList::isEmpty()) { T item = QList::takeFirst(); delete item; } } private: bool m_autoDelete; }; //! @short Case insensitive hash container supporting QString or QByteArray keys. //! Keys are turned to lowercase before inserting. template class CaseInsensitiveHash : public QHash { public: CaseInsensitiveHash() : QHash() {} typename QHash::iterator find(const Key& key) const { return QHash::find(key.toLower()); } typename QHash::const_iterator constFind(const Key& key) const { return QHash::constFind(key.toLower()); } bool contains(const Key& key) const { return QHash::contains(key.toLower()); } int count(const Key& key) const { return QHash::count(key.toLower()); } typename QHash::iterator insert(const Key& key, const T& value) { return QHash::insert(key.toLower(), value); } typename QHash::iterator insertMulti(const Key& key, const T& value) { return QHash::insertMulti(key.toLower(), value); } const Key key(const T& value, const Key& defaultKey) const { return QHash::key(value, defaultKey.toLower()); } int remove(const Key& key) { return QHash::remove(key.toLower()); } const T take(const Key& key) { return QHash::take(key.toLower()); } const T value(const Key& key) const { return QHash::value(key.toLower()); } const T value(const Key& key, const T& defaultValue) const { return QHash::value(key.toLower(), defaultValue); } QList values(const Key& key) const { return QHash::values(key.toLower()); } T& operator[](const Key& key) { return QHash::operator[](key.toLower()); } const T operator[](const Key& key) const { return QHash::operator[](key.toLower()); } }; //! A set created from static (0-terminated) array of raw null-terminated strings. class KDB_EXPORT StaticSetOfStrings { public: StaticSetOfStrings(); explicit StaticSetOfStrings(const char* const array[]); ~StaticSetOfStrings(); void setStrings(const char* const array[]); bool isEmpty() const; //! @return true if @a string can be found within set, comparison is case sensitive bool contains(const QByteArray& string) const; private: class Private; Private * const d; Q_DISABLE_COPY(StaticSetOfStrings) }; /*! @return debugging string for object @a object of type @a T */ template QString debugString(const T& object) { QString result; QDebug dbg(&result); dbg << object; return result; } //! Used by findExe(). enum class FindExeOption { //! No options None = 0, //! If set, the path returned may not have the executable bit set. IgnoreExecBit = 1 }; Q_DECLARE_FLAGS(FindExeOptions, FindExeOption) /** * Finds the executable in the system path. * * A valid executable must be a file and have its executable bit set. * * @param appname The name of the executable file for which to search. * if this contains a path separator, it will be resolved * according to the current working directory * (shell-like behavior). * @param path The path which will be searched. If this is * null (default), the @c $PATH environment variable will * be searched. * @param options Options, see FindExeOption. * * @return The path of the executable. If it was not found, returns QString(). */ QString findExe(const QString& appname, const QString& path = QString(), FindExeOptions options = FindExeOption::None); //! A single property //! @note This property is general-purpose and not related to Qt Properties. //! @see KDbUtils::PropertySet class KDB_EXPORT Property { public: //! Constructs a null property Property(); Property(const QVariant &aValue, const QString &aCaption); Property(const Property &other); ~Property(); bool operator==(const Property &other) const; bool operator!=(const Property &other) const { return !operator==(other); } bool isNull() const; QVariant value() const; void setValue(const QVariant &value); QString caption() const; void setCaption(const QString &caption); private: class Private; Private * const d; }; //! A set of properties. //! @note These properties are general-purpose and not related to Qt Properties. //! @see KDbUtils::Property class KDB_EXPORT PropertySet { public: PropertySet(); PropertySet(const PropertySet &other); ~PropertySet(); //! Assigns @a other to this property set and returns a reference to this property set. PropertySet& operator=(const PropertySet &other); //! @return true if this property set has exactly the same properties as @a other //! @since 3.1 bool operator==(const PropertySet &other) const; //! @return true if this property differs in at least one property from @a other //! @since 3.1 bool operator!=(const PropertySet &other) const { return !operator==(other); } //! Inserts property with a given @a name, @a value and @a caption. //! If @a caption is empty, caption from existing property is reused. //! @a name must be a valid identifier (see KDb::isIdentifier()). void insert(const QByteArray &name, const QVariant &value, const QString &caption = QString()); //! Sets caption for property @a name to @a caption. //! If such property does not exist, does nothing. //! @since 3.1 void setCaption(const QByteArray &name, const QString &caption); //! Sets value for property @a name to @a value. //! If such property does not exist, does nothing. //! @since 3.1 void setValue(const QByteArray &name, const QVariant &value); //! Removes property with a given @a name. void remove(const QByteArray &name); //! @return property with a given @a name. //! If not found, a null Property is returned (Property::isNull). Property property(const QByteArray &name) const; //! @return a list of property names. QList names() const; private: class Private; Private * const d; }; } // KDbUtils #endif //KDB_TOOLS_UTILS_H diff --git a/tests/features/parser_test.h b/tests/features/parser_test.h index 919649d6..b7919046 100644 --- a/tests/features/parser_test.h +++ b/tests/features/parser_test.h @@ -1,70 +1,68 @@ /* This file is part of the KDE project Copyright (C) 2003-2004 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. */ #ifndef PARSER_TEST_H #define PARSER_TEST_H #include #include #include int parserTest(const KDbEscapedString &st, const QStringList ¶ms) { int r = 0; if (!conn->useDatabase()) { qDebug() << conn->result(); return 1; } KDbParser parser(conn); const bool ok = parser.parse(st); - KDbQuerySchema *q = parser.query(); + QScopedPointer q(parser.query()); QList variantParams; for(const QString ¶m : params) { variantParams.append(param.toLocal8Bit()); } if (ok && q) { cout << qPrintable(KDbUtils::debugString(KDbConnectionAndQuerySchema(conn, *q))) << '\n'; KDbNativeStatementBuilder builder(conn, KDb::DriverEscaping); KDbEscapedString sql; - if (builder.generateSelectStatement(&sql, q, variantParams)) { + if (builder.generateSelectStatement(&sql, q.data(), variantParams)) { cout << "-STATEMENT:\n" << sql.toByteArray().constData() << '\n'; } else { cout << "-CANNOT GENERATE STATEMENT\n"; } } else { qDebug() << parser.error(); r = 1; } - delete q; - q = nullptr; - + q.reset(); if (!conn->closeDatabase()) { qDebug() << conn->result(); return 1; } return r; } #endif