diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..894cbff --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +Makefile* +build* +*.o +moc_*.* +*.moc +ui_*.* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7967b27 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,107 @@ +project(sqlatelib) +set(SQLATE_LIBRARY_VERSION 0) +set(SQLATE_LIBRARY_SO_VERSION 1) + +cmake_minimum_required(VERSION 2.8) +set(CMAKE_AUTOMOC true) +option(SQL_ENABLE_NETWORK_WATCHER "Enable the watcher threads for SQL queries over the network. The default is disabled." FALSE) + +if (SQL_ENABLE_NETWORK_WATCHER) + add_definitions(-DSQL_ENABLE_NETWORK_WATCHER) +endif () + +if (ANDROID) + find_host_package(Qt4 REQUIRED) + include(Android) +else (ANDROID) + find_package(Qt4 REQUIRED) +endif() + +find_package(Boost 1.40 REQUIRED) +add_definitions( -DBOOST_MPL_LIMIT_VECTOR_SIZE=50 -DBOOST_MPL_CFG_NO_PREPROCESSED_HEADERS ) +add_definitions( -DQT_STRICT_ITERATORS ) +add_definitions( -DQT_NO_CAST_FROM_ASCII ) +add_definitions( -DQT_NO_CAST_TO_ASCII ) +add_definitions( -DQT_NO_CAST_FROM_BYTEARRAY ) +add_definitions( -DQT_USE_FAST_CONCATENATION -DQT_USE_FAST_OPERATOR_PLUS) + +enable_testing() + +if(APPLE) + set(BIN_INSTALL_DIR ".") + # No LIB_INSTALL_DIR on APPLE, we use BundleUtilities instead. + # No PLUGIN_INSTALL_DIR on APPLE, we use BundleUtilities instead. + # If the install directory is the default then set to a child dir + # of the the binary install. Otherwise we assume the user has specified + # a CMAKE_INSTALL_PREFIX define + # FIXME: there must be a better way of detecting the default vs user set + if(CMAKE_INSTALL_PREFIX MATCHES "/usr/local") + set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/bin") + endif() +elseif(WIN32) + set(BIN_INSTALL_DIR ".") + set(LIB_INSTALL_DIR ".") + set(PLUGIN_INSTALL_DIR "plugins") +elseif(NOT ANDROID) + set(BIN_INSTALL_DIR "bin") + set(LIB_SUFFIX "" CACHE STRING "Define suffix of directory name (32/64)") + set(LIB_INSTALL_DIR "lib${LIB_SUFFIX}") + set(PLUGIN_INSTALL_DIR "${LIB_INSTALL_DIR}/plugins") +endif() + +include_directories( ${CMAKE_SOURCE_DIR} + ${CMAKE_BINARY_DIR} + ${QT_INCLUDES}) + +set(SQLATE_SRCS + kdthreadrunner.cpp + SqlCondition.cpp + SqlQuery.cpp + SqlQueryBuilderBase.cpp + SqlConditionalQueryBuilderBase.cpp + SqlDeleteQueryBuilder.cpp + SqlSelectQueryBuilder.cpp + SqlInsertQueryBuilder.cpp + SqlUpdateQueryBuilder.cpp + SqlCreateTable.cpp + SqlSchema.cpp + SqlTransaction.cpp + SqlMonitor.cpp + SqlQueryManager.cpp + SqlQueryWatcher.cpp + SqlUtils.cpp + PostgresSchema.cpp + SchemaUpdater.cpp + SqlQueryCache.cpp +) + +qt4_add_resources(sql_resources SqlResources.qrc ) + +add_library(sqlate SHARED ${SQLATE_SRCS} ${sql_resources}) + +if (NOT ANDROID) +set_target_properties(sqlate PROPERTIES + VERSION ${SQLATE_LIBRARY_VERSION} + SOVERSION ${SQLATE_LIBRARY_SO_VERSION} +) +endif() + +set_target_properties(sqlate PROPERTIES + DEFINE_SYMBOL SQLATE_BUILD_SQLATE_LIB +) + +target_link_libraries(sqlate ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${QT_QTSQL_LIBRARY}) + +if(NOT APPLE AND NOT ANDROID) + install(TARGETS sqlate RUNTIME DESTINATION ${BIN_INSTALL_DIR} LIBRARY DESTINATION ${LIB_INSTALL_DIR}) +endif() + +add_executable(sqlschema2dot sqlschema2dot.cpp) +target_link_libraries(sqlschema2dot ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${QT_QTSQL_LIBRARY} sqlate) + +add_custom_target(schema-graph + ${CMAKE_CURRENT_BINARY_DIR}/sqlschema2dot > sqlschema.dot && + dot -Tpng sqlschema.dot > sqlschema.png +) + +add_subdirectory(tests) diff --git a/PostgresSchema.cpp b/PostgresSchema.cpp new file mode 100644 index 0000000..8f8dac2 --- /dev/null +++ b/PostgresSchema.cpp @@ -0,0 +1,12 @@ +/* + * Copyright (C) 2012 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + * Author: Volker Krause + */ + +#include "PostgresSchema.h" + +namespace Sql { + +DEFINE_SCHEMA( POSTGRES_SCHEMA ) + +} \ No newline at end of file diff --git a/PostgresSchema.h b/PostgresSchema.h new file mode 100644 index 0000000..b71e402 --- /dev/null +++ b/PostgresSchema.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2012 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + * Author: Volker Krause + */ + +#ifndef POSTGRESSCHEMA_H +#define POSTGRESSCHEMA_H + +#include "SqlSchema_p.h" +#include "sqlate_export.h" + +#include + +/** + * Schema definition for the PostgreSQL system tables to the extend we need to access them + * @note Do not use this in combination with the create table query builder! + */ +namespace Sql { + +TABLE( PgUser, SQLATE_EXPORT ) { + SQL_NAME( "pg_user" ); + COLUMN( usename, QString, Null ); + COLUMN( usesysid, int, Null ); + COLUMN( usecreatedb, bool, Null ); + COLUMN( usesuper, bool, Null ); + COLUMN( usecatupd, bool, Null ); + COLUMN( userepl, bool, Null ); + COLUMN( passwd, QString, Null ); + COLUMN( valuntil, QDateTime, Null ); + typedef boost::mpl::vector columns; +}; + +TABLE( PgGroup, SQLATE_EXPORT ) { + SQL_NAME( "pg_group" ); + COLUMN( groname, QString, Null ); + COLUMN( grosysid, int, Null ); + typedef boost::mpl::vector columns; +}; + +TABLE( PgAuthMembers, SQLATE_EXPORT ) { + SQL_NAME( "pg_auth_members" ); + COLUMN( roleid, int, NotNull ); + COLUMN( member, int, NotNull ); + COLUMN( grantor, int, NotNull ); + COLUMN( admin_option, bool, NotNull ); + typedef boost::mpl::vector columns; +}; + +#define POSTGRES_SCHEMA (PgUser)(PgGroup)(PgAuthMembers) + +DECLARE_SCHEMA( PostgresSchema, POSTGRES_SCHEMA ); + +} + +#endif \ No newline at end of file diff --git a/SchemaUpdater.cpp b/SchemaUpdater.cpp new file mode 100644 index 0000000..a28b45b --- /dev/null +++ b/SchemaUpdater.cpp @@ -0,0 +1,56 @@ +#include "SchemaUpdater.h" + +#include "sql.h" +#include "SqlQuery.h" +#include "SqlUtils.h" + +#include + +using namespace Sql; + +SchemaUpdaterBase::SchemaUpdaterBase (int targetVersion, const QString & pluginName /* = QString()*/ , QWidget* parentWidget /*= 0*/, QObject *parent /*= 0*/) : + QObject( parent ), + m_parentWidget(parentWidget), + m_currentVersion(-1), + m_targetVersion(targetVersion), + m_pluginName(pluginName) +{ +} + +QVector SchemaUpdaterBase::pendingUpdates(const QDir & dir) const +{ + QVector updates; + updates.reserve(m_targetVersion - m_currentVersion); + + for ( int i = m_currentVersion + 1; i <= m_targetVersion; ++i ) { + UpdateInfo info; + info.version = i; + const QString fileName = QString::fromLatin1("%1.sql").arg(i); + if (dir.exists(fileName)) { + info.updateFile = dir.absoluteFilePath(fileName); + updates.push_back(info); + } + } + return updates; +} + +void SchemaUpdaterBase::execUpdate ( const QString& updateFile ) +{ + QFile file(updateFile); + if (file.open(QFile::ReadOnly | QIODevice::Text)) { + QTextStream stream(&file); + foreach ( QString statement, SqlUtils::splitQueries(stream.readAll()) ) { + SqlUtils::stripComments(statement); + if (statement.isEmpty()) + continue; + + SqlQuery query; + query.exec(statement); + } + } else { + // coming from a QRC file, always readable... + qFatal("unable to open %s", qPrintable(updateFile)); + } +} + +#include "moc_SchemaUpdater.cpp" diff --git a/SchemaUpdater.h b/SchemaUpdater.h new file mode 100644 index 0000000..a05f482 --- /dev/null +++ b/SchemaUpdater.h @@ -0,0 +1,298 @@ +#ifndef SCHEMAUPDATER_H +#define SCHEMAUPDATER_H +#include "sqlate_export.h" + +#include "SqlCreateTable.h" +#include "SqlUtils.h" +#include "SqlExceptions.h" +#include "SqlQueryCache.h" +#include "SqlUpdateQueryBuilder.h" +#include "SqlTransaction.h" + +#include +#include +#include +#include +#include +#include + + +/** + * @internal + * Methods for performing the database schema upgrade. + * Connect the signals defined in here to the log window. + * + * @note Use SchemaUpdater instead! + */ + +class SQLATE_EXPORT SchemaUpdaterBase : public QObject +{ + Q_OBJECT +public: + /** + * @param targetVersion The version we want to upgrade to. + * @param parentWidget widget used as dialog parent + */ + explicit SchemaUpdaterBase( int targetVersion, const QString & pluginName = QString(), QWidget* parentWidget = 0, QObject *parent = 0 ); + + /** + * Returns @c true if the schema needs to be updated. + * @param the table in which the version of the schema is stored + * @note 1 table for the main app, 1 table for each plugin + */ + virtual bool needsUpdate() const = 0; + + /** + * Executes all necessary database schema updates. + * @param interactive Ask user for confirmation. + */ + virtual void execUpdates(bool interactive) = 0; + + +signals: + void infoMessage(const QString &msg) const; + void successMessage(const QString &msg) const; + void errorMessage(const QString &msg) const; + +protected: + virtual void createMissingTables() = 0; + virtual void createMissingColumns() = 0; + virtual void createRulesPermissionsAndTriggers() = 0; + + struct UpdateInfo { + int version; + QString updateFile; + }; + + /** + * Lists all necessary updates to execute + * @param dir: the directory containing the scripts files + */ + QVector pendingUpdates(const QDir & dir) const; + + /** + * Executes a single update step. + * @throws SqlException on query errors. + */ + void execUpdate(const QString &updateFile); + + QWidget *m_parentWidget; + mutable int m_currentVersion; + const int m_targetVersion; + QString m_pluginName; +}; + + +namespace detail { + +/** + * Helper class for MPL iteration to create missing columns. + * @internal + */ +struct missing_column_creator +{ + explicit missing_column_creator( const QSqlDatabase &db ) : m_db( db ) {} + + /** + * Determines the missing columns and creates them. + * @tparam Table The table type. + */ + template + void operator() ( const Table &table ) + { + typedef QPair StringTypePair; + const QVector existingColumns = SqlUtils::columnsOfTable(table, m_db); + + QStringList columnNames; + Sql::detail::sql_name_accumulator nameAccu( columnNames ); + boost::mpl::for_each >( nameAccu ); + + QStringList columnStatements; + Sql::detail::column_creator stmtAccu( columnStatements ); + boost::mpl::for_each >( stmtAccu ); + + Q_ASSERT(columnNames.size() == columnStatements.size()); + for ( QStringList::const_iterator it = columnNames.constBegin(), it2 = columnStatements.constBegin(); it != columnNames.constEnd(); ++it, ++it2 ) { + bool found = false; + foreach ( const StringTypePair &existinCol, existingColumns ) { + if ( existinCol.first.toLower() == (*it).toLower() ) { + found = true; + break; + } + } + if (found) + continue; + + QString alterStmt = QLatin1Literal("ALTER TABLE ") % Table::tableName() % QLatin1Literal(" ADD COLUMN ") % *it2; + SqlQuery query(m_db); + query.exec(alterStmt); + } + } + + QSqlDatabase m_db; +}; + + +} // detail + + +/** + * Database schema updater. + * Additionally to SchemaUpdaterBase this also adds missing tables and columns based on the given schema. + */ +template +class SchemaUpdater : public SchemaUpdaterBase +{ +public: + /** + * @param targetVersion The version we want to upgrade to. + * @param parentWidget widget used as dialog parent + */ + explicit SchemaUpdater( int targetVersion, const QString & pluginName = QString(), QWidget* parentWidget = 0, QObject *parent = 0 ) : + SchemaUpdaterBase(targetVersion,pluginName, parentWidget, parent) + {} + + /** + * Returns @c true if the schema needs to be updated. + * @param the table in which the version of the schema is stored + * @note 1 table for the main app, 1 table for each plugin + */ + bool needsUpdate() const + { + tableVersion table; + if (m_currentVersion >= 0) + return m_currentVersion < m_targetVersion; + + try { + SqlQuery q = select( table.version ).from( table ); + q.exec(); + if (q.next()) { + m_currentVersion = q.value(0).toInt(); + Q_ASSERT(m_currentVersion >= 0); + return m_currentVersion < m_targetVersion; + } + } catch (const SqlException& e) { + emit errorMessage( QObject::tr("There was an error while checking the database version. This is a critical error, the application will terminate.
\ + The error was: %1.").arg(e.error().text()) ); + QApplication::exit(1); + return false; + } + + Q_ASSERT(false); // this case is caught way earlier + return true; + } + + /** + * Executes all necessary database schema updates. + * @param interactive Ask user for confirmation. + */ + void execUpdates(bool interactive) + { + SqlQueryCache::setEnabled(false); + + tableVersion table; + const QString schemaName = m_pluginName.isNull()?tr("Main application"):m_pluginName; + if (!needsUpdate()) { + emit successMessage(QObject::tr("schema is up-to-date for %1.").arg(schemaName)); + return; + } + emit errorMessage(QObject::tr("schema is outdated for %1.").arg(schemaName)); + + if (interactive && QMessageBox::warning( m_parentWidget, QObject::tr("Database Schema Update Required"), + QObject::tr("An update to the database schema is required to proceed. Such an update can take considerable time and will require updates to all client installations as well. " + "Also, ensure you have current backups before proceeding."), QMessageBox::Apply | QMessageBox::Abort, QMessageBox::Abort) + != QMessageBox::Apply) + { + QApplication::quit(); + return; + } + + // step 1: auto-create missing tables + try { + createMissingTables(); + } catch (const SqlException &e) { + emit errorMessage(QObject::tr("Database schema update failed during creation of new tables: %1.").arg(e.error().text())); + QApplication::exit(1); + return; + } + + // step 2 auto-create missing columns, if the previous step succeeded this must not fail due to missing tables + try { + createMissingColumns(); + } catch (const SqlException &e) { + emit errorMessage(QObject::tr("Database schema update failed during creation of new columns: %1.").arg(e.error().text())); + QApplication::exit(1); + return; + } + + // step 3: run custom update scripts + foreach (const UpdateInfo &update, pendingUpdates(QDir(QString::fromLatin1(":/%1schemaUpdates/").arg(m_pluginName)))) { + try { + emit infoMessage(QObject::tr("Upgrading schema to version %1...").arg(update.version)); + SqlTransaction t; + + execUpdate(update.updateFile); + + SqlUpdateQueryBuilder qb; + qb.setTable(table); + qb.addColumnValue(table.version, update.version); + qb.exec(); + + t.commit(); + m_currentVersion = update.version; + emit successMessage(QObject::tr("Schema successfully upgraded to version %1").arg(update.version)); + } catch (const SqlException &e) { + emit errorMessage(QObject::tr("Schema update failed to version %1. The error was '%2'.").arg(update.version).arg(e.error().text())); + QApplication::exit(1); + return; + } + } + + // step 3 create the rules/triggers for tables. Must be done at the end, after the tables are fully updated + try { + createRulesPermissionsAndTriggers(); + } catch (const SqlException &e) { + emit errorMessage(QObject::tr("Database schema update failed during creation of new columns: %1").arg(e.error().text())); + QApplication::exit(1); + return; + } + + // step 4: if necessary increase the version number beyond what the custom updates did, in case the latest version + // only included automatic changes. + if (m_currentVersion < m_targetVersion) { + try { + SqlUpdateQueryBuilder qb; + qb.setTable(table); + qb.addColumnValue(table.version, m_targetVersion); + qb.exec(); + m_currentVersion = m_targetVersion; + } catch (const SqlException &e) { + emit errorMessage(QObject::tr("The database schema version number could not be updated: %1.").arg(e.error().text())); + QApplication::exit(1); + return; + } + } + + SqlQueryCache::setEnabled(true); + } + +protected: + void createMissingTables() + { + Sql::createMissingTables(QSqlDatabase::database()); + } + + void createMissingColumns() + { + ::detail::missing_column_creator creator(QSqlDatabase::database()); + boost::mpl::for_each( creator ); + } + + void createRulesPermissionsAndTriggers() + { + Sql::createRulesPermissionsAndTriggers(QSqlDatabase::database()); + } +}; + + +#endif // SCHEMAUPDATER_H diff --git a/Sql.h b/Sql.h new file mode 100644 index 0000000..f7bde59 --- /dev/null +++ b/Sql.h @@ -0,0 +1,38 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +*/ +#ifndef SQL_H +#define SQL_H + +/** + * @file Sql.h + * Convenience include for all the SQL template stuff + * Preferably include this file instead of any of the other ones directly, + * since this one cleans up a bunch of otherwise leaked macros. + */ + +/** + * @namespace Sql + * Sql access classes and templates, including the database schema definition. + */ + +#include "SqlSchema.h" +#include "SqlCreateTable.h" +#include "SqlSelect.h" + +// macro cleanup +#undef SQL_NAME +#undef COLUMN +#undef COLUMN_ALIAS +#undef FOREIGN_KEY +#undef TABLE +#undef LOOKUP_TABLE +#undef RELATION +#undef RECURSIVE_RELATION +#undef DECLARE_SCHEMA +#undef DEFINE_SCHEMA +#undef DECLARE_SCHEMA_MAKE_TYPE +#undef DEFINE_SCHEMA_MAKE_DEF + +#endif diff --git a/SqlCondition.cpp b/SqlCondition.cpp new file mode 100644 index 0000000..814aacc --- /dev/null +++ b/SqlCondition.cpp @@ -0,0 +1,92 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +*/ +#include "SqlCondition.h" + +SqlCondition::SqlCondition(SqlCondition::LogicOperator op) : + m_compareOp( Equals ), + m_logicOp( op ), + m_isCaseSensitive( true ) +{ +} + +void SqlCondition::addValueCondition(const QString& column, SqlCondition::CompareOperator op, const QVariant& value) +{ + Q_ASSERT( !column.isEmpty() ); + SqlCondition c; + c.m_compareOp = op; + if ( (!m_isCaseSensitive) && value.type() == QVariant::String ) + { + c.m_comparedValue = value.toString().toLower(); + c.m_column = QString::fromLatin1( "LOWER( %1 ) " ).arg( column ); + } + else + { + c.m_comparedValue = value; + c.m_column = column; + } + m_subConditions.push_back( c ); +} + +void SqlCondition::addPlaceholderCondition(const QString& column, SqlCondition::CompareOperator op, const QString& placeholder) +{ + Q_ASSERT( !column.isEmpty() ); + Q_ASSERT( placeholder.size() >= 2 ); + Q_ASSERT( placeholder.startsWith( QLatin1Char( ':' ) ) ); + Q_ASSERT( !placeholder.at( 1 ).isDigit() ); + SqlCondition c; + c.m_compareOp = op; + if ( m_isCaseSensitive ) + { + c.m_placeholder = placeholder; + c.m_column = column; + } + else + { + c.m_placeholder = placeholder.toLower(); + c.m_column = QString::fromLatin1( "LOWER( %1 ) " ).arg( column ); + } + m_subConditions.push_back( c ); +} + +void SqlCondition::addColumnCondition(const QString& column, SqlCondition::CompareOperator op, const QString& column2) +{ + Q_ASSERT( !column.isEmpty() ); + Q_ASSERT( !column2.isEmpty() ); + SqlCondition c; + c.m_column = column; + c.m_comparedColumn = column2; + c.m_compareOp = op; + m_subConditions.push_back( c ); +} + +void SqlCondition::addCondition(const SqlCondition& condition) +{ + m_subConditions.push_back( condition ); +} + +void SqlCondition::setLogicOperator(SqlCondition::LogicOperator op) +{ + m_logicOp = op; +} + +QVector< SqlCondition > SqlCondition::subConditions() const +{ + return m_subConditions; +} + +bool SqlCondition::hasSubConditions() const +{ + return !m_subConditions.isEmpty(); +} + +void SqlCondition::setCaseSensitive( const bool isCaseSensitive ) +{ + m_isCaseSensitive = isCaseSensitive; +} + +bool SqlCondition::isCaseSensitive() const +{ + return m_isCaseSensitive; +} diff --git a/SqlCondition.h b/SqlCondition.h new file mode 100644 index 0000000..a07c23c --- /dev/null +++ b/SqlCondition.h @@ -0,0 +1,159 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +*/ + +#ifndef SQLCONDITION_H +#define SQLCONDITION_H + +#include "sqlate_export.h" +#include "SqlInternals_p.h" + +#include +#include +#include + +#include +#include +#include + +/** SQL NULL type, to allow using NULL in template code, rather than falling back to QVariant(). */ +struct SqlNullType {}; +static const SqlNullType SqlNull = {}; // "Null" is already in use, also in the Sql namespace, so we have to settle for this + +/** SQL now type, to allow using server-side current date/time in template code, rahter than hardcoded SQL strings or client-side time. */ +struct SqlNowType {}; +static const SqlNowType SqlNow = {}; +Q_DECLARE_METATYPE(SqlNowType) + +/** Dummy type for compile time warnings about usage of client side time. */ +struct UsageOfClientSideTime {}; + + +/** Represents a part of a SQL WHERE expression. */ +class SQLATE_EXPORT SqlCondition +{ +public: + /** Compare operators to be used in query conditions. */ + enum CompareOperator { + Equals, + NotEquals, + Is, + IsNot, + Less, + LessOrEqual, + Greater, + GreaterOrEqual, + Like + }; + + /** Logic operation to combine multiple conditions. */ + enum LogicOperator { + And, + Or + }; + + /** Create an empty condition, with sub-queries combined using @p op. */ + explicit SqlCondition( LogicOperator op = And ); + + /** + Add a condition which compares a column with a given fixed value. + @param column The column that should be compared. + @param op The operator used for comparison + @param value The value @p column is compared to. + */ + void addValueCondition( const QString &column, CompareOperator op, const QVariant &value ); + template + inline void addValueCondition( const Column &column, CompareOperator op, const typename Column::type &value ) + { + Sql::warning, UsageOfClientSideTime>::print(); + addValueCondition( column.name(), op, QVariant::fromValue(value)); + } + template + inline void addValueCondition( const Column &column, CompareOperator op, SqlNullType ) + { + // asserting on Column::notNull is too strict, this can be used in combination with outer joins! + //BOOST_MPL_ASSERT(( boost::mpl::not_ )); + // TODO idealy we would also static assert on op == Is[Not] + addValueCondition( column.name(), op, QVariant() ); + } + template + inline void addValueCondition( const Column &column, CompareOperator op, SqlNowType now ) + { + BOOST_MPL_ASSERT(( boost::is_same )); + // TODO this could also be restricted to less/greater than operations + addValueCondition( column.name(), op, QVariant::fromValue(now) ); + } + + /** + * Same as addValueCondition, but for defered value binding. + * @param column The column that should be compared. + * @param op The operator used for comparison. + * @param placeholder A placeholder (with leading ':'), not starting with a number. + */ + void addPlaceholderCondition( const QString &column, CompareOperator op, const QString &placeholder ); + template + inline void addPlaceholderCondition( const Column &column, CompareOperator op, const QString &placeholder ) + { + addPlaceholderCondition( column.name(), op, placeholder ); + } + + /** + Add a condition which compares a column with another column. + @param column The column that should be compared. + @param op The operator used for comparison. + @param column2 The column @p column is compared to. + */ + void addColumnCondition( const QString &column, CompareOperator op, const QString &column2 ); + template + inline void addColumnCondition( const Column1 &column1, CompareOperator op, const Column2 &column2 ) + { + BOOST_MPL_ASSERT(( boost::is_same )); + addColumnCondition(column1.name(), op, column2.name()); + } + + /** + Set the case sensitive flag. This is defaulted to true and must be set before calling @func addCondition() or @func addPlaceholderCondition(). + @param isCasesensitive the operands are converted to the same case before comparison + */ + void setCaseSensitive( const bool isCaseSensitive ); + /** + returns the value of the case sensitive flag. + */ + bool isCaseSensitive() const; + + /** + Add a nested condition. + */ + void addCondition( const SqlCondition &condition ); + + /** + Set how sub-conditions should be combined, default is And. + */ + void setLogicOperator( LogicOperator op ); + + /** + * For nested conditions, this returns the list of sub-conditions. + */ + QVector subConditions() const; + + /** + * Checks if this condition has sub-conditions. + */ + bool hasSubConditions() const; + +private: + friend class SqlConditionalQueryBuilderBase; + QVector m_subConditions; + QString m_column; + QString m_comparedColumn; + QString m_placeholder; + QVariant m_comparedValue; + CompareOperator m_compareOp; + LogicOperator m_logicOp; + bool m_isCaseSensitive; +}; + +Q_DECLARE_TYPEINFO( SqlCondition, Q_MOVABLE_TYPE ); + +#endif diff --git a/SqlConditionalQueryBuilderBase.cpp b/SqlConditionalQueryBuilderBase.cpp new file mode 100644 index 0000000..30df21d --- /dev/null +++ b/SqlConditionalQueryBuilderBase.cpp @@ -0,0 +1,85 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +*/ +#include "SqlConditionalQueryBuilderBase.h" + +#include "SqlExceptions.h" + +#include + +SqlConditionalQueryBuilderBase::SqlConditionalQueryBuilderBase(const QSqlDatabase& db) : + SqlQueryBuilderBase( db ), + m_bindedValuesOffset(0) +{ +} + +SqlCondition& SqlConditionalQueryBuilderBase::whereCondition() +{ + return m_whereCondition; +} + +QString SqlConditionalQueryBuilderBase::registerBindValue(const QVariant& value) +{ + m_bindValues.push_back( value ); + return QLatin1Char( ':' ) + QString::number( m_bindedValuesOffset + m_bindValues.size() - 1 ); + +} + +static QString logicOperatorToString( SqlCondition::LogicOperator op ) +{ + switch ( op ) { + case SqlCondition::And: return QLatin1String( " AND " ); + case SqlCondition::Or: return QLatin1String( " OR " ); + } + qFatal( "Unknown logic operator" ); + return QString(); +} + +static QString compareOperatorToString( SqlCondition::CompareOperator op ) +{ + switch ( op ) { + case SqlCondition::Equals: return QLatin1String( " = " ); + case SqlCondition::NotEquals: return QLatin1String( " <> " ); + case SqlCondition::Is: return QLatin1String( " IS " ); + case SqlCondition::IsNot: return QLatin1String( " IS NOT " ); + case SqlCondition::Less: return QLatin1String( " < " ); + case SqlCondition::LessOrEqual: return QLatin1String( " <= " ); + case SqlCondition::Greater: return QLatin1String( " > " ); + case SqlCondition::GreaterOrEqual: return QLatin1String( " >= " ); + case SqlCondition::Like: return QLatin1String( " LIKE " ); + } + qFatal( "Unknown compare operator." ); + return QString(); +} + +QString SqlConditionalQueryBuilderBase::conditionToString(const SqlCondition& condition) +{ + if ( condition.hasSubConditions() ) { + QStringList conds; + foreach ( const SqlCondition &c, condition.subConditions() ) + conds << conditionToString( c ); + if ( conds.size() == 1 ) + return conds.first(); + return QLatin1Char( '(' ) + conds.join( logicOperatorToString( condition.m_logicOp ) ) + QLatin1Char( ')' ); + } else { + QString stmt = condition.m_column; + stmt += compareOperatorToString( condition.m_compareOp ); + if ( condition.m_comparedColumn.isEmpty() ) { + if ( condition.m_comparedValue.isValid() ) { + if ( condition.m_comparedValue.userType() == qMetaTypeId() ) + stmt += currentDateTime(); + else + stmt += registerBindValue( condition.m_comparedValue ); + } else if ( !condition.m_placeholder.isEmpty() ) { + stmt += condition.m_placeholder; + } else { + stmt += QLatin1String( "NULL" ); + } + } else { + stmt += condition.m_comparedColumn; + } + return stmt; + } +} + diff --git a/SqlConditionalQueryBuilderBase.h b/SqlConditionalQueryBuilderBase.h new file mode 100644 index 0000000..a6e32b1 --- /dev/null +++ b/SqlConditionalQueryBuilderBase.h @@ -0,0 +1,41 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +*/ + +#ifndef SQLCONDITIONALQUERYBUILDERBASE_H +#define SQLCONDITIONALQUERYBUILDERBASE_H + +#include "SqlQueryBuilderBase.h" + +#include "SqlCondition.h" +#include "sqlate_export.h" + +/** Base class for query builders operating having a WHERE condition. + */ +class SQLATE_EXPORT SqlConditionalQueryBuilderBase : public SqlQueryBuilderBase +{ +public: + /// Create a new query builder for the given database + explicit SqlConditionalQueryBuilderBase( const QSqlDatabase &db = QSqlDatabase::database() ); + + /// access to the top-level WHERE condition + SqlCondition& whereCondition(); + +protected: + /** Register a bound value, to be replaced after query preparation + * @param value The value to bind + * @return A unique placeholder to use in the query. + */ + QString registerBindValue( const QVariant &value ); + + QString conditionToString( const SqlCondition &condition ); + +protected: + SqlCondition m_whereCondition; + QVector m_bindValues; + int m_bindedValuesOffset; //holds the parameters offset + +}; + +#endif diff --git a/SqlCreateRule.h b/SqlCreateRule.h new file mode 100644 index 0000000..2834b48 --- /dev/null +++ b/SqlCreateRule.h @@ -0,0 +1,128 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +* Benoit Dumas +*/ +#ifndef SQLCREATERULE_H +#define SQLCREATERULE_H + +#include "SqlInternals_p.h" +#include "SqlSchema_p.h" +#include "SqlQuery.h" +#include "SqlUtils.h" + +#include +#include + +#include + +/** + * @file SqlCreateRule.h + * Classes and functions to create tables rules based on the schema definition in SqlSchema.h + */ + +namespace Sql { + +namespace detail { + +/** + * Table-wise notification rule creator. + * @internal + */ +struct notification_rule_creator { + notification_rule_creator( QStringList &stmts ) : m_stmts( stmts ) {} + template + void operator()( wrap ) { + if ( T::is_lookup_table::value || T::is_relation::value ) + return; + const QStringList ops = QStringList() << QLatin1String( "Insert" ) << QLatin1String( "Update" ) << QLatin1String( "Delete" ); + foreach ( const QString& op, ops ) { + const QString stmt = QLatin1Literal( "CREATE OR REPLACE RULE " ) + % T::tableName() + % QLatin1Literal( "Notification" ) % op % QLatin1Literal( "Rule AS ON " ) + % op.toUpper() % QLatin1Literal( " TO " ) + % T::tableName() + % QLatin1Literal( " DO ALSO NOTIFY " ) + % T::tableName() + % QLatin1Literal( "Changed" ); + m_stmts.push_back( stmt ); + } + } + QStringList &m_stmts; +}; + +/** + * Column-wise notification rule creator. + * @internal + */ +struct column_notification_rule_creator { + column_notification_rule_creator( QStringList &stmts ) : m_stmts( stmts ) {} + template + void operator()( wrap ) { + if( T::notify::value ) //notify with the corresponding column content ( as the notify channel) when a row is modified + { + //be sure to have unique rule name and stay within the allowed name size. We don't care that it's cryptic as it isn't used anywhere. so use Uuids. + QString ruleID = QUuid::createUuid().toString(); + ruleID.replace(QLatin1Char('{'), QLatin1Char('\"')); + ruleID.replace(QLatin1Char('}'), QLatin1Char('\"')); + + const QString identifier = SqlUtils::createIdentifier(T::table::sqlName() % QLatin1Literal( "_" ) % T::sqlName()); + const QString stmt = QLatin1Literal( "CREATE OR REPLACE RULE " ) + % ruleID % QLatin1Literal( " AS ON UPDATE TO " ) + % T::table::sqlName() + % QLatin1Literal( " DO ALSO SELECT pg_notify(CAST (OLD." ) % T::sqlName() + % QLatin1Literal( " AS text) || '_") % identifier % QLatin1Literal( "','')"); + m_stmts.push_back( stmt ); + } + } + QStringList &m_stmts; +}; + +/** + * Table-wise notification rule creator (create statements for each Notify-enabled column in all tables). + * @internal + */ +struct table_notification_rule_creator { + table_notification_rule_creator( QStringList &stmts ) : m_stmts( stmts ) {} + template + void operator()( wrap ) { + boost::mpl::for_each >( column_notification_rule_creator( m_stmts ) ); + } + QStringList &m_stmts; +}; + +} + +/** + * Returns a list of CREATE RULE statements to create notification rules for all mutable tables. + * @tparam Schema A MPL sequence of tables. + * @returns A list of SQL statements to create notification rules for all mutable tables. + */ +template +QStringList createNotificationRuleStatements() +{ + QStringList statements; + boost::mpl::for_each >( detail::notification_rule_creator( statements ) ); + boost::mpl::for_each >( detail::table_notification_rule_creator( statements ) ); + + return statements; +} + +/** + * Create all table change notification rules. + * @tparam Schema A MPL sequence of tables. + * @param db The database to create the tables in + * @throws SqlException in case of a database error + */ +template +void createNotificationRules( const QSqlDatabase &db ) +{ + foreach ( const QString &stmt, Sql::createNotificationRuleStatements() ) { + SqlQuery q( db ); + q.exec( stmt ); + } +} + +} + +#endif diff --git a/SqlCreateTable.cpp b/SqlCreateTable.cpp new file mode 100644 index 0000000..71b3eb4 --- /dev/null +++ b/SqlCreateTable.cpp @@ -0,0 +1,70 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +* Andras Mantia +*/ +#include "SqlCreateTable.h" + +namespace Sql { +namespace detail { + +QString sqlType( wrap, int size ) +{ + if ( size > 0 ) + return QLatin1Literal( "VARCHAR(" ) % QString::number( size ) % QLatin1Literal( ")" ); + return QLatin1String( "TEXT" ); +} + +QString sqlType( wrap, int size ) +{ + Q_ASSERT( size == -1 ); + return QLatin1String( "BOOLEAN" ); +} + +QString sqlType( wrap, int size ) +{ + Q_ASSERT( size == -1 ); + return QLatin1String( "UUID" ); +} + +QString sqlType( wrap, int size ) +{ + Q_UNUSED( size ); + /// \todo in theory there are different sized integer types + return QLatin1String( "INTEGER" ); +} + +QString sqlType( wrap, int size ) +{ + Q_ASSERT( size == -1 ); + return QLatin1String( "TIMESTAMP WITH TIME ZONE" ); +} + +QString sqlType( wrap, int size ) +{ + Q_ASSERT( size == -1 ); + return QLatin1String( "TIME" ); +} + +QString sqlType( wrap, int size ) { + Q_ASSERT( size == -1 ); + return QLatin1String( "DATE" ); +} + +QString sqlType( wrap, int size ) +{ + Q_UNUSED( size ); + // ??? + return QLatin1String( "BYTEA" ); +} + +QString sqlType(Sql::detail::wrap< float > , int size) +{ + Q_UNUSED(size); + return QLatin1String("REAL"); +} + + +} // namespace detail + +} // namespace Sql diff --git a/SqlCreateTable.h b/SqlCreateTable.h new file mode 100644 index 0000000..c8f35c9 --- /dev/null +++ b/SqlCreateTable.h @@ -0,0 +1,383 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +* Andras Mantia +*/ +#ifndef SQL_CREATETABLE_H +#define SQL_CREATETABLE_H + +#include "sqlate_export.h" +#include "SqlInternals_p.h" +#include "SqlSchema_p.h" +#include "SqlQuery.h" +#include "SqlInsertQueryBuilder.h" +#include "SqlGrantPermission.h" +#include "SqlCreateRule.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +/** + * @file SqlCreateTable.h + * Classes and functions to create tables based on the schema definition in SqlSchema.h + */ + +namespace Sql { + +template QString createTableStatement(); +template QStringList createTableTriggerStatements(); + +namespace detail { + +/** Type to string conversion. */ +SQLATE_EXPORT QString sqlType( wrap, int size ); +SQLATE_EXPORT QString sqlType( wrap, int size ); +SQLATE_EXPORT QString sqlType( wrap, int size ); +SQLATE_EXPORT QString sqlType( wrap, int size ); +SQLATE_EXPORT QString sqlType( wrap, int size ); +SQLATE_EXPORT QString sqlType( wrap, int size ); +SQLATE_EXPORT QString sqlType( wrap, int size ); +SQLATE_EXPORT QString sqlType( wrap, int size ); +SQLATE_EXPORT QString sqlType( wrap, int size ); + +/** + * Helper class to build a list of SQL names from tables or columns. + * @internal + */ +struct sql_name_accumulator +{ + explicit sql_name_accumulator( QStringList &nameList ) : m_names( nameList ) {}; + template void operator() ( wrap ) + { + m_names.push_back( T::sqlName() ); + } + QStringList &m_names; +}; + +/** + * Helper class to create a single column statement in a create table command. + * @internal + */ +struct column_creator +{ + explicit column_creator( QStringList &c ) : cols( c ) {} + + /** + * Creates the column statement. + * @tparam C The column type + */ + template void operator()( wrap ) + { + QString colStmt = C::sqlName() % QLatin1Char( ' ' ) % sqlType( detail::wrap(), C::size ); + if ( C::primaryKey::value ) { + colStmt += QLatin1String( " PRIMARY KEY" ); + } else { + if ( C::unique::value ) + colStmt += QLatin1String( " UNIQUE" ); + if ( C::notNull::value ) + colStmt += QLatin1String( " NOT NULL" ); + } + if ( C::useDefault::value ) { + // FIXME: this is a huge over-simplification + // stringification of values depends on the type (e.g. quoting for strings) + // also, we probably want defaults different than the values created by the default ctor + const QVariant v = QVariant::fromValue( typename C::type() ); + colStmt += QLatin1Literal( " DEFAULT " ) % v.toString(); + } + colStmt += referenceStatement( typename C::hasForeignKey() ); + cols.push_back( colStmt ); + } + + /** + * Creates foreign key reference statement. + * @tparam C The column type. + */ + template QString referenceStatement( boost::mpl::false_ ) { return QString(); } + template QString referenceStatement( boost::mpl::true_ ) + { + QString refStmt = QLatin1Literal( " REFERENCES " ) + % C::referenced_column::table::tableName() + % QLatin1Literal( " (" ) + % C::referenced_column::sqlName() + % QLatin1Char( ')' ); + BOOST_MPL_ASSERT(( boost::mpl::not_ > )); + if ( C::onDeleteCascade::value ) + refStmt += QLatin1String(" ON DELETE CASCADE"); + else if ( C::onDeleteRestrict::value ) + refStmt += QLatin1String(" ON DELETE RESTRICT"); + return refStmt; + } + + QStringList &cols; +}; + +/** + * Helper class to create table constraint statements. + * @internal + */ +struct table_constraint_creator +{ + explicit table_constraint_creator( QStringList &statements ) : m_statements( statements ) {}; + + /** Create multi-column uniqueness constraints. + * @tparam T MPL sequence of unique columns. + * @todo Optional safety check: Check if all those columns are in the same table + */ + template + void operator() ( UniqueConstraint ) + { + QStringList cols; + sql_name_accumulator accu( cols ); + boost::mpl::for_each >( accu ); + m_statements.push_back( QLatin1Literal( "UNIQUE( " ) % cols.join( QLatin1String( ", " ) ) % QLatin1Literal( " )" ) ); + } + + QStringList &m_statements; +}; + +/** + * Helper class to create per column trigger statements, to disallow UPDATE on a column + * @internal + */ +struct column_trigger_creator +{ + explicit column_trigger_creator( QStringList &t ) : triggers( t ) {} + + /** + * Creates the triggers for columns + * @tparam C The column type + */ + template void operator()( wrap ) + { + + C::sqlName() % QLatin1Char( ' ' ) % sqlType( detail::wrap(), C::size ); + if ( C::onUserUpdateRestrict::value || C::primaryKey::value ) { + QString triggerName = C::name(); + triggerName.replace( QLatin1Char('.'), QLatin1Char('_') ); + QString stmt = QLatin1Literal("DROP TRIGGER IF EXISTS no_column_update_") % triggerName % + QLatin1Literal(" on ") % C::table::sqlName(); + triggers.push_back( stmt ); + stmt = QLatin1Literal("CREATE TRIGGER no_column_update_") % triggerName % + QLatin1Literal(" BEFORE UPDATE on ") % C::table::sqlName() % + QLatin1Literal(" FOR EACH ROW WHEN (OLD.") % C::sqlName() % + QLatin1Literal(" IS DISTINCT FROM NEW.") % C::sqlName() % + QLatin1Literal(" ) EXECUTE PROCEDURE is_administrator()"); + triggers.push_back( stmt ); + } + } + + QStringList &triggers; +}; + + +/** + * Helper class to create table creation statements. + * @internal + */ +struct table_creator +{ + explicit table_creator( QStringList & statements ) : m_statements( statements ) {}; + + /** + * Create the table creation statement for table @p T. + * @tparam T The table type. + */ + template + void operator() ( wrap ) + { + m_statements.push_back( Sql::createTableStatement() ); + } + + QStringList &m_statements; +}; + +/** + * Helper class to create table trigger statements. + * @internal + */ +struct table_trigger_creator +{ + explicit table_trigger_creator( QStringList & statements ) : m_statements( statements ) {}; + + /** + * Create the table creation statement for table @p T. + * @tparam T The table type. + */ + template + void operator() ( wrap ) + { + m_statements.append( Sql::createTableTriggerStatements() ); + } + + QStringList &m_statements; +}; +} // detail + + +/** + * Returns the CREATE TABLE SQL command for a given table. + * @internal For unit testing only + * @tparam T The table type. + */ +template +QString createTableStatement() +{ + QStringList cols; + detail::column_creator accu( cols ); + boost::mpl::for_each >( accu ); + + detail::table_constraint_creator accu2( cols ); + boost::mpl::for_each( accu2 ); + + return QLatin1Literal( "CREATE TABLE " ) % T::tableName() % QLatin1Literal( " (\n" ) % cols.join( QLatin1String( ",\n" ) ) % QLatin1Literal( "\n)" ); +} + +template +QString createTableStatement( const T & ) { return createTableStatement(); } + +/** + * Returns a list of CREATE TABLE statements for a list of tables. + * @internal For unit testing only + * @tparam Tables A MPL sequence of tables. + */ +template +QStringList createTableStatements() +{ + QStringList tabs; + detail::table_creator accu( tabs ); + boost::mpl::for_each >( accu ); + return accu.m_statements; +} + + +/** + * Create the trigger statements for a single table. + */ +template +QStringList createTableTriggerStatements() +{ + QStringList triggers; + detail::column_trigger_creator accu( triggers ); + boost::mpl::for_each >( accu ); + + return triggers; +} + +template +QStringList createTableTriggerStatements( const T & ) { return createTableTriggerStatements(); } + +/** + * Returns a list of DROP TRIGGER / CREATE TRIGGER statements for a list of tables. + * @tparam Tables A MPL sequence of tables. + */ +template +QStringList createTableTriggers() +{ + QStringList tabs; + detail::table_trigger_creator accu( tabs ); + boost::mpl::for_each >( accu ); + return accu.m_statements; +} + +/** + * Returns a list of table names in the given schema. + * @tparam Tables A MPL sequence of tables. + * @returns A list of table names in @p Tables + */ +template +QStringList tableNames() +{ + QStringList tableNames; + boost::mpl::for_each >( detail::sql_name_accumulator( tableNames ) ); + return tableNames; +} + +/** + * Creates all rules, permissions and triggers for the tables in @p Tables in the database @p db + * @tparam Tables A MPL squence of tables. + * @param db The database to create the tables in. + * @throws SqlException in case of a database error + */ +template +void createRulesPermissionsAndTriggers( const QSqlDatabase &db = QSqlDatabase::database() ) +{ + Sql::createNotificationRules( db ); + Sql::grantPermissions( db ); + foreach ( const QString &stmt, Sql::createTableTriggers() ) { + SqlQuery q( db ); + q.exec( stmt ); + } +} + + +/** + * Create all tables in @p Tables in the database @p db. + * @tparam Tables A MPL sequence of tables. + * @param db The database to create the tables in + * @throws SqlException in case of a database error + */ +template +void createTables( const QSqlDatabase &db ) +{ + foreach ( const QString &stmt, Sql::createTableStatements() ) { + SqlQuery q( db ); + q.exec( stmt ); + } + + Sql::createRulesPermissionsAndTriggers( db ); +} + +/** + * set the database version in the database + * @param The verison table to set + */ +template +void setVersion( const Table& table, const QSqlDatabase &db , int version) +{ + SqlInsertQueryBuilder qb( db ); + qb.setTable( table ); + qb.addColumnValue( table.version, version ); + qb.exec(); +} + + +/** + * Creates all tables in @p Tables in the database @p db if a tables with the same name does not already exist. + * @tparam Tables A MPL squence of tables. + * @param db The database to create the tables in. + * @throws SqlException in case of a database error + */ +template +void createMissingTables( const QSqlDatabase &db = QSqlDatabase::database() ) +{ + QStringList existingTables = db.tables(); + std::transform(existingTables.begin(), existingTables.end(), existingTables.begin(), boost::bind(&QString::toLower, _1)); + QStringList newTables = Sql::tableNames(); + std::transform(newTables.begin(), newTables.end(), newTables.begin(), boost::bind(&QString::toLower, _1)); + const QStringList newTableStmts = Sql::createTableStatements(); + Q_ASSERT( newTables.size() == newTableStmts.size() ); + for ( QStringList::const_iterator it = newTables.constBegin(), it2 = newTableStmts.constBegin(); it != newTables.constEnd(); ++it, ++it2 ) { + if ( existingTables.contains( *it ) ) + continue; + SqlQuery q( db ); + q.exec( *it2 ); + } +} + +} + +#endif diff --git a/SqlDeleteQueryBuilder.cpp b/SqlDeleteQueryBuilder.cpp new file mode 100644 index 0000000..b5d683a --- /dev/null +++ b/SqlDeleteQueryBuilder.cpp @@ -0,0 +1,57 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Andras Mantia +*/ +#include "SqlDeleteQueryBuilder.h" +#include "SqlExceptions.h" + +#include "Sql.h" + +#include + +SqlDeleteQueryBuilder::SqlDeleteQueryBuilder(const QSqlDatabase& db) : + SqlConditionalQueryBuilderBase( db ), + m_includeSubTables( true ) +{ +} + +void SqlDeleteQueryBuilder::setIncludeSubTables(bool includeSubTables) +{ + m_includeSubTables = includeSubTables; +} + +SqlQuery& SqlDeleteQueryBuilder::query() +{ + if ( !m_assembled ) { + QStringList cols; + m_queryString = QLatin1String( "DELETE FROM " ); + if ( !m_includeSubTables ) { + m_queryString += QLatin1String( "ONLY " ); + } + m_queryString += m_table; + + m_bindValues.clear(); + if ( m_whereCondition.hasSubConditions() ) { + m_queryString += QLatin1String( " WHERE " ); + m_queryString += conditionToString( m_whereCondition ); + } + + m_queryString = m_queryString.trimmed(); + m_assembled = true; + +#ifndef QUERYBUILDER_UNITTEST + m_query = prepareQuery( m_queryString ); + + for ( int i = 0; i < m_bindValues.size(); ++i ) { + const QVariant value = m_bindValues.at( i ); + // FIXME: similar code in the other query builders, we probably want that in the base class + if ( qstrcmp( value.typeName(), QMetaType::typeName( qMetaTypeId() ) ) == 0 ) + m_query.bindValue( QLatin1Char( ':' ) + QString::number( i ), value.value().toString() ); // Qt SQL drivers don't handle QUuid + else + m_query.bindValue( QLatin1Char( ':' ) + QString::number( i ), value ); + } +#endif + } + return m_query; +} + diff --git a/SqlDeleteQueryBuilder.h b/SqlDeleteQueryBuilder.h new file mode 100644 index 0000000..319cfb9 --- /dev/null +++ b/SqlDeleteQueryBuilder.h @@ -0,0 +1,36 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Andras Mantia +*/ +#ifndef SQLDELETEQUERYBUILDER_H +#define SQLDELETEQUERYBUILDER_H + +#include "SqlConditionalQueryBuilderBase.h" +#include "sqlate_export.h" + +/** API to create DELETE queries in a safe way from code, without the risk of + * introducing SQL injection vulnerabilities or typos. + */ +class SQLATE_EXPORT SqlDeleteQueryBuilder : public SqlConditionalQueryBuilderBase +{ +public: + /// Create a new query builder for the given database + explicit SqlDeleteQueryBuilder( const QSqlDatabase &db = QSqlDatabase::database() ); + + /** + * If @p includeSubTables is true all the tables inheriting from the specified + * table are deleted. If false, only the table is deleted using + * DELETE ONLY <tableName> + */ + void setIncludeSubTables( bool includeSubTables ); + + /// Returns the created query object, when called first, the query object is assembled and prepared + /// The method throws an SqlException if there is an error preparing the query. + SqlQuery& query(); + +private: + friend class DeleteQueryBuilderTest; + bool m_includeSubTables; +}; + +#endif diff --git a/SqlExceptions.h b/SqlExceptions.h new file mode 100644 index 0000000..c5f133c --- /dev/null +++ b/SqlExceptions.h @@ -0,0 +1,29 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Andras Mantia +*/ +#ifndef SQLEXCEPTIONS_H +#define SQLEXCEPTIONS_H + +#include "sqlate_export.h" + +#include +#include + + +class SQLATE_EXPORT SqlException : public std::exception { +public: + SqlException(const QSqlError& error) throw() : m_error( error ) {} + virtual ~SqlException() throw(){}; + + virtual const char* what() const throw() { + return m_error.text().toLatin1().constData(); + } + + QSqlError error() const {return m_error;} + +private: + QSqlError m_error; +}; + +#endif diff --git a/SqlGlobal.h b/SqlGlobal.h new file mode 100644 index 0000000..37c1906 --- /dev/null +++ b/SqlGlobal.h @@ -0,0 +1,26 @@ +#ifndef SQLATE_GLOBAL_H +#define SQLATE_GLOBAL_H + +#include +#include + +#include + +#define SQLATE_DEFAULT_SERVER_PORT 5432 + +#define SQLDEBUG qDebug() << QString::fromLatin1("%1:%2").arg(QLatin1String( __FILE__ )).arg(__LINE__ ) << QLatin1String( ": " ) + + +namespace Sql { + /** A memory-layout compatible version of QUuid that can be statically initialized. */ +struct StaticUuid { + uint data1; + ushort data2; + ushort data3; + uchar data4[8]; + operator const QUuid &() const { return *reinterpret_cast(this); } +}; + +BOOST_STATIC_ASSERT( sizeof(QUuid) == sizeof(StaticUuid) ); +} +#endif diff --git a/SqlGrantPermission.h b/SqlGrantPermission.h new file mode 100644 index 0000000..043ea7a --- /dev/null +++ b/SqlGrantPermission.h @@ -0,0 +1,88 @@ +/* +* Copyright (C) 2012-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Andras Mantia +*/ +#ifndef SQLGRANTPERMISSION_H +#define SQLGRANTPERMISSION_H + +#include "SqlInternals_p.h" +#include "SqlSchema_p.h" +#include "SqlQuery.h" + +#include +#include + +#include + +/** + * @file SqlGrantPermission.h + * Classes and functions to grant tables rights based on the schema definition in SqlSchema.h + */ + +namespace Sql { + +namespace detail { + +/** + * Permission statement creator. + * @internal + */ +struct permission_statement_creator { + permission_statement_creator( QStringList &stmts ) : m_stmts( stmts ) {} + template + void operator()( wrap ) { + QString stmt = QLatin1Literal("GRANT SELECT, INSERT, UPDATE, DELETE ON ") % T::tableName() % QLatin1Literal(" TO GROUP sqladmins"); + m_stmts.push_back( stmt ); + QStringList allowedOperations; + allowedOperations << QLatin1String("SELECT"); + if ( !T::is_restricted::value ) { + if ( T::delete_rows::value) { + allowedOperations << QLatin1String("DELETE"); + } + if ( T::update_rows::value) { + allowedOperations << QLatin1String("UPDATE"); + } + if ( T::insert_rows::value) { + allowedOperations << QLatin1String("INSERT"); + } + } + stmt = QLatin1Literal("GRANT ") % allowedOperations.join(QLatin1String(", ")) % QLatin1Literal(" ON ") % T::tableName() % QLatin1Literal(" TO GROUP sqlusers"); + m_stmts.push_back( stmt ); + + } + QStringList &m_stmts; +}; + +} + +/** + * Returns a list of GRANT ... ON ... TO ... statements for all mutable tables. + * @tparam Schema A MPL sequence of tables. + * @returns A list of SQL statements to give rights to the tables. + */ +template +QStringList createPermissionStatements() +{ + QStringList statements; + boost::mpl::for_each >( detail::permission_statement_creator( statements ) ); + return statements; +} + +/** + * Set up permissings for all tables from a Schema. + * @tparam Schema A MPL sequence of tables. + * @param db The database to create the tables in + * @throws SqlException in case of a database error + */ +template +void grantPermissions( const QSqlDatabase &db ) +{ + foreach ( const QString &stmt, Sql::createPermissionStatements() ) { + SqlQuery q( db ); + q.exec( stmt ); + } +} + +} + +#endif diff --git a/SqlGraphviz.h b/SqlGraphviz.h new file mode 100644 index 0000000..4f10db9 --- /dev/null +++ b/SqlGraphviz.h @@ -0,0 +1,150 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +*/ +#ifndef SQLGRAPHVIZ_H +#define SQLGRAPHVIZ_H + +#include "SqlSchema_p.h" +#include "SqlInternals_p.h" +#include "SqlCreateTable.h" + +#include +#include +#include +#include + +#include +#include +#include + +/** + * @file SqlGraphviz.h + * Dump the database schema into a dot file for visualization. + */ + +namespace Sql { + +namespace detail { + +/** + * Helper class to dump a single column to dot format. + * @internal + * @tparam C The column type + */ +struct column_dot_creator +{ + column_dot_creator( QStringList &c, QStringList &edges ) : cols( c ), m_edges( edges ) {} + + template void operator()( wrap ) + { + const QString colStmt = + QLatin1Literal( "" ) + % sqlType( wrap(), C::size ) + % QLatin1Literal( ": " ) + % C::sqlName() % QLatin1Literal( "" ); + foreignKeyEdge( typename C::hasForeignKey() ); + cols.push_back( colStmt ); + } + + /** + * Creates a graph edge for foreign key references. + * @tparam C The column type. + */ + template void foreignKeyEdge( boost::mpl::false_ ) {} + template void foreignKeyEdge( boost::mpl::true_ ) + { + QString edge = C::table::tableName() % QLatin1Literal( ":" ) % C::sqlName() + % QLatin1Literal( " -> " ) + % C::referenced_column::table::tableName() % QLatin1Literal( ":" ) % C::referenced_column::sqlName() + % QLatin1Literal( "[label=\"n:1\"];\n" ); + m_edges.push_back( edge ); + } + + QStringList &cols; + QStringList &m_edges; +}; + +/** + * Helper class to create table nodes in dot format + * @internal + * @tparam T The table type. + */ +struct table_dot_creator +{ + table_dot_creator( QStringList & nodes, QStringList &edges ) : m_nodes( nodes ), m_edges( edges ) {}; + + template + void operator() ( wrap ) + { + if ( T::is_lookup_table::value ) { + makeNode( QLatin1String( "lightyellow" ) ); + } else if ( !T::is_relation::value ) { + makeNode( QLatin1String( "lightsteelblue" ) ); + } + if ( T::is_relation::value && boost::mpl::size::value > 2 ) + makeNode( QLatin1String( "lightgray" ) ); + else + makeRelationEdge( typename T::is_relation() ); + } + + template + void makeNode( const QString &color ) + { + QStringList cols; + detail::column_dot_creator accu( cols, m_edges ); + boost::mpl::for_each >( accu ); + + m_nodes.push_back( + T::tableName() + % QLatin1Literal( "[label=<" ) + % QLatin1Literal( "" ) % cols.join( QLatin1String( " " ) ) % QLatin1Literal( "
" ) + % T::tableName() + % QLatin1Literal( "
>];\n" ) + ); + } + + template + void makeRelationEdge( boost::mpl::false_ ) {} + template + void makeRelationEdge( boost::mpl::true_ ) + { + m_edges.push_back( T::leftType::referenced_column::table::tableName() % QLatin1Literal( ":" ) % T::leftType::referenced_column::sqlName() + % QLatin1Literal( " -> " ) + % T::rightType::referenced_column::table::tableName() % QLatin1Literal( ":" ) % T::rightType::referenced_column::sqlName() + % QLatin1Literal( "[label=\"n:m\" dir=both arrowtail=normal];\n" ) + ); + } + + QStringList &m_nodes; + QStringList &m_edges; +}; + +} // detail + + +/** + * Returns a dot representation of the database schema + * @tparam Tables A MPL sequence of tables. + */ +template +QString schemaToDot() +{ + QStringList nodes; + QStringList edges; + detail::table_dot_creator accu( nodes, edges ); + boost::mpl::for_each >( accu ); + return QLatin1Literal( "digraph \"Database Schema\" {\n" + "graph [rankdir=\"LR\" fontsize=\"10\"]\n" + "node [fontsize=\"10\" shape=\"plaintext\"]\n" + "edge [fontsize=\"10\"]\n" ) % + nodes.join( QLatin1String( "\n" ) ) % + edges.join( QLatin1String( "\n" ) ) % + QLatin1Literal( "}\n" ); +} + +} + +#endif diff --git a/SqlInsertQueryBuilder.cpp b/SqlInsertQueryBuilder.cpp new file mode 100644 index 0000000..223eda3 --- /dev/null +++ b/SqlInsertQueryBuilder.cpp @@ -0,0 +1,89 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Andras Mantia +* Volker Krause +*/ +#include "SqlInsertQueryBuilder.h" + +#include "SqlExceptions.h" +#include "Sql.h" + +SqlInsertQueryBuilder::SqlInsertQueryBuilder(const QSqlDatabase& db) : + SqlQueryBuilderBase( db ) +{ +} + +void SqlInsertQueryBuilder::addColumnValue(const QString& columnName, const QVariant& value) +{ + m_columns.push_back( qMakePair( columnName, value ) ); +} + +void SqlInsertQueryBuilder::addAllValues(const QVector< QVariant >& values) +{ + Q_FOREACH( const QVariant &value, values ) { + addColumnValue( QLatin1String( "" ), value ); + } +} + +void SqlInsertQueryBuilder::addDefaultValues() +{ + m_columns.clear(); +} + + +SqlQuery& SqlInsertQueryBuilder::query() +{ + if ( !m_assembled ) { + typedef QPair ColumnValuePair; + m_columnNames.clear(); + m_values.clear(); + foreach ( const ColumnValuePair &col, m_columns ) { + m_columnNames.push_back( col.first ); + m_values.push_back( col.second ); + } + + m_queryString = QLatin1String( "INSERT INTO " ); + m_queryString += m_table; +#ifndef QUERYBUILDER_UNITTEST + bool bindValues = false; +#endif + if ( m_columns.isEmpty() ) { //default values + m_queryString += QLatin1String(" DEFAULT VALUES"); + } else { + if ( !m_columnNames.join( QLatin1String("") ).trimmed().isEmpty() ) { //columns specified + m_queryString += QLatin1String( " (" ); + Q_FOREACH( const QString& column, m_columnNames ) { + m_queryString += column + QLatin1String( "," ); + } + m_queryString[ m_queryString.length() -1 ] = QLatin1Char(')'); + } + + m_queryString += QLatin1String( " VALUES (" ); + for (int i = 0; i < m_values.size(); ++i ) { + if (m_values.at(i).userType() == qMetaTypeId()) + m_queryString += currentDateTime() % QLatin1Char(','); + else + m_queryString += QString::fromLatin1( ":%1," ).arg(i); + } + m_queryString[ m_queryString.length() -1 ] = QLatin1Char(')'); +#ifndef QUERYBUILDER_UNITTEST + bindValues = true; +#endif + } + + m_assembled = true; + +#ifndef QUERYBUILDER_UNITTEST + m_query = prepareQuery( m_queryString ); + + if ( bindValues ) { + for ( int i = 0; i < m_values.size(); ++i ) { + const QVariant value = m_values.at( i ); + bindValue( i, value ); + } + } +#endif + } + return m_query; +} + diff --git a/SqlInsertQueryBuilder.h b/SqlInsertQueryBuilder.h new file mode 100644 index 0000000..ccb73c7 --- /dev/null +++ b/SqlInsertQueryBuilder.h @@ -0,0 +1,69 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Andras Mantia +* Volker Krause +*/ +#ifndef SQLINSERTQUERYBUILDER_H +#define SQLINSERTQUERYBUILDER_H + +#include "sqlate_export.h" +#include "SqlQueryBuilderBase.h" +#include "SqlInternals_p.h" + +#include +#include + +#include +#include +#include + +/** API to create INSERT queries in a safe way from code, without the risk of + * introducing SQL injection vulnerabilities or typos. + */ +class SQLATE_EXPORT SqlInsertQueryBuilder : public SqlQueryBuilderBase +{ +public: + + /// Create a new query builder for the given database + explicit SqlInsertQueryBuilder( const QSqlDatabase &db = QSqlDatabase::database() ); + + /// INSERT INTO table ( @p columnName , ... ) VALUES( @p value ) + void addColumnValue( const QString &columnName, const QVariant &value ); + + template + void addColumnValue( const Column &, const typename Column::type &value ) + { + Sql::warning, UsageOfClientSideTime>::print(); + addColumnValue( Column::sqlName(), QVariant::fromValue( value ) ); + } + template + void addColumnValue( const Column &, SqlNullType ) + { + BOOST_MPL_ASSERT(( boost::mpl::not_ )); + addColumnValue( Column::sqlName(), QVariant() ); + } + template + void addColumnValue( const Column &, SqlNowType now ) + { + BOOST_MPL_ASSERT(( boost::is_same )); + addColumnValue( Column::sqlName(), QVariant::fromValue(now) ); + } + + /// INSERT INTO table VALUES ( @p values )... + void addAllValues( const QVector &values ); + + /// INSERT INTO ... DEFAULT VALUES + void addDefaultValues(); + + /// Returns the created query object, when called first, the query object is assembled and prepared + SqlQuery& query(); + +private: + friend class InsertQueryBuilderTest; + QVector > m_columns; + + QStringList m_columnNames; //holds the column names, used for unit testing + QVector m_values; //holds the inserted values, used for unit testing +}; + +#endif diff --git a/SqlInternals_p.h b/SqlInternals_p.h new file mode 100644 index 0000000..4aefa8f --- /dev/null +++ b/SqlInternals_p.h @@ -0,0 +1,84 @@ +/* +* Copyright (C) 2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +*/ +#ifndef SQLINTERNALS_P_H +#define SQLINTERNALS_P_H + +#include +#include +#include + +/** + * @file SqlInternals_p.h + * Shared internals between the various Sql related templates. + */ + +namespace Sql { + +namespace detail { + +/** + * type wrapper to avoid instantiation of concrete types + * @tparam T The type to wrap + * @internal + */ +template struct wrap {}; + +/** Empty type for eg. not yet specified parts of a query. */ +struct missing {}; + +} + +/** + * Metafunctions for concatenating two MPL vectors. + * @tparam V1 First vector + * @tparam V2 Second vector + * @returns V1 + V2 + */ +template +struct append : boost::mpl::fold< + V2, + V1, + boost::mpl::push_back +> +{}; + + +namespace detail { + +/** + * Implementation details for the conditional compiler warning generator template below. + * @internal + */ +template +struct warning_helper { + typedef unsigned int signed_or_unsigned_int; +}; +template <> +struct warning_helper { + typedef signed int signed_or_unsigned_int; +}; + +} + +/** + * Triggers a conditional compiler warning. + * @tparam Condition A MPL boolean meta-type, the warning if generated when evaluating to @c true. + * @tparam Message A message to be included in the output. Needs to be a defined symbol. + * @note requires sign comparison warnings being enabled. + */ +template +struct warning { + static inline void print() { + const typename detail::warning_helper::signed_or_unsigned_int a = 0; + const unsigned int b = 0; + const bool c = (a != b); + (void) c; + } +}; + + +} + +#endif diff --git a/SqlMonitor.cpp b/SqlMonitor.cpp new file mode 100644 index 0000000..4617d02 --- /dev/null +++ b/SqlMonitor.cpp @@ -0,0 +1,72 @@ +/* +* Copyright (C) 2011 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Andras Mantia +*/ + +#include "SqlMonitor.h" +#include "SqlQueryManager.h" + + +#include +#include + +#include + +SqlMonitor::SqlMonitor( const QSqlDatabase& db, QObject *parent ): QObject(parent), m_db(db) +{ + connect( db.driver(), SIGNAL(notification(QString)), SLOT(notificationReceived(QString)) ); + SqlQueryManager::instance()->registerMonitor(this); +} + +SqlMonitor::~SqlMonitor() +{ + SqlQueryManager::instance()->unregisterMonitor(this); +} + + +void SqlMonitor::setMonitorTables( const QStringList& tables ) +{ + foreach( const QString &table, tables ) { + const QString notification = table.toLower() % QLatin1Literal( "changed" ); + if ( m_tables.contains( notification ) ) + continue; + m_tables.push_back( notification ); + subscribe(notification); + } +} + +bool SqlMonitor::subscribe( const QString& notification ) +{ + if ( m_db.driver()->subscribedToNotifications().contains( notification ) ) + return false; + m_db.driver()->subscribeToNotification( notification ); + return true; +} + +void SqlMonitor::notificationReceived( const QString& notification ) +{ + if ( m_tables.contains(notification.toLower()) ) + emit tablesChanged(); + else if ( m_monitoredValues.contains(notification) ) + emit notify(notification); +} + +void SqlMonitor::resubscribe() +{ + foreach( const QString ¬ification, m_tables ) { + m_db.driver()->subscribeToNotification( notification ); + } + foreach( const QString ¬ification, m_monitoredValues ) { + m_db.driver()->subscribeToNotification( notification ); + } +} + +void SqlMonitor::unsubscribeValuesNotifications() +{ + foreach( const QString ¬ification, m_monitoredValues ) { + m_db.driver()->unsubscribeFromNotification( notification ); + } + m_monitoredValues.clear(); +} + +#include "moc_SqlMonitor.cpp" diff --git a/SqlMonitor.h b/SqlMonitor.h new file mode 100644 index 0000000..eb3087d --- /dev/null +++ b/SqlMonitor.h @@ -0,0 +1,125 @@ +/* +* Copyright (C) 2011 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Andras Mantia +*/ + +#ifndef SQL_MONITOR_H +#define SQL_MONITOR_H + +#include +#include +#include + +#include "sqlate_export.h" +#include "SqlUtils.h" + +#include + +/** + * Helper class for monitoring a set up tables for changes. + */ +class SQLATE_EXPORT SqlMonitor : public QObject +{ + Q_OBJECT +public: + /** + * Creates a table monitor for the given database. + * @param db The database on which we want to monitor tables. + * @param parent The parent object. + */ + explicit SqlMonitor( const QSqlDatabase& db, QObject* parent = 0 ); + + virtual ~SqlMonitor(); + + /** + * Set a list of table names to monitor. + * @param tables List of table names. + */ + void setMonitorTables( const QStringList &tables ); + + + /** + * Monitor a row and emits a signal carrying the content of a selected column when an UPDATE is performed on this row. + * @note you need to declare the column as "notification ready" in the SQL schema, using the flag "Notify" + * + * @param table the table in which we want the row to be monitored + * @param column the monitored column + * + */ + template + bool addValueMonitor( const T&, const QVariant& value) + { + QString processedValue = value.toString(); + //remove braces in case of an uuid + if(processedValue.contains(QLatin1Char('{')) && processedValue.contains(QLatin1Char('}'))) + { + processedValue.remove(QLatin1Char('{')); + processedValue.remove(QLatin1Char('}')); + } + + const QString identifier = SqlUtils::createIdentifier(T::table::sqlName() + QLatin1Char( '_' ) + T::sqlName()); + const QString notification = QString::fromLatin1("%1_%2") + .arg(processedValue) + .arg(identifier); + m_monitoredValues << notification; //maybe it's already registered in another instance of the monitor, just add it in the list in this case + if(subscribe(notification)) + { + return true; + } + else return false; + } + + /** + * @brief Subscribes again to the monitored notifications. Used after an unexpected database disconnection. + **/ + void resubscribe(); + + /** + * @brief Unsubscribe from all "values" notifications + **/ + void unsubscribeValuesNotifications(); + + QSqlDatabase database() const { return m_db; } + + QStringList monitoredTables() const { return m_tables; } + QStringList monitoredValues() const { return m_monitoredValues; } + + +#ifndef SQL_MONITOR_MAX_SIZE +#define SQL_MONITOR_MAX_SIZE 15 +#endif + +#define CONST_REF(z, n, unused) const T ## n & +#define TABLE_NAME(z, n, unused) << T ## n ::tableName() +#define MONITOR_IMPL(z, n, unused) \ + template \ + void setMonitorTables( BOOST_PP_ENUM(n, CONST_REF, ~) ) { \ + setMonitorTables( QStringList() BOOST_PP_REPEAT(n, TABLE_NAME, ~) ); \ + } +BOOST_PP_REPEAT_FROM_TO(1, SQL_MONITOR_MAX_SIZE, MONITOR_IMPL, ~) + +#undef MONITOR_IMPL +#undef CONST_REF +#undef TABLE_NAME + + +Q_SIGNALS: + /** + * Emitted when any of the monitored tables changed. + */ + void tablesChanged(); + + void notify( const QString ¬ification ); + +private Q_SLOTS: + void notificationReceived( const QString ¬ification ); + +private: + bool subscribe( const QString& notification ); + + QSqlDatabase m_db; + QStringList m_tables; + QStringList m_monitoredValues; +}; + +#endif diff --git a/SqlQuery.cpp b/SqlQuery.cpp new file mode 100644 index 0000000..861b3af --- /dev/null +++ b/SqlQuery.cpp @@ -0,0 +1,141 @@ +#include "SqlQuery.h" +#include "SqlExceptions.h" +#include "SqlQueryManager.h" + +#ifdef SQLATE_ENABLE_NETWORK_WATCHER +#include "SqlQueryWatcher.h" +#include "kdthreadrunner.h" +#endif + +#include +#include +#include + +SqlQuery::SqlQuery(const QString &query /*= QString()*/, const QSqlDatabase& db /*= QSqlDatabase()*/ ) : + QSqlQuery( db ), m_db( db ) +{ + m_connectionName = db.connectionName(); + + SqlQueryManager::instance()->registerQuery(this); + if ( !query.isEmpty() ) + exec( query ); +} + +SqlQuery::SqlQuery(const QSqlDatabase& db) : + QSqlQuery( db ), m_db(db) +{ + m_connectionName = db.connectionName(); + SqlQueryManager::instance()->registerQuery(this); +} + +SqlQuery::SqlQuery(const SqlQuery &other ) : + QSqlQuery( other ), m_db(other.m_db) +{ + m_connectionName = m_db.connectionName(); + SqlQueryManager::instance()->registerQuery(this); +} + +SqlQuery::~SqlQuery() +{ + SqlQueryManager::instance()->unregisterQuery(this); +} + +SqlQuery& SqlQuery::operator=(const SqlQuery& other) +{ + QSqlQuery::operator=(other); + m_db = other.m_db; + m_connectionName = m_db.connectionName(); +//Debug line that helps finding leaking queries. It is intentionally not a SQLDEBUG. +// qDebug() << "operator= " << lastQuery() << this; + return *this; +} + +void SqlQuery::exec() +{ + SqlQueryManager::instance()->checkDbIsAlive(m_db); + +#ifdef SQLATE_ENABLE_NETWORK_WATCHER + KDThreadRunner watcher; + SqlQueryWatcherHelper* helper = watcher.startThread(); +#endif + + bool result = QSqlQuery::exec(); + +#ifdef SQLATE_ENABLE_NETWORK_WATCHER + QMetaObject::invokeMethod( helper, "quit", Qt::QueuedConnection ); + watcher.wait(); +#endif + + if (!result && ( !m_db.isOpen() || !m_db.isValid() ) ) { + SqlQueryManager::instance()->checkDbIsAlive(m_db); //double check is needed, as Qt might not set m_db.isOpen() to false after connection loss if no queries were run meantime. + result = QSqlQuery::exec(); + } + + if ( !result ) { + qWarning() << Q_FUNC_INFO << "Exec failed: " << this << QSqlQuery::lastError() << " query was: " << QSqlQuery::lastQuery() + << ", executed query: " << QSqlQuery::executedQuery() << " bound values: "<< QSqlQuery::boundValues().values(); +// qWarning() << "Database status: " << m_db.isOpen() << m_db.isValid() << m_db.isOpenError(); + throw SqlException( QSqlQuery::lastError() ); + } +} + +void SqlQuery::exec(const QString& query) +{ + SqlQueryManager::instance()->checkDbIsAlive(m_db); + +#ifdef SQLATE_ENABLE_NETWORK_WATCHER + KDThreadRunner watcher; + SqlQueryWatcherHelper* helper = watcher.startThread(); +#endif + + bool result = QSqlQuery::exec( query ); + if (!result && ( !m_db.isOpen() || !m_db.isValid() ) ) { + SqlQueryManager::instance()->checkDbIsAlive(m_db); //double check is needed, as Qt might not set m_db.isOpen() to false after connection loss if no queries were run meantime. + result = QSqlQuery::exec(); + } + +#ifdef SQLATE_ENABLE_NETWORK_WATCHER + QMetaObject::invokeMethod( helper, "quit", Qt::QueuedConnection ); + watcher.wait(); +#endif + + if ( !result ) { + qWarning() << Q_FUNC_INFO << "Exec(query) failed: " << this << QSqlQuery::lastError() << " query was: " << QSqlQuery::lastQuery() + << ", executed query: " << QSqlQuery::executedQuery() << " bound values: "<< QSqlQuery::boundValues().values(); +// qWarning() << "Database status: " << m_db.isOpen() << m_db.isValid() << m_db.isOpenError(); + throw SqlException( QSqlQuery::lastError() ); + } +} + +void SqlQuery::prepare(const QString& query) +{ + SqlQueryManager::instance()->checkDbIsAlive(m_db); + +#ifdef SQLATE_ENABLE_NETWORK_WATCHER + KDThreadRunner watcher; + SqlQueryWatcherHelper* helper = watcher.startThread(); +#endif + + bool result = QSqlQuery::prepare( query ); + if (!result && ( !m_db.isOpen() || !m_db.isValid() ) ) { + SqlQueryManager::instance()->checkDbIsAlive(m_db); //double check is needed, as Qt might not set m_db.isOpen() to false after connection loss if no queries were run meantime. + result = QSqlQuery::prepare( query ); + } + +#ifdef SQLATE_ENABLE_NETWORK_WATCHER + QMetaObject::invokeMethod( helper, "quit", Qt::QueuedConnection ); + watcher.wait(); +#endif + + if ( !result ) { + qWarning() << Q_FUNC_INFO << "Prepare failed: " << this << QSqlQuery::lastError() << " query was: " << QSqlQuery::lastQuery() + << ", executed query: " << QSqlQuery::executedQuery() << " bound values: "<< QSqlQuery::boundValues().values(); +// qWarning() << "Database status: " << m_db.isOpen() << m_db.isValid() << m_db.isOpenError(); + throw SqlException( QSqlQuery::lastError() ); + } +} + +bool SqlQuery::prepareWithoutCheck(const QString& query) +{ + return QSqlQuery::prepare(query); +} diff --git a/SqlQuery.h b/SqlQuery.h new file mode 100644 index 0000000..4f6856a --- /dev/null +++ b/SqlQuery.h @@ -0,0 +1,58 @@ +/* +* Copyright (C) 2011 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +*/ + +/*! + * This is the class that should be used in place of all QSqlQuery + * This was we can keep a check on all queries and authenticate + * appropriately. + * + * WARNING: Do NOT cast a SqlQuery to QSqlQuery and call + * prepare or exec on it, as it will not call the SqlQuery variants! + */ + +#ifndef SQLQUERY_H +#define SQLQUERY_H + +#include "sqlate_export.h" + +#include + +class SQLATE_EXPORT SqlQuery : public QSqlQuery +{ +public: + /// @todo Really we should be abstracting all this + // So this class decides which database to use, i.e. the lookup or main etc + // When we implement this, move these members to private to pick up where + // they are used + SqlQuery (const QString &query = QString(), const QSqlDatabase& db = QSqlDatabase::database() ); + SqlQuery (const QSqlDatabase& db ); + SqlQuery (const SqlQuery & other ); + + ~SqlQuery(); + + //NOTE: not that nice they overload non-virtual methods, so beware when casting a SqlQuery to QSqlQuery + // In short: do not cast. + void exec(); + void exec( const QString &query ); + void prepare( const QString &query ); + + /** + * @brief Prepare the query without checking if the connection to the database is alive. + * This should not be called from anywhere, but the SqlQueryManager::checkDbIsAlive to avoid + * multiple checking. + * @param query the query to prepare + * @return bool true if succeed, false if there was some error. See QSqlQuery::prepare. + **/ + bool prepareWithoutCheck( const QString &query ); + + QString connectionName() const { return m_connectionName; } + + SqlQuery& operator=(const SqlQuery& other); + +private: + QSqlDatabase m_db; + QString m_connectionName; +}; + +#endif diff --git a/SqlQueryBuilderBase.cpp b/SqlQueryBuilderBase.cpp new file mode 100644 index 0000000..726ac9c --- /dev/null +++ b/SqlQueryBuilderBase.cpp @@ -0,0 +1,74 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +*/ +#include "SqlQueryBuilderBase.h" + +#include "SqlExceptions.h" +#include "SqlSchema.h" +#include "SqlCondition.h" +#include "SqlQueryCache.h" + +SqlQueryBuilderBase::SqlQueryBuilderBase(const QSqlDatabase& db) : + m_db( db ), + m_query( db ), + m_assembled( false ) +{ +} + + + +SqlQueryBuilderBase::~SqlQueryBuilderBase() +{ +} + + +void SqlQueryBuilderBase::setTable(const QString& tableName) +{ + m_table = tableName; +} + +void SqlQueryBuilderBase::invalidateQuery() +{ + m_assembled = false; +} + +void SqlQueryBuilderBase::exec() +{ + query().exec(); +} + +void SqlQueryBuilderBase::bindValue(int placeholderIndex, const QVariant& value) +{ + const QString placeholder = QLatin1Char(':') + QString::number( placeholderIndex ); + + if (value.userType() == qMetaTypeId()) { + // Qt SQL drivers don't handle QUuid + m_query.bindValue( placeholder, value.value().toString() ); + } + + else if (value.userType() == qMetaTypeId()) { + // don't create any bindings for the SqlNow dummytype, it has been handled when assembling the query string already + } + + else { + m_query.bindValue( placeholder, value ); + } +} + +QString SqlQueryBuilderBase::currentDateTime() const +{ + return QLatin1String("now()"); +} + +SqlQuery SqlQueryBuilderBase::prepareQuery(const QString& sqlStatement) +{ + if (SqlQueryCache::contains(m_db.connectionName(), sqlStatement)) { + return SqlQueryCache::query(m_db.connectionName(), sqlStatement); + } + + SqlQuery q( m_db ); + q.prepare( sqlStatement ); + SqlQueryCache::insert(m_db.connectionName(), sqlStatement, q); + return q; +} diff --git a/SqlQueryBuilderBase.h b/SqlQueryBuilderBase.h new file mode 100644 index 0000000..6deae18 --- /dev/null +++ b/SqlQueryBuilderBase.h @@ -0,0 +1,69 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +*/ +#ifndef SQLQUERYBUILDERBASE_H +#define SQLQUERYBUILDERBASE_H +#include "SqlQuery.h" + +#include "SqlCondition.h" +#include "sqlate_export.h" +#include "SqlGlobal.h" + +/** Abstract base class for SQL query builders. All builders should inherit from this class. + */ +class SQLATE_EXPORT SqlQueryBuilderBase +{ +public: + /// Create a new query builder for the given database + explicit SqlQueryBuilderBase( const QSqlDatabase &db = QSqlDatabase::database() ); + virtual ~SqlQueryBuilderBase(); + /// ... FROM @p tableName ... + void setTable( const QString &tableName ); + void setTable( const QLatin1String &tableName ) { setTable( QString( tableName ) ); } + template + void setTable( const Table & = Table() ) + { + setTable( Table::sqlName() ); + } + + /// Creates the query object and executes the query. The method throws an SqlException on error. + virtual void exec(); + + /// Returns the created query object, when called first, the query object is assembled and prepared + /// Subclasses must implement this method. The method throws an SqlException if there is an error preparing + /// the query. + virtual SqlQuery& query() = 0; + + /// Resets the internal status to "not assembled", meaning the query() call will assemble the query again. + /// This makes possible to modify an already existing builder object after query() was used. + void invalidateQuery(); + +protected: + /** Binds @p value to placeholder @p placeholderIndex in m_query. + * Unlike the similar methods in QSqlQuery this also applies transformations to handle types not supported + * by QtSQL, such as UUID. + * @note This assumes the use of named bindings in the form ":<index>". + */ + void bindValue( int placeholderIndex, const QVariant &value ); + + /** Returns the SQL expression returning the current date/time on the server depending on the used database backend. */ + QString currentDateTime() const; + + /** Returns a prepared query for the given @p sqlStatement. + * Unlike calling SqlQuery::prepare() manually, this will re-use cached prepared queries. + * @throw SqlException if query preparation failed + */ + SqlQuery prepareQuery( const QString &sqlStatement ); + +protected: + friend class SelectQueryBuilderTest; + friend class SelectTest; + QSqlDatabase m_db; + QString m_table; + SqlQuery m_query; + QString m_queryString; // hold the assembled query string, used for unit testing + bool m_assembled; +}; + +#endif diff --git a/SqlQueryCache.cpp b/SqlQueryCache.cpp new file mode 100644 index 0000000..6ee2818 --- /dev/null +++ b/SqlQueryCache.cpp @@ -0,0 +1,40 @@ +/* +* Copyright (C) 2012-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +*/ +#include "SqlQueryCache.h" + +#include "SqlQuery.h" +#include + +static QHash > g_queryCache; +static bool g_queryCacheEnabled = true; + +bool SqlQueryCache::contains(const QString &dbConnectionName, const QString& queryStatement) +{ + if (!g_queryCacheEnabled) + return false; + return g_queryCache.contains(dbConnectionName) && g_queryCache.value(dbConnectionName).contains(queryStatement); +} + +SqlQuery SqlQueryCache::query(const QString &dbConnectionName, const QString& queryStatement) +{ + return g_queryCache.value(dbConnectionName).value(queryStatement); +} + +void SqlQueryCache::insert(const QString &dbConnectionName, const QString& queryStatement, const SqlQuery& query) +{ + if (g_queryCacheEnabled) + g_queryCache[dbConnectionName].insert(queryStatement, query); +} + +void SqlQueryCache::clear() +{ + g_queryCache.clear(); +} + +void SqlQueryCache::setEnabled(bool enable) +{ + g_queryCacheEnabled = enable; + clear(); +} diff --git a/SqlQueryCache.h b/SqlQueryCache.h new file mode 100644 index 0000000..37a0e47 --- /dev/null +++ b/SqlQueryCache.h @@ -0,0 +1,34 @@ +/* +* Copyright (C) 2012-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +*/ +#ifndef SQLQUERYCACHE_H +#define SQLQUERYCACHE_H + +#include "sqlate_export.h" + +class QString; +class SqlQuery; + +/** + * A per-connection cache prepared query cache. + */ +namespace SqlQueryCache +{ + /// Check whether the query @p queryStatement is cached already. + SQLATE_EXPORT bool contains( const QString& dbConnectionName, const QString& queryStatement ); + + /// Returns the cached (and prepared) query for @p queryStatement. + SQLATE_EXPORT SqlQuery query( const QString& dbConnectionName, const QString& queryStatement ); + + /// Insert @p query into the cache for @p queryStatement. + SQLATE_EXPORT void insert( const QString& dbConnectionName, const QString& queryStatement, const SqlQuery& query ); + + /// Clears the cache, must be called whenever the corresponding database connection is dropped. + SQLATE_EXPORT void clear(); + + /// Enables/disables the query cache. This can be used to temporarily disable caching while changing the db layout. + SQLATE_EXPORT void setEnabled( bool enable ); +} + +#endif diff --git a/SqlQueryManager.cpp b/SqlQueryManager.cpp new file mode 100644 index 0000000..13ff116 --- /dev/null +++ b/SqlQueryManager.cpp @@ -0,0 +1,172 @@ +/* +* Copyright (C) 2011 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Andras Mantia +*/ + +#include "SqlQueryManager.h" +#include "SqlQuery.h" +#include "SqlMonitor.h" +#include "SqlQueryCache.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +Q_GLOBAL_STATIC(QMutex, queryMutex) +Q_GLOBAL_STATIC(QMutex, monitorMutex) + +SqlQueryManager* SqlQueryManager::s_instance = 0; + +SqlQueryManager* SqlQueryManager::instance() +{ + if (!s_instance) + s_instance = new SqlQueryManager(); + return s_instance; +} + + +SqlQueryManager::SqlQueryManager() +{ +} + +SqlQueryManager::~SqlQueryManager() +{ + s_instance = 0; +} + +void SqlQueryManager::registerQuery(SqlQuery* query) +{ + QMutexLocker locker( queryMutex() ); + m_queries.append(query); +//Debug line that helps finding leaking queries. It is intentionally not a SQLDEBUG. +// qDebug() << "Registered query: " << query->lastQuery() << query; +} + +void SqlQueryManager::unregisterQuery(SqlQuery* query) +{ + QMutexLocker locker( queryMutex() ); + m_queries.removeAll(query); +//Debug line that helps finding leaking queries. It is intentionally not a SQLDEBUG. +// qDebug() << "Unregistered query: " << query->lastQuery() << query; +} + +void SqlQueryManager::registerMonitor(SqlMonitor* monitor) +{ + QMutexLocker locker( monitorMutex() ); + m_monitors.append(monitor); +} + +void SqlQueryManager::unregisterMonitor(SqlMonitor* monitor) +{ + QMutexLocker locker( monitorMutex() ); + m_monitors.removeAll(monitor); +} + +int SqlQueryManager::queryCount() const +{ + QMutexLocker locker( queryMutex() ); + return m_queries.size(); +} + +void SqlQueryManager::printQueries() const +{ + QMutexLocker locker( queryMutex() ); + //The debug lines are intentionally qDebug and not SQLDEBUG + qDebug() << "Live queries: "; + Q_FOREACH(SqlQuery *query, m_queries) { + qDebug() << query->lastQuery(); + } +} + +int SqlQueryManager::monitorCount() const +{ + QMutexLocker locker( monitorMutex() ); + return m_monitors.size(); +} + +void SqlQueryManager::printMonitors() const +{ + QMutexLocker locker( monitorMutex() ); + //The debug lines are intentionally qDebug and not SQLDEBUG + qDebug() << "Active monitors: "; + Q_FOREACH(SqlMonitor *monitor, m_monitors) { + qDebug() << "Tables: " << monitor->monitoredTables() << " Values: " << monitor->monitoredValues(); + } +} + + +void SqlQueryManager::checkDbIsAlive(QSqlDatabase& db) +{ + QMutexLocker queryLocker( queryMutex() ); + QMutexLocker monitorLocker( monitorMutex() ); + +// qDebug() << Q_FUNC_INFO << db.isOpen() << db.isValid() << ( !db.isOpen() || !db.isValid() ); + + //store all the queries and their bound values + QMap > allBoundValues; + Q_FOREACH(SqlQuery* q, m_queries) { + if (q->connectionName() != db.connectionName() ) + continue; + allBoundValues[q] = q->boundValues(); + } + int retryCount = 0; + while ( !db.isOpen() || !db.isValid() ) { + if ( QThread::currentThread() == QCoreApplication::instance()->thread() ) { +#if 0 + if ( QMessageBox::question( 0, QObject::tr("Database connection error"), + QObject::tr("The database is not available.

Press Retry to try again or Abort to terminate the application.
\ + Unsaved changes are lost if you Abort.\ +

If the problem persist, please inform your system administrator.
"), QMessageBox::Retry | QMessageBox::Abort, QMessageBox::Retry ) == QMessageBox::Abort ) { + ::exit(1) ; + } +#endif + ::exit(1); + } + retryCount++; + if (retryCount > 10 ) { //give up at some point + return; + } + + if ( db.open() ) { + SqlQueryCache::clear(); // reconnected, so all cached queries are now invalid + //if the connection was dropped and recreated all the prepared queries are invalid, so we need to reconstruct them + //using the lastQuery() string and the saved bound values + Q_FOREACH(SqlQuery* q, m_queries ) { + if (q->connectionName() != db.connectionName() ) + continue; + if ( !q->lastQuery().isEmpty() ) { + QString str = q->lastQuery(); +// qDebug() << "Prepare query again: " << q << str; + q->prepareWithoutCheck( str ); //TODO: this generates a runtime warning as the old stored query (prepare) cannot be cleaned up as it got lost. I have no idea what to do. + QMap boundValues = allBoundValues[q]; + QMap::const_iterator it; + for (it = boundValues.constBegin(); it != boundValues.constEnd(); ++it) { + q->bindValue( it.key(), it.value() ); + } + } + } + + //unsubscribe from all the notifications. Important to do it here for all notifications of the db, as + //the QPSQL implementation will not recreate the QSocketNotifier otherwise and we will not get any notifications + QStringList notifications = db.driver()->subscribedToNotifications(); + Q_FOREACH(QString notification, notifications) { + db.driver()->unsubscribeFromNotification(notification); + } + //subscribe again to the db notifications + Q_FOREACH(SqlMonitor* monitor, m_monitors ) { + if (monitor->database().connectionName() != db.connectionName() ) + continue; + monitor->resubscribe(); + } + } + } +} + + + + diff --git a/SqlQueryManager.h b/SqlQueryManager.h new file mode 100644 index 0000000..70c167c --- /dev/null +++ b/SqlQueryManager.h @@ -0,0 +1,66 @@ +/* +* Copyright (C) 2011 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Andras Mantia +*/ + +#ifndef SQLQUERYMANAGER_H +#define SQLQUERYMANAGER_H +#include "sqlate_export.h" + +#include + +class SqlMonitor; +class QSqlDatabase; +class SqlQuery; + +/** + * @brief Keeps track of queries and handles unexpected database disconnections. + **/ +class SQLATE_EXPORT SqlQueryManager +{ + +public: + /** + * @brief Get an instance to the singleton object. + * + * @return SqlQueryManager* + **/ + static SqlQueryManager* instance(); + + /** + * @brief Check if the database connection is still alive. If it is not, show a warning and offer + * the user to retry the SQL command or abort it. Abort aborts the whole application. + * Note that using this function might NOT detect the disconnection all the time, sometimes + * it detects only if there was a query run that failed. It is advised to call the function + * before executing/preparing a query or running other SQL commands and run once more + * if the command fails to test if the error was because the database connection is dropped. + * When SqlQuery or SqlTransaction is used there is no need to call the method, + * they do it themselves. + **/ + void checkDbIsAlive( QSqlDatabase& db ); + + void registerQuery(SqlQuery* query); + void unregisterQuery(SqlQuery* query); + + void registerMonitor(SqlMonitor *monitor); + void unregisterMonitor(SqlMonitor *monitor); + + int queryCount() const; + + int monitorCount() const; + + void printQueries() const; + + void printMonitors() const; + + ~SqlQueryManager(); + +private: + SqlQueryManager(); + + static SqlQueryManager* s_instance; + QList m_queries; /// m_monitors; /// +*/ + +#include "SqlQueryWatcher.h" + +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#else +#include +#endif + +#define SQL_NETWORK_TIMEOUT 30 //define how many seconds do we wait for a query finishes until the network error dialog is shown + +QMutex mutex; +static bool s_dialogVisible = false; + +SqlQueryWatcherHelper::SqlQueryWatcherHelper(QObject* runner):QObject(0) +{ +// qDebug() << Q_FUNC_INFO << this; + m_process = 0; + m_timer = new QTimer(); + m_timer->setSingleShot(true); + connect( m_timer, SIGNAL(timeout()), SLOT(timerExpired()) ); + m_timer->start( SQL_NETWORK_TIMEOUT*1000 ); +} + +SqlQueryWatcherHelper::~SqlQueryWatcherHelper() +{ +// qDebug() << Q_FUNC_INFO << this; + delete m_timer; + if ( m_process ) { + m_process->terminate(); + m_process->kill(); + m_process->waitForFinished(); + } + emit endWatcher(); +} + +void SqlQueryWatcherHelper::dialogHidden() +{ + QMutexLocker locker(&mutex); + s_dialogVisible = false; +} + + +void SqlQueryWatcherHelper::timerExpired() +{ + QMutexLocker locker(&mutex); + if ( s_dialogVisible ) + return; + s_dialogVisible = true; + qDebug() << Q_FUNC_INFO << this; + m_process = new QProcess(this); + connect( m_process, SIGNAL(finished(int)), this, SLOT(dialogHidden()) ); + QStringList args; + args << QLatin1String("-pid"); +#ifdef Q_OS_WIN + args << QString::number( _getpid() ); +#else + args << QString::number( ::getpid() ); +#endif + + m_process->start( QLatin1String("queryWatcherHelper"), args ); + m_process->waitForStarted(); + if ( m_process->state() != QProcess::Running ) { //try to start from the build dir + m_process->start( QLatin1String("src/helpers/queryWatcherHelper"), args ); + m_process->waitForStarted(); + if ( m_process->state() != QProcess::Running ) { //try to start from the build dir + qWarning() << "Timer expired, query was too slow! Possible network error detected."; + } + } +} + + +void SqlQueryWatcherHelper::quit() +{ +// qDebug() << Q_FUNC_INFO; + thread()->quit(); +// metaObject()->invokeMethod(thread(), "quit", Qt::DirectConnection); +} + +#include "moc_SqlQueryWatcher.cpp" diff --git a/SqlQueryWatcher.h b/SqlQueryWatcher.h new file mode 100644 index 0000000..275e7a6 --- /dev/null +++ b/SqlQueryWatcher.h @@ -0,0 +1,38 @@ +/* +* Copyright (C) 2011 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Andras Mantia +*/ + +#ifndef SQLQUERYWATCHER_H +#define SQLQUERYWATCHER_H + +#include "sqlate_export.h" + +#include + +class QTimer; +class QProcess; + +/** The actual watcher class, instantiated from a KDThreadRunner thread */ +class SQLATE_EXPORT SqlQueryWatcherHelper : public QObject { +Q_OBJECT +public: + SqlQueryWatcherHelper(QObject* runner); + virtual ~SqlQueryWatcherHelper(); + +public Q_SLOTS: + void quit(); + +private Q_SLOTS: + void timerExpired(); + void dialogHidden(); + +Q_SIGNALS: + void endWatcher(); + +private: + QTimer *m_timer; + QProcess *m_process; +}; + +#endif diff --git a/SqlResources.qrc b/SqlResources.qrc new file mode 100644 index 0000000..0b7e7ad --- /dev/null +++ b/SqlResources.qrc @@ -0,0 +1,5 @@ + + + procedures/pre-create/ACLTriggerFunctions.tsp + + diff --git a/SqlSchema.cpp b/SqlSchema.cpp new file mode 100644 index 0000000..02b1b57 --- /dev/null +++ b/SqlSchema.cpp @@ -0,0 +1,7 @@ +#include "SqlSchema.h" + +namespace Sql { + + + +} diff --git a/SqlSchema.h b/SqlSchema.h new file mode 100644 index 0000000..0e60ef0 --- /dev/null +++ b/SqlSchema.h @@ -0,0 +1,23 @@ +#ifndef SQLSCHEMA_H +#define SQLSCHEMA_H + +#include "SqlSchema_p.h" + +#include +#include + +Q_DECLARE_METATYPE( QUuid ) +Q_DECLARE_METATYPE( QList ) + + +/** + * @file SqlSchema.h + * Database schema definition + */ + +namespace Sql { + + +} + +#endif diff --git a/SqlSchema_p.h b/SqlSchema_p.h new file mode 100644 index 0000000..cb40fa3 --- /dev/null +++ b/SqlSchema_p.h @@ -0,0 +1,311 @@ +/* +* Copyright (C) 2011-2013 Klaralavdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Volker Krause +* Andras Mantia +*/ +#ifndef SQLSCHEMA_P_H +#define SQLSCHEMA_P_H + +#include "SqlInternals_p.h" +#include "sqlate_export.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/** + * @file SqlSchema_p.h + * Database schema related internal classes and functions. + */ + + +/** Stringification of SQL identifier names, used in table and column classes. */ +#define SQL_NAME( x ) \ + static QString sqlName() { return QLatin1String(x); } + +/** Convenience macro to create column types. + * Use inside a table declaration. + */ +#define COLUMN( name, type, ... ) \ + struct name ## Type : Column< name ## Type, type , ## __VA_ARGS__ > { SQL_NAME( #name ) } name + +/** Convenience macro to create a column alias. + * Use inside a table definition to asign a different name to an existing column. + */ +#define COLUMN_ALIAS( column, alias ) \ + typedef column ## Type alias ## Type; \ + alias ## Type alias + +/** Convenience macro for creating foreign key columns with the default legacy fk_[table]_[col] name. + * Use inside a table declaration. + */ +#define FOREIGN_KEY( name, fkTab, fkCol, ... ) \ + struct name ## Type : ForeignKey< name ## Type, fkTab ## Type :: fkCol ## Type , ## __VA_ARGS__ > { \ + static QString sqlName() { return QLatin1Literal( "fk_" ) % referenced_column::table::tableName() % QLatin1Char('_' ) % referenced_column::sqlName(); } \ + } name + +/** Convenience macro for creating foreign key columns . + * Use inside a table declaration. + */ +#define NAMED_FOREIGN_KEY( name, fkTab, fkCol, ... ) \ + struct name ## Type : ForeignKey< name ## Type, fkTab ## Type :: fkCol ## Type , ## __VA_ARGS__ > { SQL_NAME( #name ) } name + +/** Convenience macro to create a table with non-updatable rows type. Put after one of the TABLE macros.*/ +#define NO_USER_UPDATE \ + typedef boost::mpl::false_ update_rows; + +/** Convenience macro to create a table with non-deletable rows type. Put after one of the TABLE macros.*/ +#define NO_USER_DELETE \ + typedef boost::mpl::false_ delete_rows; + +/** Convenience macro to create a table with non-insertable rows type. Put after one of the TABLE macros.*/ +#define NO_USER_INSERT \ + typedef boost::mpl::false_ insert_rows; + +/** Convenience macro to create a restricted table type: only SELECT is allowed. Put after one of the TABLE macros. + * Same as using NO_USER_UPDATE, NO_USER_DELETE, NO_USER_INSERT together + */ +#define ONLY_USER_SELECT \ + typedef boost::mpl::true_ is_restricted; + +/** Convenience macro to create a table type. */ +#define TABLE( name, _EXPORT ) \ + struct name ## Type; \ + extern _EXPORT name ## Type name; \ + struct name ## Type : Table< name ## Type > + +/** Convenience macro to create a lookup table type. */ +#define LOOKUP_TABLE( name, _EXPORT ) \ + struct name ## Type; \ + extern _EXPORT name ## Type name; \ + struct name ## Type : LookupTable< name ## Type > + +/** Convenience macro to create a n:m helper table type. */ +#define RELATION( name, leftTab, leftCol, rightTab, rightCol,_EXPORT ) \ + struct name ## Type; \ + extern _EXPORT name ## Type name; \ + struct name ## Type : Relation< name ## Type, leftTab ## Type :: leftCol ## Type, rightTab ## Type :: rightCol ## Type> + +/** Convenience macro to create a n:m helper table type. */ +#define UNIQUE_RELATION( name, leftTab, leftCol, rightTab, rightCol,_EXPORT ) \ + struct name ## Type; \ + extern _EXPORT name ## Type name; \ + struct name ## Type : UniqueRelation< name ## Type, leftTab ## Type :: leftCol ## Type, rightTab ## Type :: rightCol ## Type> + +/** Convenience macro to create a recursive n:m helper table type. */ +#define RECURSIVE_RELATION( name, tab, col, _EXPORT ) \ + struct name ## Type; \ + extern _EXPORT name ## Type name; \ + struct name ## Type : RecursiveRelation< name ## Type, tab ## Type :: col ## Type > + +/** Convenience macro to create a recursive n:m helper table type. */ +#define UNIQUE_RECURSIVE_RELATION( name, tab, col, _EXPORT ) \ + struct name ## Type; \ + extern _EXPORT name ## Type name; \ + struct name ## Type : UniqueRecursiveRelation< name ## Type, tab ## Type :: col ## Type > + +/** Convenience macro to create the schema declaration. */ +#define DECLARE_SCHEMA_MAKE_TYPE( r, unused, i, t ) BOOST_PP_COMMA_IF(i) BOOST_PP_CAT(t, Type) +#define DECLARE_SCHEMA( name, seq ) typedef boost::mpl::vector name + +/** Convenience macro to create the schema definition. */ +#define DEFINE_SCHEMA_MAKE_DEF( r, unused, t ) BOOST_PP_CAT(t, Type) t; +#define DEFINE_SCHEMA( seq ) BOOST_PP_SEQ_FOR_EACH(DEFINE_SCHEMA_MAKE_DEF, ~, seq) + + +namespace Sql { + +/** + * Properties of SQL columns. + */ +enum ColumnProperties +{ + Null = 0, ///< column can be NULL + NotNull = 1, ///< column must not be NULL + Unique = 2, ///< column content must be unique + PrimaryKey = 4 | NotNull | Unique, ///< column is the primary key + Default = 8, ///< Use default value + OnDeleteCascade = 16, ///< For foreign key constraints, cascade on deletion. + OnDeleteRestrict = 32, ///< for foreign key constraints, restrict deletion. + OnUserUpdateRestrict = 64, ///< Restrict update for regular users. For this to work there must be an is_administrator() stored procedure that checks if the current user is administrator or not. + Notify = 128 ///< When an UPDATE is made on a row, emit a signal containing the content of the column for this row, and an encoded string corresponding to the column name. +}; + +/** + * Multi-column uniqeness table constraint. + * @tparam ColList An MPL sequence of columns whose tuple needs to be unique table-wide + */ +template +struct UniqueConstraint +{ + typedef ColList columns; +}; + + +/** + * Base class for SQL table types. + * @tparam DerivedTable CRTP + */ +template +struct Table +{ + typedef boost::mpl::false_ is_lookup_table; + typedef boost::mpl::false_ is_relation; + typedef boost::mpl::false_ is_restricted; //regular users can only select it + typedef boost::mpl::true_ delete_rows; //regular users can delete rows from it. _is_restricted takes precendece + typedef boost::mpl::true_ update_rows; //regular users can update rows from it. _is_restricted takes precendece + typedef boost::mpl::true_ insert_rows; //regular users can insert rows from it. _is_restricted takes precendece + + /** + * Base class for SQL columns of table @p DerivedTable + * @tparam Derived CRTP + * @tparam ColumnType The C++ data type of this column + * @tparam P Column property flags + * @tparam Size The maximum size of the column (-1 for unrestricted) + */ + template + struct Column + { + /** C++ type of this column. */ + typedef ColumnType type; + /** The table this column is in. */ + typedef DerivedTable table; + /** The maximum size of this column, -1 means no restrictions. */ + static const int size = Size; + + /** For identification of column classes in WhereExpr operators. */ + typedef boost::mpl::true_ is_column; + + typedef boost::mpl::false_ hasForeignKey; + typedef typename boost::mpl::if_c<(P & NotNull) != 0, boost::mpl::true_, boost::mpl::false_>::type notNull; + typedef typename boost::mpl::if_c<(P & Unique) != 0, boost::mpl::true_, boost::mpl::false_>::type unique; + typedef typename boost::mpl::if_c<((P & PrimaryKey) == PrimaryKey), boost::mpl::true_, boost::mpl::false_>::type primaryKey; + typedef typename boost::mpl::if_c<(P & Default) != 0, boost::mpl::true_, boost::mpl::false_>::type useDefault; + typedef typename boost::mpl::if_c<(P & OnUserUpdateRestrict) != 0, boost::mpl::true_, boost::mpl::false_>::type onUserUpdateRestrict; + typedef typename boost::mpl::if_c<(P & Notify) != 0, boost::mpl::true_, boost::mpl::false_>::type notify; + + /** Returns the fully qualified name of this column, ie. "tableName.columnName". */ + static QString name() { return DerivedTable::sqlName() % QLatin1Char('.') % Derived::sqlName(); } + }; + + /** + * Base class for SQL foreign key columns of table @p DerivedTable. + * @tparam Derived CRTP + * @tparam ForeignColumn Type of the column this one is a reference on. + * @tparam P Property flags. + */ + template + struct ForeignKey : Column + { + typedef ForeignColumn referenced_column; + typedef boost::mpl::true_ hasForeignKey; + + typedef typename boost::mpl::if_c<(P & OnDeleteCascade) != 0, boost::mpl::true_, boost::mpl::false_>::type onDeleteCascade; + typedef typename boost::mpl::if_c<(P & OnDeleteRestrict) != 0, boost::mpl::true_, boost::mpl::false_>::type onDeleteRestrict; + }; + + /** Returns the SQL identifier of this table. */ + static QString tableName() { return DerivedTable::sqlName(); } + + /** Sequence of table constraints. */ + typedef boost::mpl::vector<> constraints; + typedef constraints baseConstraints; +}; + +/** + * Base class for lookup tables + * @tparam DerivedTable CRTP + */ +template +struct LookupTable : Table +{ + typedef boost::mpl::true_ is_lookup_table; + typedef boost::mpl::true_ is_restricted; + typedef Table base_type; // needed for MSVC to compile the follow lines... + struct idType : base_type::template Column { SQL_NAME( "id" ) } id; + struct shortDescriptionType : base_type::template Column { SQL_NAME( "short_desc" ) } shortDescription; + struct descriptionType : base_type::template Column { SQL_NAME( "description" ) } description; + typedef boost::mpl::vector columns; + typedef columns baseColumns; +}; + +/** + * Base class for n:m relation tables + * @tparam DerivedTable CRTP + * @tparam LeftColumn Column type of the lhs of the mapping. + * @tparam RightColumn Column type of the rhs of the mapping. + */ +template +struct Relation : Table +{ + typedef boost::mpl::true_ is_relation; + typedef Table base_type; + struct leftType : base_type::template ForeignKey + { + static QString sqlName() { return QLatin1Literal( "fk_" ) % LeftColumn::table::sqlName() % QLatin1Char('_' ) % LeftColumn::sqlName(); } + } left; + struct rightType : base_type::template ForeignKey + { + static QString sqlName() { return QLatin1Literal( "fk_" ) % RightColumn::table::sqlName() % QLatin1Char('_' ) % RightColumn::sqlName(); } + } right; + + typedef boost::mpl::vector columns; + typedef columns baseColumns; +}; + +/** + * Convenience class for unique n:m releation helper classes. + * @see Relataion + */ +template +struct UniqueRelation : Relation +{ + typedef Relation base_type; // needed for MSVC to compile the follow lines... + typedef boost::mpl::vector< UniqueConstraint< typename base_type::columns > > constraints; +}; + +/** + * Base class recursive n:m relation tables + * Basically a Relation pointing to the same column with both sides of the mapping. + * @tparam DerivedTable CRTP + * @tparam ColumnT The column this mapping works on. + */ +template +struct RecursiveRelation : Table +{ + typedef boost::mpl::true_ is_relation; + typedef Table base_type; + struct leftType : base_type::template ForeignKey + { + static QString sqlName() { return QLatin1Literal( "fk_" ) % ColumnT::table::sqlName() % QLatin1Char('_') % ColumnT::sqlName(); } + } left; + struct rightType : base_type::template ForeignKey + { + static QString sqlName() { return QLatin1Literal( "fk_" ) % ColumnT::table::sqlName() % QLatin1Char('_') % ColumnT::sqlName() % QLatin1Literal("_link"); } + } right; + + typedef boost::mpl::vector columns; + typedef columns baseColumns; +}; + +/** + * Convenience class for unique recursive n:m releation helper classes. + * @see RecursiveRelataion + */ +template +struct UniqueRecursiveRelation : RecursiveRelation +{ + typedef RecursiveRelation base_type; + typedef boost::mpl::vector< UniqueConstraint< typename base_type::columns > > constraints; +}; + + +} + +#endif diff --git a/SqlSelect.h b/SqlSelect.h new file mode 100644 index 0000000..1b77634 --- /dev/null +++ b/SqlSelect.h @@ -0,0 +1,595 @@ +/* +* Copyright (C) 2011-2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com +* Author: Andras Mantia +* Volker Krause +*/ +#ifndef SQL_SELECT_H +#define SQL_SELECT_H + +#include "SqlInternals_p.h" +#include "SqlSchema_p.h" +#include "SqlCondition.h" +#include "SqlSelectQueryBuilder.h" +#include "SqlGlobal.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @file SqlSelect.h + * Select query expression templates. + */ + +namespace Sql { + +template struct ConditionExpr; +template struct ConditionColumnLeaf; +template struct ConditionValueLeaf; +template struct ConditionPlaceholderLeaf; + +namespace detail { + +/** + * MPL for_each accumulator to add columns to the query builder. + * @internal + */ +struct columns_to_querybuilder +{ + explicit columns_to_querybuilder( SqlSelectQueryBuilder &qb ) : m_qb( qb ) {} + template void operator()( wrap ) + { + m_qb.addColumn( C::name() ); + } + + SqlSelectQueryBuilder &m_qb; +}; + +/** + * MPL for_each accumulator to add group by columnt to the query builder. + * @internal + */ +struct groupby_to_querybuilder +{ + explicit groupby_to_querybuilder( SqlSelectQueryBuilder &qb ) : m_qb( qb ) {} + template void operator() ( wrap ) + { + m_qb.addGroupColumn( C::name() ); + } + SqlSelectQueryBuilder &m_qb; +}; + +/** + * Add a ConditionLeaf to an existing SqlCondition for SQL code generation. + * @internal + */ +template +void append_condition( SqlCondition &cond, const ConditionColumnLeaf & ) +{ + cond.addColumnCondition( Lhs(), Comp, Rhs() ); +} + +template +void append_condition( SqlCondition &cond, const ConditionValueLeaf &leaf ) +{ + cond.addValueCondition( Lhs::name(), Comp, leaf.value ); +} + +template +void append_condition( SqlCondition &cond, const ConditionPlaceholderLeaf &leaf ) +{ + cond.addPlaceholderCondition( Lhs::name(), Comp, leaf.placeholder ); +} + +/** + * Metafunction to identify condition expressions. + * @internal + */ +template +struct is_condition_expr : boost::mpl::false_ {}; + +template +struct is_condition_expr > : boost::mpl::true_ {}; + +/** + * Metafunction to wrap a single condition leaf into an condition expression. + * @tparam T a condition leaf or a condition expression + * @internal + */ +template +struct wrap_condition_leaf +{ + typedef typename boost::mpl::if_, T, ConditionExpr, SqlCondition::And> >::type type; +}; + +template +typename boost::enable_if, void>::type +assign_condition( SqlCondition &cond, const T &expr ) { cond = expr.condition; } + +template +typename boost::disable_if, void>::type +assign_condition( SqlCondition &cond, const T &leaf ) { append_condition( cond, leaf ); } + +/** + * Container for runtime information of joins. + * @internal + */ +struct JoinInfo { + SqlSelectQueryBuilder::JoinType type; + QString table; + SqlCondition condition; +}; + +/** + * Container for runtime information about ordering + * @internal + */ +struct OrderInfo { + QString column; + Qt::SortOrder order; +}; + +} + +/** + * Represents a single node in a conditional expression + * @internal + * @tparam Derived CRTP + * @tparam Lhs Left hand side statement + * @tparam Comp Comparator type + * @tparam Rhs Right hand side statement + */ +template