//+------------------------------------------------------------------+ //| DBSQLite.mqh | //| Copyright 2022, MetaQuotes Ltd. | //| https://www.mql5.com | //| Basic ORM (Object-Relational Mapping) MQL5 <-> SQLite | //+------------------------------------------------------------------+ // NB: structures with simple types and strings are allowed only // for binding to DB // use this in your code to disable debug logging by PRTF // #define PRTF #ifndef PRTF #include "PRTF.mqh" #endif #include "Defines.mqh" #include "AutoPtr.mqh" #include "TypeName.mqh" // use this to fill empty MqlParam.string_value with param name by default // #define DB_PARAM_NAMES #define TYPE_NULL ((ENUM_DATATYPE)(0)) #define TYPE_BLOB ((ENUM_DATATYPE)(-1)) #define DB_FIELD(S,T,N) struct S##_##T##_##N: DBEntity::DBField { S##_##T##_##N() : DBEntity::DBField(#N) {}}; const S##_##T##_##N _##S##_##T##_##N; #define DB_FIELD_C1(S,T,N,C1) struct S##_##T##_##N: DBEntity::DBField { S##_##T##_##N() : DBEntity::DBField(#N, C1) {}}; const S##_##T##_##N _##S##_##T##_##N; #define DB_FIELD_C2(S,T,N,C1,C2) struct S##_##T##_##N: DBEntity::DBField { S##_##T##_##N() : DBEntity::DBField(#N, C1 + " " + C2) {}}; const S##_##T##_##N _##S##_##T##_##N; /* DB_FIELD expands to: struct Struct_Type_Name: DBEntity::DBField { Struct_Type_Name() : DBEntity::DBField("Name"){} }; const Struct_Type_Name _Struct_Type_Name; */ //+------------------------------------------------------------------+ //| String templates of DB-constraints per field (useful reference) | //+------------------------------------------------------------------+ namespace DB_CONSTRAINT { const string PRIMARY_KEY = "PRIMARY KEY"; const string UNIQUE = "UNIQUE"; const string NOT_NULL = "NOT NULL"; const string CHECK = "CHECK (%s)"; // expression required const string CURRENT_TIMESTAMP = "CURRENT_TIMESTAMP"; // "CURRENT_TIMESTAMP" doesn't work in SQLite as expected const string CURRENT_TIME = "CURRENT_TIME"; // -- // -- const string CURRENT_DATE = "CURRENT_DATE"; // -- // -- const string AUTOINCREMENT = "AUTOINCREMENT"; const string DEFAULT = "DEFAULT (%s)"; // expression (e.g. constant, function) required } //+------------------------------------------------------------------+ //| General DB types | //+------------------------------------------------------------------+ namespace DB_TYPE { const string INTEGER = "INTEGER"; const string REAL = "REAL"; const string TEXT = "TEXT"; const string BLOB = "BLOB"; // "NONE" also ok, both means anything const string NONE = "NONE"; const string _NULL = "NULL"; } //+------------------------------------------------------------------+ //| Scalar value type is used to instantiate simple DBRow objects | //+------------------------------------------------------------------+ struct DBValue { int fake; // not used, struct can not be empty }; //+------------------------------------------------------------------+ //| Pseudo-type to declare blob fields in objects using DB_FIELD | //+------------------------------------------------------------------+ enum blob { }; //+------------------------------------------------------------------+ //| Description of table column, returned by pragma table_info('tbl')| //+------------------------------------------------------------------+ struct DBTableColumn { int cid; string name; string type; bool not_null; string default_value; bool primary_key; }; //+------------------------------------------------------------------+ //| DB-aware meta-type | //+------------------------------------------------------------------+ template struct DBEntity { template struct DBField { T f; DBField(const string name, const string constraints = "") { const int n = EXPAND(prototype); prototype[n][0] = affinity(TYPENAME(T)); prototype[n][1] = name; if(StringLen(constraints) > 0 // eliminates STRING_SMALL_LEN(5035) && StringFind(constraints, "%") >= 0) { Print("Constraint requires an expression (skipped): ", constraints); } else { prototype[n][2] = constraints; } } static string affinity(const string type) { const static string ints[] = { "bool", "char", "short", "int", "long", "uchar", "ushort", "uint", "ulong", "datetime", "color", "enum" }; for(int i = 0; i < ArraySize(ints); ++i) { if(type == ints[i]) return DB_TYPE::INTEGER; } if(type == "float" || type == "double") return DB_TYPE::REAL; if(type == "string") return DB_TYPE::TEXT; return DB_TYPE::BLOB; } }; static string prototype[][3]; // 0 - type, 1 - name, 2 - constraint }; template static string DBEntity::prototype[][3]; //+------------------------------------------------------------------+ //| Meta-info about columns of query results | //+------------------------------------------------------------------+ struct DBRowColumn { string name; // NB: the next is per row information, because in SQLite // fields are dynamically typed (properties defined at table creation // are just recommendations and can be overwritten by actual data being stored) ENUM_DATABASE_FIELD_TYPE type; int size; }; //+------------------------------------------------------------------+ //| Reading query results into general purpose array of MqlParams | //+------------------------------------------------------------------+ class DBRow { protected: const int query; int columns; DBRowColumn info[]; MqlParam data[]; const bool cache; int cursor; virtual bool DBread() { return PRTF(DatabaseRead(query)); } public: DBRow(const int q, const bool c = false): query(q), cache(c), columns(0), cursor(-1) { } int length() const { return columns; } DBRowColumn description(const int i) const { static const DBRowColumn null = {}; if(i < 0 || i >= columns) return null; return info[i]; } virtual bool next() { if(cache && cursor >= 0) { SetUserError(1); return false; // can't get next on cached instance } const bool success = DBread(); if(success) { if(cursor == -1) { columns = DatabaseColumnsCount(query); ArrayResize(info, columns); if(cache) ArrayResize(data, columns); for(int i = 0; i < columns; ++i) { DatabaseColumnName(query, i, info[i].name); info[i].type = DatabaseColumnType(query, i); info[i].size = DatabaseColumnSize(query, i); if(cache) data[i] = this[i]; } } else { // NB: since value type in every column can change for specific row, // it could be useful to rescan DatabaseColumnType and DatabaseColumnSize } ++cursor; } return success; } int name2index(const string name) const { for(int i = 0; i < columns; ++i) { if(name == info[i].name) return i; } Print("Wrong column name: ", name); SetUserError(3); // wrong column name return -1; } // read column value by name MqlParam operator[](const string name) const { const int i = name2index(name); if(i != -1) return this[i]; static MqlParam param = {}; return param; } // read column value by index virtual MqlParam operator[](const int i = 0) const { MqlParam param = {}; if(i < 0 || i >= columns) return param; if(ArraySize(data) > 0 && cursor != -1) // return cache, if exists { return data[i]; } #ifdef DB_PARAM_NAMES param.string_value = "[" + names[i] + "]"; #endif switch(info[i].type) { case DATABASE_FIELD_TYPE_INTEGER: switch(info[i].size) { case 1: param.type = TYPE_CHAR; break; case 2: param.type = TYPE_SHORT; break; case 4: param.type = TYPE_INT; break; case 8: default: param.type = TYPE_LONG; break; } DatabaseColumnLong(query, i, param.integer_value); break; case DATABASE_FIELD_TYPE_FLOAT: param.type = info[i].size == 4 ? TYPE_FLOAT : TYPE_DOUBLE; DatabaseColumnDouble(query, i, param.double_value); break; case DATABASE_FIELD_TYPE_TEXT: param.type = TYPE_STRING; DatabaseColumnText(query, i, param.string_value); break; case DATABASE_FIELD_TYPE_BLOB: // this is just a workaround { // there is no other means to pass blob via MqlParam uchar blob[]; // use getBlob to get exact binary data DatabaseColumnBlob(query, i, blob); uchar key[], text[]; if(CryptEncode(CRYPT_BASE64, blob, key, text)) { param.string_value = CharArrayToString(text); } } param.type = TYPE_BLOB; break; case DATABASE_FIELD_TYPE_NULL: param.type = TYPE_NULL; break; } return param; } // read blob column value by index template int getBlob(const int i, S &object[]) { if(ArraySize(data) > 0) { Print("Cached rows do not support blobs"); SetUserError(2); return false; // can't get blob from cache: raw blobs are not cachable } return DatabaseColumnBlob(query, i, object); } // read blob column value by name template int getBlob(const string name, S &object[]) { const int i = name2index(name); if(i != -1) { return getBlob(i, object); } return 0; } void readAll(MqlParam ¶ms[]) const { ArrayResize(params, columns); for(int i = 0; i < columns; ++i) { params[i] = this[i]; } } void moveAll(MqlParam ¶ms[]) { if(cache) // only cached row fills 'data' array { ArraySwap(params, data); } } }; //+------------------------------------------------------------------+ //| DB row for reading DB records into objects/structs | //+------------------------------------------------------------------+ template class DBRowStruct: public DBRow { protected: S object; virtual bool DBread() override { // NB: derived structures are not allowed; // number of struct fields must not exceed number of columns in table/query return PRTF(DatabaseReadBind(query, object)); } public: DBRowStruct(const int q, const bool c = false): DBRow(q, c) { } S get() const { return object; } }; //+------------------------------------------------------------------+ //| DB query wrapper class | //+------------------------------------------------------------------+ class DBQuery { protected: const string sql; const int db; const int handle; AutoPtr row; // current AutoPtr rows[]; // cached (if requested) public: DBQuery(const int owner, const string s): db(owner), sql(s), handle(PRTF(DatabasePrepare(db, sql))) { row = NULL; } ~DBQuery() { DatabaseFinalize(handle); } bool isValid() const { return handle != INVALID_HANDLE; } int getHandle() const { return handle; } int getDB() const { return db; } template bool bind(const int index, const T value) { return PRTF(DatabaseBind(handle, index, value)); } template bool bindBlob(const int index, const T &value[]) { return PRTF(DatabaseBindArray(handle, index, value)); } bool bindNull(const int index) { static const uchar null[] = {}; return bindBlob(index, null); } template DBRow *start() // S & { DatabaseReset(handle); row = TYPENAME(S) == "DBValue" ? new DBRow(handle) : new DBRowStruct(handle); return row[]; } virtual bool reset() { return DatabaseReset(handle); } // use 'execute' for parametric queries with inputs and NO outputs (batch DB edit) // use 'DBRow/DBRowStruct::next' to get outputs/results from DB virtual bool execute() { // NB: can return false and set error DATABASE_NO_MORE_DATA(5126), // which is NOT an error! return PRTF(DatabaseRead(handle)); } // get an array of cached rows bool readAll(DBRow *&result[], const bool detach = false) { DatabaseReset(handle); DBRow *temp; while((temp = new DBRow(handle, true)) != NULL && temp.next()) // generate new row on each iteration { PUSH(result, temp); if(!detach) PUSH(rows, temp); // keep trace of objects to clean them up temp = NULL; } delete temp; // clean up excessive instance for ERR_DATABASE_NO_MORE_DATA return true; } template bool readAll(S &result[]) { DatabaseReset(handle); DBRowStruct *temp = new DBRowStruct(handle); while(temp.next()) { PUSH(result, temp.get()); } delete temp; return true; } }; //+------------------------------------------------------------------+ //| Main database wrapper class | //+------------------------------------------------------------------+ class DBSQLite { protected: const string path; const int handle; const uint flags; int transaction; AutoPtr queries[]; public: DBSQLite(const string file, const uint opts = DATABASE_OPEN_CREATE | DATABASE_OPEN_READWRITE): path(file), flags(opts), handle(DatabaseOpen(file, opts)), transaction(0) { } ~DBSQLite(void) { if(handle != INVALID_HANDLE) { DatabaseClose(handle); } } int getHandle() const { return handle; } bool isOpen() const { return handle != INVALID_HANDLE; } bool execute(const string sql) // query without bound inputs/outputs (hardcoded logic) { return PRTF(DatabaseExecute(handle, sql)); } DBQuery *prepare(const string sql) { DBQuery *q = new DBQuery(handle, sql); if(!q.isValid()) { delete q; return NULL; } return PUSH(queries, q); } template string columns(const string table_constraints = "") const { static const string continuation = ",\n"; string result = ""; const int n = ArrayRange(DBEntity::prototype, 0); if(!n) return NULL; for(int i = 0; i < n; ++i) { result += StringFormat("%s%s %s %s", i > 0 ? continuation : "", DBEntity::prototype[i][1], DBEntity::prototype[i][0], DBEntity::prototype[i][2]); } if(StringLen(table_constraints)) { result += continuation + table_constraints; } return result; } template bool createTable(const bool not_exist = false, const string name = NULL, const string table_constraints = "") const { const static string query = "CREATE TABLE %s %s (%s);"; const string fields = columns(table_constraints); if(fields == NULL) { Print("Structure '", TYPENAME(S), "' with table fields is not initialized"); SetUserError(4); return false; } // attempt to create a table that already exists // without using IF NOT EXISTS will give an error. const string sql = StringFormat(query, (not_exist ? "IF NOT EXISTS" : ""), StringLen(name) ? name : TYPENAME(S), fields); return DatabaseExecute(handle, PRTF(sql)); } template bool createSimpleTable() const { return createTable(true); } bool hasTable(const string table) const { return DatabaseTableExists(handle, table); } bool deleteTable(const string name) const { const static string query = "DROP TABLE IF EXISTS '%s';"; return DatabaseExecute(handle, StringFormat(query, name)); /* // equivalent: if(!DatabaseTableExists(handle, name)) return true; if(!DatabaseExecute(handle, StringFormat("DROP TABLE '%s';", name))) return false; return !DatabaseTableExists(handle, name) && ResetLastErrorOnCondition(_LastError == ERR_DATABASE_NO_MORE_DATA); */ } static bool ResetLastErrorOnCondition(const bool cond) { if(cond) { ResetLastError(); return true; } return false; } template bool insert(S &objects[], const string table = NULL) { const static string query = "INSERT INTO '%s' VALUES(%s) RETURNING rowid;"; const string sql = StringFormat(query, StringLen(table) ? table : TYPENAME(S), qlist()); DBQuery q(handle, PRTF(sql)); if(!q.isValid()) return false; DBRow *r = q.start(); for(int i = 0; i < ArraySize(objects); ++i) { if(objects[i].bindAll(q)) { if(r.next()) // we expect one row with single value of new rowid { objects[i].rowid(r[0].integer_value); } } q.reset(); } return true; } template long insert(S &object, const string table = NULL) { const static string query = "INSERT INTO '%s' VALUES(%s) RETURNING rowid;"; const string sql = StringFormat(query, StringLen(table) ? table : TYPENAME(S), qlist()); DBQuery q(handle, PRTF(sql)); if(!q.isValid()) return 0; if(object.bindAll(q)) { DBRow *r = q.start(); // we expect one row with single value of new rowid if(r.next()) { return object.rowid(r[0].integer_value); } } return 0; } template string qlist() const { const int n = ArrayRange(DBEntity::prototype, 0); string result = "?1"; for(int i = 1; i < n; ++i) { const string constraint = DBEntity::prototype[i][2]; if(StringLen(constraint) > 0 && StringFind(constraint, DB_CONSTRAINT::CURRENT_TIMESTAMP) >= 0) // workaround for SQLite bug with not setting CURRENT_TIMESTAMP (works both on inserts and updates) result += ",STRFTIME('%s')"; // TODO: replace with UNIXEPOCH() after SQLite v.3.38 else result += StringFormat(",?%d", (i + 1)); } return result; } template string namelist() const { const int n = ArrayRange(DBEntity::prototype, 0); string result = DBEntity::prototype[0][1]; for(int i = 1; i < n; ++i) { result += "," + DBEntity::prototype[i][1]; } return result; } template bool update(S &object, const string condition = NULL, const string table = NULL) { const static string query = "UPDATE '%s' SET (%s)=(%s) %s;"; const string sql = StringFormat(query, StringLen(table) ? table : TYPENAME(S), namelist(), qlist(), StringLen(condition) ? condition : StringFormat("WHERE rowid=%ld", object.rowid())); DBQuery q(handle, PRTF(sql)); if(!q.isValid()) return false; if(object.bindAll(q)) { q.execute(); } return ResetLastErrorOnCondition(_LastError == ERR_DATABASE_NO_MORE_DATA); } template bool remove(S &object, const string table = NULL, const string column = "rowid") { const static string query = "DELETE FROM '%s' WHERE %s=%ld;"; const string sql = StringFormat(query, StringLen(table) ? table : TYPENAME(S), column, object.rowid()); DBQuery q(handle, PRTF(sql)); if(!q.isValid()) return false; q.execute(); return ResetLastErrorOnCondition(_LastError == ERR_DATABASE_NO_MORE_DATA); } template bool read(const long rowid, S &s, const string table = NULL, const string column = "rowid") { // NB: INTEGER PRIMARY KEY column may not be present in S explicitly, // then selecting '*' will not return 'rowid' const static string query = "SELECT * FROM '%s' WHERE %s=%ld;"; const string sql = StringFormat(query, StringLen(table) ? table : TYPENAME(S), column, rowid); DBQuery q(handle, PRTF(sql)); if(!q.isValid()) return false; DBRowStruct *r = q.start(); if(r.next()) { s = r.get(); return true; } return false; } bool read(const long rowid, DBRow *&r, const string table, const string column = "rowid") { // NB: INTEGER PRIMARY KEY column may not be present in S explicitly, // then selecting '*' will not return 'rowid' const static string query = "SELECT * FROM '%s' WHERE %s=%ld;"; const string sql = StringFormat(query, table, column, rowid); DBQuery *q = new DBQuery(handle, PRTF(sql)); if(q.isValid()) { r = q.start(); if(r.next()) { PUSH(queries, q); return true; } } delete q; return false; } bool begin() { if(transaction > 0) // already in transaction { transaction++; // track nesting level return true; } return (bool)(transaction = PRTF(DatabaseTransactionBegin(handle))); } bool commit() { if(transaction > 0) { if(--transaction == 0) // outermost transaction return PRTF(DatabaseTransactionCommit(handle)); } return false; } bool rollback() { if(transaction > 0) { if(--transaction == 0) return PRTF(DatabaseTransactionRollback(handle)); } return false; } }; //+------------------------------------------------------------------+ //| DB transaction guard | //+------------------------------------------------------------------+ class DBTransaction { DBSQLite *db; const bool autocommit; public: DBTransaction(DBSQLite &owner, const bool c = false): db(&owner), autocommit(c) { if(CheckPointer(db) != POINTER_INVALID) { db.begin(); } } ~DBTransaction() { if(CheckPointer(db) != POINTER_INVALID) { autocommit ? db.commit() : db.rollback(); } } bool commit() { if(CheckPointer(db) != POINTER_INVALID) { const bool done = db.commit(); db = NULL; return done; } return false; } }; //+------------------------------------------------------------------+