forked from rosh/MQL5Book
824 lines
24 KiB
MQL5
824 lines
24 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| 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<S>::DBField<T> { S##_##T##_##N() : DBEntity<S>::DBField<T>(#N) {}}; const S##_##T##_##N _##S##_##T##_##N;
| |||
#define DB_FIELD_C1(S,T,N,C1) struct S##_##T##_##N: DBEntity<S>::DBField<T> { S##_##T##_##N() : DBEntity<S>::DBField<T>(#N, C1) {}}; const S##_##T##_##N _##S##_##T##_##N;
| |||
#define DB_FIELD_C2(S,T,N,C1,C2) struct S##_##T##_##N: DBEntity<S>::DBField<T> { S##_##T##_##N() : DBEntity<S>::DBField<T>(#N, C1 + " " + C2) {}}; const S##_##T##_##N _##S##_##T##_##N;
| |||
| |||
/*
| |||
DB_FIELD expands to:
| |||
| |||
struct Struct_Type_Name: DBEntity<Struct>::DBField<Type>
| |||
{
| |||
Struct_Type_Name() : DBEntity<Struct>::DBField<Type>("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<typename S>
| |||
struct DBEntity
| |||
{
| |||
template<typename T>
| |||
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<typename S>
| |||
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<typename S>
| |||
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<typename S>
| |||
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<typename S>
| |||
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<DBRow> row; // current
| |||
AutoPtr<DBRow> 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<typename T>
| |||
bool bind(const int index, const T value)
| |||
{
| |||
return PRTF(DatabaseBind(handle, index, value));
| |||
}
| |||
| |||
template<typename T>
| |||
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<typename S>
| |||
DBRow *start() // S &
| |||
{
| |||
DatabaseReset(handle);
| |||
row = TYPENAME(S) == "DBValue" ? new DBRow(handle) : new DBRowStruct<S>(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<typename S>
| |||
bool readAll(S &result[])
| |||
{
| |||
DatabaseReset(handle);
| |||
| |||
DBRowStruct<S> *temp = new DBRowStruct<S>(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<DBQuery> 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<typename S>
| |||
string columns(const string table_constraints = "") const
| |||
{
| |||
static const string continuation = ",\n";
| |||
string result = "";
| |||
const int n = ArrayRange(DBEntity<S>::prototype, 0);
| |||
if(!n) return NULL;
| |||
for(int i = 0; i < n; ++i)
| |||
{
| |||
result += StringFormat("%s%s %s %s",
| |||
i > 0 ? continuation : "",
| |||
DBEntity<S>::prototype[i][1], DBEntity<S>::prototype[i][0], DBEntity<S>::prototype[i][2]);
| |||
}
| |||
if(StringLen(table_constraints))
| |||
{
| |||
result += continuation + table_constraints;
| |||
}
| |||
return result;
| |||
}
| |||
| |||
template<typename S>
| |||
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<S>(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<typename S>
| |||
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<typename S>
| |||
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<S>());
| |||
DBQuery q(handle, PRTF(sql));
| |||
if(!q.isValid()) return false;
| |||
DBRow *r = q.start<DBValue>();
| |||
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<typename S>
| |||
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<S>());
| |||
DBQuery q(handle, PRTF(sql));
| |||
if(!q.isValid()) return 0;
| |||
if(object.bindAll(q))
| |||
{
| |||
DBRow *r = q.start<DBValue>(); // we expect one row with single value of new rowid
| |||
if(r.next())
| |||
{
| |||
return object.rowid(r[0].integer_value);
| |||
}
| |||
}
| |||
| |||
return 0;
| |||
}
| |||
| |||
template<typename S>
| |||
string qlist() const
| |||
{
| |||
const int n = ArrayRange(DBEntity<S>::prototype, 0);
| |||
string result = "?1";
| |||
for(int i = 1; i < n; ++i)
| |||
{
| |||
const string constraint = DBEntity<S>::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<typename S>
| |||
string namelist() const
| |||
{
| |||
const int n = ArrayRange(DBEntity<S>::prototype, 0);
| |||
string result = DBEntity<S>::prototype[0][1];
| |||
for(int i = 1; i < n; ++i)
| |||
{
| |||
result += "," + DBEntity<S>::prototype[i][1];
| |||
}
| |||
return result;
| |||
}
| |||
| |||
template<typename S>
| |||
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<S>(), qlist<S>(),
| |||
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<typename S>
| |||
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<typename S>
| |||
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<S> *r = q.start<S>();
| |||
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<DBValue>();
| |||
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;
| |||
}
| |||
};
| |||
//+------------------------------------------------------------------+
|