Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OptionalRowMapperFactory can cause NullPointerExceptions with custom Row Mappers #2669

Open
Randgalt opened this issue Apr 12, 2024 · 1 comment

Comments

@Randgalt
Copy link
Contributor

Randgalt commented Apr 12, 2024

OptionalRowMapperFactory doesn't change how underlying mappers interpret propagating nulls. So, a common use case will cause NullPointerException. If a SqlQuery returns an Optional single column row, the underlying mapper is called even if the column is null when a custom row mapper is registered for a type.

Example:

Test.java

package test;

import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.config.ConfigRegistry;
import org.jdbi.v3.core.mapper.ColumnMapper;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.mapper.RowMapperFactory;
import org.jdbi.v3.core.mapper.reflect.JdbiConstructor;
import org.jdbi.v3.core.statement.StatementContext;
import org.jdbi.v3.sqlobject.SqlObjectPlugin;
import org.jdbi.v3.sqlobject.customizer.Bind;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.testcontainers.containers.PostgreSQLContainer;

import java.lang.reflect.Type;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;

import static java.util.Objects.requireNonNull;

public class Test
{
    public record Value(String value)
    {
        @JdbiConstructor
        public Value
        {
            requireNonNull(value, "value is null");
        }
    }

    public interface Dao
    {
        @SqlQuery("SELECT value FROM foo where key = :key")
        Optional<Value> get(@Bind("key") String key);

        @SqlUpdate("INSERT INTO foo(key, value) VALUES (:key, :value)")
        void insert(@Bind("key") String key, @Bind("value") Optional<Value> value);
    }

    public static class Mapper
            implements RowMapperFactory, RowMapper<Value>
    {
        @Override
        public Optional<RowMapper<?>> build(Type type, ConfigRegistry config)
        {
            if (type.equals(Value.class)) {
                return Optional.of(this);
            }
            return Optional.empty();
        }

        @Override
        public Value map(ResultSet rs, StatementContext ctx)
                throws SQLException
        {
            String value = rs.getString("value");
            return new Value(value);
        }
    }

    public static void main(String[] args)
    {
        PostgreSQLContainer container = new PostgreSQLContainer();
        container.start();

        Jdbi jdbi = Jdbi.create(container.getJdbcUrl(), container.getUsername(), container.getPassword())
                .installPlugin(new SqlObjectPlugin())
                .registerColumnMapper(Value.class, ColumnMapper.getDefaultColumnMapper())
                .registerRowMapper((RowMapperFactory) new Mapper());

        jdbi.useHandle(handle -> handle.execute("CREATE TABLE foo (key VARCHAR NOT NULL PRIMARY KEY, value VARCHAR)"));

        jdbi.useExtension(Dao.class, dao -> dao.insert("1", Optional.empty()));

        // this will throw NPE
        Optional<Value> value = jdbi.withExtension(Dao.class, dao -> dao.get("1"));

        container.stop();
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>foo</groupId>
    <artifactId>jdbi-bug</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jdbi</groupId>
                <artifactId>jdbi3-bom</artifactId>
                <version>3.45.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.jdbi</groupId>
            <artifactId>jdbi3-core</artifactId>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.7.2</version>
        </dependency>

        <dependency>
            <groupId>org.jdbi</groupId>
            <artifactId>jdbi3-json</artifactId>
        </dependency>

        <dependency>
            <groupId>org.jdbi</groupId>
            <artifactId>jdbi3-sqlobject</artifactId>
        </dependency>

        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers</artifactId>
            <version>1.19.4</version>
        </dependency>

        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>postgresql</artifactId>
            <version>1.19.4</version>
        </dependency>
    </dependencies>
</project>
@wendigo
Copy link

wendigo commented Apr 18, 2024

@hgschmie gentle ping :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants