Ejecutar consultas sin enlace de datos MQL5

Algunas consultas SQL son comandos que basta con enviar al motor tal cual. No requieren ni entrada de variables ni resultados. Por ejemplo, si nuestro programa MQL necesita crear una tabla, índice o vista con una determinada estructura y nombre en la base de datos, podemos escribirlo como una cadena constante con la sentencia «CREATE...». Además, es conveniente utilizar este tipo de consultas para el tratamiento por lotes de registros o su combinación (fusión, cálculo de indicadores agregados y modificaciones del mismo tipo). Es decir, con una sola consulta, puede convertir los datos de toda la tabla o rellenar otras tablas basándose en ella. Estos resultados pueden analizarse en las consultas posteriores.

En todos estos casos, sólo es importante obtener la confirmación del éxito de la acción. Las solicitudes de este tipo se realizan mediante la función DatabaseExecute.

bool DatabaseExecute(int database, const string sql)

La función ejecuta una consulta en la base de datos especificada por el descriptor database. La propia solicitud se envía como una cadena lista sql.

La función devuelve un indicador de éxito (true) o de error (false).

Por ejemplo, podemos complementar nuestra clase DBSQLite con este método (el descriptor ya está dentro del objeto).

class DBSQLite
{
   ...
   bool execute(const string sql)
   {
      return DatabaseExecute(handlesql);
   }
};

Entonces el script que crea una nueva tabla (y, si es necesario, previamente, la propia base de datos) puede tener este aspecto (DBcreateTable.mq5):

input string Database = "MQL5Book/DB/Example1";
input string Table = "table1";
   
void OnStart()
{
   DBSQLite db(Database);
   if(db.isOpen())
   {
      PRTF(db.execute(StringFormat("CREATE TABLE %s (msg text)"Table))); // true
   }
}

Después de ejecutar el script, intente abrir la base de datos especificada en MetaEditor y asegúrese de que contiene una tabla vacía con un único campo de texto «msg». Pero también puede hacerse mediante programación (véase la sección siguiente).

Si ejecutamos el script por segunda vez con los mismos parámetros, obtendremos un error (aunque no crítico, sin forzar el cierre del programa).

database error, table table1 already exists
db.execute(StringFormat(CREATE TABLE %s (msg text),Table))=false / DATABASE_ERROR(5601)

Esto se debe a que no se puede volver a crear una tabla existente. Pero SQL permite suprimir este error y crear una tabla sólo si aún no existe, de lo contrario, permite no hacer casi nada y devolver un indicador de éxito. Para ello, basta con añadir «IF NOT EXISTS» delante del nombre en la consulta.

   db.execute(StringFormat("CREATE TABLE IF NOT EXISTS %s (msg text)"Table));

En la práctica, las tablas son necesarias para almacenar información sobre objetos del área de aplicación, como cotizaciones, operaciones y señales de trading. Por lo tanto, es deseable automatizar la creación de tablas basadas en la descripción de objetos en MQL5. Como veremos más adelante, las funciones SQLite ofrecen la posibilidad de vincular los resultados de las consultas a estructuras MQL5 (pero no a clases). En este sentido, en el marco del envoltorio ORM, desarrollaremos un mecanismo para generar la consulta SQL «CREATE TABLE» de acuerdo con la descripción struct del tipo específico en MQL5.

Para ello es necesario registrar de algún modo los nombres y tipos de los campos de estructura en la lista general en el momento de la compilación y, a continuación, ya en la fase de ejecución del programa, se pueden generar consultas SQL a partir de esta lista.

En la fase de compilación se analizan varias categorías de entidades MQL5, que pueden utilizarse para identificar tipos y nombres:

En primer lugar, hay que recordar que las descripciones de los campos recopilados están relacionadas con el contexto de una estructura concreta y no deben mezclarse, porque el programa puede contener muchas estructuras diferentes con nombres y tipos potencialmente coincidentes. En otras palabras: es deseable acumular información en listas separadas para cada tipo de estructura. Para ello es ideal un tipo de plantilla cuyo parámetro (S) será la estructura de la aplicación. Llamemos a la plantilla DBEntity.

template<typename S>
struct DBEntity
{
   static string prototype[][3]; // 0 - type, 1 - name, 2 - constraints
   ...
};
   
template<typename T>
static string DBEntity::prototype[][3];

Dentro de la plantilla hay un array multidimensional prototype, en el que escribiremos la descripción de los campos. Para interceptar el tipo y el nombre del campo aplicado, tendrá que declarar otra estructura de plantilla, DBField, dentro de DBEntity: esta vez su parámetro T es el tipo del propio campo. En el constructor, tenemos información sobre este tipo (typename(T)), y también obtenemos el nombre del campo (y opcionalmente, la restricción) como parámetros.

template<typename S>
struct DBEntity
{
   ...
   template<typename T>
   struct DBField
   {
      T f;
      DBField(const string nameconst string constraints = "")
      {
         const int n = EXPAND(prototype);
         prototype[n][0] = typename(T);
         prototype[n][1] = name;
         prototype[n][2] = constraints;
      }
   };

El campo f no se utiliza, pero es necesario porque las estructuras no pueden estar vacías.

Supongamos que tenemos una estructura de aplicación Data (DBmetaProgramming.mq5).

struct Data
{
   long id;
   string name;
   datetime timestamp;
   double income;
};

Podemos hacer que su análogo heredado de DBEntity<DataDB>, pero con campos sustituidos basados en DBField, sea idéntico al conjunto original.

struct DataDBpublic DBEntity<DataDB>
{
   DB_FIELD(longid);
   DB_FIELD(stringname);
   DB_FIELD(datetimetimestamp);
   DB_FIELD(doubleincome);
proto;

Al sustituir el nombre de la estructura en el parámetro de la plantilla padre, la estructura proporciona al programa información sobre sus propias propiedades.

Preste atención a la definición única de la variable proto junto con la declaración de la estructura. Esto es necesario porque, en las plantillas, cada tipo parametrizado específico sólo se compila si se crea al menos un objeto de este tipo en el código fuente. Es importante para nosotros que la creación de este proto-objeto se produzca al principio del lanzamiento del programa, en el momento de la inicialización de las variables globales.

Una macro se oculta bajo el identificador DB_FIELD:

#define DB_FIELD(T,Nstruct T##_##NDBField<T> { T##_##N() : DBField<T>(#N) { } } \
   _##T##_##N;

Así es como se expande para un solo campo:

   struct Type_NameDBField<Type>
   {
      Type_Name() : DBField<Type>(Name) { }
   } _Type_Name;

Aquí la estructura no sólo se define, sino que se crea al instante: de hecho, sustituye al campo original.

Dado que la estructura DBField contiene una única variable f del tipo deseado, las dimensiones y la representación binaria interna de Data y DataDB son idénticas. Esto puede comprobarse fácilmente ejecutando el script DBmetaProgramming.mq5.

void OnStart()
{
   PRTF(sizeof(Data));
   PRTF(sizeof(DataDB));
   ArrayPrint(DataDB::prototype);
}

Éste envía al registro lo siguiente:

DBEntity<Data>::DBField<long>::DBField<long>(const string,const string)
long id
DBEntity<Data>::DBField<string>::DBField<string>(const string,const string)
string name
DBEntity<Data>::DBField<datetime>::DBField<datetime>(const string,const string)
datetime timestamp
DBEntity<Data>::DBField<double>::DBField<double>(const string,const string)
double income
sizeof(Data)=36 / ok
sizeof(DataDB)=36 / ok
            [,0]        [,1]        [,2]
[0,] "long"      "id"        ""         
[1,] "string"    "name"      ""         
[2,] "datetime"  "timestamp" ""         
[3,] "double"    "income"    ""         

Sin embargo, para acceder a los campos, tendría que escribir algo inconveniente: data._long_id.f, data._string_name.f, data._datetime_timestamp.f, data._double_income.f.

No lo haremos, no sólo y no tanto por inconveniencia, sino porque esta forma de construir metaestructuras no es compatible con los principios de vinculación de datos a consultas SQL. En las siguientes secciones exploraremos las funciones de database que permiten obtener registros de tablas y resultados de consultas SQL en estructuras MQL5. No obstante, sólo se permite utilizar estructuras simples sin herencia y miembros estáticos de tipos de objeto. Por lo tanto, es necesario modificar ligeramente el principio de revelación de la metainformación.

Tendremos que dejar sin cambios los tipos de estructuras originales y repetir de hecho la descripción para la base de datos, asegurándonos de que no haya discrepancias (erratas). Esto no es muy conveniente, pero no hay otra manera en este momento.

Trasladaremos la declaración de instancias DBEntity y DBField más allá de las estructuras de aplicación. En este caso, la macro DB_FIELD recibirá un parámetro adicional (S), en el que será necesario pasar el tipo de la estructura de la aplicación (antes se tomaba implícitamente declarándolo dentro de la propia estructura).

#define DB_FIELD(S,T,N) \
   struct S##_##T##_##NDBEntity<S>::DBField<T> \
   { \
      S##_##T##_##N() : DBEntity<S>::DBField<T>(#N) {} \
   }; \
   const S##_##T##_##N _##S##_##T##_##N;

Dado que las columnas de la tabla pueden tener restricciones, también habrá que pasarlas al constructor de DBField si es necesario. Para ello, vamos a añadir un par de macros con los parámetros adecuados (en teoría, una columna puede tener varias restricciones, pero normalmente no más de dos).

#define DB_FIELD_C1(S,T,N,C1) \
   struct S##_##T##_##NDBEntity<S>::DBField<T> \
   {
      S##_##T##_##N() : DBEntity<S>::DBField<T>(#NC1) {} \
   }; \
   const S##_##T##_##N _##S##_##T##_##N;
   
#define DB_FIELD_C2(S,T,N,C1,C2) \
   struct S##_##T##_##NDBEntity<S>::DBField<T> \
   { \
      S##_##T##_##N() : DBEntity<S>::DBField<T>(#NC1 + " " + C2) {} \
   }; \
   const S##_##T##_##N _##S##_##T##_##N;

Las tres macros, así como otros desarrollos, se añaden al archivo de encabezado DBSQLite.mqh.

Es importante tener en cuenta que esta vinculación «propia» de los objetos a una tabla sólo es necesaria para introducir datos en la base de datos, ya que la lectura de datos de una tabla a un objeto se implementa en MQL5 mediante la función DatabaseReadBind.

Mejoremos también la aplicación de DBField. Los tipos MQL5 no se corresponden exactamente con las clases de almacenamiento SQL, por lo que es necesario realizar una conversión al rellenar el elemento prototype[n][0]. Para ello se utiliza el método estático affinity.

   template<typename T>
   struct DBField
   {
      T f;
      DBField(const string nameconst string constraints = "")
      {
         const int n = EXPAND(prototype);
         prototype[n][0] = affinity(typename(T));
         ...
      }
      
      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 = 0i < 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;
      }
   };

Las constantes de texto de los tipos genéricos SQL utilizados aquí se colocan en un espacio de nombres separado: pueden ser necesarios en diferentes lugares en los programas MQL en algún momento, y es necesario asegurarse de que no hay conflictos de nombres.

namespace DB_TYPE
{
   const string INTEGER = "INTEGER";
   const string REAL = "REAL";
   const string TEXT = "TEXT";
   const string BLOB = "BLOB";
   const string NONE = "NONE";
   const string _NULL = "NULL";
}

Los preajustes de las posibles restricciones también se describen en su grupo para mayor comodidad (a modo de pista).

namespace DB_CONSTRAINT
{
   const string PRIMARY_KEY = "PRIMARY KEY";
   const string UNIQUE = "UNIQUE";
   const string NOT_NULL = "NOT NULL";
   const string CHECK = "CHECK (%s)"// requires an expression
   const string CURRENT_TIME = "CURRENT_TIME";
   const string CURRENT_DATE = "CURRENT_DATE";
   const string CURRENT_TIMESTAMP = "CURRENT_TIMESTAMP";
   const string AUTOINCREMENT = "AUTOINCREMENT";
   const string DEFAULT = "DEFAULT (%s)"// requires an expression (constants, functions)
}

Dado que algunas de las restricciones requieren parámetros (los lugares para ellos están marcados con el modificador de formato habitual «%s»), añadamos una comprobación de su presencia. Esta es la forma final del constructor DBField.

   template<typename T>
   struct DBField
   {
      T f;
      DBField(const string nameconst string constraints = "")
      {
         const int n = EXPAND(prototype);
         prototype[n][0] = affinity(typename(T));
         prototype[n][1] = name;
         if(StringLen(constraints) > 0       // avoiding error STRING_SMALL_LEN(5035)
            && StringFind(constraints"%") >= 0)
         {
            Print("Constraint requires an expression (skipped): "constraints);
         }
         else
         {
            prototype[n][2] = constraints;
         }
      }

Debido a que la combinación de macros y objetos auxiliares DBEntity<S> y DBField<T> rellena un array de prototipos, dentro de la clase DBSQlite, se hace posible implementar la generación automática de una consulta SQL para crear una tabla de estructuras.

El método createTable se modela con un tipo de estructura de aplicación y contiene un stub de consulta («CREATE TABLE %s %s (%s);»). Su primer argumento es la instrucción opcional «IF NOT EXISTS». El segundo parámetro es el nombre de la tabla, que por defecto se toma como el tipo del parámetro de plantilla typename(S), pero se puede sustituir por otra cosa si es necesario utilizando el nombre del parámetro de entrada (si no es NULL). Por último, el tercer argumento entre paréntesis es la lista de columnas de la tabla: la forma el método de ayuda columns a partir del array DBEntity <S>::prototype.

class DBSQLite
{
   ...
   template<typename S>
   bool createTable(const string name = NULL,
      const bool not_exist = falseconst 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 an already existing table will give an error,
      // if not using IF NOT EXISTS
      const string sql = StringFormat(query,
         (not_exist ? "IF NOT EXISTS" : ""),
         StringLen(name) ? name : typename(S), fields);
      PRTF(sql);
      return DatabaseExecute(handlesql);
   }
      
   template<typename S>
   string columns(const string table_constraints = ""const
   {
      static const string continuation = ",\n";
      string result = "";
      const int n = ArrayRange(DBEntity<S>::prototype0);
      if(!nreturn NULL;
      for(int i = 0i < 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;
   }
};

Para cada columna, la descripción consta de un nombre, un tipo y una restricción opcional. Además, es posible pasar una restricción general a la tabla (table_constraints).

Antes de enviar la consulta SQL generada a la función DatabaseExecute, el método createTable produce una salida de depuración del texto de la consulta al registro (toda salida de este tipo en las clases ORM puede desactivarse de forma centralizada sustituyendo la macro PRTF).

Ahora todo está listo para escribir un script de prueba DBcreateTableFromStruct.mq5, que, por declaración de estructura, crearía la tabla correspondiente en SQLite. En el parámetro de entrada, establecemos sólo el nombre de la base de datos, y el programa elegirá el nombre de la propia tabla en función del tipo de estructura.

#include <MQL5Book/DBSQLite.mqh>
   
input string Database = "MQL5Book/DB/Example1";
   
struct Struct
{
   long id;
   string name;
   double income;
   datetime time;
};
   
DB_FIELD_C1(StructlongidDB_CONSTRAINT::PRIMARY_KEY);
DB_FIELD(Structstringname);
DB_FIELD(Structdoubleincome);
DB_FIELD(Structstringtime);

En la función principal OnStart, creamos una tabla llamando a createTable con la configuración predeterminada. Si no queremos recibir una señal de error cuando intentemos crearlo la próxima vez, tenemos que pasar true como primer parámetro (db.createTable<Struct> (true)).

void OnStart()
{
   DBSQLite db(Database);
   if(db.isOpen())
   {
      PRTF(db.createTable<Struct>());
      PRTF(db.hasTable(typename(Struct)));
   }
}

El método hasTable comprueba la presencia de una tabla en la base de datos por el nombre de la tabla. Examinaremos la aplicación de este método en la sección siguiente. Ahora, vamos a ejecutar el script. Tras la primera ejecución, la tabla se crea correctamente y puede ver la consulta SQL en el registro (se muestra con saltos de línea, tal y como la formamos en el código).

sql=CREATE TABLE  Struct (id INTEGER PRIMARY KEY,
name TEXT ,
income REAL ,
time TEXT ); / ok
db.createTable<Struct>()=true / ok
db.hasTable(typename(Struct))=true / ok

La segunda ejecución devolverá un error de la llamada a DatabaseExecute, porque esta tabla ya existe, lo que se indica adicionalmente en el resultado de hasTable.

sql=CREATE TABLE  Struct (id INTEGER PRIMARY KEY,
name TEXT ,
income REAL ,
time TEXT ); / ok
database error, table Struct already exists
db.createTable<Struct>()=false / DATABASE_ERROR(5601)
db.hasTable(typename(Struct))=true / ok