#ifndef SQLITEDB_H #define SQLITEDB_H #include "sql/ObjectIdentifier.h" #include "sql/sqlitetypes.h" #include #include #include #include #include #include #include #include #include struct sqlite3; class CipherSettings; enum LogMessageType { kLogMsg_User, kLogMsg_App, kLogMsg_ErrorLog }; struct objectMap { // These map from object name to object pointer std::map tables; // This stores tables AND views std::map indices; std::map triggers; bool empty() const { return tables.empty() && indices.empty() && triggers.empty(); } }; using schemaMap = std::map; // Maps from the schema name (main, temp, attached schemas) to the object map for that schema int collCompare(void* pArg, int sizeA, const void* sA, int sizeB, const void* sB); namespace sqlb { QString escapeIdentifier(const QString& id); QString escapeString(const QString& literal); } /// represents a single SQLite database. except when noted otherwise, /// all member functions are to be called from the main UI thread /// only. class DBBrowserDB : public QObject { Q_OBJECT private: /// custom unique_ptr deleter releases database for further use by others struct DatabaseReleaser { explicit DatabaseReleaser(DBBrowserDB * pParent_ = nullptr) : pParent(pParent_) {} DBBrowserDB * pParent; void operator() (sqlite3 * db) const { if(!db || !pParent) return; std::unique_lock lk(pParent->m); pParent->db_used = false; lk.unlock(); emit pParent->databaseInUseChanged(false, QString()); pParent->cv.notify_one(); } }; public: explicit DBBrowserDB(); ~DBBrowserDB () override = default; bool open(const QString& db, bool readOnly = false); bool attach(const QString& filename, QString attach_as = QString()); /** detaches a previously attached database identified with its alias-name \param attached_as the alias-name as witch a additional database file has been attached to the connection **/ bool detach(const std::string& attached_as); bool create ( const QString & db); bool close(); bool saveAs(const std::string& filename); // This returns the SQLite version as well as the SQLCipher if DB4S is compiled with encryption support static void getSqliteVersion(QString& sqlite, QString& sqlcipher); using db_pointer_type = std::unique_ptr; /** borrow exclusive address to the currently open database, until releasing the returned unique_ptr. the intended use case is that the main UI thread can call this any time, and then optionally pass the obtained pointer to a background worker, or release it after doing work immediately. if database is currently used by somebody else, opens a dialog box and gives user the opportunity to sqlite3_interrupt() the operation of the current owner, then tries again. \param user a string that identifies the new user, and which can be displayed in the dialog box. \param force_wait if set to true we won't ask the user to cancel the running query but just wait until it is done. \returns a unique_ptr containing the SQLite database handle, or nullptr in case no database is open. **/ db_pointer_type get (const QString& user, bool force_wait = false); bool setSavepoint(const std::string& pointname = "RESTOREPOINT"); bool releaseSavepoint(const std::string& pointname = "RESTOREPOINT"); bool revertToSavepoint(const std::string& pointname = "RESTOREPOINT"); bool releaseAllSavepoints(); bool revertAll(); bool dump(const QString& filename, const std::vector& tablesToDump, bool insertColNames, bool insertNew, bool exportSchema, bool exportData, bool keepOldSchema) const; enum ChoiceOnUse { Ask, Wait, CancelOther }; // Callback to get results from executeSQL(). It is invoked for // each result row coming out of the evaluated SQL statements. If // a callback returns true (abort), the executeSQL() method // returns false (error) without invoking the callback again and // without running any subsequent SQL statements. The 1st argument // is the number of columns in the result. The 2nd argument to the // callback is the text representation of the values, one for each // column. The 3rd argument is a list of strings where each entry // represents the name of corresponding result column. using execCallback = std::function, std::vector)>; bool executeSQL(const std::string& statement, bool dirtyDB = true, bool logsql = true, execCallback callback = nullptr); bool executeMultiSQL(QByteArray query, bool dirty = true, bool log = false); QByteArray querySingleValueFromDb(const std::string& sql, bool log = true, ChoiceOnUse choice = Ask) const; const QString& lastError() const { return lastErrorMessage; } /** * @brief getRow Executes a sqlite statement to get the rowdata(columns) * for the given rowid. * @param schemaName Name of the database schema. * @param sTableName Table to query. * @param rowid The rowid to fetch. * @param rowdata A list of QByteArray containing the row data. * @return true if statement execution was ok, else false. */ bool getRow(const sqlb::ObjectIdentifier& table, const QString& rowid, std::vector& rowdata) const; /** * @brief Interrupts the currently running statement as soon as possible. */ void interruptQuery(); private: /** * @brief max Queries the table t for the max value of field. * @param tableName Table to query * @param field Name of the field to get the max value * @return the max value of the field or 0 on error */ unsigned long max(const sqlb::ObjectIdentifier& tableName, const std::string& field) const; static int callbackWrapper (void* callback, int numberColumns, char** values, char** columnNames); public: void updateSchema(); // Please don't call this from threads other than the main thread. private: /** * @brief Creates an empty insert statement. * @param schemaName The name of the database schema in which to find the table * @param pk_value This optional parameter can be used to manually set a specific value for the primary key column * @return An sqlite conform INSERT INTO statement with empty values. (NULL,'',0) */ std::string emptyInsertStmt(const std::string& schemaName, const sqlb::Table& t, const QString& pk_value = QString()) const; public: QString addRecord(const sqlb::ObjectIdentifier& tablename); bool deleteRecords(const sqlb::ObjectIdentifier& table, const std::vector& rowids, const sqlb::StringVector& pseudo_pk = {}); bool updateRecord(const sqlb::ObjectIdentifier& table, const std::string& column, const QString& rowid, const QByteArray& value, int force_type = 0, const sqlb::StringVector& pseudo_pk = {}); bool createTable(const sqlb::ObjectIdentifier& name, const sqlb::FieldVector& structure); bool renameTable(const std::string& schema, const std::string& from_table, const std::string& to_table); bool addColumn(const sqlb::ObjectIdentifier& tablename, const sqlb::Field& field); /** * @brief This type maps from old column names to new column names. Given the old and the new table definition, this suffices to * track fields between the two. * USE CASES: * 1) Don't specify a column at all or specify equal column names: Keep its name as-is. * 2) Specify different column names: Rename the field. * 3) Map from an existing column name to a Null string: Delete the column. * 4) Map from a Null column name to a new column name: Add the column. */ using AlterTableTrackColumns = std::map; /** * @brief alterTable Can be used to rename, modify or drop existing columns of a given table * @param tablename Specifies the schema and name of the table to edit * @param new_table Specifies the new table schema. This is exactly how the new table is going to look like. * @param track_columns Maps old column names to new column names. This is used to copy the data from the old table to the new one. * @param newSchema Set this to a non-empty string to move the table to a new schema * @return true if renaming was successful, false if not. In the latter case also lastErrorMessage is set */ bool alterTable(const sqlb::ObjectIdentifier& tablename, const sqlb::Table& new_table, AlterTableTrackColumns track_columns, std::string newSchemaName = std::string()); const sqlb::TablePtr getTableByName(const sqlb::ObjectIdentifier& name) const { const auto& schema = schemata.at(name.schema()); if(schema.tables.count(name.name())) return schema.tables.at(name.name()); return sqlb::TablePtr{}; } const sqlb::IndexPtr getIndexByName(const sqlb::ObjectIdentifier& name) const { const auto& schema = schemata.at(name.schema()); if(schema.indices.count(name.name())) return schema.indices.at(name.name()); return sqlb::IndexPtr{}; } const sqlb::TriggerPtr getTriggerByName(const sqlb::ObjectIdentifier& name) const { const auto& schema = schemata.at(name.schema()); if(schema.triggers.count(name.name())) return schema.triggers.at(name.name()); return sqlb::TriggerPtr{}; } bool isOpen() const; bool encrypted() const { return isEncrypted; } bool readOnly() const { return isReadOnly; } bool getDirty() const; QString currentFile() const { return curDBFilename; } /// log an SQL statement [thread-safe] void logSQL(const QString& statement, LogMessageType msgtype) const; QString getPragma(const std::string& pragma) const; bool setPragma(const std::string& pragma, const QString& value); bool setPragma(const std::string& pragma, const QString& value, QString& originalvalue); bool setPragma(const std::string& pragma, int value, int& originalvalue); bool loadExtension(const QString& filename); void loadExtensionsFromSettings(); static QStringList Datatypes; private: std::vector > queryColumnInformation(const std::string& schema_name, const std::string& object_name) const; public: std::string generateSavepointName(const std::string& identifier = std::string()) const; // This function generates the name for a temporary table. It guarantees that there is no table with this name yet std::string generateTemporaryTableName(const std::string& schema) const; schemaMap schemata; signals: void sqlExecuted(QString sql, int msgtype) const; void dbChanged(bool dirty); void structureUpdated(); void requestCollation(QString name, int eTextRep); void databaseInUseChanged(bool busy, QString user); private: /// external code needs to go through get() to obtain access to the database sqlite3 * _db; mutable std::mutex m; mutable std::condition_variable cv; bool db_used; QString db_user; /// wait for release of the DB locked through a previous get(), /// giving users the option to discard running task through a /// message box. void waitForDbRelease(ChoiceOnUse choice = Ask) const; QString curDBFilename; mutable QString lastErrorMessage; std::vector savepointList; bool isEncrypted; bool isReadOnly; sqlb::StringVector primaryKeyForEditing(const sqlb::ObjectIdentifier& table, const sqlb::StringVector& pseudo_pk) const; // SQLite Callbacks void collationNeeded(void* pData, sqlite3* db, int eTextRep, const char* sCollationName); void errorLogCallback(void* user_data, int error_code, const char* message); bool tryEncryptionSettings(const QString& filename, bool* encrypted, CipherSettings*& cipherSettings) const; bool disableStructureUpdateChecks; class NoStructureUpdateChecks { public: explicit NoStructureUpdateChecks(DBBrowserDB& db) : m_db(db) { m_db.disableStructureUpdateChecks = true; } ~NoStructureUpdateChecks() { m_db.disableStructureUpdateChecks = false; } private: DBBrowserDB& m_db; }; }; #endif