Bug when using byte[] in SQL function parameters in V3.0

Hello

I am upgrading OrientDB version in my software from V2.2.33 to V3.0.27 (I started the upgrading before V3.0.28 release).
I got a problem when using byte array as parameter of SQL function in JAVA.

Here is a test showing the bug:

package test;

import java.util.Collections;
import java.util.Map;

import org.junit.Test;

import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.ODatabaseSession;
import com.orientechnologies.orient.core.db.ODatabaseType;
import com.orientechnologies.orient.core.db.OrientDB;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.OSQLEngine;
import com.orientechnologies.orient.core.sql.functions.OSQLFunctionAbstract;
import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery;
import com.orientechnologies.orient.server.OServer;
import com.orientechnologies.orient.server.OServerMain;
import com.tinkerpop.blueprints.impls.orient.OrientBaseGraph;
import com.tinkerpop.blueprints.impls.orient.OrientGraphFactory;

public class V3Test {

    @Test
    public void test() throws Exception {
        // Initialize memory DB
        System.out.println("*** create embedded server ***");
        final OServer server = OServerMain.create();
        server.startup(getClass().getResourceAsStream("/embeddedOrientServer.xml"));
        server.activate();
        System.out.println(" *** ORIENT SERVER STARTED ***");

        System.out.println("*** create function ***");
        final MyFunction myFunction = new MyFunction();
        OSQLEngine.getInstance().registerFunction(myFunction.getName(), myFunction);

        System.out.println("*** create database ***");
        // In V2.2.33
        // final String dbUrlMem = "memory:./target/databases/MyDB";
        // OrientGraphNoTx graphDBNoTx = new OrientGraphNoTx(dbUrlMem);
        // graphDBNoTx.drop();
        // graphDBNoTx.shutdown();
        // graphDBNoTx = new OrientGraphNoTx(dbUrlMem);
        // final OSchema schema = graphDBNoTx.getRawGraph().getMetadata().getSchema();
        // final OClass myClass = schema.createClass("MyClass", schema.getClass("V"));
        // myClass.createProperty("MyAtt", OType.STRING);

        // In V3.0.27
        final OrientDB context = server.getContext();
        if (context.list().contains("MyDB")) {
            context.drop("MyDB");
        }
        context.createIfNotExists("MyDB", ODatabaseType.MEMORY);
        final ODatabaseSession session = context.open("MyDB", "admin", "admin");

        System.out.println("*** database successfully created ***");

        // Open remote connection on memory DB
        System.out.println("Use a pool for database connection with pool size " + 50);
        final String dbUrlRemote = "remote:localhost/MyDB";
        final OrientGraphFactory orientFactory = new OrientGraphFactory(dbUrlRemote, "admin", "admin").setupPool(1, 50);
        orientFactory.setAutoStartTx(false);

        final OrientBaseGraph orientBaseGraph = orientFactory.getNoTx();
        ODatabaseRecordThreadLocal.instance().set(orientBaseGraph.getRawGraph());

        // Call function
        System.out.println("*** call function with a byte array ***");
        final byte[] byteArray = "hello".getBytes();
        for (final ODocument docRet : new OSQLSynchQuery<ODocument>("SELECT myFunction(:param)")
                .run(Collections.singletonMap("param", byteArray))) {
            System.out.println("Function returned: " + docRet.field(myFunction.getName()));
        }

        System.out.println("*** call function with a map containing a byte array ***");
        for (final ODocument docRet : new OSQLSynchQuery<ODocument>("SELECT myFunction(:param)")
                .run(Collections.singletonMap("param", Collections.singletonMap("byteArray", byteArray)))) {
            System.out.println("Function returned: " + docRet.field(myFunction.getName()));
        }

        // Close DB
        orientBaseGraph.getRawGraph().close();
        server.shutdown();
    }

    public static class MyFunction extends OSQLFunctionAbstract {

        /**
         * Constructor
         */
        public MyFunction() {
            super("myFunction", 1, 1);
        }

        public Object execute(final Object iThis, final OIdentifiable iCurrentRecord, final Object iCurrentResult,
                final Object[] iParams,
                final OCommandContext iContext) {
            System.out.println("MyFunction - param received in iParams: " + iParams[0]
                    + " (" + fromByteArray(iParams[0]) + ")");
            System.out.println("MyFunction - param received in iContext: " + iContext.getVariable("param")
                    + " (" + fromByteArray(iContext.getVariable("param")) + ")");
            return "OK";
        }

        private String fromByteArray(final Object param) {
            if (param instanceof byte[]) {
                return new String((byte[]) param);
            } else if (param instanceof Map) {
                return new String((byte[]) ((Map<?, ?>) param).get("byteArray"));
            } else {
                return "not a byte array";
            }
        }

        public String getSyntax() {
            return "myFunction(<param>)";
        }
    }
}

When launching it with V2.2.33, all works perfectly:

*** call function with a byte array ***
MyFunction - param received in iParams: [B@7a44b873 (hello)
MyFunction - param received in iContext: [B@7b02991a (hello)
Function returned: OK
*** call function with a map containing a byte array ***
MyFunction - param received in iParams: {byteArray=[B@2ca262bc} (hello)
MyFunction - param received in iContext: {byteArray=[B@27e21e1} (hello)
Function returned: OK

When launching it with 3.0.27, there two bugs:

*** call function with a byte array ***
MyFunction - param received in iParams: ::param (not a byte array)
MyFunction - param received in iContext: [B@84cdf9e (hello)
Function returned: OK
*** call function with a map containing a byte array ***
Exception 2DA672AA in storage memory:MyDB: 3.0.27 - Veloce (build e4e73e77074c4f49337ae58b171513cd10ac1157, branch UNKNOWN)
java.lang.StackOverflowError
at java.util.HashMap.hash(HashMap.java:339)
at java.util.HashMap.containsKey(HashMap.java:596)
at com.orientechnologies.orient.core.sql.parser.ONamedParameter.bindFromInputParams(ONamedParameter.java:66)
at com.orientechnologies.orient.core.sql.parser.ONamedParameter.toString(ONamedParameter.java:36)
at com.orientechnologies.orient.core.sql.parser.OExpression.toString(OExpression.java:220)
at com.orientechnologies.orient.core.sql.parser.OCollection.toString(OCollection.java:43)
at com.orientechnologies.orient.core.sql.parser.ONamedParameter.toString(ONamedParameter.java:44)
at com.orientechnologies.orient.core.sql.parser.OExpression.toString(OExpression.java:220)
at com.orientechnologies.orient.core.sql.parser.OCollection.toString(OCollection.java:43)
at com.orientechnologies.orient.core.sql.parser.ONamedParameter.toString(ONamedParameter.java:44)
at com.orientechnologies.orient.core.sql.parser.OExpression.toString(OExpression.java:220)
at com.orientechnologies.orient.core.sql.parser.OCollection.toString(OCollection.java:43)
at com.orientechnologies.orient.core.sql.parser.ONamedParameter.toString(ONamedParameter.java:44)
at com.orientechnologies.orient.core.sql.parser.OExpression.toString(OExpression.java:220)
at com.orientechnologies.orient.core.sql.parser.OCollection.toString(OCollection.java:43)
at com.orientechnologies.orient.core.sql.parser.ONamedParameter.toString(ONamedParameter.java:44)
at com.orientechnologies.orient.core.sql.parser.OExpression.toString(OExpression.java:220)
at com.orientechnologies.orient.core.sql.parser.OCollection.toString(OCollection.java:43)

There is a first bug when using directly byte array as parameter.
In iParams array, the value is the name of the parameter and not the byte array. There is a workaround with iContext, but it would be better to correct it.

There is a second bug when using byte array inside a map. Doing it causes a StackOverflowError. I have no workaround for this and it is very problematic.
I looked for the bug origin and the problem is in com.orientechnologies.orient.core.sql.parser.OInputParameter class int the method toParsedTree. This method handles null value, Boolean, Integer, BigDecimal, Number, String, MultiValue (exluding byte array), Map, OIdentifiable, Date and Enum. When the value is not handled, the method return this.
With the map as parameter, it is going in MultiValue case:

    if (OMultiValue.isMultiValue(value) && !(value instanceof byte[]) && !(value instanceof Byte[])) {
      OCollection coll = new OCollection(-1);
      coll.expressions = new ArrayList<OExpression>();
      Iterator iterator = OMultiValue.getMultiValueIterator(value);
      while (iterator.hasNext()) {
        Object o = iterator.next();
        OExpression exp = new OExpression(-1);
        exp.value = toParsedTree(o);
        coll.expressions.add(exp);
      }
      return coll;
    }

When it handles the byte array map value, it calls again toParsedTree method. As the byte array is not handled, the method return this at the end but this is “:param” and not the map value so it returns on the map transformation.
I think a byte array case as in V2.2.33 would be great:
Code of OInputParameter::toParsedTree in V2.2.33:

    if (value instanceof byte[]) {
      OFunctionCall function = new OFunctionCall(-1);
      function.name = new OIdentifier(-1);
      function.name.value = "decode";

      OExpression valueExpr = new OExpression(-1);
      valueExpr.singleQuotes = true;
      valueExpr.doubleQuotes = false;
      valueExpr.value = OBase64Utils.encodeBytes((byte[]) value);
      function.getParams().add(valueExpr);

      OExpression dateFormatExpr = new OExpression(-1);
      dateFormatExpr.singleQuotes = true;
      dateFormatExpr.doubleQuotes = false;
      dateFormatExpr.value = "base64";
      function.getParams().add(dateFormatExpr);
      return function;
    }

Thanks in advance.

There is another bug when using Map in parameters. In iParams, we get a List of all map values and not the expected Map. The keys are lost.
In iContext, the map is correct.
JAVA code:

        System.out.println("*** call function with a map containing a string ***");
        for (final ODocument docRet : new OSQLSynchQuery<ODocument>("SELECT myFunction(:param)")
                .run(Collections.singletonMap("param",
                        Collections.singletonMap("string", "hello")))) {
            System.out.println("Function returned: " + docRet.field(myFunction.getName()));
        }

Result:

*** call function with a map containing a string ***
MyFunction - param received in iParams: class java.util.ArrayList: [hello]
MyFunction - param received in iContext: class com.orientechnologies.orient.core.db.record.OTrackedMap: {string=hello}
Function returned: OK