diff --git a/gradle.properties b/gradle.properties index ba766d3..c8933e5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group = org.xbib name = database -version = 0.0.5 +version = 1.0.0 org.gradle.warning.mode = ALL diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle index ce3fc3f..514af77 100644 --- a/gradle/test/junit5.gradle +++ b/gradle/test/junit5.gradle @@ -20,4 +20,5 @@ test { "${result.skippedTestCount} skipped" } } + systemProperty 'java.util.logging.config.file', 'src/test/resources/logging.properties' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ec77e5..8fad3f5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/Pool.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/Pool.java index b415cfa..bcd5829 100644 --- a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/Pool.java +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/Pool.java @@ -407,15 +407,17 @@ public class Pool implements BagStateListener { private void initializeDataSource() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { - String jdbcUrl = config.getProperties().getProperty("url"); - String dsClassName = config.getDataSourceClassName(); + String url = config.getUrl(); DataSource ds = config.getDataSource(); if (ds == null) { + String dsClassName = config.getDataSourceClassName(); if (dsClassName != null) { Class clazz = Class.forName(dsClassName, true, ClassLoader.getSystemClassLoader()); ds = (DataSource) clazz.getDeclaredConstructor().newInstance(); - } else if (jdbcUrl != null) { - ds = new DriverDataSource(jdbcUrl, config.getDriverClassName(), config.getProperties(), config.getUsername(), config.getPassword()); + } else if (url != null) { + ds = new DriverDataSource(url, config.getDriverClassName(), config.getProperties(), config.getUsername(), config.getPassword()); + } else { + throw new IllegalStateException("no dataSource configured?"); } } setTargetFromProperties(ds, config.getProperties()); diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolConfig.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolConfig.java index a09e805..f190178 100644 --- a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolConfig.java +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolConfig.java @@ -56,7 +56,7 @@ public class PoolConfig { private String driverClassName; - private String jdbcUrl; + private String url; private String poolName; @@ -106,11 +106,18 @@ public class PoolConfig { this.idleTimeout = IDLE_TIMEOUT; this.initializationFailTimeout = -1; this.isAutoCommit = true; - this.jdbcUrl = properties.getProperty("url"); this.aliveBypassWindowMs = TimeUnit.MILLISECONDS.toMillis(500); this.housekeepingPeriodMs = TimeUnit.SECONDS.toMillis(30); } + public void setUrl(String url) { + this.url = url; + } + + public String getUrl() { + return url; + } + public String getCatalog() { return catalog; } @@ -588,7 +595,7 @@ public class PoolConfig { transactionIsolationName = getNullIfEmpty(transactionIsolationName); dataSourceClassName = getNullIfEmpty(dataSourceClassName); driverClassName = getNullIfEmpty(driverClassName); - jdbcUrl = getNullIfEmpty(jdbcUrl); + url = getNullIfEmpty(url); if (dataSource != null) { if (dataSourceClassName != null) { logger.log(Level.WARNING, "using dataSource and ignoring dataSourceClassName: " + poolName); @@ -597,17 +604,17 @@ public class PoolConfig { if (driverClassName != null) { logger.log(Level.SEVERE, "cannot use driverClassName and dataSourceClassName together: " + poolName); throw new IllegalStateException("cannot use driverClassName and dataSourceClassName together."); - } else if (jdbcUrl != null) { - logger.log(Level.WARNING, "using dataSourceClassName and ignoring jdbcUrl: " + poolName); + } else if (url != null) { + logger.log(Level.WARNING, "using dataSourceClassName and ignoring url: " + poolName); } - } else if (jdbcUrl != null) { + } else if (url != null) { // ok } else if (driverClassName != null) { - logger.log(Level.SEVERE, "jdbcUrl is required with driverClassName: " + poolName); - throw new IllegalArgumentException("jdbcUrl is required with driverClassName."); + logger.log(Level.SEVERE, "url is required with driverClassName: " + poolName); + throw new IllegalArgumentException("url is required with driverClassName"); } else { - logger.log(Level.SEVERE, "dataSource or dataSourceClassName or jdbcUrl is required: " + poolName); - throw new IllegalArgumentException("dataSource or dataSourceClassName or jdbcUrl is required."); + logger.log(Level.SEVERE, "dataSource or dataSourceClassName or url is required: " + poolName); + throw new IllegalArgumentException("dataSource or dataSourceClassName or url is required"); } validateNumerics(); } diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyResultSet.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyResultSet.java index 9f7a467..1a22cae 100644 --- a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyResultSet.java +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyResultSet.java @@ -481,7 +481,7 @@ public class ProxyResultSet implements ResultSet { @Override public long getLong(int columnIndex) throws SQLException { - return delegate.getInt(columnIndex); + return delegate.getLong(columnIndex); } @Override diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/DriverDataSource.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/DriverDataSource.java index b949a22..cdaf20c 100644 --- a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/DriverDataSource.java +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/DriverDataSource.java @@ -136,12 +136,21 @@ public class DriverDataSource implements DataSource { @Override public void setLoginTimeout(int seconds) { - DriverManager.setLoginTimeout(seconds); + try { + DriverManager.setLoginTimeout(seconds); + } catch (Exception e) { + logger.log(Level.WARNING, "setLoginTimeout failed"); + } } @Override public int getLoginTimeout() { - return DriverManager.getLoginTimeout(); + try { + return DriverManager.getLoginTimeout(); + } catch (Exception e) { + logger.log(Level.WARNING, "getLoginTimeout failed"); + return 0; + } } @Override diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/BagTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/BagTest.java index ada6dcd..de0d459 100644 --- a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/BagTest.java +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/BagTest.java @@ -63,7 +63,6 @@ public class BagTest { bag.remove(inuse); bag.remove(inuse); assertTrue(bag.getLastMessage().contains("not borrowed or reserved")); - bag.close(); try { PoolEntry bagEntry = pool.newPoolEntry(); bag.add(bagEntry); diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionStateTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionStateTest.java index c8922b0..a6ce92e 100644 --- a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionStateTest.java +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionStateTest.java @@ -29,12 +29,11 @@ public class ConnectionStateTest { config.setConnectionTestQuery("VALUES 1"); config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); try (PoolDataSource ds = new PoolDataSource(config)) { - try (Connection connection = ds.getConnection()) { - Connection unwrap = connection.unwrap(Connection.class); - unwrap.setAutoCommit(false); - connection.close(); - assertFalse(unwrap.getAutoCommit()); - } + Connection connection = ds.getConnection(); + Connection unwrap = connection.unwrap(Connection.class); + unwrap.setAutoCommit(false); + connection.close(); + assertFalse(unwrap.getAutoCommit()); } } @@ -48,12 +47,11 @@ public class ConnectionStateTest { config.setConnectionTestQuery("VALUES 1"); config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); try (PoolDataSource ds = new PoolDataSource(config)) { - try (Connection connection = ds.getConnection()) { - Connection unwrap = connection.unwrap(Connection.class); - unwrap.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); - connection.close(); - assertEquals(Connection.TRANSACTION_READ_UNCOMMITTED, unwrap.getTransactionIsolation()); - } + Connection connection = ds.getConnection(); + Connection unwrap = connection.unwrap(Connection.class); + unwrap.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); + connection.close(); + assertEquals(Connection.TRANSACTION_READ_UNCOMMITTED, unwrap.getTransactionIsolation()); } } @@ -76,12 +74,11 @@ public class ConnectionStateTest { config.setConnectionTestQuery("VALUES 1"); config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); try (PoolDataSource ds = new PoolDataSource(config)) { - try (Connection connection = ds.getConnection()) { - Connection unwrap = connection.unwrap(Connection.class); - connection.setReadOnly(true); - connection.close(); - assertFalse(unwrap.isReadOnly()); - } + Connection connection = ds.getConnection(); + Connection unwrap = connection.unwrap(Connection.class); + connection.setReadOnly(true); + connection.close(); + assertFalse(unwrap.isReadOnly()); } } diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionTest.java index fcd00a0..3c388d0 100644 --- a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionTest.java +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionTest.java @@ -453,6 +453,7 @@ public class ConnectionTest { config.setDataSource(stubDataSource); try (PoolDataSource ds = new PoolDataSource(config); Connection ignored = ds.getConnection()) { + assertNotNull(ignored); fail("Initialization should have failed"); } catch (SQLTransientConnectionException e) { // passed @@ -472,6 +473,7 @@ public class ConnectionTest { config.setDataSource(stubDataSource); try (PoolDataSource ds = new PoolDataSource(config)) { try (Connection ignored = ds.getConnection()) { + assertNotNull(ignored); stubDataSource.setErrorOnConnection(true); fail("SQLException should occur!"); } catch (Exception e) { @@ -520,6 +522,7 @@ public class ConnectionTest { config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); try (PoolDataSource ds = new PoolDataSource(config); Connection ignored = ds.getConnection()) { + assertNotNull(ignored); // passed } catch (SQLTransientConnectionException sqle) { fail("Failed to obtain connection"); diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/JdbcDriverTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/JdbcDriverTest.java index 11e9a75..9288dc1 100644 --- a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/JdbcDriverTest.java +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/JdbcDriverTest.java @@ -1,5 +1,6 @@ package org.xbib.io.pool.jdbc; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.AfterEach; @@ -38,6 +39,7 @@ public class JdbcDriverTest { assertNotNull(unwrap); try (Connection connection = ds.getConnection()) { // test that getConnection() succeeds + assertFalse(connection.isClosed()); } } diff --git a/jdbc-query/build.gradle b/jdbc-query/build.gradle index cf34707..d097b06 100644 --- a/jdbc-query/build.gradle +++ b/jdbc-query/build.gradle @@ -1,6 +1,7 @@ dependencies { api project(":jdbc-connection-pool") testImplementation libs.derby + testImplementation libs.hsqldb testImplementation libs.testcontainers testImplementation libs.testcontainers.junit.jupiter testImplementation libs.testcontainers.oracle.xe diff --git a/jdbc-query/src/main/java/module-info.java b/jdbc-query/src/main/java/module-info.java index f5e3439..7990550 100644 --- a/jdbc-query/src/main/java/module-info.java +++ b/jdbc-query/src/main/java/module-info.java @@ -1,4 +1,13 @@ +import org.xbib.jdbc.query.Flavor; +import org.xbib.jdbc.query.flavor.Derby; +import org.xbib.jdbc.query.flavor.Hsql; +import org.xbib.jdbc.query.flavor.Oracle; +import org.xbib.jdbc.query.flavor.Postgresql; +import org.xbib.jdbc.query.flavor.SqlServer; + module org.xbib.jdbc.query { + uses Flavor; requires transitive org.xbib.jdbc.connection.pool; exports org.xbib.jdbc.query; + provides Flavor with Derby, Hsql, Oracle, Postgresql, SqlServer; } diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFrom.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFrom.java index a297726..ae8ebf0 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFrom.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFrom.java @@ -1,6 +1,6 @@ package org.xbib.jdbc.query; -import java.io.File; +import java.io.InputStream; import java.nio.charset.CharsetDecoder; import java.util.Properties; import java.util.function.Function; @@ -47,7 +47,7 @@ public interface ConfigFrom extends Supplier { * defaultPropertyFiles("properties", "conf/app.properties", "local.properties", "sample.properties") * */ - ConfigFrom defaultPropertyFiles(); + ConfigFrom defaultProperties(); /** * Adds a set of properties files to read from, which can be overridden by a specified system property. @@ -56,7 +56,7 @@ public interface ConfigFrom extends Supplier { * defaultPropertyFiles(systemPropertyKey, Charset.defaultCharset().newDecoder(), filenames) * */ - ConfigFrom defaultPropertyFiles(String systemPropertyKey, String... filenames); + ConfigFrom defaultProperties(String systemPropertyKey, String... filenames); /** * Adds a set of properties files to read from, which can be overridden by a specified system property. @@ -67,15 +67,15 @@ public interface ConfigFrom extends Supplier { * .split(File.pathSeparator)); * */ - ConfigFrom defaultPropertyFiles(String systemPropertyKey, CharsetDecoder decoder, String... filenames); + ConfigFrom defaultProperties(String systemPropertyKey, CharsetDecoder decoder, String... filenames); - ConfigFrom propertyFile(String... filenames); + ConfigFrom properties(String... filenames); - ConfigFrom propertyFile(CharsetDecoder decoder, String... filenames); + ConfigFrom properties(CharsetDecoder decoder, String... filenames); - ConfigFrom propertyFile(File... files); + ConfigFrom properties(InputStream... inputStreams); - ConfigFrom propertyFile(CharsetDecoder decoder, File... files); + ConfigFrom properties(CharsetDecoder decoder, InputStream... inputStreams); ConfigFrom rename(String key, String newKey); diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFromImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFromImpl.java index cc96b03..966834c 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFromImpl.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFromImpl.java @@ -1,16 +1,22 @@ package org.xbib.jdbc.query; -import java.io.File; -import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Properties; import java.util.function.Function; import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -20,6 +26,10 @@ import java.util.regex.Pattern; */ public class ConfigFromImpl implements ConfigFrom { + private static final Logger logger = Logger.getLogger(ConfigFromImpl.class.getName()); + + private static final String SEPARATOR = FileSystems.getDefault().getSeparator(); + private final List searchPath = new ArrayList<>(); public ConfigFromImpl() { @@ -72,64 +82,59 @@ public class ConfigFromImpl implements ConfigFrom { } @Override - public ConfigFrom defaultPropertyFiles() { - return defaultPropertyFiles("properties", "conf/app.properties", "local.properties", "sample.properties"); + public ConfigFrom defaultProperties() { + return defaultProperties("properties", "conf/app.properties", "local.properties", "sample.properties"); } @Override - public ConfigFrom defaultPropertyFiles(String systemPropertyKey, String... filenames) { - return defaultPropertyFiles(systemPropertyKey, Charset.defaultCharset().newDecoder(), filenames); + public ConfigFrom defaultProperties(String systemPropertyKey, String... filenames) { + return defaultProperties(systemPropertyKey, Charset.defaultCharset().newDecoder(), filenames); } @Override - public ConfigFrom defaultPropertyFiles(String systemPropertyKey, CharsetDecoder decoder, String... filenames) { - String properties = System.getProperty(systemPropertyKey, String.join(File.pathSeparator, filenames)); - return propertyFile(Charset.defaultCharset().newDecoder(), properties.split(File.pathSeparator)); + public ConfigFrom defaultProperties(String systemPropertyKey, CharsetDecoder decoder, String... filenames) { + String properties = System.getProperty(systemPropertyKey, String.join(SEPARATOR, filenames)); + return properties(Charset.defaultCharset().newDecoder(), properties.split(SEPARATOR)); } @Override - public ConfigFrom propertyFile(String... filenames) { - return propertyFile(Charset.defaultCharset().newDecoder(), filenames); + public ConfigFrom properties(String... filenames) { + return properties(Charset.defaultCharset().newDecoder(), filenames); } @Override - public ConfigFrom propertyFile(CharsetDecoder decoder, String... filenames) { + public ConfigFrom properties(CharsetDecoder decoder, String... filenames) { for (String filename : filenames) { if (filename != null) { - propertyFile(decoder, new File(filename)); + Path path = Paths.get(filename); + if (Files.exists(path)) { + try (InputStream inputStream = Files.newInputStream(path)) { + properties(decoder, inputStream); + } catch (IOException e) { + custom(k -> null, "Ignored: propertyFile(" + filename + ") " + e.getClass().getSimpleName()); + } + } else { + properties(decoder, getClass().getResourceAsStream(filename)); + } } } return this; } @Override - public ConfigFrom propertyFile(File... files) { - return propertyFile(Charset.defaultCharset().newDecoder(), files); + public ConfigFrom properties(InputStream... inputStreams) { + return properties(Charset.defaultCharset().newDecoder(), inputStreams); } @Override - public ConfigFrom propertyFile(CharsetDecoder decoder, File... files) { - for (File file : files) { - if (file != null) { - try { - Properties properties = new Properties(); - try ( - FileInputStream fis = new FileInputStream(file); - InputStreamReader reader = new InputStreamReader(fis, decoder) - ) { - properties.load(reader); - } - searchPath.add(new ConfigImpl(properties::getProperty, "propertyFile(" + file.getAbsolutePath() + ")")); - } catch (Exception e) { - // Put a "fake" provider in so we can see it failed - String fileName = file.getName(); - try { - fileName = file.getAbsolutePath(); - } catch (Exception ignored) { - // Fall back to relative name - } - custom(k -> null, "Ignored: propertyFile(" + fileName + ") " + e.getClass().getSimpleName()); - } + public ConfigFrom properties(CharsetDecoder decoder, InputStream... inputStreams) { + for (InputStream inputStream : inputStreams) { + Properties properties = new Properties(); + try (InputStreamReader reader = new InputStreamReader(inputStream, decoder)) { + properties.load(reader); + searchPath.add(new ConfigImpl(properties::getProperty, "propertyFile(" + inputStream + ")")); + } catch (Exception e) { + custom(k -> null, "Ignored: propertyFile(" + inputStream + ") " + e.getClass().getSimpleName()); } } return this; diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigImpl.java index 063b0f5..9d44e7e 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigImpl.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigImpl.java @@ -169,7 +169,6 @@ public class ConfigImpl implements Config { return null; } } - @Override public BigDecimal getBigDecimal(String key, BigDecimal defaultValue) { @@ -177,7 +176,6 @@ public class ConfigImpl implements Config { return value == null ? defaultValue : value; } - @Override public BigDecimal getBigDecimalOrThrow(String key) { return nonnull(key, getBigDecimal(key)); diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Database.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Database.java index 58548f3..c683d91 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/Database.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Database.java @@ -1,8 +1,10 @@ package org.xbib.jdbc.query; import java.sql.Connection; +import java.sql.SQLException; import java.time.LocalDateTime; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -216,4 +218,27 @@ public interface Database extends Supplier { * with millisToWarn=10000 and millisToError=30000. */ void assertTimeSynchronized(); + + /** + * Write data into a queue table, select a channel, and consume the returned primary keys. + * + * @param table the queue table + * @param channel the queue table channel + * @param data the data for the queue table + * @param consumer the consumer for the primary keys + * @throws SQLException if writing fails + */ + void writeQueue(String table, String channel, String data, Consumer consumer) throws SQLException; + + /** + * Read from a queue table and lock rows in a transaction until processing by a consumer + * has succeeded or failed. + * Commit only if processing was successful, otherwise roll back the transaction. + * The connection is switched into manual commit and afterwards, auto-commit is enabled again. + * @param table the queue table + * @param channel the queue table channel. A queue table can have many channels + * @param consumer a consumer for the queue table data column or null + * @throws SQLException if rollback fails + */ + public void readQueue(String table, String channel, Consumer consumer) throws SQLException; } diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseImpl.java index b2a93af..77d786e 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseImpl.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseImpl.java @@ -1,24 +1,27 @@ package org.xbib.jdbc.query; -import java.lang.reflect.Method; import java.sql.Connection; import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; import java.time.Duration; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import java.util.logging.Logger; /** - * Primary class for accessing a relational (SQL) database. + * Primary class for accessing a relational database. */ public class DatabaseImpl implements Database { - private static final Logger log = Logger.getLogger(Database.class.getName()); + private static final Logger logger = Logger.getLogger(Database.class.getName()); private final Connection connection; @@ -88,7 +91,7 @@ public class DatabaseImpl implements Database { } @Override - public Long nextSequenceValue(/*@Untainted*/ String sequenceName) { + public Long nextSequenceValue(String sequenceName) { return toSelect(flavor().sequenceSelectNextVal(sequenceName)).queryLongOrNull(); } @@ -97,9 +100,10 @@ public class DatabaseImpl implements Database { return options.currentDate(); } + @Override public void commitNow() { if (options.ignoreTransactionControl()) { - log.fine("Ignoring call to commitNow()"); + logger.fine("Ignoring call to commitNow()"); return; } if (!options.allowTransactionControl()) { @@ -112,9 +116,10 @@ public class DatabaseImpl implements Database { } } + @Override public void rollbackNow() { if (options.ignoreTransactionControl()) { - log.fine("Ignoring call to rollbackNow()"); + logger.fine("Ignoring call to rollbackNow()"); return; } if (!options.allowTransactionControl()) { @@ -153,53 +158,22 @@ public class DatabaseImpl implements Database { } @Override - public void dropSequenceQuietly(/*@Untainted*/ String sequenceName) { + public void dropSequenceQuietly(String sequenceName) { ddl(flavor().sequenceDrop(sequenceName)).executeQuietly(); } @Override - public void dropTableQuietly(/*@Untainted*/ String tableName) { - if (flavor() == Flavor.postgresql || flavor() == Flavor.hsqldb) { - ddl("drop table if exists " + tableName).executeQuietly(); - } else { - ddl("drop table " + tableName).executeQuietly(); - } + public void dropTableQuietly(String tableName) { + ddl(flavor().tableDrop(tableName)).executeQuietly(); } @Override public boolean tableExists(String tableName) throws DatabaseException { - - String schemaName = null; - Method getSchema = null; - try { - // Use reflections to see if connection.getSchema API exists. It should exist for any JDBC7 or later implementation - // We still support Oracle 11 with odbc6, however, so we can't assume it's there. - getSchema = connection.getClass().getDeclaredMethod("getSchema"); - } catch (NoSuchMethodException noMethodExc) { - // Expected if method does not exist - just let it go + return tableExists(tableName, connection.getSchema()); + } catch (SQLException e) { + throw new DatabaseException("Unable to getSchema()", e); } - - try { - if (getSchema != null) { - schemaName = ((String) getSchema.invoke(connection, new Object[0])); - } else if (flavor() == Flavor.oracle) { - // Oracle defaults to user name schema - use that. - log.warning("Connection getSchema API was not found. Defaulting to Oracle user name schema." + - "If this is not appropriate, please use tableExists(tableName, schemaName) API or upgrade to ojdbc7 or later"); - schemaName = connection.getMetaData().getUserName(); - } - if (schemaName == null) { - // connection.getSchema API was supported starting at JDK1.7. Method should not be null. - throw new NullPointerException("Unable to retrieve schema name."); - } - - } catch (Exception exc) { - throw new DatabaseException("Unable to determine the schema. " + - "Please use tableExists(tableName, schemaName API) or upgrade to a JDBC7 driver or later.", exc); - } - - return tableExists(tableName, schemaName); } @Override @@ -230,24 +204,24 @@ public class DatabaseImpl implements Database { try { DatabaseMetaData metaData = connection.getMetaData(); String normalizedTable = normalizeTableName(tableName); - ResultSet rs = metaData.getColumns(null, null, normalizedTable, "%"); - ResultSetMetaData rsmd = rs.getMetaData(); + ResultSet resultSet = metaData.getColumns(null, null, normalizedTable, "%"); + ResultSetMetaData rsmd = resultSet.getMetaData(); int cols = rsmd.getColumnCount(); - while (rs.next()) { + while (resultSet.next()) { String name = ""; int size = 0; for (int i = 1; i <= cols; i++) { String label = rsmd.getColumnName(i); if ("COLUMN_NAME".equalsIgnoreCase(label)) { - name = rs.getString(i); + name = resultSet.getString(i); } if ("COLUMN_SIZE".equalsIgnoreCase(label)) { - size = rs.getBigDecimal(i).intValue(); + size = resultSet.getBigDecimal(i).intValue(); } } map.put(name, size); } - rs.close(); + resultSet.close(); } catch (SQLException exc) { throw new DatabaseException("Unable to look up table " + tableName + " : " + exc.getMessage(), exc); } @@ -260,7 +234,6 @@ public class DatabaseImpl implements Database { if (tableName == null) { return tableName; } - // If user gave us a quoted string, leave it alone for look up if (tableName.length() > 2) { if (tableName.startsWith("\"") && tableName.endsWith("\"")) { @@ -268,11 +241,9 @@ public class DatabaseImpl implements Database { return tableName.substring(1, tableName.length() - 1); } } - if (flavor().isNormalizedUpperCase()) { return tableName.toUpperCase(); } - return tableName.toLowerCase(); } @@ -288,18 +259,18 @@ public class DatabaseImpl implements Database { Duration duration = Duration.between(appDate, dbDate).abs(); if (duration.getSeconds() > 3600) { throw new DatabaseException("App and db time are over an hour apart (check your timezones) app: " - + DateTimeFormatter.ISO_INSTANT.format(appDate) + " db: " - + DateTimeFormatter.ISO_INSTANT.format(dbDate)); + + appDate + " db: " + + dbDate); } if (duration.getSeconds() * 1000 > millisToError) { throw new DatabaseException("App and db time over " + millisToError + " millis apart (check your clocks) app: " - + DateTimeFormatter.ISO_INSTANT.format(appDate) + " db: " - + DateTimeFormatter.ISO_INSTANT.format(dbDate)); + + appDate + " db: " + + dbDate); } if (duration.getSeconds() * 1000 > millisToWarn) { - log.warning("App and db time are over " + millisToWarn + " millis apart (check your clocks) app: " - + DateTimeFormatter.ISO_INSTANT.format(appDate) + " db: " - + DateTimeFormatter.ISO_INSTANT.format(dbDate)); + logger.warning("App and db time are over " + millisToWarn + " millis apart (check your clocks) app: " + + appDate + " db: " + + dbDate); } return null; }); @@ -309,4 +280,83 @@ public class DatabaseImpl implements Database { public void assertTimeSynchronized() { assertTimeSynchronized(10000, 30000); } + + @Override + public void writeQueue(String table, String channel, String data, Consumer consumer) throws SQLException { + writeNextIntoQueue(connection, table, channel, data, consumer); + } + + @Override + public void readQueue(String table, String channel, Consumer consumer) throws SQLException { + try { + connection.setAutoCommit(false); + while (true) { + try (ResultSet resultSet = readNextFromQueue(connection, table, channel)) { + if (resultSet.next()) { + Long key = resultSet.getLong("key"); + try { + if (consumer != null) { + consumer.accept(resultSet.getString("data")); + } + succeedInQueue(connection, table, key); + } catch (Exception e) { + failInQueue(connection, table, key); + throw e; + } + } + } finally { + connection.commit(); + } + } + } catch (Exception e) { + connection.rollback(); + throw e; + } finally { + connection.setAutoCommit(true); + } + } + + private void writeNextIntoQueue(Connection connection, + String table, + String channel, + String data, + Consumer consumer) throws SQLException { + try (PreparedStatement preparedStatement = connection.prepareStatement(flavor().writeNextIntoQueue(table), + Statement.RETURN_GENERATED_KEYS)) { + preparedStatement.setString(1, channel); + preparedStatement.setString(2, data); + preparedStatement.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now())); + int rows = preparedStatement.executeUpdate(); + if (rows > 0) { + try (ResultSet resultSet = preparedStatement.getGeneratedKeys()) { + if (resultSet.next()) { + if (consumer != null) { + consumer.accept(resultSet.getLong(1)); + } + } + } + } + } + } + + private ResultSet readNextFromQueue(Connection connection, String table, String channel) throws SQLException { + PreparedStatement preparedStatement = connection.prepareStatement(flavor().readNextFromQueue(table)); + preparedStatement.setString(1, channel); + preparedStatement.setTimestamp(2, Timestamp.valueOf(LocalDateTime.now())); + return preparedStatement.executeQuery(); + } + + private void succeedInQueue(Connection connection, String table, Long key) throws SQLException { + try (PreparedStatement preparedStatement = connection.prepareStatement(flavor().succeedInQueue(table))) { + preparedStatement.setLong(1, key); + preparedStatement.executeUpdate(); + } + } + + private void failInQueue(Connection connection, String table, Long key) throws SQLException { + try (PreparedStatement preparedStatement = connection.prepareStatement(flavor().succeedInQueue(table))) { + preparedStatement.setLong(1, key); + preparedStatement.executeUpdate(); + } + } } diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseProvider.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseProvider.java index b191692..41e6510 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseProvider.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseProvider.java @@ -67,8 +67,7 @@ public final class DatabaseProvider implements Supplier { * database.pool.size=... How many connections in the connection pool (default 10). * database.driver.class The driver to initialize with Class.forName(). This will * be guessed from the database.url if not provided. - * database.flavor One of the enumerated values in {@link Flavor}. If this - * is not provided the flavor will be guessed based on the + * database.flavor A {@link Flavor}. If this is not provided the flavor will be guessed based on the * value for database.url, if possible. * * @@ -167,10 +166,12 @@ public final class DatabaseProvider implements Supplier { try { DriverManager.getDriver(url); } catch (SQLException e) { - try { - Class.forName(Flavor.driverForJdbcUrl(url)); - } catch (ClassNotFoundException e1) { - throw new DatabaseException("couldn't locate JDBC driver - try setting -Djdbc.drivers=some.Driver", e1); + if (flavor.driverClass() != null) { + try { + Class.forName(flavor.driverClass()); + } catch (ClassNotFoundException e1) { + throw new DatabaseException("couldn't locate JDBC driver", e1); + } } } return new BuilderImpl(null, () -> { @@ -531,7 +532,6 @@ public final class DatabaseProvider implements Supplier { if (propertyPrefix == null) { propertyPrefix = ""; } - String driver; String flavorStr; String url; @@ -561,11 +561,9 @@ public final class DatabaseProvider implements Supplier { user = properties.getProperty(propertyPrefix + "database.user"); password = properties.getProperty(propertyPrefix + "database.password"); } - if (url == null) { throw new DatabaseException("You must use -D" + propertyPrefix + "database.url=..."); } - if (user != null && password == null) { System.out.println("Enter database password for user " + user + ":"); byte[] input = new byte[256]; @@ -576,28 +574,22 @@ public final class DatabaseProvider implements Supplier { throw new DatabaseException("Error reading password from standard input", e); } } - Flavor flavor; if (flavorStr != null) { flavor = Flavor.valueOf(flavorStr); } else { flavor = Flavor.fromJdbcUrl(url); } - - if (driver == null) { - if (flavor == Flavor.oracle) { - driver = "oracle.jdbc.OracleDriver"; - } else if (flavor == Flavor.postgresql) { - driver = "org.postgresql.Driver"; - } else if (flavor == Flavor.derby) { - driver = "org.apache.derby.jdbc.EmbeddedDriver"; - } - } if (driver != null) { try { Class.forName(driver).getDeclaredConstructor().newInstance(); } catch (Exception e) { - throw new DatabaseException("Unable to load JDBC driver: " + driver, e); + driver = flavor.driverClass(); + try { + Class.forName(driver).getDeclaredConstructor().newInstance(); + } catch (Exception ee) { + throw new DatabaseException("Unable to load JDBC driver: " + driver, ee); + } } } if (user == null) { @@ -616,7 +608,12 @@ public final class DatabaseProvider implements Supplier { } PoolConfig poolConfig = new PoolConfig(); poolConfig.setPoolName(config.getString("database.pool.name", "database-pool-" + poolNameCounter.getAndAdd(1))); - poolConfig.setDriverClassName(config.getString("database.driver.class", Flavor.driverForJdbcUrl(url))); + poolConfig.setUrl(config.getString("database.url")); + // provide driver class only if in config + String driverClassName = config.getString("database.driver.class"); + if (driverClassName != null) { + poolConfig.setDriverClassName(config.getString("database.driver.class")); + } poolConfig.setUsername(config.getString("database.user")); poolConfig.setPassword(config.getString("database.password")); poolConfig.setMaximumPoolSize(config.getInteger("database.pool.size", 8)); @@ -774,7 +771,7 @@ public final class DatabaseProvider implements Supplier { try { // JDBC specifies that autoCommit is the default for all new connections. // Don't try to be clever about clearing it conditionally. - if (!options.flavor().autoCommitOnly()) { + if (!options.flavor().isAutoCommitOnly()) { connection.setAutoCommit(false); metric.checkpoint("setAutoCommit"); } @@ -868,7 +865,7 @@ public final class DatabaseProvider implements Supplier { } if (connection != null) { try { - if (!options.flavor().autoCommitOnly()) { + if (!options.flavor().isAutoCommitOnly()) { connection.commit(); } } catch (Exception e) { @@ -884,7 +881,7 @@ public final class DatabaseProvider implements Supplier { } if (connection != null) { try { - if (!options.flavor().autoCommitOnly()) { + if (!options.flavor().isAutoCommitOnly()) { connection.commit(); } } catch (Exception e) { @@ -902,7 +899,7 @@ public final class DatabaseProvider implements Supplier { if (connection != null) { try { - if (!options.flavor().autoCommitOnly()) { + if (!options.flavor().isAutoCommitOnly()) { connection.rollback(); } } catch (Exception e) { @@ -919,7 +916,7 @@ public final class DatabaseProvider implements Supplier { if (connection != null) { try { - if (!options.flavor().autoCommitOnly()) { + if (!options.flavor().isAutoCommitOnly()) { connection.rollback(); } } catch (Exception e) { diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Flavor.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Flavor.java index d98a58b..9b13fc2 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/Flavor.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Flavor.java @@ -2,993 +2,124 @@ package org.xbib.jdbc.query; import java.sql.Date; import java.sql.Timestamp; -import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.ServiceLoader; -/** - * Enumeration of supported databases with various compatibility settings. - */ -public enum Flavor { +public interface Flavor { - derby { - @Override - public boolean isNormalizedUpperCase() { - return true; - } + String getName(); - @Override - public String typeInteger() { - return "integer"; - } + boolean supports(String url); - @Override - public String typeBoolean() { - return "char(1)"; - } + String driverClass(); - @Override - public String typeLong() { - return "bigint"; - } + /** + * Returns true if DB normalizes to upper case names for ids like tables and columns + * See serviceLoader = ServiceLoader.load(Flavor.class); + for (Flavor flavor : serviceLoader) { + if (flavor.supports(url)) { + return flavor; + } + } + throw new DatabaseException("Cannot determine database flavor from url"); + } + + public static Flavor valueOf(String name) { + ServiceLoader serviceLoader = ServiceLoader.load(Flavor.class); + for (Flavor flavor : serviceLoader) { + if (flavor.getName().equals(name)) { + return flavor; + } + } + throw new DatabaseException("Cannot determine database flavor from name: " + name); + } } diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Options.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Options.java index ddf5057..c6f08ff 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/Options.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Options.java @@ -114,9 +114,6 @@ public interface Options { * *

It is strongly recommended to always run your database in GMT timezone, and * leave this set to the default.

- * - *

Behavior in releases 1.3 and prior was to use the JVM default TimeZone, and - * this was not configurable.

*/ Calendar calendarForTimestamps(); diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsOverride.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsOverride.java index 1f684fb..be2c2f9 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsOverride.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsOverride.java @@ -1,5 +1,7 @@ package org.xbib.jdbc.query; +import org.xbib.jdbc.query.flavor.Postgresql; + import java.time.LocalDateTime; import java.util.Calendar; import java.util.Date; @@ -23,7 +25,7 @@ public class OptionsOverride implements Options { * Defer to OptionsDefault for anything that is not specified, and use postgresql flavor. */ public OptionsOverride() { - parent = new OptionsDefault(Flavor.postgresql); + parent = new OptionsDefault(new Postgresql()); } /** diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Schema.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Schema.java index f687e49..4ca0c54 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/Schema.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Schema.java @@ -5,6 +5,8 @@ import org.xbib.jdbc.query.Schema.Table.Column; import org.xbib.jdbc.query.Schema.Table.ForeignKey; import org.xbib.jdbc.query.Schema.Table.Index; import org.xbib.jdbc.query.Schema.Table.Unique; +import org.xbib.jdbc.query.flavor.Oracle; +import org.xbib.jdbc.query.flavor.Postgresql; import java.sql.ResultSetMetaData; import java.sql.SQLException; @@ -274,15 +276,13 @@ public class Schema { sql.append(check.expression); sql.append(")"); } - sql.append("\n)"); if (table.customClauses.containsKey(flavor)) { sql.append(" ").append(table.customClauses.get(flavor)); } executeOrPrint(sql, db, script); sql = new Sql(); - - if (flavor == Flavor.oracle || flavor == Flavor.postgresql) { + if (flavor instanceof Oracle || flavor instanceof Postgresql) { if (table.comment != null) { sql.append("comment on table "); sql.append(table.name); @@ -292,7 +292,6 @@ public class Schema { executeOrPrint(sql, db, script); sql = new Sql(); } - for (Column c : table.columns) { if (c.comment != null) { sql.append("comment on column "); @@ -308,7 +307,6 @@ public class Schema { } } } - for (Table table : tables) { for (ForeignKey fk : table.foreignKeys) { Sql sql = new Sql(); @@ -329,7 +327,6 @@ public class Schema { executeOrPrint(sql, db, script); } } - for (Table table : tables) { for (Index index : table.indexes) { Sql sql = new Sql(); diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Sql.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Sql.java index 0af84da..1727770 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/Sql.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Sql.java @@ -22,18 +22,17 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { public Sql() { } - public Sql(/*@Untainted*/ String sql) { + public Sql(String sql) { this.sql.append(sql); } - public static Sql insert(/*@Untainted*/ String table, SqlArgs args) { + public static Sql insert(String table, SqlArgs args) { return insert(table, Collections.singletonList(args)); } - public static Sql insert(/*@Untainted*/ String table, List args) { + public static Sql insert(String table, List args) { Sql sql = null; List expectedColumns = null; - for (SqlArgs arg : args) { if (arg.positionalCount() > 0) { throw new DatabaseException("The SqlArgs must all be named to do this"); @@ -98,7 +97,7 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { return this; } - public Sql append(/*@Untainted*/ String sql) { + public Sql append(String sql) { this.sql.append(sql); return this; } @@ -133,12 +132,12 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { return this; } - public Sql replace(int start, int end, /*@Untainted*/ String str) { + public Sql replace(int start, int end, String str) { this.sql.replace(start, end, str); return this; } - public Sql insert(int offset, /*@Untainted*/ String str) { + public Sql insert(int offset, String str) { this.sql.insert(offset, str); return this; } @@ -189,7 +188,7 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { *

* Each list started must have be ended. "Lists" are only to support using listSeparator(sep) */ - public Sql listStart(/*@Untainted*/ String sql) { + public Sql listStart(String sql) { listFirstItem.push(true); return append(sql); } @@ -198,7 +197,7 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { * Appends the passed bit of sql only if a previous item has already been appended, * and notes that the list is not empty. */ - public Sql listSeparator(/*@Untainted*/ String sql) { + public Sql listSeparator(String sql) { if (listFirstItem.peek()) { listFirstItem.pop(); listFirstItem.push(false); @@ -209,12 +208,11 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { } - public Sql listEnd(/*@Untainted*/ String sql) { + public Sql listEnd(String sql) { listFirstItem.pop(); return append(sql); } - /*@Untainted*/ public String sql() { return sql.toString(); } @@ -222,7 +220,6 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { /** * Same as sql(), provided for drop-in compatibility with StringBuilder. */ - /*@Untainted*/ public String toString() { return sql(); } @@ -231,81 +228,68 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { sqlArgs.argBoolean(arg); return this; } - public Sql argBoolean( String argName, Boolean arg) { sqlArgs.argBoolean(argName, arg); return this; } - public Sql argInteger(Integer arg) { sqlArgs.argInteger(arg); return this; } - public Sql argInteger( String argName, Integer arg) { sqlArgs.argInteger(argName, arg); return this; } - public Sql argLong(Long arg) { sqlArgs.argLong(arg); return this; } - public Sql argLong(String argName, Long arg) { sqlArgs.argLong(argName, arg); return this; } - public Sql argFloat(Float arg) { sqlArgs.argFloat(arg); return this; } - public Sql argFloat( String argName, Float arg) { sqlArgs.argFloat(argName, arg); return this; } - public Sql argDouble(Double arg) { sqlArgs.argDouble(arg); return this; } - public Sql argDouble( String argName, Double arg) { sqlArgs.argDouble(argName, arg); return this; } - public Sql argBigDecimal(BigDecimal arg) { sqlArgs.argBigDecimal(arg); return this; } - public Sql argBigDecimal( String argName, BigDecimal arg) { sqlArgs.argBigDecimal(argName, arg); return this; } - public Sql argString(String arg) { sqlArgs.argString(arg); return this; } - - public Sql argString( String argName, String arg) { + public Sql argString(String argName, String arg) { sqlArgs.argString(argName, arg); return this; } @@ -314,33 +298,28 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { sqlArgs.argDate(arg); return this; } - - public Sql argDate( String argName, LocalDateTime arg) { + public Sql argDate(String argName, LocalDateTime arg) { sqlArgs.argDate(argName, arg); return this; } - public Sql argDateNowPerApp() { sqlArgs.argDateNowPerApp(); return this; } - - public Sql argDateNowPerApp( String argName) { + public Sql argDateNowPerApp(String argName) { sqlArgs.argDateNowPerApp(argName); return this; } - public Sql argDateNowPerDb() { sqlArgs.argDateNowPerDb(); return this; } - - public Sql argDateNowPerDb( String argName) { + public Sql argDateNowPerDb(String argName) { sqlArgs.argDateNowPerDb(argName); return this; } @@ -352,35 +331,30 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { } - public Sql argBlobBytes( String argName, byte[] arg) { + public Sql argBlobBytes(String argName, byte[] arg) { sqlArgs.argBlobBytes(argName, arg); return this; } - public Sql argBlobInputStream(InputStream arg) { sqlArgs.argBlobInputStream(arg); return this; } - public Sql argBlobInputStream( String argName, InputStream arg) { sqlArgs.argBlobInputStream(argName, arg); return this; } - public Sql argClobString(String arg) { sqlArgs.argClobString(arg); return this; } - public Sql argClobString( String argName, String arg) { sqlArgs.argClobString(argName, arg); return this; } - public Sql argClobReader(Reader arg) { sqlArgs.argClobReader(arg); @@ -419,7 +393,6 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { if (batched != null) { throw new DatabaseException("Batch not supported for update"); } - sqlArgs.apply(update); } @@ -427,16 +400,4 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { Integer, Long, Float, Double, BigDecimal, String, ClobString, ClobStream, BlobBytes, BlobStream, Date, DateNowPerApp, DateNowPerDb, Boolean } - - private static class Invocation { - ColumnType columnType; - String argName; - Object arg; - - Invocation(ColumnType columnType, String argName, Object arg) { - this.columnType = columnType; - this.argName = argName; - this.arg = arg; - } - } } diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlArgs.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlArgs.java index f505ef6..6b8f512 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlArgs.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlArgs.java @@ -36,7 +36,6 @@ public class SqlArgs implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Appl * @return a SqlArgs with one invocation for each column in the Row, with name * and type inferred from the metadata */ - public static SqlArgs readRow(Row r) { return new Builder(r).read(r); } @@ -118,44 +117,37 @@ public class SqlArgs implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Appl invocations.add(new Invocation(ColumnType.Double, null, arg)); return this; } - public SqlArgs argDouble( String argName, Double arg) { invocations.add(new Invocation(ColumnType.Double, argName, arg)); return this; } - public SqlArgs argBigDecimal(BigDecimal arg) { invocations.add(new Invocation(ColumnType.BigDecimal, null, arg)); return this; } - public SqlArgs argBigDecimal( String argName, BigDecimal arg) { invocations.add(new Invocation(ColumnType.BigDecimal, argName, arg)); return this; } - public SqlArgs argString(String arg) { invocations.add(new Invocation(ColumnType.String, null, arg)); return this; } - public SqlArgs argString( String argName, String arg) { invocations.add(new Invocation(ColumnType.String, argName, arg)); return this; } - public SqlArgs argDate(LocalDateTime arg) { // date argument with a time on it invocations.add(new Invocation(ColumnType.Date, null, arg)); return this; } - public SqlArgs argDate( String argName, LocalDateTime arg) { // date argument with a time on it @@ -693,11 +685,9 @@ public class SqlArgs implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Appl throw new DatabaseException("Unable to retrieve metadata from ResultSet", e); } } - public SqlArgs read(Row r) { SqlArgs args = new SqlArgs(); - for (int i = 0; i < names.length; i++) { switch (types[i]) { case Types.SMALLINT: @@ -705,22 +695,24 @@ public class SqlArgs implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Appl args.argInteger(names[i], r.getIntegerOrNull()); break; case Types.BIGINT: - args.argLong(names[i], r.getLongOrNull()); + if (precision[i] <= 64 && scale[i] == 0) { + args.argLong(names[i], r.getLongOrNull()); + } else { + args.argBigDecimal(names[i], r.getBigDecimalOrNull()); + } break; case Types.REAL: - case 100: // Oracle proprietary it seems + case 100: // Oracle binary float args.argFloat(names[i], r.getFloatOrNull()); break; case Types.DOUBLE: - case 101: // Oracle proprietary it seems + case 101: // Oracle binary double args.argDouble(names[i], r.getDoubleOrNull()); break; case Types.NUMERIC: - if (precision[i] == 10 && scale[i] == 0) { - // Oracle reports integer as numeric + if (precision[i] <= 10 && scale[i] == 0) { args.argInteger(names[i], r.getIntegerOrNull()); - } else if (precision[i] == 19 && scale[i] == 0) { - // Oracle reports long as numeric + } else if (precision[i] <= 19 && scale[i] == 0) { args.argLong(names[i], r.getLongOrNull()); } else { args.argBigDecimal(names[i], r.getBigDecimalOrNull()); diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsertImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsertImpl.java index f9c14c7..143544a 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsertImpl.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsertImpl.java @@ -288,7 +288,6 @@ public class SqlInsertImpl implements SqlInsert { if (!hasPk()) { throw new DatabaseException("Call argPkSeq() before insertReturningPkSeq()"); } - if (options.flavor().supportsInsertReturning()) { return updateInternal(1, primaryKeyColumnName); } else { diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/When.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/When.java index d66c152..a6b7d2c 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/When.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/When.java @@ -1,5 +1,10 @@ package org.xbib.jdbc.query; +import org.xbib.jdbc.query.flavor.Derby; +import org.xbib.jdbc.query.flavor.Oracle; +import org.xbib.jdbc.query.flavor.Postgresql; +import org.xbib.jdbc.query.flavor.SqlServer; + import java.util.Objects; /** @@ -16,28 +21,28 @@ public class When { } public When oracle(String sql) { - if (actualFlavor == Flavor.oracle) { + if (actualFlavor instanceof Oracle) { chosen = sql; } return this; } public When derby(String sql) { - if (actualFlavor == Flavor.derby) { + if (actualFlavor instanceof Derby) { chosen = sql; } return this; } public When postgres(String sql) { - if (actualFlavor == Flavor.postgresql) { + if (actualFlavor instanceof Postgresql) { chosen = sql; } return this; } public When sqlserver(String sql) { - if (actualFlavor == Flavor.sqlserver) { + if (actualFlavor instanceof SqlServer) { chosen = sql; } return this; diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/BigQuery.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/BigQuery.java new file mode 100644 index 0000000..8004eeb --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/BigQuery.java @@ -0,0 +1,196 @@ +package org.xbib.jdbc.query.flavor; + +import org.xbib.jdbc.query.Flavor; + +import java.sql.Date; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +public class BigQuery implements Flavor { + + @Override + public String getName() { + return "bigQuery"; + } + + @Override + public boolean supports(String url) { + return url.startsWith("jdbc:bigquery:"); + } + + @Override + public String driverClass() { + return "com.simba.googlebigquery.jdbc42.Driver"; + } + + @Override + public boolean isNormalizedUpperCase() { + return false; + } + + @Override + public String typeInteger() { + return "int64"; + } + + @Override + public String typeBoolean() { + // BigQuery has a native boolean type, but we're not trying to use it + return "string"; + } + + @Override + public String typeLong() { + return "int64"; + } + + @Override + public String typeFloat() { + return "float64"; + } + + @Override + public String typeDouble() { + return "float64"; + } + + @Override + public String typeBigDecimal(int size, int precision) { + return "numeric"; + } + + @Override + public String typeStringVar(int length) { + return "string"; + } + + @Override + public String typeStringFixed(int length) { + return "string"; + } + + @Override + public String typeClob() { + return "string"; + } + + @Override + public String typeBlob() { + return "bytes"; + } + + @Override + public String typeDate() { + return "datetime"; + } + + @Override + public String typeLocalDate() { + return "date"; + } + + @Override + public boolean useStringForClob() { + return true; + } + + @Override + public boolean useBytesForBlob() { + return true; + } + + @Override + public String sequenceNextVal(String sequenceName) { + throw new UnsupportedOperationException(); + } + + @Override + public String sequenceSelectNextVal(String sequenceName) { + throw new UnsupportedOperationException(); + } + + @Override + public String sequenceDrop(String dbtestSeq) { + throw new UnsupportedOperationException(); + } + + @Override + public String tableDrop(String table) { + return "drop table " + table; + } + + @Override + public boolean supportsInsertReturning() { + return false; + } + + @Override + public String dbTimeMillis() { + return "current_timestamp()"; + } + + @Override + public String sequenceCacheClause(int nbrValuesToCache) { + throw new UnsupportedOperationException(); + } + + @Override + public String sequenceOrderClause(boolean order) { + throw new UnsupportedOperationException(); + } + + @Override + public String sequenceCycleClause(boolean cycle) { + throw new UnsupportedOperationException(); + } + + @Override + public String fromAny() { + return ""; + } + + @Override + public String dateAsSqlFunction(Timestamp date, Calendar calendar) { + // Construct a datetime literal + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000"); + dateFormat.setCalendar(calendar); + return String.format("datetime '%s'", dateFormat.format(date)); + } + + @Override + public String localDateAsSqlFunction(Date date) { + // Construct a datetime literal + return String.format("datetime '%s'", date.toString()); + } + + @Override + public String sequenceOptions() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isAutoCommitOnly() { + return true; + } + + @Override + public String writeNextIntoQueue(String table) { + throw new UnsupportedOperationException(); + } + + @Override + public String readNextFromQueue(String table) { + throw new UnsupportedOperationException(); + } + + @Override + public String succeedInQueue(String table) { + throw new UnsupportedOperationException(); + } + + @Override + public String failInQueue(String table) { + throw new UnsupportedOperationException(); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/Derby.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/Derby.java new file mode 100644 index 0000000..4853e01 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/Derby.java @@ -0,0 +1,194 @@ +package org.xbib.jdbc.query.flavor; + +import org.xbib.jdbc.query.Flavor; + +import java.sql.Date; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +public class Derby implements Flavor { + + @Override + public String getName() { + return "derby"; + } + + @Override + public boolean supports(String url) { + return url.startsWith("jdbc:derby:"); + } + + @Override + public String driverClass() { + return "org.apache.derby.jdbc.EmbeddedDriver"; + } + + @Override + public boolean isNormalizedUpperCase() { + return true; + } + + @Override + public String typeInteger() { + return "integer"; + } + + @Override + public String typeBoolean() { + return "char(1)"; + } + + @Override + public String typeLong() { + return "bigint"; + } + + @Override + public String typeFloat() { + return "real"; + } + + @Override + public String typeDouble() { + return "double"; + } + + @Override + public String typeBigDecimal(int size, int precision) { + return "numeric(" + size + "," + precision + ")"; + } + + @Override + public String typeStringVar(int length) { + return "varchar(" + length + ")"; + } + + @Override + public String typeStringFixed(int length) { + return "char(" + length + ")"; + } + + @Override + public String typeClob() { + return "clob"; + } + + @Override + public String typeBlob() { + return "blob"; + } + + @Override + public String typeDate() { + return "timestamp"; + } + + @Override + public String typeLocalDate() { + return "date"; + } + + @Override + public boolean useStringForClob() { + return false; + } + + @Override + public boolean useBytesForBlob() { + return false; + } + + @Override + public String sequenceNextVal(String sequenceName) { + return "next value for " + sequenceName; + } + + @Override + public String sequenceSelectNextVal(String sequenceName) { + return "values next value for " + sequenceName; + } + + @Override + public String sequenceDrop(String sequenceName) { + return "drop sequence " + sequenceName + " restrict"; + } + + @Override + public String tableDrop(String table) { + return "drop table " + table; + } + + @Override + public boolean supportsInsertReturning() { + return false; + } + + @Override + public String sequenceCacheClause(int nbrValuesToCache) { + return ""; + } + + @Override + public String sequenceOrderClause(boolean order) { + return ""; + } + + @Override + public String sequenceCycleClause(boolean cycle) { + return cycle ? " cycle" : " no cycle"; + } + + @Override + public String dbTimeMillis() { + return "current_timestamp"; + } + + @Override + public String fromAny() { + return " from sysibm.sysdummy1"; + } + + @Override + public String dateAsSqlFunction(Timestamp date, Calendar calendar) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + dateFormat.setCalendar(calendar); + return "timestamp('" + dateFormat.format(date) + "')"; + } + + @Override + public String localDateAsSqlFunction(Date date) { + return "'" + date.toString() + "'"; + } + + @Override + public String sequenceOptions() { + return " as bigint"; + } + + @Override + public boolean isAutoCommitOnly() { + return false; + } + + @Override + public String writeNextIntoQueue(String table) { + throw new UnsupportedOperationException(); + } + + @Override + public String readNextFromQueue(String table) { + throw new UnsupportedOperationException(); + } + + @Override + public String succeedInQueue(String table) { + throw new UnsupportedOperationException(); + } + + @Override + public String failInQueue(String table) { + throw new UnsupportedOperationException(); + } + +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/Hsql.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/Hsql.java new file mode 100644 index 0000000..ef4c383 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/Hsql.java @@ -0,0 +1,192 @@ +package org.xbib.jdbc.query.flavor; + +import org.xbib.jdbc.query.Flavor; + +import java.sql.Date; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +public class Hsql implements Flavor { + + @Override + public String getName() { + return "hsqldb"; + } + + @Override + public boolean supports(String url) { + return url.startsWith("jdbc:hsqldb:"); + } + + @Override + public String driverClass() { + return "org.hsqldb.jdbc.JDBCDriver"; + } + + @Override + public boolean isNormalizedUpperCase() { + return true; + } + + @Override + public String typeInteger() { + return "integer"; + } + + @Override + public String typeBoolean() { + return "char(1)"; + } + + @Override + public String typeLong() { + return "bigint"; + } + + @Override + public String typeFloat() { + return "double"; + } + + @Override + public String typeDouble() { + return "double"; + } + + @Override + public String typeBigDecimal(int size, int precision) { + return "numeric(" + size + "," + precision + ")"; + } + + @Override + public String typeStringVar(int length) { + return "varchar(" + length + ")"; + } + + @Override + public String typeStringFixed(int length) { + return "char(" + length + ")"; + } + + @Override + public String typeClob() { + return "clob(2G)"; + } + + @Override + public String typeBlob() { + return "blob(2G)"; + } + + @Override + public String typeDate() { + return "timestamp(3)"; + } + + @Override + public String typeLocalDate() { + return "date"; + } + + @Override + public boolean useStringForClob() { + return true; + } + + @Override + public boolean useBytesForBlob() { + return true; + } + + @Override + public String sequenceNextVal(String sequenceName) { + return "next value for " + sequenceName + ""; + } + + @Override + public String sequenceSelectNextVal(String sequenceName) { + return "select " + sequenceNextVal(sequenceName) + fromAny(); + } + + @Override + public String sequenceDrop(String dbtestSeq) { + return "drop sequence if exists " + dbtestSeq; + } + + @Override + public String tableDrop(String table) { + return "drop table if exists " + table; + } + + @Override + public String sequenceOrderClause(boolean order) { + return ""; + } + + @Override + public String sequenceCycleClause(boolean cycle) { + return cycle ? " cycle" : " no cycle"; + } + + @Override + public String fromAny() { + return " from (values(0))"; + } + + @Override + public boolean supportsInsertReturning() { + return false; + } + + @Override + public String dbTimeMillis() { + return "localtimestamp"; + } + + @Override + public String sequenceCacheClause(int nbrValuesToCache) { + return ""; + } + + @Override + public String dateAsSqlFunction(Timestamp date, Calendar calendar) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000XXX"); + dateFormat.setCalendar(calendar); + return "cast(timestamp '" + dateFormat.format(date) + "' as timestamp without time zone)"; + } + + @Override + public String localDateAsSqlFunction(Date date) { + return "'" + date.toString() + "'"; + } + + @Override + public String sequenceOptions() { + return " as bigint"; + } + + @Override + public boolean isAutoCommitOnly() { + return false; + } + + @Override + public String writeNextIntoQueue(String table) { + throw new UnsupportedOperationException(); + } + + @Override + public String readNextFromQueue(String table) { + throw new UnsupportedOperationException(); + } + + @Override + public String succeedInQueue(String table) { + throw new UnsupportedOperationException(); + } + @Override + public String failInQueue(String table) { + throw new UnsupportedOperationException(); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/Oracle.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/Oracle.java new file mode 100644 index 0000000..385e352 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/Oracle.java @@ -0,0 +1,200 @@ +package org.xbib.jdbc.query.flavor; + +import org.xbib.jdbc.query.Flavor; + +import java.sql.Date; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +public class Oracle implements Flavor { + + @Override + public String getName() { + return "oracle"; + } + + @Override + public boolean supports(String url) { + return url.startsWith("jdbc:oracle:"); + } + + @Override + public String driverClass() { + return "oracle.jdbc.OracleDriver"; + } + + @Override + public boolean isNormalizedUpperCase() { + return true; + } + + @Override + public String typeFloat() { + return "binary_float"; + } + + @Override + public String typeDouble() { + return "binary_double"; + } + + @Override + public String typeBigDecimal(int size, int precision) { + return "numeric(" + size + "," + precision + ")"; + } + + @Override + public String typeInteger() { + return "numeric(10)"; + } + + @Override + public String typeBoolean() { + return "char(1 char)"; + } + + @Override + public String typeLong() { + return "numeric(19)"; + } + + @Override + public String typeDate() { + return "timestamp(3)"; + } + + @Override + public String typeLocalDate() { + return "date"; + } + + @Override + public boolean useStringForClob() { + return false; + } + + @Override + public boolean useBytesForBlob() { + return false; + } + + @Override + public String sequenceNextVal(String sequenceName) { + return sequenceName + ".nextval"; + } + + @Override + public String sequenceSelectNextVal(String sequenceName) { + return "select " + sequenceName + ".nextval from dual"; + } + + @Override + public String sequenceDrop(String sequenceName) { + return "drop sequence " + sequenceName; + } + + @Override + public String tableDrop(String table) { + return "drop table " + table; + } + + @Override + public String typeStringVar(int length) { + return "varchar2(" + length + " char)"; + } + + @Override + public String typeStringFixed(int length) { + return "char(" + length + " char)"; + } + + @Override + public String typeClob() { + return "clob"; + } + + @Override + public String typeBlob() { + return "blob"; + } + + @Override + public String sequenceOrderClause(boolean order) { + return order ? " order" : " noorder"; + } + + @Override + public String sequenceCycleClause(boolean cycle) { + return cycle ? " cycle" : " nocycle"; + } + + @Override + public boolean supportsInsertReturning() { + return true; + } + + @Override + public String dbTimeMillis() { + return "systimestamp(3)"; + } + + @Override + public String sequenceCacheClause(int nbrValuesToCache) { + if (nbrValuesToCache < 2) { + return " nocache"; + } + return " cache " + nbrValuesToCache; + } + + @Override + public String fromAny() { + return " from dual"; + } + + @Override + public String dateAsSqlFunction(Timestamp date, Calendar calendar) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000"); + dateFormat.setCalendar(calendar); + return "timestamp '" + dateFormat.format(date) + "'"; + } + + @Override + public String localDateAsSqlFunction(Date date) { + return "to_date('" + date.toString() + "', 'yyyy-mm-dd')"; + } + + @Override + public String sequenceOptions() { + return ""; + } + + @Override + public boolean isAutoCommitOnly() { + return false; + } + + @Override + public String writeNextIntoQueue(String table) { + return "insert into " + table + + " (channel, data, state, delay, modified) " + + "values (?, ?, null, 0, ?)"; + } + + @Override + public String readNextFromQueue(String table) { + return "select id, data from " + table + + " where channel = ? and state is NULL and modified <= (CAST (? as TIMESTAMP) - (INTERVAL '1 minute' * delay))" + + " order by id asc limit 1 for update skip lock"; + } + + @Override + public String succeedInQueue(String table) { + return "update" + table + " set state = 'OK', modified = " + dbTimeMillis() + " where id = ?"; + } + + @Override + public String failInQueue(String table) { + return "update" + table + " set state = 'FAIL', modified = " + dbTimeMillis() + " where id = ?"; + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/Postgresql.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/Postgresql.java new file mode 100644 index 0000000..0e5b0a7 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/Postgresql.java @@ -0,0 +1,197 @@ +package org.xbib.jdbc.query.flavor; + +import org.xbib.jdbc.query.Flavor; + +import java.sql.Date; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +public class Postgresql implements Flavor { + + @Override + public String getName() { + return "postgresql"; + } + + @Override + public boolean supports(String url) { + return url.startsWith("jdbc:postgresql:"); + } + + @Override + public String driverClass() { + return "org.postgresql.Driver"; + } + + @Override + public boolean isNormalizedUpperCase() { + return false; + } + + @Override + public String typeInteger() { + return "integer"; + } + + @Override + public String typeBoolean() { + return "char(1)"; + } + + @Override + public String typeLong() { + return "bigint"; + } + + @Override + public String typeFloat() { + return "real"; + } + + @Override + public String typeDouble() { + return "double precision"; + } + + @Override + public String typeBigDecimal(int size, int precision) { + return "numeric(" + size + "," + precision + ")"; + } + + @Override + public String typeStringVar(int length) { + return "varchar(" + length + ")"; + } + + @Override + public String typeStringFixed(int length) { + return "char(" + length + ")"; + } + + @Override + public String typeClob() { + return "text"; + } + + @Override + public String typeBlob() { + return "bytea"; + } + + @Override + public String typeDate() { + return "timestamp(3)"; + } + + @Override + public String typeLocalDate() { + return "date"; + } + + @Override + public boolean useStringForClob() { + return true; + } + + @Override + public boolean useBytesForBlob() { + return true; + } + + @Override + public String sequenceNextVal(String sequenceName) { + return "nextval('" + sequenceName + "')"; + } + + @Override + public String sequenceSelectNextVal(String sequenceName) { + return "select nextval('" + sequenceName + "')"; + } + + @Override + public String sequenceDrop(String dbtestSeq) { + return "drop sequence " + dbtestSeq; + } + + @Override + public String tableDrop(String table) { + return "drop table if exists " + table; + } + + @Override + public String sequenceOrderClause(boolean order) { + return ""; + } + + @Override + public String sequenceCycleClause(boolean cycle) { + return cycle ? " cycle" : " no cycle"; + } + + @Override + public String fromAny() { + return ""; + } + + @Override + public boolean supportsInsertReturning() { + return true; + } + + @Override + public String dbTimeMillis() { + return "date_trunc('milliseconds',localtimestamp)"; + } + + @Override + public String sequenceCacheClause(int nbrValuesToCache) { + return " cache " + nbrValuesToCache; + } + + @Override + public String dateAsSqlFunction(Timestamp date, Calendar calendar) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000"); + dateFormat.setCalendar(calendar); + return "'" + dateFormat.format(date) + " GMT'::timestamp"; + } + + @Override + public String localDateAsSqlFunction(Date date) { + return "'" + date.toString() + "'"; + } + + @Override + public String sequenceOptions() { + return ""; + } + + @Override + public boolean isAutoCommitOnly() { + return false; + } + + @Override + public String writeNextIntoQueue(String table) { + return "insert into " + table + + " (channel, data, state, delay, modified) " + + "values (?, ?, null, 0, ?)"; + } + + @Override + public String readNextFromQueue(String table) { + return "select id, data from " + table + + " where channel = ? and state is NULL and pushed_at <= (CAST (? as TIMESTAMP) - (INTERVAL '1 minute' * delay))" + + " order by id asc limit 1 for update skip lock"; + } + + @Override + public String succeedInQueue(String table) { + return "update" + table + " set state = 'OK', modified = " + dbTimeMillis() + " where id = ?"; + } + + @Override + public String failInQueue(String table) { + return "update" + table + " set state = 'FAIL', modified = " + dbTimeMillis() + " where id = ?"; + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/SqlServer.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/SqlServer.java new file mode 100644 index 0000000..2c1ce32 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/flavor/SqlServer.java @@ -0,0 +1,197 @@ +package org.xbib.jdbc.query.flavor; + +import org.xbib.jdbc.query.Flavor; + +import java.sql.Date; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +public class SqlServer implements Flavor { + + @Override + public String getName() { + return "sqlServer"; + } + + @Override + public boolean supports(String url) { + return url.startsWith("jdbc:sqlserver:"); + } + + @Override + public String driverClass() { + return "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + } + + @Override + public boolean isNormalizedUpperCase() { + return false; + } + + @Override + public String typeFloat() { + return "float(24)"; + } + + @Override + public String typeDouble() { + return "float(53)"; + } + + @Override + public String typeBigDecimal(int size, int precision) { + return "numeric(" + size + "," + precision + ")"; + } + + @Override + public String typeInteger() { + return "numeric(10)"; + } + + @Override + public String typeBoolean() { + return "char(1)"; + } + + @Override + public String typeLong() { + return "numeric(19)"; + } + + @Override + public String typeDate() { + return "datetime2(3)"; + } + + @Override + public String typeLocalDate() { + return "date"; + } + + @Override + public boolean useStringForClob() { + return false; + } + + @Override + public boolean useBytesForBlob() { + return false; + } + + @Override + public String sequenceNextVal(String sequenceName) { + return "next value for " + sequenceName; + } + + @Override + public String sequenceSelectNextVal(String sequenceName) { + return "select next value for " + sequenceName; + } + + @Override + public String sequenceDrop(String dbtestSeq) { + return "drop sequence " + dbtestSeq; + } + + @Override + public String tableDrop(String table) { + return "drop table " + table; + } + + @Override + public String typeStringVar(int length) { + return "varchar(" + length + ")"; + } + + @Override + public String typeStringFixed(int length) { + return "char(" + length + ")"; + } + + @Override + public String typeClob() { + return "varchar(max)"; + } + + @Override + public String typeBlob() { + return "varbinary(max)"; + } + + @Override + public String sequenceOrderClause(boolean order) { + // Not supported + return ""; + } + + @Override + public String sequenceCycleClause(boolean cycle) { + return cycle ? " cycle" : " no cycle"; + } + + @Override + public boolean supportsInsertReturning() { + return true; + } + + @Override + public String dbTimeMillis() { + return "current_timestamp"; + } + + @Override + public String sequenceCacheClause(int nbrValuesToCache) { + if (nbrValuesToCache < 2) { + return " no cache"; + } + return " cache " + nbrValuesToCache; + } + + @Override + public String fromAny() { + return ""; + } + + @Override + public String dateAsSqlFunction(Timestamp date, Calendar calendar) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000"); + dateFormat.setCalendar(calendar); + return "cast('" + dateFormat.format(date) + "' as datetime2(3))"; + } + + @Override + public String localDateAsSqlFunction(Date date) { + return "'" + date.toString() + "'"; + } + + @Override + public String sequenceOptions() { + return ""; + } + + @Override + public boolean isAutoCommitOnly() { + return false; + } + + @Override + public String writeNextIntoQueue(String table) { + throw new UnsupportedOperationException(); + } + + @Override + public String readNextFromQueue(String table) { + throw new UnsupportedOperationException(); + } + + @Override + public String succeedInQueue(String table) { + throw new UnsupportedOperationException(); + } + + @Override + public String failInQueue(String table) { + throw new UnsupportedOperationException(); + } +} diff --git a/jdbc-query/src/main/resources/META-INF/services/org.xbib.jdbc.query.Flavor b/jdbc-query/src/main/resources/META-INF/services/org.xbib.jdbc.query.Flavor new file mode 100644 index 0000000..84e6cb3 --- /dev/null +++ b/jdbc-query/src/main/resources/META-INF/services/org.xbib.jdbc.query.Flavor @@ -0,0 +1,6 @@ +org.xbib.jdbc.query.flavor.BigQuery +org.xbib.jdbc.query.flavor.Derby +org.xbib.jdbc.query.flavor.Hsql +org.xbib.jdbc.query.flavor.Oracle +org.xbib.jdbc.query.flavor.Postgresql +org.xbib.jdbc.query.flavor.SqlServer \ No newline at end of file diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/CommonTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/CommonTest.java index 7461f86..ba51d4a 100644 --- a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/CommonTest.java +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/CommonTest.java @@ -26,14 +26,12 @@ import java.io.StringReader; import java.math.BigDecimal; import java.sql.ResultSetMetaData; import java.sql.Timestamp; -import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.Month; import java.time.ZoneId; import java.time.ZoneOffset; -import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -54,7 +52,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; /** - * Exercise Database functionality with a real databases. + * Exercise Database functionality with a real database. */ public abstract class CommonTest { @@ -62,10 +60,6 @@ public abstract class CommonTest { final static String TEST_TABLE_NAME = "dbtest"; - /** - * Enable retrying failed tests if they have the @Retry annotation. - */ - protected DatabaseProvider dbp; protected Database db; diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/ConfigTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/ConfigTest.java index f0f47d8..ead771d 100644 --- a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/ConfigTest.java +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/ConfigTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test; import org.xbib.jdbc.query.Config; import org.xbib.jdbc.query.ConfigFrom; -import java.io.File; import java.io.FileWriter; import java.math.BigDecimal; import java.util.Properties; @@ -54,7 +53,7 @@ public class ConfigTest { properties.store(new FileWriter(filename2), null); // Throw a null in here just to make sure it doesn't blow up - Config config = ConfigFrom.firstOf().propertyFile(filename1, null, filename2).get(); + Config config = ConfigFrom.firstOf().properties(filename1, null, filename2).get(); assertEquals(Integer.valueOf(1), config.getInteger("foo")); assertEquals(Integer.valueOf(-2), config.getInteger("foo2")); @@ -62,12 +61,12 @@ public class ConfigTest { assertEquals(5, config.getInteger("unknown", 5)); // Now flip the order and verify precedence works - config = ConfigFrom.firstOf().propertyFile(filename2, null, filename1).get(); + config = ConfigFrom.firstOf().properties(filename2, null, filename1).get(); assertEquals(Integer.valueOf(2), config.getInteger("foo")); assertEquals(Integer.valueOf(-2), config.getInteger("foo2")); // Same as above tests, but using File version rather than filename String - config = ConfigFrom.firstOf().propertyFile(new File(filename1), new File("does not exist"), new File(filename2)).get(); + config = ConfigFrom.firstOf().properties(filename1, "does not exist", filename2).get(); assertEquals(Integer.valueOf(1), config.getInteger("foo")); assertEquals(Integer.valueOf(-2), config.getInteger("foo2")); @@ -75,7 +74,7 @@ public class ConfigTest { assertEquals(5, config.getInteger("unknown", 5)); // Now flip the order and verify precedence works - config = ConfigFrom.firstOf().propertyFile(new File(filename2), null, new File(filename1)).get(); + config = ConfigFrom.firstOf().properties(filename2, null, filename1).get(); assertEquals(Integer.valueOf(2), config.getInteger("foo")); assertEquals(Integer.valueOf(-2), config.getInteger("foo2")); } diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DerbyTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DerbyTest.java index 861f760..12a20e2 100644 --- a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DerbyTest.java +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DerbyTest.java @@ -12,7 +12,6 @@ import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.assertEquals; - /** * Exercise Database functionality with a real database (Derby). */ @@ -79,46 +78,6 @@ public class DerbyTest extends CommonTest { super.intervals(); } - @Test - public void argBigDecimal31Precision0() { - db.dropTableQuietly("dbtest"); - new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 0).schema().execute(db); - BigDecimal value = new BigDecimal("9999999999999999999999999999999"); // 31 digits - db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); - assertEquals(value, - db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); - } - - @Test - public void argBigDecimal31Precision1() { - db.dropTableQuietly("dbtest"); - new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 1).schema().execute(db); - BigDecimal value = new BigDecimal("999999999999999999999999999999.9"); // 31 digits - db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); - assertEquals(value, - db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); - } - - @Test - public void argBigDecimal31Precision30() { - db.dropTableQuietly("dbtest"); - new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 30).schema().execute(db); - BigDecimal value = new BigDecimal("9.999999999999999999999999999999"); // 31 digits - db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); - assertEquals(value, - db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); - } - - @Test - public void argBigDecimal31Precision31() { - db.dropTableQuietly("dbtest"); - new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 31).schema().execute(db); - BigDecimal value = new BigDecimal("0.9999999999999999999999999999999"); // 31 digits - db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); - logger.log(Level.INFO, db.toSelect("select i from dbtest").queryBigDecimalOrNull().toString()); - assertEquals(value, - db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); - } @Disabled("Derby limits out at precision 31") @Test @@ -143,4 +102,41 @@ public class DerbyTest extends CommonTest { public void argBigDecimal38Precision38() { super.argBigDecimal38Precision38(); } + + @Test + public void argBigDecimal31Precision0() { + db.dropTableQuietly("dbtest"); + new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 0).schema().execute(db); + BigDecimal value = new BigDecimal("9999999999999999999999999999999"); // 31 digits + db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); + assertEquals(value, db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); + } + + @Test + public void argBigDecimal31Precision1() { + db.dropTableQuietly("dbtest"); + new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 1).schema().execute(db); + BigDecimal value = new BigDecimal("999999999999999999999999999999.9"); // 31 digits + db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); + assertEquals(value, db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); + } + + @Test + public void argBigDecimal31Precision30() { + db.dropTableQuietly("dbtest"); + new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 30).schema().execute(db); + BigDecimal value = new BigDecimal("9.999999999999999999999999999999"); // 31 digits + db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); + assertEquals(value, db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); + } + + @Test + public void argBigDecimal31Precision31() { + db.dropTableQuietly("dbtest"); + new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 31).schema().execute(db); + BigDecimal value = new BigDecimal("0.9999999999999999999999999999999"); // 31 digits + db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); + logger.log(Level.INFO, db.toSelect("select i from dbtest").queryBigDecimalOrNull().toString()); + assertEquals(value, db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); + } } diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/HsqldbTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/HsqldbTest.java index 4c2fdc9..e52a96a 100644 --- a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/HsqldbTest.java +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/HsqldbTest.java @@ -1,6 +1,5 @@ package org.xbib.jdbc.query.test; -import java.util.logging.Level; import java.util.logging.Logger; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -22,24 +21,21 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** * Exercise database functionality with a real HyperSQL database. */ -@Disabled public class HsqldbTest extends CommonTest { private static final Logger logger = Logger.getLogger(HsqldbTest.class.getName()); @Override protected DatabaseProvider createDatabaseProvider(OptionsOverride options) throws Exception { - String propertiesFile = System.getProperty("local.properties", "local.properties"); + String propertiesFile = "hsqldb.properties"; Config config = ConfigFrom.firstOf() - .systemProperties() - .propertyFile(propertiesFile) - .excludePrefix("database.") - .removePrefix("hsqldb.").get(); - logger.log(Level.INFO, "config = " + config); + .properties(getClass().getResourceAsStream(propertiesFile)) + .get(); return DatabaseProvider.builder(config) .withSqlParameterLogging() .withSqlInExceptionMessages() - .withOptions(options).build(); + .withOptions(options) + .build(); } @Test @@ -61,9 +57,16 @@ public class HsqldbTest extends CommonTest { }); } + @Disabled("HSQLDB uses always static GMT timezone") + @Test + public void clockSync() { + db.assertTimeSynchronized(); + } + @Disabled("LocalDate implementations should be TimeZone agnostic, but HSQLDB implementation has a bug.") @Test public void argLocalDateTimeZones() { + // <2000-01-01> but was: <1999-12-31> // See bug: https://bugs.documentfoundation.org/show_bug.cgi?id=63566 super.argLocalDateTimeZones(); } @@ -91,19 +94,35 @@ public class HsqldbTest extends CommonTest { db.toInsert("insert into dbtest (nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar," + " str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date) values (?,?,?,?,?,?,?,?,?,?,?,?)") - .argInteger(Integer.MAX_VALUE).argLong(Long.MAX_VALUE).argDouble((double) Float.MAX_VALUE) - .argDouble(Double.MAX_VALUE).argBigDecimal(new BigDecimal("123.456")) - .argString("hello").argString("Z").argClobString("hello again") - .argBlobBytes(new byte[]{'1', '2'}).argBoolean(true) - .argDateNowPerApp().argLocalDate(localDateNow).insert(1); + .argInteger(Integer.MAX_VALUE) + .argLong(Long.MAX_VALUE) + .argDouble((double) Float.MAX_VALUE) + .argDouble(Double.MAX_VALUE) + .argBigDecimal(new BigDecimal("123.456")) + .argString("hello") + .argString("Z") + .argClobString("hello again") + .argBlobBytes(new byte[]{'1', '2'}) + .argBoolean(true) + .argDateNowPerApp() + .argLocalDate(localDateNow) + .insert(1); db.toInsert("insert into dbtest (nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar," + " str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date) values (?,?,?,?,?,?,?,?,?,?,?,?)") - .argInteger(Integer.MIN_VALUE).argLong(Long.MIN_VALUE).argDouble(0.000001d) - .argDouble(Double.MIN_VALUE).argBigDecimal(new BigDecimal("-123.456")) - .argString("goodbye").argString("A").argClobString("bye again") - .argBlobBytes(new byte[]{'3', '4'}).argBoolean(false) - .argDateNowPerApp().argLocalDate(localDateNow).insert(1); + .argInteger(Integer.MIN_VALUE) + .argLong(Long.MIN_VALUE) + .argDouble(0.000001d) + .argDouble(Double.MIN_VALUE) + .argBigDecimal(new BigDecimal("-123.456")) + .argString("goodbye") + .argString("A") + .argClobString("bye again") + .argBlobBytes(new byte[]{'3', '4'}) + .argBoolean(false) + .argDateNowPerApp() + .argLocalDate(localDateNow) + .insert(1); String expectedSchema = new Schema().addTable("dbtest2") .addColumn("nbr_integer").asInteger().table() diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlServerTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlServerTest.java index 9228dc0..0b93794 100644 --- a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlServerTest.java +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlServerTest.java @@ -20,9 +20,10 @@ public class SqlServerTest extends CommonTest { String propertiesFile = System.getProperty("local.properties", "local.properties"); Config config = ConfigFrom.firstOf() .systemProperties() - .propertyFile(propertiesFile) + .properties(propertiesFile) .excludePrefix("database.") - .removePrefix("sqlserver.").get(); + .removePrefix("sqlserver.") + .get(); return DatabaseProvider.builder(config) .withSqlParameterLogging() .withSqlInExceptionMessages() diff --git a/jdbc-query/src/test/resources/logging.properties b/jdbc-query/src/test/resources/logging.properties new file mode 100644 index 0000000..d5f5e0b --- /dev/null +++ b/jdbc-query/src/test/resources/logging.properties @@ -0,0 +1,9 @@ +handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.FileHandler.level=ALL +java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.FileHandler.pattern=build/marc.log +jdk.event.security.level=INFO diff --git a/jdbc-query/src/test/resources/org/xbib/jdbc/query/test/hsqldb.properties b/jdbc-query/src/test/resources/org/xbib/jdbc/query/test/hsqldb.properties new file mode 100644 index 0000000..630755f --- /dev/null +++ b/jdbc-query/src/test/resources/org/xbib/jdbc/query/test/hsqldb.properties @@ -0,0 +1,3 @@ +database.url=jdbc:hsqldb:file:build/hsqldb;shutdown=true +database.user=SA +database.password= \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 87dc8e9..92ce4c8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,7 @@ dependencyResolutionManagement { versionCatalogs { libs { - version('gradle', '7.5') + version('gradle', '7.5.1') version('junit', '5.8.2') version('testcontainers', '1.17.3') library('junit-jupiter-api', 'org.junit.jupiter', 'junit-jupiter-api').versionRef('junit') @@ -10,6 +10,7 @@ dependencyResolutionManagement { library('hamcrest', 'org.hamcrest', 'hamcrest-library').version('2.2') library('junit4', 'junit', 'junit').version('4.13.2') library('derby', 'org.apache.derby', 'derby').version('10.15.2.0') + library('hsqldb', 'org.hsqldb', 'hsqldb').version('2.7.1') library('h2', 'com.h2database', 'h2').version('1.4.200') library('testcontainers', 'org.testcontainers', 'testcontainers').versionRef('testcontainers') library('testcontainers-junit-jupiter', 'org.testcontainers', 'junit-jupiter').versionRef('testcontainers')