Search code examples
sqlitejdbcguicemybatisguacamole

SQLite + Mybatis-guice throw ArrayIndexOutOfBoundsException


I'm trying to implement a Guice module that to lets Guacamole use SQLite as a backend. The Guacamole project has a generic JDBC base module. This lets you implement modules for specific datastores with less code. Most of the lines of code end up being in mapper XML files. The project provides PostgreSQL and MySQL implementations.

I based this SQLite module off of the MySQL module. For the mapper XML files, SQLite and MySQL are similar enough that I didn't have to make any changes. However, when I try to use the SQLite module, I get this error:

### Error querying database.  Cause: java.lang.ArrayIndexOutOfBoundsException: 2
### The error may exist in org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: SELECT             guacamole_connection_group.connection_group_id,             connection_group_name,             parent_id,             type,             max_connections,             max_connections_per_user,             enable_session_affinity         FROM guacamole_connection_group         JOIN guacamole_connection_group_permission ON guacamole_connection_group_permission.connection_group_id = guacamole_connection_group.connection_group_id         WHERE guacamole_connection_group.connection_group_id IN              (                   ?              )              AND user_id = ?             AND permission = 'READ';          SELECT parent_id, guacamole_connection_group.connection_group_id         FROM guacamole_connection_group         JOIN guacamole_connection_group_permission ON guacamole_connection_group_permission.connection_group_id = guacamole_connection_group.connection_group_id         WHERE parent_id IN              (                   ?              )              AND user_id = ?             AND permission = 'READ';          SELECT parent_id, guacamole_connection.connection_id         FROM guacamole_connection         JOIN guacamole_connection_permission ON guacamole_connection_permission.connection_id = guacamole_connection.connection_id         WHERE parent_id IN              (                   ?              )              AND user_id = ?             AND permission = 'READ';
### Cause: java.lang.ArrayIndexOutOfBoundsException: 2

It looks like the problem is that two parameters are passed to the query, but each is repeated three times. When MyBatis generates the PreparedStatement, it acts as if there are six parameters that needed to be passed in.

Here's the query it has a problem with:

<!-- Select multiple connection groups by identifier only if readable -->
<select id="selectReadable" resultMap="ConnectionGroupResultMap"
        resultSets="connectionGroups,childConnectionGroups,childConnections">

    SELECT
        guacamole_connection_group.connection_group_id,
        connection_group_name,
        parent_id,
        type,
        max_connections,
        max_connections_per_user,
        enable_session_affinity
    FROM guacamole_connection_group
    JOIN guacamole_connection_group_permission ON guacamole_connection_group_permission.connection_group_id = guacamole_connection_group.connection_group_id
    WHERE guacamole_connection_group.connection_group_id IN
        <foreach collection="identifiers" item="identifier"
                 open="(" separator="," close=")">
            #{identifier,jdbcType=VARCHAR}
        </foreach>
        AND user_id = #{user.objectID,jdbcType=INTEGER}
        AND permission = 'READ';

    SELECT parent_id, guacamole_connection_group.connection_group_id
    FROM guacamole_connection_group
    JOIN guacamole_connection_group_permission ON guacamole_connection_group_permission.connection_group_id = guacamole_connection_group.connection_group_id
    WHERE parent_id IN
        <foreach collection="identifiers" item="identifier"
                 open="(" separator="," close=")">
            #{identifier,jdbcType=VARCHAR}
        </foreach>
        AND user_id = #{user.objectID,jdbcType=INTEGER}
        AND permission = 'READ';

    SELECT parent_id, guacamole_connection.connection_id
    FROM guacamole_connection
    JOIN guacamole_connection_permission ON guacamole_connection_permission.connection_id = guacamole_connection.connection_id
    WHERE parent_id IN
        <foreach collection="identifiers" item="identifier"
                 open="(" separator="," close=")">
            #{identifier,jdbcType=VARCHAR}
        </foreach>
        AND user_id = #{user.objectID,jdbcType=INTEGER}
        AND permission = 'READ';

</select>

If I manually populate the parameters, I can execute this against the SQLite database. Also, the MySQL version works fine.

What the heck is going on? What can I do to debug this? Is it a MyBatis problem or something with the JDBC connector?

If it helps, you can see the code for the module here.

Here's the method for the parameter the mapper related to this query. The full mapper classes for ConnectionGroup are here and here. The full mapper XML for my SQLite module is here.

Collection<ModelType> selectReadable(@Param("user") UserModel user,
        @Param("identifiers") Collection<String> identifiers);

This is what the ConnectionGroupResultMap looks like:

<resultMap id="ConnectionGroupResultMap" type="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupModel" >

    <!-- Connection group properties -->
    <id     column="connection_group_id"      property="objectID"               jdbcType="INTEGER"/>
    <result column="connection_group_name"    property="name"                   jdbcType="VARCHAR"/>
    <result column="parent_id"                property="parentIdentifier"       jdbcType="INTEGER"/>
    <result column="type"                     property="type"                   jdbcType="VARCHAR"
            javaType="org.apache.guacamole.net.auth.ConnectionGroup$Type"/>
    <result column="max_connections"          property="maxConnections"         jdbcType="INTEGER"/>
    <result column="max_connections_per_user" property="maxConnectionsPerUser"  jdbcType="INTEGER"/>
    <result column="enable_session_affinity"  property="sessionAffinityEnabled" jdbcType="BOOLEAN"/>

    <!-- Child connection groups -->
    <collection property="connectionGroupIdentifiers" resultSet="childConnectionGroups" ofType="java.lang.String"
                column="connection_group_id" foreignColumn="parent_id">
        <result column="connection_group_id"/>
    </collection>

    <!-- Child connections -->
    <collection property="connectionIdentifiers" resultSet="childConnections" ofType="java.lang.String"
                column="connection_group_id" foreignColumn="parent_id">
        <result column="connection_id"/>
    </collection>

</resultMap>

Solution

  • David,

    I know this is a little old, but I'm also trying to implement a SQLite JDBC module, and ran into exactly the same problem. I've managed to track down the source of the issue, and have filed an issue on the JDBC SQLite github page:

    https://github.com/xerial/sqlite-jdbc/issues/277

    Basically, the SQLite JDBC driver computes one of its array sizes based on the parameter count for a prepared statement. However, in the cases you mentioned, there are multiple SELECT statements which take the same parameters, so the array needs to be x * y (x = parameter count, y = number of select statements) rather than just the number of parameters. When it tries to prepare the statement, it hits the first position beyond the parameter count and generates this exception.

    The other JDBC modules - MySQL, PostgreSQL, and SQL Server, as well as Oracle and H2 that I'm messing with - seem to handle this situation correctly (well, Oracle is a little...special...), but the SQLite driver does not.

    I was able to work around the issue in a really, really kludgy way, by creating two different result maps, one for the generic select and one for the read that checks for READ permissions, and then break out each of the select queries into its own SELECT block and call those from the collection inside the result map. It's nowhere near as elegant as the existing code, but it works.