From 3f5d9eaa2a8db2e06ae7783fd65ac579a9e43afb Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sat, 16 Dec 2023 13:47:00 +0100 Subject: [PATCH] Add DB Dialects --- .../org/itsallcode/jdbc/ExasolTypeTest.java | 3 +- .../itsallcode/jdbc/ConnectionFactory.java | 9 --- .../java/org/itsallcode/jdbc/Context.java | 31 +++++++++- .../jdbc/SimplePreparedStatement.java | 3 +- .../itsallcode/jdbc/dialect/DbDialect.java | 7 +++ .../jdbc/dialect/ExasolDialect.java | 20 +++++++ .../itsallcode/jdbc/dialect/Extractor.java | 11 ++++ .../itsallcode/jdbc/dialect/Extractors.java | 48 +++++++++++++++ .../itsallcode/jdbc/dialect/H2Dialect.java | 20 +++++++ .../jdbc/resultset/ColumnValueConverter.java | 26 ++++++++ .../jdbc/resultset/ConvertingResultSet.java | 10 ++-- .../resultset/ResultSetValueConverter.java | 59 +++++++++++++++++++ .../resultset/generic/GenericRowMapper.java | 2 +- .../resultset/generic/SimpleMetaData.java | 27 ++++++++- .../jdbc/ConnectionFactoryITest.java | 15 ++--- .../java/org/itsallcode/jdbc/ExampleTest.java | 4 +- .../org/itsallcode/jdbc/H2TestFixture.java | 4 +- .../jdbc/SimpleConnectionITest.java | 3 +- 18 files changed, 269 insertions(+), 33 deletions(-) create mode 100644 src/main/java/org/itsallcode/jdbc/dialect/DbDialect.java create mode 100644 src/main/java/org/itsallcode/jdbc/dialect/ExasolDialect.java create mode 100644 src/main/java/org/itsallcode/jdbc/dialect/Extractor.java create mode 100644 src/main/java/org/itsallcode/jdbc/dialect/Extractors.java create mode 100644 src/main/java/org/itsallcode/jdbc/dialect/H2Dialect.java create mode 100644 src/main/java/org/itsallcode/jdbc/resultset/ColumnValueConverter.java create mode 100644 src/main/java/org/itsallcode/jdbc/resultset/ResultSetValueConverter.java diff --git a/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java b/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java index 89ab97d..e4ccc3f 100644 --- a/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java +++ b/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java @@ -8,6 +8,7 @@ import java.time.LocalDate; import java.util.stream.Stream; +import org.itsallcode.jdbc.dialect.ExasolDialect; import org.itsallcode.jdbc.resultset.SimpleResultSet; import org.itsallcode.jdbc.resultset.generic.Row; import org.junit.jupiter.api.AfterAll; @@ -35,7 +36,7 @@ static void stopDb() { } SimpleConnection connect() { - return ConnectionFactory.create(Context.builder().build()) // + return ConnectionFactory.create(Context.builder().dialect(new ExasolDialect()).build()) // .create(container.getJdbcUrl(), container.getUsername(), container.getPassword()); } diff --git a/src/main/java/org/itsallcode/jdbc/ConnectionFactory.java b/src/main/java/org/itsallcode/jdbc/ConnectionFactory.java index a8395b8..0e0eb03 100644 --- a/src/main/java/org/itsallcode/jdbc/ConnectionFactory.java +++ b/src/main/java/org/itsallcode/jdbc/ConnectionFactory.java @@ -13,15 +13,6 @@ private ConnectionFactory(final Context context) { this.context = context; } - /** - * Create a new connection factory with a default context. - * - * @return a new instance - */ - public static ConnectionFactory create() { - return create(Context.builder().build()); - } - /** * Create a new connection factory with a custom context. * diff --git a/src/main/java/org/itsallcode/jdbc/Context.java b/src/main/java/org/itsallcode/jdbc/Context.java index 6fc5a0e..9482a7e 100644 --- a/src/main/java/org/itsallcode/jdbc/Context.java +++ b/src/main/java/org/itsallcode/jdbc/Context.java @@ -1,5 +1,14 @@ package org.itsallcode.jdbc; +import static java.util.stream.Collectors.toList; + +import java.sql.ResultSet; +import java.util.List; +import java.util.Objects; + +import org.itsallcode.jdbc.dialect.DbDialect; +import org.itsallcode.jdbc.resultset.*; +import org.itsallcode.jdbc.resultset.generic.SimpleMetaData; import org.itsallcode.jdbc.resultset.generic.ValueExtractorFactory; /** @@ -7,7 +16,10 @@ */ public class Context { - private Context() { + private final DbDialect dialect; + + private Context(final ContextBuilder builder) { + this.dialect = Objects.requireNonNull(builder.dialect, "dialect"); } /** @@ -28,6 +40,14 @@ public ParameterMapper getParameterMapper() { return ParameterMapper.create(); } + ResultSet convertingResultSet(final ResultSet resultSet) { + final SimpleMetaData metaData = SimpleMetaData.create(resultSet, this); + final List converters = metaData.getColumns().stream() + .map(col -> ColumnValueConverter.simple(dialect.createConverter(col))) + .collect(toList()); + return new ConvertingResultSet(resultSet, ResultSetValueConverter.create(metaData, converters)); + } + /** * Create a new builder for {@link Context} objects. * @@ -42,16 +62,23 @@ public static ContextBuilder builder() { */ public static class ContextBuilder { + private DbDialect dialect; + private ContextBuilder() { } + public ContextBuilder dialect(final DbDialect dialect) { + this.dialect = dialect; + return this; + } + /** * Build a new context. * * @return a new context */ public Context build() { - return new Context(); + return new Context(this); } } } diff --git a/src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java b/src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java index 1ec0c73..99c5196 100644 --- a/src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java +++ b/src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java @@ -18,7 +18,8 @@ class SimplePreparedStatement implements AutoCloseable { SimpleResultSet executeQuery(final RowMapper rowMapper) { final ResultSet resultSet = doExecute(); - return new SimpleResultSet<>(context, resultSet, rowMapper); + final ResultSet convertingResultSet = context.convertingResultSet(resultSet); + return new SimpleResultSet<>(context, convertingResultSet, rowMapper); } private ResultSet doExecute() { diff --git a/src/main/java/org/itsallcode/jdbc/dialect/DbDialect.java b/src/main/java/org/itsallcode/jdbc/dialect/DbDialect.java new file mode 100644 index 0000000..f5f420a --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/dialect/DbDialect.java @@ -0,0 +1,7 @@ +package org.itsallcode.jdbc.dialect; + +import org.itsallcode.jdbc.resultset.generic.SimpleMetaData.ColumnMetaData; + +public interface DbDialect { + Extractor createConverter(final ColumnMetaData column); +} diff --git a/src/main/java/org/itsallcode/jdbc/dialect/ExasolDialect.java b/src/main/java/org/itsallcode/jdbc/dialect/ExasolDialect.java new file mode 100644 index 0000000..9a0dcf3 --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/dialect/ExasolDialect.java @@ -0,0 +1,20 @@ +package org.itsallcode.jdbc.dialect; + +import java.time.LocalDate; +import java.time.LocalTime; + +import org.itsallcode.jdbc.resultset.generic.SimpleMetaData.ColumnMetaData; + +public class ExasolDialect implements DbDialect { + + public Extractor createConverter(final ColumnMetaData column) { + return switch (column.getType().getJdbcType()) { + case TIMESTAMP -> Extractors.timestampToUTCInstant(); + case CLOB -> Extractors.clobToString(); + case BLOB -> Extractors.blobToBytes(); + case TIME -> Extractors.forType(LocalTime.class); + case DATE -> Extractors.forType(LocalDate.class); + default -> Extractors.generic(); + }; + } +} diff --git a/src/main/java/org/itsallcode/jdbc/dialect/Extractor.java b/src/main/java/org/itsallcode/jdbc/dialect/Extractor.java new file mode 100644 index 0000000..4e8c262 --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/dialect/Extractor.java @@ -0,0 +1,11 @@ +package org.itsallcode.jdbc.dialect; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@FunctionalInterface +public interface Extractor { + + Object getObject(ResultSet resultSet, int columnIndex) throws SQLException; + +} \ No newline at end of file diff --git a/src/main/java/org/itsallcode/jdbc/dialect/Extractors.java b/src/main/java/org/itsallcode/jdbc/dialect/Extractors.java new file mode 100644 index 0000000..c245045 --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/dialect/Extractors.java @@ -0,0 +1,48 @@ +package org.itsallcode.jdbc.dialect; + +import java.sql.*; +import java.util.Calendar; +import java.util.TimeZone; + +public class Extractors { + private static final Calendar UTC_CALENDAR = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + + private Extractors() { + } + + static Extractor timestampToUTCInstant() { + return nonNull((resultSet, columnIndex) -> resultSet.getTimestamp(columnIndex, UTC_CALENDAR).toInstant()); + } + + private static Extractor nonNull(final Extractor extractor) { + return (resultSet, columnIndex) -> { + resultSet.getObject(columnIndex); + if (resultSet.wasNull()) { + return null; + } + return extractor.getObject(resultSet, columnIndex); + }; + } + + static Extractor clobToString() { + return nonNull((resultSet, columnIndex) -> { + final Clob clob = resultSet.getClob(columnIndex); + return clob.getSubString(1, (int) clob.length()); + }); + } + + static Extractor blobToBytes() { + return nonNull((resultSet, columnIndex) -> { + final Blob blob = resultSet.getBlob(columnIndex); + return blob.getBytes(1, (int) blob.length()); + }); + } + + static Extractor forType(final Class type) { + return (resultSet, columnIndex) -> resultSet.getObject(columnIndex, type); + } + + static Extractor generic() { + return ResultSet::getObject; + } +} diff --git a/src/main/java/org/itsallcode/jdbc/dialect/H2Dialect.java b/src/main/java/org/itsallcode/jdbc/dialect/H2Dialect.java new file mode 100644 index 0000000..65781de --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/dialect/H2Dialect.java @@ -0,0 +1,20 @@ +package org.itsallcode.jdbc.dialect; + +import java.time.LocalDate; +import java.time.LocalTime; + +import org.itsallcode.jdbc.resultset.generic.SimpleMetaData.ColumnMetaData; + +public class H2Dialect implements DbDialect { + + public Extractor createConverter(final ColumnMetaData column) { + return switch (column.getType().getJdbcType()) { + case TIMESTAMP -> Extractors.timestampToUTCInstant(); + case CLOB -> Extractors.clobToString(); + case BLOB -> Extractors.blobToBytes(); + case TIME -> Extractors.forType(LocalTime.class); + case DATE -> Extractors.forType(LocalDate.class); + default -> Extractors.generic(); + }; + } +} diff --git a/src/main/java/org/itsallcode/jdbc/resultset/ColumnValueConverter.java b/src/main/java/org/itsallcode/jdbc/resultset/ColumnValueConverter.java new file mode 100644 index 0000000..294e835 --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/resultset/ColumnValueConverter.java @@ -0,0 +1,26 @@ +package org.itsallcode.jdbc.resultset; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.itsallcode.jdbc.dialect.Extractor; + +@FunctionalInterface +public interface ColumnValueConverter { + + T getObject(ResultSet resultSet, int columnIndex, Class type) throws SQLException; + + static ColumnValueConverter generic() { + return simple(ResultSet::getObject); + } + + static ColumnValueConverter simple(final Extractor extractor) { + return new ColumnValueConverter() { + @Override + public T getObject(final ResultSet resultSet, final int columnIndex, final Class type) + throws SQLException { + return type.cast(extractor.getObject(resultSet, columnIndex)); + } + }; + } +} diff --git a/src/main/java/org/itsallcode/jdbc/resultset/ConvertingResultSet.java b/src/main/java/org/itsallcode/jdbc/resultset/ConvertingResultSet.java index 1cf4857..55ae2b2 100644 --- a/src/main/java/org/itsallcode/jdbc/resultset/ConvertingResultSet.java +++ b/src/main/java/org/itsallcode/jdbc/resultset/ConvertingResultSet.java @@ -3,21 +3,23 @@ import java.sql.ResultSet; import java.sql.SQLException; -class ConvertingResultSet extends DelegatingResultSet { +public class ConvertingResultSet extends DelegatingResultSet { private final ResultSet delegate; + private final ResultSetValueConverter converter; - ConvertingResultSet(final ResultSet delegate) { + public ConvertingResultSet(final ResultSet delegate, final ResultSetValueConverter converter) { super(delegate); this.delegate = delegate; + this.converter = converter; } @Override public T getObject(final int columnIndex, final Class type) throws SQLException { - throw new UnsupportedOperationException(); + return converter.getObject(delegate, columnIndex, type); } @Override public T getObject(final String columnLabel, final Class type) throws SQLException { - throw new UnsupportedOperationException(); + return converter.getObject(delegate, columnLabel, type); } } diff --git a/src/main/java/org/itsallcode/jdbc/resultset/ResultSetValueConverter.java b/src/main/java/org/itsallcode/jdbc/resultset/ResultSetValueConverter.java new file mode 100644 index 0000000..1537e07 --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/resultset/ResultSetValueConverter.java @@ -0,0 +1,59 @@ +package org.itsallcode.jdbc.resultset; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; + +import org.itsallcode.jdbc.resultset.generic.SimpleMetaData; +import org.itsallcode.jdbc.resultset.generic.SimpleMetaData.ColumnMetaData; + +public class ResultSetValueConverter { + + private final Map convertersByIndex; + private final Map columnIndexByLabel; + + private ResultSetValueConverter(final Map convertersByIndex, + final Map columnIndexByLabel) { + this.convertersByIndex = convertersByIndex; + this.columnIndexByLabel = columnIndexByLabel; + } + + public static ResultSetValueConverter create(final SimpleMetaData resultSetMetadata, + final List converters) { + final Map convertersByIndex = new HashMap<>(); + final Map columnIndexByLabel = new HashMap<>(); + for (int i = 1; i <= converters.size(); i++) { + final ColumnValueConverter converter = converters.get(i - 1); + final ColumnMetaData metaData = resultSetMetadata.getColumnByIndex(i); + convertersByIndex.put(metaData.getColumnIndex(), converter); + columnIndexByLabel.put(metaData.getLabel(), i); + } + return new ResultSetValueConverter(convertersByIndex, columnIndexByLabel); + } + + public T getObject(final ResultSet delegate, final int columnIndex, final Class type) throws SQLException { + return getConverter(columnIndex).getObject(delegate, columnIndex, type); + } + + public T getObject(final ResultSet delegate, final String columnLabel, final Class type) + throws SQLException { + final int columnIndex = getIndexForLabel(columnLabel); + return getConverter(columnIndex).getObject(delegate, columnIndex, type); + } + + private int getIndexForLabel(final String columnLabel) { + final Integer index = columnIndexByLabel.get(columnLabel); + if (index == null) { + throw new IllegalStateException("No index found for column label '" + columnLabel + "'"); + } + return index.intValue(); + } + + private ColumnValueConverter getConverter(final int columnIndex) { + final ColumnValueConverter converter = convertersByIndex.get(columnIndex); + if (converter == null) { + throw new IllegalStateException("No converter found for column index " + columnIndex); + } + return converter; + } +} diff --git a/src/main/java/org/itsallcode/jdbc/resultset/generic/GenericRowMapper.java b/src/main/java/org/itsallcode/jdbc/resultset/generic/GenericRowMapper.java index ae4eddd..ac5336e 100644 --- a/src/main/java/org/itsallcode/jdbc/resultset/generic/GenericRowMapper.java +++ b/src/main/java/org/itsallcode/jdbc/resultset/generic/GenericRowMapper.java @@ -32,7 +32,7 @@ public GenericRowMapper(final ColumnValuesConverter converter) { @Override public T mapRow(final Context context, final ResultSet resultSet, final int rowNum) throws SQLException { if (rowBuilder == null) { - rowBuilder = new ResultSetRowBuilder(SimpleMetaData.create(resultSet.getMetaData(), context)); + rowBuilder = new ResultSetRowBuilder(SimpleMetaData.create(resultSet, context)); } final Row row = rowBuilder.buildRow(resultSet, rowNum); return converter.mapRow(row); diff --git a/src/main/java/org/itsallcode/jdbc/resultset/generic/SimpleMetaData.java b/src/main/java/org/itsallcode/jdbc/resultset/generic/SimpleMetaData.java index 7424e65..7fabeae 100644 --- a/src/main/java/org/itsallcode/jdbc/resultset/generic/SimpleMetaData.java +++ b/src/main/java/org/itsallcode/jdbc/resultset/generic/SimpleMetaData.java @@ -1,7 +1,6 @@ package org.itsallcode.jdbc.resultset.generic; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; +import java.sql.*; import java.util.ArrayList; import java.util.List; @@ -18,7 +17,19 @@ private SimpleMetaData(final List columns) { this.columns = columns; } - static SimpleMetaData create(final ResultSetMetaData metaData, final Context context) { + public static SimpleMetaData create(final ResultSet resultSet, final Context context) { + return create(getMetaData(resultSet), context); + } + + private static ResultSetMetaData getMetaData(final ResultSet resultSet) { + try { + return resultSet.getMetaData(); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error getting meta data", e); + } + } + + private static SimpleMetaData create(final ResultSetMetaData metaData, final Context context) { try { final List columns = createColumnMetaData(metaData, context); return new SimpleMetaData(columns); @@ -49,6 +60,16 @@ public List getColumns() { return columns; } + /** + * Get column metadata for a given index (one based). + * + * @param index column index (one based) + * @return column metadata + */ + public ColumnMetaData getColumnByIndex(final int index) { + return columns.get(index - 1); + } + /** * Represents the metadata of a single column. */ diff --git a/src/test/java/org/itsallcode/jdbc/ConnectionFactoryITest.java b/src/test/java/org/itsallcode/jdbc/ConnectionFactoryITest.java index dbd374a..f111666 100644 --- a/src/test/java/org/itsallcode/jdbc/ConnectionFactoryITest.java +++ b/src/test/java/org/itsallcode/jdbc/ConnectionFactoryITest.java @@ -2,24 +2,21 @@ import static org.assertj.core.api.Assertions.assertThat; +import org.itsallcode.jdbc.dialect.H2Dialect; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class ConnectionFactoryITest -{ +class ConnectionFactoryITest { private ConnectionFactory connectionFactory; @BeforeEach - void setUp() - { - connectionFactory = ConnectionFactory.create(); + void setUp() { + connectionFactory = ConnectionFactory.create(Context.builder().dialect(new H2Dialect()).build()); } @Test - void createConnection() - { - try (SimpleConnection connection = connectionFactory.create("jdbc:h2:mem:")) - { + void createConnection() { + try (SimpleConnection connection = connectionFactory.create("jdbc:h2:mem:")) { assertThat(connection).isNotNull(); } } diff --git a/src/test/java/org/itsallcode/jdbc/ExampleTest.java b/src/test/java/org/itsallcode/jdbc/ExampleTest.java index 5d272e5..8d32ae8 100644 --- a/src/test/java/org/itsallcode/jdbc/ExampleTest.java +++ b/src/test/java/org/itsallcode/jdbc/ExampleTest.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.stream.Stream; +import org.itsallcode.jdbc.dialect.H2Dialect; import org.itsallcode.jdbc.resultset.SimpleResultSet; import org.itsallcode.jdbc.resultset.generic.Row; import org.junit.jupiter.api.Test; @@ -20,7 +21,8 @@ Object[] toRow() { return new Object[] { id, name }; } } - final ConnectionFactory connectionFactory = ConnectionFactory.create(); + final ConnectionFactory connectionFactory = ConnectionFactory + .create(Context.builder().dialect(new H2Dialect()).build()); try (SimpleConnection connection = connectionFactory.create("jdbc:h2:mem:", "user", "password")) { connection.executeScript(readResource("/schema.sql")); connection.insert("NAMES", List.of("ID", "NAME"), Name::toRow, diff --git a/src/test/java/org/itsallcode/jdbc/H2TestFixture.java b/src/test/java/org/itsallcode/jdbc/H2TestFixture.java index e42f185..5eed1fe 100644 --- a/src/test/java/org/itsallcode/jdbc/H2TestFixture.java +++ b/src/test/java/org/itsallcode/jdbc/H2TestFixture.java @@ -1,8 +1,10 @@ package org.itsallcode.jdbc; +import org.itsallcode.jdbc.dialect.H2Dialect; + public class H2TestFixture { public static SimpleConnection createMemConnection() { - return createMemConnection(Context.builder().build()); + return createMemConnection(Context.builder().dialect(new H2Dialect()).build()); } public static SimpleConnection createMemConnection(final Context context) { diff --git a/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java b/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java index 1885a3f..29f8353 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java @@ -10,6 +10,7 @@ import java.util.*; import java.util.stream.Stream; +import org.itsallcode.jdbc.dialect.H2Dialect; import org.itsallcode.jdbc.identifier.Identifier; import org.itsallcode.jdbc.resultset.RowMapper; import org.itsallcode.jdbc.resultset.SimpleResultSet; @@ -62,7 +63,7 @@ void executeQueryWithGenericRowMapper() { @Test void executeQueryWithListRowMapper() { - final ConnectionFactory factory = ConnectionFactory.create(); + final ConnectionFactory factory = ConnectionFactory.create(Context.builder().dialect(new H2Dialect()).build()); try (SimpleConnection connection = factory.create("jdbc:h2:mem:")) { connection.executeScript("CREATE TABLE TEST(ID INT, NAME VARCHAR(255));" + "insert into test (id, name) values (1, 'test');");