Extensibilidad del nuevo motor SQL de mongo
Disponible desde la versión 2.2.1-hyperblast
Con el nuevo motor SQL es posible modificar o incluir nuevas funciones en el mismo, de modo que se puede extender su funcionalidad.
Todo esto se puede hacer con un usuario administrador de plataforma, desde el menú de configuraciones:
El nuevo motor SQL tiene que estar activado con la opción de use quasar a false:
Debemos tener en cuenta que, según el valor de use-legacysql, tendremos activados los _id con formato string (legacysql a true) o mongodb con el $oid (legacysql a false).
Existen dos configuraciones a tener en cuenta (tipo SQLENGINE). Cualquier cambio en las mismas requerirá el reinicio de lo módulo de plataforma que use el SQLENGINE (controlpanel, dashboardengine, router, …).
Json Dictionary
Se trata de un json en el que cada entrada, representa el nombre y la definición de la funcionalidad de esa función. Cada entrada se estructura de la siguiente forma:
{
"functionName": {
"expression": "$mongofn",
"steps": ["project",...],
"argsLength": 1,
"type": "inline",
"argsType": "direct",
"comment": "mi comentario sobre la funcion"
"switchbyargument":{
"argument": 0,
"switch": {
"month": "$month",
"year": "$year",
"dayOfMonth": "$dayOfMonth",
"hour": "$hour",
"minute": "$minute",
"second": "$second",
"millisecond": "$millisecond",
"dayOfYear": "$dayOfYear",
"dayOfWeek": "$dayOfWeek",
"week": "$week"
}
},
"preExpressionSteps": [{
"$unwind": {
"path": "${0}",
"preserveNullAndEmptyArrays": "${1}"
}
}],
"unique": true,
"codeFn": "tsExp"
}
}
Hay 4 tipos de formas de generar funciones dependiendo de la tipología de la misma. Veamos primeros lo parámetros comunes:
functionName → Nombre de la función a definir. Debemos tener en cuenta que sólo están permitidas funciones con letras, números y _ que no empiecen por _ o número. A la hora de usar la función será en modo case insensitive. Es obligatorio siempre definir el nombre de la función y este será el determinante del uso, por lo que todas las funciones tienen que tener diferente nombre.
steps → array de lugares dentro de la query donde es posible usar esa función (PROJECT, WHERE, HAVING, GROUPBY, ORDERBY). Por ejemplo, funciones que incrementen el número de registros (por ejemplo un aplanado de un array) tiene sentido que sólo puedan usarse dentro de la etapa project. Por defecto, si no se incluye nada, será permitida en todos los lugares de la query. Se usará para validar los lugares. Si usamos esta función en un lugar no permitido se nos generará el error correspondiente.
unique → true/false. False por defecto. Si está a true, sólo se podrá tener esa función en la posición de la query determinada. Esto es útil por ejemplo para funciones de proyección que modifican los registros y no son compatibles con otras funciones. Si se incluye con otras funciones se dará el error correspondiente.
argsLength → Número de argumentos que tiene la función. Por defecto a 1. Si la función tiene un número variable de parámetros se puede usar el valor -1. Si se usa la función con otro número de argumentos se obtendrá un error.
comment → descripción abierta de la función a modo informativo de como se usará, parámetros, resultados, … Por defecto a cadena vacía.
type → Tipo de la función que determinará la construcción de la misma. Se admiten valores inline, switchbyargument, customexp y customcode. Por defecto será inline.
Tipos de construcción de funciones:
inline: cambio directo de función SQL a función de mongoDB. Se usan los siguientes parámetros:
expression → funcion de mongodb a usar que empezará por $, por ejemplo “$concat“
argsType →tipo de argumentos de la función. Se tienen dos tipos: direct y array.
Direct (opción por defecto) será para funciones de un parámetro que se incluya directamente, por ejemplo $toInt:
"toInt": {
"expression": "$toint"
}
array para funciones con parámetros que serán llevados a un array, por ejemplo la función concat:
"concat": {
"expression": "$concat",
"argsType": "array",
"argsLength": -1
}
switchbyargument: funciones que dado un determinado parámetro de entrada, llaman a una u otra función de mongo. Es necesario definir la entrada con el mismo nombre que tendrá dos parámetros:
argument → posición del parámetro que determinará la función a aplicar
switch → clave-valor que determinará dada una entrada del parámetro con posición “argument“ a que función de mongo se llamará.
Por ejemplo date_part('mask',timestamp)
"date_part": {
"type": "switchbyargument",
"switchbyargument":{
"argument": 0,
"switch": {
"month": "$month",
"year": "$year",
"dayOfMonth": "$dayOfMonth",
"hour": "$hour",
"minute": "$minute",
"second": "$second",
"millisecond": "$millisecond",
"dayOfYear": "$dayOfYear",
"dayOfWeek": "$dayOfWeek",
"week": "$week"
}
},
"argsType": "direct",
"argsLength": 2,
"comment": "date_part(\"year\",c.dateLastUpdate)"
}
customexp: funciones más complejas que las anteriores que requieren de expresiones custom que no cumplen los parámetros anteriores. Los parámetros afectados serían:
expression → en este caso será un json con la expresión. Dentro de este json se podrán tener los parámetros con "${i}" siendo i la posición del parámetro. Por ejemplo:
"to_timestamp": {
"type": "customexp",
"expression": {
"$dateFromString": {
"dateString":"${0}"
}
}
}
La sustitución, si es un valor de campo será precedido con “$” ($field). Si es otro valor se sustituye tal cual. Puede ocurrir que queramos hacer una pequeña transformación del parámetro a la hora de sustituirlo de modo que tenemos:
“${i}|nodollar” → inserta el parámetro de tipo campo pero no incluye el $ previo
“${i}|jsonParse“ → parsea el parámetro a la hora de usarlo de modo que aunque sea string el input se utilizará con la estructura especificada.
"geoWithin": {
"steps": ["where"],
"type": "customexp",
"expression": {
"${0}|nodollar": {
"$geoWithin": {
"$geometry": "${1}|jsonParse"
}
}
},
"argsType": "direct",
"argsLength": 2,
"comment": "geowithin(field, geometry)"
}
preExpressionSteps → para etapas project de tipo especial, puede ser necesario hacer algún paso previo a modo etapa aggregate. En este caso igual que en “expression” se tiene una expressión en la que se sustituirán los valores de los parámetros. Un ejemplo es el unzip de un array que no tiene parte de expression:
"unzip": {
"steps": ["project"],
"type": "customexp",
"preExpressionSteps": [{
"$unwind": {
"path": "${0}",
"preserveNullAndEmptyArrays": "${1}"
}
}],
"argsLength": 2,
"unique": true
}
customcode: en ciertos escenarios, si ningún caso anterior cubre la forma de generar la query, se puede generar mediante código java directamente. Se tienen dos parámetros a usar:
preCodeFn → en etapas project que necesitan tratamiento previo muy custom, es necesario incluir aquí el nombre de la función java definida en la otra configuración de java util class. La función tiene que devolver un List<Document> con las transformaciones necesarias.
codeFn → en etapas project que necesitan tratamiento muy custom, es necesario incluir aquí el nombre de la función java definida en la otra configuración de java util class. La función tiene que devolver un Document con la parte de la función en si, null si no es necesario.
Un ejemplo es la función de unzipts:
"unzipts": {
"steps": ["project"],
"type": "customcode",
"preCodeFn": "tsPreSteps",
"codeFn": "tsExp",
"argsLength": 2,
"unique": true
}
Dentro de la configuración de java util class tenemos las dos definiciones que siempre tendrán la misma parametría.
final List<Expression> le, final MongoDBQueryHolder mongoDBQueryHolder
El primer parámetro son la list de expresiones parseadas con JSQLParser. El segundo es una estructura completa de la query de salida que puede ser modificada si es necesario.
import com.github.vincentrussell.query.mongodb.sql.converter.holder.MongoDBQueryHolder;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import org.bson.Document;
import java.util.LinkedList;
import java.util.List;
public final class UtilsCode {
public enum WINDOWTS {
SECONDS,
MINUTES,
HOURS,
DAYS,
MONTHS
}
private static final int[] TIMETOSEG = new int[] {1, 60, 3600, 86400};
private static final int[] TIMETONEXT = new int[] {1, 60, 60, 24};
/**
* previous steps for timeseries unzip by n levels.
* This use the period and frecuency for calculating this variable steps
* @param le params: 0 upper(period), 1 lower(frecuency)
* @param mongoDBQueryHolder
* @return list of documents for previous steps in unzip ts
*/
public static List<Document> tsPreSteps(final List<Expression> le, final MongoDBQueryHolder mongoDBQueryHolder) {
WINDOWTS windowh = WINDOWTS.valueOf(((StringValue) le.get(0)).getValue().toUpperCase());
WINDOWTS windowl = WINDOWTS.valueOf(((StringValue) le.get(1)).getValue().toUpperCase());
List<Document> ldocs = new LinkedList<>();
ldocs.add(
Document.parse("{\n"
+ " \"$project\": {\n"
+ " \"TimeSerie\": 1,\n"
+ " \"tmpArray\": { \"$objectToArray\": \"$TimeSerie.values.v\" }\n"
+ " \"tmpIndex\": { \"$literal\": 0 }\n"
+ " },\n"
+ " }")
);
ldocs.add(
Document.parse("{\n"
+ " \"$unwind\": \"$tmpArray\"\n"
+ " }")
);
for (int i = windowh.ordinal() - 1; i > windowl.ordinal(); i--) {
ldocs.add(
Document.parse("{\n"
+ " \"$project\": {\n"
+ " \"TimeSerie\": 1,\n"
+ " \"tmpIndex\": { "
+ " \"$multiply\": ["
+ "{\"$sum\":[\"$tmpIndex\",{\"$toInt\":\"$tmpArray.k\"}]},"
+ TIMETONEXT[i]
+ " ]},\n"
+ " \"tmpArray\": { \"$objectToArray\": \"$tmpArray.v\" }\n"
+ " }\n"
+ " }")
);
ldocs.add(
Document.parse("{\n"
+ " \"$unwind\": \"$tmpArray\"\n"
+ " }")
);
}
ldocs.add(
Document.parse("{\n"
+ " \"$addFields\": {\n"
+ " \"TimeSerie.value\": \"$tmpArray.v\",\n"
+ " \"TimeSerie.timestamp\": {\n"
+ " \"$dateFromParts\": {\n"
+ " \"year\": { \"$year\": \"$TimeSerie.timestamp\" },\n"
+ " \"month\": { \"$month\": \"$TimeSerie.timestamp\" },\n"
+ " \"day\": { \"$dayOfMonth\": \"$TimeSerie.timestamp\" },\n"
+ " \"hour\": { \"$hour\": \"$TimeSerie.timestamp\" },\n"
+ " \"minute\": { \"$minute\": \"$TimeSerie.timestamp\" },\n"
+ " \"second\": { \"$multiply\": ["
+ " {\"$sum\":[\"$tmpIndex\",{\"$toInt\":\"$tmpArray.k\"}]},"
+ TIMETOSEG[windowl.ordinal()] + "]}\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }")
);
return ldocs;
}
/**
* include ts fields in project, combine then with previous.
* @param le
* @param mongoDBQueryHolder
* @return null
*/
public static Document tsExp(final List<Expression> le, final MongoDBQueryHolder mongoDBQueryHolder) {
mongoDBQueryHolder.getProjection().put("tmpArray", 0);
mongoDBQueryHolder.getProjection().put("tmpIndex", 0);
mongoDBQueryHolder.getProjection().put("TimeSerie.values", 0);
return null;
}