├── .gitignore ├── LICENSE ├── README.md ├── qsqlbuilder.pro ├── sqlbuilder ├── Config.cpp ├── Config.h ├── Deleter.cpp ├── Deleter.h ├── Inserter.cpp ├── Inserter.h ├── Query.cpp ├── Query.h ├── Selector.cpp ├── Selector.h ├── Updater.cpp ├── Updater.h ├── Where.cpp ├── Where.h └── sqlbuilder.pro ├── sqlbuilder_include.pri └── test ├── test.pro └── tst_builder_test.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | *.slo 3 | *.lo 4 | *.o 5 | *.a 6 | *.la 7 | *.lai 8 | *.so 9 | *.dll 10 | *.dylib 11 | 12 | # Qt-es 13 | object_script.*.Release 14 | object_script.*.Debug 15 | *_plugin_import.cpp 16 | /.qmake.cache 17 | /.qmake.stash 18 | *.pro.user 19 | *.pro.user.* 20 | *.qbs.user 21 | *.qbs.user.* 22 | *.moc 23 | moc_*.cpp 24 | moc_*.h 25 | qrc_*.cpp 26 | ui_*.h 27 | *.qmlc 28 | *.jsc 29 | Makefile* 30 | *build-* 31 | 32 | # Qt unit tests 33 | target_wrapper.* 34 | 35 | # QtCreator 36 | *.autosave 37 | 38 | # QtCreator Qml 39 | *.qmlproject.user 40 | *.qmlproject.user.* 41 | 42 | # QtCreator CMake 43 | CMakeLists.txt.user* 44 | 45 | # builddir 46 | bin -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alexander «MasterAler» Kasian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QSqlBuilder 2 | 3 | A small C++11 library, built over Qt, that provides rapid building & executing of SQL queries, hiding raw query text. 4 | 5 | ### What is it? What for? 6 | 7 | Well, here is a simple & easy-to-use (I hope!) SQL query builder using Qt's types and database interaction classes. 8 | It uses all the same QSqlDatabase, QSqlQuery, etc., but you'll be saved the effort of writing 100500 strings with SQL (*and checking errors in each of them*) in your sources. 9 | Most of the documentation is provided as Doxxygen comments, unit-test subproject also demonstates the usage. 10 | 11 | For some sophisticated purposed you'd probably like to use some ORM, for example an excellent [sqlpp11](https://github.com/rbock/sqlpp11), but sometimes you *just don't need that much*. That could be a fine time-saver in the case. 12 | 13 | When a thing like the descibed here could be of use: 14 | 15 | * You are writing a small Qt backend (whatever reason there be, ex.: small application service with API, that was my case) 16 | * Your Qt client is small and doesn't need a real ORM (ex.: SQLITE used) 17 | * You need code mocking or *fast implementing* of smth. 18 | * You are going to change your database structure a lot later and that's where a good C++ ORM will bring you pain 19 | 20 | In other languages there exist reflection, LINQ (in C#) and other conveniences. But in C++ a small custom builder can help instead. 21 | Basic CRUD (create + read + update + delete) are implemented, by "create" an INSERT query is meant. 22 | 23 | ## Usage 24 | 25 | There is a `Query` class, which is supposed to be used locally/on demand/once per set of requests. Should not be a global state, anyway it's instances share the connection, opening and closing it on costruction/destruction, other classes are intenal and designed to be rvalue-only. Thread-safety is questionable, it is equal to the thread-safety of QSqlDatabase class. Of course, the generators consist of string manipulations only, that's pretty safe, but if you start a transaction in one thread (thransactions are supported) and perform a SELECT in the ither -- that should cause a failure. Exactly as it does with the QSqlDatabase. You can still have a `Query` lvalue instance (it is move-constructible), no need to reopen connection every time. By the way, despite the methods returning some internall classes all the time, it won't cost you much in terms of performance, the classes are not only lightweight but also heavily use copy elision everywhere. 26 | 27 | The rest is better shown by example. 28 | 29 | ### Configuration 30 | 31 | ```cpp 32 | Config::setConnectionParams("QPSQL", "127.0.0.1", "db_name", "root", ""); // specify your connection parameters here 33 | Query::setQueryLoggingEnabled(true); // enable/disable logging via qDebug() 34 | ``` 35 | Logging is essentially useful to check the generated SQL for better understanding the concept, it's likely that examples do not cover all the caveats. 36 | 37 | ### Raw SQL (something too complex to be generated) 38 | 39 | ```cpp 40 | QSqlQuery q = Query().performSQL("SELECT _id, name FROM my_table LIMIT 3;"); 41 | ``` 42 | Should be something *really* complex, then use a plin QSqlQuery, as usually, the connection would be already configured. 43 | 44 | ### Select 45 | 46 | ```cpp 47 | auto res = Query("my_table") 48 | .select({"id", "name"}) 49 | .where(OP::EQ("some_field", "some_value") && OP::LE("id", 1234)) // WHERE ("some_field" = 'some_value') AND ("id" <= 1234) 50 | .orderBy("id", Order::DESC) 51 | .limit(3) 52 | .offset(20) 53 | .perform(); 54 | 55 | auto res = Query("my_table") 56 | .select({"id", "name", "guid"}) 57 | .where(OP::IN("id", {23, 55, 66, 77}) || !OP::IS_NULL("some_key")) // WHERE ("id" IN ('23', '55', '66', '77')) OR NOT("some_key" IS NULL) 58 | .orderBy("id", Order::ASC) 59 | .perform(); 60 | ``` 61 | You'll get a `QVariantList`, each map contains same column names as specified in `select()`, aliases ("col AS smth") are also supported. Actually, aliases should save you from trying to figure out, how the `join()` is working in details. 62 | 63 | NOTE: the WHERE part is implemented cpp-style, it generates lots of braces, but that is how your natural cpp logic is being translated into SQL without surprising permutations. Order of the calls does not matter, except for joins. That WHERE clauses are used by all the generators internally. 64 | 65 | ### Delete 66 | 67 | ```cpp 68 | const auto query = Query("my_table"); // yeah, you can have a variable 69 | bool ok = query.delete_(OP::IN("id", {11, 22, 33}).perform(); 70 | ``` 71 | Pretty simple, result means "affected rows > 0"; 72 | 73 | ### Update 74 | 75 | ```cpp 76 | bool ok = query 77 | .update({{"descr", "OLOLOLO"}}) 78 | .where(OP::LE("id", 100500)) 79 | .perform(); 80 | ``` 81 | Same as DELETE, result means "affected rows > 0"; 82 | 83 | ### Insert 84 | 85 | ```cpp 86 | auto ids = query 87 | .insert({"some_number", "guid", "date"}) 88 | .values({42, QUuid::createUuid().toString(), QDate::currentDate()}) 89 | .values({42, QUuid::createUuid().toString(), QDate::currentDate().addDays(3)}) 90 | .values({42, QUuid::createUuid().toString(), QDate::currentDate().addDays(-3)}) 91 | .perform(); 92 | ``` 93 | Returns a list of newly inserted ids. 94 | NOTE: this functional relies on "... RETURNINF id;" feature support, my target was PostgreSQL. The `Query` class will try to determine the primary key, but if you *really mean something strange* another column can be specified instead, like `Query("my_table", "some_col")`. It is just a string, you can pass there whatever `RETURNING` supports, but a have not tested that option thorougly. 95 | 96 | ### Transactions 97 | 98 | Here is a sample from the project's self-test: 99 | 100 | ```cpp 101 | auto query = Query(TARGET_TABLE); 102 | 103 | const QString GOOD_NAME {"GOOD_TRANSACTION"}; 104 | const QString BAD_NAME {"BAD_TRANSACTION"}; 105 | 106 | bool ok = query.transact([&]{ // don't use default capture in real project 107 | auto ids = query 108 | .insert({"_otype", "guid", "name"}) 109 | .values({33, QUuid::createUuid().toString(), GOOD_NAME}) 110 | .values({33, QUuid::createUuid().toString(), GOOD_NAME}) 111 | .perform(); 112 | 113 | Q_ASSERT(ids.count() == 2); 114 | Q_ASSERT(!query.hasError()); // yaeh, error checking works 115 | 116 | bool d_ok = query.delete_(OP::EQ("_id", ids.first())).perform(); 117 | Q_ASSERT(d_ok); 118 | Q_ASSERT(!query.hasError()); 119 | }); 120 | Q_ASSERT(ok); 121 | 122 | // The transaction has been commited, we can check that there's only one record on the table 123 | auto check1 = query.select().where(OP::EQ("name", GOOD_NAME)).perform(); 124 | Q_ASSERT(!query.hasError()); 125 | Q_ASSERT(check1.count() == 1); 126 | ``` 127 | 128 | ... and here goes a "bad" transaction, that's been rollbacked: 129 | 130 | ```cpp 131 | ok = query.transact([&]{ 132 | auto ids = query 133 | .insert({"_otype", "guid", "name"}) 134 | .values({33, QUuid::createUuid().toString(), BAD_NAME}) 135 | .values({33, QUuid::createUuid().toString(), BAD_NAME}) 136 | .perform(); 137 | Q_ASSERT(ids.count() == 2); 138 | Q_ASSERT(!query.hasError()); 139 | 140 | bool d_ok = query.delete_(OP::EQ("_id_OOPS", ids.first())).perform(); 141 | Q_ASSERT(!d_ok); 142 | Q_ASSERT(query.hasError()); 143 | }); 144 | Q_ASSERT(!ok); 145 | 146 | // Nothing's been inserted due to the error, after rollback we can check it 147 | auto check2 = query.select().where(OP::EQ("name", BAD_NAME)).perform(); 148 | Q_ASSERT(!query.hasError()); 149 | Q_ASSERT(check2.isEmpty()); 150 | ``` 151 | 152 | You can even write a *nested* transaction, but the behaviour would much depend on the database engine. Once again, it would be similar to the one of QSqlDatabase. 153 | On PostgreSQL, for example, you'll get a warning, but the code would not fail. 154 | 155 | Note that the returned success/failure indicated transaction's success/failure. Qt's logic differs a bit, but I cannot imagine how would anyone handle the failure of rollback operation. 156 | 157 | ### Select with JOINs 158 | 159 | ```cpp 160 | auto res = query 161 | .select({"id", "some_text", "name", "guid"}) 162 | .join("other_table_name", {"some_fkey", "id"}, Join::INNER) 163 | .orderBy("id", Order::ASC) 164 | .perform(); 165 | 166 | auto res = query 167 | .select() 168 | .join("other_table_name", {"some_fkey", "id"}, Join::INNER, true) // overriding disambiguation resolution 169 | .orderBy("id", Order::DESC) 170 | .perform(); 171 | ``` 172 | 173 | Well, that is where the tricky part is. While you are having only two joined tables everything is pretty simple -- you specify the parameters and still don't nave to care about methods' call order. 174 | If you are not carefull and column's name disambiguation occurs, it's gonna be resolved in favour of the table, specified in the `Query`'s constructor by deafult. Another behaviour can be obtained via boolean flag. 175 | 176 | But things are different with multiple joins. Actually, `join()` is the only method that doesn't overwrite the state being called more than once, accumulating JOIN parts instead. I cannot imagine from the top of my head WHY would anyone write SQL with multiple JOINs and column disambiguation at the same time, so here works a simple rule -- if you do it's either resolved by default or you *may* set a magic flag again... but wherever you do it, resolution's gonna work only in favour of the table in the first `join()` call, whatever it be. 177 | 178 | ```cpp 179 | auto res = complex_query 180 | .select({"_id", "some_text", "name", "guid", "some_date"}) 181 | .join("second_table", {"some_fkey", "_id"}, Join::INNER, true) // disambig can ONLY be resolved to this table, no matter where is the flag 182 | .join("third_table", {"date_fkey", "_id"}, Join::INNER, true) 183 | .where(OP::IN("_id", joinIds)) 184 | .orderBy("_id", Order::ASC) 185 | .perform(); 186 | ``` 187 | Honestly you should not write queries like that even manually. 188 | 189 | ### Extras 190 | 191 | ```cpp 192 | const auto query = Query("my_table"); 193 | 194 | auto res = query.select({"*"}) // this means "SELECT * FROM ..." 195 | .orderBy("_id", Order::ASC) 196 | .limit(5) 197 | .perform(); 198 | 199 | res = query.select({"*"}) // this means "SELECT id, name, ... FROM ...", all column names are inserted 200 | .orderBy("_id", Order::ASC) 201 | .limit(5) 202 | .perform(); 203 | 204 | res = query.select({"COUNT(*) as count"}).perform(); // yeah, you can use functions 205 | Q_ASSERT(!res.isEmpty()); 206 | Q_ASSERT(res.first().toMap().contains("count")); 207 | 208 | res = query.select({"MAX(_id) as max_id"}).perform(); // or even like that 209 | ``` 210 | 211 | There possible exist some other hacky ways to use the builder, I just did my best to make sure that the worst you'd get is a query error, reported through the `Query` class (exactly as Qt reports it, receiving from the database itself), all the rest builder classes and helpers are hopefully lightweight enought not to bring any unexpected trouble. 212 | 213 | ## Tests 214 | 215 | The tests are numerous but far from being full. QtTest project contains a creation of three tables and performing some queries upon them. You'll need to set your own connection parameters, of course. Uncommenting the cleanup code there can vary usage from debugging to real smoke-test of the functional. 216 | 217 | 218 | ## Requirements 219 | 220 | Honestly, the library has been tested on PostgreSQL only, but most of the SQL being built is simple and should be easily ported to other DB engine 221 | And as mentioned in the title, C++11 suppor is required. 222 | 223 | Tested on: 224 | * PostgreSQL 9.4+ 225 | * MSVC 2015 / gcc 5.4+ 226 | * Qt 5.6+ (actually Qt's version should not be important, mine was Qt 5.10) 227 | 228 | ## Credits 229 | 230 | Extra thanks to Igor Oferkin for the convenient idea of WHERE clauses. 231 | 232 | **GL;HF** 233 | -------------------------------------------------------------------------------- /qsqlbuilder.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = subdirs 2 | 3 | CONFIG += ordered 4 | 5 | SUBDIRS += \ 6 | sqlbuilder \ 7 | test 8 | -------------------------------------------------------------------------------- /sqlbuilder/Config.cpp: -------------------------------------------------------------------------------- 1 | #include "Config.h" 2 | 3 | QString Config::DRIVER {}; 4 | QString Config::DBNAME {}; 5 | QString Config::HOSTNAME {}; 6 | QString Config::USERNAME {}; 7 | QString Config::PASSWORD {}; 8 | -------------------------------------------------------------------------------- /sqlbuilder/Config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | /*! 6 | * \brief The Config struct 7 | * is a primitive wrapper for connection params. 8 | * It could be somehow more sophisticated, with connection 9 | * managing and all, but it's not the purpose of the library, 10 | * so several static variables just store params, so that 11 | * other classes (Query class to be exact) are not garbaged with them. 12 | */ 13 | struct Config 14 | { 15 | /*! 16 | * \brief setConnectionParams -- convenience method, sets all connection params at once 17 | * \param driver -- Qt driver choosing string, like "QPSQL" 18 | * \param hostname -- obviously, hostname 19 | * \param dbname -- obviously, database name 20 | * \param username -- obviously, db user name 21 | * \param password -- obviously, db user password 22 | */ 23 | static void setConnectionParams(const QString& driver, const QString& hostname, const QString& dbname 24 | , const QString& username, const QString& password) 25 | { 26 | Config::DRIVER = driver; 27 | Config::DBNAME = dbname; 28 | Config::HOSTNAME = hostname; 29 | Config::USERNAME = username; 30 | Config::PASSWORD = password; 31 | } 32 | 33 | static QString DRIVER; 34 | static QString DBNAME; 35 | static QString HOSTNAME; 36 | static QString USERNAME; 37 | static QString PASSWORD; 38 | }; 39 | -------------------------------------------------------------------------------- /sqlbuilder/Deleter.cpp: -------------------------------------------------------------------------------- 1 | #include "Deleter.h" 2 | #include "Query.h" 3 | 4 | #include 5 | #include 6 | 7 | struct Deleter::DeleterPrivate 8 | { 9 | DeleterPrivate(const Query *q, OP::Clause&& whereClause) 10 | : m_query(q) 11 | , m_where{std::move(whereClause).getSQl()} 12 | {} 13 | 14 | const Query* m_query; 15 | QString m_where; 16 | }; 17 | 18 | const QString Deleter::DELETE_SQL { "DELETE FROM %1 WHERE %2;" }; 19 | 20 | /***************************************************************************************/ 21 | 22 | Deleter::Deleter(const Query *q, OP::Clause&& whereClause) 23 | : impl(new DeleterPrivate(q, std::forward(whereClause))) 24 | { } 25 | 26 | Deleter::~Deleter() 27 | { } 28 | 29 | Deleter::Deleter(Deleter &&) = default; 30 | 31 | bool Deleter::perform() && 32 | { 33 | const QString sql = Deleter::DELETE_SQL 34 | .arg(impl->m_query->tableName()) 35 | .arg(impl->m_where.isEmpty() ? "True" : impl->m_where); 36 | 37 | QSqlQuery q = impl->m_query->performSQL(sql); 38 | return q.numRowsAffected() > 0; 39 | } 40 | -------------------------------------------------------------------------------- /sqlbuilder/Deleter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "Where.h" 7 | QT_FORWARD_DECLARE_CLASS(Query) 8 | 9 | /*! 10 | * \brief The Deleter class 11 | * is a DELETE SQL query generator. Supports WHERE clauses, 12 | * should *never* be used as lvalue or constructed manually. 13 | * Don't clean tables with it, it is possible, but not efficient. 14 | */ 15 | class Deleter 16 | { 17 | Q_DISABLE_COPY(Deleter) 18 | public: 19 | /*! 20 | * \brief Deleter -- constructs the generator 21 | * \param q -- ptr to the Query class, that created it 22 | * \param whereClause -- "WHERE ..." clause (see OP namespace for details) 23 | */ 24 | Deleter(const Query* q, OP::Clause&& whereClause); 25 | ~Deleter(); 26 | 27 | Deleter(Deleter&&); 28 | Deleter& operator=(Deleter&&) = default; 29 | 30 | /*! 31 | * \brief perform -- executes the generated query 32 | * \return -- success/failure of the query (affected rows > 0) 33 | */ 34 | bool perform() &&; 35 | 36 | private: 37 | struct DeleterPrivate; 38 | std::unique_ptr impl; 39 | 40 | static const QString DELETE_SQL; 41 | }; 42 | -------------------------------------------------------------------------------- /sqlbuilder/Inserter.cpp: -------------------------------------------------------------------------------- 1 | #include "Inserter.h" 2 | #include "Query.h" 3 | #include "Where.h" 4 | 5 | #include 6 | #include 7 | 8 | struct Inserter::InserterPrivate 9 | { 10 | InserterPrivate(const Query* q, const QStringList& fields) 11 | : m_query(q) 12 | , m_fields(fields) 13 | {} 14 | 15 | const Query* m_query; 16 | const QStringList m_fields; 17 | 18 | QList m_data; 19 | }; 20 | 21 | /***************************************************************************************/ 22 | 23 | const QString Inserter::INSERT_SQL { "INSERT INTO %1 %2 VALUES %3 RETURNING %4;" }; 24 | 25 | Inserter::Inserter(const Query* q, const QStringList& fields) 26 | : impl(new InserterPrivate(q, fields)) 27 | { } 28 | 29 | Inserter::~Inserter() 30 | { } 31 | 32 | Inserter::Inserter(Inserter &&) = default; 33 | 34 | InserterPerformer Inserter::values(const QVariantList& data) && 35 | { 36 | return InserterPerformer(std::move(*this)).values(data); 37 | } 38 | 39 | /***************************************************************************************/ 40 | 41 | InserterPerformer::~InserterPerformer() 42 | { } 43 | 44 | InserterPerformer::InserterPerformer(Inserter&& inserter) 45 | : impl(std::move(inserter.impl)) 46 | { } 47 | 48 | InserterPerformer InserterPerformer::values(const QVariantList& data) && 49 | { 50 | impl->m_data.append(data); 51 | return std::move(*this); 52 | } 53 | 54 | QList InserterPerformer::perform() && 55 | { 56 | QList result; 57 | 58 | QStringList valueTail; 59 | for(const QVariantList& dataTuple : impl->m_data) 60 | { 61 | QStringList vBlock; 62 | for(const QVariant& value : dataTuple) 63 | vBlock << OP::Clause::escapeValue(value); 64 | 65 | valueTail << QString("(%1)").arg(vBlock.join(',')); 66 | } 67 | 68 | const QString sql = Inserter::INSERT_SQL 69 | .arg(impl->m_query->tableName()) 70 | .arg(QString("(%1)").arg(impl->m_fields.join(','))) 71 | .arg(valueTail.join(',')) 72 | .arg(impl->m_query->primaryKeyName()); 73 | 74 | QSqlQuery q = impl->m_query->performSQL(sql); 75 | while(q.next()) 76 | result.append(q.value(0).toInt()); 77 | 78 | return result; 79 | } 80 | -------------------------------------------------------------------------------- /sqlbuilder/Inserter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | QT_FORWARD_DECLARE_CLASS(Query) 7 | QT_FORWARD_DECLARE_CLASS(InserterPerformer) 8 | 9 | /*! 10 | * \brief The Inserter class 11 | * is a INSERT SQL query generator. Two classes are 12 | * used, so that performing a query without providing values is not possible. 13 | */ 14 | class Inserter 15 | { 16 | Q_DISABLE_COPY(Inserter) 17 | public: 18 | /*! 19 | * \brief Inserter -- constructor, obviously 20 | * \param q -- ptr to the Query class, that created it 21 | * \param fields -- list of column names in INSERT INTO tbl (...) 22 | */ 23 | Inserter(const Query* q, const QStringList& fields); 24 | ~Inserter(); 25 | 26 | Inserter(Inserter&&); 27 | Inserter& operator=(Inserter&&) = default; 28 | 29 | /*! 30 | * \brief values -- adds list of values to be inserted to the generator 31 | * \param data -- list of inserted values, shoul match by count the number of fields 32 | * \return -- returns similar generator, but with the ability to execute the query 33 | */ 34 | InserterPerformer values(const QVariantList& data) &&; 35 | 36 | private: 37 | struct InserterPrivate; 38 | std::unique_ptr impl; 39 | 40 | friend class InserterPerformer; 41 | static const QString INSERT_SQL; 42 | }; 43 | 44 | /*******************************************************************************************/ 45 | 46 | /*! 47 | * \brief The InserterPerformer class 48 | * is another internal class, similar to Inserter, but 49 | * alredy propagated with some values, so it can execute the query. 50 | * Supports adding more values, like in VALUES(...),(...),(...) 51 | * IMPORTANT: the functional relies on "... RETURNING id;" SQL syntax, 52 | * has not been tested without it. That is exactly where existence of 53 | * primary key (or some of it's replacement) is crusial (provided in Query class). 54 | */ 55 | class InserterPerformer 56 | { 57 | Q_DISABLE_COPY(InserterPerformer) 58 | public: 59 | ~InserterPerformer(); 60 | 61 | /*! 62 | * \brief InserterPerformer -- move-construncts the class from Inserter 63 | * \param inserter -- Inserter class to be constructed from 64 | */ 65 | InserterPerformer(Inserter&& inserter); 66 | 67 | InserterPerformer(InserterPerformer&&) = default; 68 | InserterPerformer& operator=(InserterPerformer&&) = default; 69 | 70 | /*! 71 | * \brief values -- adds list of values to be inserted to the generator 72 | * \param data -- list of inserted values, shoul match by count the number of fields 73 | * \return -- returns this generator, so that more values can be added 74 | */ 75 | InserterPerformer values(const QVariantList& data) &&; 76 | 77 | /*! 78 | * \brief perform -- executes the query 79 | * \return -- list of inserted records' ids or empty list on failure, 80 | * also check Query's hasError() if you want to ensure the result 81 | */ 82 | QList perform() &&; 83 | 84 | private: 85 | std::unique_ptr impl; 86 | }; 87 | -------------------------------------------------------------------------------- /sqlbuilder/Query.cpp: -------------------------------------------------------------------------------- 1 | #include "Query.h" 2 | 3 | #include "Config.h" 4 | #include "Selector.h" 5 | #include "Inserter.h" 6 | #include "Deleter.h" 7 | #include "Updater.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | struct Query::QueryPrivate 19 | { 20 | QueryPrivate(const QString& tableName, const QString& pkey) 21 | : m_DB(Query::defaultConnection()) 22 | , m_tableName(tableName) 23 | { 24 | m_DB.setDatabaseName(Config::DBNAME); 25 | m_DB.setHostName(Config::HOSTNAME); 26 | m_DB.setUserName(Config::USERNAME); 27 | m_DB.setPassword(Config::PASSWORD); 28 | 29 | if (!m_DB.isOpen()) 30 | { 31 | if (!m_DB.open()) 32 | throw std::runtime_error("Database was not opened! =("); 33 | } 34 | 35 | m_pkey = pkey.isEmpty() ? m_DB.primaryIndex(m_tableName).fieldName(0) : pkey; 36 | 37 | QSqlRecord columns = m_DB.record(m_tableName); 38 | for(int i = 0; i < columns.count(); ++i) 39 | m_columnNames << columns.fieldName(i); 40 | } 41 | 42 | QSqlDatabase m_DB; 43 | QString m_tableName; 44 | 45 | QString m_pkey; 46 | QStringList m_columnNames; 47 | 48 | QSqlError m_lastError; 49 | }; 50 | 51 | bool Query::LOG_QUERIES { false }; 52 | 53 | /**********************************************************************************/ 54 | 55 | Query::Query(const QString& tableName, const QString& pkey) 56 | : impl(new QueryPrivate(tableName, pkey)) 57 | { } 58 | 59 | Query::~Query() 60 | { 61 | impl->m_DB.close(); 62 | } 63 | 64 | Query::Query(Query &&) = default; 65 | 66 | void Query::setQueryLoggingEnabled(bool enabled) 67 | { 68 | Query::LOG_QUERIES = enabled; 69 | } 70 | 71 | QSqlQuery Query::performSQL(const QString& sql) const 72 | { 73 | QSqlQuery sqlQuery(impl->m_DB); 74 | sqlQuery.exec(sql); 75 | 76 | if (Query::LOG_QUERIES) 77 | qDebug() << sqlQuery.lastQuery(); 78 | 79 | impl->m_lastError = sqlQuery.lastError(); 80 | return sqlQuery; 81 | } 82 | 83 | QSqlError Query::lastError() const 84 | { 85 | return impl->m_lastError; 86 | } 87 | 88 | bool Query::hasError() const 89 | { 90 | return impl->m_lastError.isValid(); 91 | } 92 | 93 | QString Query::tableName() const 94 | { 95 | return impl->m_tableName; 96 | } 97 | 98 | QString Query::primaryKeyName() const 99 | { 100 | return impl->m_pkey; 101 | } 102 | 103 | QStringList Query::columnNames() const 104 | { 105 | return impl->m_columnNames; 106 | } 107 | 108 | QStringList Query::tableColumnNames(const QString& tableName) const 109 | { 110 | QStringList result; 111 | 112 | QSqlRecord columns = impl->m_DB.record(tableName); 113 | for(int i=0; i < columns.count(); ++i) 114 | result << columns.fieldName(i); 115 | 116 | return result; 117 | } 118 | 119 | Selector Query::select(const QStringList& fields) const 120 | { 121 | return Selector(this, fields); 122 | } 123 | 124 | Inserter Query::insert(const QStringList& fields) const 125 | { 126 | return Inserter(this, fields); 127 | } 128 | 129 | Deleter Query::delete_(OP::Clause&& whereClause) const 130 | { 131 | return Deleter(this, std::forward(whereClause)); 132 | } 133 | 134 | Updater Query::update(const QVariantMap& updateValues) const 135 | { 136 | return Updater(this, updateValues); 137 | } 138 | 139 | bool Query::transact(std::function&& operations) 140 | { 141 | bool result; 142 | 143 | if (impl->m_DB.transaction()) 144 | { 145 | std::move(operations)(); 146 | 147 | if (hasError()) 148 | { 149 | impl->m_DB.rollback(); 150 | result = false; 151 | } 152 | else 153 | result = impl->m_DB.commit(); 154 | } 155 | else 156 | return false; 157 | 158 | return result; 159 | } 160 | 161 | QSqlDatabase& Query::defaultConnection() 162 | { 163 | static QSqlDatabase dbInstance = QSqlDatabase::addDatabase(Config::DRIVER, QUuid::createUuid().toString()); 164 | return dbInstance; 165 | } 166 | -------------------------------------------------------------------------------- /sqlbuilder/Query.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "Where.h" 8 | 9 | QT_FORWARD_DECLARE_CLASS(QSqlDatabase) 10 | QT_FORWARD_DECLARE_CLASS(QSqlQuery) 11 | QT_FORWARD_DECLARE_CLASS(QSqlError) 12 | 13 | QT_FORWARD_DECLARE_CLASS(Selector) 14 | QT_FORWARD_DECLARE_CLASS(Inserter) 15 | QT_FORWARD_DECLARE_CLASS(Deleter) 16 | QT_FORWARD_DECLARE_CLASS(Updater) 17 | 18 | /*! 19 | * \brief The Query class 20 | * is the prime class, that manages everything and creates query classes. 21 | * Naming is not best perhaps, it shares a connection, performs SQL and 22 | * provides access to all the other rvalue-only generators. They are rvalue-only, 23 | * because reusing them is undefined in terms of common sense, but you can reuse this Query class. 24 | * It is designed to be used somewhere locally when needed, opens/closes it's shared connection 25 | * on construction/destruction. Provides all the basics, CRUD + transactions. It is supposed 26 | * that all tables have primary key, not that it won't work without those, but the classes were 27 | * tesed on the data where they exist, use-case was the similar. 28 | */ 29 | class Query 30 | { 31 | Q_DISABLE_COPY(Query) 32 | public: 33 | /*! 34 | * \brief Query -- constructor, opens db connection, throws std::runtime_error if database was not opened 35 | * \param tableName -- name of the table to be used in the current set of queries 36 | * \param pkey -- primary key name, in case Qt will not be able to determine it 37 | */ 38 | Query(const QString& tableName = QString(), const QString& pkey = QString()); 39 | 40 | /*! 41 | * \brief ~Query -- note: the destructors closes the shared connection 42 | */ 43 | ~Query(); 44 | 45 | Query(Query&&); 46 | Query& operator=(Query&&) = default; 47 | 48 | /*! 49 | * \brief setQueryLoggingEnabled -- globally enables debug logging of SQL queries via qDebug() 50 | * \param enabled -- logging enabled/disabled 51 | */ 52 | static void setQueryLoggingEnabled(bool enabled); 53 | 54 | /*! 55 | * \brief select -- creates SELECT query generator 56 | * \param fields -- column names in SELECT ... FROM 57 | * \return -- select generator 58 | */ 59 | Selector select(const QStringList& fields = QStringList()) const; 60 | 61 | /*! 62 | * \brief insert -- creates INSERT query generator 63 | * \param fields -- column names in INSERT INTO tbl (...) 64 | * \return -- insert query generator 65 | */ 66 | Inserter insert(const QStringList& fields) const; 67 | 68 | /*! 69 | * \brief delete_ -- creates DELETE query generator 70 | * \param whereClause -- "WHERE ..." clause (see OP namespace for details) 71 | * \return -- delete query generator 72 | */ 73 | Deleter delete_(OP::Clause&& whereClause) const; 74 | 75 | /*! 76 | * \brief update -- creates UPDATE query generator 77 | * \param updateValues -- map of [column : value] to be set 78 | * \return -- update query generator 79 | */ 80 | Updater update(const QVariantMap& updateValues) const; 81 | 82 | /*! 83 | * \brief transact -- executes the given commands in a trancation 84 | * \param operations -- some callable, containing queries' execution 85 | * \return -- success/failure of the transaction 86 | */ 87 | bool transact(std::function&& operations); 88 | 89 | public: 90 | /*! 91 | * \brief performSQL -- performs *raw* SQL, because not all use-cases can be covered 92 | * in the current library implementation in principle. Also used internally by the generators. 93 | * \param sql -- string with SQL query to be executed 94 | * \return -- Qt's query object with the state of the query 95 | */ 96 | QSqlQuery performSQL(const QString& sql) const; 97 | 98 | /*! 99 | * \brief lastError -- wrapper method for obtaining last error of the last query 100 | * \return -- last QSqlQuery's lastError() 101 | */ 102 | QSqlError lastError() const; 103 | 104 | /*! 105 | * \brief hasError -- convenience method th check if last query had any errors while executed 106 | * \return -- ok/failure 107 | */ 108 | bool hasError() const; 109 | 110 | /*! 111 | * \brief tableName -- name of the table, chosen as primary queries' target 112 | * \return -- name of the table, of course 113 | */ 114 | QString tableName() const; 115 | 116 | /*! 117 | * \brief primaryKeyName -- name of the primary key column, set for the chosen table 118 | * \return -- primary key column name 119 | */ 120 | QString primaryKeyName() const; 121 | 122 | /*! 123 | * \brief columnNames -- list of columns' names of the the chosen table 124 | * \return -- returns as described above 125 | */ 126 | QStringList columnNames() const; 127 | 128 | /*! 129 | * \brief tableColumnNames -- helper method, can get a list of column's names for arbitrary existing table 130 | * \param tableName -- name of the table to be examined 131 | * \return -- returns as supposed 132 | */ 133 | QStringList tableColumnNames(const QString& tableName) const; 134 | 135 | private: 136 | static QSqlDatabase& defaultConnection(); 137 | static bool LOG_QUERIES; 138 | 139 | private: 140 | struct QueryPrivate; 141 | std::unique_ptr impl; 142 | }; 143 | -------------------------------------------------------------------------------- /sqlbuilder/Selector.cpp: -------------------------------------------------------------------------------- 1 | #include "Selector.h" 2 | #include "Query.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | struct Selector::SelectorPrivate 9 | { 10 | SelectorPrivate(const Query* q, const QStringList& fields) 11 | : m_query(q) 12 | , m_fields(!fields.isEmpty() ? fields : q->columnNames()) 13 | , m_where{""} 14 | , m_limit{""} 15 | , m_order{""} 16 | , m_having{""} 17 | , m_groupBy{""} 18 | , m_offset{""} 19 | { } 20 | 21 | struct JoinPart 22 | { 23 | QString m_sql; 24 | QString m_joinTable; 25 | bool m_joinDisambigToOther; 26 | }; 27 | 28 | const Query* m_query; 29 | QStringList m_fields; 30 | 31 | QString m_where; 32 | QString m_limit; 33 | QString m_order; 34 | 35 | QString m_having; 36 | QString m_groupBy; 37 | QString m_offset; 38 | 39 | QList m_joinParts; 40 | 41 | // This should resolve disambiduation in column names 42 | void resolveColumnDisambiguation() 43 | { 44 | QSet thisColumnSet = QSet::fromList(m_query->columnNames()); 45 | for (const auto& part: m_joinParts) 46 | { 47 | QSet otherColumnSet = QSet::fromList(m_query->tableColumnNames(part.m_joinTable)); 48 | otherColumnSet.intersect(thisColumnSet); 49 | 50 | for(int i = 0; i < m_fields.count(); ++i) 51 | { 52 | QString fieldName = m_fields[i]; 53 | if (otherColumnSet.contains(fieldName)) 54 | { 55 | QString resolutionFieldName = QString("%1.%2") 56 | .arg(!part.m_joinDisambigToOther 57 | ? m_query->tableName() 58 | : part.m_joinTable) 59 | .arg(fieldName); 60 | // hacky a bit, but IT WORKS 61 | m_where.replace(fieldName, resolutionFieldName); 62 | m_where.replace('.', "\".\""); 63 | m_fields.replace(i, resolutionFieldName); 64 | } 65 | } 66 | } 67 | } 68 | 69 | //------- 70 | 71 | QString getJoinTail() const 72 | { 73 | QString result; 74 | 75 | if (!m_joinParts.isEmpty()) 76 | { 77 | QStringList joiner; 78 | for (const auto& part: m_joinParts) 79 | joiner << part.m_sql; 80 | result = joiner.join(' '); 81 | } 82 | 83 | return result; 84 | } 85 | }; 86 | 87 | /***************************************************************************************/ 88 | 89 | const QString Selector::SELECT_SQL { "SELECT %1 FROM %2 %3 WHERE %4 %5;" }; 90 | 91 | Selector::Selector(const Query* q, const QStringList& fields) 92 | : impl(new SelectorPrivate(q, fields)) 93 | { } 94 | 95 | Selector::~Selector() 96 | { } 97 | 98 | Selector::Selector(Selector &&) = default; 99 | 100 | Selector Selector::join(const QString& otherTable, const std::pair& joinColumns, Join::JoinType joinType, bool resolveDisambigToOther) 101 | { 102 | SelectorPrivate::JoinPart part; 103 | 104 | part.m_joinTable = otherTable; 105 | part.m_joinDisambigToOther = resolveDisambigToOther; 106 | part.m_sql = QString("%1 JOIN %2 on %3") 107 | .arg(QVariant::fromValue(joinType).toString()) 108 | .arg(otherTable) 109 | .arg(QString("\"%1\".\"%2\"=\"%3\".\"%4\"") 110 | .arg(impl->m_query->tableName(), joinColumns.first, otherTable, joinColumns.second)); 111 | 112 | impl->m_joinParts.append(part); 113 | return std::move(*this); 114 | } 115 | 116 | Selector Selector::where(OP::Clause&& clause) && 117 | { 118 | impl->m_where = std::move(clause).getSQl(); 119 | return std::move(*this); 120 | } 121 | 122 | Selector Selector::limit(int count) && 123 | { 124 | impl->m_limit = count > 0 125 | ? QString("LIMIT %1").arg(count) 126 | : ""; 127 | return std::move(*this); 128 | } 129 | 130 | Selector Selector::orderBy(const QString& field, Order::OrderType selectOrder) && 131 | { 132 | impl->m_order = QString("ORDER BY %1 %2").arg(field).arg(QVariant::fromValue(selectOrder).toString()); 133 | return std::move(*this); 134 | } 135 | 136 | Selector Selector::groupBy(const QString& field) && 137 | { 138 | impl->m_groupBy = QString("GROUP BY %1").arg(field); 139 | return std::move(*this); 140 | } 141 | 142 | Selector Selector::having(const QString& havingClause) && 143 | { 144 | impl->m_having = QString("HAVING %1").arg(havingClause); 145 | return std::move(*this); 146 | } 147 | 148 | Selector Selector::offset(int offset) && 149 | { 150 | impl->m_offset = QString("OFFSET %1").arg(offset); 151 | return std::move(*this); 152 | } 153 | 154 | QVariantList Selector::perform() && 155 | { 156 | QVariantList result; 157 | impl->resolveColumnDisambiguation(); 158 | 159 | const QStringList tail = QStringList() 160 | << impl->m_groupBy 161 | << impl->m_having 162 | << impl->m_order 163 | << impl->m_limit 164 | << impl->m_offset; 165 | 166 | const QString sql = Selector::SELECT_SQL 167 | .arg(impl->m_fields.join(", ")) 168 | .arg(impl->m_query->tableName()) 169 | .arg(impl->getJoinTail()) 170 | .arg(impl->m_where.isEmpty() ? "True" : impl->m_where) 171 | .arg(tail.join(" ")); 172 | 173 | QSqlQuery q = impl->m_query->performSQL(sql); 174 | QSqlRecord r = q.record(); 175 | 176 | while(q.next()) 177 | { 178 | QVariantMap resultRow; 179 | for(int i = 0; i < r.count(); ++i) 180 | resultRow[r.fieldName(i)] = q.value(i); 181 | 182 | result.append(resultRow); 183 | } 184 | 185 | return result; 186 | } 187 | -------------------------------------------------------------------------------- /sqlbuilder/Selector.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "Where.h" 7 | QT_FORWARD_DECLARE_CLASS(Query) 8 | 9 | //--------------------------- *** helpers go here *** ----------------------------------// 10 | 11 | /*! 12 | * \brief The Join class 13 | * is a convenient wrapper for JOIN 14 | * type enum. Most common are supported. 15 | */ 16 | class Join 17 | { 18 | Q_GADGET 19 | public: 20 | /*! 21 | * \brief The JoinType enum 22 | * (surprise) enums JOIN types 23 | */ 24 | enum JoinType 25 | { 26 | INNER, // INNER JOIN 27 | LEFT, // LEFT JOIN 28 | RIGHT, // RIGHT JOIN 29 | CROSS // CROSS JOIN (if supported) 30 | }; 31 | Q_ENUM(JoinType) 32 | }; 33 | 34 | //--- 35 | 36 | /*! 37 | * \brief The Order class 38 | * is a convenient wrapper for ORDER BY 39 | * type enum. Most common are supported. 40 | */ 41 | class Order 42 | { 43 | Q_GADGET 44 | public: 45 | /*! 46 | * \brief The OrderType enum 47 | * (surprise) enums ORDER BY options 48 | */ 49 | enum OrderType 50 | { 51 | ASC, // ORDER BY *ASC* 52 | DESC // ORDER BY *DESC* 53 | }; 54 | Q_ENUM(OrderType) 55 | }; 56 | 57 | /***************************************************************************************/ 58 | 59 | /*! 60 | * \brief The Selector class 61 | * is the most complex generator class, the SELECT query generator. 62 | * Supports basics (check the methods' names), tries to resolve disambiguation in 63 | * JOINs and multiple JOINs (but do not use both at once, it probably would not fail 64 | * but it would mean that you're doing something terribly wrong in terms of common sense). 65 | * Unlike in SQL methods' call order does not matter (if you are not trying something strange 66 | * with JOINs, as mentioned above). Calling same method more than once rewrites the data with 67 | * the last parameters (but not with join() -- it stores all the parts). Assume this generator 68 | * as an implicit state machine (all the generators work that way), but don't think too much about it. 69 | * If you are carefull, it is even possible to use "SELECT smth AS alias ..." and "SELECT SUM(...)" 70 | * queries. The returned maps of data have same string keys as the column names you've provided and/or 71 | * aliases, the disamiguation (I need to repeat it) is *attemted* to be resoled, but don't abuse it, 72 | * the implementation if far from perfect, mostly because it is a rare use-case. 73 | * NOTICE: "target table" below means the one you've specified in the Query() constructor. 74 | */ 75 | class Selector 76 | { 77 | Q_DISABLE_COPY(Selector) 78 | public: 79 | /*! 80 | * \brief Selector -- constructor of the generator, don't use it manually 81 | * \param q -- ptr to the Query class, that created it 82 | * \param fields --list of column names in SELECT ... FROM 83 | */ 84 | explicit Selector(const Query* q, const QStringList& fields); 85 | ~Selector(); 86 | 87 | Selector(Selector&&); 88 | Selector& operator=(Selector&&) = default; 89 | 90 | /*! 91 | * \brief join -- adds "JOIN ..." part, use thoughtfully 92 | * \param otherTable -- another table name name to be joined with 93 | * \param joinColumns -- column names to join by. TARGET table column name goes FIRST, 94 | * OTHER table column name goes SECOND. That's how it is designed to work. 95 | * \param joinType -- INNER/LEFT/RIGHT/CROSS join type 96 | * \param resolveDisambigToOther -- is a tricky flag for rare use cases. By default if you specify a column in 97 | * your SELECT with the name, that already exists in the other table being joined, generator supposes you've meant *target* table's 98 | * column. Setting this flag to 'true' result in generator supposing otherwise. That's how it works with TWO tables. If you try it 99 | * with multiple JOINs and set the flag to 'true', the generator would resolve the problem column **to the first external table, mentioned in join**. 100 | * Simply put -- you add multiple join() calls, you set this flag to 'true' in any of them, generator resolves everything to the table name 101 | * **specified in the first join() call**, whatever it be and wherever you set the flag. Contrintuitional? Yes. But WHY would you want to achieve 102 | * something THAT strange and curious in your real query? Don't see the purpose, really. So, it's been left that way to fit two purposes: 103 | * 1) make join of TWO tables fully customizable 2) prevent any unhandled failure in case of misuse 104 | * \return -- this generator as rvalue to be reused 105 | */ 106 | Selector join(const QString& otherTable, const std::pair& joinColumns 107 | , Join::JoinType joinType, bool resolveDisambigToOther = false); 108 | 109 | /*! 110 | * \brief where -- "WHERE ..." clause (see OP namespace for details) 111 | * \param clause -- some aggregated clause 112 | * \return -- this generator as rvalue to be reused 113 | */ 114 | Selector where(OP::Clause&& clause) &&; 115 | 116 | /*! 117 | * \brief limit -- "LIMIT x" part 118 | * \param count -- limit value 119 | * \return -- this generator as rvalue to be reused 120 | */ 121 | Selector limit(int count) &&; 122 | 123 | /*! 124 | * \brief orderBy -- "ORDER BY ... ASC/DESC" part 125 | * \param field -- column name for ordering by 126 | * \param selectOrder -- ASC/DESC order type 127 | * \return -- this generator as rvalue to be reused 128 | */ 129 | Selector orderBy(const QString& field, Order::OrderType selectOrder) &&; 130 | 131 | /*! 132 | * \brief groupBy -- "GROUP BY ..." part 133 | * \param field -- column name to be grouped by 134 | * \return -- this generator as rvalue to be reused 135 | */ 136 | Selector groupBy(const QString& field) &&; 137 | 138 | /*! 139 | * \brief having -- "HAVING ..." part 140 | * \param havingClause -- some manually-written clause as string 141 | * \return -- this generator as rvalue to be reused 142 | */ 143 | Selector having(const QString& havingClause) &&; 144 | 145 | /*! 146 | * \brief offset -- "... OFFSET x" part 147 | * \param offset -- offset valuse 148 | * \return -- this generator as rvalue to be reused 149 | */ 150 | Selector offset(int offset) &&; 151 | 152 | /*! 153 | * \brief perform -- executes the query, returning the data 154 | * \return -- list of QVariantMaps with keys similar to columns & aliases provided earlier 155 | */ 156 | QVariantList perform() &&; 157 | 158 | private: 159 | struct SelectorPrivate; 160 | std::unique_ptr impl; 161 | 162 | static const QString SELECT_SQL; 163 | }; 164 | -------------------------------------------------------------------------------- /sqlbuilder/Updater.cpp: -------------------------------------------------------------------------------- 1 | #include "Updater.h" 2 | #include "Query.h" 3 | 4 | #include 5 | 6 | struct Updater::UpdaterPrivate 7 | { 8 | UpdaterPrivate(const Query* q, const QVariantMap& updateValues) 9 | : m_query(q) 10 | , m_updateValues(updateValues) 11 | {} 12 | 13 | const Query* m_query; 14 | const QVariantMap m_updateValues; 15 | 16 | QString m_where; 17 | }; 18 | 19 | const QString Updater::UPDATE_SQL { "UPDATE %1 SET %2 WHERE %3" }; 20 | 21 | /***************************************************************************************/ 22 | 23 | Updater::Updater(const Query* q, const QVariantMap& updateValues) 24 | : impl(new UpdaterPrivate(q, updateValues)) 25 | { } 26 | 27 | Updater::~Updater() 28 | { } 29 | 30 | Updater::Updater(Updater &&) = default; 31 | 32 | Updater Updater::where(OP::Clause&& clause) && 33 | { 34 | impl->m_where = std::move(clause).getSQl(); 35 | return std::move(*this); 36 | } 37 | 38 | bool Updater::perform() && 39 | { 40 | QStringList setPart; 41 | for(const QString& key : impl->m_updateValues.keys()) 42 | { 43 | setPart << QString("\"%1\"=%2") 44 | .arg(key) 45 | .arg(OP::Clause::escapeValue(impl->m_updateValues[key])); 46 | } 47 | 48 | const QString sql = Updater::UPDATE_SQL 49 | .arg(impl->m_query->tableName()) 50 | .arg(setPart.join(',')) 51 | .arg(impl->m_where.isEmpty() ? "True" : impl->m_where); 52 | 53 | QSqlQuery q = impl->m_query->performSQL(sql); 54 | return q.numRowsAffected() > 0; 55 | } 56 | -------------------------------------------------------------------------------- /sqlbuilder/Updater.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "Where.h" 7 | QT_FORWARD_DECLARE_CLASS(Query) 8 | 9 | /*! 10 | * \brief The Updater class 11 | * is an UPDATE query generator. Works as all the 12 | * other generators, supports clauses. 13 | * IMPORTANT: if you forget to call "where()" and specify some correct update 14 | * values, you'll get ALL OF YOU TABLE updated. That's how SQL works, but 15 | * here it is a bit less obvious way to shoot your foot badly. 16 | */ 17 | class Updater 18 | { 19 | Q_DISABLE_COPY(Updater) 20 | public: 21 | /*! 22 | * \brief Updater -- constructor of the generator, don't use it manually 23 | * \param q -- ptr to the Query class, that created it 24 | * \param updateValues -- map of values like in "...SET column_name = 'value',...", maps 25 | * (obviously) column names on values being set 26 | */ 27 | Updater(const Query* q, const QVariantMap& updateValues); 28 | ~Updater(); 29 | 30 | Updater(Updater&&); 31 | Updater& operator=(Updater&&) = default; 32 | 33 | /*! 34 | * \brief where -- "WHERE ..." clause (see OP namespace for details) 35 | * \param clause -- some aggregated clause 36 | * \return -- this generator as rvalue to be reused 37 | */ 38 | Updater where(OP::Clause&& clause) &&; 39 | 40 | /*! 41 | * \brief perform -- executes the generated query 42 | * \return -- success/failure of the query (affected rows > 0) 43 | */ 44 | bool perform() &&; 45 | 46 | private: 47 | struct UpdaterPrivate; 48 | std::unique_ptr impl; 49 | 50 | static const QString UPDATE_SQL; 51 | }; 52 | 53 | -------------------------------------------------------------------------------- /sqlbuilder/Where.cpp: -------------------------------------------------------------------------------- 1 | #include "Where.h" 2 | #include "Query.h" 3 | 4 | #include 5 | 6 | namespace OP 7 | { 8 | 9 | Clause Clause::operator!() && 10 | { 11 | m_sql = QString("NOT (%1)").arg(m_sql); 12 | return std::move(*this); 13 | } 14 | 15 | Clause Clause::operator&&(Clause&& other) && 16 | { 17 | m_sql = QString("(%1) AND (%2)").arg(m_sql, other.m_sql); 18 | return std::move(*this); 19 | } 20 | 21 | Clause Clause::operator||(Clause&& other) && 22 | { 23 | m_sql = QString("(%1) OR (%2)").arg(m_sql, other.m_sql); 24 | return std::move(*this); 25 | } 26 | 27 | QString Clause::getSQl() && 28 | { 29 | return std::move(m_sql); 30 | } 31 | 32 | QString Clause::escapeValue(const QVariant& value) 33 | { 34 | /* Honestly taken from QSqlDriver class */ 35 | const QString NULL_STR = "NULL"; 36 | 37 | if (value.isNull()) 38 | return NULL_STR; 39 | 40 | QString result; 41 | 42 | switch(value.type()) 43 | { 44 | case QVariant::Int: 45 | case QVariant::UInt: 46 | result = value.toString(); 47 | break; 48 | 49 | case QVariant::Date: 50 | result = value.toDate().isValid() 51 | ? value.toDate().toString(Qt::ISODate) 52 | : NULL_STR; 53 | break; 54 | case QVariant::Time: 55 | result = value.toTime().isValid() 56 | ? value.toTime().toString(Qt::ISODate) 57 | : NULL_STR; 58 | break; 59 | case QVariant::DateTime: 60 | result = value.toDateTime().isValid() 61 | ? value.toDateTime().toString(Qt::ISODate) 62 | : NULL_STR; 63 | break; 64 | 65 | case QVariant::String: 66 | case QVariant::Char: 67 | result = value.toString().trimmed().replace('\'', "''"); 68 | break; 69 | 70 | case QVariant::Bool: 71 | result = QString::number(value.toBool()); 72 | break; 73 | 74 | case QVariant::ByteArray: 75 | { 76 | static const char hexchars[] = "0123456789abcdef"; 77 | QString res; 78 | QByteArray ba = value.toByteArray(); 79 | for (int i = 0; i < ba.size(); ++i) 80 | { 81 | uchar s = static_cast(ba[i]); 82 | res += QLatin1Char(hexchars[s >> 4]); 83 | res += QLatin1Char(hexchars[s & 0x0f]); 84 | } 85 | result = res; 86 | break; 87 | } 88 | 89 | default: 90 | result = value.toString(); 91 | } 92 | 93 | return QString("'%1'").arg(result); 94 | } 95 | 96 | Clause EQ(const QString& fieldName, const QVariant& value) 97 | { 98 | return Clause{fieldName, "=", Clause::escapeValue(value.toString())}; 99 | } 100 | 101 | Clause NEQ(const QString& fieldName, const QVariant& value) 102 | { 103 | return Clause{fieldName, "!=", Clause::escapeValue(value.toString())}; 104 | } 105 | 106 | Clause LT(const QString& fieldName, const QVariant& value) 107 | { 108 | return Clause{fieldName, "<", Clause::escapeValue(value.toString())}; 109 | } 110 | 111 | Clause GT(const QString& fieldName, const QVariant& value) 112 | { 113 | return Clause{fieldName, ">", Clause::escapeValue(value.toString())}; 114 | } 115 | 116 | Clause LE(const QString& fieldName, const QVariant& value) 117 | { 118 | return Clause{fieldName, "<=", Clause::escapeValue(value.toString())}; 119 | } 120 | 121 | Clause GE(const QString& fieldName, const QVariant& value) 122 | { 123 | return Clause{fieldName, ">=", Clause::escapeValue(value.toString())}; 124 | } 125 | 126 | Clause IN(const QString& fieldName, const QVariantList& values) 127 | { 128 | QStringList stringValues; 129 | for (const QVariant& value : values) 130 | stringValues << Clause::escapeValue(value); 131 | 132 | return Clause{fieldName, "IN", QString("(%1)").arg(stringValues.join(','))}; 133 | } 134 | 135 | Clause IS_NULL(const QString& fieldName) 136 | { 137 | return Clause{fieldName, "IS", Clause::escapeValue(QVariant())}; 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /sqlbuilder/Where.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | /*! 7 | * Here goes a namespase of helpers that implement "WHERE ..." support 8 | * in all the generators. Can be extended with other conditions, perhaps. 9 | */ 10 | namespace OP 11 | { 12 | 13 | /*! 14 | * \brief The Clause class 15 | * is an internal class, that translates C++ boolean logic into "WHERE ..." statements. 16 | * It optimizes nothing, no syntax tree parsing here, so don't abuse your queries with too much 17 | * of clauses. Executed clause is guaranteed to have the same interpretation when calculated by 18 | * database as you've written in your sources. Just using braces here =) 19 | */ 20 | class Clause 21 | { 22 | public: 23 | /*! 24 | * \brief Clause -- is a constructor of smth like " col='val' " 25 | * \param field -- column name in the clause 26 | * \param op -- clause operation (=,>,<, IN, etc.) 27 | * \param value -- value in the clause 28 | */ 29 | Clause(const QString& field, const QString& op, const QString& value) 30 | : m_sql(QString("\"%1\" %2 %3").arg(field, op, value)) 31 | { } 32 | 33 | /*! 34 | * \brief operator ! -- is a SQL negation, uses "NOT (...)" 35 | * \return -- this mini-generator as rvalue to be reused 36 | */ 37 | Clause operator!() &&; 38 | 39 | /*! 40 | * \brief operator ! -- is a SQL logical conjunction, uses "(...) AND (...)" 41 | * \return -- this mini-generator as rvalue to be reused 42 | */ 43 | Clause operator&&(Clause&& other) &&; 44 | 45 | /*! 46 | * \brief operator ! -- is a SQL logical union, uses "(...) OR (...)" 47 | * \return -- this mini-generator as rvalue to be reused 48 | */ 49 | Clause operator||(Clause&& other) &&; 50 | 51 | /*! 52 | * \brief getSQl -- returns the curently generated SQL 53 | * \return -- string with the accumulated clause 54 | */ 55 | QString getSQl() &&; 56 | 57 | /*! 58 | * \brief escapeValue -- escapes values due to database rules. Honestly taken from 59 | * the QSqlDriver class (same idea, written in a more simple way). Has not been tested fully, 60 | * the target was PostgreSQL, so binary data may not be supported by other DB engines. It's not 61 | * only used by the clauses, also internally used in other generators. 62 | * DB identifiers are just escaped like "id" everywhere through the code. 63 | * \param value -- value to be escaped propely 64 | * \return -- string represetnation of the value to be used 65 | */ 66 | static QString escapeValue(const QVariant& value); 67 | 68 | private: 69 | QString m_sql; 70 | }; 71 | 72 | /*! 73 | * \brief EQ -- helper, that constructs " col='val' " clause part 74 | * \param fieldName -- column name 75 | * \param value -- value to be used 76 | * \return -- clause entity (see the above class) 77 | */ 78 | Clause EQ(const QString& fieldName, const QVariant& value); 79 | 80 | /*! 81 | * \brief NEQ -- helper, that constructs " col!='val' " clause part 82 | * \param fieldName -- column name 83 | * \param value -- value to be used 84 | * \return -- clause entity (see the above class) 85 | */ 86 | Clause NEQ(const QString& fieldName, const QVariant& value); 87 | 88 | /*! 89 | * \brief LT -- helper, that constructs " col<'val' " clause part 90 | * \param fieldName -- column name 91 | * \param value -- value to be used 92 | * \return -- clause entity (see the above class) 93 | */ 94 | Clause LT(const QString& fieldName, const QVariant& value); 95 | 96 | /*! 97 | * \brief GT -- helper, that constructs " col>'val' " clause part 98 | * \param fieldName -- column name 99 | * \param value -- value to be used 100 | * \return -- clause entity (see the above class) 101 | */ 102 | Clause GT(const QString& fieldName, const QVariant& value); 103 | 104 | /*! 105 | * \brief LE -- helper, that constructs " col<='val' " clause part 106 | * \param fieldName -- column name 107 | * \param value -- value to be used 108 | * \return -- clause entity (see the above class) 109 | */ 110 | Clause LE(const QString& fieldName, const QVariant& value); 111 | 112 | /*! 113 | * \brief GE -- helper, that constructs " col>='val' " clause part 114 | * \param fieldName -- column name 115 | * \param value -- value to be used 116 | * \return -- clause entity (see the above class) 117 | */ 118 | Clause GE(const QString& fieldName, const QVariant& value); 119 | 120 | /*! 121 | * \brief IN -- helper, that constructs "col IN ('val1', 'val2', ...)" clause part 122 | * \param fieldName -- column name 123 | * \param values -- values to be used 124 | * \return -- clause entity (see the above class) 125 | */ 126 | Clause IN(const QString& fieldName, const QVariantList& values); 127 | 128 | /*! 129 | * \brief EQ -- helper, that constructs "col IS NULL" clause part 130 | * \param fieldName -- column name 131 | * \return -- clause entity (see the above class) 132 | */ 133 | Clause IS_NULL(const QString& fieldName); 134 | 135 | } //namespace OP 136 | -------------------------------------------------------------------------------- /sqlbuilder/sqlbuilder.pro: -------------------------------------------------------------------------------- 1 | DESTDIR = $$PWD/../bin 2 | 3 | QT += core sql 4 | 5 | TARGET = sqlbuilder 6 | TEMPLATE = lib 7 | CONFIG += staticlib c++11 8 | 9 | SOURCES += \ 10 | Config.cpp \ 11 | Query.cpp \ 12 | Selector.cpp \ 13 | Where.cpp \ 14 | Inserter.cpp \ 15 | Deleter.cpp \ 16 | Updater.cpp 17 | 18 | HEADERS += \ 19 | Config.h \ 20 | Query.h \ 21 | Selector.h \ 22 | Where.h \ 23 | Inserter.h \ 24 | Deleter.h \ 25 | Updater.h 26 | 27 | DEFINES *= QT_USE_QSTRINGBUILDER 28 | -------------------------------------------------------------------------------- /sqlbuilder_include.pri: -------------------------------------------------------------------------------- 1 | QT += sql 2 | 3 | SQLBUILDER_DIR = $$PWD/sqlbuilder 4 | 5 | HEADERS += \ 6 | $$SQLBUILDER_DIR/Config.h \ 7 | $$SQLBUILDER_DIR/Query.h \ 8 | $$SQLBUILDER_DIR/Where.h \ 9 | $$SQLBUILDER_DIR/Selector.h \ 10 | $$SQLBUILDER_DIR/Inserter.h \ 11 | $$SQLBUILDER_DIR/Deleter.h 12 | 13 | INCLUDEPATH *= $$SQLBUILDER_DIR 14 | 15 | LIBS += \ 16 | -L$$DESTDIR \ 17 | -lsqlbuilder 18 | -------------------------------------------------------------------------------- /test/test.pro: -------------------------------------------------------------------------------- 1 | DESTDIR = $$PWD/../bin 2 | 3 | QT += core testlib 4 | 5 | CONFIG += c++11 qt console warn_on depend_includepath testcase 6 | CONFIG -= app_bundle 7 | 8 | SOURCES += \ 9 | tst_builder_test.cpp 10 | 11 | include($$PWD/../sqlbuilder_include.pri) 12 | 13 | DEFINES *= QT_FORCE_ASSERTS 14 | -------------------------------------------------------------------------------- /test/tst_builder_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "Config.h" 12 | #include "Query.h" 13 | #include "Selector.h" 14 | #include "Inserter.h" 15 | #include "Deleter.h" 16 | #include "Updater.h" 17 | 18 | class builder_test : public QObject 19 | { 20 | Q_OBJECT 21 | 22 | public: 23 | builder_test() 24 | : m_showDebug(true) 25 | , TARGET_TABLE("some_object") 26 | , SECOND_TABLE("other_table") 27 | , THIRD_TABLE("third_table") 28 | {} 29 | 30 | ~builder_test() 31 | {} 32 | 33 | private slots: 34 | void initTestCase(); 35 | void cleanupTestCase(); 36 | 37 | void test_insert_sql(); 38 | 39 | void test_raw_sql(); 40 | void test_select_basic(); 41 | void test_star_selection(); 42 | 43 | void test_simple_where(); 44 | void test_comlex_where(); 45 | 46 | void test_is_null(); 47 | 48 | void test_delete_simple(); 49 | void test_insert_delete(); 50 | 51 | void test_error_reporting(); 52 | void test_insert_wrong_usage(); 53 | 54 | void test_update_simple(); 55 | void test_full_cycle(); 56 | 57 | void test_transcations(); 58 | void test_nested_transactions(); 59 | void test_column_getter(); 60 | void test_select_functions(); 61 | 62 | void test_join(); 63 | void test_join_complex(); 64 | void test_multiple_joins(); 65 | 66 | private: 67 | bool m_showDebug; 68 | 69 | const QString TARGET_TABLE; 70 | const QString SECOND_TABLE; 71 | const QString THIRD_TABLE; // used 4 multi-joins 72 | }; 73 | 74 | void builder_test::initTestCase() 75 | { 76 | /* setup test tables */ 77 | 78 | const QString CONN_NAME {"LOL_TEST"}; 79 | { 80 | QSqlDatabase conn = QSqlDatabase::addDatabase("QPSQL", CONN_NAME); 81 | conn.setHostName("127.0.0.1"); 82 | conn.setDatabaseName("ksvd4db"); 83 | conn.setUserName("postgres"); 84 | conn.setPassword(""); 85 | 86 | if (!conn.open()) 87 | qCritical() << "DATABASE IS NOT AVAILABLE"; 88 | else 89 | { 90 | const QString dbRole{"su"}; 91 | QSqlQuery query(conn); 92 | 93 | if (!conn.tables().contains(TARGET_TABLE)) 94 | { 95 | const QString createSQL = QString("CREATE TABLE %1 \ 96 | ( \ 97 | _id serial NOT NULL, \ 98 | _otype integer NOT NULL, \ 99 | _parent integer, \ 100 | guid text NOT NULL, \ 101 | name text NOT NULL, \ 102 | descr text, \ 103 | CONSTRAINT some_object_pkey PRIMARY KEY (_id) \ 104 | );").arg(TARGET_TABLE); 105 | 106 | if (!query.exec(createSQL)) 107 | { 108 | qCritical() << "table1 creation failed: " << query.lastError().text(); 109 | throw std::runtime_error("TEST TABLE1 CREATION FAILED"); 110 | } 111 | 112 | const QString ownerSQL = QString("ALTER TABLE %1 OWNER to %2;").arg(TARGET_TABLE, dbRole); 113 | 114 | if (!query.exec(ownerSQL)) 115 | qCritical() << "owner change failed"; 116 | 117 | const QString grantSQL = QString("GRANT ALL ON TABLE %1 TO %2;").arg(TARGET_TABLE, dbRole); 118 | 119 | if (!query.exec(grantSQL)) 120 | qCritical() << "grant role failed"; 121 | } 122 | 123 | if (!conn.tables().contains(SECOND_TABLE)) 124 | { 125 | const QString createThirdSql = QString("CREATE TABLE %1 \ 126 | ( \ 127 | _id serial NOT NULL, \ 128 | some_date date NOT NULL DEFAULT CURRENT_DATE, \ 129 | CONSTRAINT third_table_pkey PRIMARY KEY (_id) \ 130 | )").arg(THIRD_TABLE); 131 | if (!query.exec(createThirdSql)) 132 | { 133 | qCritical() << "table3 creation failed: " << query.lastError().text(); 134 | throw std::runtime_error("TEST TABLE2 CREATION FAILED"); 135 | } 136 | } 137 | 138 | if (!conn.tables().contains(SECOND_TABLE)) 139 | { 140 | const QString createSecondSql = QString("CREATE TABLE %1 \ 141 | ( \ 142 | _id serial NOT NULL, \ 143 | some_text text NOT NULL, \ 144 | some_fkey integer NOT NULL , \ 145 | date_fkey integer, \ 146 | CONSTRAINT other_table_pkey PRIMARY KEY (_id), \ 147 | CONSTRAINT other_table__some_fkey_fkey FOREIGN KEY (some_fkey) \ 148 | REFERENCES some_object (_id) MATCH SIMPLE \ 149 | ON UPDATE CASCADE \ 150 | ON DELETE CASCADE, \ 151 | CONSTRAINT other_table__date_fkey_fkey FOREIGN KEY (date_fkey) \ 152 | REFERENCES third_table (_id) MATCH SIMPLE \ 153 | ON UPDATE CASCADE \ 154 | ON DELETE SET NULL \ 155 | )").arg(SECOND_TABLE); 156 | if (!query.exec(createSecondSql)) 157 | { 158 | qCritical() << "table2 creation failed: " << query.lastError().text(); 159 | throw std::runtime_error("TEST TABLE2 CREATION FAILED"); 160 | } 161 | } 162 | qInfo() << "sample tables prepared"; 163 | } 164 | } 165 | QSqlDatabase::removeDatabase(CONN_NAME); 166 | 167 | // configure connection 4 library classes 168 | Config::setConnectionParams("QPSQL", "127.0.0.1", "ksvd4db", "postgres", ""); 169 | Query::setQueryLoggingEnabled(true); 170 | 171 | Query().performSQL(QString("TRUNCATE TABLE %1 CASCADE;").arg(TARGET_TABLE)); 172 | } 173 | 174 | void builder_test::cleanupTestCase() 175 | { 176 | /* Uncomment to make real cleanup, choose appropriate */ 177 | 178 | Query(TARGET_TABLE).performSQL(QString("TRUNCATE TABLE %1;").arg(TARGET_TABLE)); 179 | 180 | // -- or full clean -- 181 | 182 | // Query(TARGET_TABLE).performSQL(QString("DROP TABLE %1;").arg(TARGET_TABLE)); 183 | // Query(TARGET_TABLE).performSQL(QString("DROP TABLE %1;").arg(SECOND_TABLE)); 184 | // Query(TARGET_TABLE).performSQL(QString("DROP TABLE %1;").arg(THIRD_TABLE)); 185 | } 186 | 187 | void builder_test::test_insert_sql() 188 | { 189 | auto res = Query(TARGET_TABLE) 190 | .insert({"_otype", "guid", "name"}) 191 | .values({42, "LOL", "TEST"}) 192 | .perform(); 193 | 194 | auto resMulti = Query(TARGET_TABLE) 195 | .insert({"_otype", "guid", "name"}) 196 | .values({42, "LOL", "TEST"}) 197 | .values({66, "LOL66", "TEST66"}) 198 | .values({77, "LOL77", "TEST77"}) 199 | .perform(); 200 | 201 | if (m_showDebug) 202 | { 203 | qInfo() << "One insert: " << res; 204 | qInfo() << "Multi insert: " << resMulti; 205 | } 206 | 207 | auto query = Query(TARGET_TABLE); 208 | query.transact([&]{ 209 | for(int i=0; i < 30; ++i) 210 | { 211 | query 212 | .insert({"_otype", "guid", "name"}) 213 | .values({ 20 * rand() / RAND_MAX, QUuid::createUuid().toString(), QString("RND %1").arg(i + 1)}) 214 | .perform(); 215 | } 216 | }); 217 | } 218 | 219 | void builder_test::test_raw_sql() 220 | { 221 | Query query; 222 | QSqlQuery q = query.performSQL(QString("SELECT _id, name FROM %1 LIMIT 3;").arg(TARGET_TABLE)); 223 | 224 | if (m_showDebug) 225 | { 226 | while(q.next()) 227 | qInfo() << q.value("_id") << "\t" << q.value("name"); 228 | } 229 | 230 | Q_ASSERT(q.numRowsAffected() > 0); 231 | } 232 | 233 | void builder_test::test_select_basic() 234 | { 235 | auto res = Query(TARGET_TABLE).select(QStringList() << "_id" << "name") 236 | .orderBy("_id", Order::DESC) 237 | .limit(3) 238 | .offset(20) 239 | .perform(); 240 | Q_ASSERT(res.count() == 3); 241 | 242 | if (m_showDebug) 243 | qInfo() << QJsonDocument::fromVariant(res); 244 | } 245 | 246 | void builder_test::test_star_selection() 247 | { 248 | auto res = Query(TARGET_TABLE).select() 249 | .orderBy("_id", Order::ASC) 250 | .limit(5) 251 | .perform(); 252 | Q_ASSERT(res.count() == 5); 253 | 254 | if (m_showDebug) 255 | qInfo() << QJsonDocument::fromVariant(res); 256 | 257 | res = Query(TARGET_TABLE).select({"*"}) 258 | .orderBy("_id", Order::ASC) 259 | .limit(5) 260 | .perform(); 261 | Q_ASSERT(res.count() == 5); 262 | 263 | if (m_showDebug) 264 | qInfo() << QJsonDocument::fromVariant(res); 265 | } 266 | 267 | void builder_test::test_simple_where() 268 | { 269 | auto res = Query(TARGET_TABLE).select(QStringList() << "_id" << "name") 270 | .where(OP::EQ("_otype", 2) && (OP::LT("_id", 100) || OP::EQ("guid", "rte"))) 271 | .perform(); 272 | if (m_showDebug) 273 | qInfo() << QJsonDocument::fromVariant(res); 274 | } 275 | 276 | void builder_test::test_comlex_where() 277 | { 278 | auto res = Query(TARGET_TABLE).select({"_id", "name", "guid"}) 279 | .where(OP::IN("_id", { 2, 31, 25, 10 })).perform(); 280 | qInfo() << QJsonDocument::fromVariant(res); 281 | } 282 | 283 | void builder_test::test_is_null() 284 | { 285 | auto res = Query(TARGET_TABLE) 286 | .insert({"_otype", "guid", "name"}) 287 | .values({999, "SOME_GUID", "EMPTY_PARENTED"}) 288 | .perform(); 289 | 290 | int id = res.first(); 291 | auto check = Query(TARGET_TABLE) 292 | .select() 293 | .where(OP::EQ("_id", id) && OP::IS_NULL("_parent")) 294 | .perform(); 295 | 296 | if (m_showDebug) 297 | qInfo() << QJsonDocument::fromVariant(check); 298 | } 299 | 300 | void builder_test::test_delete_simple() 301 | { 302 | auto target = Query(TARGET_TABLE) 303 | .select({"_id"}) 304 | .limit(5) 305 | .orderBy("_id", Order::DESC) 306 | .perform(); 307 | Q_ASSERT(target.count() == 5); 308 | 309 | QVariantList ids; 310 | for (const auto& item : target) 311 | ids << item.toMap()["_id"]; 312 | 313 | bool ok = Query(TARGET_TABLE).delete_(OP::IN("_id", ids)).perform(); 314 | Q_ASSERT(ok); 315 | } 316 | 317 | void builder_test::test_insert_delete() 318 | { 319 | auto query = Query(TARGET_TABLE); 320 | auto id = query 321 | .insert({"_otype", "name", "guid"}) 322 | .values({123, "GUID_&&%$%$#$_GUID_", "TO_DELETE"}) 323 | .perform(); 324 | Q_ASSERT(id.count() > 0); 325 | Q_ASSERT(!query.lastError().isValid()); 326 | 327 | bool ok = query.delete_(OP::EQ("_id", id.first())).perform(); 328 | Q_ASSERT(ok); 329 | Q_ASSERT(!query.lastError().isValid()); 330 | } 331 | 332 | void builder_test::test_error_reporting() 333 | { 334 | auto no_query = Query("no_table"); 335 | 336 | no_query.select().perform(); 337 | Q_ASSERT(no_query.lastError().isValid()); 338 | 339 | no_query.select().join("fuu", {"lsodf", "idd"}, Join::CROSS).perform(); 340 | Q_ASSERT(no_query.lastError().isValid()); 341 | 342 | no_query.insert({"lol"}).values({112, 3333}).perform(); 343 | Q_ASSERT(no_query.lastError().isValid()); 344 | 345 | no_query.delete_(OP::GE("dfdf", 33333)).perform(); 346 | Q_ASSERT(no_query.lastError().isValid()); 347 | 348 | no_query.update({{"asdf", "asfasf"}}).perform(); 349 | Q_ASSERT(no_query.lastError().isValid()); 350 | 351 | // ----------------------------------------------------- 352 | 353 | auto query = Query(TARGET_TABLE); 354 | 355 | query.select().where(OP::EQ("fuubar", "sOmEcRaZy_sTuFf")).perform(); 356 | Q_ASSERT(query.lastError().isValid()); 357 | 358 | query.select({"_id"}).limit(1).perform(); 359 | Q_ASSERT(!query.lastError().isValid()); 360 | 361 | query.select({"_id"}).join("no_table", {"_otype", "asdfaf"}, Join::LEFT).limit(1).perform(); 362 | Q_ASSERT(query.lastError().isValid()); 363 | 364 | query.delete_(OP::EQ("sdfgsdg", 3333)).perform(); 365 | Q_ASSERT(query.lastError().isValid()); 366 | 367 | bool ok = query.delete_(OP::EQ("_id", -11)).perform(); 368 | Q_ASSERT(!ok); 369 | Q_ASSERT(!query.lastError().isValid()); 370 | 371 | query.insert({"olol"}).values({"FUUUU"}).perform(); 372 | Q_ASSERT(query.lastError().isValid()); 373 | 374 | query.update({{"sdsdg", 33} , {"hdfdfh", "asfasf"}}).perform(); 375 | Q_ASSERT(query.lastError().isValid()); 376 | 377 | query.update({{"sdsdg", 33} , {"hdfdfh", "asfasf"}}).where(OP::GT("_id", 100)).perform(); 378 | Q_ASSERT(query.lastError().isValid()); 379 | } 380 | 381 | void builder_test::test_insert_wrong_usage() 382 | { 383 | auto query = Query(TARGET_TABLE); 384 | query 385 | .insert({"_otype", "name", "guid"}) 386 | .values({ 42, "HELLO", "WORLD", "OOPS", "EXTRA", "DATA"}) 387 | .perform(); 388 | Q_ASSERT(query.lastError().isValid()); 389 | 390 | query 391 | .insert({"_otype", "name", "guid"}) 392 | .values({ 42, "OH_I_FORGOT_TO_ADD_MORE_DATA"}) 393 | .perform(); 394 | Q_ASSERT(query.lastError().isValid()); 395 | } 396 | 397 | void builder_test::test_update_simple() 398 | { 399 | const auto query = Query(TARGET_TABLE); 400 | 401 | auto res = query.select({"_id"}).orderBy("_id", Order::ASC).offset(5).perform(); 402 | int id5 = res[5].toMap()["_id"].toInt(); 403 | 404 | bool ok = query 405 | .update({{"descr", "OLOLOLO"}}) 406 | .where(OP::LE("_id", id5)) 407 | .perform(); 408 | Q_ASSERT(ok); 409 | Q_ASSERT(!query.lastError().isValid()); 410 | } 411 | 412 | void builder_test::test_full_cycle() 413 | { 414 | const auto query = Query(TARGET_TABLE); 415 | 416 | auto ids = query 417 | .insert({"_otype", "guid", "name"}) 418 | .values({42, QUuid::createUuid().toString(), "CYCLE_TEST1"}) 419 | .values({42, QUuid::createUuid().toString(), "CYCLE_TEST2"}) 420 | .values({42, QUuid::createUuid().toString(), "CYCLE_TEST3"}) 421 | .perform(); 422 | Q_ASSERT(ids.count() == 3); 423 | Q_ASSERT(!query.hasError()); 424 | 425 | QVariantList idData; 426 | for(const int& id: ids) 427 | idData << id; 428 | 429 | const QString descr = "THIS_IS_A_DESCRIPTION"; 430 | bool ok = query.update({{"descr", descr}}).where(OP::IN("_id", idData)).perform(); 431 | Q_ASSERT(ok); 432 | Q_ASSERT(!query.hasError()); 433 | 434 | auto data = query.select({"descr"}).where(OP::IN("_id", idData)).perform(); 435 | Q_ASSERT(data.count() == 3); 436 | Q_ASSERT(!query.hasError()); 437 | 438 | for(const auto& val: data) 439 | Q_ASSERT(val.toMap()["descr"].toString() == descr); 440 | 441 | ok = query.delete_(OP::IN("_id", idData)).perform(); 442 | Q_ASSERT(ok); 443 | Q_ASSERT(!query.hasError()); 444 | 445 | data = query.select({"descr"}).where(OP::IN("_id", idData)).perform(); 446 | Q_ASSERT(data.isEmpty()); 447 | Q_ASSERT(!query.hasError()); 448 | } 449 | 450 | void builder_test::test_transcations() 451 | { 452 | auto query = Query(TARGET_TABLE); 453 | 454 | const QString GOOD_NAME {"GOOD_TRANSACTION"}; 455 | 456 | bool errorCode = query.transact([&]{ 457 | auto ids = query 458 | .insert({"_otype", "guid", "name"}) 459 | .values({33, QUuid::createUuid().toString(), GOOD_NAME}) 460 | .values({33, QUuid::createUuid().toString(), GOOD_NAME}) 461 | .perform(); 462 | 463 | Q_ASSERT(ids.count() == 2); 464 | Q_ASSERT(!query.hasError()); 465 | 466 | bool d_ok = query.delete_(OP::EQ("_id", ids.first())).perform(); 467 | Q_ASSERT(d_ok); 468 | Q_ASSERT(!query.hasError()); 469 | }); 470 | Q_ASSERT(errorCode); 471 | 472 | auto check1 = query.select().where(OP::EQ("name", GOOD_NAME)).perform(); 473 | Q_ASSERT(!query.hasError()); 474 | Q_ASSERT(check1.count() == 1); 475 | 476 | const QString BAD_NAME {"BAD_TRANSACTION"}; 477 | 478 | errorCode = query.transact([&]{ 479 | auto ids = query 480 | .insert({"_otype", "guid", "name"}) 481 | .values({33, QUuid::createUuid().toString(), BAD_NAME}) 482 | .values({33, QUuid::createUuid().toString(), BAD_NAME}) 483 | .perform(); 484 | Q_ASSERT(ids.count() == 2); 485 | Q_ASSERT(!query.hasError()); 486 | 487 | bool d_ok = query.delete_(OP::EQ("_id_OOPS", ids.first())).perform(); 488 | Q_ASSERT(!d_ok); 489 | Q_ASSERT(query.hasError()); 490 | }); 491 | Q_ASSERT(!errorCode); 492 | 493 | auto check2 = query.select().where(OP::EQ("name", BAD_NAME)).perform(); 494 | Q_ASSERT(!query.hasError()); 495 | Q_ASSERT(check2.isEmpty()); 496 | } 497 | 498 | void builder_test::test_nested_transactions() 499 | { 500 | auto query = Query(TARGET_TABLE); 501 | 502 | const QString GOOD_NAME {"GOOD_TRANSACTION"}; 503 | const QString BAD_NAME {"BAD_TRANSACTION"}; 504 | 505 | query.delete_(OP::EQ("name", GOOD_NAME)).perform(); 506 | 507 | bool errorCode = query.transact([&]{ 508 | auto ids = query 509 | .insert({"_otype", "guid", "name"}) 510 | .values({33, QUuid::createUuid().toString(), GOOD_NAME}) 511 | .values({33, QUuid::createUuid().toString(), GOOD_NAME}) 512 | .perform(); 513 | 514 | Q_ASSERT(ids.count() == 2); 515 | Q_ASSERT(!query.hasError()); 516 | 517 | bool d_ok = query.delete_(OP::EQ("_id", ids.first())).perform(); 518 | Q_ASSERT(d_ok); 519 | Q_ASSERT(!query.hasError()); 520 | 521 | auto check1 = query.select().where(OP::EQ("name", GOOD_NAME)).perform(); 522 | Q_ASSERT(!query.hasError()); 523 | Q_ASSERT(check1.count() == 1); 524 | 525 | bool nestedErrorCode = query.transact([&]{ 526 | auto ids = query 527 | .insert({"_otype", "guid", "name"}) 528 | .values({33, QUuid::createUuid().toString(), GOOD_NAME}) 529 | .values({33, QUuid::createUuid().toString(), GOOD_NAME}) 530 | .perform(); 531 | Q_ASSERT(ids.count() == 2); 532 | Q_ASSERT(!query.hasError()); 533 | 534 | bool d_ok = query.delete_(OP::EQ("_id", ids.first())).perform(); 535 | Q_ASSERT(d_ok); 536 | Q_ASSERT(!query.hasError()); 537 | 538 | auto check2 = query.select().where(OP::EQ("name", BAD_NAME)).perform(); 539 | Q_ASSERT(!query.hasError()); 540 | Q_ASSERT(check2.isEmpty()); 541 | }); 542 | Q_ASSERT(nestedErrorCode); 543 | }); 544 | Q_ASSERT(errorCode); 545 | 546 | auto check1 = query.select().where(OP::EQ("name", GOOD_NAME)).perform(); 547 | Q_ASSERT(!query.hasError()); 548 | Q_ASSERT(check1.count() == 2); 549 | } 550 | 551 | void builder_test::test_column_getter() 552 | { 553 | auto columns = Query().tableColumnNames(SECOND_TABLE); 554 | Q_ASSERT(columns.count() == 4); 555 | 556 | if (m_showDebug) 557 | qInfo() << columns; 558 | } 559 | 560 | void builder_test::test_select_functions() 561 | { 562 | const auto query = Query(TARGET_TABLE); 563 | 564 | auto res = query.select({"COUNT(*) as lol"}).perform(); 565 | Q_ASSERT(!query.hasError()); 566 | Q_ASSERT(!res.isEmpty()); 567 | Q_ASSERT(res.first().toMap().contains("lol")); 568 | 569 | if (m_showDebug) 570 | qInfo() << QJsonDocument::fromVariant(res); 571 | 572 | res = query.select({"SUM(_id) as strange_sum"}).perform(); 573 | Q_ASSERT(!query.hasError()); 574 | Q_ASSERT(!res.isEmpty()); 575 | Q_ASSERT(res.first().toMap().contains("strange_sum")); 576 | 577 | if (m_showDebug) 578 | qInfo() << QJsonDocument::fromVariant(res); 579 | 580 | res = query.select({"MAX(_id) as max_id"}).perform(); 581 | Q_ASSERT(!query.hasError()); 582 | Q_ASSERT(!res.isEmpty()); 583 | Q_ASSERT(res.first().toMap().contains("max_id")); 584 | 585 | if (m_showDebug) 586 | qInfo() << QJsonDocument::fromVariant(res); 587 | } 588 | 589 | void builder_test::test_join() 590 | { 591 | const auto query = Query(TARGET_TABLE); 592 | auto second_query = Query(SECOND_TABLE); 593 | 594 | const QString JOIN_RECORD_NAME {"JOIN_TEST"}; 595 | const QString JOIN_RECORD_DESCR {"JOIN_DESCR"}; 596 | 597 | auto ids = query 598 | .insert({"_otype", "guid", "name", "descr"}) 599 | .values({77, QUuid::createUuid().toString(), JOIN_RECORD_NAME, JOIN_RECORD_DESCR}) 600 | .values({77, QUuid::createUuid().toString(), JOIN_RECORD_NAME, JOIN_RECORD_DESCR}) 601 | .values({77, QUuid::createUuid().toString(), JOIN_RECORD_NAME, JOIN_RECORD_DESCR}) 602 | .perform(); 603 | Q_ASSERT(!query.hasError()); 604 | Q_ASSERT(ids.count() == 3); 605 | 606 | QList otherIds; 607 | bool ok = second_query.transact([&]{ 608 | for (const int& id: ids) 609 | { 610 | auto res = second_query 611 | .insert({"some_text", "some_fkey"}) 612 | .values({QUuid::createUuid().toString(), id}) 613 | .perform(); 614 | Q_ASSERT(!res.isEmpty()); 615 | otherIds.append(res.first()); 616 | } 617 | }); 618 | Q_ASSERT(ok); 619 | Q_ASSERT(otherIds.count() == 3); 620 | 621 | auto join_res = second_query 622 | .select({"some_text", "name", "guid", "descr"}) 623 | .join(TARGET_TABLE, {"some_fkey", "_id"}, Join::INNER) 624 | .perform(); 625 | Q_ASSERT(!second_query.hasError()); 626 | Q_ASSERT(join_res.count() == 3); 627 | 628 | if (m_showDebug) 629 | qInfo() << QJsonDocument::fromVariant(join_res); 630 | 631 | // should fail 632 | auto lol = second_query 633 | .select({"some_text", "name"}) 634 | .join(TARGET_TABLE, {"some_fkey", "_id_OOOPS"}, Join::INNER) 635 | .perform(); 636 | Q_ASSERT(second_query.hasError()); 637 | Q_ASSERT(lol.isEmpty()); 638 | 639 | // disambig 640 | auto other_join_res = second_query 641 | .select({"_id", "some_text", "name", "guid", "descr"}) 642 | .join(TARGET_TABLE, {"some_fkey", "_id"}, Join::INNER) 643 | .orderBy("_id", Order::ASC) 644 | .perform(); 645 | Q_ASSERT(!second_query.hasError()); 646 | Q_ASSERT(other_join_res.count() == 3); 647 | 648 | for(int i=0; i < other_join_res.count(); ++i) 649 | Q_ASSERT(other_join_res[i].toMap()["_id"].toInt() == otherIds[i]); 650 | 651 | if (m_showDebug) 652 | qInfo() << QJsonDocument::fromVariant(other_join_res); 653 | 654 | // disambig other 655 | other_join_res = second_query 656 | .select({"_id", "some_text", "name", "guid", "descr"}) 657 | .join(TARGET_TABLE, {"some_fkey", "_id"}, Join::INNER, true) 658 | .orderBy("_id", Order::ASC) 659 | .perform(); 660 | Q_ASSERT(!second_query.hasError()); 661 | Q_ASSERT(other_join_res.count() == 3); 662 | 663 | for(int i=0; i < other_join_res.count(); ++i) 664 | Q_ASSERT(other_join_res[i].toMap()["_id"].toInt() == ids[i]); 665 | 666 | if (m_showDebug) 667 | qInfo() << QJsonDocument::fromVariant(other_join_res); 668 | 669 | // test for inaccurate users 670 | auto all = second_query 671 | .select() 672 | .join(TARGET_TABLE, {"some_fkey", "_id"}, Join::INNER) 673 | .perform(); 674 | Q_ASSERT(!second_query.hasError()); 675 | Q_ASSERT(!other_join_res.isEmpty()); 676 | } 677 | 678 | void builder_test::test_join_complex() 679 | { 680 | const auto query = Query(TARGET_TABLE); 681 | const auto second_query = Query(SECOND_TABLE); 682 | 683 | auto id = query 684 | .insert({"name", "guid", "_otype"}) 685 | .values({"NULL_TEST", "SMTH", 1234}) 686 | .perform(); 687 | Q_ASSERT(!query.hasError()); 688 | Q_ASSERT(id.count() == 1); 689 | 690 | auto id2 = second_query 691 | .insert({"some_text", "some_fkey"}) 692 | .values({"OLOLOLOL", id.first()}) 693 | .perform(); 694 | Q_ASSERT(!second_query.hasError()); 695 | Q_ASSERT(id2.count() == 1); 696 | 697 | auto lame_join = second_query 698 | .select({"_id", "_parent", "some_text", "name", "guid", "descr"}) 699 | .join(TARGET_TABLE, {"some_fkey", "_id"}, Join::INNER, true) 700 | .where(OP::EQ("_id", id2.first())) 701 | .orderBy("_id", Order::ASC) 702 | .perform(); 703 | Q_ASSERT(!second_query.hasError()); 704 | Q_ASSERT(lame_join.isEmpty()); 705 | 706 | auto other_join_res = second_query 707 | .select({"_id", "_parent", "some_text", "name", "guid", "descr"}) 708 | .join(TARGET_TABLE, {"some_fkey", "_id"}, Join::INNER) 709 | .where(OP::EQ("_id", id2.first())) 710 | .orderBy("_id", Order::ASC) 711 | .perform(); 712 | Q_ASSERT(!second_query.hasError()); 713 | Q_ASSERT(!other_join_res.isEmpty()); 714 | Q_ASSERT(other_join_res.count() == 1); 715 | Q_ASSERT(other_join_res.first().toMap()["_id"].toInt() == id2.first()); 716 | 717 | // strange order test 718 | other_join_res = second_query 719 | .select({"_id", "_parent", "some_text", "name", "guid", "descr"}) 720 | .where(OP::EQ("_id", id2.first())) 721 | .orderBy("_id", Order::ASC) 722 | .join(TARGET_TABLE, {"some_fkey", "_id"}, Join::INNER) 723 | .perform(); 724 | Q_ASSERT(!second_query.hasError()); 725 | Q_ASSERT(!other_join_res.isEmpty()); 726 | Q_ASSERT(other_join_res.count() == 1); 727 | Q_ASSERT(other_join_res.first().toMap()["_id"].toInt() == id2.first()); 728 | 729 | // cascade delete test 730 | 731 | bool ok = query.delete_(OP::EQ("_id", id.first())).perform(); 732 | Q_ASSERT(!query.hasError()); 733 | Q_ASSERT(ok); 734 | 735 | auto tst = second_query.select().where(OP::EQ("_id", id2.first())).perform(); 736 | Q_ASSERT(!query.hasError()); 737 | Q_ASSERT(tst.isEmpty()); 738 | } 739 | 740 | void builder_test::test_multiple_joins() 741 | { 742 | const auto query = Query(TARGET_TABLE); 743 | auto second_query = Query(SECOND_TABLE); 744 | const auto third_query = Query(THIRD_TABLE); 745 | 746 | auto ids = query 747 | .insert({"_otype", "name", "guid"}) 748 | .values({999, "CRAZY_JOIN_TEST", QUuid::createUuid().toString()}) 749 | .values({999, "CRAZY_JOIN_TEST", QUuid::createUuid().toString()}) 750 | .values({999, "CRAZY_JOIN_TEST", QUuid::createUuid().toString()}) 751 | .values({999, "CRAZY_JOIN_TEST", QUuid::createUuid().toString()}) 752 | .perform(); 753 | Q_ASSERT(!query.hasError()); 754 | Q_ASSERT(ids.count() == 4); 755 | 756 | auto ids3 = third_query 757 | .insert({"some_date"}) 758 | .values({QDate::currentDate()}) 759 | .values({QDate::currentDate().addDays(3)}) 760 | .values({QDate::currentDate().addDays(-3)}) 761 | .values({QDate::currentDate().addDays(7)}) 762 | .perform(); 763 | Q_ASSERT(!third_query.hasError()); 764 | Q_ASSERT(ids3.count() == 4); 765 | 766 | QList secondIds; 767 | bool t_ok = second_query.transact([&](){ 768 | for(int i=0; i < 4; ++i) 769 | { 770 | auto ids2 = second_query 771 | .insert({"some_text", "some_fkey", "date_fkey"}) 772 | .values({"RECORD_FOR_CRAZY_JOIN_TEST", ids[i], ids3[i]}) 773 | .perform(); 774 | Q_ASSERT(ids2.count() == 1); 775 | Q_ASSERT(!second_query.hasError()); 776 | 777 | secondIds.append(ids2.first()); 778 | } 779 | }); 780 | Q_ASSERT(t_ok); 781 | Q_ASSERT(!second_query.hasError()); 782 | Q_ASSERT(secondIds.count() == 4); 783 | 784 | // --- finally, join test ---- 785 | 786 | QVariantList joinIds; 787 | for(const auto& id: secondIds) 788 | joinIds << id; 789 | 790 | auto res = second_query 791 | .select({"_id", "some_text", "name", "guid", "some_date"}) 792 | .join(TARGET_TABLE, {"some_fkey", "_id"}, Join::INNER) 793 | .join(THIRD_TABLE, {"date_fkey", "_id"}, Join::INNER) 794 | .where(OP::IN("_id", joinIds)) 795 | .orderBy("_id", Order::ASC) 796 | .perform(); 797 | Q_ASSERT(!second_query.hasError()); 798 | Q_ASSERT(res.count() == 4); 799 | 800 | for(int i=0; i < res.count(); ++i) 801 | Q_ASSERT(res[i].toMap()["_id"].toInt() == secondIds[i]); 802 | 803 | if (m_showDebug) 804 | qInfo() << QJsonDocument::fromVariant(res); 805 | 806 | // --- and a lazy-lamer test 807 | 808 | res = second_query 809 | .select() 810 | .join(TARGET_TABLE, {"some_fkey", "_id"}, Join::INNER) 811 | .join(THIRD_TABLE, {"date_fkey", "_id"}, Join::INNER) 812 | .where(OP::IN("_id", joinIds)) 813 | .orderBy("_id", Order::ASC) 814 | .perform(); 815 | Q_ASSERT(!second_query.hasError()); 816 | Q_ASSERT(!res.isEmpty()); 817 | 818 | if (m_showDebug) 819 | qInfo() << QJsonDocument::fromVariant(res); 820 | 821 | QVariantList firstIds; 822 | for(const auto& id: ids) 823 | firstIds << id; 824 | 825 | res = second_query 826 | .select() 827 | .join(TARGET_TABLE, {"some_fkey", "_id"}, Join::INNER, true) 828 | .join(THIRD_TABLE, {"date_fkey", "_id"}, Join::INNER) 829 | .where(OP::IN("_id", firstIds)) 830 | .orderBy("_id", Order::ASC) 831 | .perform(); 832 | Q_ASSERT(!second_query.hasError()); 833 | Q_ASSERT(res.count() == 4); 834 | 835 | for(int i=0; i < res.count(); ++i) 836 | Q_ASSERT(res[i].toMap()["_id"].toInt() == ids[i]); 837 | 838 | if (m_showDebug) 839 | qInfo() << QJsonDocument::fromVariant(res); 840 | 841 | // -- partial cleanup --- 842 | 843 | QVariantList delList; 844 | for(const auto& id: ids3) 845 | delList << id; 846 | 847 | bool ok = third_query.delete_(OP::IN("_id", delList)).perform(); 848 | Q_ASSERT(ok); 849 | Q_ASSERT(!third_query.hasError()); 850 | } 851 | 852 | QTEST_MAIN(builder_test) 853 | 854 | #include "tst_builder_test.moc" 855 | --------------------------------------------------------------------------------