Any value greater than zero will be treated as a timeout for pool initialization.
+ * The calling thread will be blocked from continuing until a successful connection
+ * to the database, or until the timeout is reached. If the timeout is reached, then
+ * a {@code PoolInitializationException} will be thrown.
+ *
A value of zero will not prevent the pool from starting in the
+ * case that a connection cannot be obtained. However, upon start the pool will
+ * attempt to obtain a connection and validate that the {@code connectionTestQuery}
+ * and {@code connectionInitSql} are valid. If those validations fail, an exception
+ * will be thrown. If a connection cannot be obtained, the validation is skipped
+ * and the the pool will start and continue to try to obtain connections in the
+ * background. This can mean that callers to {@code DataSource#getConnection()} may
+ * encounter exceptions.
+ *
A value less than zero will bypass any connection attempt and validation during
+ * startup, and therefore the pool will start immediately. The pool will continue to
+ * try to obtain connections in the background. This can mean that callers to
+ * {@code DataSource#getConnection()} may encounter exceptions.
+ *
+ * Note that if this timeout value is greater than or equal to zero (0), and therefore an
+ * initial connection validation is performed, this timeout does not override the
+ * {@code connectionTimeout} or {@code validationTimeout}; they will be honored before this
+ * timeout is applied. The default value is one millisecond.
+ *
+ * @param initializationFailTimeout the number of milliseconds before the
+ * pool initialization fails, or 0 to validate connection setup but continue with
+ * pool start, or less than zero to skip all initialization checks and start the
+ * pool without delay.
+ */
+ public void setInitializationFailTimeout(long initializationFailTimeout) {
+ this.initializationFailTimeout = initializationFailTimeout;
+ }
+
+ /**
+ * Determine whether internal pool queries, principally aliveness checks, will be isolated in their own transaction
+ * via {@link Connection#rollback()}. Defaults to {@code false}.
+ *
+ * @return {@code true} if internal pool queries are isolated, {@code false} if not
+ */
+ public boolean isIsolateInternalQueries() {
+ return isIsolateInternalQueries;
+ }
+
+ /**
+ * Configure whether internal pool queries, principally aliveness checks, will be isolated in their own transaction
+ * via {@link Connection#rollback()}. Defaults to {@code false}.
+ *
+ * @param isolate {@code true} if internal pool queries should be isolated, {@code false} if not
+ */
+ public void setIsolateInternalQueries(boolean isolate) {
+ this.isIsolateInternalQueries = isolate;
+ }
+
+ /**
+ * Determine whether the Connections in the pool are in read-only mode.
+ *
+ * @return {@code true} if the Connections in the pool are read-only, {@code false} if not
+ */
+ public boolean isReadOnly() {
+ return isReadOnly;
+ }
+
+ /**
+ * Configures the Connections to be added to the pool as read-only Connections.
+ *
+ * @param readOnly {@code true} if the Connections in the pool are read-only, {@code false} if not
+ */
+ public void setReadOnly(boolean readOnly) {
+ this.isReadOnly = readOnly;
+ }
+
+ public String getPoolName() {
+ return poolName;
+ }
+
+ /**
+ * Set the name of the connection pool. This is primarily used for the MBean
+ * to uniquely identify the pool configuration.
+ *
+ * @param poolName the name of the connection pool to use
+ */
+ public void setPoolName(String poolName) {
+ this.poolName = poolName;
+ }
+
+ /**
+ * Get the ScheduledExecutorService used for housekeeping.
+ *
+ * @return the executor
+ */
+ public ScheduledExecutorService getScheduledExecutor() {
+ return scheduledExecutor;
+ }
+
+ /**
+ * Set the ScheduledExecutorService used for housekeeping.
+ *
+ * @param executor the ScheduledExecutorService
+ */
+ public void setScheduledExecutor(ScheduledExecutorService executor) {
+ this.scheduledExecutor = executor;
+ }
+
+ public String getTransactionIsolation() {
+ return transactionIsolationName;
+ }
+
+ /**
+ * Get the default schema name to be set on connections.
+ *
+ * @return the default schema name
+ */
+ public String getSchema() {
+ return schema;
+ }
+
+ /**
+ * Set the default schema name to be set on connections.
+ *
+ * @param schema the name of the default schema
+ */
+ public void setSchema(String schema) {
+ this.schema = schema;
+ }
+
+ /**
+ * Set the default transaction isolation level. The specified value is the
+ * constant name from the Connection class, eg.
+ * TRANSACTION_REPEATABLE_READ.
+ *
+ * @param isolationLevel the name of the isolation level
+ */
+ public void setTransactionIsolation(String isolationLevel) {
+ this.transactionIsolationName = isolationLevel;
+ }
+
+ public void setAliveBypassWindowMs(long aliveBypassWindowMs) {
+ this.aliveBypassWindowMs = aliveBypassWindowMs;
+ }
+
+ public long getAliveBypassWindowMs() {
+ return aliveBypassWindowMs;
+ }
+
+ public void setHousekeepingPeriodMs(long housekeepingPeriodMs) {
+ this.housekeepingPeriodMs = housekeepingPeriodMs;
+ }
+
+ public long getHousekeepingPeriodMs() {
+ return housekeepingPeriodMs;
+ }
+
+ /**
+ * Get the thread factory used to create threads.
+ *
+ * @return the thread factory (may be null, in which case the default thread factory is used)
+ */
+ public ThreadFactory getThreadFactory() {
+ return threadFactory;
+ }
+
+ /**
+ * Set the thread factory to be used to create threads.
+ *
+ * @param threadFactory the thread factory (setting to null causes the default thread factory to be used)
+ */
+ public void setThreadFactory(ThreadFactory threadFactory) {
+ this.threadFactory = threadFactory;
+ }
+
+ private Class> attemptFromContextLoader(final String driverClassName) {
+ final ClassLoader threadContextClassLoader = Thread.currentThread().getContextClassLoader();
+ if (threadContextClassLoader != null) {
+ try {
+ final Class> driverClass = threadContextClassLoader.loadClass(driverClassName);
+ logger.log(Level.FINE, "Driver class found in Thread context class loader:" +
+ driverClassName + " " + threadContextClassLoader);
+ return driverClass;
+ } catch (ClassNotFoundException e) {
+ logger.log(Level.FINE, "Driver class not found in Thread context class loader, trying classloader: " +
+ driverClassName + " " + threadContextClassLoader + " " + this.getClass().getClassLoader());
+ }
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("StatementWithEmptyBody")
+ public void validate() {
+ if (poolName == null) {
+ poolName = generatePoolName();
+ }
+ catalog = getNullIfEmpty(catalog);
+ connectionInitSql = getNullIfEmpty(connectionInitSql);
+ connectionTestQuery = getNullIfEmpty(connectionTestQuery);
+ transactionIsolationName = getNullIfEmpty(transactionIsolationName);
+ dataSourceClassName = getNullIfEmpty(dataSourceClassName);
+ driverClassName = getNullIfEmpty(driverClassName);
+ jdbcUrl = getNullIfEmpty(jdbcUrl);
+ if (dataSource != null) {
+ if (dataSourceClassName != null) {
+ logger.log(Level.WARNING, "using dataSource and ignoring dataSourceClassName: " + poolName);
+ }
+ } else if (dataSourceClassName != null) {
+ 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 (jdbcUrl != null) {
+ // ok
+ } else if (driverClassName != null) {
+ logger.log(Level.SEVERE, "jdbcUrl is required with driverClassName: " + poolName);
+ throw new IllegalArgumentException("jdbcUrl 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.");
+ }
+ validateNumerics();
+ }
+
+ /**
+ * @return null if string is null or empty
+ */
+ private static String getNullIfEmpty(final String text) {
+ return text == null ? null : text.trim().isEmpty() ? null : text.trim();
+ }
+
+ private void validateNumerics() {
+ if (maxLifetime != 0 && maxLifetime < TimeUnit.SECONDS.toMillis(30)) {
+ logger.log(Level.WARNING, "maxLifetime is less than 30000ms, setting to default ms: " +
+ poolName + " " + MAX_LIFETIME);
+ maxLifetime = MAX_LIFETIME;
+ }
+ if (leakDetectionThreshold > 0) {
+ if (leakDetectionThreshold < TimeUnit.SECONDS.toMillis(2) || (leakDetectionThreshold > maxLifetime && maxLifetime > 0)) {
+ logger.log(Level.WARNING, "leakDetectionThreshold is less than 2000ms or more than maxLifetime, disabling it: " +
+ poolName);
+ leakDetectionThreshold = 0;
+ }
+ }
+ if (connectionTimeout < 250) {
+ logger.log(Level.WARNING, "connectionTimeout is less than 250ms, setting to ms: " +
+ poolName + " " + CONNECTION_TIMEOUT);
+ connectionTimeout = CONNECTION_TIMEOUT;
+ }
+ if (validationTimeout < 250) {
+ logger.log(Level.WARNING, "validationTimeout is less than 250ms, setting to ms" +
+ poolName + " " + VALIDATION_TIMEOUT);
+ validationTimeout = VALIDATION_TIMEOUT;
+ }
+ if (maxPoolSize < 1) {
+ maxPoolSize = DEFAULT_POOL_SIZE;
+ }
+ if (minIdle < 0 || minIdle > maxPoolSize) {
+ minIdle = maxPoolSize;
+ }
+ if (idleTimeout + TimeUnit.SECONDS.toMillis(1) > maxLifetime && maxLifetime > 0 && minIdle < maxPoolSize) {
+ logger.log(Level.WARNING, "idleTimeout is close to or more than maxLifetime, disabling it:" + poolName);
+ idleTimeout = 0;
+ } else if (idleTimeout != 0 && idleTimeout < TimeUnit.SECONDS.toMillis(10) && minIdle < maxPoolSize) {
+ logger.log(Level.WARNING, "idleTimeout is less than 10000ms, setting to default ms: " +
+ poolName + " " + IDLE_TIMEOUT);
+ idleTimeout = IDLE_TIMEOUT;
+ } else if (idleTimeout != IDLE_TIMEOUT && idleTimeout != 0 && minIdle == maxPoolSize) {
+ logger.log(Level.WARNING, "idleTimeout has been set but has no effect because the pool is operating as a fixed size pool: " + poolName);
+ }
+ }
+
+ private static String generatePoolName() {
+ return "xbib-pool-jdbc-" + POOL_COUNTER.getAndIncrement();
+ }
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolDataSource.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolDataSource.java
new file mode 100644
index 0000000..a0d2bab
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolDataSource.java
@@ -0,0 +1,224 @@
+package org.xbib.jdbc.connection.pool;
+
+import java.io.Closeable;
+import java.io.PrintWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.sql.DataSource;
+
+/**
+ * The pooled DataSource.
+ */
+public class PoolDataSource implements DataSource, Closeable {
+
+ private static final Logger logger = Logger.getLogger(PoolDataSource.class.getName());
+
+ private final AtomicBoolean isShutdown = new AtomicBoolean();
+
+ private final PoolConfig configuration;
+
+ private Pool pool;
+
+ /**
+ * Construct a {@link PoolDataSource} with the specified configuration. The
+ * {@link PoolConfig} is copied and the pool is started by invoking this
+ * constructor.
+ * The {@link PoolConfig} can be modified without affecting the DataSource
+ * and used to initialize another DataSource instance.
+ *
+ * @param configuration a config instance
+ */
+ public PoolDataSource(PoolConfig configuration)
+ throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
+ this.configuration = configuration;
+ pool = new Pool(configuration);
+ }
+
+ public Pool getPool() {
+ return pool;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Connection getConnection() throws SQLException {
+ if (isClosed()) {
+ throw new SQLException("PoolDataSource " + this + " has been closed");
+ }
+ Pool result = pool;
+ if (result == null) {
+ synchronized (this) {
+ result = pool;
+ if (result == null) {
+ configuration.validate();
+ logger.log(Level.INFO, "Starting: " + configuration.getPoolName());
+ try {
+ pool = result = new Pool(configuration);
+ } catch (Exception pie) {
+ throw new SQLException(pie);
+ }
+ logger.log(Level.INFO, "Start completed: " + configuration.getPoolName());
+ }
+ }
+ }
+ return result.getConnection();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Connection getConnection(String username, String password) throws SQLException {
+ throw new SQLFeatureNotSupportedException();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public PrintWriter getLogWriter() throws SQLException {
+ Pool p = pool;
+ return (p != null ? p.getUnwrappedDataSource().getLogWriter() : null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setLogWriter(PrintWriter out) throws SQLException {
+ Pool p = pool;
+ if (p != null) {
+ p.getUnwrappedDataSource().setLogWriter(out);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setLoginTimeout(int seconds) throws SQLException {
+ Pool p = pool;
+ if (p != null) {
+ p.getUnwrappedDataSource().setLoginTimeout(seconds);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getLoginTimeout() throws SQLException {
+ Pool p = pool;
+ return (p != null ? p.getUnwrappedDataSource().getLoginTimeout() : 0);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+ throw new SQLFeatureNotSupportedException();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public T unwrap(Class iface) throws SQLException {
+ if (iface.isInstance(this)) {
+ return (T) this;
+ }
+ Pool p = pool;
+ if (p != null) {
+ final DataSource unwrappedDataSource = p.getUnwrappedDataSource();
+ if (iface.isInstance(unwrappedDataSource)) {
+ return (T) unwrappedDataSource;
+ }
+ if (unwrappedDataSource != null) {
+ return unwrappedDataSource.unwrap(iface);
+ }
+ }
+ throw new SQLException("wrapped DataSource is not an instance of " + iface);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isWrapperFor(Class> iface) throws SQLException {
+ if (iface.isInstance(this)) {
+ return true;
+ }
+ Pool p = pool;
+ if (p != null) {
+ final DataSource unwrappedDataSource = p.getUnwrappedDataSource();
+ if (iface.isInstance(unwrappedDataSource)) {
+ return true;
+ }
+
+ if (unwrappedDataSource != null) {
+ return unwrappedDataSource.isWrapperFor(iface);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Evict a connection from the pool. If the connection has already been closed (returned to the pool)
+ * this may result in a "soft" eviction; the connection will be evicted sometime in the future if it is
+ * currently in use. If the connection has not been closed, the eviction is immediate.
+ *
+ * @param connection the connection to evict from the pool
+ */
+ public void evictConnection(Connection connection) {
+ Pool p;
+ if (!isClosed() && (p = pool) != null) {
+ p.evictConnection(connection);
+ }
+ }
+
+ /**
+ * Shutdown the DataSource and its associated pool.
+ */
+ @Override
+ public void close() {
+ if (isShutdown.getAndSet(true)) {
+ return;
+ }
+ Pool p = pool;
+ if (p != null) {
+ try {
+ logger.log(Level.INFO, () -> "shutdown initiated: " + configuration.getPoolName());
+ p.shutdown();
+ logger.log(Level.INFO, () -> "shutdown completed: " + configuration.getPoolName());
+ } catch (InterruptedException e) {
+ logger.log(Level.WARNING, "interrupted during closing: " + configuration.getPoolName() + " " + e.getMessage());
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ /**
+ * Determine whether the DataSource has been closed.
+ *
+ * @return true if the DataSource has been closed, false otherwise
+ */
+ public boolean isClosed() {
+ return isShutdown.get();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return "PoolDataSource (" + pool + ")";
+ }
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntry.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntry.java
new file mode 100644
index 0000000..fa53695
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntry.java
@@ -0,0 +1,176 @@
+package org.xbib.jdbc.connection.pool;
+
+import org.xbib.jdbc.connection.pool.util.ClockSource;
+import org.xbib.jdbc.connection.pool.util.FastList;
+import org.xbib.jdbc.connection.pool.util.BagEntry;
+import java.sql.Connection;
+import java.sql.Statement;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Pool entry used in the Bag to track Connection instances.
+ */
+public class PoolEntry implements BagEntry {
+
+ private static final Logger logger = Logger.getLogger(PoolEntry.class.getName());
+
+ private static final AtomicIntegerFieldUpdater stateUpdater =
+ AtomicIntegerFieldUpdater.newUpdater(PoolEntry.class, "state");
+
+ private Connection connection;
+
+ private long lastAccessed;
+
+ private long lastBorrowed;
+
+ private volatile int state = 0;
+
+ private volatile boolean evict;
+
+ private volatile ScheduledFuture> endOfLife;
+
+ private final FastList openStatements;
+
+ private final Pool pool;
+
+ private final boolean isReadOnly;
+
+ private final boolean isAutoCommit;
+
+ public PoolEntry(Connection connection, Pool pool, final boolean isReadOnly, final boolean isAutoCommit) {
+ this.connection = connection;
+ this.pool = pool;
+ this.isReadOnly = isReadOnly;
+ this.isAutoCommit = isAutoCommit;
+ this.lastAccessed = ClockSource.currentTime();
+ this.openStatements = new FastList<>(Statement.class, 16);
+ }
+
+ public Connection getConnection() {
+ return connection;
+ }
+
+ public Pool getPool() {
+ return pool;
+ }
+
+ public long getLastAccessed() {
+ return lastAccessed;
+ }
+
+ public long getLastBorrowed() {
+ return lastBorrowed;
+ }
+
+ /**
+ * Release this entry back to the pool.
+ *
+ * @param lastAccessed last access time-stamp
+ */
+ public void recycle(final long lastAccessed) {
+ if (connection != null) {
+ this.lastAccessed = lastAccessed;
+ pool.recycle(this);
+ }
+ }
+
+ /**
+ * Set the end of life {@link ScheduledFuture}.
+ *
+ * @param endOfLife this PoolEntry/Connection's end of life {@link ScheduledFuture}
+ */
+ public void setFutureEol(final ScheduledFuture> endOfLife) {
+ this.endOfLife = endOfLife;
+ }
+
+ public Connection createProxyConnection(final ProxyLeakTask leakTask, final long now) {
+ return ProxyFactory.getProxyConnection(this, connection, openStatements, leakTask, now, isReadOnly, isAutoCommit);
+ }
+
+ public String getPoolName() {
+ return pool.toString();
+ }
+
+ public boolean isMarkedEvicted() {
+ return evict;
+ }
+
+ public void markEvicted() {
+ this.evict = true;
+ }
+
+ public void evict(final String closureReason) {
+ pool.closeConnection(this, closureReason);
+ }
+
+ /**
+ * Returns millis since lastBorrowed
+ */
+ public long getMillisSinceBorrowed() {
+ return ClockSource.elapsedMillis(lastBorrowed);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ final long now = ClockSource.currentTime();
+ return connection
+ + ", accessed " + ClockSource.elapsedDisplayString(lastAccessed, now) + " ago, "
+ + stateToString();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getState() {
+ return stateUpdater.get(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean compareAndSet(int expect, int update) {
+ return stateUpdater.compareAndSet(this, expect, update);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setState(int update) {
+ stateUpdater.set(this, update);
+ }
+
+ public Connection close() {
+ ScheduledFuture> eol = endOfLife;
+ if (eol != null && !eol.isDone() && !eol.cancel(false)) {
+ logger.log(Level.WARNING, "maxLifeTime expiration task cancellation unexpectedly returned false for connection " + getPoolName() + " " + connection);
+ }
+ Connection con = connection;
+ connection = null;
+ endOfLife = null;
+ return con;
+ }
+
+ private String stateToString() {
+ switch (state) {
+ case STATE_IN_USE:
+ return "IN_USE";
+ case STATE_NOT_IN_USE:
+ return "NOT_IN_USE";
+ case STATE_REMOVED:
+ return "REMOVED";
+ case STATE_RESERVED:
+ return "RESERVED";
+ default:
+ return "Invalid";
+ }
+ }
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntryException.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntryException.java
new file mode 100644
index 0000000..60563cd
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntryException.java
@@ -0,0 +1,9 @@
+package org.xbib.jdbc.connection.pool;
+
+@SuppressWarnings("serial")
+public class PoolEntryException extends Exception {
+
+ public PoolEntryException(Throwable t) {
+ super(t);
+ }
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolInitializationException.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolInitializationException.java
new file mode 100644
index 0000000..d44a269
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolInitializationException.java
@@ -0,0 +1,14 @@
+package org.xbib.jdbc.connection.pool;
+
+@SuppressWarnings("serial")
+public class PoolInitializationException extends RuntimeException {
+
+ /**
+ * Construct an exception, possibly wrapping the provided Throwable as the cause.
+ *
+ * @param t the Throwable to wrap
+ */
+ public PoolInitializationException(Throwable t) {
+ super("Failed to initialize pool: " + t.getMessage(), t);
+ }
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyCallableStatement.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyCallableStatement.java
new file mode 100644
index 0000000..b7921ed
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyCallableStatement.java
@@ -0,0 +1,595 @@
+package org.xbib.jdbc.connection.pool;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.net.URL;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.CallableStatement;
+import java.sql.Clob;
+import java.sql.Date;
+import java.sql.NClob;
+import java.sql.Ref;
+import java.sql.RowId;
+import java.sql.SQLException;
+import java.sql.SQLXML;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Map;
+
+/**
+ * This is the proxy class for java.sql.CallableStatement.
+ */
+public class ProxyCallableStatement extends ProxyPreparedStatement implements CallableStatement {
+
+ public ProxyCallableStatement(ProxyConnection connection, CallableStatement statement) {
+ super(connection, statement);
+ }
+
+ @Override
+ public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException {
+ ((CallableStatement) delegate).registerOutParameter(parameterIndex, sqlType);
+ }
+
+ @Override
+ public void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException {
+ ((CallableStatement) delegate).registerOutParameter(parameterIndex, sqlType, scale);
+ }
+
+ @Override
+ public boolean wasNull() throws SQLException {
+ return ((CallableStatement) delegate).wasNull();
+ }
+
+ @Override
+ public String getString(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getString(parameterIndex);
+ }
+
+ @Override
+ public boolean getBoolean(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getBoolean(parameterIndex);
+ }
+
+ @Override
+ public byte getByte(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getByte(parameterIndex);
+ }
+
+ @Override
+ public short getShort(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getShort(parameterIndex);
+ }
+
+ @Override
+ public int getInt(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getInt(parameterIndex);
+ }
+
+ @Override
+ public long getLong(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getLong(parameterIndex);
+ }
+
+ @Override
+ public float getFloat(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getFloat(parameterIndex);
+ }
+
+ @Override
+ public double getDouble(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getDouble(parameterIndex);
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException {
+ return ((CallableStatement) delegate).getBigDecimal(parameterIndex, scale);
+ }
+
+ @Override
+ public byte[] getBytes(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getBytes(parameterIndex);
+ }
+
+ @Override
+ public Date getDate(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getDate(parameterIndex);
+ }
+
+ @Override
+ public Time getTime(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getTime(parameterIndex);
+ }
+
+ @Override
+ public Timestamp getTimestamp(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getTimestamp(parameterIndex);
+ }
+
+ @Override
+ public Object getObject(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getObject(parameterIndex);
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getBigDecimal(parameterIndex);
+ }
+
+ @Override
+ public Object getObject(int parameterIndex, Map> map) throws SQLException {
+ return ((CallableStatement) delegate).getObject(parameterIndex, map);
+ }
+
+ @Override
+ public Ref getRef(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getRef(parameterIndex);
+ }
+
+ @Override
+ public Blob getBlob(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getBlob(parameterIndex);
+ }
+
+ @Override
+ public Clob getClob(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getClob(parameterIndex);
+ }
+
+ @Override
+ public Array getArray(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getArray(parameterIndex);
+ }
+
+ @Override
+ public Date getDate(int parameterIndex, Calendar cal) throws SQLException {
+ return ((CallableStatement) delegate).getDate(parameterIndex, cal);
+ }
+
+ @Override
+ public Time getTime(int parameterIndex, Calendar cal) throws SQLException {
+ return ((CallableStatement) delegate).getTime(parameterIndex, cal);
+ }
+
+ @Override
+ public Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException {
+ return ((CallableStatement) delegate).getTimestamp(parameterIndex, cal);
+ }
+
+ @Override
+ public void registerOutParameter(int parameterIndex, int sqlType, String typeName) throws SQLException {
+ ((CallableStatement) delegate).registerOutParameter(parameterIndex, sqlType, typeName);
+ }
+
+ @Override
+ public void registerOutParameter(String parameterName, int sqlType) throws SQLException {
+ ((CallableStatement) delegate).registerOutParameter(parameterName, sqlType);
+ }
+
+ @Override
+ public void registerOutParameter(String parameterName, int sqlType, int scale) throws SQLException {
+ ((CallableStatement) delegate).registerOutParameter(parameterName, sqlType, scale);
+ }
+
+ @Override
+ public void registerOutParameter(String parameterName, int sqlType, String typeName) throws SQLException {
+ ((CallableStatement) delegate).registerOutParameter(parameterName, sqlType, typeName);
+ }
+
+ @Override
+ public URL getURL(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getURL(parameterIndex);
+ }
+
+ @Override
+ public void setURL(String parameterName, URL val) throws SQLException {
+ ((CallableStatement) delegate).setURL(parameterName, val);
+ }
+
+ @Override
+ public void setNull(String parameterName, int sqlType) throws SQLException {
+ ((CallableStatement) delegate).setNull(parameterName, sqlType);
+ }
+
+ @Override
+ public void setBoolean(String parameterName, boolean x) throws SQLException {
+ ((CallableStatement) delegate).setBoolean(parameterName, x);
+ }
+
+ @Override
+ public void setByte(String parameterName, byte x) throws SQLException {
+ ((CallableStatement) delegate).setByte(parameterName, x);
+ }
+
+ @Override
+ public void setShort(String parameterName, short x) throws SQLException {
+ ((CallableStatement) delegate).setShort(parameterName, x);
+ }
+
+ @Override
+ public void setInt(String parameterName, int x) throws SQLException {
+ ((CallableStatement) delegate).setInt(parameterName, x);
+ }
+
+ @Override
+ public void setLong(String parameterName, long x) throws SQLException {
+ ((CallableStatement) delegate).setLong(parameterName, x);
+ }
+
+ @Override
+ public void setFloat(String parameterName, float x) throws SQLException {
+ ((CallableStatement) delegate).setFloat(parameterName, x);
+ }
+
+ @Override
+ public void setDouble(String parameterName, double x) throws SQLException {
+ ((CallableStatement) delegate).setDouble(parameterName, x);
+ }
+
+ @Override
+ public void setBigDecimal(String parameterName, BigDecimal x) throws SQLException {
+ ((CallableStatement) delegate).setBigDecimal(parameterName, x);
+ }
+
+ @Override
+ public void setString(String parameterName, String x) throws SQLException {
+ ((CallableStatement) delegate).setString(parameterName, x);
+ }
+
+ @Override
+ public void setBytes(String parameterName, byte[] x) throws SQLException {
+ ((CallableStatement) delegate).setBytes(parameterName, x);
+ }
+
+ @Override
+ public void setDate(String parameterName, Date x) throws SQLException {
+ ((CallableStatement) delegate).setDate(parameterName, x);
+ }
+
+ @Override
+ public void setTime(String parameterName, Time x) throws SQLException {
+ ((CallableStatement) delegate).setTime(parameterName, x);
+ }
+
+ @Override
+ public void setTimestamp(String parameterName, Timestamp x) throws SQLException {
+ ((CallableStatement) delegate).setTimestamp(parameterName, x);
+ }
+
+ @Override
+ public void setAsciiStream(String parameterName, InputStream x, int length) throws SQLException {
+ ((CallableStatement) delegate).setAsciiStream(parameterName, x, length);
+ }
+
+ @Override
+ public void setBinaryStream(String parameterName, InputStream x, int length) throws SQLException {
+ ((CallableStatement) delegate).setBinaryStream(parameterName, x, length);
+ }
+
+ @Override
+ public void setObject(String parameterName, Object x, int targetSqlType, int scale) throws SQLException {
+ ((CallableStatement) delegate).setObject(parameterName, x, targetSqlType, scale);
+ }
+
+ @Override
+ public void setObject(String parameterName, Object x, int targetSqlType) throws SQLException {
+ ((CallableStatement) delegate).setObject(parameterName, x, targetSqlType);
+ }
+
+ @Override
+ public void setObject(String parameterName, Object x) throws SQLException {
+ ((CallableStatement) delegate).setObject(parameterName, x);
+ }
+
+ @Override
+ public void setCharacterStream(String parameterName, Reader reader, int length) throws SQLException {
+ ((CallableStatement) delegate).setCharacterStream(parameterName, reader, length);
+ }
+
+ @Override
+ public void setDate(String parameterName, Date x, Calendar cal) throws SQLException {
+ ((CallableStatement) delegate).setDate(parameterName, x, cal);
+ }
+
+ @Override
+ public void setTime(String parameterName, Time x, Calendar cal) throws SQLException {
+ ((CallableStatement) delegate).setTime(parameterName, x, cal);
+ }
+
+ @Override
+ public void setTimestamp(String parameterName, Timestamp x, Calendar cal) throws SQLException {
+ ((CallableStatement) delegate).setTimestamp(parameterName, x, cal);
+ }
+
+ @Override
+ public void setNull(String parameterName, int sqlType, String typeName) throws SQLException {
+ ((CallableStatement) delegate).setNull(parameterName, sqlType, typeName);
+ }
+
+ @Override
+ public String getString(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getString(parameterName);
+ }
+
+ @Override
+ public boolean getBoolean(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getBoolean(parameterName);
+ }
+
+ @Override
+ public byte getByte(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getByte(parameterName);
+ }
+
+ @Override
+ public short getShort(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getShort(parameterName);
+ }
+
+ @Override
+ public int getInt(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getInt(parameterName);
+ }
+
+ @Override
+ public long getLong(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getLong(parameterName);
+ }
+
+ @Override
+ public float getFloat(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getFloat(parameterName);
+ }
+
+ @Override
+ public double getDouble(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getDouble(parameterName);
+ }
+
+ @Override
+ public byte[] getBytes(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getBytes(parameterName);
+ }
+
+ @Override
+ public Date getDate(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getDate(parameterName);
+ }
+
+ @Override
+ public Time getTime(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getTime(parameterName);
+ }
+
+ @Override
+ public Timestamp getTimestamp(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getTimestamp(parameterName);
+ }
+
+ @Override
+ public Object getObject(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getObject(parameterName);
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getBigDecimal(parameterName);
+ }
+
+ @Override
+ public Object getObject(String parameterName, Map> map) throws SQLException {
+ return ((CallableStatement) delegate).getObject(parameterName, map);
+ }
+
+ @Override
+ public Ref getRef(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getRef(parameterName);
+ }
+
+ @Override
+ public Blob getBlob(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getBlob(parameterName);
+ }
+
+ @Override
+ public Clob getClob(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getClob(parameterName);
+ }
+
+ @Override
+ public Array getArray(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getArray(parameterName);
+ }
+
+ @Override
+ public Date getDate(String parameterName, Calendar cal) throws SQLException {
+ return ((CallableStatement) delegate).getDate(parameterName, cal);
+ }
+
+ @Override
+ public Time getTime(String parameterName, Calendar cal) throws SQLException {
+ return ((CallableStatement) delegate).getTime(parameterName, cal);
+ }
+
+ @Override
+ public Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException {
+ return ((CallableStatement) delegate).getTimestamp(parameterName, cal);
+ }
+
+ @Override
+ public URL getURL(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getURL(parameterName);
+ }
+
+ @Override
+ public RowId getRowId(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getRowId(parameterIndex);
+ }
+
+ @Override
+ public RowId getRowId(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getRowId(parameterName);
+ }
+
+ @Override
+ public void setRowId(String parameterName, RowId x) throws SQLException {
+ ((CallableStatement) delegate).setRowId(parameterName, x);
+ }
+
+ @Override
+ public void setNString(String parameterName, String value) throws SQLException {
+ ((CallableStatement) delegate).setNString(parameterName, value);
+ }
+
+ @Override
+ public void setNCharacterStream(String parameterName, Reader value, long length) throws SQLException {
+ ((CallableStatement) delegate).setNCharacterStream(parameterName, value, length);
+ }
+
+ @Override
+ public void setNClob(String parameterName, NClob value) throws SQLException {
+ ((CallableStatement) delegate).setNClob(parameterName, value);
+ }
+
+ @Override
+ public void setClob(String parameterName, Reader reader, long length) throws SQLException {
+ ((CallableStatement) delegate).setClob(parameterName, reader, length);
+ }
+
+ @Override
+ public void setBlob(String parameterName, InputStream inputStream, long length) throws SQLException {
+ ((CallableStatement) delegate).setBlob(parameterName, inputStream, length);
+ }
+
+ @Override
+ public void setNClob(String parameterName, Reader reader, long length) throws SQLException {
+ ((CallableStatement) delegate).setNClob(parameterName, reader, length);
+ }
+
+ @Override
+ public NClob getNClob(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getNClob(parameterIndex);
+ }
+
+ @Override
+ public NClob getNClob(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getNClob(parameterName);
+ }
+
+ @Override
+ public void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException {
+ ((CallableStatement) delegate).setSQLXML(parameterName, xmlObject);
+ }
+
+ @Override
+ public SQLXML getSQLXML(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getSQLXML(parameterIndex);
+ }
+
+ @Override
+ public SQLXML getSQLXML(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getSQLXML(parameterName);
+ }
+
+ @Override
+ public String getNString(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getNString(parameterIndex);
+ }
+
+ @Override
+ public String getNString(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getNString(parameterName);
+ }
+
+ @Override
+ public Reader getNCharacterStream(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getNCharacterStream(parameterIndex);
+ }
+
+ @Override
+ public Reader getNCharacterStream(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getNCharacterStream(parameterName);
+ }
+
+ @Override
+ public Reader getCharacterStream(int parameterIndex) throws SQLException {
+ return ((CallableStatement) delegate).getCharacterStream(parameterIndex);
+ }
+
+ @Override
+ public Reader getCharacterStream(String parameterName) throws SQLException {
+ return ((CallableStatement) delegate).getCharacterStream(parameterName);
+ }
+
+ @Override
+ public void setBlob(String parameterName, Blob x) throws SQLException {
+ ((CallableStatement) delegate).setBlob(parameterName, x);
+ }
+
+ @Override
+ public void setClob(String parameterName, Clob x) throws SQLException {
+ ((CallableStatement) delegate).setClob(parameterName, x);
+ }
+
+ @Override
+ public void setAsciiStream(String parameterName, InputStream x, long length) throws SQLException {
+ ((CallableStatement) delegate).setAsciiStream(parameterName, x, length);
+ }
+
+ @Override
+ public void setBinaryStream(String parameterName, InputStream x, long length) throws SQLException {
+ ((CallableStatement) delegate).setBinaryStream(parameterName, x, length);
+ }
+
+ @Override
+ public void setCharacterStream(String parameterName, Reader reader, long length) throws SQLException {
+ ((CallableStatement) delegate).setCharacterStream(parameterName, reader, length);
+ }
+
+ @Override
+ public void setAsciiStream(String parameterName, InputStream x) throws SQLException {
+ ((CallableStatement) delegate).setAsciiStream(parameterName, x);
+ }
+
+ @Override
+ public void setBinaryStream(String parameterName, InputStream x) throws SQLException {
+ ((CallableStatement) delegate).setBinaryStream(parameterName, x);
+ }
+
+ @Override
+ public void setCharacterStream(String parameterName, Reader reader) throws SQLException {
+ ((CallableStatement) delegate).setCharacterStream(parameterName, reader);
+ }
+
+ @Override
+ public void setNCharacterStream(String parameterName, Reader value) throws SQLException {
+ ((CallableStatement) delegate).setNCharacterStream(parameterName, value);
+ }
+
+ @Override
+ public void setClob(String parameterName, Reader reader) throws SQLException {
+ ((CallableStatement) delegate).setClob(parameterName, reader);
+ }
+
+ @Override
+ public void setBlob(String parameterName, InputStream inputStream) throws SQLException {
+ ((CallableStatement) delegate).setBlob(parameterName, inputStream);
+ }
+
+ @Override
+ public void setNClob(String parameterName, Reader reader) throws SQLException {
+ ((CallableStatement) delegate).setNClob(parameterName, reader);
+ }
+
+ @Override
+ public T getObject(int parameterIndex, Class type) throws SQLException {
+ return ((CallableStatement) delegate).getObject(parameterIndex, type);
+ }
+
+ @Override
+ public T getObject(String parameterName, Class type) throws SQLException {
+ return ((CallableStatement) delegate).getObject(parameterName, type);
+ }
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyConnection.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyConnection.java
new file mode 100644
index 0000000..4093259
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyConnection.java
@@ -0,0 +1,697 @@
+package org.xbib.jdbc.connection.pool;
+
+import org.xbib.jdbc.connection.pool.util.ClockSource;
+import org.xbib.jdbc.connection.pool.util.FastList;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.CallableStatement;
+import java.sql.Clob;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.NClob;
+import java.sql.PreparedStatement;
+import java.sql.SQLClientInfoException;
+import java.sql.SQLException;
+import java.sql.SQLTimeoutException;
+import java.sql.SQLWarning;
+import java.sql.SQLXML;
+import java.sql.Savepoint;
+import java.sql.Statement;
+import java.sql.Struct;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This is the proxy class for java.sql.Connection.
+ */
+public class ProxyConnection implements Connection {
+
+ private static final Logger logger = Logger.getLogger(ProxyConnection.class.getName());
+
+ private static final int DIRTY_BIT_READONLY = 0b000001;
+
+ private static final int DIRTY_BIT_AUTOCOMMIT = 0b000010;
+
+ private static final int DIRTY_BIT_ISOLATION = 0b000100;
+
+ private static final int DIRTY_BIT_CATALOG = 0b001000;
+
+ private static final int DIRTY_BIT_NETTIMEOUT = 0b010000;
+
+ private static final int DIRTY_BIT_SCHEMA = 0b100000;
+
+ private static final Set ERROR_STATES;
+
+ private static final Set ERROR_CODES;
+
+ private Connection delegate;
+
+ private final PoolEntry poolEntry;
+
+ private final ProxyLeakTask leakTask;
+
+ private final FastList openStatements;
+
+ private int dirtyBits;
+
+ private long lastAccess;
+
+ private boolean isCommitStateDirty;
+
+ private boolean isReadOnly;
+
+ private boolean isAutoCommit;
+
+ private int networkTimeout;
+
+ private int transactionIsolation;
+
+ private String dbcatalog;
+
+ private String dbschema;
+
+ static {
+ ERROR_STATES = new HashSet<>();
+ ERROR_STATES.add("0A000"); // FEATURE UNSUPPORTED
+ ERROR_STATES.add("57P01"); // ADMIN SHUTDOWN
+ ERROR_STATES.add("57P02"); // CRASH SHUTDOWN
+ ERROR_STATES.add("57P03"); // CANNOT CONNECT NOW
+ ERROR_STATES.add("01002"); // SQL92 disconnect error
+ ERROR_STATES.add("JZ0C0"); // Sybase disconnect error
+ ERROR_STATES.add("JZ0C1"); // Sybase disconnect error
+
+ ERROR_CODES = new HashSet<>();
+ ERROR_CODES.add(500150);
+ ERROR_CODES.add(2399);
+ }
+
+ public ProxyConnection(PoolEntry poolEntry,
+ Connection connection,
+ FastList openStatements,
+ ProxyLeakTask leakTask,
+ long now,
+ boolean isReadOnly,
+ boolean isAutoCommit) {
+ this.poolEntry = poolEntry;
+ this.delegate = connection;
+ this.openStatements = openStatements;
+ this.leakTask = leakTask;
+ this.lastAccess = now;
+ this.isReadOnly = isReadOnly;
+ this.isAutoCommit = isAutoCommit;
+ }
+
+ public boolean isCommitStateDirty() {
+ return isCommitStateDirty;
+ }
+
+ /**
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public final String toString() {
+ return getClass().getSimpleName() + '@' + System.identityHashCode(this) + " wrapping " + delegate;
+ }
+
+ public boolean getAutoCommitState() {
+ return isAutoCommit;
+ }
+
+ public String getCatalogState() {
+ return dbcatalog;
+ }
+
+ public String getSchemaState() {
+ return dbschema;
+ }
+
+ public int getTransactionIsolationState() {
+ return transactionIsolation;
+ }
+
+ public boolean getReadOnlyState() {
+ return isReadOnly;
+ }
+
+ public int getNetworkTimeoutState() {
+ return networkTimeout;
+ }
+
+ public PoolEntry getPoolEntry() {
+ return poolEntry;
+ }
+
+ public SQLException checkException(SQLException sqle) {
+ boolean evict = false;
+ SQLException nse = sqle;
+ for (int depth = 0; delegate != CLOSED_CONNECTION && nse != null && depth < 10; depth++) {
+ final String sqlState = nse.getSQLState();
+ if (sqlState != null && sqlState.startsWith("08")
+ || nse instanceof SQLTimeoutException
+ || ERROR_STATES.contains(sqlState)
+ || ERROR_CODES.contains(nse.getErrorCode())) {
+ // broken connection
+ evict = true;
+ break;
+ } else {
+ nse = nse.getNextException();
+ }
+ }
+ if (evict) {
+ logger.log(Level.WARNING, "Connection marked as broken because of SQLSTATE(), ErrorCode(): " +
+ poolEntry.getPoolName() + " " + delegate + " " + nse.getSQLState() + " " + nse.getErrorCode(), nse);
+ leakTask.cancel();
+ poolEntry.evict("(connection is broken)");
+ delegate = CLOSED_CONNECTION;
+ }
+ return sqle;
+ }
+
+ public synchronized void untrackStatement(final Statement statement) {
+ openStatements.remove(statement);
+ }
+
+ public void markCommitStateDirty() {
+ if (isAutoCommit) {
+ lastAccess = ClockSource.currentTime();
+ } else {
+ isCommitStateDirty = true;
+ }
+ }
+
+ public void cancelLeakTask() {
+ leakTask.cancel();
+ }
+
+ private synchronized T trackStatement(final T statement) {
+ openStatements.add(statement);
+ return statement;
+ }
+
+ @SuppressWarnings("try")
+ private synchronized void closeStatements() {
+ final int size = openStatements.size();
+ if (size > 0) {
+ for (int i = 0; i < size && delegate != CLOSED_CONNECTION; i++) {
+ try (Statement ignored = openStatements.get(i)) {
+ // automatic resource cleanup
+ } catch (SQLException e) {
+ logger.log(Level.WARNING, "Connection marked as broken because of an exception closing open statements during Connection.close(): " +
+ poolEntry.getPoolName() + " " + delegate);
+ leakTask.cancel();
+ poolEntry.evict("(exception closing Statements during Connection.close())");
+ delegate = CLOSED_CONNECTION;
+ }
+ }
+ openStatements.clear();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close() throws SQLException {
+ closeStatements();
+ if (delegate != CLOSED_CONNECTION) {
+ leakTask.cancel();
+ try {
+ if (isCommitStateDirty && !isAutoCommit) {
+ delegate.rollback();
+ lastAccess = ClockSource.currentTime();
+ logger.log(Level.FINE, "Executed rollback on connection due to dirty commit state on close(): " + poolEntry.getPoolName() + " " + delegate);
+ }
+ if (dirtyBits != 0) {
+ //poolEntry.resetConnectionState(this, dirtyBits);
+ resetConnectionState(poolEntry.getConnection(), dirtyBits,
+ poolEntry.getPool().getConfig().getCatalog() ,
+ poolEntry.getPool().getConfig().getSchema());
+ lastAccess = ClockSource.currentTime();
+ }
+ delegate.clearWarnings();
+ } catch (SQLException e) {
+ // when connections are aborted, exceptions are often thrown that should not reach the application
+ if (!poolEntry.isMarkedEvicted()) {
+ throw checkException(e);
+ }
+ } finally {
+ delegate = CLOSED_CONNECTION;
+ poolEntry.recycle(lastAccess);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isClosed() throws SQLException {
+ return delegate == CLOSED_CONNECTION;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Statement createStatement() throws SQLException {
+ return ProxyFactory.getProxyStatement(this, trackStatement(delegate.createStatement()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Statement createStatement(int resultSetType, int concurrency) throws SQLException {
+ return ProxyFactory.getProxyStatement(this, trackStatement(delegate.createStatement(resultSetType, concurrency)));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Statement createStatement(int resultSetType, int concurrency, int holdability) throws SQLException {
+ return ProxyFactory.getProxyStatement(this, trackStatement(delegate.createStatement(resultSetType, concurrency, holdability)));
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CallableStatement prepareCall(String sql) throws SQLException {
+ return ProxyFactory.getProxyCallableStatement(this, trackStatement(delegate.prepareCall(sql)));
+ }
+
+ @Override
+ public String nativeSQL(String sql) throws SQLException {
+ return delegate.nativeSQL(sql);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CallableStatement prepareCall(String sql, int resultSetType, int concurrency) throws SQLException {
+ return ProxyFactory.getProxyCallableStatement(this, trackStatement(delegate.prepareCall(sql, resultSetType, concurrency)));
+ }
+
+ @Override
+ public Map> getTypeMap() throws SQLException {
+ return delegate.getTypeMap();
+ }
+
+ @Override
+ public void setTypeMap(Map> map) throws SQLException {
+ delegate.setTypeMap(map);
+ }
+
+ @Override
+ public void setHoldability(int holdability) throws SQLException {
+ delegate.setHoldability(holdability);
+ }
+
+ @Override
+ public int getHoldability() throws SQLException {
+ return delegate.getHoldability();
+ }
+
+ @Override
+ public Savepoint setSavepoint() throws SQLException {
+ return delegate.setSavepoint();
+ }
+
+ @Override
+ public Savepoint setSavepoint(String name) throws SQLException {
+ return delegate.setSavepoint(name);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CallableStatement prepareCall(String sql, int resultSetType, int concurrency, int holdability) throws SQLException {
+ return ProxyFactory.getProxyCallableStatement(this, trackStatement(delegate.prepareCall(sql, resultSetType, concurrency, holdability)));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public PreparedStatement prepareStatement(String sql) throws SQLException {
+ return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql)));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
+ return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, autoGeneratedKeys)));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public PreparedStatement prepareStatement(String sql, int resultSetType, int concurrency) throws SQLException {
+ return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, resultSetType, concurrency)));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public PreparedStatement prepareStatement(String sql, int resultSetType, int concurrency, int holdability) throws SQLException {
+ return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, resultSetType, concurrency, holdability)));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
+ return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, columnIndexes)));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
+ return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, columnNames)));
+ }
+
+ @Override
+ public Clob createClob() throws SQLException {
+ return delegate.createClob();
+ }
+
+ @Override
+ public Blob createBlob() throws SQLException {
+ return delegate.createBlob();
+ }
+
+ @Override
+ public NClob createNClob() throws SQLException {
+ return delegate.createNClob();
+ }
+
+ @Override
+ public SQLXML createSQLXML() throws SQLException {
+ return delegate.createSQLXML();
+ }
+
+ @Override
+ public boolean isValid(int timeout) throws SQLException {
+ return delegate.isValid(timeout);
+ }
+
+ @Override
+ public void setClientInfo(String name, String value) throws SQLClientInfoException {
+ delegate.setClientInfo(name, value);
+ }
+
+ @Override
+ public void setClientInfo(Properties properties) throws SQLClientInfoException {
+ delegate.setClientInfo(properties);
+ }
+
+ @Override
+ public String getClientInfo(String name) throws SQLException {
+ return delegate.getClientInfo(name);
+ }
+
+ @Override
+ public Properties getClientInfo() throws SQLException {
+ return delegate.getClientInfo();
+ }
+
+ @Override
+ public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
+ return delegate.createArrayOf(typeName, elements);
+ }
+
+ @Override
+ public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
+ return delegate.createStruct(typeName, attributes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public DatabaseMetaData getMetaData() throws SQLException {
+ markCommitStateDirty();
+ return ProxyFactory.getProxyDatabaseMetaData(this, delegate.getMetaData());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void commit() throws SQLException {
+ delegate.commit();
+ isCommitStateDirty = false;
+ lastAccess = ClockSource.currentTime();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void rollback() throws SQLException {
+ delegate.rollback();
+ isCommitStateDirty = false;
+ lastAccess = ClockSource.currentTime();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void rollback(Savepoint savepoint) throws SQLException {
+ delegate.rollback(savepoint);
+ isCommitStateDirty = false;
+ lastAccess = ClockSource.currentTime();
+ }
+
+ @Override
+ public void releaseSavepoint(Savepoint savepoint) throws SQLException {
+ delegate.releaseSavepoint(savepoint);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setAutoCommit(boolean autoCommit) throws SQLException {
+ delegate.setAutoCommit(autoCommit);
+ isAutoCommit = autoCommit;
+ dirtyBits |= DIRTY_BIT_AUTOCOMMIT;
+ }
+
+ @Override
+ public boolean getAutoCommit() throws SQLException {
+ return delegate.getAutoCommit();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setReadOnly(boolean readOnly) throws SQLException {
+ delegate.setReadOnly(readOnly);
+ isReadOnly = readOnly;
+ isCommitStateDirty = false;
+ dirtyBits |= DIRTY_BIT_READONLY;
+ }
+
+ @Override
+ public boolean isReadOnly() throws SQLException {
+ return delegate.isReadOnly();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setTransactionIsolation(int level) throws SQLException {
+ delegate.setTransactionIsolation(level);
+ transactionIsolation = level;
+ dirtyBits |= DIRTY_BIT_ISOLATION;
+ }
+
+ @Override
+ public int getTransactionIsolation() throws SQLException {
+ return delegate.getTransactionIsolation();
+ }
+
+ @Override
+ public SQLWarning getWarnings() throws SQLException {
+ return delegate.getWarnings();
+ }
+
+ @Override
+ public void clearWarnings() throws SQLException {
+ delegate.clearWarnings();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setCatalog(String catalog) throws SQLException {
+ delegate.setCatalog(catalog);
+ dbcatalog = catalog;
+ dirtyBits |= DIRTY_BIT_CATALOG;
+ }
+
+ @Override
+ public String getCatalog() throws SQLException {
+ return delegate.getCatalog();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
+ delegate.setNetworkTimeout(executor, milliseconds);
+ networkTimeout = milliseconds;
+ dirtyBits |= DIRTY_BIT_NETTIMEOUT;
+ }
+
+ @Override
+ public int getNetworkTimeout() throws SQLException {
+ return delegate.getNetworkTimeout();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setSchema(String schema) throws SQLException {
+ delegate.setSchema(schema);
+ dbschema = schema;
+ dirtyBits |= DIRTY_BIT_SCHEMA;
+ }
+
+ @Override
+ public String getSchema() throws SQLException {
+ return delegate.getSchema();
+ }
+
+ @Override
+ public void abort(Executor executor) throws SQLException {
+ delegate.abort(executor);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final boolean isWrapperFor(Class> iface) throws SQLException {
+ return iface.isInstance(delegate) || (delegate != null && delegate.isWrapperFor(iface));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public T unwrap(Class iface) throws SQLException {
+ if (iface.isInstance(delegate)) {
+ return (T) delegate;
+ } else if (delegate != null) {
+ return delegate.unwrap(iface);
+ }
+ throw new SQLException("Wrapped connection is not an instance of " + iface);
+ }
+
+ static final Connection CLOSED_CONNECTION = getClosedConnection();
+
+ private static Connection getClosedConnection() {
+ InvocationHandler handler = (proxy, method, args) -> {
+ final String methodName = method.getName();
+ if ("isClosed".equals(methodName)) {
+ return Boolean.TRUE;
+ } else if ("isValid".equals(methodName)) {
+ return Boolean.FALSE;
+ }
+ if ("abort".equals(methodName)) {
+ return Void.TYPE;
+ }
+ if ("close".equals(methodName)) {
+ return Void.TYPE;
+ } else if ("toString".equals(methodName)) {
+ return ProxyConnection.class.getCanonicalName();
+ }
+ throw new SQLException("connection is closed");
+ };
+ return (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(),
+ new Class>[] { Connection.class }, handler);
+ }
+
+ private void resetConnectionState(Connection connection, int dirtyBits, String catalog, String schema) throws SQLException {
+ int resetBits = 0;
+ if ((dirtyBits & DIRTY_BIT_READONLY) != 0 && getReadOnlyState() != isReadOnly) {
+ connection.setReadOnly(isReadOnly);
+ resetBits |= DIRTY_BIT_READONLY;
+ }
+ if ((dirtyBits & DIRTY_BIT_AUTOCOMMIT) != 0 && getAutoCommitState() != isAutoCommit) {
+ connection.setAutoCommit(isAutoCommit);
+ resetBits |= DIRTY_BIT_AUTOCOMMIT;
+ }
+ if ((dirtyBits & DIRTY_BIT_ISOLATION) != 0 && getTransactionIsolationState() != transactionIsolation) {
+ connection.setTransactionIsolation(transactionIsolation);
+ resetBits |= DIRTY_BIT_ISOLATION;
+ }
+ if ((dirtyBits & DIRTY_BIT_CATALOG) != 0 && catalog != null && !catalog.equals(getCatalogState())) {
+ connection.setCatalog(catalog);
+ resetBits |= DIRTY_BIT_CATALOG;
+ }
+ if ((dirtyBits & DIRTY_BIT_NETTIMEOUT) != 0 && getNetworkTimeoutState() != networkTimeout) {
+ connection.setNetworkTimeout(Runnable::run, networkTimeout);
+ resetBits |= DIRTY_BIT_NETTIMEOUT;
+ }
+ if ((dirtyBits & DIRTY_BIT_SCHEMA) != 0 && schema != null && !schema.equals(getSchemaState())) {
+ connection.setSchema(schema);
+ resetBits |= DIRTY_BIT_SCHEMA;
+ }
+ if (resetBits != 0 && logger.isLoggable(Level.FINE)) {
+ final String string = stringFromResetBits(resetBits);
+ logger.log(Level.FINE, () -> "reset on connection: " + string + " " + connection);
+ }
+ }
+
+ /**
+ * This will create a string for debug logging. Given a set of "reset bits", this
+ * method will return a concatenated string, for example
+ * Input : 0b00110
+ * Output: "autoCommit, isolation"
+ *
+ * @param bits a set of "reset bits"
+ * @return a string of which states were reset
+ */
+ private String stringFromResetBits(final int bits) {
+ final StringBuilder sb = new StringBuilder();
+ for (int ndx = 0; ndx < RESET_STATES.length; ndx++) {
+ if ((bits & (0b1 << ndx)) != 0) {
+ sb.append(RESET_STATES[ndx]).append(", ");
+ }
+ }
+ sb.setLength(sb.length() - 2); // trim trailing comma
+ return sb.toString();
+ }
+
+ private static final String[] RESET_STATES = {"readOnly", "autoCommit", "isolation", "catalog", "netTimeout", "schema"};
+
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyDatabaseMetaData.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyDatabaseMetaData.java
new file mode 100644
index 0000000..7f099e6
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyDatabaseMetaData.java
@@ -0,0 +1,1056 @@
+package org.xbib.jdbc.connection.pool;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.RowIdLifetime;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class ProxyDatabaseMetaData implements DatabaseMetaData {
+
+ protected final ProxyConnection connection;
+
+ private final DatabaseMetaData delegate;
+
+ public ProxyDatabaseMetaData(ProxyConnection connection, DatabaseMetaData metaData) {
+ this.connection = connection;
+ this.delegate = metaData;
+ }
+
+ @SuppressWarnings("unused")
+ public SQLException checkException(SQLException e) {
+ return connection.checkException(e);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ final String delegateToString = delegate.toString();
+ return this.getClass().getSimpleName() + '@' + System.identityHashCode(this) + " wrapping " + delegateToString;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Connection getConnection() {
+ return connection;
+ }
+
+ @Override
+ public boolean supportsSavepoints() throws SQLException {
+ return delegate.supportsSavepoints();
+ }
+
+ @Override
+ public boolean supportsNamedParameters() throws SQLException {
+ return delegate.supportsNamedParameters();
+ }
+
+ @Override
+ public boolean supportsMultipleOpenResults() throws SQLException {
+ return delegate.supportsMultipleOpenResults();
+ }
+
+ @Override
+ public boolean supportsGetGeneratedKeys() throws SQLException {
+ return delegate.supportsGetGeneratedKeys();
+ }
+
+ @Override
+ public boolean allProceduresAreCallable() throws SQLException {
+ return delegate.allProceduresAreCallable();
+ }
+
+ @Override
+ public boolean allTablesAreSelectable() throws SQLException {
+ return delegate.allTablesAreSelectable();
+ }
+
+ @Override
+ public String getURL() throws SQLException {
+ return delegate.getURL();
+ }
+
+ @Override
+ public String getUserName() throws SQLException {
+ return delegate.getUserName();
+ }
+
+ @Override
+ public boolean isReadOnly() throws SQLException {
+ return delegate.isReadOnly();
+ }
+
+ @Override
+ public boolean nullsAreSortedHigh() throws SQLException {
+ return delegate.nullsAreSortedHigh();
+ }
+
+ @Override
+ public boolean nullsAreSortedLow() throws SQLException {
+ return delegate.nullsAreSortedLow();
+ }
+
+ @Override
+ public boolean nullsAreSortedAtStart() throws SQLException {
+ return delegate.nullsAreSortedAtStart();
+ }
+
+ @Override
+ public boolean nullsAreSortedAtEnd() throws SQLException {
+ return delegate.nullsAreSortedAtEnd();
+ }
+
+ @Override
+ public String getDatabaseProductName() throws SQLException {
+ return delegate.getDatabaseProductName();
+ }
+
+ @Override
+ public String getDatabaseProductVersion() throws SQLException {
+ return delegate.getDatabaseProductVersion();
+ }
+
+ @Override
+ public String getDriverName() throws SQLException {
+ return delegate.getDriverName();
+ }
+
+ @Override
+ public String getDriverVersion() throws SQLException {
+ return delegate.getDriverVersion();
+ }
+
+ @Override
+ public int getDriverMajorVersion() {
+ return delegate.getDriverMajorVersion();
+ }
+
+ @Override
+ public int getDriverMinorVersion() {
+ return delegate.getDriverMinorVersion();
+ }
+
+ @Override
+ public boolean usesLocalFiles() throws SQLException {
+ return delegate.usesLocalFiles();
+ }
+
+ @Override
+ public boolean usesLocalFilePerTable() throws SQLException {
+ return delegate.usesLocalFilePerTable();
+ }
+
+ @Override
+ public boolean supportsMixedCaseIdentifiers() throws SQLException {
+ return delegate.supportsMixedCaseIdentifiers();
+ }
+
+ @Override
+ public boolean storesUpperCaseIdentifiers() throws SQLException {
+ return delegate.storesUpperCaseIdentifiers();
+ }
+
+ @Override
+ public boolean storesLowerCaseIdentifiers() throws SQLException {
+ return delegate.storesLowerCaseIdentifiers();
+ }
+
+ @Override
+ public boolean storesMixedCaseIdentifiers() throws SQLException {
+ return delegate.supportsMixedCaseIdentifiers();
+ }
+
+ @Override
+ public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException {
+ return delegate.supportsMixedCaseQuotedIdentifiers();
+ }
+
+ @Override
+ public boolean storesUpperCaseQuotedIdentifiers() throws SQLException {
+ return delegate.storesUpperCaseQuotedIdentifiers();
+ }
+
+ @Override
+ public boolean storesLowerCaseQuotedIdentifiers() throws SQLException {
+ return delegate.storesLowerCaseQuotedIdentifiers();
+ }
+
+ @Override
+ public boolean storesMixedCaseQuotedIdentifiers() throws SQLException {
+ return delegate.storesMixedCaseQuotedIdentifiers();
+ }
+
+ @Override
+ public String getIdentifierQuoteString() throws SQLException {
+ return delegate.getIdentifierQuoteString();
+ }
+
+ @Override
+ public String getSQLKeywords() throws SQLException {
+ return delegate.getSQLKeywords();
+ }
+
+ @Override
+ public String getNumericFunctions() throws SQLException {
+ return delegate.getNumericFunctions();
+ }
+
+ @Override
+ public String getStringFunctions() throws SQLException {
+ return delegate.getStringFunctions();
+ }
+
+ @Override
+ public String getSystemFunctions() throws SQLException {
+ return delegate.getSystemFunctions();
+ }
+
+ @Override
+ public String getTimeDateFunctions() throws SQLException {
+ return delegate.getTimeDateFunctions();
+ }
+
+ @Override
+ public String getSearchStringEscape() throws SQLException {
+ return delegate.getSearchStringEscape();
+ }
+
+ @Override
+ public String getExtraNameCharacters() throws SQLException {
+ return delegate.getExtraNameCharacters();
+ }
+
+ @Override
+ public boolean supportsAlterTableWithAddColumn() throws SQLException {
+ return delegate.supportsAlterTableWithAddColumn();
+ }
+
+ @Override
+ public boolean supportsAlterTableWithDropColumn() throws SQLException {
+ return delegate.supportsAlterTableWithDropColumn();
+ }
+
+ @Override
+ public boolean supportsColumnAliasing() throws SQLException {
+ return delegate.supportsColumnAliasing();
+ }
+
+ @Override
+ public boolean nullPlusNonNullIsNull() throws SQLException {
+ return delegate.nullPlusNonNullIsNull();
+ }
+
+ @Override
+ public boolean supportsConvert() throws SQLException {
+ return delegate.supportsConvert();
+ }
+
+ @Override
+ public boolean supportsConvert(int fromType, int toType) throws SQLException {
+ return delegate.supportsConvert(fromType, toType);
+ }
+
+ @Override
+ public boolean supportsTableCorrelationNames() throws SQLException {
+ return delegate.supportsTableCorrelationNames();
+ }
+
+ @Override
+ public boolean supportsDifferentTableCorrelationNames() throws SQLException {
+ return delegate.supportsDifferentTableCorrelationNames();
+ }
+
+ @Override
+ public boolean supportsExpressionsInOrderBy() throws SQLException {
+ return delegate.supportsExpressionsInOrderBy();
+ }
+
+ @Override
+ public boolean supportsOrderByUnrelated() throws SQLException {
+ return delegate.supportsOrderByUnrelated();
+ }
+
+ @Override
+ public boolean supportsGroupBy() throws SQLException {
+ return delegate.supportsGroupBy();
+ }
+
+ @Override
+ public boolean supportsGroupByUnrelated() throws SQLException {
+ return delegate.supportsGroupByUnrelated();
+ }
+
+ @Override
+ public boolean supportsGroupByBeyondSelect() throws SQLException {
+ return delegate.supportsGroupByBeyondSelect();
+ }
+
+ @Override
+ public boolean supportsLikeEscapeClause() throws SQLException {
+ return delegate.supportsLikeEscapeClause();
+ }
+
+ @Override
+ public boolean supportsMultipleResultSets() throws SQLException {
+ return delegate.supportsMultipleResultSets();
+ }
+
+ @Override
+ public boolean supportsMultipleTransactions() throws SQLException {
+ return delegate.supportsMultipleTransactions();
+ }
+
+ @Override
+ public boolean supportsNonNullableColumns() throws SQLException {
+ return delegate.supportsNonNullableColumns();
+ }
+
+ @Override
+ public boolean supportsMinimumSQLGrammar() throws SQLException {
+ return delegate.supportsMinimumSQLGrammar();
+ }
+
+ @Override
+ public boolean supportsCoreSQLGrammar() throws SQLException {
+ return delegate.supportsCoreSQLGrammar();
+ }
+
+ @Override
+ public boolean supportsExtendedSQLGrammar() throws SQLException {
+ return delegate.supportsExtendedSQLGrammar();
+ }
+
+ @Override
+ public boolean supportsANSI92EntryLevelSQL() throws SQLException {
+ return delegate.supportsANSI92EntryLevelSQL();
+ }
+
+ @Override
+ public boolean supportsANSI92IntermediateSQL() throws SQLException {
+ return delegate.supportsANSI92IntermediateSQL();
+ }
+
+ @Override
+ public boolean supportsANSI92FullSQL() throws SQLException {
+ return delegate.supportsANSI92FullSQL();
+ }
+
+ @Override
+ public boolean supportsIntegrityEnhancementFacility() throws SQLException {
+ return delegate.supportsIntegrityEnhancementFacility();
+ }
+
+ @Override
+ public boolean supportsOuterJoins() throws SQLException {
+ return delegate.supportsOuterJoins();
+ }
+
+ @Override
+ public boolean supportsFullOuterJoins() throws SQLException {
+ return delegate.supportsFullOuterJoins();
+ }
+
+ @Override
+ public boolean supportsLimitedOuterJoins() throws SQLException {
+ return delegate.supportsLimitedOuterJoins();
+ }
+
+ @Override
+ public String getSchemaTerm() throws SQLException {
+ return delegate.getSchemaTerm();
+ }
+
+ @Override
+ public String getProcedureTerm() throws SQLException {
+ return delegate.getProcedureTerm();
+ }
+
+ @Override
+ public String getCatalogTerm() throws SQLException {
+ return delegate.getCatalogTerm();
+ }
+
+ @Override
+ public boolean isCatalogAtStart() throws SQLException {
+ return delegate.isCatalogAtStart();
+ }
+
+ @Override
+ public String getCatalogSeparator() throws SQLException {
+ return delegate.getCatalogSeparator();
+ }
+
+ @Override
+ public boolean supportsSchemasInDataManipulation() throws SQLException {
+ return delegate.supportsSchemasInDataManipulation();
+ }
+
+ @Override
+ public boolean supportsSchemasInProcedureCalls() throws SQLException {
+ return delegate.supportsSchemasInProcedureCalls();
+ }
+
+ @Override
+ public boolean supportsSchemasInTableDefinitions() throws SQLException {
+ return delegate.supportsSchemasInTableDefinitions();
+ }
+
+ @Override
+ public boolean supportsSchemasInIndexDefinitions() throws SQLException {
+ return delegate.supportsSchemasInIndexDefinitions();
+ }
+
+ @Override
+ public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException {
+ return delegate.supportsSchemasInPrivilegeDefinitions();
+ }
+
+ @Override
+ public boolean supportsCatalogsInDataManipulation() throws SQLException {
+ return delegate.supportsCatalogsInDataManipulation();
+ }
+
+ @Override
+ public boolean supportsCatalogsInProcedureCalls() throws SQLException {
+ return delegate.supportsCatalogsInProcedureCalls();
+ }
+
+ @Override
+ public boolean supportsCatalogsInTableDefinitions() throws SQLException {
+ return delegate.supportsCatalogsInTableDefinitions();
+ }
+
+ @Override
+ public boolean supportsCatalogsInIndexDefinitions() throws SQLException {
+ return delegate.supportsCatalogsInIndexDefinitions();
+ }
+
+ @Override
+ public boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException {
+ return delegate.supportsCatalogsInPrivilegeDefinitions();
+ }
+
+ @Override
+ public boolean supportsPositionedDelete() throws SQLException {
+ return delegate.supportsPositionedDelete();
+ }
+
+ @Override
+ public boolean supportsPositionedUpdate() throws SQLException {
+ return delegate.supportsPositionedUpdate();
+ }
+
+ @Override
+ public boolean supportsSelectForUpdate() throws SQLException {
+ return delegate.supportsSelectForUpdate();
+ }
+
+ @Override
+ public boolean supportsStoredProcedures() throws SQLException {
+ return delegate.supportsStoredProcedures();
+ }
+
+ @Override
+ public boolean supportsSubqueriesInComparisons() throws SQLException {
+ return delegate.supportsSubqueriesInComparisons();
+ }
+
+ @Override
+ public boolean supportsSubqueriesInExists() throws SQLException {
+ return delegate.supportsSubqueriesInExists();
+ }
+
+ @Override
+ public boolean supportsSubqueriesInIns() throws SQLException {
+ return delegate.supportsSubqueriesInIns();
+ }
+
+ @Override
+ public boolean supportsSubqueriesInQuantifieds() throws SQLException {
+ return delegate.supportsSubqueriesInQuantifieds();
+ }
+
+ @Override
+ public boolean supportsCorrelatedSubqueries() throws SQLException {
+ return delegate.supportsCorrelatedSubqueries();
+ }
+
+ @Override
+ public boolean supportsUnion() throws SQLException {
+ return delegate.supportsUnion();
+ }
+
+ @Override
+ public boolean supportsUnionAll() throws SQLException {
+ return delegate.supportsUnionAll();
+ }
+
+ @Override
+ public boolean supportsOpenCursorsAcrossCommit() throws SQLException {
+ return delegate.supportsOpenCursorsAcrossCommit();
+ }
+
+ @Override
+ public boolean supportsOpenCursorsAcrossRollback() throws SQLException {
+ return delegate.supportsOpenCursorsAcrossRollback();
+ }
+
+ @Override
+ public boolean supportsOpenStatementsAcrossCommit() throws SQLException {
+ return delegate.supportsOpenStatementsAcrossCommit();
+ }
+
+ @Override
+ public boolean supportsOpenStatementsAcrossRollback() throws SQLException {
+ return delegate.supportsOpenStatementsAcrossRollback();
+ }
+
+ @Override
+ public int getMaxBinaryLiteralLength() throws SQLException {
+ return delegate.getMaxBinaryLiteralLength();
+ }
+
+ @Override
+ public int getMaxCharLiteralLength() throws SQLException {
+ return delegate.getMaxCharLiteralLength();
+ }
+
+ @Override
+ public int getMaxColumnNameLength() throws SQLException {
+ return delegate.getMaxColumnNameLength();
+ }
+
+ @Override
+ public int getMaxColumnsInGroupBy() throws SQLException {
+ return delegate.getMaxColumnsInGroupBy();
+ }
+
+ @Override
+ public int getMaxColumnsInIndex() throws SQLException {
+ return delegate.getMaxColumnsInIndex();
+ }
+
+ @Override
+ public int getMaxColumnsInOrderBy() throws SQLException {
+ return delegate.getMaxColumnsInOrderBy();
+ }
+
+ @Override
+ public int getMaxColumnsInSelect() throws SQLException {
+ return delegate.getMaxColumnsInSelect();
+ }
+
+ @Override
+ public int getMaxColumnsInTable() throws SQLException {
+ return delegate.getMaxColumnsInTable();
+ }
+
+ @Override
+ public int getMaxConnections() throws SQLException {
+ return delegate.getMaxConnections();
+ }
+
+ @Override
+ public int getMaxCursorNameLength() throws SQLException {
+ return delegate.getMaxCursorNameLength();
+ }
+
+ @Override
+ public int getMaxIndexLength() throws SQLException {
+ return delegate.getMaxIndexLength();
+ }
+
+ @Override
+ public int getMaxSchemaNameLength() throws SQLException {
+ return delegate.getMaxSchemaNameLength();
+ }
+
+ @Override
+ public int getMaxProcedureNameLength() throws SQLException {
+ return delegate.getMaxProcedureNameLength();
+ }
+
+ @Override
+ public int getMaxCatalogNameLength() throws SQLException {
+ return delegate.getMaxCatalogNameLength();
+ }
+
+ @Override
+ public int getMaxRowSize() throws SQLException {
+ return delegate.getMaxRowSize();
+ }
+
+ @Override
+ public boolean doesMaxRowSizeIncludeBlobs() throws SQLException {
+ return delegate.doesMaxRowSizeIncludeBlobs();
+ }
+
+ @Override
+ public int getMaxStatementLength() throws SQLException {
+ return delegate.getMaxStatementLength();
+ }
+
+ @Override
+ public int getMaxStatements() throws SQLException {
+ return delegate.getMaxStatements();
+ }
+
+ @Override
+ public int getMaxTableNameLength() throws SQLException {
+ return delegate.getMaxTableNameLength();
+ }
+
+ @Override
+ public int getMaxTablesInSelect() throws SQLException {
+ return delegate.getMaxTablesInSelect();
+ }
+
+ @Override
+ public int getMaxUserNameLength() throws SQLException {
+ return delegate.getMaxUserNameLength();
+ }
+
+ @Override
+ public int getDefaultTransactionIsolation() throws SQLException {
+ return delegate.getDefaultTransactionIsolation();
+ }
+
+ @Override
+ public boolean supportsTransactions() throws SQLException {
+ return delegate.supportsTransactions();
+ }
+
+ @Override
+ public boolean supportsTransactionIsolationLevel(int level) throws SQLException {
+ return delegate.supportsTransactionIsolationLevel(level);
+ }
+
+ @Override
+ public boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException {
+ return delegate.supportsDataDefinitionAndDataManipulationTransactions();
+ }
+
+ @Override
+ public boolean supportsDataManipulationTransactionsOnly() throws SQLException {
+ return delegate.supportsDataManipulationTransactionsOnly();
+ }
+
+ @Override
+ public boolean dataDefinitionCausesTransactionCommit() throws SQLException {
+ return delegate.dataDefinitionCausesTransactionCommit();
+ }
+
+ @Override
+ public boolean dataDefinitionIgnoredInTransactions() throws SQLException {
+ return delegate.dataDefinitionIgnoredInTransactions();
+ }
+
+ @Override
+ public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException {
+ ResultSet resultSet = delegate.getProcedures(catalog, schemaPattern, procedureNamePattern);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException {
+ ResultSet resultSet = delegate.getProcedureColumns(catalog, schemaPattern, procedureNamePattern, columnNamePattern);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) throws SQLException {
+ ResultSet resultSet = delegate.getTables(catalog, schemaPattern, tableNamePattern, types);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getSchemas() throws SQLException {
+ ResultSet resultSet = delegate.getSchemas();
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getCatalogs() throws SQLException {
+ ResultSet resultSet = delegate.getCatalogs();
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getTableTypes() throws SQLException {
+ ResultSet resultSet = delegate.getTableTypes();
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException {
+ ResultSet resultSet = delegate.getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException {
+ ResultSet resultSet = delegate.getColumnPrivileges(catalog, schema, table, columnNamePattern);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException {
+ ResultSet resultSet = delegate.getTablePrivileges(catalog, schemaPattern, tableNamePattern);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException {
+ ResultSet resultSet = delegate.getBestRowIdentifier(catalog, schema, table, scope, nullable);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException {
+ ResultSet resultSet = delegate.getVersionColumns(catalog, schema, table);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
+ ResultSet resultSet = delegate.getPrimaryKeys(catalog, schema, table);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException {
+ ResultSet resultSet = delegate.getImportedKeys(catalog, schema, table);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException {
+ ResultSet resultSet = delegate.getExportedKeys(catalog, schema, table);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) throws SQLException {
+ ResultSet resultSet = delegate.getCrossReference(parentCatalog, parentSchema, parentTable, foreignCatalog, foreignSchema, foreignTable);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getTypeInfo() throws SQLException {
+ ResultSet resultSet = delegate.getTypeInfo();
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException {
+ ResultSet resultSet = delegate.getIndexInfo(catalog, schema, table, unique, approximate);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public boolean supportsResultSetType(int type) throws SQLException {
+ return delegate.supportsResultSetType(type);
+ }
+
+ @Override
+ public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException {
+ return delegate.supportsResultSetConcurrency(type, concurrency);
+ }
+
+ @Override
+ public boolean ownUpdatesAreVisible(int type) throws SQLException {
+ return delegate.ownUpdatesAreVisible(type);
+ }
+
+ @Override
+ public boolean ownDeletesAreVisible(int type) throws SQLException {
+ return delegate.ownDeletesAreVisible(type);
+ }
+
+ @Override
+ public boolean ownInsertsAreVisible(int type) throws SQLException {
+ return delegate.ownInsertsAreVisible(type);
+ }
+
+ @Override
+ public boolean othersUpdatesAreVisible(int type) throws SQLException {
+ return delegate.othersUpdatesAreVisible(type);
+ }
+
+ @Override
+ public boolean othersDeletesAreVisible(int type) throws SQLException {
+ return delegate.othersDeletesAreVisible(type);
+ }
+
+ @Override
+ public boolean othersInsertsAreVisible(int type) throws SQLException {
+ return delegate.othersInsertsAreVisible(type);
+ }
+
+ @Override
+ public boolean updatesAreDetected(int type) throws SQLException {
+ return delegate.updatesAreDetected(type);
+ }
+
+ @Override
+ public boolean deletesAreDetected(int type) throws SQLException {
+ return delegate.deletesAreDetected(type);
+ }
+
+ @Override
+ public boolean insertsAreDetected(int type) throws SQLException {
+ return delegate.insertsAreDetected(type);
+ }
+
+ @Override
+ public boolean supportsBatchUpdates() throws SQLException {
+ return delegate.supportsBatchUpdates();
+ }
+
+ @Override
+ public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException {
+ ResultSet resultSet = delegate.getUDTs(catalog, schemaPattern, typeNamePattern, types);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException {
+ ResultSet resultSet = delegate.getSuperTypes(catalog, schemaPattern, typeNamePattern);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException {
+ ResultSet resultSet = delegate.getSuperTables(catalog, schemaPattern, tableNamePattern);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) throws SQLException {
+ ResultSet resultSet = delegate.getAttributes(catalog, schemaPattern, typeNamePattern, attributeNamePattern);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public boolean supportsResultSetHoldability(int holdability) throws SQLException {
+ return delegate.supportsResultSetHoldability(holdability);
+ }
+
+ @Override
+ public int getResultSetHoldability() throws SQLException {
+ return delegate.getResultSetHoldability();
+ }
+
+ @Override
+ public int getDatabaseMajorVersion() throws SQLException {
+ return delegate.getDatabaseMajorVersion();
+ }
+
+ @Override
+ public int getDatabaseMinorVersion() throws SQLException {
+ return delegate.getDatabaseMinorVersion();
+ }
+
+ @Override
+ public int getJDBCMajorVersion() throws SQLException {
+ return delegate.getJDBCMajorVersion();
+ }
+
+ @Override
+ public int getJDBCMinorVersion() throws SQLException {
+ return delegate.getJDBCMinorVersion();
+ }
+
+ @Override
+ public int getSQLStateType() throws SQLException {
+ return delegate.getSQLStateType();
+ }
+
+ @Override
+ public boolean locatorsUpdateCopy() throws SQLException {
+ return delegate.locatorsUpdateCopy();
+ }
+
+ @Override
+ public boolean supportsStatementPooling() throws SQLException {
+ return delegate.supportsStatementPooling();
+ }
+
+ @Override
+ public RowIdLifetime getRowIdLifetime() throws SQLException {
+ return delegate.getRowIdLifetime();
+ }
+
+ @Override
+ public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException {
+ ResultSet resultSet = delegate.getSchemas(catalog, schemaPattern);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException {
+ return delegate.supportsStoredFunctionsUsingCallSyntax();
+ }
+
+ @Override
+ public boolean autoCommitFailureClosesAllResultSets() throws SQLException {
+ return delegate.autoCommitFailureClosesAllResultSets();
+ }
+
+ @Override
+ public ResultSet getClientInfoProperties() throws SQLException {
+ ResultSet resultSet = delegate.getClientInfoProperties();
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException {
+ ResultSet resultSet = delegate.getFunctions(catalog, schemaPattern, functionNamePattern);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) throws SQLException {
+ ResultSet resultSet = delegate.getFunctionColumns(catalog, schemaPattern, functionNamePattern, columnNamePattern);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException {
+ ResultSet resultSet = delegate.getPseudoColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern);
+ Statement statement = resultSet.getStatement();
+ if (statement != null) {
+ statement = ProxyFactory.getProxyStatement(connection, statement);
+ }
+ return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet);
+ }
+
+ @Override
+ public boolean generatedKeyAlwaysReturned() throws SQLException {
+ return delegate.generatedKeyAlwaysReturned();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public T unwrap(Class iface) throws SQLException {
+ if (iface.isInstance(delegate)) {
+ return (T) delegate;
+ } else if (delegate != null) {
+ return delegate.unwrap(iface);
+ }
+ throw new SQLException("Wrapped DatabaseMetaData is not an instance of " + iface);
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) throws SQLException {
+ return delegate.isWrapperFor(iface);
+ }
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyFactory.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyFactory.java
new file mode 100644
index 0000000..c09325f
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyFactory.java
@@ -0,0 +1,68 @@
+package org.xbib.jdbc.connection.pool;
+
+import org.xbib.jdbc.connection.pool.util.FastList;
+import java.sql.CallableStatement;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.Statement;
+
+/**
+ * A factory class that produces proxies around instances of the standard JDBC interfaces.
+ */
+@SuppressWarnings("unused")
+public class ProxyFactory {
+
+ private ProxyFactory() {
+ // unconstructable
+ }
+
+ /**
+ * Create a proxy for the specified {@link Connection} instance.
+ *
+ * @param poolEntry the PoolEntry holding pool state
+ * @param connection the raw database Connection
+ * @param openStatements a reusable list to track open Statement instances
+ * @param leakTask the ProxyLeakTask for this connection
+ * @param now the current timestamp
+ * @param isReadOnly the default readOnly state of the connection
+ * @param isAutoCommit the default autoCommit state of the connection
+ * @return a proxy that wraps the specified {@link Connection}
+ */
+ public static Connection getProxyConnection(PoolEntry poolEntry,
+ Connection connection,
+ FastList openStatements,
+ ProxyLeakTask leakTask,
+ long now,
+ boolean isReadOnly,
+ boolean isAutoCommit) {
+ return new ProxyConnection(poolEntry, connection, openStatements, leakTask, now, isReadOnly, isAutoCommit);
+ }
+
+ public static Statement getProxyStatement(ProxyConnection connection,
+ Statement statement) {
+ return new ProxyStatement(connection, statement);
+ }
+
+ public static CallableStatement getProxyCallableStatement(ProxyConnection connection,
+ CallableStatement statement) {
+ return new ProxyCallableStatement(connection, statement);
+ }
+
+ public static PreparedStatement getProxyPreparedStatement(ProxyConnection connection,
+ PreparedStatement statement) {
+ return new ProxyPreparedStatement(connection, statement);
+ }
+
+ public static ResultSet getProxyResultSet(ProxyConnection connection,
+ ProxyStatement statement,
+ ResultSet resultSet) {
+ return new ProxyResultSet(connection, statement, resultSet);
+ }
+
+ public static DatabaseMetaData getProxyDatabaseMetaData(ProxyConnection connection,
+ DatabaseMetaData metaData) {
+ return new ProxyDatabaseMetaData(connection, metaData);
+ }
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTask.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTask.java
new file mode 100644
index 0000000..0ae99ac
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTask.java
@@ -0,0 +1,77 @@
+package org.xbib.jdbc.connection.pool;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A Runnable that is scheduled in the future to report leaks.
+ * The ScheduledFuture is cancelled if the connection is closed before the leak time expires.
+ */
+public class ProxyLeakTask implements Runnable {
+
+ private static final Logger logger = Logger.getLogger(ProxyLeakTask.class.getName());
+
+ public static final ProxyLeakTask NO_LEAK;
+
+ private ScheduledFuture> scheduledFuture;
+
+ private String connectionName;
+
+ private Exception exception;
+
+ private String threadName;
+
+ private boolean isLeaked;
+
+ static {
+ NO_LEAK = new ProxyLeakTask() {
+ @Override
+ public void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold) {
+ }
+
+ @Override
+ public void run() {
+ }
+
+ @Override
+ public void cancel() {
+ }
+ };
+ }
+
+ public ProxyLeakTask(final PoolEntry poolEntry) {
+ this.exception = new Exception("Apparent connection leak detected");
+ this.threadName = Thread.currentThread().getName();
+ this.connectionName = poolEntry.getConnection().toString();
+ }
+
+ private ProxyLeakTask() {
+ }
+
+ public void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold) {
+ scheduledFuture = executorService.schedule(this, leakDetectionThreshold, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run() {
+ isLeaked = true;
+ final StackTraceElement[] stackTrace = exception.getStackTrace();
+ final StackTraceElement[] trace = new StackTraceElement[stackTrace.length - 5];
+ System.arraycopy(stackTrace, 5, trace, 0, trace.length);
+ exception.setStackTrace(trace);
+ logger.log(Level.WARNING, "Connection leak detection triggered for on thread, stack trace follows: " + connectionName + " " + threadName, exception);
+ }
+
+ public void cancel() {
+ scheduledFuture.cancel(false);
+ if (isLeaked) {
+ logger.log(Level.INFO, "Previously reported leaked connection on thread was returned to the pool (unleaked: )" + connectionName + " " + threadName);
+ }
+ }
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTaskFactory.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTaskFactory.java
new file mode 100644
index 0000000..b89c410
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTaskFactory.java
@@ -0,0 +1,32 @@
+package org.xbib.jdbc.connection.pool;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * A factory for {@link ProxyLeakTask} Runnables that are scheduled in the future to report leaks.
+ */
+public class ProxyLeakTaskFactory {
+
+ private final ScheduledExecutorService executorService;
+
+ private long leakDetectionThreshold;
+
+ public ProxyLeakTaskFactory(final long leakDetectionThreshold, final ScheduledExecutorService executorService) {
+ this.executorService = executorService;
+ this.leakDetectionThreshold = leakDetectionThreshold;
+ }
+
+ public ProxyLeakTask schedule(final PoolEntry poolEntry) {
+ return (leakDetectionThreshold == 0) ? ProxyLeakTask.NO_LEAK : scheduleNewTask(poolEntry);
+ }
+
+ public void updateLeakDetectionThreshold(final long leakDetectionThreshold) {
+ this.leakDetectionThreshold = leakDetectionThreshold;
+ }
+
+ private ProxyLeakTask scheduleNewTask(PoolEntry poolEntry) {
+ ProxyLeakTask task = new ProxyLeakTask(poolEntry);
+ task.schedule(executorService, leakDetectionThreshold);
+ return task;
+ }
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyPreparedStatement.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyPreparedStatement.java
new file mode 100644
index 0000000..bf77159
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyPreparedStatement.java
@@ -0,0 +1,329 @@
+package org.xbib.jdbc.connection.pool;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.net.URL;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.Clob;
+import java.sql.Date;
+import java.sql.NClob;
+import java.sql.ParameterMetaData;
+import java.sql.PreparedStatement;
+import java.sql.Ref;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.RowId;
+import java.sql.SQLException;
+import java.sql.SQLXML;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+
+/**
+ * This is the proxy class for java.sql.PreparedStatement.
+ */
+public class ProxyPreparedStatement extends ProxyStatement implements PreparedStatement {
+
+ public ProxyPreparedStatement(ProxyConnection connection, PreparedStatement statement) {
+ super(connection, statement);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean execute() throws SQLException {
+ connection.markCommitStateDirty();
+ return ((PreparedStatement) delegate).execute();
+ }
+
+ @Override
+ public void addBatch() throws SQLException {
+ ((PreparedStatement) delegate).addBatch();
+ }
+
+ @Override
+ public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException {
+ ((PreparedStatement) delegate).setCharacterStream(parameterIndex, reader, length);
+ }
+
+ @Override
+ public void setRef(int parameterIndex, Ref x) throws SQLException {
+ ((PreparedStatement) delegate).setRef(parameterIndex, x);
+ }
+
+ @Override
+ public void setBlob(int parameterIndex, Blob x) throws SQLException {
+ ((PreparedStatement) delegate).setBlob(parameterIndex, x);
+ }
+
+ @Override
+ public void setClob(int parameterIndex, Clob x) throws SQLException {
+ ((PreparedStatement) delegate).setClob(parameterIndex, x);
+ }
+
+ @Override
+ public void setArray(int parameterIndex, Array x) throws SQLException {
+ ((PreparedStatement) delegate).setArray(parameterIndex, x);
+ }
+
+ @Override
+ public ResultSetMetaData getMetaData() throws SQLException {
+ return ((PreparedStatement) delegate).getMetaData();
+ }
+
+ @Override
+ public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {
+ ((PreparedStatement) delegate).setDate(parameterIndex, x);
+ }
+
+ @Override
+ public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {
+ ((PreparedStatement) delegate).setTime(parameterIndex, x);
+ }
+
+ @Override
+ public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {
+ ((PreparedStatement) delegate).setTimestamp(parameterIndex, x, cal);
+ }
+
+ @Override
+ public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException {
+ ((PreparedStatement) delegate).setNull(parameterIndex, sqlType, typeName);
+ }
+
+ @Override
+ public void setURL(int parameterIndex, URL x) throws SQLException {
+ ((PreparedStatement) delegate).setURL(parameterIndex, x);
+ }
+
+ @Override
+ public ParameterMetaData getParameterMetaData() throws SQLException {
+ return ((PreparedStatement) delegate).getParameterMetaData();
+ }
+
+ @Override
+ public void setRowId(int parameterIndex, RowId x) throws SQLException {
+ ((PreparedStatement) delegate).setRowId(parameterIndex, x);
+ }
+
+ @Override
+ public void setNString(int parameterIndex, String value) throws SQLException {
+ ((PreparedStatement) delegate).setNString(parameterIndex, value);
+ }
+
+ @Override
+ public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException {
+ ((PreparedStatement) delegate).setNCharacterStream(parameterIndex, value, length);
+ }
+
+ @Override
+ public void setNClob(int parameterIndex, NClob value) throws SQLException {
+ ((PreparedStatement) delegate).setNClob(parameterIndex, value);
+ }
+
+ @Override
+ public void setClob(int parameterIndex, Reader reader, long length) throws SQLException {
+ ((PreparedStatement) delegate).setClob(parameterIndex, reader, length);
+ }
+
+ @Override
+ public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException {
+ ((PreparedStatement) delegate).setBlob(parameterIndex, inputStream, length);
+ }
+
+ @Override
+ public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {
+ ((PreparedStatement) delegate).setNClob(parameterIndex, reader, length);
+ }
+
+ @Override
+ public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {
+ ((PreparedStatement) delegate).setSQLXML(parameterIndex, xmlObject);
+ }
+
+ @Override
+ public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException {
+ ((PreparedStatement) delegate).setObject(parameterIndex, x, targetSqlType, scaleOrLength);
+ }
+
+ @Override
+ public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException {
+ ((PreparedStatement) delegate).setAsciiStream(parameterIndex, x, length);
+ }
+
+ @Override
+ public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException {
+ ((PreparedStatement) delegate).setBinaryStream(parameterIndex, x, length);
+ }
+
+ @Override
+ public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException {
+ ((PreparedStatement) delegate).setCharacterStream(parameterIndex, reader, length);
+ }
+
+ @Override
+ public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException {
+ ((PreparedStatement) delegate).setAsciiStream(parameterIndex, x);
+ }
+
+ @Override
+ public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException {
+ ((PreparedStatement) delegate).setBinaryStream(parameterIndex, x);
+ }
+
+ @Override
+ public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException {
+ ((PreparedStatement) delegate).setCharacterStream(parameterIndex, reader);
+ }
+
+ @Override
+ public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException {
+ ((PreparedStatement) delegate).setNCharacterStream(parameterIndex, value);
+ }
+
+ @Override
+ public void setClob(int parameterIndex, Reader reader) throws SQLException {
+ ((PreparedStatement) delegate).setClob(parameterIndex, reader);
+ }
+
+ @Override
+ public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException {
+ ((PreparedStatement) delegate).setBlob(parameterIndex, inputStream);
+ }
+
+ @Override
+ public void setNClob(int parameterIndex, Reader reader) throws SQLException {
+ ((PreparedStatement) delegate).setNClob(parameterIndex, reader);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ResultSet executeQuery() throws SQLException {
+ connection.markCommitStateDirty();
+ ResultSet resultSet = ((PreparedStatement) getDelegate()).executeQuery();
+ return ProxyFactory.getProxyResultSet(connection, this, resultSet);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int executeUpdate() throws SQLException {
+ connection.markCommitStateDirty();
+ return ((PreparedStatement) getDelegate()).executeUpdate();
+ }
+
+ @Override
+ public void setNull(int parameterIndex, int sqlType) throws SQLException {
+ ((PreparedStatement) delegate).setNull(parameterIndex, sqlType);
+ }
+
+ @Override
+ public void setBoolean(int parameterIndex, boolean x) throws SQLException {
+ ((PreparedStatement) delegate).setBoolean(parameterIndex, x);
+ }
+
+ @Override
+ public void setByte(int parameterIndex, byte x) throws SQLException {
+ ((PreparedStatement) delegate).setByte(parameterIndex, x);
+ }
+
+ @Override
+ public void setShort(int parameterIndex, short x) throws SQLException {
+ ((PreparedStatement) delegate).setShort(parameterIndex, x);
+ }
+
+ @Override
+ public void setInt(int parameterIndex, int x) throws SQLException {
+ ((PreparedStatement) delegate).setInt(parameterIndex, x);
+ }
+
+ @Override
+ public void setLong(int parameterIndex, long x) throws SQLException {
+ ((PreparedStatement) delegate).setLong(parameterIndex, x);
+ }
+
+ @Override
+ public void setFloat(int parameterIndex, float x) throws SQLException {
+ ((PreparedStatement) delegate).setFloat(parameterIndex, x);
+ }
+
+ @Override
+ public void setDouble(int parameterIndex, double x) throws SQLException {
+ ((PreparedStatement) delegate).setDouble(parameterIndex, x);
+ }
+
+ @Override
+ public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException {
+ ((PreparedStatement) delegate).setBigDecimal(parameterIndex, x);
+ }
+
+ @Override
+ public void setString(int parameterIndex, String x) throws SQLException {
+ ((PreparedStatement) delegate).setString(parameterIndex, x);
+ }
+
+ @Override
+ public void setBytes(int parameterIndex, byte[] x) throws SQLException {
+ ((PreparedStatement) delegate).setBytes(parameterIndex, x);
+ }
+
+ @Override
+ public void setDate(int parameterIndex, Date x) throws SQLException {
+ ((PreparedStatement) delegate).setDate(parameterIndex, x);
+ }
+
+ @Override
+ public void setTime(int parameterIndex, Time x) throws SQLException {
+ ((PreparedStatement) delegate).setTime(parameterIndex, x);
+ }
+
+ @Override
+ public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
+ ((PreparedStatement) delegate).setTimestamp(parameterIndex, x);
+ }
+
+ @Override
+ public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException {
+ ((PreparedStatement) delegate).setAsciiStream(parameterIndex, x, length);
+ }
+
+ @Override
+ public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException {
+ ((PreparedStatement) delegate).setUnicodeStream(parameterIndex, x, length);
+ }
+
+ @Override
+ public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {
+ ((PreparedStatement) delegate).setBinaryStream(parameterIndex, x, length);
+ }
+
+ @Override
+ public void clearParameters() throws SQLException {
+ ((PreparedStatement) delegate).clearParameters();
+ }
+
+ @Override
+ public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException {
+ ((PreparedStatement) delegate).setObject(parameterIndex, x, targetSqlType);
+ }
+
+ @Override
+ public void setObject(int parameterIndex, Object x) throws SQLException {
+ ((PreparedStatement) delegate).setObject(parameterIndex, x);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long executeLargeUpdate() throws SQLException {
+ connection.markCommitStateDirty();
+ return ((PreparedStatement) getDelegate()).executeLargeUpdate();
+ }
+}
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
new file mode 100644
index 0000000..aaecb95
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyResultSet.java
@@ -0,0 +1,1033 @@
+package org.xbib.jdbc.connection.pool;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.net.URL;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.Clob;
+import java.sql.Date;
+import java.sql.NClob;
+import java.sql.Ref;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.RowId;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.SQLXML;
+import java.sql.Statement;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Map;
+
+/**
+ * This is the proxy class for java.sql.ResultSet.
+ */
+public class ProxyResultSet implements ResultSet {
+
+ private final ProxyConnection connection;
+
+ private final ProxyStatement statement;
+
+ private final ResultSet delegate;
+
+ public ProxyResultSet(ProxyConnection connection, ProxyStatement statement, ResultSet resultSet) {
+ this.connection = connection;
+ this.statement = statement;
+ this.delegate = resultSet;
+ }
+
+ public ResultSet getDelegate() {
+ return delegate;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName() + '@' + System.identityHashCode(this) + " wrapping " + delegate;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Statement getStatement() throws SQLException {
+ return statement;
+ }
+
+ @Override
+ public Object getObject(int columnIndex, Map> map) throws SQLException {
+ return delegate.getObject(columnIndex, map);
+ }
+
+ @Override
+ public Ref getRef(int columnIndex) throws SQLException {
+ return delegate.getRef(columnIndex);
+ }
+
+ @Override
+ public Blob getBlob(int columnIndex) throws SQLException {
+ return delegate.getBlob(columnIndex);
+ }
+
+ @Override
+ public Clob getClob(int columnIndex) throws SQLException {
+ return delegate.getClob(columnIndex);
+ }
+
+ @Override
+ public Array getArray(int columnIndex) throws SQLException {
+ return delegate.getArray(columnIndex);
+ }
+
+ @Override
+ public Object getObject(String columnLabel, Map> map) throws SQLException {
+ return delegate.getObject(columnLabel, map);
+ }
+
+ @Override
+ public Ref getRef(String columnLabel) throws SQLException {
+ return delegate.getRef(columnLabel);
+ }
+
+ @Override
+ public Blob getBlob(String columnLabel) throws SQLException {
+ return delegate.getBlob(columnLabel);
+ }
+
+ @Override
+ public Clob getClob(String columnLabel) throws SQLException {
+ return delegate.getClob(columnLabel);
+ }
+
+ @Override
+ public Array getArray(String columnLabel) throws SQLException {
+ return delegate.getArray(columnLabel);
+ }
+
+ @Override
+ public Date getDate(int columnIndex, Calendar cal) throws SQLException {
+ return delegate.getDate(columnIndex, cal);
+ }
+
+ @Override
+ public Date getDate(String columnLabel, Calendar cal) throws SQLException {
+ return delegate.getDate(columnLabel, cal);
+ }
+
+ @Override
+ public Time getTime(int columnIndex, Calendar cal) throws SQLException {
+ return delegate.getTime(columnIndex, cal);
+ }
+
+ @Override
+ public Time getTime(String columnLabel, Calendar cal) throws SQLException {
+ return delegate.getTime(columnLabel, cal);
+ }
+
+ @Override
+ public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException {
+ return delegate.getTimestamp(columnIndex, cal);
+ }
+
+ @Override
+ public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException {
+ return delegate.getTimestamp(columnLabel, cal);
+ }
+
+ @Override
+ public URL getURL(int columnIndex) throws SQLException {
+ return delegate.getURL(columnIndex);
+ }
+
+ @Override
+ public URL getURL(String columnLabel) throws SQLException {
+ return delegate.getURL(columnLabel);
+ }
+
+ @Override
+ public void updateRef(int columnIndex, Ref x) throws SQLException {
+ delegate.updateRef(columnIndex, x);
+ }
+
+ @Override
+ public void updateRef(String columnLabel, Ref x) throws SQLException {
+ delegate.updateRef(columnLabel, x);
+ }
+
+ @Override
+ public void updateBlob(int columnIndex, Blob x) throws SQLException {
+ delegate.updateBlob(columnIndex, x);
+ }
+
+ @Override
+ public void updateBlob(String columnLabel, Blob x) throws SQLException {
+ delegate.updateBlob(columnLabel, x);
+ }
+
+ @Override
+ public void updateClob(int columnIndex, Clob x) throws SQLException {
+ delegate.updateClob(columnIndex, x);
+ }
+
+ @Override
+ public void updateClob(String columnLabel, Clob x) throws SQLException {
+ delegate.updateClob(columnLabel, x);
+ }
+
+ @Override
+ public void updateArray(int columnIndex, Array x) throws SQLException {
+ delegate.updateArray(columnIndex, x);
+ }
+
+ @Override
+ public void updateArray(String columnLabel, Array x) throws SQLException {
+ delegate.updateArray(columnLabel, x);
+ }
+
+ @Override
+ public RowId getRowId(int columnIndex) throws SQLException {
+ return delegate.getRowId(columnIndex);
+ }
+
+ @Override
+ public RowId getRowId(String columnLabel) throws SQLException {
+ return delegate.getRowId(columnLabel);
+ }
+
+ @Override
+ public void updateRowId(int columnIndex, RowId x) throws SQLException {
+ delegate.updateRowId(columnIndex, x);
+ }
+
+ @Override
+ public void updateRowId(String columnLabel, RowId x) throws SQLException {
+ delegate.updateRowId(columnLabel, x);
+ }
+
+ @Override
+ public int getHoldability() throws SQLException {
+ return delegate.getHoldability();
+ }
+
+ @Override
+ public boolean isClosed() throws SQLException {
+ return delegate.isClosed();
+ }
+
+ @Override
+ public void updateNString(int columnIndex, String nString) throws SQLException {
+ delegate.updateNString(columnIndex, nString);
+ }
+
+ @Override
+ public void updateNString(String columnLabel, String nString) throws SQLException {
+ delegate.updateNString(columnLabel, nString);
+ }
+
+ @Override
+ public void updateNClob(int columnIndex, NClob nClob) throws SQLException {
+ delegate.updateNClob(columnIndex, nClob);
+ }
+
+ @Override
+ public void updateNClob(String columnLabel, NClob nClob) throws SQLException {
+ delegate.updateNClob(columnLabel, nClob);
+ }
+
+ @Override
+ public NClob getNClob(int columnIndex) throws SQLException {
+ return delegate.getNClob(columnIndex);
+ }
+
+ @Override
+ public NClob getNClob(String columnLabel) throws SQLException {
+ return delegate.getNClob(columnLabel);
+ }
+
+ @Override
+ public SQLXML getSQLXML(int columnIndex) throws SQLException {
+ return delegate.getSQLXML(columnIndex);
+ }
+
+ @Override
+ public SQLXML getSQLXML(String columnLabel) throws SQLException {
+ return delegate.getSQLXML(columnLabel);
+ }
+
+ @Override
+ public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException {
+ delegate.updateSQLXML(columnIndex, xmlObject);
+ }
+
+ @Override
+ public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException {
+ delegate.updateSQLXML(columnLabel, xmlObject);
+ }
+
+ @Override
+ public String getNString(int columnIndex) throws SQLException {
+ return delegate.getNString(columnIndex);
+ }
+
+ @Override
+ public String getNString(String columnLabel) throws SQLException {
+ return delegate.getNString(columnLabel);
+ }
+
+ @Override
+ public Reader getNCharacterStream(int columnIndex) throws SQLException {
+ return delegate.getNCharacterStream(columnIndex);
+ }
+
+ @Override
+ public Reader getNCharacterStream(String columnLabel) throws SQLException {
+ return delegate.getNCharacterStream(columnLabel);
+ }
+
+ @Override
+ public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException {
+ delegate.updateNCharacterStream(columnIndex, x, length);
+ }
+
+ @Override
+ public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException {
+ delegate.updateNCharacterStream(columnLabel, reader, length);
+ }
+
+ @Override
+ public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException {
+ delegate.updateAsciiStream(columnIndex, x, length);
+ }
+
+ @Override
+ public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException {
+ delegate.updateBinaryStream(columnIndex, x, length);
+ }
+
+ @Override
+ public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException {
+ delegate.updateCharacterStream(columnIndex, x, length);
+ }
+
+ @Override
+ public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException {
+ delegate.updateAsciiStream(columnLabel, x, length);
+ }
+
+ @Override
+ public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException {
+ delegate.updateBinaryStream(columnLabel, x, length);
+ }
+
+ @Override
+ public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException {
+ delegate.updateCharacterStream(columnLabel, reader, length);
+ }
+
+ @Override
+ public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException {
+ delegate.updateBlob(columnIndex, inputStream, length);
+ }
+
+ @Override
+ public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException {
+ delegate.updateBlob(columnLabel, inputStream, length);
+ }
+
+ @Override
+ public void updateClob(int columnIndex, Reader reader, long length) throws SQLException {
+ delegate.updateClob(columnIndex, reader, length);
+ }
+
+ @Override
+ public void updateClob(String columnLabel, Reader reader, long length) throws SQLException {
+ delegate.updateClob(columnLabel, reader, length);
+ }
+
+ @Override
+ public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException {
+ delegate.updateNClob(columnIndex, reader, length);
+ }
+
+ @Override
+ public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException {
+ delegate.updateNClob(columnLabel, reader, length);
+ }
+
+ @Override
+ public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException {
+ delegate.updateNCharacterStream(columnIndex, x);
+ }
+
+ @Override
+ public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException {
+ delegate.updateNCharacterStream(columnLabel, reader);
+ }
+
+ @Override
+ public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException {
+ delegate.updateAsciiStream(columnIndex, x);
+ }
+
+ @Override
+ public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException {
+ delegate.updateBinaryStream(columnIndex, x);
+ }
+
+ @Override
+ public void updateCharacterStream(int columnIndex, Reader x) throws SQLException {
+ delegate.updateCharacterStream(columnIndex, x);
+ }
+
+ @Override
+ public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException {
+ delegate.updateAsciiStream(columnLabel, x);
+ }
+
+ @Override
+ public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException {
+ delegate.updateBinaryStream(columnLabel, x);
+ }
+
+ @Override
+ public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException {
+ delegate.updateCharacterStream(columnLabel, reader);
+ }
+
+ @Override
+ public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException {
+ delegate.updateBlob(columnIndex, inputStream);
+ }
+
+ @Override
+ public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException {
+ delegate.updateBlob(columnLabel, inputStream);
+ }
+
+ @Override
+ public void updateClob(int columnIndex, Reader reader) throws SQLException {
+ delegate.updateClob(columnIndex, reader);
+ }
+
+ @Override
+ public void updateClob(String columnLabel, Reader reader) throws SQLException {
+ delegate.updateClob(columnLabel, reader);
+ }
+
+ @Override
+ public void updateNClob(int columnIndex, Reader reader) throws SQLException {
+ delegate.updateNClob(columnIndex, reader);
+ }
+
+ @Override
+ public void updateNClob(String columnLabel, Reader reader) throws SQLException {
+ delegate.updateNClob(columnLabel, reader);
+ }
+
+ @Override
+ public T getObject(int columnIndex, Class type) throws SQLException {
+ return delegate.getObject(columnIndex, type);
+ }
+
+ @Override
+ public T getObject(String columnLabel, Class type) throws SQLException {
+ return delegate.getObject(columnLabel, type);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void updateRow() throws SQLException {
+ connection.markCommitStateDirty();
+ delegate.updateRow();
+ }
+
+ @Override
+ public boolean next() throws SQLException {
+ return delegate.next();
+ }
+
+ @Override
+ public void close() throws SQLException {
+ delegate.close();
+ }
+
+ @Override
+ public boolean wasNull() throws SQLException {
+ return delegate.wasNull();
+ }
+
+ @Override
+ public String getString(int columnIndex) throws SQLException {
+ return delegate.getString(columnIndex);
+ }
+
+ @Override
+ public boolean getBoolean(int columnIndex) throws SQLException {
+ return delegate.getBoolean(columnIndex);
+ }
+
+ @Override
+ public byte getByte(int columnIndex) throws SQLException {
+ return delegate.getByte(0);
+ }
+
+ @Override
+ public short getShort(int columnIndex) throws SQLException {
+ return delegate.getShort(columnIndex);
+ }
+
+ @Override
+ public int getInt(int columnIndex) throws SQLException {
+ return delegate.getInt(columnIndex);
+ }
+
+ @Override
+ public long getLong(int columnIndex) throws SQLException {
+ return delegate.getInt(columnIndex);
+ }
+
+ @Override
+ public float getFloat(int columnIndex) throws SQLException {
+ return delegate.getFloat(columnIndex);
+ }
+
+ @Override
+ public double getDouble(int columnIndex) throws SQLException {
+ return delegate.getDouble(columnIndex);
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException {
+ return delegate.getBigDecimal(columnIndex);
+ }
+
+ @Override
+ public byte[] getBytes(int columnIndex) throws SQLException {
+ return delegate.getBytes(columnIndex);
+ }
+
+ @Override
+ public Date getDate(int columnIndex) throws SQLException {
+ return delegate.getDate(columnIndex);
+ }
+
+ @Override
+ public Time getTime(int columnIndex) throws SQLException {
+ return delegate.getTime(columnIndex);
+ }
+
+ @Override
+ public Timestamp getTimestamp(int columnIndex) throws SQLException {
+ return delegate.getTimestamp(columnIndex);
+ }
+
+ @Override
+ public InputStream getAsciiStream(int columnIndex) throws SQLException {
+ return delegate.getAsciiStream(columnIndex);
+ }
+
+ @Override
+ public InputStream getUnicodeStream(int columnIndex) throws SQLException {
+ return delegate.getUnicodeStream(columnIndex);
+ }
+
+ @Override
+ public InputStream getBinaryStream(int columnIndex) throws SQLException {
+ return delegate.getBinaryStream(columnIndex);
+ }
+
+ @Override
+ public String getString(String columnLabel) throws SQLException {
+ return delegate.getString(columnLabel);
+ }
+
+ @Override
+ public boolean getBoolean(String columnLabel) throws SQLException {
+ return delegate.getBoolean(columnLabel);
+ }
+
+ @Override
+ public byte getByte(String columnLabel) throws SQLException {
+ return delegate.getByte(columnLabel);
+ }
+
+ @Override
+ public short getShort(String columnLabel) throws SQLException {
+ return delegate.getShort(columnLabel);
+ }
+
+ @Override
+ public int getInt(String columnLabel) throws SQLException {
+ return delegate.getInt(columnLabel);
+ }
+
+ @Override
+ public long getLong(String columnLabel) throws SQLException {
+ return delegate.getLong(columnLabel);
+ }
+
+ @Override
+ public float getFloat(String columnLabel) throws SQLException {
+ return delegate.getFloat(columnLabel);
+ }
+
+ @Override
+ public double getDouble(String columnLabel) throws SQLException {
+ return delegate.getDouble(columnLabel);
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException {
+ return delegate.getBigDecimal(columnLabel, scale);
+ }
+
+ @Override
+ public byte[] getBytes(String columnLabel) throws SQLException {
+ return delegate.getBytes(columnLabel);
+ }
+
+ @Override
+ public Date getDate(String columnLabel) throws SQLException {
+ return delegate.getDate(columnLabel);
+ }
+
+ @Override
+ public Time getTime(String columnLabel) throws SQLException {
+ return delegate.getTime(columnLabel);
+ }
+
+ @Override
+ public Timestamp getTimestamp(String columnLabel) throws SQLException {
+ return delegate.getTimestamp(columnLabel);
+ }
+
+ @Override
+ public InputStream getAsciiStream(String columnLabel) throws SQLException {
+ return delegate.getAsciiStream(columnLabel);
+ }
+
+ @Override
+ public InputStream getUnicodeStream(String columnLabel) throws SQLException {
+ return delegate.getUnicodeStream(columnLabel);
+ }
+
+ @Override
+ public InputStream getBinaryStream(String columnLabel) throws SQLException {
+ return delegate.getBinaryStream(columnLabel);
+ }
+
+ @Override
+ public SQLWarning getWarnings() throws SQLException {
+ return delegate.getWarnings();
+ }
+
+ @Override
+ public void clearWarnings() throws SQLException {
+ delegate.clearWarnings();
+ }
+
+ @Override
+ public String getCursorName() throws SQLException {
+ return delegate.getCursorName();
+ }
+
+ @Override
+ public ResultSetMetaData getMetaData() throws SQLException {
+ return delegate.getMetaData();
+ }
+
+ @Override
+ public Object getObject(int columnIndex) throws SQLException {
+ return delegate.getObject(columnIndex);
+ }
+
+ @Override
+ public Object getObject(String columnLabel) throws SQLException {
+ return delegate.getObject(columnLabel);
+ }
+
+ @Override
+ public int findColumn(String columnLabel) throws SQLException {
+ return delegate.findColumn(columnLabel);
+ }
+
+ @Override
+ public Reader getCharacterStream(int columnIndex) throws SQLException {
+ return delegate.getCharacterStream(columnIndex);
+ }
+
+ @Override
+ public Reader getCharacterStream(String columnLabel) throws SQLException {
+ return delegate.getCharacterStream(columnLabel);
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(int columnIndex) throws SQLException {
+ return delegate.getBigDecimal(columnIndex);
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(String columnLabel) throws SQLException {
+ return delegate.getBigDecimal(columnLabel);
+ }
+
+ @Override
+ public boolean isBeforeFirst() throws SQLException {
+ return delegate.isBeforeFirst();
+ }
+
+ @Override
+ public boolean isAfterLast() throws SQLException {
+ return delegate.isAfterLast();
+ }
+
+ @Override
+ public boolean isFirst() throws SQLException {
+ return delegate.isFirst();
+ }
+
+ @Override
+ public boolean isLast() throws SQLException {
+ return delegate.isLast();
+ }
+
+ @Override
+ public void beforeFirst() throws SQLException {
+ delegate.beforeFirst();
+ }
+
+ @Override
+ public void afterLast() throws SQLException {
+ delegate.afterLast();
+ }
+
+ @Override
+ public boolean first() throws SQLException {
+ return delegate.first();
+ }
+
+ @Override
+ public boolean last() throws SQLException {
+ return delegate.last();
+ }
+
+ @Override
+ public int getRow() throws SQLException {
+ return delegate.getRow();
+ }
+
+ @Override
+ public boolean absolute(int row) throws SQLException {
+ return delegate.absolute(row);
+ }
+
+ @Override
+ public boolean relative(int rows) throws SQLException {
+ return delegate.relative(rows);
+ }
+
+ @Override
+ public boolean previous() throws SQLException {
+ return delegate.previous();
+ }
+
+ @Override
+ public void setFetchDirection(int direction) throws SQLException {
+ delegate.setFetchSize(direction);
+ }
+
+ @Override
+ public int getFetchDirection() throws SQLException {
+ return delegate.getFetchDirection();
+ }
+
+ @Override
+ public void setFetchSize(int rows) throws SQLException {
+ delegate.setFetchSize(rows);
+ }
+
+ @Override
+ public int getFetchSize() throws SQLException {
+ return delegate.getFetchSize();
+ }
+
+ @Override
+ public int getType() throws SQLException {
+ return delegate.getType();
+ }
+
+ @Override
+ public int getConcurrency() throws SQLException {
+ return delegate.getConcurrency();
+ }
+
+ @Override
+ public boolean rowUpdated() throws SQLException {
+ return delegate.rowUpdated();
+ }
+
+ @Override
+ public boolean rowInserted() throws SQLException {
+ return delegate.rowInserted();
+ }
+
+ @Override
+ public boolean rowDeleted() throws SQLException {
+ return delegate.rowDeleted();
+ }
+
+ @Override
+ public void updateNull(int columnIndex) throws SQLException {
+ delegate.updateNull(columnIndex);
+ }
+
+ @Override
+ public void updateBoolean(int columnIndex, boolean x) throws SQLException {
+ delegate.updateBoolean(columnIndex, x);
+ }
+
+ @Override
+ public void updateByte(int columnIndex, byte x) throws SQLException {
+ delegate.updateByte(columnIndex, x);
+ }
+
+ @Override
+ public void updateShort(int columnIndex, short x) throws SQLException {
+ delegate.updateShort(columnIndex, x);
+ }
+
+ @Override
+ public void updateInt(int columnIndex, int x) throws SQLException {
+ delegate.updateInt(columnIndex, x);
+ }
+
+ @Override
+ public void updateLong(int columnIndex, long x) throws SQLException {
+ delegate.updateLong(columnIndex, x);
+ }
+
+ @Override
+ public void updateFloat(int columnIndex, float x) throws SQLException {
+ delegate.updateFloat(columnIndex, x);
+ }
+
+ @Override
+ public void updateDouble(int columnIndex, double x) throws SQLException {
+ delegate.updateDouble(columnIndex, x);
+ }
+
+ @Override
+ public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException {
+ delegate.updateBigDecimal(columnIndex, x);
+ }
+
+ @Override
+ public void updateString(int columnIndex, String x) throws SQLException {
+ delegate.updateString(columnIndex, x);
+ }
+
+ @Override
+ public void updateBytes(int columnIndex, byte[] x) throws SQLException {
+ delegate.updateBytes(columnIndex, x);
+ }
+
+ @Override
+ public void updateDate(int columnIndex, Date x) throws SQLException {
+ delegate.updateDate(columnIndex, x);
+ }
+
+ @Override
+ public void updateTime(int columnIndex, Time x) throws SQLException {
+ delegate.updateTime(columnIndex, x);
+ }
+
+ @Override
+ public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException {
+ delegate.updateTimestamp(columnIndex, x);
+ }
+
+ @Override
+ public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException {
+ delegate.updateAsciiStream(columnIndex, x, length);
+ }
+
+ @Override
+ public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException {
+ delegate.updateBinaryStream(columnIndex, x, length);
+ }
+
+ @Override
+ public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException {
+ delegate.updateCharacterStream(columnIndex, x, length);
+ }
+
+ @Override
+ public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException {
+ delegate.updateObject(columnIndex, x, scaleOrLength);
+ }
+
+ @Override
+ public void updateObject(int columnIndex, Object x) throws SQLException {
+ delegate.updateObject(columnIndex, x);
+ }
+
+ @Override
+ public void updateNull(String columnLabel) throws SQLException {
+ delegate.updateNull(columnLabel);
+ }
+
+ @Override
+ public void updateBoolean(String columnLabel, boolean x) throws SQLException {
+ delegate.updateBoolean(columnLabel, x);
+ }
+
+ @Override
+ public void updateByte(String columnLabel, byte x) throws SQLException {
+ delegate.updateByte(columnLabel, x);
+ }
+
+ @Override
+ public void updateShort(String columnLabel, short x) throws SQLException {
+ delegate.updateShort(columnLabel, x);
+ }
+
+ @Override
+ public void updateInt(String columnLabel, int x) throws SQLException {
+ delegate.updateInt(columnLabel, x);
+ }
+
+ @Override
+ public void updateLong(String columnLabel, long x) throws SQLException {
+ delegate.updateLong(columnLabel, x);
+ }
+
+ @Override
+ public void updateFloat(String columnLabel, float x) throws SQLException {
+ delegate.updateFloat(columnLabel, x);
+ }
+
+ @Override
+ public void updateDouble(String columnLabel, double x) throws SQLException {
+ delegate.updateDouble(columnLabel, x);
+ }
+
+ @Override
+ public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException {
+ delegate.updateBigDecimal(columnLabel, x);
+ }
+
+ @Override
+ public void updateString(String columnLabel, String x) throws SQLException {
+ delegate.updateString(columnLabel, x);
+ }
+
+ @Override
+ public void updateBytes(String columnLabel, byte[] x) throws SQLException {
+ delegate.updateBytes(columnLabel, x);
+ }
+
+ @Override
+ public void updateDate(String columnLabel, Date x) throws SQLException {
+ delegate.updateDate(columnLabel, x);
+ }
+
+ @Override
+ public void updateTime(String columnLabel, Time x) throws SQLException {
+ delegate.updateTime(columnLabel, x);
+ }
+
+ @Override
+ public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException {
+ delegate.updateTimestamp(columnLabel, x);
+ }
+
+ @Override
+ public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException {
+ delegate.updateAsciiStream(columnLabel, x, length);
+ }
+
+ @Override
+ public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException {
+ delegate.updateBinaryStream(columnLabel, x, length);
+ }
+
+ @Override
+ public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException {
+ delegate.updateCharacterStream(columnLabel, reader, length);
+ }
+
+ @Override
+ public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException {
+ delegate.updateObject(columnLabel, x, scaleOrLength);
+ }
+
+ @Override
+ public void updateObject(String columnLabel, Object x) throws SQLException {
+ delegate.updateObject(columnLabel, x);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void insertRow() throws SQLException {
+ connection.markCommitStateDirty();
+ delegate.insertRow();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void deleteRow() throws SQLException {
+ connection.markCommitStateDirty();
+ delegate.deleteRow();
+ }
+
+ @Override
+ public void refreshRow() throws SQLException {
+ delegate.refreshRow();
+ }
+
+ @Override
+ public void cancelRowUpdates() throws SQLException {
+ delegate.cancelRowUpdates();
+ }
+
+ @Override
+ public void moveToInsertRow() throws SQLException {
+ delegate.moveToInsertRow();
+ }
+
+ @Override
+ public void moveToCurrentRow() throws SQLException {
+ delegate.moveToCurrentRow();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public final T unwrap(Class iface) throws SQLException {
+ if (iface.isInstance(delegate)) {
+ return (T) delegate;
+ } else if (delegate != null) {
+ return delegate.unwrap(iface);
+ }
+
+ throw new SQLException("Wrapped ResultSet is not an instance of " + iface);
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) throws SQLException {
+ return delegate.isWrapperFor(iface);
+ }
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyStatement.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyStatement.java
new file mode 100644
index 0000000..04cf67b
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyStatement.java
@@ -0,0 +1,394 @@
+package org.xbib.jdbc.connection.pool;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.Statement;
+
+/**
+ * This is the proxy class for java.sql.Statement.
+ */
+public class ProxyStatement implements Statement {
+
+ protected final ProxyConnection connection;
+
+ protected final Statement delegate;
+
+ private boolean isClosed;
+
+ private ResultSet proxyResultSet;
+
+ public ProxyStatement(ProxyConnection connection, Statement statement) {
+ this.connection = connection;
+ this.delegate = statement;
+ }
+
+ public Statement getDelegate() {
+ return delegate;
+ }
+
+ @SuppressWarnings("unused")
+ public SQLException checkException(SQLException e) {
+ return connection.checkException(e);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ final String delegateToString = delegate.toString();
+ return this.getClass().getSimpleName() + '@' + System.identityHashCode(this) + " wrapping " + delegateToString;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close() throws SQLException {
+ synchronized (this) {
+ if (isClosed) {
+ return;
+ }
+ isClosed = true;
+ }
+ connection.untrackStatement(delegate);
+ try {
+ delegate.close();
+ } catch (SQLException e) {
+ throw connection.checkException(e);
+ }
+ }
+
+ @Override
+ public int getMaxFieldSize() throws SQLException {
+ return delegate.getMaxFieldSize();
+ }
+
+ @Override
+ public void setMaxFieldSize(int max) throws SQLException {
+ delegate.setMaxFieldSize(max);
+ }
+
+ @Override
+ public int getMaxRows() throws SQLException {
+ return delegate.getMaxRows();
+ }
+
+ @Override
+ public void setMaxRows(int max) throws SQLException {
+ delegate.setMaxRows(max);
+ }
+
+ @Override
+ public void setEscapeProcessing(boolean enable) throws SQLException {
+ delegate.setEscapeProcessing(enable);
+ }
+
+ @Override
+ public int getQueryTimeout() throws SQLException {
+ return delegate.getQueryTimeout();
+ }
+
+ @Override
+ public void setQueryTimeout(int seconds) throws SQLException {
+ delegate.setQueryTimeout(seconds);
+ }
+
+ @Override
+ public void cancel() throws SQLException {
+ delegate.cancel();
+ }
+
+ @Override
+ public SQLWarning getWarnings() throws SQLException {
+ return delegate.getWarnings();
+ }
+
+ @Override
+ public void clearWarnings() throws SQLException {
+ delegate.clearWarnings();
+ }
+
+ @Override
+ public void setCursorName(String name) throws SQLException {
+ delegate.setCursorName(name);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Connection getConnection() throws SQLException {
+ return connection;
+ }
+
+ @Override
+ public boolean getMoreResults(int current) throws SQLException {
+ return delegate.getMoreResults(current);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean execute(String sql) throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.execute(sql);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean execute(String sql, int autoGeneratedKeys) throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.execute(sql, autoGeneratedKeys);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ResultSet executeQuery(String sql) throws SQLException {
+ connection.markCommitStateDirty();
+ ResultSet resultSet = delegate.executeQuery(sql);
+ return ProxyFactory.getProxyResultSet(connection, this, resultSet);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int executeUpdate(String sql) throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.executeUpdate(sql);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int[] executeBatch() throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.executeBatch();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.executeUpdate(sql, autoGeneratedKeys);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.executeUpdate(sql, columnIndexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int executeUpdate(String sql, String[] columnNames) throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.executeUpdate(sql, columnNames);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean execute(String sql, int[] columnIndexes) throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.execute(sql, columnIndexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean execute(String sql, String[] columnNames) throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.execute(sql, columnNames);
+ }
+
+ @Override
+ public int getResultSetHoldability() throws SQLException {
+ return delegate.getResultSetHoldability();
+ }
+
+ @Override
+ public boolean isClosed() throws SQLException {
+ return delegate.isClosed();
+ }
+
+ @Override
+ public void setPoolable(boolean poolable) throws SQLException {
+ delegate.setPoolable(poolable);
+ }
+
+ @Override
+ public boolean isPoolable() throws SQLException {
+ return delegate.isPoolable();
+ }
+
+ @Override
+ public void closeOnCompletion() throws SQLException {
+ delegate.closeOnCompletion();
+ }
+
+ @Override
+ public boolean isCloseOnCompletion() throws SQLException {
+ return delegate.isCloseOnCompletion();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long[] executeLargeBatch() throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.executeLargeBatch();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long executeLargeUpdate(String sql) throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.executeLargeUpdate(sql);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.executeLargeUpdate(sql, autoGeneratedKeys);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.executeLargeUpdate(sql, columnIndexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException {
+ connection.markCommitStateDirty();
+ return delegate.executeLargeUpdate(sql, columnNames);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ResultSet getResultSet() throws SQLException {
+ final ResultSet resultSet = delegate.getResultSet();
+ if (resultSet != null) {
+ if (proxyResultSet == null || ((ProxyResultSet) proxyResultSet).getDelegate() != resultSet) {
+ proxyResultSet = ProxyFactory.getProxyResultSet(connection, this, resultSet);
+ }
+ } else {
+ proxyResultSet = null;
+ }
+ return proxyResultSet;
+ }
+
+ @Override
+ public int getUpdateCount() throws SQLException {
+ return delegate.getUpdateCount();
+ }
+
+ @Override
+ public boolean getMoreResults() throws SQLException {
+ return delegate.getMoreResults();
+ }
+
+ @Override
+ public void setFetchDirection(int direction) throws SQLException {
+ delegate.setFetchDirection(direction);
+ }
+
+ @Override
+ public int getFetchDirection() throws SQLException {
+ return delegate.getFetchDirection();
+ }
+
+ @Override
+ public void setFetchSize(int rows) throws SQLException {
+ delegate.setFetchSize(rows);
+ }
+
+ @Override
+ public int getFetchSize() throws SQLException {
+ return delegate.getFetchSize();
+ }
+
+ @Override
+ public int getResultSetConcurrency() throws SQLException {
+ return delegate.getResultSetConcurrency();
+ }
+
+ @Override
+ public int getResultSetType() throws SQLException {
+ return delegate.getResultSetType();
+ }
+
+ @Override
+ public void addBatch(String sql) throws SQLException {
+ delegate.addBatch(sql);
+ }
+
+ @Override
+ public void clearBatch() throws SQLException {
+ delegate.clearBatch();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ResultSet getGeneratedKeys() throws SQLException {
+ ResultSet resultSet = delegate.getGeneratedKeys();
+ if (proxyResultSet == null || ((ProxyResultSet) proxyResultSet).getDelegate() != resultSet) {
+ proxyResultSet = ProxyFactory.getProxyResultSet(connection, this, resultSet);
+ }
+ return proxyResultSet;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public final T unwrap(Class iface) throws SQLException {
+ if (iface.isInstance(delegate)) {
+ return (T) delegate;
+ } else if (delegate != null) {
+ return delegate.unwrap(iface);
+ }
+ throw new SQLException("Wrapped statement is not an instance of " + iface);
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) throws SQLException {
+ return delegate.isWrapperFor(iface);
+ }
+}
diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/Bag.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/Bag.java
new file mode 100644
index 0000000..9a367e5
--- /dev/null
+++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/Bag.java
@@ -0,0 +1,333 @@
+package org.xbib.jdbc.connection.pool.util;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.AbstractQueuedLongSynchronizer;
+import java.util.concurrent.locks.LockSupport;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * This is a specialized concurrent bag that achieves superior performance
+ * to {@link java.util.concurrent.LinkedBlockingQueue} and
+ * {@link java.util.concurrent.LinkedTransferQueue} for the purposes of a
+ * connection pool. It uses {@link ThreadLocal} storage when possible to avoid
+ * locks, but resorts to scanning a common collection if there are no
+ * available items in the {@link ThreadLocal} list. Not-in-use items in the
+ * {@link ThreadLocal} lists can be "stolen" when the borrowing thread has none
+ * of its own. It is a "lock-less" implementation using a specialized
+ * {@link AbstractQueuedLongSynchronizer} to manage cross-thread signaling.
+ * Note that items that are "borrowed" from the bag are not actually
+ * removed from any collection, so garbage collection will not occur
+ * even if the reference is abandoned. Thus care must be taken to
+ * {@link Bag#requite(T)} borrowed objects otherwise a memory leak will result.
+ * Only the {@link Bag#remove(T)} method can completely remove an object.
+ *
+ * @param the templated type to store in the bag
+ */
+public class Bag implements AutoCloseable {
+
+ private static final Logger logger = Logger.getLogger(Bag.class.getName());
+
+ private final CopyOnWriteArrayList sharedList;
+
+ private final boolean weakThreadLocals;
+
+ private final ThreadLocal> threadList;
+
+ private final BagStateListener listener;
+
+ private final AtomicInteger waiters;
+
+ private volatile boolean closed;
+
+ private final SynchronousQueue handoffQueue;
+
+ private String lastMessage;
+
+ /**
+ * Construct a Bag with the specified listener.
+ *
+ * @param listener the IBagStateListener to attach to this bag
+ */
+ public Bag(BagStateListener listener) {
+ this.listener = listener;
+ this.weakThreadLocals = useWeakThreadLocals();
+ this.handoffQueue = new SynchronousQueue<>(true);
+ this.waiters = new AtomicInteger();
+ this.sharedList = new CopyOnWriteArrayList<>();
+ if (weakThreadLocals) {
+ this.threadList = ThreadLocal.withInitial(() -> new ArrayList<>(16));
+ } else {
+ this.threadList = ThreadLocal.withInitial(() -> new FastList<>(BagEntry.class, 16));
+ }
+ }
+
+ public String getLastMessage() {
+ return lastMessage;
+ }
+
+ /**
+ * The method will borrow a BagEntry from the bag, blocking for the
+ * specified timeout if none are available.
+ *
+ * @param timeout how long to wait before giving up, in units of unit
+ * @param timeUnit a TimeUnit determining how to interpret the timeout parameter
+ * @return a borrowed instance from the bag or null if a timeout occurs
+ * @throws InterruptedException if interrupted while waiting
+ */
+ public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
+ // Try the thread-local list first
+ final List
+ *
+ * @param code the code you want to run as a transaction with a Database
+ * @see #transact(DbCodeTx)
+ */
+ void transact(DbCode code);
+
+ /**
+ * This method is the same as {@link #transact(DbCode)} but allows a return value.
+ *
+ *
Here is a typical usage:
+ *
+ * List r = dbp.transact(dbs -> {
+ * return dbs.get().toSelect("select a from b where c=?").argInteger(1).queryStrings();
+ * });
+ *
+ *
+ */
+ T transactReturning(DbCodeTyped code);
+
+ /**
+ * This is a convenience method to eliminate the need for explicitly
+ * managing the resources (and error handling) for this class. After
+ * the run block is complete commit() will be called unless either the
+ * {@link DbCodeTx#run(Supplier, Transaction)} method threw a {@link Throwable}
+ * while {@link Transaction#isRollbackOnError()} returns true, or
+ * {@link Transaction#isRollbackOnly()} returns a true value.
+ *
+ *
+ *
+ *
+ * @param code the code you want to run as a transaction with a Database
+ */
+ void transact(DbCodeTx code);
+
+ void close();
+ }
+
+ private static class BuilderImpl implements Builder {
+
+ private PoolDataSource ds;
+
+ private final Supplier connectionProvider;
+
+ private final Options options;
+
+ private BuilderImpl(PoolDataSource ds, Supplier connectionProvider, Options options) {
+ this.ds = ds;
+ this.connectionProvider = connectionProvider;
+ this.options = options;
+ }
+
+ @Override
+ public Builder withOptions(OptionsOverride options) {
+ return new BuilderImpl(ds, connectionProvider, options.withParent(this.options));
+ }
+
+ @Override
+ public Builder withSqlParameterLogging() {
+ return new BuilderImpl(ds, connectionProvider, new OptionsOverride() {
+ @Override
+ public boolean isLogParameters() {
+ return true;
+ }
+ }.withParent(this.options));
+ }
+
+ @Override
+ public Builder withSqlInExceptionMessages() {
+ return new BuilderImpl(ds, connectionProvider, new OptionsOverride() {
+ @Override
+ public boolean isDetailedExceptions() {
+ return true;
+ }
+ }.withParent(this.options));
+ }
+
+ @Override
+ public Builder withDatePerAppOnly() {
+ return new BuilderImpl(ds, connectionProvider, new OptionsOverride() {
+ @Override
+ public boolean useDatePerAppOnly() {
+ return true;
+ }
+ }.withParent(this.options));
+ }
+
+ @Override
+ public Builder withTransactionControl() {
+ return new BuilderImpl(ds, connectionProvider, new OptionsOverride() {
+ @Override
+ public boolean allowTransactionControl() {
+ return true;
+ }
+ }.withParent(this.options));
+ }
+
+ @Override
+ public Builder withTransactionControlSilentlyIgnored() {
+ return new BuilderImpl(ds, connectionProvider, new OptionsOverride() {
+ @Override
+ public boolean ignoreTransactionControl() {
+ return true;
+ }
+ }.withParent(this.options));
+ }
+
+ @Override
+ public Builder withConnectionAccess() {
+ return new BuilderImpl(ds, connectionProvider, new OptionsOverride() {
+ @Override
+ public boolean allowConnectionAccess() {
+ return true;
+ }
+ }.withParent(this.options));
+ }
+
+ @Override
+ public DatabaseProvider create() {
+ return new DatabaseProvider(connectionProvider, options);
+ }
+
+ @Override
+ public void transact(DbCode tx) {
+ create().transact(tx);
+ }
+
+ @Override
+ public T transactReturning(DbCodeTyped tx) {
+ return create().transactReturning(tx);
+ }
+
+ @Override
+ public void transact(DbCodeTx tx) {
+ create().transact(tx);
+ }
+
+ @Override
+ public void close() {
+ if (ds != null) {
+ ds.close();
+ ds = null;
+ }
+ }
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCode.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCode.java
new file mode 100644
index 0000000..f1df0dd
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCode.java
@@ -0,0 +1,18 @@
+package org.xbib.jdbc.query;
+
+import java.util.function.Supplier;
+
+/**
+ * A block of runnable code using a transacted Database.
+ */
+public interface DbCode {
+ /**
+ * Implement this method to provide a block of code that uses the provided database
+ * and is transacted. Whether the transaction will commit or rollback is typically
+ * controlled by the code that invokes this method.
+ *
+ *
If a {@link Throwable} is thrown from this method, it will be caught, wrapped in
+ * a DatabaseException (if it is not already one), and then propagated.
+ */
+ void run(Supplier dbs) throws Exception;
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTx.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTx.java
new file mode 100644
index 0000000..6ec3ade
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTx.java
@@ -0,0 +1,19 @@
+package org.xbib.jdbc.query;
+
+import java.util.function.Supplier;
+
+/**
+ * A block of runnable code using a transacted Database.
+ */
+@FunctionalInterface
+public interface DbCodeTx {
+ /**
+ * Implement this method to provide a block of code that uses the provided database
+ * and is transacted. Whether the transaction will commit or rollback is typically
+ * controlled by the code that invokes this method.
+ *
+ *
If a {@link Throwable} is thrown from this method, it will be caught, wrapped in
+ * a DatabaseException (if it is not already one), and then propagated.
+ */
+ void run(Supplier db, Transaction tx) throws Exception;
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTyped.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTyped.java
new file mode 100644
index 0000000..b3fbb77
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTyped.java
@@ -0,0 +1,19 @@
+package org.xbib.jdbc.query;
+
+import java.util.function.Supplier;
+
+/**
+ * A block of runnable code using a transacted Database.
+ */
+@FunctionalInterface
+public interface DbCodeTyped {
+ /**
+ * Implement this method to provide a block of code that uses the provided database
+ * and is transacted. Whether the transaction will commit or rollback is typically
+ * controlled by the code that invokes this method.
+ *
+ *
If a {@link Throwable} is thrown from this method, it will be caught, wrapped in
+ * a DatabaseException (if it is not already one), and then propagated.
+ */
+ T run(Supplier dbs) throws Exception;
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTypedTx.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTypedTx.java
new file mode 100644
index 0000000..76ecb87
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTypedTx.java
@@ -0,0 +1,19 @@
+package org.xbib.jdbc.query;
+
+import java.util.function.Supplier;
+
+/**
+ * A block of runnable code using a transacted Database.
+ */
+@FunctionalInterface
+public interface DbCodeTypedTx {
+ /**
+ * Implement this method to provide a block of code that uses the provided database
+ * and is transacted. Whether the transaction will commit or rollback is typically
+ * controlled by the code that invokes this method.
+ *
+ *
If a {@link Throwable} is thrown from this method, it will be caught, wrapped in
+ * a DatabaseException (if it is not already one), and then propagated.
+ */
+ T run(Supplier db, Transaction tx) throws Exception;
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Ddl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Ddl.java
new file mode 100644
index 0000000..fff4b8e
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Ddl.java
@@ -0,0 +1,19 @@
+package org.xbib.jdbc.query;
+
+/**
+ * Interface for executing a chunk of DDL within the database.
+ */
+public interface Ddl {
+ /**
+ * Execute the DDL statement. All checked SQLExceptions get wrapped in DatabaseExceptions.
+ */
+ void execute();
+
+ /**
+ * This just does an execute() call and silently discards any DatabaseException
+ * that might occur. This can be useful for things like drop statements, where
+ * some databases don't make it easy to conditionally drop things only if they
+ * exist.
+ */
+ void executeQuietly();
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DdlImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DdlImpl.java
new file mode 100644
index 0000000..abd6640
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DdlImpl.java
@@ -0,0 +1,97 @@
+package org.xbib.jdbc.query;
+
+import org.xbib.jdbc.query.util.DebugSql;
+import org.xbib.jdbc.query.util.Metric;
+
+import java.sql.CallableStatement;
+import java.sql.Connection;
+import java.sql.Statement;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class DdlImpl implements Ddl {
+
+ private static final Logger log = Logger.getLogger(Database.class.getName());
+
+ private static final Logger logQuiet = Logger.getLogger(Database.class.getName() + ".quiet");
+
+ private final Connection connection;
+
+ private final String sql;
+
+ private final Options options;
+
+ DdlImpl(Connection connection, String sql, Options options) {
+ this.connection = connection;
+ this.sql = sql;
+ this.options = options;
+ }
+
+ private void updateInternal(boolean quiet) {
+ CallableStatement ps = null;
+ Metric metric = new Metric(log.isLoggable(Level.FINE));
+
+ boolean isSuccess = false;
+ String errorCode = null;
+ Exception logEx = null;
+ try {
+ ps = connection.prepareCall(sql);
+
+ metric.checkpoint("prep");
+ ps.execute();
+ metric.checkpoint("exec");
+ isSuccess = true;
+ } catch (Exception e) {
+ errorCode = options.generateErrorCode();
+ logEx = e;
+ throw DatabaseException.wrap(DebugSql.exceptionMessage(sql, null, errorCode, options), e);
+ } finally {
+ close(ps);
+ metric.checkpoint("close");
+ // PostgreSQL requires explicit commit since we are running with setAutoCommit(false)
+ commit(connection);
+ metric.done("commit");
+ if (isSuccess) {
+ DebugSql.logSuccess("DDL", log, metric, sql, null, options);
+ } else if (quiet) {
+ DebugSql.logWarning("DDL", logQuiet, metric, errorCode, sql, null, options, logEx);
+ } else {
+ DebugSql.logError("DDL", log, metric, errorCode, sql, null, options, logEx);
+ }
+ }
+ }
+
+ @Override
+ public void execute() {
+ updateInternal(false);
+ }
+
+ @Override
+ public void executeQuietly() {
+ try {
+ updateInternal(true);
+ } catch (DatabaseException e) {
+ // Ignore, as requested
+ }
+ }
+
+ private void close(Statement s) {
+ if (s != null) {
+ try {
+ s.close();
+ } catch (Exception e) {
+ log.log(Level.SEVERE, "Caught exception closing the Statement", e);
+ }
+ }
+ }
+
+ private void commit(Connection c) {
+ if (c != null) {
+ try {
+ c.commit();
+ } catch (Exception e) {
+ log.log(Level.SEVERE, "Caught exception on commit", 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
new file mode 100644
index 0000000..8eb8ea9
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Flavor.java
@@ -0,0 +1,993 @@
+package org.xbib.jdbc.query;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * Enumeration of supported databases with various compatibility settings.
+ */
+public enum Flavor {
+
+ derby {
+ @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 dbtestSeq) {
+ return "drop sequence " + dbtestSeq + " restrict";
+ }
+
+ @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(Date 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 "'" + date.toString() + "'";
+ }
+
+ @Override
+ public String sequenceOptions() {
+ return " as bigint";
+ }
+
+ @Override
+ public boolean autoCommitOnly() {
+ return false;
+ }
+ },
+ sqlserver {
+ @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 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() {
+ // TODO it probably does, but I haven't figure it out yet
+ return false;
+ }
+
+ @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(Date 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 autoCommitOnly() {
+ return false;
+ }
+ },
+ oracle {
+ @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 dbtestSeq) {
+ return "drop sequence " + dbtestSeq;
+ }
+
+ @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(Date 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 autoCommitOnly() {
+ return false;
+ }
+ },
+ postgresql {
+ @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 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(Date 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 autoCommitOnly() {
+ return false;
+ }
+ },
+ hsqldb {
+ @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 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(Date 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 autoCommitOnly() {
+ return false;
+ }
+ },
+ bigquery {
+ @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 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(Date 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 autoCommitOnly() {
+ return true;
+ }
+ };
+
+ public static Flavor fromJdbcUrl(String url) {
+ if (url == null) {
+ throw new DatabaseException("url must not be null");
+ }
+ if (url.startsWith("jdbc:postgresql:")) {
+ return postgresql;
+ } else if (url.startsWith("jdbc:oracle:")) {
+ return oracle;
+ } else if (url.startsWith("jdbc:sqlserver:")) {
+ return sqlserver;
+ } else if (url.startsWith("jdbc:hsqldb:")) {
+ return hsqldb;
+ } else if (url.startsWith("jdbc:derby:")) {
+ return derby;
+ } else if (url.startsWith("jdbc:bigquery:")) {
+ return bigquery;
+ } else {
+ throw new DatabaseException("Cannot determine database flavor from url");
+ }
+ }
+
+ public static String driverForJdbcUrl(String url) {
+ if (url == null) {
+ throw new DatabaseException("url must not be null");
+ }
+ if (url.startsWith("jdbc:postgresql:")) {
+ return "org.postgresql.Driver";
+ } else if (url.startsWith("jdbc:oracle:")) {
+ return "oracle.jdbc.OracleDriver";
+ } else if (url.startsWith("jdbc:sqlserver:")) {
+ return "com.microsoft.sqlserver.jdbc.SQLServerDriver";
+ } else if (url.startsWith("jdbc:hsqldb:")) {
+ return "org.hsqldb.jdbc.JDBCDriver";
+ } else if (url.startsWith("jdbc:derby:")) {
+ return "org.apache.derby.jdbc.EmbeddedDriver";
+ } else if (url.startsWith("jdbc:bigquery:")) {
+ return "com.simba.googlebigquery.jdbc42.Driver";
+ } else {
+ throw new DatabaseException("Cannot determine database driver class from url");
+ }
+ }
+
+ // Returns true if DB normalizes to upper case names for ids like tables and columns
+ // See https://github.com/ontop/ontop/wiki/Case-sensitivity-for-SQL-identifiers
+ public abstract boolean isNormalizedUpperCase();
+
+ public abstract String typeInteger();
+
+ public abstract String typeBoolean();
+
+ public abstract String typeLong();
+
+ public abstract String typeFloat();
+
+ public abstract String typeDouble();
+
+ public abstract String typeBigDecimal(int size, int precision);
+
+ public abstract String typeStringVar(int length);
+
+ public abstract String typeStringFixed(int length);
+
+ public abstract String typeClob();
+
+ public abstract String typeBlob();
+
+ public abstract String typeDate();
+
+ public abstract String typeLocalDate();
+
+ public abstract boolean useStringForClob();
+
+ public abstract boolean useBytesForBlob();
+
+ public abstract String sequenceNextVal(String sequenceName);
+
+ public abstract String sequenceSelectNextVal(String sequenceName);
+
+ public abstract String sequenceDrop(String dbtestSeq);
+
+ public abstract boolean supportsInsertReturning();
+
+ public abstract String dbTimeMillis();
+
+ public abstract String sequenceCacheClause(int nbrValuesToCache);
+
+ public abstract String sequenceOrderClause(boolean order);
+
+ public abstract String sequenceCycleClause(boolean cycle);
+
+ /**
+ * Indicate what should follow a constant select statement. For example, "select 1"
+ * works on some databases, while Oracle requires "select 1 from dual". For Oracle
+ * this function should return " from dual" (including the leading space).
+ */
+ public abstract String fromAny();
+
+ /**
+ * Return a SQL function representing the specified date. For example, in PostgreSQL this
+ * looks like "'1970-01-02 02:17:36.789000 GMT'::timestamp".
+ */
+ public abstract String dateAsSqlFunction(Date date, Calendar calendar);
+
+ /**
+ * Return a SQL function representing the specified date without time.
+ */
+ public abstract String localDateAsSqlFunction(Date date);
+
+ public abstract String sequenceOptions();
+
+ public abstract boolean autoCommitOnly();
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/MixedParameterSql.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/MixedParameterSql.java
new file mode 100644
index 0000000..ab6c6c7
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/MixedParameterSql.java
@@ -0,0 +1,133 @@
+package org.xbib.jdbc.query;
+
+import org.xbib.jdbc.query.util.RewriteArg;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Convenience class to allow use of (:mylabel) for SQL parameters in addition to
+ * positional (?) parameters. This doesn't do any smart parsing of the SQL, it is just
+ * looking for ':' and '?' characters. If the SQL needs to include an actual ':' or '?'
+ * character, use two of them ('::' or '??'), and they will be replaced with a
+ * single ':' or '?'.
+ */
+public class MixedParameterSql {
+
+ private final String sqlToExecute;
+
+ private final Object[] args;
+
+ public MixedParameterSql(String sql, List positionalArgs, Map nameToArg) {
+ if (positionalArgs == null) {
+ positionalArgs = new ArrayList<>();
+ }
+ if (nameToArg == null) {
+ nameToArg = new HashMap<>();
+ }
+
+ StringBuilder newSql = new StringBuilder(sql.length());
+ List argNamesList = new ArrayList<>();
+ List rewrittenArgs = new ArrayList<>();
+ List argsList = new ArrayList<>();
+ int searchIndex = 0;
+ int currentPositionalArg = 0;
+ while (searchIndex < sql.length()) {
+ int nextColonIndex = sql.indexOf(':', searchIndex);
+ int nextQmIndex = sql.indexOf('?', searchIndex);
+
+ if (nextColonIndex < 0 && nextQmIndex < 0) {
+ newSql.append(sql.substring(searchIndex));
+ break;
+ }
+
+ if (nextColonIndex >= 0 && (nextQmIndex == -1 || nextColonIndex < nextQmIndex)) {
+ // The next parameter we found is a named parameter (":foo")
+ if (nextColonIndex > sql.length() - 2) {
+ // Probably illegal sql, but handle boundary condition
+ break;
+ }
+
+ // Allow :: as escape for :
+ if (sql.charAt(nextColonIndex + 1) == ':') {
+ newSql.append(sql, searchIndex, nextColonIndex + 1);
+ searchIndex = nextColonIndex + 2;
+ continue;
+ }
+
+ int endOfNameIndex = nextColonIndex + 1;
+ while (endOfNameIndex < sql.length() && Character.isJavaIdentifierPart(sql.charAt(endOfNameIndex))) {
+ endOfNameIndex++;
+ }
+ newSql.append(sql, searchIndex, nextColonIndex);
+ String paramName = sql.substring(nextColonIndex + 1, endOfNameIndex);
+ boolean secretParam = paramName.startsWith("secret");
+ Object arg = nameToArg.get(paramName);
+ if (arg instanceof RewriteArg) {
+ newSql.append(((RewriteArg) arg).getSql());
+ rewrittenArgs.add(paramName);
+ } else {
+ newSql.append('?');
+ if (nameToArg.containsKey(paramName)) {
+ argsList.add(secretParam ? new SecretArg(arg) : arg);
+ } else {
+ throw new DatabaseException("The SQL requires parameter ':" + paramName + "' but no value was provided");
+ }
+ argNamesList.add(paramName);
+ }
+ searchIndex = endOfNameIndex;
+ } else {
+ // The next parameter we found is a positional parameter ("?")
+
+ // Allow ?? as escape for ?
+ if (nextQmIndex < sql.length() - 1 && sql.charAt(nextQmIndex + 1) == '?') {
+ newSql.append(sql, searchIndex, nextQmIndex + 1);
+ searchIndex = nextQmIndex + 2;
+ continue;
+ }
+
+ newSql.append(sql, searchIndex, nextQmIndex);
+ if (currentPositionalArg >= positionalArgs.size()) {
+ throw new DatabaseException("Not enough positional parameters (" + positionalArgs.size() + ") were provided");
+ }
+ if (positionalArgs.get(currentPositionalArg) instanceof RewriteArg) {
+ newSql.append(((RewriteArg) positionalArgs.get(currentPositionalArg)).getSql());
+ } else {
+ newSql.append('?');
+ argsList.add(positionalArgs.get(currentPositionalArg));
+ }
+ currentPositionalArg++;
+ searchIndex = nextQmIndex + 1;
+ }
+ }
+ this.sqlToExecute = newSql.toString();
+ args = argsList.toArray(new Object[argsList.size()]);
+
+ // Sanity check number of arguments to provide a better error message
+ if (currentPositionalArg != positionalArgs.size()) {
+ throw new DatabaseException("Wrong number of positional parameters were provided (expected: "
+ + currentPositionalArg + ", actual: " + positionalArgs.size() + ")");
+ }
+ if (nameToArg.size() > args.length - Math.max(0, positionalArgs.size() - 1) + rewrittenArgs.size()) {
+ Set unusedNames = new HashSet<>(nameToArg.keySet());
+ unusedNames.removeAll(argNamesList);
+ unusedNames.removeAll(rewrittenArgs);
+ if (!unusedNames.isEmpty()) {
+ throw new DatabaseException("These named parameters do not exist in the query: " + unusedNames);
+ }
+ }
+ }
+
+ public String getSqlToExecute() {
+ return sqlToExecute;
+ }
+
+ public Object[] getArgs() {
+ return args;
+ }
+
+}
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
new file mode 100644
index 0000000..9667b6b
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Options.java
@@ -0,0 +1,130 @@
+package org.xbib.jdbc.query;
+
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * Control various optional behavior for the database interactions.
+ */
+public interface Options {
+ /**
+ * Control whether the Database object will allow calls to commitNow()
+ * and rollbackNow(). By default it will throw exceptions if you try to
+ * call those.
+ */
+ boolean allowTransactionControl();
+
+ /**
+ * Useful for testing code that explicitly controls transactions, and you
+ * don't really want it to commit/rollback. Disabled by default, meaning
+ * calls will be allowed or throw exceptions depending on allowTransctionControl().
+ * The value of allowTranscationControl() has no affect if this returns true.
+ */
+ boolean ignoreTransactionControl();
+
+ /**
+ * Control whether the Database object will allow calls to underlyingConnection().
+ * By default that method will throw an exception.
+ */
+ boolean allowConnectionAccess();
+
+ /**
+ * If this is false, log messages will look something like:
+ *
+ *
+ * ...select a from b where c=?
+ *
+ *
+ * If this is true, log messages will look something like:
+ *
+ *
+ * ...select a from b where c=?|select a from b where c='abc'
+ *
+ *
+ * @return true if parameter values should be logged along with SQL, false otherwise
+ */
+ boolean isLogParameters();
+
+ /**
+ * If true, text of the SQL and possibly parameter values (depending on @{#isLogParameters()})
+ * will be included in exception messages. This can be very helpful for debugging, but poses
+ * some disclosure risks.
+ *
+ * @return true to add possibly sensitive data in exception messages, false otherwise
+ */
+ boolean isDetailedExceptions();
+
+ /**
+ * In cases where exceptions are thrown, use this method to provide a common
+ * code that will be included in the exception message and the log message
+ * so they can be searched and correlated later.
+ *
+ * @return an arbitrary, fairly unique, speakable over the phone, without whitespace
+ */
+ String generateErrorCode();
+
+ /**
+ * Indicate whether to use the Blob functionality of the underlying database driver,
+ * or whether to use setBytes() methods instead. Using Blobs is preferred, but is not
+ * supported by all drivers.
+ *
+ *
The default behavior of this method is to delegate to flavor().useBytesForBlob(),
+ * but it is provided on this interface so the behavior can be controlled.
+ *
+ * @return true to avoid using Blob functionality, false otherwise
+ */
+ boolean useBytesForBlob();
+
+ /**
+ * Indicate whether to use the Clob functionality of the underlying database driver,
+ * or whether to use setString() methods instead. Using Clobs is preferred, but is not
+ * supported by all drivers.
+ *
+ *
The default behavior of this method is to delegate to flavor().useStringForClob(),
+ * but it is provided on this interface so the behavior can be controlled.
+ *
+ * @return true to avoid using Clob functionality, false otherwise
+ */
+ boolean useStringForClob();
+
+ /**
+ * Access compatibility information for the underlying database. The
+ * Flavor class enumerates the known databases and tries to smooth over
+ * some of the variations in features and syntax.
+ */
+ Flavor flavor();
+
+ /**
+ * The value returned by this method will be used for argDateNowPerApp() calls. It
+ * may also be used for argDateNowPerDb() calls if you have enabled that.
+ */
+ Date currentDate();
+
+ /**
+ * Wherever argDateNowPerDb() is specified, use argDateNowPerApp() instead. This is
+ * useful for testing purposes as you can use OptionsOverride to provide your
+ * own system clock that will be used for time travel.
+ */
+ boolean useDatePerAppOnly();
+
+ /**
+ * This calendar will be used for conversions when storing and retrieving timestamps
+ * from the database. By default this is the JVM default with TimeZone explicitly set
+ * to GMT (so timestamps will be stored in the database as GMT).
+ *
+ *
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();
+
+ /**
+ * The maximum number of characters to print in debug SQL for a given String type
+ * insert/update/query parameter. If it exceeds this length, the parameter value
+ * will be truncated at the max and a "..." will be appended. Note this affects
+ * both {@code argString()} and {@code argClobString()} methods.
+ */
+ int maxStringLengthParam();
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsDefault.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsDefault.java
new file mode 100644
index 0000000..44a4911
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsDefault.java
@@ -0,0 +1,84 @@
+package org.xbib.jdbc.query;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * Control various optional behavior for the database interactions.
+ */
+public class OptionsDefault implements Options {
+
+ private final Flavor flavor;
+
+ public OptionsDefault(Flavor flavor) {
+ this.flavor = flavor;
+ }
+
+ @Override
+ public boolean allowTransactionControl() {
+ return false;
+ }
+
+ @Override
+ public boolean ignoreTransactionControl() {
+ return false;
+ }
+
+ @Override
+ public boolean allowConnectionAccess() {
+ return false;
+ }
+
+ @Override
+ public boolean isLogParameters() {
+ return false;
+ }
+
+ @Override
+ public boolean isDetailedExceptions() {
+ return false;
+ }
+
+ @Override
+ public String generateErrorCode() {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd:H:m:s");
+ return sdf.format(new Date()) + "-" + Math.round(Math.random() * 1000000);
+ }
+
+ @Override
+ public boolean useBytesForBlob() {
+ return flavor().useBytesForBlob();
+ }
+
+ @Override
+ public boolean useStringForClob() {
+ return flavor().useStringForClob();
+ }
+
+ @Override
+ public Flavor flavor() {
+ return flavor;
+ }
+
+ @Override
+ public Date currentDate() {
+ return new Date();
+ }
+
+ @Override
+ public boolean useDatePerAppOnly() {
+ return false;
+ }
+
+ @Override
+ public Calendar calendarForTimestamps() {
+ return Calendar.getInstance(TimeZone.getDefault());
+ }
+
+ @Override
+ public int maxStringLengthParam() {
+ return 4000;
+ }
+}
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
new file mode 100644
index 0000000..6994a0a
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsOverride.java
@@ -0,0 +1,108 @@
+package org.xbib.jdbc.query;
+
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * Base class for selectively overriding another Options object.
+ */
+public class OptionsOverride implements Options {
+
+ private Options parent;
+
+ /**
+ * Wrap another {@code Options} and defer to it for anything we choose not
+ * to override.
+ */
+ public OptionsOverride(Options parent) {
+ this.parent = parent;
+ }
+
+ /**
+ * Defer to OptionsDefault for anything that is not specified, and use postgresql flavor.
+ */
+ public OptionsOverride() {
+ parent = new OptionsDefault(Flavor.postgresql);
+ }
+
+ /**
+ * Defer to OptionsDefault for anything that is not specified, using the specified flavor.
+ */
+ public OptionsOverride(Flavor flavor) {
+ parent = new OptionsDefault(flavor);
+ }
+
+ public void setParent(Options parent) {
+ this.parent = parent;
+ }
+
+ public OptionsOverride withParent(Options parent) {
+ this.parent = parent;
+ return this;
+ }
+
+ @Override
+ public boolean allowTransactionControl() {
+ return parent.allowTransactionControl();
+ }
+
+ @Override
+ public boolean ignoreTransactionControl() {
+ return parent.ignoreTransactionControl();
+ }
+
+ @Override
+ public boolean allowConnectionAccess() {
+ return parent.allowConnectionAccess();
+ }
+
+ @Override
+ public boolean isLogParameters() {
+ return parent.isLogParameters();
+ }
+
+ @Override
+ public boolean isDetailedExceptions() {
+ return parent.isDetailedExceptions();
+ }
+
+ @Override
+ public String generateErrorCode() {
+ return parent.generateErrorCode();
+ }
+
+ @Override
+ public boolean useBytesForBlob() {
+ return parent.useBytesForBlob();
+ }
+
+ @Override
+ public boolean useStringForClob() {
+ return parent.useStringForClob();
+ }
+
+ @Override
+ public Flavor flavor() {
+ return parent.flavor();
+ }
+
+ @Override
+ public Date currentDate() {
+ return parent.currentDate();
+ }
+
+ @Override
+ public boolean useDatePerAppOnly() {
+ return parent.useDatePerAppOnly();
+ }
+
+ @Override
+ public Calendar calendarForTimestamps() {
+ return parent.calendarForTimestamps();
+ }
+
+ @Override
+ public int maxStringLengthParam() {
+ return parent.maxStringLengthParam();
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/QueryTimedOutException.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/QueryTimedOutException.java
new file mode 100644
index 0000000..1ba9796
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/QueryTimedOutException.java
@@ -0,0 +1,13 @@
+package org.xbib.jdbc.query;
+
+/**
+ * Thrown when a query is interrupted because a timeout was exceeded or it was
+ * explicitly cancelled.
+ */
+@SuppressWarnings("serial")
+public class QueryTimedOutException extends DatabaseException {
+
+ public QueryTimedOutException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Row.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Row.java
new file mode 100644
index 0000000..3f99ebe
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Row.java
@@ -0,0 +1,435 @@
+package org.xbib.jdbc.query;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.sql.ResultSetMetaData;
+import java.time.LocalDate;
+import java.util.Date;
+
+/**
+ * Interface for reading results from a database query.
+ */
+public interface Row {
+ /**
+ * Obtain the names of the columns in the database. You probably want to
+ * avoid this method if possible, as the way column names are handled varies
+ * by database and driver. For example, Derby and Oracle normally convert
+ * column names to uppercase, while PostgreSQL normally converts column
+ * names to lowercase. If you do use this method, you might want to either
+ * call toUppercase()/toLowercase() or ensure the SQL explicitly specifies
+ * parameters with AS "FOO" (including quotes) to ensure your desired name
+ * will be honored.
+ */
+ String[] getColumnLabels();
+
+ /**
+ * Get raw access to the underlying JDBC metadata.
+ */
+ ResultSetMetaData getMetadata();
+
+ /**
+ * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database,
+ * typically in a {@code CHAR(1)} column. This reads the value and converts it
+ * to {@code Boolean} or {@code null} as appropriate.
+ *
+ *
This is a short-hand method that reads columns in order, starting
+ * with the first, and automatically incrementing the column index.
+ *
+ *
If you call one of the methods using an explicit column index or column name before
+ * calling this method, it will pick up at the next column following the explicit one.
+ * For example:
+ *
+ *
+ * getX(); // column 1
+ * getX(5); // or getX("foo") if foo is column 5
+ * getX(); // column 6
+ *
+ *
+ * @return true if the value was "Y", false if it was "N", or null
+ * @throws DatabaseException if the value was something other than Y, N, or null
+ */
+ Boolean getBooleanOrNull();
+
+ /**
+ * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database,
+ * typically in a {@code CHAR(1)} column. This reads the value and converts it
+ * to {@code Boolean} or {@code null} as appropriate.
+ *
+ * @param columnOneBased column number to read (1 is the first column)
+ * @return true if the value was "Y", false if it was "N", or null
+ * @throws DatabaseException if the value was something other than Y, N, or null
+ */
+ Boolean getBooleanOrNull(int columnOneBased);
+
+ /**
+ * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database,
+ * typically in a {@code CHAR(1)} column. This reads the value and converts it
+ * to {@code Boolean} or {@code null} as appropriate.
+ *
+ * @param columnName SQL alias of the column to read (use all lowercase)
+ * @return true if the value was "Y", false if it was "N", or null
+ * @throws DatabaseException if the value was something other than Y, N, or null
+ */
+ Boolean getBooleanOrNull(String columnName);
+
+ /**
+ * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database,
+ * typically in a {@code CHAR(1)} column. This reads the value and converts it
+ * to a {@code boolean}. If the value is {@code null}, it will be converted to {@code false}.
+ *
+ *
This is a short-hand method that reads columns in order, starting
+ * with the first, and automatically incrementing the column index.
+ *
+ *
If you call one of the methods using an explicit column index or column name before
+ * calling this method, it will pick up at the next column following the explicit one.
+ * For example:
+ *
+ *
+ * getX(); // column 1
+ * getX(5); // or getX("foo") if foo is column 5
+ * getX(); // column 6
+ *
+ *
+ * @return true if the value was "Y", false if it was either "N" or null
+ * @throws DatabaseException if the value was something other than Y, N, or null
+ */
+ boolean getBooleanOrFalse();
+
+ /**
+ * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database,
+ * typically in a {@code CHAR(1)} column. This reads the value and converts it
+ * to a {@code boolean}. If the value is {@code null}, it will be converted to {@code false}.
+ *
+ *
This is a short-hand method that reads columns in order, starting
+ * with the first, and automatically incrementing the column index.
+ *
+ * @param columnOneBased column number to read (1 is the first column)
+ * @return true if the value was "Y", false if it was either "N" or null
+ * @throws DatabaseException if the value was something other than Y, N, or null
+ */
+ boolean getBooleanOrFalse(int columnOneBased);
+
+ /**
+ * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database,
+ * typically in a {@code CHAR(1)} column. This reads the value and converts it
+ * to a {@code boolean}. If the value is {@code null}, it will be converted to {@code false}.
+ *
+ *
This is a short-hand method that reads columns in order, starting
+ * with the first, and automatically incrementing the column index.
+ *
+ * @param columnName SQL alias of the column to read (use all lowercase)
+ * @return true if the value was "Y", false if it was either "N" or null
+ * @throws DatabaseException if the value was something other than Y, N, or null
+ */
+ boolean getBooleanOrFalse(String columnName);
+
+ /**
+ * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database,
+ * typically in a {@code CHAR(1)} column. This reads the value and converts it
+ * to a {@code boolean}. If the value is {@code null}, it will be converted to {@code true}.
+ *
+ *
This is a short-hand method that reads columns in order, starting
+ * with the first, and automatically incrementing the column index.
+ *
+ *
If you call one of the methods using an explicit column index or column name before
+ * calling this method, it will pick up at the next column following the explicit one.
+ * For example:
+ *
+ *
+ * getX(); // column 1
+ * getX(5); // or getX("foo") if foo is column 5
+ * getX(); // column 6
+ *
+ *
+ * @return true if the value was either "Y" or null, false if it was "N"
+ * @throws DatabaseException if the value was something other than Y, N, or null
+ */
+ boolean getBooleanOrTrue();
+
+ /**
+ * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database,
+ * typically in a {@code CHAR(1)} column. This reads the value and converts it
+ * to a {@code boolean}. If the value is {@code null}, it will be converted to {@code true}.
+ *
+ *
This is a short-hand method that reads columns in order, starting
+ * with the first, and automatically incrementing the column index.
+ *
+ * @param columnOneBased column number to read (1 is the first column)
+ * @return true if the value was either "Y" or null, false if it was "N"
+ * @throws DatabaseException if the value was something other than Y, N, or null
+ */
+ boolean getBooleanOrTrue(int columnOneBased);
+
+ /**
+ * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database,
+ * typically in a {@code CHAR(1)} column. This reads the value and converts it
+ * to a {@code boolean}. If the value is {@code null}, it will be converted to {@code true}.
+ *
+ *
This is a short-hand method that reads columns in order, starting
+ * with the first, and automatically incrementing the column index.
+ *
+ * @param columnName SQL alias of the column to read (use all lowercase)
+ * @return true if the value was either "Y" or null, false if it was "N"
+ * @throws DatabaseException if the value was something other than Y, N, or null
+ */
+ boolean getBooleanOrTrue(String columnName);
+
+ Integer getIntegerOrNull();
+
+ Integer getIntegerOrNull(int columnOneBased);
+
+ Integer getIntegerOrNull(String columnName);
+
+ int getIntegerOrZero();
+
+ int getIntegerOrZero(int columnOneBased);
+
+ int getIntegerOrZero(String columnName);
+
+ Long getLongOrNull();
+
+ Long getLongOrNull(int columnOneBased);
+
+ Long getLongOrNull(String columnName);
+
+ long getLongOrZero();
+
+ long getLongOrZero(int columnOneBased);
+
+ long getLongOrZero(String columnName);
+
+ Float getFloatOrNull();
+
+ Float getFloatOrNull(int columnOneBased);
+
+ Float getFloatOrNull(String columnName);
+
+ float getFloatOrZero();
+
+ float getFloatOrZero(int columnOneBased);
+
+ float getFloatOrZero(String columnName);
+
+ Double getDoubleOrNull();
+
+ Double getDoubleOrNull(int columnOneBased);
+
+ Double getDoubleOrNull(String columnName);
+
+ double getDoubleOrZero();
+
+ double getDoubleOrZero(int columnOneBased);
+
+ double getDoubleOrZero(String columnName);
+
+ /**
+ * Note this method attempts to correct for "artifical" scale due to the database
+ * representation. Some databases will pad the number out to "full precision". This
+ * method tries to reduce scale if there is zero padding to the right of the decimal.
+ */
+
+ BigDecimal getBigDecimalOrNull();
+
+
+ BigDecimal getBigDecimalOrNull(int columnOneBased);
+
+
+ BigDecimal getBigDecimalOrNull(String columnName);
+
+
+ BigDecimal getBigDecimalOrZero();
+
+
+ BigDecimal getBigDecimalOrZero(int columnOneBased);
+
+
+ BigDecimal getBigDecimalOrZero(String columnName);
+
+ /**
+ * @return the value, or null if it is SQL null; never returns the empty string
+ */
+
+ String getStringOrNull();
+
+ /**
+ * @return the value, or null if it is SQL null; never returns the empty string
+ */
+
+ String getStringOrNull(int columnOneBased);
+
+ /**
+ * @return the value, or null if it is SQL null; never returns the empty string
+ */
+
+ String getStringOrNull(String columnName);
+
+ /**
+ * @return the value, or the empty string if it is SQL null; never returns null
+ */
+
+ String getStringOrEmpty();
+
+ /**
+ * @return the value, or the empty string if it is SQL null; never returns null
+ */
+
+ String getStringOrEmpty(int columnOneBased);
+
+ /**
+ * @return the value, or the empty string if it is SQL null; never returns null
+ */
+
+ String getStringOrEmpty(String columnName);
+
+ /**
+ * @return the value, or null if it is SQL null; never returns the empty string
+ */
+
+ String getClobStringOrNull();
+
+ /**
+ * @return the value, or null if it is SQL null; never returns the empty string
+ */
+
+ String getClobStringOrNull(int columnOneBased);
+
+ /**
+ * @return the value, or null if it is SQL null; never returns the empty string
+ */
+
+ String getClobStringOrNull(String columnName);
+
+ /**
+ * @return the value, or the empty string if it is SQL null; never returns null
+ */
+
+ String getClobStringOrEmpty();
+
+ /**
+ * @return the value, or the empty string if it is SQL null; never returns null
+ */
+
+ String getClobStringOrEmpty(int columnOneBased);
+
+ /**
+ * @return the value, or the empty string if it is SQL null; never returns null
+ */
+
+ String getClobStringOrEmpty(String columnName);
+
+ /**
+ * @return the value, or null if it is SQL null
+ */
+
+ Reader getClobReaderOrNull();
+
+ /**
+ * @return the value, or null if it is SQL null
+ */
+
+ Reader getClobReaderOrNull(int columnOneBased);
+
+ /**
+ * @return the value, or null if it is SQL null
+ */
+
+ Reader getClobReaderOrNull(String columnName);
+
+ /**
+ * @return the value, or a StringReader containing the empty string if it is SQL null
+ */
+
+ Reader getClobReaderOrEmpty();
+
+ /**
+ * @return the value, or a StringReader containing the empty string if it is SQL null
+ */
+
+ Reader getClobReaderOrEmpty(int columnOneBased);
+
+ /**
+ * @return the value, or a StringReader containing the empty string if it is SQL null
+ */
+
+ Reader getClobReaderOrEmpty(String columnName);
+
+
+ byte[] getBlobBytesOrNull();
+
+
+ byte[] getBlobBytesOrNull(int columnOneBased);
+
+
+ byte[] getBlobBytesOrNull(String columnName);
+
+
+ byte[] getBlobBytesOrZeroLen();
+
+
+ byte[] getBlobBytesOrZeroLen(int columnOneBased);
+
+
+ byte[] getBlobBytesOrZeroLen(String columnName);
+
+
+ InputStream getBlobInputStreamOrNull();
+
+
+ InputStream getBlobInputStreamOrNull(int columnOneBased);
+
+
+ InputStream getBlobInputStreamOrNull(String columnName);
+
+
+ InputStream getBlobInputStreamOrEmpty();
+
+
+ InputStream getBlobInputStreamOrEmpty(int columnOneBased);
+
+
+ InputStream getBlobInputStreamOrEmpty(String columnName);
+
+ /**
+ * Return the millisecond precision Date, which should be represented as a TIMESTAMP
+ * in the database. The nanoseconds are truncated.
+ */
+
+ Date getDateOrNull();
+
+ /**
+ * Return the millisecond precision Date, which should be represented as a TIMESTAMP
+ * in the database. The nanoseconds are truncated.
+ */
+
+ Date getDateOrNull(int columnOneBased);
+
+
+ Date getDateOrNull(String columnName);
+
+ /**
+ * Retrieve column as LocalDate, .i.e, date with no time.
+ *
+ * @return LocalDate of the database column value
+ */
+
+ LocalDate getLocalDateOrNull();
+
+ /**
+ * Get the Date field, with no timestamp
+ *
+ * @param columnOneBased column number starting at 1, not 0
+ * @return LocalDate of the column value
+ */
+
+ LocalDate getLocalDateOrNull(int columnOneBased);
+
+ /**
+ * Get the Date field, with no timestamp
+ *
+ * @param columnName column name to retrieve
+ * @return LocalDate of the column value
+ */
+
+ LocalDate getLocalDateOrNull(String columnName);
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/RowHandler.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/RowHandler.java
new file mode 100644
index 0000000..47c13bb
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/RowHandler.java
@@ -0,0 +1,8 @@
+package org.xbib.jdbc.query;
+
+/**
+ * Type-safe callback to read query results.
+ */
+public interface RowHandler {
+ T process(Row r) throws Exception;
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Rows.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Rows.java
new file mode 100644
index 0000000..595be05
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Rows.java
@@ -0,0 +1,8 @@
+package org.xbib.jdbc.query;
+
+/**
+ * Interface for reading results from a database query.
+ */
+public interface Rows extends Row {
+ boolean next();
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/RowsAdaptor.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/RowsAdaptor.java
new file mode 100644
index 0000000..4b66444
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/RowsAdaptor.java
@@ -0,0 +1,835 @@
+package org.xbib.jdbc.query;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.util.Date;
+
+/**
+ * Safely wrap a ResultSet and provide access to the data it contains.
+ */
+class RowsAdaptor implements Rows {
+
+ private final ResultSet rs;
+
+ private final Options options;
+
+ private int column = 1;
+
+ public RowsAdaptor(ResultSet rs, Options options) {
+ this.rs = rs;
+ this.options = options;
+ }
+
+ @Override
+ public boolean next() {
+ try {
+ column = 1;
+ return rs.next();
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+
+ @Override
+ public String[] getColumnLabels() {
+ try {
+ ResultSetMetaData metaData = rs.getMetaData();
+ String[] names = new String[metaData.getColumnCount()];
+ for (int i = 0; i < names.length; i++) {
+ names[i] = metaData.getColumnLabel(i + 1);
+ }
+ return names;
+ } catch (SQLException e) {
+ throw new DatabaseException("Unable to retrieve metadata from ResultSet", e);
+ }
+ }
+
+
+ @Override
+ public ResultSetMetaData getMetadata() {
+ try {
+ return rs.getMetaData();
+ } catch (SQLException e) {
+ throw new DatabaseException("Unable to retrieve metadata from ResultSet", e);
+ }
+ }
+
+
+ @Override
+ public Boolean getBooleanOrNull() {
+ return getBooleanOrNull(column++);
+ }
+
+
+ @Override
+ public Boolean getBooleanOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ return toBoolean(rs, columnOneBased);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+
+ @Override
+ public Boolean getBooleanOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ return toBoolean(rs, columnName);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public boolean getBooleanOrFalse() {
+ return getBooleanOrFalse(column++);
+ }
+
+ @Override
+ public boolean getBooleanOrFalse(int columnOneBased) {
+ Boolean result = getBooleanOrNull(columnOneBased);
+ if (result == null) {
+ result = Boolean.FALSE;
+ }
+ return result;
+ }
+
+ @Override
+ public boolean getBooleanOrFalse(String columnName) {
+ Boolean result = getBooleanOrNull(columnName);
+ if (result == null) {
+ result = Boolean.FALSE;
+ }
+ return result;
+ }
+
+ @Override
+ public boolean getBooleanOrTrue() {
+ return getBooleanOrTrue(column++);
+ }
+
+ @Override
+ public boolean getBooleanOrTrue(int columnOneBased) {
+ Boolean result = getBooleanOrNull(columnOneBased);
+ if (result == null) {
+ result = Boolean.TRUE;
+ }
+ return result;
+ }
+
+ @Override
+ public boolean getBooleanOrTrue(String columnName) {
+ Boolean result = getBooleanOrNull(columnName);
+ if (result == null) {
+ result = Boolean.TRUE;
+ }
+ return result;
+ }
+
+
+ @Override
+ public Integer getIntegerOrNull() {
+ return getIntegerOrNull(column++);
+ }
+
+ @Override
+ public Integer getIntegerOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ return toInteger(rs, columnOneBased);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public Integer getIntegerOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ return toInteger(rs, columnName);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public int getIntegerOrZero() {
+ return getIntegerOrZero(column++);
+ }
+
+ @Override
+ public int getIntegerOrZero(int columnOneBased) {
+ Integer result = getIntegerOrNull(columnOneBased);
+ if (result == null) {
+ result = 0;
+ }
+ return result;
+ }
+
+ @Override
+ public int getIntegerOrZero(String columnName) {
+ Integer result = getIntegerOrNull(columnName);
+ if (result == null) {
+ result = 0;
+ }
+ return result;
+ }
+
+
+ @Override
+ public Long getLongOrNull() {
+ return getLongOrNull(column++);
+ }
+
+ @Override
+ public Long getLongOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ return toLong(rs, columnOneBased);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public Long getLongOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ return toLong(rs, columnName);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public long getLongOrZero() {
+ return getLongOrZero(column++);
+ }
+
+ @Override
+ public long getLongOrZero(int columnOneBased) {
+ Long result = getLongOrNull(columnOneBased);
+ if (result == null) {
+ result = 0L;
+ }
+ return result;
+ }
+
+ @Override
+ public long getLongOrZero(String columnName) {
+ Long result = getLongOrNull(columnName);
+ if (result == null) {
+ result = 0L;
+ }
+ return result;
+ }
+
+
+ @Override
+ public Float getFloatOrNull() {
+ return getFloatOrNull(column++);
+ }
+
+ @Override
+ public Float getFloatOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ return toFloat(rs, columnOneBased);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public Float getFloatOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ return toFloat(rs, columnName);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public float getFloatOrZero() {
+ return getFloatOrZero(column++);
+ }
+
+ @Override
+ public float getFloatOrZero(int columnOneBased) {
+ Float result = getFloatOrNull(columnOneBased);
+ if (result == null) {
+ result = 0f;
+ }
+ return result;
+ }
+
+ @Override
+ public float getFloatOrZero(String columnName) {
+ Float result = getFloatOrNull(columnName);
+ if (result == null) {
+ result = 0f;
+ }
+ return result;
+ }
+
+
+ @Override
+ public Double getDoubleOrNull() {
+ return getDoubleOrNull(column++);
+ }
+
+ @Override
+ public Double getDoubleOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ return toDouble(rs, columnOneBased);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public Double getDoubleOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ return toDouble(rs, columnName);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public double getDoubleOrZero() {
+ return getDoubleOrZero(column++);
+ }
+
+ @Override
+ public double getDoubleOrZero(int columnOneBased) {
+ Double result = getDoubleOrNull(columnOneBased);
+ if (result == null) {
+ result = 0d;
+ }
+ return result;
+ }
+
+ @Override
+ public double getDoubleOrZero(String columnName) {
+ Double result = getDoubleOrNull(columnName);
+ if (result == null) {
+ result = 0d;
+ }
+ return result;
+ }
+
+
+ @Override
+ public BigDecimal getBigDecimalOrNull() {
+ return getBigDecimalOrNull(column++);
+ }
+
+ @Override
+ public BigDecimal getBigDecimalOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ return toBigDecimal(rs, columnOneBased);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public BigDecimal getBigDecimalOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ return toBigDecimal(rs, columnName);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+
+ @Override
+ public BigDecimal getBigDecimalOrZero() {
+ return getBigDecimalOrZero(column++);
+ }
+
+
+ @Override
+ public BigDecimal getBigDecimalOrZero(int columnOneBased) {
+ BigDecimal result = getBigDecimalOrNull(columnOneBased);
+ if (result == null) {
+ result = BigDecimal.ZERO;
+ }
+ return result;
+ }
+
+
+ @Override
+ public BigDecimal getBigDecimalOrZero(String columnName) {
+ BigDecimal result = getBigDecimalOrNull(columnName);
+ if (result == null) {
+ result = BigDecimal.ZERO;
+ }
+ return result;
+ }
+
+
+ @Override
+ public String getStringOrNull() {
+ return getStringOrNull(column++);
+ }
+
+ @Override
+ public String getStringOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ String result = rs.getString(columnOneBased);
+ if (result != null && result.length() == 0) {
+ result = null;
+ }
+ return result;
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public String getStringOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ String result = rs.getString(columnName);
+ if (result != null && result.length() == 0) {
+ result = null;
+ }
+ return result;
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+
+ @Override
+ public String getStringOrEmpty() {
+ return getStringOrEmpty(column++);
+ }
+
+
+ @Override
+ public String getStringOrEmpty(int columnOneBased) {
+ String result = getStringOrNull(columnOneBased);
+ if (result == null) {
+ result = "";
+ }
+ return result;
+ }
+
+
+ @Override
+ public String getStringOrEmpty(String columnName) {
+ String result = getStringOrNull(columnName);
+ if (result == null) {
+ result = "";
+ }
+ return result;
+ }
+
+
+ @Override
+ public String getClobStringOrNull() {
+ return getClobStringOrNull(column++);
+ }
+
+ @Override
+ public String getClobStringOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ String result = rs.getString(columnOneBased);
+ if (result != null && result.length() == 0) {
+ result = null;
+ }
+ return result;
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public String getClobStringOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ String result = rs.getString(columnName);
+ if (result != null && result.length() == 0) {
+ result = null;
+ }
+ return result;
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+
+ @Override
+ public String getClobStringOrEmpty() {
+ return getClobStringOrEmpty(column++);
+ }
+
+
+ @Override
+ public String getClobStringOrEmpty(int columnOneBased) {
+ String result = getClobStringOrNull(columnOneBased);
+ if (result == null) {
+ result = "";
+ }
+ return result;
+ }
+
+
+ @Override
+ public String getClobStringOrEmpty(String columnName) {
+ String result = getClobStringOrNull(columnName);
+ if (result == null) {
+ result = "";
+ }
+ return result;
+ }
+
+
+ @Override
+ public Reader getClobReaderOrNull() {
+ return getClobReaderOrNull(column++);
+ }
+
+ @Override
+ public Reader getClobReaderOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ return rs.getCharacterStream(columnOneBased);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public Reader getClobReaderOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ return rs.getCharacterStream(columnName);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+
+ @Override
+ public Reader getClobReaderOrEmpty() {
+ return getClobReaderOrEmpty(column++);
+ }
+
+
+ @Override
+ public Reader getClobReaderOrEmpty(int columnOneBased) {
+ Reader result = getClobReaderOrNull(columnOneBased);
+ if (result == null) {
+ result = new StringReader("");
+ }
+ return result;
+ }
+
+
+ @Override
+ public Reader getClobReaderOrEmpty(String columnName) {
+ Reader result = getClobReaderOrNull(columnName);
+ if (result == null) {
+ result = new StringReader("");
+ }
+ return result;
+ }
+
+
+ @Override
+ public byte[] getBlobBytesOrNull() {
+ return getBlobBytesOrNull(column++);
+ }
+
+ @Override
+ public byte[] getBlobBytesOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ return rs.getBytes(columnOneBased);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public byte[] getBlobBytesOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ return rs.getBytes(columnName);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+
+ @Override
+ public byte[] getBlobBytesOrZeroLen() {
+ return getBlobBytesOrZeroLen(column++);
+ }
+
+
+ @Override
+ public byte[] getBlobBytesOrZeroLen(int columnOneBased) {
+ byte[] result = getBlobBytesOrNull(columnOneBased);
+ if (result == null) {
+ result = new byte[0];
+ }
+ return result;
+ }
+
+
+ @Override
+ public byte[] getBlobBytesOrZeroLen(String columnName) {
+ byte[] result = getBlobBytesOrNull(columnName);
+ if (result == null) {
+ result = new byte[0];
+ }
+ return result;
+ }
+
+
+ @Override
+ public InputStream getBlobInputStreamOrNull() {
+ return getBlobInputStreamOrNull(column++);
+ }
+
+ @Override
+ public InputStream getBlobInputStreamOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ return rs.getBinaryStream(columnOneBased);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ @Override
+ public InputStream getBlobInputStreamOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ return rs.getBinaryStream(columnName);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+
+ @Override
+ public InputStream getBlobInputStreamOrEmpty() {
+ return getBlobInputStreamOrEmpty(column++);
+ }
+
+
+ @Override
+ public InputStream getBlobInputStreamOrEmpty(int columnOneBased) {
+ InputStream result = getBlobInputStreamOrNull(columnOneBased);
+ if (result == null) {
+ result = new ByteArrayInputStream(new byte[0]);
+ }
+ return result;
+ }
+
+
+ @Override
+ public InputStream getBlobInputStreamOrEmpty(String columnName) {
+ InputStream result = getBlobInputStreamOrNull(columnName);
+ if (result == null) {
+ result = new ByteArrayInputStream(new byte[0]);
+ }
+ return result;
+ }
+
+
+ @Override
+ public Date getDateOrNull() {
+ return getDateOrNull(column++);
+ }
+
+
+ @Override
+ public Date getDateOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ return toDate(rs, columnOneBased);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+
+ @Override
+ public Date getDateOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ return toDate(rs, columnName);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+
+ @Override
+ public LocalDate getLocalDateOrNull() {
+ return getLocalDateOrNull(column++);
+ }
+
+
+ @Override
+ public LocalDate getLocalDateOrNull(int columnOneBased) {
+ try {
+ column = columnOneBased + 1;
+ return toLocalDate(rs, columnOneBased);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+
+ @Override
+ public LocalDate getLocalDateOrNull(String columnName) {
+ try {
+ column = rs.findColumn(columnName) + 1;
+ return toLocalDate(rs, columnName);
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ }
+ }
+
+ /**
+ * Make sure the Timestamp will return getTime() accurate to the millisecond
+ * (if possible) and truncate away nanoseconds.
+ */
+ private Date timestampToDate(Timestamp ts) {
+ long millis = ts.getTime();
+ int nanos = ts.getNanos();
+ return new Date(millis / 1000 * 1000 + nanos / 1000000);
+ }
+
+ private Date toDate(ResultSet rs, int col) throws SQLException {
+ Timestamp val = rs.getTimestamp(col, options.calendarForTimestamps());
+ return val == null ? null : timestampToDate(val);
+ }
+
+ private Date toDate(ResultSet rs, String col) throws SQLException {
+ Timestamp val = rs.getTimestamp(col, options.calendarForTimestamps());
+ return val == null ? null : timestampToDate(val);
+ }
+
+ private LocalDate toLocalDate(ResultSet rs, int col) throws SQLException {
+ java.sql.Date val = rs.getDate(col);
+ return val == null ? null : val.toLocalDate();
+ }
+
+ private LocalDate toLocalDate(ResultSet rs, String col) throws SQLException {
+ java.sql.Date val = rs.getDate(col);
+ return val == null ? null : val.toLocalDate();
+ }
+
+ private Boolean toBoolean(ResultSet rs, int col) throws SQLException {
+ String val = rs.getString(col);
+ if (val == null) {
+ return null;
+ } else if (val.equals("Y") || val.equals("1")) {
+ return Boolean.TRUE;
+ } else if (val.equals("N") || val.equals("0")) {
+ return Boolean.FALSE;
+ } else {
+ throw new DatabaseException("Reading boolean from column " + col + " but the value was not 'Y' or 'N'");
+ }
+ }
+
+ private Boolean toBoolean(ResultSet rs, String col) throws SQLException {
+ String val = rs.getString(col);
+ if (val == null) {
+ return null;
+ } else if (val.equals("Y") || val.equals("1")) {
+ return Boolean.TRUE;
+ } else if (val.equals("N") || val.equals("0")) {
+ return Boolean.FALSE;
+ } else {
+ throw new DatabaseException("Reading boolean from column \"" + col + "\" but the value was not 'Y' or 'N'");
+ }
+ }
+
+ private Integer toInteger(ResultSet rs, int col) throws SQLException {
+ int val = rs.getInt(col);
+ return rs.wasNull() ? null : val;
+ }
+
+ private Integer toInteger(ResultSet rs, String col) throws SQLException {
+ int val = rs.getInt(col);
+ return rs.wasNull() ? null : val;
+ }
+
+ private Long toLong(ResultSet rs, int col) throws SQLException {
+ long val = rs.getLong(col);
+ return rs.wasNull() ? null : val;
+ }
+
+ private Long toLong(ResultSet rs, String col) throws SQLException {
+ long val = rs.getLong(col);
+ return rs.wasNull() ? null : val;
+ }
+
+ private Float toFloat(ResultSet rs, int col) throws SQLException {
+ float val = rs.getFloat(col);
+ return rs.wasNull() ? null : val;
+ }
+
+ private Float toFloat(ResultSet rs, String col) throws SQLException {
+ float val = rs.getFloat(col);
+ return rs.wasNull() ? null : val;
+ }
+
+ private Double toDouble(ResultSet rs, int col) throws SQLException {
+ double val = rs.getDouble(col);
+ return rs.wasNull() ? null : val;
+ }
+
+ private Double toDouble(ResultSet rs, String col) throws SQLException {
+ double val = rs.getDouble(col);
+ return rs.wasNull() ? null : val;
+ }
+
+ private BigDecimal fixBigDecimal(BigDecimal val) {
+ if (val.scale() > 0) {
+ val = val.stripTrailingZeros();
+ if (val.scale() < 0) {
+ val = val.setScale(0);
+ }
+ }
+ return val;
+ }
+
+ private BigDecimal toBigDecimal(ResultSet rs, int col) throws SQLException {
+ BigDecimal val = rs.getBigDecimal(col);
+ return val == null ? null : fixBigDecimal(val);
+ }
+
+ private BigDecimal toBigDecimal(ResultSet rs, String col) throws SQLException {
+ BigDecimal val = rs.getBigDecimal(col);
+ return val == null ? null : fixBigDecimal(val);
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/RowsHandler.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/RowsHandler.java
new file mode 100644
index 0000000..968229b
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/RowsHandler.java
@@ -0,0 +1,9 @@
+package org.xbib.jdbc.query;
+
+/**
+ * Type-safe callback to read query results.
+ */
+@FunctionalInterface
+public interface RowsHandler {
+ T process(Rows rs) throws Exception;
+}
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
new file mode 100644
index 0000000..0b5809c
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Schema.java
@@ -0,0 +1,949 @@
+package org.xbib.jdbc.query;
+
+import org.xbib.jdbc.query.Schema.Table.Check;
+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 java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * Java representation of a database schema with the various things it can contain.
+ */
+public class Schema {
+
+ private final List
tables = new ArrayList<>();
+
+ private final List sequences = new ArrayList<>();
+
+ private boolean indexForeignKeys = true;
+
+ private String userTableName = "user_principal";
+
+ public Sequence addSequence(String name) {
+ Sequence sequence = new Sequence(name);
+ sequences.add(sequence);
+ return sequence;
+ }
+
+ public Schema withoutForeignKeyIndexing() {
+ indexForeignKeys = false;
+ return this;
+ }
+
+ /**
+ * Set the table to which the foreign key will be created for
+ * user change tracking ({@link Table#trackCreateTimeAndUser(String)}
+ * and {@link Table#trackUpdateTimeAndUser(String)}).
+ *
+ * @param userTableName the default table name containing users
+ */
+ public Schema userTableName(String userTableName) {
+ this.userTableName = userTableName;
+ return this;
+ }
+
+ public void validate() {
+ for (Table t : tables) {
+ t.validate();
+ }
+ for (Sequence s : sequences) {
+ s.validate();
+ }
+ }
+
+ public Table addTable(String name) {
+ Table table = new Table(name);
+ tables.add(table);
+ return table;
+ }
+
+ public Table addTableFromRow(String tableName, Row r) {
+ Table table = addTable(tableName);
+ try {
+ ResultSetMetaData metadata = r.getMetadata();
+
+ int columnCount = metadata.getColumnCount();
+ String[] names = new String[columnCount];
+ for (int i = 0; i < columnCount; i++) {
+ names[i] = metadata.getColumnName(i + 1);
+ }
+ names = SqlArgs.tidyColumnNames(names);
+
+ for (int i = 0; i < columnCount; i++) {
+ int type = metadata.getColumnType(i + 1);
+
+ switch (type) {
+ case Types.SMALLINT:
+ case Types.INTEGER:
+ table.addColumn(names[i]).asInteger();
+ break;
+ case Types.BIGINT:
+ table.addColumn(names[i]).asLong();
+ break;
+ case Types.REAL:
+ case 100: // Oracle proprietary it seems
+ table.addColumn(names[i]).asFloat();
+ break;
+ case Types.DOUBLE:
+ case 101: // Oracle proprietary it seems
+ table.addColumn(names[i]).asDouble();
+ break;
+ case Types.NUMERIC:
+ int precision1 = metadata.getPrecision(i + 1);
+ int scale = metadata.getScale(i + 1);
+ if (precision1 == 10 && scale == 0) {
+ // Oracle reports integer as numeric
+ table.addColumn(names[i]).asInteger();
+ } else if (precision1 == 19 && scale == 0) {
+ // Oracle reports long as numeric
+ table.addColumn(names[i]).asLong();
+ } else if (precision1 == 126 && scale == -127) {
+ // this clause was added to support ETL from MSSQL Server
+ table.addColumn(names[i]).asFloat();
+ } else if (precision1 == 0 && scale == -127) {
+ // this clause was also added to support ETL from MSSQL Server
+ table.addColumn(names[i]).asInteger();
+ } else {
+ table.addColumn(names[i]).asBigDecimal(precision1, scale);
+ }
+ break;
+ case Types.BINARY:
+ case Types.VARBINARY:
+ case Types.BLOB:
+ table.addColumn(names[i]).asBlob();
+ break;
+ case Types.CLOB:
+ case Types.NCLOB:
+ table.addColumn(names[i]).asClob();
+ break;
+
+ // The date type is used for a true date - no time info.
+ // It must be checked before TimeStamp because sql dates are also
+ // recognized as sql timestamp.
+ case Types.DATE:
+ table.addColumn(names[i]).asLocalDate();
+ break;
+
+ // This is the type dates and times with time and time zone associated.
+ // Note that Oracle dates are always really Timestamps.
+ case Types.TIMESTAMP:
+ // Check if we really have a LocalDate implemented by the DB as a timestamp
+ if (metadata.getScale(i + 1) == 0) {
+ // If the scale is 0, this is a LocalDate (no time/timezone).
+ // Anything with a time/timezone will have a non-zero scale
+ table.addColumn(names[i]).asLocalDate();
+ } else {
+ table.addColumn(names[i]).asDate();
+ }
+ break;
+
+ case Types.NVARCHAR:
+ case Types.VARCHAR:
+ int precision = metadata.getPrecision(i + 1);
+ if (precision >= 2147483647) {
+ // Postgres seems to report clobs are varchar(2147483647)
+ table.addColumn(names[i]).asClob();
+ } else {
+ table.addColumn(names[i]).asString(precision);
+ }
+ break;
+ case Types.CHAR:
+ case Types.NCHAR:
+ table.addColumn(names[i]).asStringFixed(metadata.getPrecision(i + 1));
+ break;
+ default:
+ throw new DatabaseException("Don't know what type to use for: " + type);
+ }
+ }
+ } catch (SQLException e) {
+ throw new DatabaseException("Unable to retrieve metadata from ResultSet", e);
+ }
+ return table;
+ }
+
+ public void execute(Supplier db) {
+ executeOrPrint(db.get(), null);
+ }
+
+ public String print(Flavor flavor) {
+ return executeOrPrint(null, flavor);
+ }
+
+ private String executeOrPrint(Database db, Flavor flavor) {
+ validate();
+
+ if (flavor == null) {
+ flavor = db.flavor();
+ }
+ StringBuilder script = new StringBuilder();
+
+ for (Table table : tables) {
+ Sql sql = new Sql();
+ sql.append("create table ").append(table.name).append(" (\n");
+ boolean first = true;
+ for (Column column : table.columns) {
+ if (first) {
+ first = false;
+ sql.append(" ");
+ } else {
+ sql.append(",\n ");
+ }
+ sql.append(rpad(column.name, 30)).append(" ");
+ switch (column.type) {
+ case Boolean:
+ sql.append(flavor.typeBoolean());
+ break;
+ case Integer:
+ sql.append(flavor.typeInteger());
+ break;
+ case Long:
+ sql.append(flavor.typeLong());
+ break;
+ case Float:
+ sql.append(flavor.typeFloat());
+ break;
+ case Double:
+ sql.append(flavor.typeDouble());
+ break;
+ case BigDecimal:
+ sql.append(flavor.typeBigDecimal(column.scale, column.precision));
+ break;
+ case StringVar:
+ sql.append(flavor.typeStringVar(column.scale));
+ break;
+ case StringFixed:
+ sql.append(flavor.typeStringFixed(column.scale));
+ break;
+ case Date:
+ sql.append(flavor.typeDate()); // Append a date with time
+ break;
+ case LocalDate:
+ sql.append(flavor.typeLocalDate()); // Append a true date - no time
+ break;
+ case Clob:
+ sql.append(flavor.typeClob());
+ break;
+ case Blob:
+ sql.append(flavor.typeBlob());
+ break;
+ }
+ if (column.notNull) {
+ sql.append(" not null");
+ }
+ }
+
+ if (table.primaryKey != null) {
+ sql.append(",\n constraint ");
+ sql.append(rpad(table.primaryKey.name, 30));
+ sql.listStart(" primary key (");
+ for (String name : table.primaryKey.columnNames) {
+ sql.listSeparator(", ");
+ sql.append(name);
+ }
+ sql.listEnd(")");
+ }
+
+ for (Unique u : table.uniques) {
+ sql.append(",\n constraint ");
+ sql.append(rpad(u.name, 30));
+ sql.listStart(" unique (");
+ for (String name : u.columnNames) {
+ sql.listSeparator(", ");
+ sql.append(name);
+ }
+ sql.listEnd(")");
+ }
+
+ for (Check check : table.checks) {
+ sql.append(",\n constraint ");
+ sql.append(rpad(check.name, 30));
+ sql.append(" check (");
+ 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 (table.comment != null) {
+ sql.append("comment on table ");
+ sql.append(table.name);
+ sql.append(" is \n'");
+ sql.append(table.comment.replace("'", "''"));
+ sql.append("'");
+ executeOrPrint(sql, db, script);
+ sql = new Sql();
+ }
+
+ for (Column c : table.columns) {
+ if (c.comment != null) {
+ sql.append("comment on column ");
+ sql.append(table.name);
+ sql.append(".");
+ sql.append(c.name);
+ sql.append(" is \n'");
+ sql.append(c.comment.replace("'", "''"));
+ sql.append("'");
+ executeOrPrint(sql, db, script);
+ sql = new Sql();
+ }
+ }
+ }
+ }
+
+ for (Table table : tables) {
+ for (ForeignKey fk : table.foreignKeys) {
+ Sql sql = new Sql();
+ sql.append("alter table ");
+ sql.append(table.name);
+ sql.append(" add constraint ");
+ sql.append(fk.name);
+ sql.listStart("\n foreign key (");
+ for (String name : fk.columnNames) {
+ sql.listSeparator(", ");
+ sql.append(name);
+ }
+ sql.listEnd(") references ");
+ sql.append(fk.foreignTable);
+ if (fk.onDeleteCascade) {
+ sql.append(" on delete cascade");
+ }
+ executeOrPrint(sql, db, script);
+ }
+ }
+
+ for (Table table : tables) {
+ for (Index index : table.indexes) {
+ Sql sql = new Sql();
+ sql.append("create ");
+ if (index.unique) {
+ sql.append("unique ");
+ }
+ sql.append("index ");
+ sql.append(index.name);
+ sql.append(" on ");
+ sql.append(table.name);
+ sql.listStart(" (");
+ for (String name : index.columnNames) {
+ sql.listSeparator(", ");
+ sql.append(name);
+ }
+ sql.listEnd(")");
+ executeOrPrint(sql, db, script);
+ }
+ }
+
+ for (Sequence sequence : sequences) {
+ Sql sql = new Sql();
+ sql.append("create sequence ");
+ sql.append(sequence.name);
+ sql.append(flavor.sequenceOptions());
+ sql.append(" minvalue ");
+ sql.append(sequence.min);
+ sql.append(" maxvalue ");
+ sql.append(sequence.max);
+ sql.append(" start with ");
+ sql.append(sequence.start);
+ sql.append(" increment by ");
+ sql.append(sequence.increment);
+ sql.append(flavor.sequenceCacheClause(sequence.cache));
+ sql.append(flavor.sequenceOrderClause(sequence.order));
+ sql.append(flavor.sequenceCycleClause(sequence.cycle));
+ executeOrPrint(sql, db, script);
+ }
+
+ if (db == null) {
+ return script.toString();
+ }
+ return null;
+ }
+
+ private void executeOrPrint(Sql sql, Database db, StringBuilder script) {
+ if (db != null) {
+ db.ddl(sql.toString()).execute();
+ } else {
+ script.append(sql.toString());
+ script.append(";\n\n");
+ }
+ }
+
+ private String toName(String name) {
+ name = name.toLowerCase().trim();
+
+ if (!name.matches("[a-z][a-z0-9_]{0,28}[a-z0-9]?")) {
+ throw new IllegalArgumentException("Identifier name should match pattern [a-z][a-z0-9_]{0,28}[a-z0-9]?");
+ }
+
+ return name;
+ }
+
+ private String rpad(String s, int size) {
+ if (s.length() < size) {
+ s += " ".substring(0, size - s.length());
+ }
+ return s;
+ }
+
+ public enum ColumnType {
+ Integer, Long, Float, Double, BigDecimal, StringVar, StringFixed, Clob, Blob, Date, LocalDate, Boolean
+ }
+
+ public class Sequence {
+ private final String name;
+ private long min = 1;
+ private long max = 999999999999999999L;
+ private int increment = 1;
+ private long start = 1;
+ private int cache = 1;
+ private boolean order;
+ private boolean cycle;
+
+ public Sequence(String name) {
+ this.name = toName(name);
+ }
+
+ public Sequence min(long min) {
+ if (start == this.min) {
+ start = min;
+ }
+ this.min = min;
+ return this;
+ }
+
+ public Sequence max(long max) {
+ this.max = max;
+ return this;
+ }
+
+ public Sequence increment(int increment) {
+ this.increment = increment;
+ return this;
+ }
+
+ public Sequence start(long start) {
+ this.start = start;
+ return this;
+ }
+
+ public Sequence cache(int cache) {
+ this.cache = cache < 2 ? 1 : cache;
+ return this;
+ }
+
+ /**
+ * On databases that support it, indicate you want to strictly order the values returned
+ * from the sequence. This is generally NOT what you want, because it can dramatically
+ * reduce performance (requires locking and synchronization). Also keep in mind it doesn't
+ * guarantee there will not be gaps in the numbers handed out (nothing you can do will
+ * ever prevent that).
+ */
+ public Sequence order() {
+ order = true;
+ return this;
+ }
+
+ public Sequence cycle() {
+ cycle = true;
+ return this;
+ }
+
+ private void validate() {
+
+ }
+
+ public Schema schema() {
+ validate();
+ return Schema.this;
+ }
+ }
+
+ public class Table {
+ private final String name;
+ private String comment;
+ private final List columns = new ArrayList<>();
+ private PrimaryKey primaryKey;
+ private final List foreignKeys = new ArrayList<>();
+ private final List indexes = new ArrayList<>();
+ private final List checks = new ArrayList<>();
+ private final List uniques = new ArrayList<>();
+ private final Map customClauses = new HashMap<>();
+ private boolean createTracking;
+ private String createTrackingFkName;
+ private String createTrackingFkTable;
+ private boolean updateTracking;
+ private String updateTrackingFkName;
+ private String updateTrackingFkTable;
+ private boolean updateSequence;
+ private boolean historyTable;
+
+ public Table(String name) {
+ this.name = toName(name);
+ if (this.name.length() > 27) {
+ throw new RuntimeException("Table name should be 27 characters or less");
+ }
+ }
+
+ public void validate() {
+ if (columns.size() < 1) {
+ throw new RuntimeException("Table " + name + " needs at least one column");
+ }
+ for (Column c : columns) {
+ c.validate();
+ }
+
+ if (primaryKey != null) {
+ primaryKey.validate();
+ }
+
+ for (ForeignKey fk : foreignKeys) {
+ fk.validate();
+ }
+
+ for (Check c : checks) {
+ c.validate();
+ }
+
+ for (Index i : indexes) {
+ i.validate();
+ }
+ }
+
+ public Schema schema() {
+ if (createTracking) {
+ addColumn("create_time").asDate().table();
+ }
+ if (createTrackingFkName != null) {
+ addColumn("create_user").foreignKey(createTrackingFkName).references(createTrackingFkTable).table();
+ }
+ if (updateTracking || updateSequence) {
+ addColumn("update_time").asDate().table();
+ }
+ if (updateTrackingFkName != null) {
+ addColumn("update_user").foreignKey(updateTrackingFkName).references(updateTrackingFkTable).table();
+ }
+ if (updateSequence) {
+ addColumn("update_sequence").asLong().table();
+ }
+ // Avoid auto-indexing foreign keys if an index already exists (the first columns of the pk or explicit index)
+ if (indexForeignKeys) {
+ for (ForeignKey fk : foreignKeys) {
+ if (primaryKey != null && 0 == Collections.indexOfSubList(primaryKey.columnNames, fk.columnNames)) {
+ continue;
+ }
+ boolean skip = false;
+ for (Index i : indexes) {
+ if (0 == Collections.indexOfSubList(i.columnNames, fk.columnNames)) {
+ skip = true;
+ break;
+ }
+ }
+ if (!skip) {
+ addIndex(fk.name + "_ix", fk.columnNames.toArray(new String[fk.columnNames.size()]));
+ }
+ }
+ }
+ validate();
+ if (historyTable) {
+ String historyTableName = name + "_history";
+ if (historyTableName.length() > 27 && historyTableName.length() <= 30) {
+ historyTableName = name + "_hist";
+ }
+ Table hist = Schema.this.addTable(historyTableName);
+ // History table needs all the same columns as the original
+ hist.columns.addAll(columns);
+ // Add a synthetic column to indicate when the original row has been deleted
+ hist.addColumn("is_deleted").asBoolean().table();
+ List pkColumns = new ArrayList<>();
+ pkColumns.addAll(primaryKey.columnNames);
+ // Index the primary key from the regular table for retrieving history
+ hist.addIndex(historyTableName + "_ix", pkColumns.toArray(new String[pkColumns.size()]));
+ // The primary key for the history table will be that of the original table, plus the update sequence
+ pkColumns.add("update_sequence");
+ hist.addPrimaryKey(historyTableName + "_pk", pkColumns.toArray(new String[pkColumns.size()]));
+ // To perform any validation
+ hist.schema();
+ }
+ return Schema.this;
+ }
+
+ public Table withComment(String comment) {
+ this.comment = comment;
+ return this;
+ }
+
+ public Table withStandardPk() {
+ return addColumn(name + "_id").primaryKey().table();
+ }
+
+ public Table trackCreateTime() {
+ createTracking = true;
+ return this;
+ }
+
+ public Table trackCreateTimeAndUser(String fkConstraintName) {
+ return trackCreateTimeAndUser(fkConstraintName, userTableName);
+ }
+
+ public Table trackCreateTimeAndUser(String fkConstraintName, String fkReferencesTable) {
+ createTracking = true;
+ createTrackingFkName = fkConstraintName;
+ createTrackingFkTable = fkReferencesTable;
+ return this;
+ }
+
+ public Table trackUpdateTime() {
+ updateTracking = true;
+ updateSequence = true;
+ return this;
+ }
+
+ public Table trackUpdateTimeAndUser(String fkConstraintName) {
+ return trackUpdateTimeAndUser(fkConstraintName, userTableName);
+ }
+
+ public Table trackUpdateTimeAndUser(String fkConstraintName, String fkReferencesTable) {
+ updateTracking = true;
+ updateSequence = true;
+ updateTrackingFkName = fkConstraintName;
+ updateTrackingFkTable = fkReferencesTable;
+ return this;
+ }
+
+ public Table withHistoryTable() {
+ updateSequence = true;
+ historyTable = true;
+ return this;
+ }
+
+ public Column addColumn(String name) {
+ Column column = new Column(name);
+ columns.add(column);
+ return column;
+ }
+
+ public PrimaryKey addPrimaryKey(String name, String... columnNames) {
+ if (primaryKey != null) {
+ throw new RuntimeException("Only one primary key is allowed. For composite keys use"
+ + " addPrimaryKey(name, c1, c2, ...).");
+ }
+ for (Column c : columns) {
+ if (c.name.equalsIgnoreCase(name)) {
+ throw new RuntimeException("For table: " + this.name + " primary key name should not be a column name: " + name);
+ }
+ }
+ primaryKey = new PrimaryKey(name, columnNames);
+ return primaryKey;
+ }
+
+ public ForeignKey addForeignKey(String name, String... columnNames) {
+ ForeignKey foreignKey = new ForeignKey(name, columnNames);
+ foreignKeys.add(foreignKey);
+ return foreignKey;
+ }
+
+ public Check addCheck(String name, String expression) {
+ Check check = new Check(name, expression);
+ checks.add(check);
+ return check;
+ }
+
+ public Unique addUnique(String name, String... columnNames) {
+ Unique unique = new Unique(name, columnNames);
+ uniques.add(unique);
+ return unique;
+ }
+
+ public Index addIndex(String name, String... columnNames) {
+ Index index = new Index(name, columnNames);
+ indexes.add(index);
+ return index;
+ }
+
+ public Table customTableClause(Flavor flavor, String clause) {
+ customClauses.put(flavor, clause);
+ return this;
+ }
+
+ public class PrimaryKey {
+ private final String name;
+ private final List columnNames = new ArrayList<>();
+
+ public PrimaryKey(String name, String[] columnNames) {
+ this.name = toName(name);
+ for (String s : columnNames) {
+ this.columnNames.add(toName(s));
+ }
+ }
+
+ public void validate() {
+
+ }
+
+ public Table table() {
+ validate();
+ return Table.this;
+ }
+ }
+
+ public class Unique {
+ private final String name;
+ private final List columnNames = new ArrayList<>();
+
+ public Unique(String name, String[] columnNames) {
+ this.name = toName(name);
+ for (String s : columnNames) {
+ this.columnNames.add(toName(s));
+ }
+ }
+
+ public void validate() {
+
+ }
+
+ public Table table() {
+ validate();
+ return Table.this;
+ }
+ }
+
+ public class ForeignKey {
+ private final String name;
+ private final List columnNames = new ArrayList<>();
+ public String foreignTable;
+ private boolean onDeleteCascade = false;
+
+ public ForeignKey(String name, String[] columnNames) {
+ this.name = toName(name);
+ for (String s : columnNames) {
+ this.columnNames.add(toName(s));
+ }
+ }
+
+ public ForeignKey references(String tableName) {
+ foreignTable = toName(tableName);
+ return this;
+ }
+
+ public ForeignKey onDeleteCascade() {
+ onDeleteCascade = true;
+ return this;
+ }
+
+ private void validate() {
+ if (foreignTable == null) {
+ throw new RuntimeException("Foreign key " + name + " must reference a table");
+ }
+ }
+
+ public Table table() {
+ validate();
+ return Table.this;
+ }
+ }
+
+ public class Check {
+ private final String name;
+ private final String expression;
+
+ public Check(String name, String expression) {
+ this.name = toName(name);
+ this.expression = expression;
+ }
+
+ private void validate() {
+ if (expression == null) {
+ throw new RuntimeException("Expression needed for check constraint " + name + " on table " + Table.this.name);
+ }
+ }
+
+ public Table table() {
+ validate();
+ return Table.this;
+ }
+ }
+
+ public class Index {
+ private final String name;
+ private final List columnNames = new ArrayList<>();
+ private boolean unique;
+
+ public Index(String name, String[] columnNames) {
+ this.name = toName(name);
+ for (String s : columnNames) {
+ this.columnNames.add(toName(s));
+ }
+ }
+
+ public Index unique() {
+ unique = true;
+ return this;
+ }
+
+ private void validate() {
+ if (columnNames.size() < 1) {
+ throw new RuntimeException("Index " + name + " needs at least one column");
+ }
+ }
+
+ public Table table() {
+ validate();
+ return Table.this;
+ }
+ }
+
+ public class Column {
+ private final String name;
+ private ColumnType type;
+ private int scale;
+ private int precision;
+ private boolean notNull;
+ private String comment;
+
+ public Column(String name) {
+ this.name = toName(name);
+ }
+
+ /**
+ * Create a boolean column, usually char(1) to hold values 'Y' or 'N'. This
+ * parameterless version does not create any check constraint at the database
+ * level.
+ */
+ public Column asBoolean() {
+ return asType(ColumnType.Boolean);
+ }
+
+ /**
+ * Create a boolean column, usually char(1) to hold values 'Y' or 'N'. This
+ * version creates a check constraint at the database level with the provided name.
+ */
+ public Column asBoolean(String checkConstraintName) {
+ return asBoolean().check(checkConstraintName, name + " in ('Y', 'N')");
+ }
+
+ public Column asInteger() {
+ return asType(ColumnType.Integer);
+ }
+
+ public Column asLong() {
+ return asType(ColumnType.Long);
+ }
+
+ public Column asFloat() {
+ return asType(ColumnType.Float);
+ }
+
+ public Column asDouble() {
+ return asType(ColumnType.Double);
+ }
+
+ public Column asBigDecimal(int scale, int precision) {
+ this.scale = scale;
+ this.precision = precision;
+ return asType(ColumnType.BigDecimal);
+ }
+
+ public Column asString(int scale) {
+ this.scale = scale;
+ return asType(ColumnType.StringVar);
+ }
+
+ public Column asStringFixed(int scale) {
+ this.scale = scale;
+ return asType(ColumnType.StringFixed);
+ }
+
+ // This type is for dates that have time associated
+ public Column asDate() {
+ return asType(ColumnType.Date);
+ }
+
+ // This type is for true dates with no time associated
+ public Column asLocalDate() {
+ return asType(ColumnType.LocalDate);
+ }
+
+ public Column asClob() {
+ return asType(ColumnType.Clob);
+ }
+
+ public Column asBlob() {
+ return asType(ColumnType.Blob);
+ }
+
+ private Column asType(ColumnType type) {
+ this.type = type;
+ return this;
+ }
+
+ public Column notNull() {
+ this.notNull = true;
+ return this;
+ }
+
+ private void validate() {
+ if (type == null) {
+ throw new RuntimeException("Call as*() on column " + name + " table " + Table.this.name);
+ }
+ }
+
+ public Table table() {
+ validate();
+ return Table.this;
+ }
+
+ public ForeignKey foreignKey(String constraintName) {
+ if (type == null) {
+ asLong();
+ }
+ return table().addForeignKey(constraintName, name);
+ }
+
+ public Column check(String checkConstraintName, String expression) {
+ table().addCheck(checkConstraintName, expression).table();
+ return this;
+ }
+
+ public Column primaryKey() {
+ if (type == null) {
+ asLong();
+ }
+ if (comment == null) {
+ comment = "Internally generated primary key";
+ }
+ notNull();
+ Table.this.addPrimaryKey(Table.this.name + "_pk", name);
+ return this;
+ }
+
+ public Column unique(String constraintName) {
+ notNull();
+ Table.this.addUnique(constraintName, name);
+ return this;
+ }
+
+ public Column withComment(String comment) {
+ this.comment = comment;
+ return this;
+ }
+
+ public Schema schema() {
+ return table().schema();
+ }
+ }
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SecretArg.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SecretArg.java
new file mode 100644
index 0000000..dcaa945
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SecretArg.java
@@ -0,0 +1,19 @@
+package org.xbib.jdbc.query;
+
+public class SecretArg {
+
+ private final Object arg;
+
+ public SecretArg(Object arg) {
+ this.arg = arg;
+ }
+
+ Object getArg() {
+ return arg;
+ }
+
+ @Override
+ public String toString() {
+ return "";
+ }
+}
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
new file mode 100644
index 0000000..091e159
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Sql.java
@@ -0,0 +1,443 @@
+package org.xbib.jdbc.query;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Stack;
+
+/**
+ * This class is useful for dynamically generating SQL. It can "buffer" the
+ * various arg*() calls and replay them later via the apply(sqlArgs) methods.
+ */
+public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply {
+ private final StringBuilder sql = new StringBuilder();
+ private final Stack listFirstItem = new Stack<>();
+ private List batched;
+ private SqlArgs sqlArgs = new SqlArgs();
+
+ public Sql() {
+ }
+
+ public Sql(/*@Untainted*/ String sql) {
+ this.sql.append(sql);
+ }
+
+ public static Sql insert(/*@Untainted*/ String table, SqlArgs args) {
+ return insert(table, Collections.singletonList(args));
+ }
+
+ public static Sql insert(/*@Untainted*/ 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");
+ }
+ List columns = arg.names();
+ if (columns.size() < 1) {
+ throw new DatabaseException("You must add named arguments to SqlArgs");
+ }
+ if (sql == null) {
+ expectedColumns = columns;
+ sql = new Sql("insert into ").append(table);
+ sql.listStart(" (");
+ for (String column : columns) {
+ sql.listSeparator(",");
+ sql.append(column);
+ }
+ sql.listEnd(") values (");
+ sql.appendQuestionMarks(columns.size());
+ sql.append(")");
+ } else {
+ if (!expectedColumns.equals(columns)) {
+ throw new DatabaseException("The columns for all rows in a batch must match. \nFirst: " + expectedColumns
+ + "\nCurrent: " + columns);
+ }
+ sql.batch();
+ }
+ sql.setSqlArgs(arg.makePositional());
+ }
+ return sql;
+ }
+
+ public Sql setSqlArgs(SqlArgs args) {
+ sqlArgs = args;
+ return this;
+ }
+
+ public Sql batch() {
+ if (sqlArgs.argCount() > 0) {
+ if (batched == null) {
+ batched = new ArrayList<>();
+ }
+ batched.add(sqlArgs);
+ sqlArgs = new SqlArgs();
+ }
+ return this;
+ }
+
+ public int argCount() {
+ return sqlArgs.argCount();
+ }
+
+ public Sql appendQuestionMarks(int howMany) {
+ boolean first = true;
+ for (int i = 0; i < howMany; i++) {
+ if (first) {
+ first = false;
+ append("?");
+ } else {
+ append(",?");
+ }
+ }
+ return this;
+ }
+
+ public Sql append(/*@Untainted*/ String sql) {
+ this.sql.append(sql);
+ return this;
+ }
+
+ public Sql append(boolean value) {
+ this.sql.append(value);
+ return this;
+ }
+
+ public Sql append(int value) {
+ this.sql.append(value);
+ return this;
+ }
+
+ public Sql append(long value) {
+ this.sql.append(value);
+ return this;
+ }
+
+ public Sql append(float value) {
+ this.sql.append(value);
+ return this;
+ }
+
+ public Sql append(double value) {
+ this.sql.append(value);
+ return this;
+ }
+
+ public Sql deleteCharAt(int index) {
+ this.sql.deleteCharAt(index);
+ return this;
+ }
+
+ public Sql replace(int start, int end, /*@Untainted*/ String str) {
+ this.sql.replace(start, end, str);
+ return this;
+ }
+
+ public Sql insert(int offset, /*@Untainted*/ String str) {
+ this.sql.insert(offset, str);
+ return this;
+ }
+
+ public Sql insert(int offset, boolean value) {
+ this.sql.insert(offset, value);
+ return this;
+ }
+
+ public Sql insert(int offset, int value) {
+ this.sql.insert(offset, value);
+ return this;
+ }
+
+ public Sql insert(int offset, long value) {
+ this.sql.insert(offset, value);
+ return this;
+ }
+
+ public Sql insert(int offset, double value) {
+ this.sql.insert(offset, value);
+ return this;
+ }
+
+ public Sql insert(int offset, float value) {
+ this.sql.insert(offset, value);
+ return this;
+ }
+
+ public int indexOf(String str) {
+ return this.sql.indexOf(str);
+ }
+
+ public int indexOf(String str, int fromIndex) {
+ return this.sql.indexOf(str, fromIndex);
+ }
+
+ public int lastIndexOf(String str) {
+ return this.sql.lastIndexOf(str);
+ }
+
+ public int lastIndexOf(String str, int fromIndex) {
+ return this.sql.lastIndexOf(str, fromIndex);
+ }
+
+ /**
+ * Appends the bit of sql and notes that a list, or a sublist, has started.
+ *
+ * Each list started must have be ended. "Lists" are only to support using listSeparator(sep)
+ */
+ public Sql listStart(/*@Untainted*/ String sql) {
+ listFirstItem.push(true);
+ return append(sql);
+ }
+
+ /**
+ * 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) {
+ if (listFirstItem.peek()) {
+ listFirstItem.pop();
+ listFirstItem.push(false);
+ return this;
+ } else {
+ return append(sql);
+ }
+ }
+
+
+ public Sql listEnd(/*@Untainted*/ String sql) {
+ listFirstItem.pop();
+ return append(sql);
+ }
+
+ /*@Untainted*/
+ public String sql() {
+ return sql.toString();
+ }
+
+ /**
+ * Same as sql(), provided for drop-in compatibility with StringBuilder.
+ */
+ /*@Untainted*/
+ public String toString() {
+ return sql();
+ }
+
+ public Sql argBoolean(Boolean arg) {
+ 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) {
+ sqlArgs.argString(argName, arg);
+ return this;
+ }
+
+
+ public Sql argDate(Date arg) {
+ sqlArgs.argDate(arg);
+ return this;
+ }
+
+
+ public Sql argDate( String argName, Date arg) {
+ sqlArgs.argDate(argName, arg);
+ return this;
+ }
+
+
+ public Sql argDateNowPerApp() {
+ sqlArgs.argDateNowPerApp();
+ return this;
+ }
+
+
+ public Sql argDateNowPerApp( String argName) {
+ sqlArgs.argDateNowPerApp(argName);
+ return this;
+ }
+
+
+ public Sql argDateNowPerDb() {
+ sqlArgs.argDateNowPerDb();
+ return this;
+ }
+
+
+ public Sql argDateNowPerDb( String argName) {
+ sqlArgs.argDateNowPerDb(argName);
+ return this;
+ }
+
+
+ public Sql argBlobBytes(byte[] arg) {
+ sqlArgs.argBlobBytes(arg);
+ return this;
+ }
+
+
+ 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);
+ return this;
+ }
+
+
+ public Sql argClobReader( String argName, Reader arg) {
+ sqlArgs.argClobReader(argName, arg);
+ return this;
+ }
+
+ @Override
+ public void apply(SqlSelect select) {
+ if (batched != null) {
+ throw new DatabaseException("Batch not supported for select");
+ }
+ sqlArgs.apply(select);
+ }
+
+ @Override
+ public void apply(SqlInsert insert) {
+ if (batched != null) {
+ batch();
+ for (SqlArgs args : batched) {
+ args.apply(insert);
+ insert.batch();
+ }
+ } else {
+ sqlArgs.apply(insert);
+ }
+ }
+
+ @Override
+ public void apply(SqlUpdate update) {
+ if (batched != null) {
+ throw new DatabaseException("Batch not supported for update");
+ }
+
+ sqlArgs.apply(update);
+ }
+
+ public enum ColumnType {
+ 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
new file mode 100644
index 0000000..7ee86cf
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlArgs.java
@@ -0,0 +1,773 @@
+package org.xbib.jdbc.query;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This class is useful for dynamically generating SQL. It can "buffer" the
+ * various arg*() calls and replay them later via the apply(sqlArgs) methods.
+ */
+public class SqlArgs implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply {
+ private final List invocations = new ArrayList<>();
+
+
+ public static Builder fromMetadata(Row r) {
+ return new Builder(r);
+ }
+
+ /**
+ * Convenience method for reading a single row. If you are reading multiple
+ * rows, see {@link #fromMetadata(Row)} to avoid reading metadata multiple times.
+ *
+ * @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);
+ }
+
+ public static String[] tidyColumnNames(String[] names) {
+ Set uniqueNames = new LinkedHashSet<>();
+ for (String name : names) {
+ if (name == null || name.length() == 0) {
+ name = "column_" + (uniqueNames.size() + 1);
+ }
+ name = name.replaceAll("[^a-zA-Z0-9]", " ");
+ name = name.replaceAll("([a-z])([A-Z])", "$1_$2");
+ name = name.trim().toLowerCase();
+ name = name.replaceAll("\\s", "_");
+ if (Character.isDigit(name.charAt(0))) {
+ name = "a" + name;
+ }
+ int i = 2;
+ String uniqueName = name;
+ while (uniqueNames.contains(uniqueName)) {
+ uniqueName = name + "_" + i++;
+ }
+ name = uniqueName;
+ uniqueNames.add(name);
+ }
+ return uniqueNames.toArray(new String[uniqueNames.size()]);
+ }
+
+
+ public SqlArgs argBoolean(Boolean arg) {
+ invocations.add(new Invocation(ColumnType.Boolean, null, arg));
+ return this;
+ }
+
+
+ public SqlArgs argBoolean( String argName, Boolean arg) {
+ invocations.add(new Invocation(ColumnType.Boolean, argName, arg));
+ return this;
+ }
+
+
+ public SqlArgs argInteger(Integer arg) {
+ invocations.add(new Invocation(ColumnType.Integer, null, arg));
+ return this;
+ }
+
+
+ public SqlArgs argInteger( String argName, Integer arg) {
+ invocations.add(new Invocation(ColumnType.Integer, argName, arg));
+ return this;
+ }
+
+
+ public SqlArgs argLong(Long arg) {
+ invocations.add(new Invocation(ColumnType.Long, null, arg));
+ return this;
+ }
+
+
+ public SqlArgs argLong( String argName, Long arg) {
+ invocations.add(new Invocation(ColumnType.Long, argName, arg));
+ return this;
+ }
+
+
+ public SqlArgs argFloat(Float arg) {
+ invocations.add(new Invocation(ColumnType.Float, null, arg));
+ return this;
+ }
+
+
+ public SqlArgs argFloat( String argName, Float arg) {
+ invocations.add(new Invocation(ColumnType.Float, argName, arg));
+ return this;
+ }
+
+
+ public SqlArgs argDouble(Double arg) {
+ 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(Date arg) {
+ // date argument with a time on it
+ invocations.add(new Invocation(ColumnType.Date, null, arg));
+ return this;
+ }
+
+
+ public SqlArgs argDate( String argName, Date arg) {
+ // date argument with a time on it
+ invocations.add(new Invocation(ColumnType.Date, argName, arg));
+ return this;
+ }
+
+
+ public SqlArgs argLocalDate(LocalDate arg) {
+ // date argument with no time on it
+ invocations.add(new Invocation(ColumnType.LocalDate, null, arg));
+ return this;
+ }
+
+
+ public SqlArgs argLocalDate( String argName, LocalDate arg) {
+ // date argument with no time on it
+ invocations.add(new Invocation(ColumnType.LocalDate, argName, arg));
+ return this;
+ }
+
+
+ public SqlArgs argDateNowPerApp() {
+ invocations.add(new Invocation(ColumnType.DateNowPerApp, null, null));
+ return this;
+ }
+
+
+ public SqlArgs argDateNowPerApp( String argName) {
+ invocations.add(new Invocation(ColumnType.DateNowPerApp, argName, null));
+ return this;
+ }
+
+
+ public SqlArgs argDateNowPerDb() {
+ invocations.add(new Invocation(ColumnType.DateNowPerDb, null, null));
+ return this;
+ }
+
+
+ public SqlArgs argDateNowPerDb( String argName) {
+ invocations.add(new Invocation(ColumnType.DateNowPerDb, argName, null));
+ return this;
+ }
+
+
+ public SqlArgs argBlobBytes(byte[] arg) {
+ invocations.add(new Invocation(ColumnType.BlobBytes, null, arg));
+ return this;
+ }
+
+
+ public SqlArgs argBlobBytes( String argName, byte[] arg) {
+ invocations.add(new Invocation(ColumnType.BlobBytes, argName, arg));
+ return this;
+ }
+
+
+ public SqlArgs argBlobInputStream(InputStream arg) {
+ invocations.add(new Invocation(ColumnType.BlobStream, null, arg));
+ return this;
+ }
+
+
+ public SqlArgs argBlobInputStream( String argName, InputStream arg) {
+ invocations.add(new Invocation(ColumnType.BlobStream, argName, arg));
+ return this;
+ }
+
+
+ public SqlArgs argClobString(String arg) {
+ invocations.add(new Invocation(ColumnType.ClobString, null, arg));
+ return this;
+ }
+
+
+ public SqlArgs argClobString( String argName, String arg) {
+ invocations.add(new Invocation(ColumnType.ClobString, argName, arg));
+ return this;
+ }
+
+
+ public SqlArgs argClobReader(Reader arg) {
+ invocations.add(new Invocation(ColumnType.ClobStream, null, arg));
+ return this;
+ }
+
+
+ public SqlArgs argClobReader( String argName, Reader arg) {
+ invocations.add(new Invocation(ColumnType.ClobStream, argName, arg));
+ return this;
+ }
+
+
+ public SqlArgs makePositional() {
+ for (Invocation invocation : invocations) {
+ invocation.argName = null;
+ }
+ return this;
+ }
+
+
+ public List names() {
+ List names = new ArrayList<>();
+ for (Invocation invocation : invocations) {
+ if (invocation.argName != null) {
+ names.add(invocation.argName);
+ }
+ }
+ return names;
+ }
+
+ public int argCount() {
+ return invocations.size();
+ }
+
+ public int positionalCount() {
+ int count = 0;
+ for (Invocation invocation : invocations) {
+ if (invocation.argName == null) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ @Override
+ public void apply(SqlSelect select) {
+ for (Invocation i : invocations) {
+ switch (i.columnType) {
+ case Boolean:
+ if (i.argName == null) {
+ select.argBoolean((Boolean) i.arg);
+ } else {
+ select.argBoolean(i.argName, (Boolean) i.arg);
+ }
+ break;
+ case Integer:
+ if (i.argName == null) {
+ select.argInteger((Integer) i.arg);
+ } else {
+ select.argInteger(i.argName, (Integer) i.arg);
+ }
+ break;
+ case Long:
+ if (i.argName == null) {
+ select.argLong((Long) i.arg);
+ } else {
+ select.argLong(i.argName, (Long) i.arg);
+ }
+ break;
+ case Float:
+ if (i.argName == null) {
+ select.argFloat((Float) i.arg);
+ } else {
+ select.argFloat(i.argName, (Float) i.arg);
+ }
+ break;
+ case Double:
+ if (i.argName == null) {
+ select.argDouble((Double) i.arg);
+ } else {
+ select.argDouble(i.argName, (Double) i.arg);
+ }
+ break;
+ case BigDecimal:
+ if (i.argName == null) {
+ select.argBigDecimal((BigDecimal) i.arg);
+ } else {
+ select.argBigDecimal(i.argName, (BigDecimal) i.arg);
+ }
+ break;
+ case String:
+ if (i.argName == null) {
+ select.argString((String) i.arg);
+ } else {
+ select.argString(i.argName, (String) i.arg);
+ }
+ break;
+ case ClobString:
+ if (i.argName == null) {
+ select.argString((String) i.arg);
+ } else {
+ select.argString(i.argName, (String) i.arg);
+ }
+ break;
+ case ClobStream:
+ throw new DatabaseException("Don't use Clob stream parameters with select statements");
+ case BlobBytes:
+ throw new DatabaseException("Don't use Blob parameters with select statements");
+ case BlobStream:
+ throw new DatabaseException("Don't use Blob parameters with select statements");
+ case LocalDate:
+ // date argument with no time on it
+ if (i.argName == null) {
+ select.argLocalDate((LocalDate) i.arg);
+ } else {
+ select.argLocalDate(i.argName, (LocalDate) i.arg);
+ }
+ break;
+ case Date:
+ // date argument with a time on it
+ if (i.argName == null) {
+ select.argDate((Date) i.arg);
+ } else {
+ select.argDate(i.argName, (Date) i.arg);
+ }
+ break;
+ case DateNowPerApp:
+ if (i.argName == null) {
+ select.argDateNowPerApp();
+ } else {
+ select.argDateNowPerApp(i.argName);
+ }
+ break;
+ case DateNowPerDb:
+ if (i.argName == null) {
+ select.argDateNowPerDb();
+ } else {
+ select.argDateNowPerDb(i.argName);
+ }
+ break;
+ }
+ }
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ @Override
+ public void apply(SqlInsert insert) {
+ for (Invocation i : invocations) {
+ switch (i.columnType) {
+ case Boolean:
+ if (i.argName == null) {
+ insert.argBoolean((Boolean) i.arg);
+ } else {
+ insert.argBoolean(i.argName, (Boolean) i.arg);
+ }
+ break;
+ case Integer:
+ if (i.argName == null) {
+ insert.argInteger((Integer) i.arg);
+ } else {
+ insert.argInteger(i.argName, (Integer) i.arg);
+ }
+ break;
+ case Long:
+ if (i.argName == null) {
+ insert.argLong((Long) i.arg);
+ } else {
+ insert.argLong(i.argName, (Long) i.arg);
+ }
+ break;
+ case Float:
+ if (i.argName == null) {
+ insert.argFloat((Float) i.arg);
+ } else {
+ insert.argFloat(i.argName, (Float) i.arg);
+ }
+ break;
+ case Double:
+ if (i.argName == null) {
+ insert.argDouble((Double) i.arg);
+ } else {
+ insert.argDouble(i.argName, (Double) i.arg);
+ }
+ break;
+ case BigDecimal:
+ if (i.argName == null) {
+ insert.argBigDecimal((BigDecimal) i.arg);
+ } else {
+ insert.argBigDecimal(i.argName, (BigDecimal) i.arg);
+ }
+ break;
+ case String:
+ if (i.argName == null) {
+ insert.argString((String) i.arg);
+ } else {
+ insert.argString(i.argName, (String) i.arg);
+ }
+ break;
+ case ClobString:
+ if (i.argName == null) {
+ insert.argClobString((String) i.arg);
+ } else {
+ insert.argClobString(i.argName, (String) i.arg);
+ }
+ break;
+ case ClobStream:
+ if (i.argName == null) {
+ insert.argClobReader((Reader) i.arg);
+ } else {
+ insert.argClobReader(i.argName, (Reader) i.arg);
+ }
+ break;
+ case BlobBytes:
+ if (i.argName == null) {
+ insert.argBlobBytes((byte[]) i.arg);
+ } else {
+ insert.argBlobBytes(i.argName, (byte[]) i.arg);
+ }
+ break;
+ case BlobStream:
+ if (i.argName == null) {
+ insert.argBlobStream((InputStream) i.arg);
+ } else {
+ insert.argBlobStream(i.argName, (InputStream) i.arg);
+ }
+ break;
+ case LocalDate:
+ // date argument with no time on it
+ if (i.argName == null) {
+ insert.argLocalDate((LocalDate) i.arg);
+ } else {
+ insert.argLocalDate(i.argName, (LocalDate) i.arg);
+ }
+ break;
+ case Date:
+ // date argument with a time on it
+ if (i.argName == null) {
+ insert.argDate((Date) i.arg);
+ } else {
+ insert.argDate(i.argName, (Date) i.arg);
+ }
+ break;
+ case DateNowPerApp:
+ if (i.argName == null) {
+ insert.argDateNowPerApp();
+ } else {
+ insert.argDateNowPerApp(i.argName);
+ }
+ break;
+ case DateNowPerDb:
+ if (i.argName == null) {
+ insert.argDateNowPerDb();
+ } else {
+ insert.argDateNowPerDb(i.argName);
+ }
+ break;
+ }
+ }
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ @Override
+ public void apply(SqlUpdate update) {
+ for (Invocation i : invocations) {
+ switch (i.columnType) {
+ case Boolean:
+ if (i.argName == null) {
+ update.argBoolean((Boolean) i.arg);
+ } else {
+ update.argBoolean(i.argName, (Boolean) i.arg);
+ }
+ break;
+ case Integer:
+ if (i.argName == null) {
+ update.argInteger((Integer) i.arg);
+ } else {
+ update.argInteger(i.argName, (Integer) i.arg);
+ }
+ break;
+ case Long:
+ if (i.argName == null) {
+ update.argLong((Long) i.arg);
+ } else {
+ update.argLong(i.argName, (Long) i.arg);
+ }
+ break;
+ case Float:
+ if (i.argName == null) {
+ update.argFloat((Float) i.arg);
+ } else {
+ update.argFloat(i.argName, (Float) i.arg);
+ }
+ break;
+ case Double:
+ if (i.argName == null) {
+ update.argDouble((Double) i.arg);
+ } else {
+ update.argDouble(i.argName, (Double) i.arg);
+ }
+ break;
+ case BigDecimal:
+ if (i.argName == null) {
+ update.argBigDecimal((BigDecimal) i.arg);
+ } else {
+ update.argBigDecimal(i.argName, (BigDecimal) i.arg);
+ }
+ break;
+ case String:
+ if (i.argName == null) {
+ update.argString((String) i.arg);
+ } else {
+ update.argString(i.argName, (String) i.arg);
+ }
+ break;
+ case ClobString:
+ if (i.argName == null) {
+ update.argClobString((String) i.arg);
+ } else {
+ update.argClobString(i.argName, (String) i.arg);
+ }
+ break;
+ case ClobStream:
+ if (i.argName == null) {
+ update.argClobReader((Reader) i.arg);
+ } else {
+ update.argClobReader(i.argName, (Reader) i.arg);
+ }
+ break;
+ case BlobBytes:
+ if (i.argName == null) {
+ update.argBlobBytes((byte[]) i.arg);
+ } else {
+ update.argBlobBytes(i.argName, (byte[]) i.arg);
+ }
+ break;
+ case BlobStream:
+ if (i.argName == null) {
+ update.argBlobStream((InputStream) i.arg);
+ } else {
+ update.argBlobStream(i.argName, (InputStream) i.arg);
+ }
+ break;
+ case LocalDate:
+ // date argument with no time on it
+ if (i.argName == null) {
+ update.argLocalDate((LocalDate) i.arg);
+ } else {
+ update.argLocalDate(i.argName, (LocalDate) i.arg);
+ }
+ break;
+ case Date:
+ // date argument with a time on it
+ if (i.argName == null) {
+ update.argDate((Date) i.arg);
+ } else {
+ update.argDate(i.argName, (Date) i.arg);
+ }
+ break;
+ case DateNowPerApp:
+ if (i.argName == null) {
+ update.argDateNowPerApp();
+ } else {
+ update.argDateNowPerApp(i.argName);
+ }
+ break;
+ case DateNowPerDb:
+ if (i.argName == null) {
+ update.argDateNowPerDb();
+ } else {
+ update.argDateNowPerDb(i.argName);
+ }
+ break;
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SqlArgs sqlArgs = (SqlArgs) o;
+ return Objects.equals(invocations, sqlArgs.invocations);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(invocations);
+ }
+
+ @Override
+ public String toString() {
+ return "SqlArgs" + invocations;
+ }
+
+ public enum ColumnType {
+ Integer, Long, Float, Double, BigDecimal, String, ClobString, ClobStream,
+ BlobBytes, BlobStream, Date, LocalDate, 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;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Invocation that = (Invocation) o;
+ return columnType == that.columnType &&
+ Objects.equals(argName, that.argName) &&
+ Objects.deepEquals(arg, that.arg);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(columnType, argName, arg);
+ }
+
+ @Override
+ public String toString() {
+ return "{name=" + argName + ", type=" + columnType + ", arg=" + arg + '}';
+ }
+ }
+
+ public static class Builder {
+ private final int[] types;
+ private final int[] precision;
+ private final int[] scale;
+ private String[] names;
+
+ public Builder(Row r) {
+ try {
+ ResultSetMetaData metadata = r.getMetadata();
+ int columnCount = metadata.getColumnCount();
+ names = new String[columnCount];
+ types = new int[columnCount];
+ precision = new int[columnCount];
+ scale = new int[columnCount];
+
+ for (int i = 0; i < columnCount; i++) {
+ names[i] = metadata.getColumnLabel(i + 1);
+ types[i] = metadata.getColumnType(i + 1);
+ precision[i] = metadata.getPrecision(i + 1);
+ scale[i] = metadata.getScale(i + 1);
+ }
+
+ names = tidyColumnNames(names);
+ } catch (SQLException e) {
+ 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:
+ case Types.INTEGER:
+ args.argInteger(names[i], r.getIntegerOrNull());
+ break;
+ case Types.BIGINT:
+ args.argLong(names[i], r.getLongOrNull());
+ break;
+ case Types.REAL:
+ case 100: // Oracle proprietary it seems
+ args.argFloat(names[i], r.getFloatOrNull());
+ break;
+ case Types.DOUBLE:
+ case 101: // Oracle proprietary it seems
+ args.argDouble(names[i], r.getDoubleOrNull());
+ break;
+ case Types.NUMERIC:
+ if (precision[i] == 10 && scale[i] == 0) {
+ // Oracle reports integer as numeric
+ args.argInteger(names[i], r.getIntegerOrNull());
+ } else if (precision[i] == 19 && scale[i] == 0) {
+ // Oracle reports long as numeric
+ args.argLong(names[i], r.getLongOrNull());
+ } else {
+ args.argBigDecimal(names[i], r.getBigDecimalOrNull());
+ }
+ break;
+ case Types.BINARY:
+ case Types.VARBINARY:
+ case Types.BLOB:
+ args.argBlobBytes(names[i], r.getBlobBytesOrNull());
+ break;
+ case Types.CLOB:
+ case Types.NCLOB:
+ args.argClobString(names[i], r.getClobStringOrNull());
+ break;
+
+ // Check Date before TimeStamp because SQL dates are also timestamps
+ case Types.DATE:
+ args.argLocalDate(names[i], r.getLocalDateOrNull());
+ break;
+
+ case Types.TIMESTAMP:
+ if (this.scale[i] == 0) {
+ // If the scale is 0, this is a LocalDate (no time/timezone).
+ // Anything with a time will have a non-zero scale
+ args.argLocalDate(names[i], r.getLocalDateOrNull());
+ } else {
+ args.argDate(names[i], r.getDateOrNull());
+ }
+ break;
+
+ case Types.NVARCHAR:
+ case Types.VARCHAR:
+ case Types.CHAR:
+ case Types.NCHAR:
+ if (precision[i] >= 2147483647) {
+ // Postgres seems to report clobs are varchar(2147483647)
+ args.argClobString(names[i], r.getClobStringOrNull());
+ } else {
+ args.argString(names[i], r.getStringOrNull());
+ }
+ break;
+ default:
+ throw new DatabaseException("Don't know how to deal with column type: " + types[i]);
+ }
+ }
+ return args;
+ }
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsert.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsert.java
new file mode 100644
index 0000000..ff5119c
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsert.java
@@ -0,0 +1,152 @@
+package org.xbib.jdbc.query;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Date;
+
+/**
+ * Interface for configuring (setting parameters) and executing a chunk of SQL.
+ */
+public interface SqlInsert {
+
+ SqlInsert argBoolean(Boolean arg);
+
+ SqlInsert argBoolean( String argName, Boolean arg);
+
+ SqlInsert argInteger(Integer arg);
+
+ SqlInsert argInteger( String argName, Integer arg);
+
+ SqlInsert argLong(Long arg);
+
+ SqlInsert argLong( String argName, Long arg);
+
+ SqlInsert argFloat(Float arg);
+
+ SqlInsert argFloat( String argName, Float arg);
+
+ SqlInsert argDouble(Double arg);
+
+ SqlInsert argDouble( String argName, Double arg);
+
+ SqlInsert argBigDecimal(BigDecimal arg);
+
+ SqlInsert argBigDecimal( String argName, BigDecimal arg);
+
+ SqlInsert argString(String arg);
+
+ SqlInsert argString( String argName, String arg);
+
+ SqlInsert argDate(Date arg); // date with time
+
+ SqlInsert argDate( String argName, Date arg); // date with time
+
+ SqlInsert argLocalDate(LocalDate arg); // date only - no timestamp
+
+ SqlInsert argLocalDate( String argName, LocalDate arg); // date only - no timestamp
+
+ SqlInsert argDateNowPerApp();
+
+ SqlInsert argDateNowPerApp( String argName);
+
+ SqlInsert argDateNowPerDb();
+
+ SqlInsert argDateNowPerDb( String argName);
+
+ SqlInsert argBlobBytes(byte[] arg);
+
+ SqlInsert argBlobBytes( String argName, byte[] arg);
+
+ SqlInsert argBlobStream(InputStream arg);
+
+ SqlInsert argBlobStream( String argName, InputStream arg);
+
+ SqlInsert argClobString(String arg);
+
+ SqlInsert argClobString( String argName, String arg);
+
+ SqlInsert argClobReader(Reader arg);
+
+ SqlInsert argClobReader( String argName, Reader arg);
+
+ SqlInsert withArgs(SqlArgs args);
+
+ SqlInsert apply(Apply apply);
+
+ /**
+ * Call this between setting rows of parameters for a SQL statement. You may call it before
+ * setting any parameters, after setting all, or multiple times between rows. This feature
+ * only currently works with basic inserts (you can't do insertReturning type operations).
+ */
+ SqlInsert batch();
+
+ /**
+ * Perform the insert into the database without any verification of how many rows
+ * were affected.
+ *
+ * @return the number of rows affected
+ */
+ int insert();
+
+ /**
+ * Perform the insert into the database. This will automatically verify
+ * that the specified number of rows was affected, and throw a {@link WrongNumberOfRowsException}
+ * if it does not match.
+ */
+ void insert(int expectedRowsUpdated);
+
+ /**
+ * Insert multiple rows in one database call. This will automatically verify
+ * that exactly 1 row is affected for each row of parameters.
+ */
+ void insertBatch();
+
+ /**
+ * Insert multiple rows in one database call. This returns the results for
+ * each row so you can check them yourself.
+ *
+ * @return an array with an element for each row in the batch; the value
+ * of each array indicates how many rows were affected; note that
+ * some database/driver combinations do now return this information
+ * (for example, older versions of Oracle return -2 rather than the
+ * number of rows)
+ */
+ int[] insertBatchUnchecked();
+
+ /**
+ * Use this method in conjunction with argPkSeq() to optimize inserts where the
+ * primary key is being populated from a database sequence at insert time. If the
+ * database can't support this feature it will be simulated with a select and then
+ * the insert.
+ *
+ *
This version of insert expects exactly one row to be inserted, and will throw
+ * a DatabaseException if that isn't the case.
+ */
+
+ Long insertReturningPkSeq(String primaryKeyColumnName);
+
+ T insertReturning(String tableName, String primaryKeyColumnName, RowsHandler rowsHandler,
+ String... otherColumnNames);
+
+ SqlInsert argPkSeq( String sequenceName);
+
+ /**
+ * Use this method to populate the primary key value (assumed to be type Long)
+ * from a sequence in the database. This can be used standalone, but is intended
+ * to be used in conjunction with insertReturningPkSeq() to both insert and obtain
+ * the inserted value in an optimized way (if possible). For databases that are
+ * unable to return the value from the insert (such as Derby) this will be simulated
+ * first issuing a select to read the sequence, then an insert.
+ */
+ SqlInsert argPkSeq( String argName, String sequenceName);
+
+ SqlInsert argPkLong(Long pkValue);
+
+ SqlInsert argPkLong(String argName, Long pkValue);
+
+ interface Apply {
+ void apply(SqlInsert insert);
+ }
+}
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
new file mode 100644
index 0000000..9fcdcea
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsertImpl.java
@@ -0,0 +1,708 @@
+package org.xbib.jdbc.query;
+
+import org.xbib.jdbc.query.util.DebugSql;
+import org.xbib.jdbc.query.util.InternalStringReader;
+import org.xbib.jdbc.query.util.Metric;
+import org.xbib.jdbc.query.util.RewriteArg;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This is the key class for configuring (query parameters) and executing a database query.
+ */
+public class SqlInsertImpl implements SqlInsert {
+
+ private static final Logger log = Logger.getLogger(Database.class.getName());
+
+ private final Connection connection;
+
+ private final StatementAdaptor adaptor;
+
+ private final String sql;
+
+ private final Options options;
+
+ private List batched;
+
+ private List parameterList; // !null ==> traditional ? args
+
+ private Map parameterMap; // !null ==> named :abc args
+
+ private String pkArgName;
+
+ private int pkPos;
+
+ private String pkSeqName;
+
+ private Long pkLong;
+
+ public SqlInsertImpl(Connection connection, String sql, Options options) {
+ this.connection = connection;
+ this.sql = sql;
+ this.options = options;
+ adaptor = new StatementAdaptor(options);
+ }
+
+
+ @Override
+ public SqlInsert argBoolean(Boolean arg) {
+ return positionalArg(adaptor.nullString(booleanToString(arg)));
+ }
+
+
+ @Override
+ public SqlInsert argBoolean( String argName, Boolean arg) {
+ return namedArg(argName, adaptor.nullString(booleanToString(arg)));
+ }
+
+ @Override
+
+ public SqlInsert argInteger(Integer arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlInsert argInteger( String argName, Integer arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlInsert argLong(Long arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlInsert argLong( String argName, Long arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlInsert argFloat(Float arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlInsert argFloat( String argName, Float arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlInsert argDouble(Double arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlInsert argDouble( String argName, Double arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlInsert argBigDecimal(BigDecimal arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlInsert argBigDecimal( String argName, BigDecimal arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlInsert argString(String arg) {
+ return positionalArg(adaptor.nullString(arg));
+ }
+
+ @Override
+
+ public SqlInsert argString( String argName, String arg) {
+ return namedArg(argName, adaptor.nullString(arg));
+ }
+
+ @Override
+
+ public SqlInsert argDate(Date arg) {
+ return positionalArg(adaptor.nullDate(arg));
+ }
+
+ @Override
+
+ public SqlInsert argDate( String argName, Date arg) {
+ return namedArg(argName, adaptor.nullDate(arg));
+ }
+
+ @Override
+
+ public SqlInsert argLocalDate( String argName, LocalDate arg) {
+ return namedArg(argName, adaptor.nullLocalDate(arg));
+ }
+
+ @Override
+
+ public SqlInsert argLocalDate(LocalDate arg) {
+ return positionalArg(adaptor.nullLocalDate(arg));
+ }
+
+
+ @Override
+ public SqlInsert argDateNowPerApp() {
+ return positionalArg(adaptor.nullDate(options.currentDate()));
+ }
+
+ @Override
+
+ public SqlInsert argDateNowPerApp( String argName) {
+ return namedArg(argName, adaptor.nullDate(options.currentDate()));
+ }
+
+
+ @Override
+ public SqlInsert argDateNowPerDb() {
+ if (options.useDatePerAppOnly()) {
+ return positionalArg(adaptor.nullDate(options.currentDate()));
+ }
+ return positionalArg(new RewriteArg(options.flavor().dbTimeMillis()));
+ }
+
+ @Override
+
+ public SqlInsert argDateNowPerDb( String argName) {
+ if (options.useDatePerAppOnly()) {
+ return namedArg(argName, adaptor.nullDate(options.currentDate()));
+ }
+ return namedArg(argName, new RewriteArg(options.flavor().dbTimeMillis()));
+ }
+
+ @Override
+
+ public SqlInsert argBlobBytes(byte[] arg) {
+ return positionalArg(adaptor.nullBytes(arg));
+ }
+
+ @Override
+
+ public SqlInsert argBlobBytes( String argName, byte[] arg) {
+ return namedArg(argName, adaptor.nullBytes(arg));
+ }
+
+ @Override
+
+ public SqlInsert argBlobStream(InputStream arg) {
+ return positionalArg(adaptor.nullInputStream(arg));
+ }
+
+ @Override
+
+ public SqlInsert argBlobStream( String argName, InputStream arg) {
+ return namedArg(argName, adaptor.nullInputStream(arg));
+ }
+
+ @Override
+
+ public SqlInsert argClobString(String arg) {
+ return positionalArg(adaptor.nullClobReader(arg == null ? null : new InternalStringReader(arg)));
+ }
+
+ @Override
+
+ public SqlInsert argClobString( String argName, String arg) {
+ return namedArg(argName, adaptor.nullClobReader(arg == null ? null : new InternalStringReader(arg)));
+ }
+
+ @Override
+
+ public SqlInsert argClobReader(Reader arg) {
+ return positionalArg(adaptor.nullClobReader(arg));
+ }
+
+ @Override
+
+ public SqlInsert argClobReader( String argName, Reader arg) {
+ return namedArg(argName, adaptor.nullClobReader(arg));
+ }
+
+
+ @Override
+ public SqlInsert withArgs(SqlArgs args) {
+ return apply(args);
+ }
+
+
+ @Override
+ public SqlInsert apply(Apply apply) {
+ apply.apply(this);
+ return this;
+ }
+
+ @Override
+ public SqlInsert batch() {
+ if ((parameterList != null && !parameterList.isEmpty())
+ || (parameterMap != null && !parameterMap.isEmpty())) {
+ if (batched == null) {
+ batched = new ArrayList<>();
+ }
+ batched.add(new Batch(parameterList, parameterMap));
+ parameterList = new ArrayList<>();
+ parameterMap = new HashMap<>();
+ }
+ return this;
+ }
+
+ @Override
+ public int insert() {
+ return updateInternal(0);
+ }
+
+ @Override
+ public void insert(int expectedRowsUpdated) {
+ updateInternal(expectedRowsUpdated);
+ }
+
+ @Override
+ public void insertBatch() {
+ int[] result = updateBatch();
+ for (int r : result) {
+ // Tolerate SUCCESS_NO_INFO for older versions of Oracle
+ if (r != 1 && r != Statement.SUCCESS_NO_INFO) {
+ throw new DatabaseException("Batch did not return the expected result: " + Arrays.toString(result));
+ }
+ }
+ }
+
+ @Override
+ public int[] insertBatchUnchecked() {
+ return updateBatch();
+ }
+
+ @Override
+ public Long insertReturningPkSeq(String primaryKeyColumnName) {
+ if (!hasPk()) {
+ throw new DatabaseException("Call argPkSeq() before insertReturningPkSeq()");
+ }
+
+ if (options.flavor().supportsInsertReturning()) {
+ return updateInternal(1, primaryKeyColumnName);
+ } else {
+ // Simulate by issuing a select for the next sequence value, inserting, and returning it
+ Long pk = new SqlSelectImpl(connection, options.flavor().sequenceSelectNextVal(pkSeqName), options).queryLongOrNull();
+ if (pk == null) {
+ throw new DatabaseException("Unable to retrieve next sequence value from " + pkSeqName);
+ }
+ if (pkArgName != null) {
+ namedArg(pkArgName, adaptor.nullNumeric(pk));
+ } else {
+ parameterList.set(pkPos, adaptor.nullNumeric(pk));
+ }
+ updateInternal(1);
+ return pk;
+ }
+ }
+
+ @Override
+ public T insertReturning(String tableName, String primaryKeyColumnName, RowsHandler handler,
+ String... otherColumnNames) {
+ if (!hasPk()) {
+ throw new DatabaseException("Identify a primary key with argPk*() before insertReturning()");
+ }
+
+ if (options.flavor().supportsInsertReturning()) {
+ return updateInternal(1, primaryKeyColumnName, handler, otherColumnNames);
+ } else if (pkSeqName != null) {
+ // Simulate by issuing a select for the next sequence value, inserting, and returning it
+ Long pk = new SqlSelectImpl(connection, options.flavor().sequenceSelectNextVal(pkSeqName), options)
+ .queryLongOrNull();
+ if (pk == null) {
+ throw new DatabaseException("Unable to retrieve next sequence value from " + pkSeqName);
+ }
+ if (pkArgName != null) {
+ namedArg(pkArgName, adaptor.nullNumeric(pk));
+ } else {
+ parameterList.set(pkPos, adaptor.nullNumeric(pk));
+ }
+ updateInternal(1);
+ StringBuilder sql = new StringBuilder();
+ sql.append("select ").append(primaryKeyColumnName);
+ for (String colName : otherColumnNames) {
+ sql.append(", ").append(colName);
+ }
+ sql.append(" from ").append(tableName).append(" where ").append(primaryKeyColumnName).append("=?");
+ return new SqlSelectImpl(connection, sql.toString(), options).argLong(pk).query(handler);
+ } else if (pkLong != null) {
+ // Insert the value, then do a select based on the primary key
+ updateInternal(1);
+ StringBuilder sql = new StringBuilder();
+ sql.append("select ").append(primaryKeyColumnName);
+ for (String colName : otherColumnNames) {
+ sql.append(", ").append(colName);
+ }
+ sql.append(" from ").append(tableName).append(" where ").append(primaryKeyColumnName).append("=?");
+ return new SqlSelectImpl(connection, sql.toString(), options).argLong(pkLong).query(handler);
+ } else {
+ // Should never happen if our safety checks worked
+ throw new DatabaseException("Internal error");
+ }
+ }
+
+
+ @Override
+ public SqlInsert argPkSeq( String sequenceName) {
+ if (hasPk() && batched == null) {
+ throw new DatabaseException("Only call one argPk*() method");
+ }
+ if (hasPk() && (!pkSeqName.equals(sequenceName) || pkPos != parameterList.size())) {
+ throw new DatabaseException("The argPkSeq() calls must be in the same position across batch records");
+ }
+ pkSeqName = sequenceName;
+ SqlInsert sqlInsert = positionalArg(new RewriteArg(options.flavor().sequenceNextVal(sequenceName)));
+ pkPos = parameterList.size() - 1;
+ return sqlInsert;
+ }
+
+ @Override
+
+ public SqlInsert argPkSeq( String argName, String sequenceName) {
+ if (hasPk() && batched == null) {
+ throw new DatabaseException("Only call one argPk*() method");
+ }
+ if (hasPk() && !argName.equals(pkArgName)) {
+ throw new DatabaseException("The primary key argument name must match across batch rows");
+ }
+ pkArgName = argName;
+ pkSeqName = sequenceName;
+ return namedArg(argName, new RewriteArg(options.flavor().sequenceNextVal(sequenceName)));
+ }
+
+ @Override
+
+ public SqlInsert argPkLong(String argName, Long arg) {
+ if (hasPk() && batched == null) {
+ throw new DatabaseException("Only call one argPk*() method");
+ }
+ if (hasPk() && !argName.equals(pkArgName)) {
+ throw new DatabaseException("The primary key argument name must match across batch rows");
+ }
+ pkArgName = argName;
+ pkLong = arg;
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlInsert argPkLong(Long arg) {
+ if (hasPk() && batched == null) {
+ throw new DatabaseException("Only call one argPk*() method");
+ }
+ if (hasPk() && pkPos != parameterList.size()) {
+ throw new DatabaseException("The argPkLong() calls must be in the same position across batch records");
+ }
+ pkLong = arg;
+ SqlInsert sqlInsert = positionalArg(adaptor.nullNumeric(arg));
+ pkPos = parameterList.size() - 1;
+ return sqlInsert;
+ }
+
+ private boolean hasPk() {
+ return pkArgName != null || pkSeqName != null || pkLong != null;
+ }
+
+ private int[] updateBatch() {
+ batch();
+
+ if (batched == null || batched.size() == 0) {
+ throw new DatabaseException("Batch insert requires parameters");
+ }
+
+ PreparedStatement ps = null;
+ Metric metric = new Metric(log.isLoggable(Level.FINE));
+
+ String executeSql = sql;
+ Object[] firstRowParameters = null;
+ List parameters = new ArrayList<>();
+
+ boolean isSuccess = false;
+ String errorCode = null;
+ Exception logEx = null;
+ try {
+ for (Batch batch : batched) {
+ MixedParameterSql mpSql = new MixedParameterSql(sql, batch.parameterList, batch.parameterMap);
+ if (firstRowParameters == null) {
+ executeSql = mpSql.getSqlToExecute();
+ firstRowParameters = mpSql.getArgs();
+ } else {
+ if (!executeSql.equals(mpSql.getSqlToExecute())) {
+ throw new DatabaseException("All rows in a batch must use parameters in the same way. \nSQL1: "
+ + executeSql + "\nSQL2: " + mpSql.getSqlToExecute());
+ }
+ }
+ parameters.add(mpSql.getArgs());
+ }
+
+ if (connection != null) {
+ ps = connection.prepareStatement(executeSql);
+
+ for (Object[] params : parameters) {
+ adaptor.addParameters(ps, params);
+ ps.addBatch();
+ }
+
+ metric.checkpoint("prep");
+ int[] numAffectedRows = ps.executeBatch();
+ metric.checkpoint("execBatch", parameters.size());
+ isSuccess = true;
+ return numAffectedRows;
+ } else {
+ return new int[]{-1};
+ }
+ } catch (WrongNumberOfRowsException e) {
+ throw e;
+ } catch (Exception e) {
+ errorCode = options.generateErrorCode();
+ logEx = e;
+ throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, firstRowParameters, errorCode, options), e);
+ } finally {
+ adaptor.closeQuietly(ps, log);
+ metric.done("close");
+ if (isSuccess) {
+ DebugSql.logSuccess("Insert", log, metric, executeSql, firstRowParameters, options);
+ } else {
+ DebugSql.logError("Insert", log, metric, errorCode, executeSql, firstRowParameters, options, logEx);
+ }
+ }
+ }
+
+ private int updateInternal(int expectedNumAffectedRows) {
+ if (batched != null) {
+ throw new DatabaseException("Call insertBatch() if you are using the batch() feature");
+ }
+
+ PreparedStatement ps = null;
+ Metric metric = new Metric(log.isLoggable(Level.FINE));
+
+ String executeSql = sql;
+ Object[] parameters = null;
+
+ boolean isSuccess = false;
+ String errorCode = null;
+ Exception logEx = null;
+ try {
+ MixedParameterSql mpSql = new MixedParameterSql(sql, parameterList, parameterMap);
+ executeSql = mpSql.getSqlToExecute();
+ parameters = mpSql.getArgs();
+
+ if (connection != null) {
+ ps = connection.prepareStatement(executeSql);
+
+ adaptor.addParameters(ps, parameters);
+ metric.checkpoint("prep");
+ int numAffectedRows = ps.executeUpdate();
+ metric.checkpoint("exec", numAffectedRows);
+ if (expectedNumAffectedRows > 0 && numAffectedRows != expectedNumAffectedRows) {
+ errorCode = options.generateErrorCode();
+ throw new WrongNumberOfRowsException("The number of affected rows was " + numAffectedRows + ", but "
+ + expectedNumAffectedRows + " were expected." + "\n"
+ + DebugSql.exceptionMessage(executeSql, parameters, errorCode, options));
+ }
+ isSuccess = true;
+ return numAffectedRows;
+ } else {
+ return -1;
+ }
+ } catch (WrongNumberOfRowsException e) {
+ throw e;
+ } catch (Exception e) {
+ errorCode = options.generateErrorCode();
+ logEx = e;
+ throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, parameters, errorCode, options), e);
+ } finally {
+ adaptor.closeQuietly(ps, log);
+ metric.done("close");
+ if (isSuccess) {
+ DebugSql.logSuccess("Insert", log, metric, executeSql, parameters, options);
+ } else {
+ DebugSql.logError("Insert", log, metric, errorCode, executeSql, parameters, options, logEx);
+ }
+ }
+ }
+
+ private Long updateInternal(int expectedNumAffectedRows, String pkToReturn) {
+ if (batched != null) {
+ throw new DatabaseException("Call insertBatch() if you are using the batch() feature");
+ }
+
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ Metric metric = new Metric(log.isLoggable(Level.FINE));
+
+ String executeSql = sql;
+ Object[] parameters = null;
+
+ boolean isSuccess = false;
+ String errorCode = null;
+ Exception logEx = null;
+ try {
+ MixedParameterSql mpSql = new MixedParameterSql(sql, parameterList, parameterMap);
+ executeSql = mpSql.getSqlToExecute();
+ parameters = mpSql.getArgs();
+
+ if (connection != null) {
+ ps = connection.prepareStatement(executeSql, new String[]{pkToReturn});
+
+ adaptor.addParameters(ps, parameters);
+ metric.checkpoint("prep");
+ int numAffectedRows = ps.executeUpdate();
+ metric.checkpoint("exec", numAffectedRows);
+ if (expectedNumAffectedRows > 0 && numAffectedRows != expectedNumAffectedRows) {
+ errorCode = options.generateErrorCode();
+ throw new WrongNumberOfRowsException("The number of affected rows was " + numAffectedRows + ", but "
+ + expectedNumAffectedRows + " were expected." + "\n"
+ + DebugSql.exceptionMessage(executeSql, parameters, errorCode, options));
+ }
+ rs = ps.getGeneratedKeys();
+ Long pk = null;
+ if (rs != null && rs.next()) {
+ pk = rs.getLong(1);
+ }
+ isSuccess = true;
+ return pk;
+ } else {
+ return null;
+ }
+ } catch (WrongNumberOfRowsException e) {
+ throw e;
+ } catch (Exception e) {
+ errorCode = options.generateErrorCode();
+ logEx = e;
+ throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, parameters, errorCode, options), e);
+ } finally {
+ adaptor.closeQuietly(rs, log);
+ adaptor.closeQuietly(ps, log);
+ metric.done("close");
+ if (isSuccess) {
+ DebugSql.logSuccess("Insert", log, metric, executeSql, parameters, options);
+ } else {
+ DebugSql.logError("Insert", log, metric, errorCode, executeSql, parameters, options, logEx);
+ }
+ }
+ }
+
+ private T updateInternal(int expectedNumAffectedRows, String pkToReturn, RowsHandler handler,
+ String... otherCols) {
+ if (batched != null) {
+ throw new DatabaseException("Call insertBatch() if you are using the batch() feature");
+ }
+
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ Metric metric = new Metric(log.isLoggable(Level.FINE));
+
+ String executeSql = sql;
+ Object[] parameters = null;
+
+ boolean isSuccess = false;
+ String errorCode = null;
+ Exception logEx = null;
+ try {
+ MixedParameterSql mpSql = new MixedParameterSql(sql, parameterList, parameterMap);
+ executeSql = mpSql.getSqlToExecute();
+ parameters = mpSql.getArgs();
+
+ String[] returnCols = new String[otherCols.length + 1];
+ returnCols[0] = pkToReturn;
+ System.arraycopy(otherCols, 0, returnCols, 1, otherCols.length);
+
+ if (connection != null) {
+ ps = connection.prepareStatement(executeSql, returnCols);
+
+ adaptor.addParameters(ps, parameters);
+ metric.checkpoint("prep");
+ int numAffectedRows = ps.executeUpdate();
+ metric.checkpoint("exec", numAffectedRows);
+ if (expectedNumAffectedRows > 0 && numAffectedRows != expectedNumAffectedRows) {
+ errorCode = options.generateErrorCode();
+ throw new WrongNumberOfRowsException("The number of affected rows was " + numAffectedRows + ", but "
+ + expectedNumAffectedRows + " were expected." + "\n"
+ + DebugSql.exceptionMessage(executeSql, parameters, errorCode, options));
+ }
+ rs = ps.getGeneratedKeys();
+ final ResultSet finalRs = rs;
+ T result = handler.process(new RowsAdaptor(finalRs, options));
+ metric.checkpoint("read");
+ isSuccess = true;
+ return result;
+ } else {
+ return null;
+ }
+ } catch (WrongNumberOfRowsException e) {
+ throw e;
+ } catch (Exception e) {
+ errorCode = options.generateErrorCode();
+ logEx = e;
+ throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, parameters, errorCode, options), e);
+ } finally {
+ adaptor.closeQuietly(rs, log);
+ adaptor.closeQuietly(ps, log);
+ metric.done("close");
+ if (isSuccess) {
+ DebugSql.logSuccess("Insert", log, metric, executeSql, parameters, options);
+ } else {
+ DebugSql.logError("Insert", log, metric, errorCode, executeSql, parameters, options, logEx);
+ }
+ }
+ }
+
+ private SqlInsert positionalArg(Object arg) {
+ if (parameterList == null) {
+ parameterList = new ArrayList<>();
+ }
+ parameterList.add(arg);
+ return this;
+ }
+
+ private SqlInsert namedArg(String argName, Object arg) {
+ if (parameterMap == null) {
+ parameterMap = new HashMap<>();
+ }
+ if (argName.startsWith(":")) {
+ argName = argName.substring(1);
+ }
+ parameterMap.put(argName, arg);
+ return this;
+ }
+
+ private String booleanToString(Boolean b) {
+ return b == null ? null : b ? "Y" : "N";
+ }
+
+ private class Batch {
+ private final List parameterList; // !null ==> traditional ? args
+ private final Map parameterMap; // !null ==> named :abc args
+
+ public Batch(List parameterList, Map parameterMap) {
+ this.parameterList = parameterList;
+ this.parameterMap = parameterMap;
+ }
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlNull.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlNull.java
new file mode 100644
index 0000000..230ff3e
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlNull.java
@@ -0,0 +1,14 @@
+package org.xbib.jdbc.query;
+
+public class SqlNull {
+
+ private final int type;
+
+ public SqlNull(int type) {
+ this.type = type;
+ }
+
+ int getType() {
+ return type;
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelect.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelect.java
new file mode 100644
index 0000000..82985c8
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelect.java
@@ -0,0 +1,289 @@
+package org.xbib.jdbc.query;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Interface for configuring (setting parameters) and executing a chunk of SQL.
+ */
+public interface SqlSelect {
+
+
+ SqlSelect argBoolean(Boolean arg);
+
+
+
+ SqlSelect argBoolean( String argName, Boolean arg);
+
+
+
+ SqlSelect argInteger(Integer arg);
+
+
+
+ SqlSelect argInteger( String argName, Integer arg);
+
+
+
+ SqlSelect argLong(Long arg);
+
+
+
+ SqlSelect argLong( String argName, Long arg);
+
+
+
+ SqlSelect argFloat(Float arg);
+
+
+
+ SqlSelect argFloat( String argName, Float arg);
+
+
+
+ SqlSelect argDouble(Double arg);
+
+
+
+ SqlSelect argDouble( String argName, Double arg);
+
+
+
+ SqlSelect argBigDecimal(BigDecimal arg);
+
+
+
+ SqlSelect argBigDecimal( String argName, BigDecimal arg);
+
+
+
+ SqlSelect argString(String arg);
+
+
+
+ SqlSelect argString( String argName, String arg);
+
+
+
+ SqlSelect argDate(Date arg); // Date with time
+
+
+
+ SqlSelect argDate( String argName, Date arg); // Date with time
+
+
+
+ SqlSelect argLocalDate(LocalDate arg); // Date without time
+
+
+
+ SqlSelect argLocalDate( String argName, LocalDate arg); // Date without time
+
+
+
+ SqlSelect argDateNowPerApp();
+
+
+
+ SqlSelect argDateNowPerApp( String argName);
+
+
+
+ SqlSelect argDateNowPerDb();
+
+
+
+ SqlSelect argDateNowPerDb( String argName);
+
+
+
+ SqlSelect withTimeoutSeconds(int seconds);
+
+
+
+ SqlSelect withMaxRows(int rows);
+
+
+
+ SqlSelect withArgs(SqlArgs args);
+
+
+
+ SqlSelect apply(Apply apply);
+
+
+
+ SqlSelect fetchSize(int fetchSize);
+
+
+
+ Boolean queryBooleanOrNull();
+
+
+ boolean queryBooleanOrFalse();
+
+
+ boolean queryBooleanOrTrue();
+
+
+
+ Long queryLongOrNull();
+
+
+ long queryLongOrZero();
+
+ /**
+ * Shorthand for reading numbers from the first column of the result.
+ *
+ * @return the first column values, omitting any that were null
+ */
+
+
+ List queryLongs();
+
+
+
+ Integer queryIntegerOrNull();
+
+
+ int queryIntegerOrZero();
+
+
+
+ List queryIntegers();
+
+
+
+ Float queryFloatOrNull();
+
+
+ float queryFloatOrZero();
+
+
+
+ List queryFloats();
+
+
+
+ Double queryDoubleOrNull();
+
+
+ double queryDoubleOrZero();
+
+
+
+ List queryDoubles();
+
+
+
+ BigDecimal queryBigDecimalOrNull();
+
+
+
+ BigDecimal queryBigDecimalOrZero();
+
+
+
+ List queryBigDecimals();
+
+
+
+ String queryStringOrNull();
+
+
+
+ String queryStringOrEmpty();
+
+ /**
+ * Shorthand for reading strings from the first column of the result.
+ *
+ * @return the first column values, omitting any that were null
+ */
+
+
+ List queryStrings();
+
+
+
+ Date queryDateOrNull(); // Date with time
+
+
+
+ List queryDates(); // Date with time
+
+
+
+ LocalDate queryLocalDateOrNull(); // Date without time
+
+
+
+ List queryLocalDates(); // Date without time
+
+ /**
+ * This is the most generic and low-level way to iterate the query results.
+ * Consider using one of the other methods that can handle the iteration for you.
+ *
+ * @param rowsHandler the process() method of this handler will be called once
+ * and it will be responsible for iterating the results
+ */
+ T query(RowsHandler rowsHandler);
+
+ /**
+ * Query zero or one row. If zero rows are available a null will be returned.
+ * If more than one row is available a {@link ConstraintViolationException}
+ * will be thrown.
+ *
+ * @param rowHandler the process() method of this handler will be called once
+ * if there are results, or will not be called if there are
+ * no results
+ */
+ T queryOneOrNull(RowHandler rowHandler);
+
+ /**
+ * Query exactly one row. If zero rows are available or more than one row is
+ * available a {@link ConstraintViolationException} will be thrown.
+ *
+ * @param rowHandler the process() method of this handler will be called once
+ * if there are results, or will not be called if there are
+ * no results
+ */
+ T queryOneOrThrow(RowHandler rowHandler);
+
+ /**
+ * Query zero or one row. If zero rows are available a null will be returned.
+ * If more than one row is available the first row will be returned.
+ *
+ * @param rowHandler the process() method of this handler will be called once
+ * if there are results (for the first row), or will not be
+ * called if there are no results
+ */
+ T queryFirstOrNull(RowHandler rowHandler);
+
+ /**
+ * Query zero or one row. If zero rows are available a {@link ConstraintViolationException}
+ * will be thrown. If more than one row is available the first row will be returned.
+ *
+ * @param rowHandler the process() method of this handler will be called once
+ * if there are results (for the first row), or will not be
+ * called if there are no results
+ */
+ T queryFirstOrThrow(RowHandler rowHandler);
+
+ /**
+ * Query zero or more rows. If zero rows are available an empty list will be returned.
+ * If one or more rows are available each row will be read and added to a list, which
+ * is returned.
+ *
+ * @param rowHandler the process() method of this handler will be called once
+ * for each row in the result, or will not be called if there are
+ * no results. Only non-null values returned will be added to the
+ * result list.
+ */
+ List queryMany(RowHandler rowHandler);
+
+ interface Apply {
+ void apply(SqlSelect select);
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelectImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelectImpl.java
new file mode 100644
index 0000000..e429ac4
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelectImpl.java
@@ -0,0 +1,766 @@
+package org.xbib.jdbc.query;
+
+import org.xbib.jdbc.query.util.DebugSql;
+import org.xbib.jdbc.query.util.Metric;
+import org.xbib.jdbc.query.util.RewriteArg;
+
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This is the key class for configuring (query parameters) and executing a database query.
+ */
+public class SqlSelectImpl implements SqlSelect {
+
+ private static final Logger log = Logger.getLogger(Database.class.getName());
+
+ private final Connection connection;
+
+ private final StatementAdaptor adaptor;
+
+ private final Object cancelLock = new Object();
+
+ private final String sql;
+
+ private final Options options;
+
+ private PreparedStatement ps; // hold reference to support cancel from another thread
+
+ private List parameterList; // !null ==> traditional ? args
+
+ private Map parameterMap; // !null ==> named :abc args
+
+ private int timeoutSeconds = -1; // -1 ==> no timeout
+
+ private int maxRows = -1; // -1 ==> unlimited
+
+ private int fetchSize = -1; // -1 ==> do not call setFetchSize()
+
+ public SqlSelectImpl(Connection connection, String sql, Options options) {
+ this.connection = connection;
+ this.sql = sql;
+ this.options = options;
+ adaptor = new StatementAdaptor(options);
+ }
+
+
+ @Override
+ public SqlSelect argBoolean(Boolean arg) {
+ return positionalArg(adaptor.nullString(booleanToString(arg)));
+ }
+
+
+ @Override
+ public SqlSelect argBoolean( String argName, Boolean arg) {
+ return namedArg(argName, adaptor.nullString(booleanToString(arg)));
+ }
+
+
+ @Override
+ public SqlSelect argInteger(Integer arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+
+ @Override
+ public SqlSelect argInteger( String argName, Integer arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+
+ @Override
+ public SqlSelect argLong(Long arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+
+ @Override
+ public SqlSelect argLong( String argName, Long arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+
+ @Override
+ public SqlSelect argFloat(Float arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+
+ @Override
+ public SqlSelect argFloat( String argName, Float arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+
+ @Override
+ public SqlSelect argDouble(Double arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+
+ @Override
+ public SqlSelect argDouble( String argName, Double arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+
+ @Override
+ public SqlSelect argBigDecimal(BigDecimal arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+
+ @Override
+ public SqlSelect argBigDecimal( String argName, BigDecimal arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+
+ @Override
+ public SqlSelect argString(String arg) {
+ return positionalArg(adaptor.nullString(arg));
+ }
+
+
+ @Override
+ public SqlSelect argString( String argName, String arg) {
+ return namedArg(argName, adaptor.nullString(arg));
+ }
+
+
+ @Override
+ public SqlSelect argDate(Date arg) {
+ // Date with time
+ return positionalArg(adaptor.nullDate(arg));
+ }
+
+
+ @Override
+ public SqlSelect argDate( String argName, Date arg) {
+ // Date with time
+ return namedArg(argName, adaptor.nullDate(arg));
+ }
+
+
+ @Override
+ public SqlSelect argLocalDate(LocalDate arg) {
+ // Date with no time
+ return positionalArg(adaptor.nullLocalDate(arg));
+ }
+
+
+ @Override
+ public SqlSelect argLocalDate( String argName, LocalDate arg) {
+ // Date with no time
+ return namedArg(argName, adaptor.nullLocalDate(arg));
+ }
+
+
+ @Override
+ public SqlSelect argDateNowPerApp() {
+ return positionalArg(adaptor.nullDate(options.currentDate()));
+ }
+
+
+ @Override
+ public SqlSelect argDateNowPerApp( String argName) {
+ return namedArg(argName, adaptor.nullDate(options.currentDate()));
+ }
+
+
+ @Override
+ public SqlSelect argDateNowPerDb() {
+ if (options.useDatePerAppOnly()) {
+ return positionalArg(adaptor.nullDate(options.currentDate()));
+ }
+ return positionalArg(new RewriteArg(options.flavor().dbTimeMillis()));
+ }
+
+
+ @Override
+ public SqlSelect argDateNowPerDb( String argName) {
+ if (options.useDatePerAppOnly()) {
+ return namedArg(argName, adaptor.nullDate(options.currentDate()));
+ }
+ return namedArg(argName, new RewriteArg(options.flavor().dbTimeMillis()));
+ }
+
+
+ @Override
+ public SqlSelect withTimeoutSeconds(int seconds) {
+ timeoutSeconds = seconds;
+ return this;
+ }
+
+
+ @Override
+ public SqlSelect withMaxRows(int rows) {
+ maxRows = rows;
+ return this;
+ }
+
+
+ @Override
+ public SqlSelect withArgs(SqlArgs args) {
+ return apply(args);
+ }
+
+
+ @Override
+ public SqlSelect apply(Apply apply) {
+ apply.apply(this);
+ return this;
+ }
+
+
+ @Override
+ public SqlSelect fetchSize(int rows) {
+ fetchSize = rows;
+ return this;
+ }
+
+
+ @Override
+ public Boolean queryBooleanOrNull() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public Boolean process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getBooleanOrNull();
+ }
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public boolean queryBooleanOrFalse() {
+ Boolean result = queryBooleanOrNull();
+ return result != null && result;
+ }
+
+ @Override
+ public boolean queryBooleanOrTrue() {
+ Boolean result = queryBooleanOrNull();
+ return result == null || result;
+ }
+
+ @Override
+
+ public Long queryLongOrNull() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public Long process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getLongOrNull(1);
+ }
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public long queryLongOrZero() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public Long process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getLongOrZero(1);
+ }
+ return 0L;
+ }
+ });
+ }
+
+
+ @Override
+ public List queryLongs() {
+ return queryWithTimeout(new RowsHandler>() {
+ @Override
+ public List process(Rows rs) throws Exception {
+ List result = new ArrayList<>();
+ while (rs.next()) {
+ Long value = rs.getLongOrNull(1);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ return result;
+ }
+ });
+ }
+
+
+ @Override
+ public Integer queryIntegerOrNull() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public Integer process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getIntegerOrNull(1);
+ }
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public int queryIntegerOrZero() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public Integer process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getIntegerOrZero(1);
+ }
+ return 0;
+ }
+ });
+ }
+
+
+ @Override
+ public List queryIntegers() {
+ return queryWithTimeout(new RowsHandler>() {
+ @Override
+ public List process(Rows rs) throws Exception {
+ List result = new ArrayList<>();
+ while (rs.next()) {
+ Integer value = rs.getIntegerOrNull(1);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ return result;
+ }
+ });
+ }
+
+
+ @Override
+ public Float queryFloatOrNull() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public Float process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getFloatOrNull(1);
+ }
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public float queryFloatOrZero() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public Float process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getFloatOrZero(1);
+ }
+ return 0f;
+ }
+ });
+ }
+
+
+ @Override
+ public List queryFloats() {
+ return queryWithTimeout(new RowsHandler>() {
+ @Override
+ public List process(Rows rs) throws Exception {
+ List result = new ArrayList<>();
+ while (rs.next()) {
+ Float value = rs.getFloatOrNull(1);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ return result;
+ }
+ });
+ }
+
+
+ @Override
+ public Double queryDoubleOrNull() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public Double process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getDoubleOrNull(1);
+ }
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public double queryDoubleOrZero() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public Double process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getDoubleOrZero(1);
+ }
+ return 0d;
+ }
+ });
+ }
+
+
+ @Override
+ public List queryDoubles() {
+ return queryWithTimeout(new RowsHandler>() {
+ @Override
+ public List process(Rows rs) throws Exception {
+ List result = new ArrayList<>();
+ while (rs.next()) {
+ Double value = rs.getDoubleOrNull(1);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ return result;
+ }
+ });
+ }
+
+
+ @Override
+ public BigDecimal queryBigDecimalOrNull() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public BigDecimal process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getBigDecimalOrNull(1);
+ }
+ return null;
+ }
+ });
+ }
+
+
+ @Override
+ public BigDecimal queryBigDecimalOrZero() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public BigDecimal process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getBigDecimalOrZero(1);
+ }
+ return new BigDecimal(0);
+ }
+ });
+ }
+
+
+ @Override
+ public List queryBigDecimals() {
+ return queryWithTimeout(new RowsHandler>() {
+ @Override
+ public List process(Rows rs) throws Exception {
+ List result = new ArrayList<>();
+ while (rs.next()) {
+ BigDecimal value = rs.getBigDecimalOrNull(1);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ return result;
+ }
+ });
+ }
+
+ @Override
+ public String queryStringOrNull() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public String process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getStringOrNull(1);
+ }
+ return null;
+ }
+ });
+ }
+
+
+ @Override
+ public String queryStringOrEmpty() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public String process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getStringOrEmpty(1);
+ }
+ return "";
+ }
+ });
+ }
+
+
+ @Override
+ public List queryStrings() {
+ return queryWithTimeout(new RowsHandler>() {
+ @Override
+ public List process(Rows rs) throws Exception {
+ List result = new ArrayList<>();
+ while (rs.next()) {
+ String value = rs.getStringOrNull(1);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ return result;
+ }
+ });
+ }
+
+
+ @Override
+ public Date queryDateOrNull() {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public Date process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getDateOrNull(1);
+ }
+ return null;
+ }
+ });
+ }
+
+
+ @Override
+ public List queryDates() {
+ return queryWithTimeout(new RowsHandler>() {
+ @Override
+ public List process(Rows rs) throws Exception {
+ List result = new ArrayList<>();
+ while (rs.next()) {
+ Date value = rs.getDateOrNull(1);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ return result;
+ }
+ });
+ }
+
+
+ @Override
+ public LocalDate queryLocalDateOrNull() {
+ // Date without time
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public LocalDate process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rs.getLocalDateOrNull(1);
+ }
+ return null;
+ }
+ });
+ }
+
+
+ @Override
+ public List queryLocalDates() {
+ // Date without time
+ return queryWithTimeout(new RowsHandler>() {
+ @Override
+ public List process(Rows rs) throws Exception {
+ List result = new ArrayList<>();
+ while (rs.next()) {
+ LocalDate value = rs.getLocalDateOrNull(1);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ return result;
+ }
+ });
+ }
+
+ @Override
+ public T query(RowsHandler rowsHandler) {
+ return queryWithTimeout(rowsHandler);
+ }
+
+ @Override
+ public T queryOneOrNull(final RowHandler rowHandler) {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public T process(Rows rs) throws Exception {
+ if (rs.next()) {
+ T result = rowHandler.process(rs);
+ if (rs.next()) {
+ throw new ConstraintViolationException("Expected exactly one row to be returned but found multiple");
+ }
+ return result;
+ }
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public T queryOneOrThrow(RowHandler rowHandler) {
+ T result = queryOneOrNull(rowHandler);
+ if (result == null) {
+ throw new ConstraintViolationException("Expected exactly one row to be returned but found none");
+ }
+ return result;
+ }
+
+ @Override
+ public T queryFirstOrNull(final RowHandler rowHandler) {
+ return queryWithTimeout(new RowsHandler() {
+ @Override
+ public T process(Rows rs) throws Exception {
+ if (rs.next()) {
+ return rowHandler.process(rs);
+ }
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public T queryFirstOrThrow(RowHandler rowHandler) {
+ T result = queryFirstOrNull(rowHandler);
+ if (result == null) {
+ throw new ConstraintViolationException("Expected one or more rows to be returned but found none");
+ }
+ return result;
+ }
+
+ @Override
+ public List queryMany(final RowHandler rowHandler) {
+ return queryWithTimeout(new RowsHandler>() {
+ @Override
+ public List process(Rows rs) throws Exception {
+ List result = new ArrayList<>();
+
+ while (rs.next()) {
+ T row = rowHandler.process(rs);
+ if (row != null) {
+ result.add(row);
+ }
+ }
+
+ return result;
+ }
+ });
+ }
+
+ private SqlSelect positionalArg(Object arg) {
+ if (parameterList == null) {
+ parameterList = new ArrayList<>();
+ }
+ parameterList.add(arg);
+ return this;
+ }
+
+ private SqlSelect namedArg(String argName, Object arg) {
+ if (parameterMap == null) {
+ parameterMap = new HashMap<>();
+ }
+ if (argName.startsWith(":")) {
+ argName = argName.substring(1);
+ }
+ parameterMap.put(argName, arg);
+ return this;
+ }
+
+ private String booleanToString(Boolean b) {
+ return b == null ? null : b ? "Y" : "N";
+ }
+
+ private T queryWithTimeout(RowsHandler handler) {
+ assert ps == null;
+ ResultSet rs = null;
+ Metric metric = new Metric(log.isLoggable(Level.FINE));
+
+ String executeSql = sql;
+ Object[] parameters = null;
+
+ boolean isWarn = false;
+ boolean isSuccess = false;
+ String errorCode = null;
+ Exception logEx = null;
+ try {
+ MixedParameterSql mpSql = new MixedParameterSql(sql, parameterList, parameterMap);
+ executeSql = mpSql.getSqlToExecute();
+ parameters = mpSql.getArgs();
+
+ if (connection != null) {
+ synchronized (cancelLock) {
+ ps = connection.prepareStatement(executeSql);
+ }
+
+ if (timeoutSeconds >= 0) {
+ ps.setQueryTimeout(timeoutSeconds);
+ }
+
+ if (maxRows > 0) {
+ ps.setMaxRows(maxRows);
+ }
+
+ if (fetchSize >= 0) {
+ ps.setFetchSize(fetchSize);
+ }
+
+ adaptor.addParameters(ps, parameters);
+ metric.checkpoint("prep");
+ rs = ps.executeQuery();
+ metric.checkpoint("exec");
+ final ResultSet finalRs = rs;
+ T result = handler.process(new RowsAdaptor(finalRs, options));
+ metric.checkpoint("read");
+ isSuccess = true;
+ return result;
+ } else {
+ return null;
+ }
+ } catch (SQLException e) {
+ if (e.getErrorCode() == 1013) {
+ isWarn = true;
+ // It's ambiguous based on the Oracle error code whether it was a timeout or cancel
+ throw new QueryTimedOutException("Timeout of " + timeoutSeconds + " seconds exceeded or user cancelled", e);
+ }
+ errorCode = options.generateErrorCode();
+ logEx = e;
+ throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, parameters, errorCode, options), e);
+ } catch (Exception e) {
+ errorCode = options.generateErrorCode();
+ logEx = e;
+ throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, parameters, errorCode, options), e);
+ } finally {
+ adaptor.closeQuietly(rs, log);
+ adaptor.closeQuietly(ps, log);
+ synchronized (cancelLock) {
+ ps = null;
+ }
+ metric.done("close");
+ if (isSuccess) {
+ DebugSql.logSuccess("Query", log, metric, executeSql, parameters, options);
+ } else if (isWarn) {
+ DebugSql.logWarning("Query", log, metric, "QueryTimedOutException", executeSql, parameters, options, logEx);
+ } else {
+ DebugSql.logError("Query", log, metric, errorCode, executeSql, parameters, options, logEx);
+ }
+ }
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdate.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdate.java
new file mode 100644
index 0000000..e2c14cc
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdate.java
@@ -0,0 +1,165 @@
+package org.xbib.jdbc.query;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Date;
+
+/**
+ * Interface for configuring (setting parameters) and executing a chunk of SQL.
+ */
+public interface SqlUpdate {
+
+
+ SqlUpdate argBoolean(Boolean arg);
+
+
+
+ SqlUpdate argBoolean( String argName, Boolean arg);
+
+
+
+ SqlUpdate argInteger(Integer arg);
+
+
+
+ SqlUpdate argInteger( String argName, Integer arg);
+
+
+
+ SqlUpdate argLong(Long arg);
+
+
+
+ SqlUpdate argLong( String argName, Long arg);
+
+
+
+ SqlUpdate argFloat(Float arg);
+
+
+
+ SqlUpdate argFloat( String argName, Float arg);
+
+
+
+ SqlUpdate argDouble(Double arg);
+
+
+
+ SqlUpdate argDouble( String argName, Double arg);
+
+
+
+ SqlUpdate argBigDecimal(BigDecimal arg);
+
+
+
+ SqlUpdate argBigDecimal( String argName, BigDecimal arg);
+
+
+
+ SqlUpdate argString(String arg);
+
+
+
+ SqlUpdate argString( String argName, String arg);
+
+
+
+ SqlUpdate argDate(Date arg); // Date with timestamp
+
+
+
+ SqlUpdate argDate( String argName, Date arg); // Date with timestamp
+
+
+
+ SqlUpdate argLocalDate(LocalDate arg); // Date only - no timestamp
+
+
+
+ SqlUpdate argLocalDate( String argName, LocalDate arg); // Date only - no timestamp
+
+
+
+ SqlUpdate argDateNowPerApp();
+
+
+
+ SqlUpdate argDateNowPerApp( String argName);
+
+
+
+ SqlUpdate argDateNowPerDb();
+
+
+
+ SqlUpdate argDateNowPerDb( String argName);
+
+
+
+ SqlUpdate argBlobBytes(byte[] arg);
+
+
+
+ SqlUpdate argBlobBytes( String argName, byte[] arg);
+
+
+
+ SqlUpdate argBlobStream(InputStream arg);
+
+
+
+ SqlUpdate argBlobStream( String argName, InputStream arg);
+
+
+
+ SqlUpdate argClobString(String arg);
+
+
+
+ SqlUpdate argClobString( String argName, String arg);
+
+
+
+ SqlUpdate argClobReader(Reader arg);
+
+
+
+ SqlUpdate argClobReader( String argName, Reader arg);
+
+
+
+ SqlUpdate withArgs(SqlArgs args);
+
+
+
+ SqlUpdate apply(Apply apply);
+
+ /**
+ * Execute the SQL update and return the number of rows was affected.
+ */
+ int update();
+
+ /**
+ * Execute the SQL update and check that the expected number of rows was affected.
+ *
+ * @throws WrongNumberOfRowsException if the number of rows affected did not match
+ * the value provided
+ */
+ void update(int expectedRowsUpdated);
+
+ /**
+ * Call this between setting rows of parameters for a SQL statement. You may call it before
+ * setting any parameters, after setting all, or multiple times between rows.
+ */
+// SqlUpdate batch();
+
+// SqlUpdate withTimeoutSeconds(int seconds);
+
+ interface Apply {
+ void apply(SqlUpdate update);
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdateImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdateImpl.java
new file mode 100644
index 0000000..98d01ed
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdateImpl.java
@@ -0,0 +1,334 @@
+package org.xbib.jdbc.query;
+
+import org.xbib.jdbc.query.util.DebugSql;
+import org.xbib.jdbc.query.util.InternalStringReader;
+import org.xbib.jdbc.query.util.Metric;
+import org.xbib.jdbc.query.util.RewriteArg;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This is the key class for configuring (query parameters) and executing a database query.
+ */
+public class SqlUpdateImpl implements SqlUpdate {
+
+ private static final Logger log = Logger.getLogger(Database.class.getName());
+
+ private final Connection connection;
+
+ private final StatementAdaptor adaptor;
+
+ private final String sql;
+
+ private final Options options;
+
+ private List parameterList;
+
+ private Map parameterMap;
+
+ public SqlUpdateImpl(Connection connection, String sql, Options options) {
+ this.connection = connection;
+ this.sql = sql;
+ this.options = options;
+ adaptor = new StatementAdaptor(options);
+ }
+
+
+ @Override
+ public SqlUpdate argBoolean(Boolean arg) {
+ return positionalArg(adaptor.nullString(booleanToString(arg)));
+ }
+
+
+ @Override
+ public SqlUpdate argBoolean(String argName, Boolean arg) {
+ return namedArg(argName, adaptor.nullString(booleanToString(arg)));
+ }
+
+ @Override
+
+ public SqlUpdate argInteger(Integer arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argInteger(String argName, Integer arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argLong(Long arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argLong(String argName, Long arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argFloat(Float arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argFloat(String argName, Float arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argDouble(Double arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argDouble(String argName, Double arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argBigDecimal(BigDecimal arg) {
+ return positionalArg(adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argBigDecimal(String argName, BigDecimal arg) {
+ return namedArg(argName, adaptor.nullNumeric(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argString(String arg) {
+ return positionalArg(adaptor.nullString(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argString(String argName, String arg) {
+ return namedArg(argName, adaptor.nullString(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argDate(Date arg) {
+ // Date with time
+ return positionalArg(adaptor.nullDate(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argDate(String argName, Date arg) {
+ // Date with time
+ return namedArg(argName, adaptor.nullDate(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argLocalDate(LocalDate arg) {
+ // Date with no time
+ return positionalArg(adaptor.nullLocalDate(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argLocalDate(String argName, LocalDate arg) {
+ // Date with no time
+ return namedArg(argName, adaptor.nullLocalDate(arg));
+ }
+
+
+ @Override
+ public SqlUpdate argDateNowPerApp() {
+ return positionalArg(adaptor.nullDate(options.currentDate()));
+ }
+
+ @Override
+
+ public SqlUpdate argDateNowPerApp(String argName) {
+ return namedArg(argName, adaptor.nullDate(options.currentDate()));
+ }
+
+
+ @Override
+ public SqlUpdate argDateNowPerDb() {
+ if (options.useDatePerAppOnly()) {
+ return positionalArg(adaptor.nullDate(options.currentDate()));
+ }
+ return positionalArg(new RewriteArg(options.flavor().dbTimeMillis()));
+ }
+
+ @Override
+
+ public SqlUpdate argDateNowPerDb(String argName) {
+ if (options.useDatePerAppOnly()) {
+ return namedArg(argName, adaptor.nullDate(options.currentDate()));
+ }
+ return namedArg(argName, new RewriteArg(options.flavor().dbTimeMillis()));
+ }
+
+ @Override
+
+ public SqlUpdate argBlobBytes(byte[] arg) {
+ return positionalArg(adaptor.nullBytes(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argBlobBytes(String argName, byte[] arg) {
+ return namedArg(argName, adaptor.nullBytes(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argBlobStream(InputStream arg) {
+ return positionalArg(adaptor.nullInputStream(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argBlobStream(String argName, InputStream arg) {
+ return namedArg(argName, adaptor.nullInputStream(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argClobString(String arg) {
+ return positionalArg(adaptor.nullClobReader(arg == null ? null : new InternalStringReader(arg)));
+ }
+
+ @Override
+
+ public SqlUpdate argClobString(String argName, String arg) {
+ return namedArg(argName, adaptor.nullClobReader(arg == null ? null : new InternalStringReader(arg)));
+ }
+
+ @Override
+
+ public SqlUpdate argClobReader(Reader arg) {
+ return positionalArg(adaptor.nullClobReader(arg));
+ }
+
+ @Override
+
+ public SqlUpdate argClobReader(String argName, Reader arg) {
+ return namedArg(argName, adaptor.nullClobReader(arg));
+ }
+
+
+ @Override
+ public SqlUpdate withArgs(SqlArgs args) {
+ return apply(args);
+ }
+
+
+ @Override
+ public SqlUpdate apply(Apply apply) {
+ apply.apply(this);
+ return this;
+ }
+
+ @Override
+ public int update() {
+ return updateInternal(0);
+ }
+
+ @Override
+ public void update(int expectedNumAffectedRows) {
+ updateInternal(expectedNumAffectedRows);
+ }
+
+ private int updateInternal(int expectedNumAffectedRows) {
+ PreparedStatement ps = null;
+ Metric metric = new Metric(log.isLoggable(Level.FINE));
+
+ String executeSql = sql;
+ Object[] parameters = null;
+
+ boolean isSuccess = false;
+ String errorCode = null;
+ Exception logEx = null;
+ try {
+ MixedParameterSql mpSql = new MixedParameterSql(sql, parameterList, parameterMap);
+ executeSql = mpSql.getSqlToExecute();
+ parameters = mpSql.getArgs();
+
+ if (connection != null) {
+ ps = connection.prepareStatement(executeSql);
+ adaptor.addParameters(ps, parameters);
+ metric.checkpoint("prep");
+ int numAffectedRows = ps.executeUpdate();
+ metric.checkpoint("exec", numAffectedRows);
+ if (expectedNumAffectedRows > 0 && numAffectedRows != expectedNumAffectedRows) {
+ errorCode = options.generateErrorCode();
+ throw new WrongNumberOfRowsException("The number of affected rows was " + numAffectedRows + ", but "
+ + expectedNumAffectedRows + " were expected." + "\n"
+ + DebugSql.exceptionMessage(executeSql, parameters, errorCode, options));
+ }
+ isSuccess = true;
+ return numAffectedRows;
+ } else {
+ return -1;
+ }
+ } catch (WrongNumberOfRowsException e) {
+ throw e;
+ } catch (Exception e) {
+ errorCode = options.generateErrorCode();
+ logEx = e;
+ throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, parameters, errorCode, options), e);
+ } finally {
+ adaptor.closeQuietly(ps, log);
+ metric.done("close");
+ if (isSuccess) {
+ DebugSql.logSuccess("Update", log, metric, executeSql, parameters, options);
+ } else {
+ DebugSql.logError("Update", log, metric, errorCode, executeSql, parameters, options, logEx);
+ }
+ }
+ }
+
+
+ private SqlUpdate positionalArg(Object arg) {
+ if (parameterList == null) {
+ parameterList = new ArrayList<>();
+ }
+ parameterList.add(arg);
+ return this;
+ }
+
+
+ private SqlUpdate namedArg(String argName, Object arg) {
+ if (parameterMap == null) {
+ parameterMap = new HashMap<>();
+ }
+ if (argName.startsWith(":")) {
+ argName = argName.substring(1);
+ }
+ parameterMap.put(argName, arg);
+ return this;
+ }
+
+ private String booleanToString(Boolean b) {
+ return b == null ? null : b ? "Y" : "N";
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/StatementAdaptor.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/StatementAdaptor.java
new file mode 100644
index 0000000..47099da
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/StatementAdaptor.java
@@ -0,0 +1,220 @@
+package org.xbib.jdbc.query;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.sql.ParameterMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.time.LocalDate;
+import java.util.Date;
+import java.util.Scanner;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Deal with mapping parameters into prepared statements.
+ */
+public class StatementAdaptor {
+ private final Options options;
+
+ public StatementAdaptor(Options options) {
+ this.options = options;
+ }
+
+ private static String readerToString(Reader r) {
+ Scanner s = new Scanner(r).useDelimiter("\\A");
+ return s.hasNext() ? s.next() : "";
+ }
+
+ private static byte[] streamToBytes(InputStream is) throws SQLException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int length;
+
+ try {
+ while ((length = is.read(buffer)) != -1) {
+ out.write(buffer, 0, length);
+ }
+ } catch (IOException e) {
+ throw new SQLException("Unable to convert InputStream parameter to bytes", e);
+ }
+
+ return out.toByteArray();
+ }
+
+ /**
+ * Converts the java.util.Date into a java.sql.Timestamp, following the nanos/millis canonicalization
+ * required by the spec. If a java.sql.Timestamp is passed in (since it extends java.util.Date),
+ * it will be checked and canonicalized only if not already correct.
+ */
+ private static Timestamp toSqlTimestamp(Date date) {
+ long millis = date.getTime();
+ int fractionalSecondMillis = (int) (millis % 1000); // guaranteed < 1000
+
+ if (fractionalSecondMillis == 0) { // this means it's already correct by the spec
+ if (date instanceof Timestamp) {
+ return (Timestamp) date;
+ } else {
+ return new Timestamp(millis);
+ }
+ } else { // the millis are invalid and need to be corrected
+ int tsNanos = fractionalSecondMillis * 1000000;
+ long tsMillis = millis - fractionalSecondMillis;
+ Timestamp timestamp = new Timestamp(tsMillis);
+ timestamp.setNanos(tsNanos);
+ return timestamp;
+ }
+ }
+
+ public void addParameters(PreparedStatement ps, Object[] parameters) throws SQLException {
+ for (int i = 0; i < parameters.length; i++) {
+ Object parameter = parameters[i];
+
+ // Unwrap secret args here so we can use them
+ if (parameter instanceof SecretArg) {
+ parameter = ((SecretArg) parameter).getArg();
+ }
+
+ if (parameter == null) {
+ ParameterMetaData metaData;
+ int parameterType;
+ try {
+ metaData = ps.getParameterMetaData();
+ parameterType = metaData.getParameterType(i + 1);
+ } catch (SQLException e) {
+ throw new DatabaseException("Parameter " + (i + 1)
+ + " was null and the JDBC driver could not report the type of this column."
+ + " Please update the JDBC driver to support PreparedStatement.getParameterMetaData()"
+ + " or use SqlNull in place of null values to this query.", e);
+ }
+ ps.setNull(i + 1, parameterType);
+ } else if (parameter instanceof SqlNull) {
+ SqlNull sqlNull = (SqlNull) parameter;
+ if (options.useBytesForBlob() && sqlNull.getType() == Types.BLOB) {
+ // The setNull() seems more correct, but PostgreSQL chokes on it
+ ps.setBytes(i + 1, null);
+ } else {
+ ps.setNull(i + 1, sqlNull.getType());
+ }
+ } else if (parameter instanceof java.sql.Date) {
+ ps.setDate(i + 1, (java.sql.Date) parameter);
+ } else if (parameter instanceof Date) {
+ // this will correct the millis and nanos according to the JDBC spec
+ // if a correct Timestamp is passed in, this will detect that and leave it alone
+ ps.setTimestamp(i + 1, toSqlTimestamp((Date) parameter), options.calendarForTimestamps());
+ } else if (parameter instanceof Reader) {
+ if (options.useStringForClob()) {
+ ps.setString(i + 1, readerToString((Reader) parameter));
+ } else {
+ ps.setCharacterStream(i + 1, (Reader) parameter);
+ }
+ } else if (parameter instanceof InputStream) {
+ if (options.useBytesForBlob()) {
+ ps.setBytes(i + 1, streamToBytes((InputStream) parameter));
+ } else {
+ ps.setBinaryStream(i + 1, (InputStream) parameter);
+ }
+ } else if (parameter instanceof Float) {
+ //if (options.flavor() == Flavor.oracle && ps.isWrapperFor(OraclePreparedStatement.class)) {
+ // The Oracle 11 driver setDouble() first converts the double to NUMBER, causing underflow
+ // for small values so we need to use the proprietary mechanism
+ //ps.unwrap(OraclePreparedStatement.class).setBinaryFloat(i + 1, (Float) parameter);
+ //} else {
+ ps.setFloat(i + 1, (Float) parameter);
+ //}
+ } else if (parameter instanceof Double) {
+ //if (options.flavor() == Flavor.oracle && ps.isWrapperFor(OraclePreparedStatement.class)) {
+ // The Oracle 11 driver setDouble() first converts the double to NUMBER, causing underflow
+ // for small values so we need to use the proprietary mechanism
+ //ps.unwrap(OraclePreparedStatement.class).setBinaryDouble(i + 1, (Double) parameter);
+ //} else {
+ ps.setDouble(i + 1, (Double) parameter);
+ //}
+ } else {
+ ps.setObject(i + 1, parameter);
+ }
+ }
+ }
+
+ public Object nullDate(Date arg) {
+ if (arg == null) {
+ return new SqlNull(Types.TIMESTAMP);
+ }
+ return new Timestamp(arg.getTime());
+ }
+
+ // Processes a true date without time information.
+ public Object nullLocalDate(LocalDate arg) {
+ if (arg == null) {
+ return new SqlNull(Types.DATE);
+ }
+
+ return java.sql.Date.valueOf(arg);
+ }
+
+ public Object nullNumeric(Number arg) {
+ if (arg == null) {
+ return new SqlNull(Types.NUMERIC);
+ }
+ return arg;
+ }
+
+ public Object nullString(String arg) {
+ if (arg == null) {
+ return new SqlNull(Types.VARCHAR);
+ }
+ return arg;
+ }
+
+ public Object nullClobReader(Reader arg) {
+ if (arg == null) {
+ return new SqlNull(Types.VARCHAR);
+ }
+ return arg;
+ }
+
+ public Object nullBytes(byte[] arg) {
+ if (arg == null) {
+ return new SqlNull(Types.BLOB);
+ }
+ return arg;
+ }
+
+ public Object nullInputStream(InputStream arg) {
+ if (arg == null) {
+ return new SqlNull(Types.BLOB);
+ }
+ return arg;
+ }
+
+ public void closeQuietly(ResultSet rs, Logger log) {
+ if (rs != null) {
+ try {
+ rs.close();
+ } catch (Exception e) {
+ if (log != null) {
+ log.log(Level.SEVERE, "Caught exception closing the ResultSet", e);
+ }
+ }
+ }
+ }
+
+ public void closeQuietly( Statement s, Logger log) {
+ if (s != null) {
+ try {
+ s.close();
+ } catch (Exception e) {
+ if (log != null) {
+ log.log(Level.SEVERE, "Caught exception closing the Statement", e);
+ }
+ }
+ }
+ }
+
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Transaction.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Transaction.java
new file mode 100644
index 0000000..af78996
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Transaction.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2015 The Board of Trustees of The Leland Stanford Junior University.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.xbib.jdbc.query;
+
+/**
+ * Allow customization of the transaction behavior.
+ */
+public interface Transaction {
+ /**
+ * @return whether this code block has requested rollback upon a {@link Throwable}
+ * being thrown from the run method - this only reflects what was requested
+ * by calling {@link #setRollbackOnError(boolean)}, which is not necessarily
+ * what will actually happen
+ */
+ boolean isRollbackOnError();
+
+ /**
+ * Use this to request either "commit always" or "commit unless error" behavior.
+ * This will have no effect if {@link #isRollbackOnly()} returns true.
+ *
+ * @param rollbackOnError true to rollback after errors; false to commit or rollback based on
+ * the other settings
+ * @see DatabaseProvider#transact(DbCodeTx)
+ */
+ void setRollbackOnError(boolean rollbackOnError);
+
+ /**
+ * @return whether this code block has requested unconditional rollback - this only
+ * reflects what was requested by calling {@link #setRollbackOnly(boolean)},
+ * which is not necessarily what will actually happen
+ */
+ boolean isRollbackOnly();
+
+ /**
+ *
If your code inside run() decides for some reason the transaction should rollback
+ * rather than commit, use this method.
+ *
+ * @param rollbackOnly true to request an unconditional rollback; false to commit or rollback based on
+ * the other settings
+ * @see DatabaseProvider#transact(DbCodeTx)
+ */
+ void setRollbackOnly(boolean rollbackOnly);
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/TransactionImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/TransactionImpl.java
new file mode 100644
index 0000000..b518cef
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/TransactionImpl.java
@@ -0,0 +1,30 @@
+package org.xbib.jdbc.query;
+
+/**
+ * Simple bean representing how the database transaction should behave
+ * in terms of commit/rollback.
+ */
+public class TransactionImpl implements Transaction {
+ private boolean rollbackOnError;
+ private boolean rollbackOnly;
+
+ @Override
+ public boolean isRollbackOnError() {
+ return rollbackOnError;
+ }
+
+ @Override
+ public void setRollbackOnError(boolean rollbackOnError) {
+ this.rollbackOnError = rollbackOnError;
+ }
+
+ @Override
+ public boolean isRollbackOnly() {
+ return rollbackOnly;
+ }
+
+ @Override
+ public void setRollbackOnly(boolean rollbackOnly) {
+ this.rollbackOnly = rollbackOnly;
+ }
+}
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
new file mode 100644
index 0000000..d66c152
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/When.java
@@ -0,0 +1,75 @@
+package org.xbib.jdbc.query;
+
+import java.util.Objects;
+
+/**
+ * Convenience for conditional SQL generation.
+ */
+public class When {
+
+ private final Flavor actualFlavor;
+
+ private String chosen;
+
+ public When(Flavor actualFlavor) {
+ this.actualFlavor = actualFlavor;
+ }
+
+ public When oracle(String sql) {
+ if (actualFlavor == Flavor.oracle) {
+ chosen = sql;
+ }
+ return this;
+ }
+
+ public When derby(String sql) {
+ if (actualFlavor == Flavor.derby) {
+ chosen = sql;
+ }
+ return this;
+ }
+
+ public When postgres(String sql) {
+ if (actualFlavor == Flavor.postgresql) {
+ chosen = sql;
+ }
+ return this;
+ }
+
+ public When sqlserver(String sql) {
+ if (actualFlavor == Flavor.sqlserver) {
+ chosen = sql;
+ }
+ return this;
+ }
+
+
+ public String other(String sql) {
+ if (chosen == null) {
+ chosen = sql;
+ }
+ return chosen;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ When when = (When) o;
+ return Objects.equals(chosen, when.chosen) && actualFlavor == when.actualFlavor;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(chosen, actualFlavor);
+ }
+
+ @Override
+ public String toString() {
+ return chosen == null ? "" : chosen;
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/WrongNumberOfRowsException.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/WrongNumberOfRowsException.java
new file mode 100644
index 0000000..860d4c6
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/WrongNumberOfRowsException.java
@@ -0,0 +1,13 @@
+package org.xbib.jdbc.query;
+
+/**
+ * Thrown when inserting/updating rows and the actual number of rows modified does
+ * not match the expected number of rows.
+ */
+@SuppressWarnings("serial")
+public class WrongNumberOfRowsException extends DatabaseException {
+
+ public WrongNumberOfRowsException(String message) {
+ super(message);
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/util/DebugSql.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/DebugSql.java
new file mode 100644
index 0000000..c3f469f
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/DebugSql.java
@@ -0,0 +1,175 @@
+package org.xbib.jdbc.query.util;
+
+import org.xbib.jdbc.query.Options;
+import org.xbib.jdbc.query.SecretArg;
+import org.xbib.jdbc.query.SqlNull;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Convenience class to substitute real values into a database query for debugging, logging, etc.
+ *
+ * WARNING!!! Never execute this SQL without manual inspection because this class does NOTHING
+ * to prevent SQL injection or any other bad things.
+ */
+public class DebugSql {
+ public static final String PARAM_SQL_SEPARATOR = "\tParamSql:\t";
+
+ public static String printDebugOnlySqlString(String sql, Object[] args, Options options) {
+ StringBuilder buf = new StringBuilder();
+ printSql(buf, sql, args, false, true, options);
+ return buf.toString();
+ }
+
+ public static void printSql(StringBuilder buf, String sql, Object[] args, Options options) {
+ printSql(buf, sql, args, true, options.isLogParameters(), options);
+ }
+
+ public static void printSql(StringBuilder buf, String sql, Object[] args, boolean includeExecSql,
+ boolean includeParameters, Options options) {
+ Object[] argsToPrint = args;
+ if (argsToPrint == null) {
+ argsToPrint = new Object[0];
+ }
+ int batchSize = -1;
+ if (argsToPrint.length > 0 && argsToPrint instanceof Object[][]) {
+ // The arguments provided were from a batch - just use the first set
+ batchSize = argsToPrint.length;
+ argsToPrint = (Object[]) argsToPrint[0];
+ }
+ String[] sqlParts = sql.split("\\?");
+ if (sqlParts.length != argsToPrint.length + (sql.endsWith("?") ? 0 : 1)) {
+ buf.append("(wrong # args) query: ");
+ buf.append(sql);
+ if (args != null) {
+ buf.append(" args: ");
+ if (includeParameters) {
+ buf.append(Arrays.toString(argsToPrint));
+ } else {
+ buf.append(argsToPrint.length);
+ }
+ }
+ } else {
+ if (includeExecSql) {
+ buf.append(removeTabs(sql));
+ }
+ if (includeParameters && argsToPrint.length > 0) {
+ if (includeExecSql) {
+ buf.append(PARAM_SQL_SEPARATOR);
+ }
+ for (int i = 0; i < argsToPrint.length; i++) {
+ buf.append(removeTabs(sqlParts[i]));
+ Object argToPrint = argsToPrint[i];
+ if (argToPrint instanceof String) {
+ String argToPrintString = (String) argToPrint;
+ int maxLength = options.maxStringLengthParam();
+ if (argToPrintString.length() > maxLength && maxLength > 0) {
+ buf.append("'").append(argToPrintString, 0, maxLength).append("...'");
+ } else {
+ buf.append("'");
+ buf.append(removeTabs(escapeSingleQuoted(argToPrintString)));
+ buf.append("'");
+ }
+ } else if (argToPrint instanceof SqlNull || argToPrint == null) {
+ buf.append("null");
+ } else if (argToPrint instanceof java.sql.Timestamp) {
+ buf.append(options.flavor().dateAsSqlFunction((Date) argToPrint, options.calendarForTimestamps()));
+ } else if (argToPrint instanceof java.sql.Date) {
+ buf.append(options.flavor().localDateAsSqlFunction((Date) argToPrint));
+ } else if (argToPrint instanceof Number) {
+ buf.append(argToPrint);
+ } else if (argToPrint instanceof Boolean) {
+ buf.append(((Boolean) argToPrint) ? "'Y'" : "'N'");
+ } else if (argToPrint instanceof SecretArg) {
+ buf.append("");
+ } else if (argToPrint instanceof InternalStringReader) {
+ String argToPrintString = ((InternalStringReader) argToPrint).getString();
+ int maxLength = options.maxStringLengthParam();
+ if (argToPrintString.length() > maxLength && maxLength > 0) {
+ buf.append("'").append(argToPrintString, 0, maxLength).append("...'");
+ } else {
+ buf.append("'");
+ buf.append(removeTabs(escapeSingleQuoted(argToPrintString)));
+ buf.append("'");
+ }
+ } else if (argToPrint instanceof Reader || argToPrint instanceof InputStream) {
+ buf.append("<").append(argToPrint.getClass().getName()).append(">");
+ } else if (argToPrint instanceof byte[]) {
+ buf.append("<").append(((byte[]) argToPrint).length).append(" bytes>");
+ } else {
+ buf.append("");
+ }
+ }
+ if (sqlParts.length > argsToPrint.length) {
+ buf.append(sqlParts[sqlParts.length - 1]);
+ }
+ }
+ }
+ if (batchSize != -1) {
+ buf.append(" (first in batch of ");
+ buf.append(batchSize);
+ buf.append(')');
+ }
+ }
+
+ private static String removeTabs(String s) {
+ return s == null ? null : s.replace("\t", "");
+ }
+
+ private static String escapeSingleQuoted(String s) {
+ return s == null ? null : s.replace("'", "''");
+ }
+
+ public static String exceptionMessage(String sql, Object[] parameters, String errorCode, Options options) {
+ StringBuilder buf = new StringBuilder("Error executing SQL");
+ if (errorCode != null) {
+ buf.append(" (errorCode=").append(errorCode).append(")");
+ }
+ if (options.isDetailedExceptions()) {
+ buf.append(": ");
+ DebugSql.printSql(buf, sql, parameters, options);
+ }
+ return buf.toString();
+ }
+
+ public static void logSuccess(String sqlType, Logger log, Metric metric, String sql, Object[] args, Options options) {
+ if (log.isLoggable(Level.FINE)) {
+ String msg = logMiddle('\t', sqlType, metric, null, sql, args, options);
+ log.fine(msg);
+ }
+ }
+
+ public static void logWarning(String sqlType, Logger log, Metric metric, String errorCode, String sql, Object[] args,
+ Options options, Throwable t) {
+ if (log.isLoggable(Level.WARNING)) {
+ String msg = logMiddle(' ', sqlType, metric, errorCode, sql, args, options);
+ log.log(Level.WARNING, msg, t);
+ }
+ }
+
+ public static void logError(String sqlType, Logger log, Metric metric, String errorCode, String sql, Object[] args,
+ Options options, Throwable t) {
+ if (log.isLoggable(Level.SEVERE)) {
+ String msg = logMiddle(' ', sqlType, metric, errorCode, sql, args, options);
+ log.log(Level.SEVERE, msg, t);
+ }
+ }
+
+ private static String logMiddle(char separator, String sqlType, Metric metric,
+ String errorCode, String sql, Object[] args, Options options) {
+ StringBuilder buf = new StringBuilder();
+ if (errorCode != null) {
+ buf.append("errorCode=").append(errorCode).append(" ");
+ }
+ buf.append(sqlType).append(": ");
+ metric.printMessage(buf);
+ buf.append(separator);
+ printSql(buf, sql, args, options);
+ return buf.toString();
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/util/InternalStringReader.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/InternalStringReader.java
new file mode 100644
index 0000000..3e807c6
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/InternalStringReader.java
@@ -0,0 +1,21 @@
+package org.xbib.jdbc.query.util;
+
+import java.io.StringReader;
+
+/**
+ * This class exists to distinguish cases where we are mapping String to Reader
+ * internally, but want to be able to know they really started as a String (and
+ * be able to get the String back for things like logging).
+ */
+public final class InternalStringReader extends StringReader {
+ private final String s;
+
+ public InternalStringReader(String s) {
+ super(s);
+ this.s = s;
+ }
+
+ public String getString() {
+ return s;
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/util/Metric.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/Metric.java
new file mode 100644
index 0000000..dd3ec02
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/Metric.java
@@ -0,0 +1,243 @@
+package org.xbib.jdbc.query.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class provides explicit instrumentation functionality.
+ */
+public class Metric {
+
+ private final boolean enabled;
+
+ private boolean done;
+
+ private long startNanos;
+
+ private long lastCheckpointNanos;
+
+ private List checkpoints;
+
+ /**
+ *
Create a metric tracking object and start the nanosecond timer. Times
+ * are obtained using {@code System.nanoTime()}. The canonical way to use
+ * this looks something like this:
+ *
+ *
+ * @param enabled {@code true} if timings will be taken, {@code false} to
+ * optimize out the time tracking
+ */
+ public Metric(boolean enabled) {
+ this.enabled = enabled;
+ if (enabled) {
+ checkpoints = new ArrayList<>();
+ startNanos = System.nanoTime();
+ lastCheckpointNanos = startNanos;
+ }
+ }
+
+ /**
+ * Find out how many milliseconds have elapsed since this timer was started.
+ *
+ * @return the number of milliseconds elapsed, or -1 if {@code false} was
+ * passed in the constructor
+ */
+ public long elapsedMillis() {
+ if (!enabled) {
+ return -1;
+ }
+ return (System.nanoTime() - startNanos) / 1000000;
+ }
+
+ /**
+ * Find out how many nanoseconds have elapsed since this timer was started.
+ *
+ * @return the number of nanoseconds elapsed, or -1 if {@code false} was
+ * passed in the constructor
+ */
+ public long elapsedNanos() {
+ if (!enabled) {
+ return -1;
+ }
+ return (System.nanoTime() - startNanos);
+ }
+
+ /**
+ * Set a mark for timing. It is strongly recommended to use a short,
+ * simple alphanumeric description. For example, "sent" or "didThat".
+ * With this version you can provide additional arguments, such as
+ * byte counts, which will be appended comma-separated within brackets.
+ * For example, {@code checkpoint("sent", 25, 3)} will result in a
+ * description "sent[25,3]". The string evaluation and concatenation is
+ * lazy, and won't be done if this metric is disabled.
+ *
+ * @param description a label for this mark; may not be null; spaces
+ * and tabs will be converted to underscores
+ * @param args additional information to append to the description; will
+ * print "null" if null; evaluated with String.valueOf() lazily
+ * and sanitized of most non-alphanumeric characters
+ */
+ public void checkpoint(String description, Object... args) {
+ if (enabled) {
+ long currentCheckpointNanos = System.nanoTime();
+ checkpoints.add(new Checkpoint(noTabsOrSpaces(description), currentCheckpointNanos - lastCheckpointNanos, args));
+ lastCheckpointNanos = currentCheckpointNanos;
+ }
+ }
+
+ /**
+ * Set a final mark for timing and stop the timer. Once you call this
+ * method, subsequent calls have no effect.
+ *
+ * @param description a label for this mark; may not be null; spaces
+ * and tabs will be converted to underscores
+ * @return time in nanoseconds from the start of this metric, or -1
+ * if {@code false} was passed in the constructor
+ */
+ public long done(String description, Object... args) {
+ checkpoint(description, args);
+ return done();
+ }
+
+ /**
+ * Indicate we are done (stop the timer). Once you call this
+ * method, subsequent calls have no effect.
+ *
+ * @return time in nanoseconds from the start of this metric, or -1
+ * if {@code false} was passed in the constructor
+ */
+ public long done() {
+ if (enabled) {
+ if (!done) {
+ lastCheckpointNanos = System.nanoTime();
+ done = true;
+ }
+ return lastCheckpointNanos - startNanos;
+ }
+ return -1;
+ }
+
+ /**
+ * Construct and return a message based on the timing and checkpoints. This
+ * will look like "123.456ms(checkpoint1=100.228ms,checkpoint2=23.228ms)"
+ * without the quotes. There will be no spaces or tabs in the output.
+ *
+ *
This will automatically call the done() method to stop the timer if
+ * you haven't already done so.
+ *
+ * @return a string with timing information, or {@code "metricsDisabled"}
+ * if {@code false} was passed in the constructor.
+ * @see #printMessage(StringBuilder)
+ */
+ public String getMessage() {
+ if (enabled) {
+ StringBuilder buf = new StringBuilder();
+ printMessage(buf);
+ return buf.toString();
+ }
+ return "metricsDisabled";
+ }
+
+ /**
+ * Construct and print a message based on the timing and checkpoints. This
+ * will look like "123.456ms(checkpoint1=100.228ms,checkpoint2=23.228ms)"
+ * without the quotes. There will be no spaces or tabs in the output. A
+ * value of {@code "metricsDisabled"} will be printed if {@code false} was
+ * passed in the constructor.
+ *
+ *
This will automatically call the done() method to stop the timer if
+ * you haven't already done so.
+ *
+ * @param buf the message will be printed to this builder
+ * @see #getMessage()
+ */
+ public void printMessage(StringBuilder buf) {
+ if (enabled) {
+ done();
+ writeNanos(buf, lastCheckpointNanos - startNanos);
+ if (!checkpoints.isEmpty()) {
+ buf.append("(");
+ boolean first = true;
+ for (Checkpoint checkpoint : checkpoints) {
+ if (first) {
+ first = false;
+ } else {
+ buf.append(',');
+ }
+ buf.append(checkpoint.description);
+ if (checkpoint.args != null && checkpoint.args.length > 0) {
+ buf.append('[');
+ boolean firstArg = true;
+ for (Object o : checkpoint.args) {
+ if (firstArg) {
+ firstArg = false;
+ } else {
+ buf.append(',');
+ }
+ buf.append(sanitizeArg(String.valueOf(o)));
+ }
+ buf.append(']');
+ }
+ buf.append('=');
+ writeNanos(buf, checkpoint.durationNanos);
+ }
+ buf.append(')');
+ }
+ } else {
+ buf.append("metricsDisabled");
+ }
+ }
+
+ private void writeNanos(StringBuilder buf, long nanos) {
+ if (nanos < 0) {
+ buf.append("-");
+ nanos = -nanos;
+ }
+ String nanosStr = Long.toString(nanos);
+ if (nanosStr.length() > 6) {
+ buf.append(nanosStr, 0, nanosStr.length() - 6);
+ buf.append('.');
+ buf.append(nanosStr, nanosStr.length() - 6, nanosStr.length() - 3);
+ } else {
+ buf.append("0.0000000", 0, 8 - Math.max(nanosStr.length(), 4));
+ if (nanosStr.length() > 3) {
+ buf.append(nanosStr, 0, nanosStr.length() - 3);
+ }
+ }
+ buf.append("ms");
+ }
+
+ private String noTabsOrSpaces(String s) {
+ return s.replace(' ', '_').replace('\t', '_');
+ }
+
+ private String sanitizeArg(String s) {
+ return s.replaceAll("[^\\p{Alnum}_.\\-\\+]", "*");
+ }
+
+ private static class Checkpoint {
+ private final Object[] args;
+ String description;
+ long durationNanos;
+
+ Checkpoint(String description, long durationNanos, Object... args) {
+ this.description = description;
+ this.durationNanos = durationNanos;
+ this.args = args;
+ }
+ }
+}
diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/util/RewriteArg.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/RewriteArg.java
new file mode 100644
index 0000000..675bc7b
--- /dev/null
+++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/RewriteArg.java
@@ -0,0 +1,14 @@
+package org.xbib.jdbc.query.util;
+
+public class RewriteArg {
+
+ private final String sql;
+
+ public RewriteArg(String sql) {
+ this.sql = sql;
+ }
+
+ public String getSql() {
+ return sql;
+ }
+}
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
new file mode 100644
index 0000000..eac47ee
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/CommonTest.java
@@ -0,0 +1,1715 @@
+package org.xbib.jdbc.query.test;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.xbib.jdbc.query.ConstraintViolationException;
+import org.xbib.jdbc.query.Database;
+import org.xbib.jdbc.query.DatabaseException;
+import org.xbib.jdbc.query.DatabaseProvider;
+import org.xbib.jdbc.query.OptionsDefault;
+import org.xbib.jdbc.query.OptionsOverride;
+import org.xbib.jdbc.query.Row;
+import org.xbib.jdbc.query.RowHandler;
+import org.xbib.jdbc.query.RowsHandler;
+import org.xbib.jdbc.query.Schema;
+import org.xbib.jdbc.query.Sql;
+import org.xbib.jdbc.query.SqlArgs;
+import org.xbib.jdbc.query.StatementAdaptor;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.sql.ResultSetMetaData;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.Month;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Exercise Database functionality with a real databases.
+ */
+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;
+
+ protected Date now = new Date();
+
+ protected LocalDate localDateNow = LocalDate.now();
+
+ @BeforeEach
+ public void setupJdbc() throws Exception {
+ dbp = createDatabaseProvider(new OptionsOverride() {
+ @Override
+ public Date currentDate() {
+ return now;
+ }
+
+ @Override
+ public Calendar calendarForTimestamps() {
+ return Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles"));
+ }
+ });
+ db = dbp.get();
+ db.dropTableQuietly(TEST_TABLE_NAME);
+ }
+
+ protected abstract DatabaseProvider createDatabaseProvider(OptionsOverride options) throws Exception;
+
+ @AfterEach
+ public void closeJdbc() {
+ if (dbp != null) {
+ dbp.commitAndClose();
+ }
+ }
+
+ @Test
+ public void tableExists() {
+ // Verify dbtest table does not exist
+ String lowercaseTable = TEST_TABLE_NAME.toLowerCase();
+ testTableLookup(lowercaseTable);
+ db.dropTableQuietly(lowercaseTable);
+
+ // Let's try creating a table with an upper case name and verify it works
+ String uppercaseTable = TEST_TABLE_NAME.toUpperCase();
+ testTableLookup(uppercaseTable);
+ db.dropTableQuietly(uppercaseTable);
+
+ // Verify that null or empty name is handled gracefully
+ assertFalse(db.tableExists(null));
+ assertFalse(db.tableExists(""));
+ }
+
+ private void testTableLookup(String tableName) {
+ // Verify test table does not exist
+ assertFalse(db.tableExists(tableName));
+
+ // Create and verify it exists.
+ new Schema().addTable(tableName).addColumn("pk").primaryKey().schema().execute(db);
+ assertTrue(db.tableExists(tableName));
+ }
+
+ @Test
+ public void normalizeTableName() {
+ // Verify that null and empty cases are handled gracefully
+ assertNull(db.normalizeTableName(null));
+ assertEquals("", db.normalizeTableName(""));
+
+ // Verify a quoted table name is returned in exactly the same case, with quotes removed.
+ String camelCaseTableName = "\"DbTest\"";
+ assertEquals(camelCaseTableName.substring(1, camelCaseTableName.length() - 1),
+ db.normalizeTableName(camelCaseTableName));
+
+ // Verify that the database flavor gets the expected normalized case
+ boolean isUpperCase = db.flavor().isNormalizedUpperCase();
+ if (isUpperCase) {
+ assertEquals(TEST_TABLE_NAME.toUpperCase(), db.normalizeTableName(TEST_TABLE_NAME));
+ } else {
+ assertEquals(TEST_TABLE_NAME.toLowerCase(), db.normalizeTableName(TEST_TABLE_NAME));
+ }
+ }
+
+ @Test
+ public void selectNewTable() {
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("nbr_integer").asInteger().primaryKey().table()
+ .addColumn("nbr_long").asLong().table()
+ .addColumn("nbr_float").asFloat().table()
+ .addColumn("nbr_double").asDouble().table()
+ .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table()
+ .addColumn("str_varchar").asString(80).table()
+ .addColumn("str_fixed").asStringFixed(1).table()
+ .addColumn("str_lob").asClob().table()
+ .addColumn("bin_blob").asBlob().table()
+ .addColumn("date_millis").asDate().table()
+ .addColumn("local_date").asLocalDate().table().schema().execute(db);
+
+ BigDecimal bigDecimal = new BigDecimal("5.3");
+ db.toInsert("insert into dbtest values (?,?,?,?,?,?,?,?,?,?,?)").argInteger(1).argLong(2L).argFloat(3.2f).argDouble(4.2)
+ .argBigDecimal(bigDecimal).argString("Hello").argString("T").argClobString("World")
+ .argBlobBytes("More".getBytes()).argDate(now).argLocalDate(localDateNow).insert(1);
+
+ db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, "
+ + "bin_blob, date_millis, local_date from dbtest")
+ .query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals(Integer.valueOf(1), rs.getIntegerOrNull(1));
+ assertEquals(Integer.valueOf(1), rs.getIntegerOrNull("nbr_integer"));
+ assertEquals(1, rs.getIntegerOrZero(1));
+ assertEquals(1, rs.getIntegerOrZero("nbr_integer"));
+ assertEquals(Long.valueOf(2), rs.getLongOrNull(2));
+ assertEquals(Long.valueOf(2), rs.getLongOrNull("nbr_long"));
+ assertEquals(2, rs.getLongOrZero(2));
+ assertEquals(2, rs.getLongOrZero("nbr_long"));
+ assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull(3));
+ assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull("nbr_float"));
+ assertEquals(3.2, rs.getFloatOrZero(3), 0.01);
+ assertEquals(3.2, rs.getFloatOrZero("nbr_float"), 0.01);
+ assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull(4));
+ assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull("nbr_double"));
+ assertEquals(4.2, rs.getDoubleOrZero(4), 0.01);
+ assertEquals(4.2, rs.getDoubleOrZero("nbr_double"), 0.01);
+ assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull(5));
+ assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull("nbr_big_decimal"));
+ assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrZero(5));
+ assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrZero("nbr_big_decimal"));
+ assertEquals("Hello", rs.getStringOrNull(6));
+ assertEquals("Hello", rs.getStringOrNull("str_varchar"));
+ assertEquals("Hello", rs.getStringOrEmpty(6));
+ assertEquals("Hello", rs.getStringOrEmpty("str_varchar"));
+ assertEquals("T", rs.getStringOrNull(7));
+ assertEquals("T", rs.getStringOrNull("str_fixed"));
+ assertEquals("T", rs.getStringOrEmpty(7));
+ assertEquals("T", rs.getStringOrEmpty("str_fixed"));
+ assertEquals("World", rs.getClobStringOrNull(8));
+ assertEquals("World", rs.getClobStringOrNull("str_lob"));
+ assertEquals("World", rs.getClobStringOrEmpty(8));
+ assertEquals("World", rs.getClobStringOrEmpty("str_lob"));
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull(9));
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull("bin_blob"));
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrZeroLen(9));
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrZeroLen("bin_blob"));
+ assertEquals(now, rs.getDateOrNull(10));
+ assertEquals(now, rs.getDateOrNull("date_millis"));
+ assertEquals(localDateNow, rs.getLocalDateOrNull(11));
+ assertEquals(localDateNow, rs.getLocalDateOrNull("local_date"));
+ return null;
+ });
+ // Repeat the above query, using the various methods that automatically infer the column
+ db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, "
+ + "bin_blob, date_millis, local_date from dbtest")
+ .query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals(Integer.valueOf(1), rs.getIntegerOrNull());
+ assertEquals(Long.valueOf(2), rs.getLongOrNull());
+ assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull());
+ assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull());
+ assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull());
+ assertEquals("Hello", rs.getStringOrNull());
+ assertEquals("T", rs.getStringOrNull());
+ assertEquals("World", rs.getClobStringOrNull());
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull());
+ assertEquals(now, rs.getDateOrNull());
+ assertEquals(localDateNow, rs.getLocalDateOrNull());
+ return null;
+ });
+ db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, "
+ + "bin_blob, date_millis, local_date from dbtest")
+ .query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals(1, rs.getIntegerOrZero());
+ assertEquals(2, rs.getLongOrZero());
+ assertEquals(3.2, rs.getFloatOrZero(), 0.01);
+ assertEquals(4.2, rs.getDoubleOrZero(), 0.01);
+ assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrZero());
+ assertEquals("Hello", rs.getStringOrEmpty());
+ assertEquals("T", rs.getStringOrEmpty());
+ assertEquals("World", rs.getClobStringOrEmpty());
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrZeroLen());
+ return null;
+ });
+ db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals("World", readerToString(rs.getClobReaderOrNull(1)));
+ assertArrayEquals("More".getBytes(), inputStreamToString(rs.getBlobInputStreamOrNull(2)));
+ return null;
+ });
+ db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals("World", readerToString(rs.getClobReaderOrEmpty(1)));
+ assertArrayEquals("More".getBytes(), inputStreamToString(rs.getBlobInputStreamOrEmpty(2)));
+ return null;
+ });
+ db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals("World", readerToString(rs.getClobReaderOrNull()));
+ assertArrayEquals("More".getBytes(), inputStreamToString(rs.getBlobInputStreamOrNull()));
+ return null;
+ });
+ db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals("World", readerToString(rs.getClobReaderOrEmpty()));
+ assertArrayEquals("More".getBytes(), inputStreamToString(rs.getBlobInputStreamOrEmpty()));
+ return null;
+ });
+ db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals("World", readerToString(rs.getClobReaderOrNull("str_lob")));
+ assertArrayEquals("More".getBytes(), inputStreamToString(rs.getBlobInputStreamOrNull("bin_blob")));
+ return null;
+ });
+ db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals("World", readerToString(rs.getClobReaderOrEmpty("str_lob")));
+ assertArrayEquals("More".getBytes(), inputStreamToString(rs.getBlobInputStreamOrEmpty("bin_blob")));
+ return null;
+ });
+ assertEquals(Long.valueOf(1), db.toSelect("select count(*) from dbtest where nbr_integer=:i and nbr_long=:l and "
+ + "abs(nbr_float-:f)<0.01 and abs(nbr_double-:d)<0.01 and nbr_big_decimal=:bd and str_varchar=:s "
+ + "and str_fixed=:sf and date_millis=:date and local_date=:local_date")
+ .argInteger("i", 1)
+ .argLong("l", 2L)
+ .argFloat("f", 3.2f)
+ .argDouble("d", 4.2)
+ .argBigDecimal("bd", bigDecimal)
+ .argString("s", "Hello")
+ .argString("sf", "T")
+ .argDate("date", now)
+ .argLocalDate("local_date", localDateNow)
+ .queryLongOrNull());
+ List result = db.toSelect("select count(*) from dbtest where nbr_integer=:i and nbr_long=:l and "
+ + "abs(nbr_float-:f)<0.01 and abs(nbr_double-:d)<0.01 and nbr_big_decimal=:bd and str_varchar=:s "
+ + "and str_fixed=:sf and date_millis=:date and local_date=:local_date").argInteger("i", 1).argLong("l", 2L).argFloat("f", 3.2f)
+ .argDouble("d", 4.2).argBigDecimal("bd", bigDecimal).argString("s", "Hello").argString("sf", "T")
+ .argDate("date", now).argLocalDate("local_date", localDateNow).queryLongs();
+ assertEquals(1, result.size());
+ assertEquals(Long.valueOf(1), result.get(0));
+ }
+
+ @Test
+ public void updatePositionalArgs() {
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("nbr_integer").asInteger().table()
+ .addColumn("nbr_long").asLong().table()
+ .addColumn("nbr_float").asFloat().table()
+ .addColumn("nbr_double").asDouble().table()
+ .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table()
+ .addColumn("str_varchar").asString(80).table()
+ .addColumn("str_fixed").asStringFixed(1).table()
+ .addColumn("str_lob").asClob().table()
+ .addColumn("bin_blob").asBlob().table()
+ .addColumn("date_millis").asDate().table()
+ .addColumn("local_date").asLocalDate().table().schema().execute(db);
+
+ BigDecimal bigDecimal = new BigDecimal("5.3");
+ assertEquals(1, db.toInsert("insert into dbtest values (?,?,?,?,?,?,?,?,?,?,?,?)")
+ .argLong(1L)
+ .argInteger(1)
+ .argLong(2L)
+ .argFloat(3.2f)
+ .argDouble(4.2)
+ .argBigDecimal(bigDecimal)
+ .argString("Hello")
+ .argString("T")
+ .argClobString("World")
+ .argBlobBytes("More".getBytes())
+ .argDate(now)
+ .argLocalDate(localDateNow).insert());
+ db.toUpdate("update dbtest set nbr_integer=?, nbr_long=?, nbr_float=?, nbr_double=?, nbr_big_decimal=?, "
+ + "str_varchar=?, str_fixed=?, str_lob=?, bin_blob=?, date_millis=?, local_date=?").argInteger(null).argLong(null)
+ .argFloat(null).argDouble(null).argBigDecimal(null).argString(null).argString(null).argClobString(null)
+ .argBlobBytes(null).argDate(null).argLocalDate(null).update(1);
+ db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, "
+ + "bin_blob, date_millis, local_date from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertNull(rs.getIntegerOrNull(1));
+ assertNull(rs.getIntegerOrNull("nbr_integer"));
+ assertNull(rs.getLongOrNull(2));
+ assertNull(rs.getLongOrNull("nbr_long"));
+ assertNull(rs.getFloatOrNull(3));
+ assertNull(rs.getFloatOrNull("nbr_float"));
+ assertNull(rs.getDoubleOrNull(4));
+ assertNull(rs.getDoubleOrNull("nbr_double"));
+ assertNull(rs.getBigDecimalOrNull(5));
+ assertNull(rs.getBigDecimalOrNull("nbr_big_decimal"));
+ assertNull(rs.getStringOrNull(6));
+ assertNull(rs.getStringOrNull("str_varchar"));
+ assertNull(rs.getStringOrNull(7));
+ assertNull(rs.getStringOrNull("str_fixed"));
+ assertNull(rs.getClobStringOrNull(8));
+ assertNull(rs.getClobStringOrNull("str_lob"));
+ assertNull(rs.getBlobBytesOrNull(9));
+ assertNull(rs.getBlobBytesOrNull("bin_blob"));
+ assertNull(rs.getDateOrNull(10));
+ assertNull(rs.getDateOrNull("date_millis"));
+ assertNull(rs.getLocalDateOrNull(11));
+ assertNull(rs.getLocalDateOrNull("local_date"));
+ return null;
+ });
+ assertEquals(1, db.toUpdate("update dbtest set nbr_integer=?, nbr_long=?, nbr_float=?, nbr_double=?, "
+ + "nbr_big_decimal=?, str_varchar=?, str_fixed=?, str_lob=?, bin_blob=?, date_millis=?, local_date=?").argInteger(1)
+ .argLong(2L).argFloat(3.2f).argDouble(4.2).argBigDecimal(bigDecimal).argString("Hello").argString("T")
+ .argClobString("World").argBlobBytes("More".getBytes()).argDate(now).argLocalDate(localDateNow).update());
+ db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, "
+ + "bin_blob, date_millis, local_date from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals(Integer.valueOf(1), rs.getIntegerOrNull(1));
+ assertEquals(Integer.valueOf(1), rs.getIntegerOrNull("nbr_integer"));
+ assertEquals(Long.valueOf(2), rs.getLongOrNull(2));
+ assertEquals(Long.valueOf(2), rs.getLongOrNull("nbr_long"));
+ assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull(3));
+ assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull("nbr_float"));
+ assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull(4));
+ assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull("nbr_double"));
+ assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull(5));
+ assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull("nbr_big_decimal"));
+ assertEquals("Hello", rs.getStringOrNull(6));
+ assertEquals("Hello", rs.getStringOrNull("str_varchar"));
+ assertEquals("T", rs.getStringOrNull(7));
+ assertEquals("T", rs.getStringOrNull("str_fixed"));
+ assertEquals("World", rs.getClobStringOrNull(8));
+ assertEquals("World", rs.getClobStringOrNull("str_lob"));
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull(9));
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull("bin_blob"));
+ assertEquals(now, rs.getDateOrNull(10));
+ assertEquals(now, rs.getDateOrNull("date_millis"));
+ assertEquals(localDateNow, rs.getLocalDateOrNull(11));
+ assertEquals(localDateNow, rs.getLocalDateOrNull("local_date"));
+ return null;
+ });
+ db.toUpdate("update dbtest set str_lob=?, bin_blob=?").argClobReader(null).argBlobStream(null).update(1);
+ db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertNull(rs.getClobStringOrNull(1));
+ assertNull(rs.getClobStringOrNull("str_lob"));
+ assertNull(rs.getBlobBytesOrNull(2));
+ assertNull(rs.getBlobBytesOrNull("bin_blob"));
+ return null;
+ });
+ db.toUpdate("update dbtest set str_lob=?, bin_blob=?").argClobReader(new StringReader("World"))
+ .argBlobStream(new ByteArrayInputStream("More".getBytes())).update(1);
+ db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals("World", rs.getClobStringOrNull(1));
+ assertEquals("World", rs.getClobStringOrNull("str_lob"));
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull(2));
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull("bin_blob"));
+ return null;
+ });
+ }
+
+ @Test
+ public void updateNamedArgs() {
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("nbr_integer").asInteger().table()
+ .addColumn("nbr_long").asLong().table()
+ .addColumn("nbr_float").asFloat().table()
+ .addColumn("nbr_double").asDouble().table()
+ .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table()
+ .addColumn("str_varchar").asString(80).table()
+ .addColumn("str_fixed").asStringFixed(1).table()
+ .addColumn("str_lob").asClob().table()
+ .addColumn("bin_blob").asBlob().table()
+ .addColumn("date_millis").asDate().table()
+ .addColumn("local_date").asLocalDate().table().schema().execute(db);
+ BigDecimal bigDecimal = new BigDecimal("5.3");
+ db.toInsert("insert into dbtest values (:pk,:a,:b,:c,:d,:e,:f,:sf,:g,:h,:i,:j)").argLong(":pk", 1L).argInteger(":a", 1)
+ .argLong(":b", 2L).argFloat(":c", 3.2f).argDouble(":d", 4.2).argBigDecimal(":e", bigDecimal)
+ .argString(":f", "Hello").argString(":sf", "T")
+ .argClobString(":g", "World").argBlobBytes(":h", "More".getBytes())
+ .argDate(":i", now).argLocalDate(":j", localDateNow).insert(1);
+ db.toUpdate("update dbtest set nbr_integer=:a, nbr_long=:b, nbr_float=:c, nbr_double=:d, nbr_big_decimal=:e, "
+ + "str_varchar=:f, str_fixed=:sf, str_lob=:g, bin_blob=:h, date_millis=:i, local_date=:j").argInteger(":a", null)
+ .argLong(":b", null).argFloat(":c", null).argDouble(":d", null).argBigDecimal(":e", null)
+ .argString(":f", null).argString(":sf", null)
+ .argClobString(":g", null).argBlobBytes(":h", null)
+ .argDate(":i", null).argLocalDate(":j", null).update(1);
+ db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, "
+ + "bin_blob, date_millis, local_date from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertNull(rs.getIntegerOrNull(1));
+ assertNull(rs.getIntegerOrNull("nbr_integer"));
+ assertNull(rs.getLongOrNull(2));
+ assertNull(rs.getLongOrNull("nbr_long"));
+ assertNull(rs.getFloatOrNull(3));
+ assertNull(rs.getFloatOrNull("nbr_float"));
+ assertNull(rs.getDoubleOrNull(4));
+ assertNull(rs.getDoubleOrNull("nbr_double"));
+ assertNull(rs.getBigDecimalOrNull(5));
+ assertNull(rs.getBigDecimalOrNull("nbr_big_decimal"));
+ assertNull(rs.getStringOrNull(6));
+ assertNull(rs.getStringOrNull("str_varchar"));
+ assertNull(rs.getStringOrNull(7));
+ assertNull(rs.getStringOrNull("str_fixed"));
+ assertNull(rs.getClobStringOrNull(8));
+ assertNull(rs.getClobStringOrNull("str_lob"));
+ assertNull(rs.getBlobBytesOrNull(9));
+ assertNull(rs.getBlobBytesOrNull("bin_blob"));
+ assertNull(rs.getDateOrNull(10));
+ assertNull(rs.getDateOrNull("date_millis"));
+ assertNull(rs.getLocalDateOrNull(11));
+ assertNull(rs.getLocalDateOrNull("local_date"));
+ return null;
+ });
+ db.toUpdate("update dbtest set nbr_integer=:a, nbr_long=:b, nbr_float=:c, nbr_double=:d, nbr_big_decimal=:e, "
+ + "str_varchar=:f, str_fixed=:sf, str_lob=:g, bin_blob=:h, date_millis=:i, local_date=:j").argInteger(":a", 1)
+ .argLong(":b", 2L).argFloat(":c", 3.2f).argDouble(":d", 4.2).argBigDecimal(":e", bigDecimal)
+ .argString(":f", "Hello").argString(":sf", "T")
+ .argClobString(":g", "World").argBlobBytes(":h", "More".getBytes())
+ .argDate(":i", now).argLocalDate(":j", localDateNow).update(1);
+ db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, "
+ + "bin_blob, date_millis, local_date from dbtest")
+ .query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals(Integer.valueOf(1), rs.getIntegerOrNull(1));
+ assertEquals(Integer.valueOf(1), rs.getIntegerOrNull("nbr_integer"));
+ assertEquals(Long.valueOf(2), rs.getLongOrNull(2));
+ assertEquals(Long.valueOf(2), rs.getLongOrNull("nbr_long"));
+ assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull(3));
+ assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull("nbr_float"));
+ assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull(4));
+ assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull("nbr_double"));
+ assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull(5));
+ assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull("nbr_big_decimal"));
+ assertEquals("Hello", rs.getStringOrNull(6));
+ assertEquals("Hello", rs.getStringOrNull("str_varchar"));
+ assertEquals("T", rs.getStringOrNull(7));
+ assertEquals("T", rs.getStringOrNull("str_fixed"));
+ assertEquals("World", rs.getClobStringOrNull(8));
+ assertEquals("World", rs.getClobStringOrNull("str_lob"));
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull(9));
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull("bin_blob"));
+ assertEquals(now, rs.getDateOrNull(10));
+ assertEquals(now, rs.getDateOrNull("date_millis"));
+ assertEquals(localDateNow, rs.getLocalDateOrNull(11));
+ assertEquals(localDateNow, rs.getLocalDateOrNull("local_date"));
+ return null;
+ });
+
+ db.toUpdate("update dbtest set str_lob=:a, bin_blob=:b").argClobReader(":a", null).argBlobStream(":b", null).update(1);
+ db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertNull(rs.getClobStringOrNull(1));
+ assertNull(rs.getClobStringOrNull("str_lob"));
+ assertNull(rs.getBlobBytesOrNull(2));
+ assertNull(rs.getBlobBytesOrNull("bin_blob"));
+ return null;
+ });
+ db.toUpdate("update dbtest set str_lob=:a, bin_blob=:b").argClobReader(":a", new StringReader("World"))
+ .argBlobStream(":b", new ByteArrayInputStream("More".getBytes())).update(1);
+ db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals("World", rs.getClobStringOrNull(1));
+ assertEquals("World", rs.getClobStringOrNull("str_lob"));
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull(2));
+ assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull("bin_blob"));
+ return null;
+ });
+ }
+
+ @Test
+ public void nullValues() {
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("nbr_integer").asInteger().table()
+ .addColumn("nbr_long").asLong().table()
+ .addColumn("nbr_float").asFloat().table()
+ .addColumn("nbr_double").asDouble().table()
+ .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table()
+ .addColumn("str_varchar").asString(80).table()
+ .addColumn("str_fixed").asStringFixed(1).table()
+ .addColumn("str_lob").asClob().table()
+ .addColumn("bin_blob").asBlob().table()
+ .addColumn("date_millis").asDate().table()
+ .addColumn("local_date").asLocalDate().table().schema().execute(db);
+ db.toInsert("insert into dbtest values (?,?,?,?,?,?,?,?,?,?,?,?)").argLong(1L).argInteger(null).argLong(null)
+ .argFloat(null).argDouble(null).argBigDecimal(null).argString(null).argString(null).argClobString(null)
+ .argBlobBytes(null).argDate(null).argLocalDate(null).insert(1);
+ db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, "
+ + "bin_blob, date_millis, local_date from dbtest")
+ .query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertNull(rs.getIntegerOrNull(1));
+ assertNull(rs.getIntegerOrNull("nbr_integer"));
+ assertNull(rs.getLongOrNull(2));
+ assertNull(rs.getLongOrNull("nbr_long"));
+ assertNull(rs.getFloatOrNull(3));
+ assertNull(rs.getFloatOrNull("nbr_float"));
+ assertNull(rs.getDoubleOrNull(4));
+ assertNull(rs.getDoubleOrNull("nbr_double"));
+ assertNull(rs.getBigDecimalOrNull(5));
+ assertNull(rs.getBigDecimalOrNull("nbr_big_decimal"));
+ assertNull(rs.getStringOrNull(6));
+ assertNull(rs.getStringOrNull("str_varchar"));
+ assertNull(rs.getStringOrNull(7));
+ assertNull(rs.getStringOrNull("str_fixed"));
+ assertNull(rs.getClobStringOrNull(8));
+ assertNull(rs.getClobStringOrNull("str_lob"));
+ assertNull(rs.getBlobBytesOrNull(9));
+ assertNull(rs.getBlobBytesOrNull("bin_blob"));
+ assertNull(rs.getDateOrNull(10));
+ assertNull(rs.getDateOrNull("date_millis"));
+ assertNull(rs.getLocalDateOrNull(11));
+ assertNull(rs.getLocalDateOrNull("local_date"));
+ return null;
+ });
+ db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertNull(rs.getClobReaderOrNull(1));
+ assertNull(rs.getBlobInputStreamOrNull(2));
+ return null;
+ });
+ db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertNull(rs.getClobReaderOrNull("str_lob"));
+ assertNull(rs.getBlobInputStreamOrNull("bin_blob"));
+ return null;
+ });
+ }
+
+ @Test
+ public void fromAny() {
+ assertEquals(db.toSelect("select 1" + db.flavor().fromAny()).queryIntegerOrZero(), 1);
+ }
+
+ @Test
+ public void metadataColumnNames() {
+ new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db);
+ db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\" from dbtest").query(rs -> {
+ assertArrayEquals(new String[]{"PK", "FOO", "Foo"}, rs.getColumnLabels());
+ return null;
+ });
+ }
+
+ @Test
+ public void metadataColumnTypes() {
+ String timestampColumnName = "data_millis";
+ String dateColumnName = "local_date";
+ new Schema()
+ .addTable("dbtest")
+ .addColumn(timestampColumnName).asDate().table()
+ .addColumn(dateColumnName).asLocalDate().table().schema().execute(db);
+ db.toSelect("select * from dbtest").query((RowsHandler) rs -> {
+ ResultSetMetaData metadata = rs.getMetadata();
+ for (int i = 1; i <= metadata.getColumnCount(); i++) {
+ String columnName = metadata.getColumnName(i);
+ String columnType = metadata.getColumnTypeName(i);
+ if (columnName.equalsIgnoreCase(timestampColumnName)) {
+ if ("sqlserver".equals(db.flavor().toString())) {
+ assertEquals("DATETIME2", columnType.toUpperCase());
+ } else {
+ assertEquals("TIMESTAMP", columnType.toUpperCase());
+ }
+ } else if (columnName.equalsIgnoreCase(dateColumnName)) {
+ assertEquals("DATE", columnType.toUpperCase());
+ } else {
+ fail("Unexpected column " + columnName + " of type " + columnType);
+ }
+ }
+ return null;
+ });
+ }
+
+ @Test
+ public void intervals() {
+ new Schema().addTable("dbtest").addColumn("d").asDate().schema().execute(db);
+ db.toInsert("insert into dbtest (d) values (?)").argDate(now).insert(1);
+ assertEquals(1, db.toSelect("select count(1) from dbtest where d - interval '1' hour * ? < ?")
+ .argInteger(2)
+ .argDate(now)
+ .queryIntegerOrZero());
+ }
+
+ @Test
+ public void saveResultAsTable() {
+ new Schema().addTable("dbtest")
+ .addColumn("nbr_integer").asInteger().primaryKey().table()
+ .addColumn("nbr_long").asLong().table()
+ .addColumn("nbr_float").asFloat().table()
+ .addColumn("nbr_double").asDouble().table()
+ .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table()
+ .addColumn("str_varchar").asString(80).table()
+ .addColumn("str_fixed").asStringFixed(1).table()
+ .addColumn("str_lob").asClob().table()
+ .addColumn("bin_blob").asBlob().table()
+ .addColumn("boolean_flag").asBoolean().table()
+ .addColumn("date_millis").asDate().table()
+ .addColumn("local_date").asLocalDate().schema().execute(db);
+ 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).argFloat(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).argFloat(0.000001f)
+ .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()
+ .addColumn("nbr_long").asLong().table()
+ .addColumn("nbr_float").asFloat().table()
+ .addColumn("nbr_double").asDouble().table()
+ .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table()
+ .addColumn("str_varchar").asString(80).table()
+ .addColumn("str_fixed").asStringFixed(1).table()
+ .addColumn("str_lob").asClob().table()
+ .addColumn("bin_blob").asBlob().table()
+ .addColumn("boolean_flag").asBoolean().table()
+ .addColumn("date_millis").asDate().table()
+ .addColumn("local_date").asLocalDate().schema().print(db.flavor());
+ List args = db.toSelect("select 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 from dbtest")
+ .query(rs -> {
+ List result = new ArrayList<>();
+ while (rs.next()) {
+ if (result.size() == 0) {
+ db.dropTableQuietly("dbtest2");
+ Schema schema = new Schema().addTableFromRow("dbtest2", rs).schema();
+ assertEquals(expectedSchema, schema.print(db.flavor()));
+ schema.execute(db);
+ }
+ result.add(SqlArgs.readRow(rs));
+ }
+ return result;
+ });
+
+ db.toInsert(Sql.insert("dbtest2", args)).insertBatch();
+
+ assertEquals(2, db.toSelect("select count(*) from dbtest2").queryIntegerOrZero());
+
+ assertEquals(
+ db.toSelect("select 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 from dbtest order by 1")
+ .queryMany(SqlArgs::readRow),
+ db.toSelect("select 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 from dbtest2 order by 1")
+ .queryMany(SqlArgs::readRow));
+
+ assertEquals(
+ Arrays.asList(
+ new SqlArgs().argInteger("nbr_integer", Integer.MIN_VALUE)
+ .argLong("nbr_long", Long.MIN_VALUE)
+ .argFloat("nbr_float", 0.000001f)
+ .argDouble("nbr_double", Double.MIN_VALUE)
+ .argBigDecimal("nbr_big_decimal", new BigDecimal("-123.456"))
+ .argString("str_varchar", "goodbye")
+ .argString("str_fixed", "A")
+ .argClobString("str_lob", "bye again")
+ .argBlobBytes("bin_blob", new byte[]{'3', '4'})
+ .argString("boolean_flag", "N")//.argBoolean("boolean_flag", false)
+ .argDate("date_millis", now)
+ .argLocalDate("local_date", localDateNow),
+ new SqlArgs().argInteger("nbr_integer", Integer.MAX_VALUE)
+ .argLong("nbr_long", Long.MAX_VALUE)
+ .argFloat("nbr_float", Float.MAX_VALUE)
+ .argDouble("nbr_double", Double.MAX_VALUE)
+ .argBigDecimal("nbr_big_decimal", new BigDecimal("123.456"))
+ .argString("str_varchar", "hello")
+ .argString("str_fixed", "Z")
+ .argClobString("str_lob", "hello again")
+ .argBlobBytes("bin_blob", new byte[]{'1', '2'})
+ .argString("boolean_flag", "Y")//.argBoolean("boolean_flag", true)
+ .argDate("date_millis", now)
+ .argLocalDate("local_date", localDateNow)),
+ db.toSelect("select 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 from dbtest2 order by 1")
+ .queryMany(SqlArgs::readRow));
+ }
+
+ @Test
+ public void readSqlArgs() {
+ new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db);
+ db.toInsert("insert into dbtest (pk) values (?)").argInteger(1).insert(1);
+ SqlArgs args = db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\", pk as \"g arB#G!\","
+ + " pk as \"TitleCase\" from dbtest")
+ .queryOneOrThrow(SqlArgs::readRow);
+
+ assertEquals(Arrays.asList("pk", "foo", "foo_2", "g_ar_b_g", "title_case"), args.names());
+ }
+
+ @Test
+ public void clockSync() {
+ db.assertTimeSynchronized();
+ }
+
+ @Test
+ public void booleanColumn() {
+ new Schema().addTable("dbtest")
+ .addColumn("t").asBoolean().table()
+ .addColumn("f").asBoolean().table()
+ .addColumn("n").asBoolean().schema().execute(db);
+ db.toInsert("insert into dbtest (t,f,n) values (?,:f,?)")
+ .argBoolean(true).argBoolean("f", false).argBoolean(null).insert(1);
+ db.toSelect("select t,f,n from dbtest")
+ .query(rs -> {
+ assertTrue(rs.next());
+ assertSame(rs.getBooleanOrNull(), Boolean.TRUE);
+ assertSame(rs.getBooleanOrNull(), Boolean.FALSE);
+ assertNull(rs.getBooleanOrNull());
+ return null;
+ });
+ // Verify use of getBooleanOrNull(int) followed by default getBooleanOrNull() tracks
+ // the current column index correctly (picks up where the explicit one left off)
+ db.toSelect("select t,f,n from dbtest")
+ .query(rs -> {
+ assertTrue(rs.next());
+ assertSame(rs.getBooleanOrNull(2), Boolean.FALSE);
+ assertNull(rs.getBooleanOrNull());
+ return null;
+ });
+ // Verify use of getBooleanOrNull(String) followed by default getBooleanOrNull() tracks
+ // the current column index correctly (picks up where the explicit one left off)
+ db.toSelect("select t,f,n from dbtest")
+ .query(rs -> {
+ assertTrue(rs.next());
+ assertSame(rs.getBooleanOrNull("f"), Boolean.FALSE);
+ assertNull(rs.getBooleanOrNull());
+ return null;
+ });
+ db.toSelect("select t,f,n from dbtest")
+ .query(rs -> {
+ assertTrue(rs.next());
+ assertTrue(rs.getBooleanOrFalse());
+ assertFalse(rs.getBooleanOrFalse());
+ assertFalse(rs.getBooleanOrFalse());
+ return null;
+ });
+ // Verify use of getBooleanOrFalse(int) followed by default getBooleanOrFalse() tracks
+ // the current column index correctly (picks up where the explicit one left off)
+ db.toSelect("select t,f,n from dbtest")
+ .query(rs -> {
+ assertTrue(rs.next());
+ assertFalse(rs.getBooleanOrFalse(2));
+ assertFalse(rs.getBooleanOrFalse());
+ return null;
+ });
+ // Verify use of getBooleanOrFalse(String) followed by default getBooleanOrFalse() tracks
+ // the current column index correctly (picks up where the explicit one left off)
+ db.toSelect("select t,f,n from dbtest")
+ .query(rs -> {
+ assertTrue(rs.next());
+ assertFalse(rs.getBooleanOrFalse("f"));
+ assertFalse(rs.getBooleanOrFalse());
+ return null;
+ });
+ db.toSelect("select t,f,n from dbtest")
+ .query(rs -> {
+ assertTrue(rs.next());
+ assertTrue(rs.getBooleanOrTrue());
+ assertFalse(rs.getBooleanOrTrue());
+ assertTrue(rs.getBooleanOrTrue());
+ return null;
+ });
+ // Verify use of getBooleanOrTrue(int) followed by default getBooleanOrTrue() tracks
+ // the current column index correctly (picks up where the explicit one left off)
+ db.toSelect("select t,f,n from dbtest")
+ .query(rs -> {
+ assertTrue(rs.next());
+ assertFalse(rs.getBooleanOrTrue(2));
+ assertTrue(rs.getBooleanOrTrue());
+ return null;
+ });
+ // Verify use of getBooleanOrTrue(String) followed by default getBooleanOrTrue() tracks
+ // the current column index correctly (picks up where the explicit one left off)
+ db.toSelect("select t,f,n from dbtest")
+ .query(rs -> {
+ assertTrue(rs.next());
+ assertFalse(rs.getBooleanOrTrue("f"));
+ assertTrue(rs.getBooleanOrTrue());
+ return null;
+ });
+ db.toDelete("delete from dbtest where t=? and f=?")
+ .argBoolean(true).argBoolean(false).update(1);
+
+ db.toInsert("insert into dbtest (t,f,n) values (:t,:f,:n)")
+ .argBoolean("t", true).argBoolean("f", false).argBoolean("n", null).insert(1);
+ db.toSelect("select t,f,n from dbtest")
+ .query(rs -> {
+ assertTrue(rs.next());
+ assertSame(rs.getBooleanOrNull(1), Boolean.TRUE);
+ assertSame(rs.getBooleanOrNull(2), Boolean.FALSE);
+ assertNull(rs.getBooleanOrNull(3));
+ assertEquals(rs.getBooleanOrFalse(1), Boolean.TRUE);
+ assertEquals(rs.getBooleanOrFalse(2), Boolean.FALSE);
+ assertEquals(rs.getBooleanOrFalse(3), Boolean.FALSE);
+ assertEquals(rs.getBooleanOrTrue(1), Boolean.TRUE);
+ assertEquals(rs.getBooleanOrTrue(2), Boolean.FALSE);
+ assertEquals(rs.getBooleanOrTrue(3), Boolean.TRUE);
+ assertSame(rs.getBooleanOrNull("t"), Boolean.TRUE);
+ assertSame(rs.getBooleanOrNull("f"), Boolean.FALSE);
+ assertNull(rs.getBooleanOrNull("n"));
+ assertEquals(rs.getBooleanOrFalse("t"), Boolean.TRUE);
+ assertEquals(rs.getBooleanOrFalse("f"), Boolean.FALSE);
+ assertEquals(rs.getBooleanOrFalse("n"), Boolean.FALSE);
+ assertEquals(rs.getBooleanOrTrue("t"), Boolean.TRUE);
+ assertEquals(rs.getBooleanOrTrue("f"), Boolean.FALSE);
+ assertEquals(rs.getBooleanOrTrue("n"), Boolean.TRUE);
+ return null;
+ });
+ assertSame(db.toSelect("select t from dbtest").queryBooleanOrNull(), Boolean.TRUE);
+ assertTrue(db.toSelect("select t from dbtest").queryBooleanOrFalse());
+ assertTrue(db.toSelect("select t from dbtest").queryBooleanOrTrue());
+ assertSame(db.toSelect("select f from dbtest").queryBooleanOrNull(), Boolean.FALSE);
+ assertFalse(db.toSelect("select f from dbtest").queryBooleanOrFalse());
+ assertFalse(db.toSelect("select f from dbtest").queryBooleanOrTrue());
+ assertNull(db.toSelect("select n from dbtest").queryBooleanOrNull());
+ assertFalse(db.toSelect("select n from dbtest").queryBooleanOrFalse());
+ assertTrue(db.toSelect("select n from dbtest").queryBooleanOrTrue());
+ }
+
+ @Test
+ public void batchInsert() {
+ new Schema().addTable("dbtest")
+ .addColumn("pk").primaryKey().schema().execute(db);
+
+ db.toInsert("insert into dbtest (pk) values (?)")
+ .argInteger(1).batch()
+ .argInteger(2).batch()
+ .argInteger(3).batch().insertBatch();
+
+ assertEquals(3, db.toSelect("select count(*) from dbtest").queryIntegerOrZero());
+ }
+
+ @Test
+ public void batchInsertPkLong() {
+ new Schema().addTable("dbtest")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("s").asString(10).schema().execute(db);
+
+ db.toInsert("insert into dbtest (pk,s) values (?,?)")
+ .argPkLong(1L).argString("hi").batch()
+ .argPkLong(2L).argString("hello").batch()
+ .argPkLong(3L).argString("howdy").batch().insertBatch();
+
+ assertEquals(3, db.toSelect("select count(*) from dbtest").queryIntegerOrZero());
+
+ try {
+ db.toInsert("insert into dbtest (pk,s) values (?,?)")
+ .argPkLong(1L).argString("hi").batch()
+ // argPkLong in different position ==> error
+ .argString("hello").argPkLong(2L).batch().insertBatch();
+ fail("Expecting an exception to be thrown");
+ } catch (DatabaseException e) {
+ assertEquals("The argPkLong() calls must be in the same position across batch records", e.getMessage());
+ }
+
+ try {
+ db.toInsert("insert into dbtest (pk,s) values (?,?)")
+ // multiple pk calls ==> error
+ .argPkLong(1L).argPkLong(1L).batch()
+ .argPkLong(2L).argString("hello").batch().insertBatch();
+ fail("Expecting an exception to be thrown");
+ } catch (DatabaseException e) {
+ assertEquals("Only call one argPk*() method", e.getMessage());
+ }
+ }
+
+ @Test
+ public void batchInsertPkLongNamed() {
+ new Schema().addTable("dbtest")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("s").asString(10).schema().execute(db);
+
+ db.toInsert("insert into dbtest (pk,s) values (:pk,?)")
+ .argPkLong("pk", 1L).argString("hi").batch()
+ .argPkLong("pk", 2L).argString("hello").batch()
+ .argPkLong("pk", 3L).argString("howdy").batch().insertBatch();
+
+ assertEquals(3, db.toSelect("select count(*) from dbtest").queryIntegerOrZero());
+
+ db.toInsert("insert into dbtest (pk,s) values (:pk,?)")
+ .batch().argPkLong("pk", 4L).argString("hi").batch()
+ .argString("hello").argPkLong("pk", 5L).insertBatch();
+
+ assertEquals(5, db.toSelect("select count(*) from dbtest").queryIntegerOrZero());
+
+ try {
+ db.toInsert("insert into dbtest (pk,s) values (:pk,?)")
+ // multiple pk calls ==> error
+ .argPkLong("pk", 1L).argPkLong(1L).batch().insertBatch();
+ fail("Expecting an exception to be thrown");
+ } catch (DatabaseException e) {
+ assertEquals("Only call one argPk*() method", e.getMessage());
+ }
+
+ try {
+ db.toInsert("insert into dbtest (pk,s) values (?,?)")
+ .argPkLong("pk", 1L).argString("howdy").batch()
+ // different name for pk on second batch ==> error
+ .argPkLong("na", 2L).argString("hello").batch().insertBatch();
+ fail("Expecting an exception to be thrown");
+ } catch (DatabaseException e) {
+ assertEquals("The primary key argument name must match across batch rows", e.getMessage());
+ }
+ }
+
+ @Test
+ public void batchInsertPkSeq() {
+ db.dropSequenceQuietly("seq");
+ new Schema().addTable("dbtest")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("s").asString(10).schema()
+ .addSequence("seq").schema().execute(db);
+
+ db.toInsert("insert into dbtest (pk,s) values (?,?)")
+ .argPkSeq("seq").argString("hi").batch()
+ .argPkSeq("seq").argString("hello").batch()
+ .argPkSeq("seq").argString("howdy").batch().insertBatch();
+
+ assertEquals(3, db.toSelect("select count(*) from dbtest").queryIntegerOrZero());
+
+ try {
+ db.toInsert("insert into dbtest (pk,s) values (?,?)")
+ .argPkSeq("seq").argString("hi").batch()
+ // argPkLong in different position ==> error
+ .argString("hello").argPkSeq("seq").batch().insertBatch();
+ fail("Expecting an exception to be thrown");
+ } catch (DatabaseException e) {
+ assertEquals("The argPkSeq() calls must be in the same position across batch records", e.getMessage());
+ }
+
+ try {
+ db.toInsert("insert into dbtest (pk,s) values (?,?)")
+ // multiple pk calls ==> error
+ .argPkSeq("seq").argPkSeq("seq").batch().insertBatch();
+ fail("Expecting an exception to be thrown");
+ } catch (DatabaseException e) {
+ assertEquals("Only call one argPk*() method", e.getMessage());
+ }
+ }
+
+ @Test
+ public void batchInsertPkSeqNamed() {
+ db.dropSequenceQuietly("seq");
+ new Schema().addTable("dbtest")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("s").asString(10).schema()
+ .addSequence("seq").schema().execute(db);
+
+ db.toInsert("insert into dbtest (pk,s) values (:pk,?)")
+ .argPkSeq("pk", "seq").argString("hi").batch()
+ .argPkSeq("pk", "seq").argString("hello").batch()
+ .argPkSeq("pk", "seq").argString("howdy").batch().insertBatch();
+
+ assertEquals(3, db.toSelect("select count(*) from dbtest").queryIntegerOrZero());
+
+ db.toInsert("insert into dbtest (pk,s) values (:pk,?)")
+ .batch().argPkSeq("pk", "seq").argString("hi").batch()
+ .argString("hello").argPkSeq("pk", "seq").insertBatch();
+
+ assertEquals(5, db.toSelect("select count(*) from dbtest").queryIntegerOrZero());
+
+ try {
+ db.toInsert("insert into dbtest (pk,s) values (:pk,?)")
+ // multiple pk calls ==> error
+ .argPkSeq("pk", "seq").argPkSeq("pk", "seq").batch().insertBatch();
+ fail("Expecting an exception to be thrown");
+ } catch (DatabaseException e) {
+ assertEquals("Only call one argPk*() method", e.getMessage());
+ }
+
+ try {
+ db.toInsert("insert into dbtest (pk,s) values (?,?)")
+ .argPkSeq("pk", "seq").argString("howdy").batch()
+ // different name for pk on second batch ==> error
+ .argPkSeq("na", "seq").argString("hello").batch().insertBatch();
+ fail("Expecting an exception to be thrown");
+ } catch (DatabaseException e) {
+ assertEquals("The primary key argument name must match across batch rows", e.getMessage());
+ }
+ }
+
+ @Test
+ public void bigClob() {
+ new Schema().addTable("dbtest").addColumn("str_lob").asClob().schema().execute(db);
+ final String longString = "0123456789".repeat(40000);
+ db.toInsert("insert into dbtest values (?)").argClobString(longString).insert(1);
+ db.toSelect("select str_lob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals(longString, rs.getClobStringOrNull(1));
+ assertEquals(longString, rs.getClobStringOrNull("str_lob"));
+ assertEquals(longString, readerToString(rs.getClobReaderOrNull(1)));
+ return null;
+ });
+ // Intentional slight variation here to test get()
+ db.get().toSelect("select str_lob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals(longString, readerToString(rs.getClobReaderOrNull("str_lob")));
+ return null;
+ });
+ db.toDelete("delete from dbtest").update(1);
+ db.toInsert("insert into dbtest values (?)").argClobReader(new StringReader(longString)).insert(1);
+ db.toSelect("select str_lob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals(longString, rs.getClobStringOrNull(1));
+ assertEquals(longString, rs.getClobStringOrNull("str_lob"));
+ assertEquals(longString, readerToString(rs.getClobReaderOrNull(1)));
+ return null;
+ });
+ db.toSelect("select str_lob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertEquals(longString, readerToString(rs.getClobReaderOrNull("str_lob")));
+ return null;
+ });
+ }
+
+ @Test
+ public void bigBlob() {
+ new Schema().addTable("dbtest").addColumn("bin_blob").asBlob().schema().execute(db);
+ final byte[] bigBytes = "0123456789".repeat(40000).getBytes();
+ db.toInsert("insert into dbtest values (?)").argBlobBytes(bigBytes).insert(1);
+ db.toSelect("select bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertArrayEquals(bigBytes, rs.getBlobBytesOrNull(1));
+ assertArrayEquals(bigBytes, rs.getBlobBytesOrNull("bin_blob"));
+ assertArrayEquals(bigBytes, inputStreamToString(rs.getBlobInputStreamOrNull(1)));
+ return null;
+ });
+ db.toSelect("select bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertArrayEquals(bigBytes, inputStreamToString(rs.getBlobInputStreamOrNull("bin_blob")));
+ return null;
+ });
+ db.toDelete("delete from dbtest").update(1);
+ db.toInsert("insert into dbtest values (?)").argBlobStream(new ByteArrayInputStream(bigBytes)).insert(1);
+ db.toSelect("select bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertArrayEquals(bigBytes, rs.getBlobBytesOrNull(1));
+ assertArrayEquals(bigBytes, rs.getBlobBytesOrNull("bin_blob"));
+ assertArrayEquals(bigBytes, inputStreamToString(rs.getBlobInputStreamOrNull(1)));
+ return null;
+ });
+ db.toSelect("select bin_blob from dbtest").query((RowsHandler) rs -> {
+ assertTrue(rs.next());
+ assertArrayEquals(bigBytes, inputStreamToString(rs.getBlobInputStreamOrNull("bin_blob")));
+ return null;
+ });
+ }
+
+ @Test
+ public void argLocalDateTimeZones() {
+ LocalDate januaryOne2000 = LocalDate.of(2000, Month.JANUARY, 1);
+ // Verify we always get the same LocalDate regardless of time zone and DB across all drivers
+ new Schema().addTable("dbtest").addColumn("i").asLocalDate().schema().execute(db);
+ db.toInsert("insert into dbtest (i) values (?)").argLocalDate(januaryOne2000).insert(1);
+ // Query without specifying a zone
+ assertEquals(januaryOne2000,
+ db.toSelect("select i from dbtest where i=?").argLocalDate(januaryOne2000).queryLocalDateOrNull());
+ TimeZone defaultTZ = TimeZone.getDefault();
+ try {
+ String[] availableTZs = TimeZone.getAvailableIDs();
+ for (String tz : availableTZs) {
+ TimeZone.setDefault(TimeZone.getTimeZone(tz));
+ LocalDate result =
+ db.toSelect("select i from dbtest where i=?").argLocalDate(januaryOne2000).queryLocalDateOrNull();
+ assertEquals(januaryOne2000, result);
+ }
+ } finally {
+ TimeZone.setDefault(defaultTZ);
+ }
+ }
+
+ @Test
+ public void argLocalDateLeapYear() {
+ new Schema().addTable("dbtest").addColumn("testdate").asLocalDate().schema().execute(db);
+
+ // Start by adding Febriary 28 and March 1 of 1900. This was not a leap year.
+ LocalDate feb1900 = LocalDate.of(1900, Month.FEBRUARY, 28);
+ db.toInsert("insert into dbtest (testdate) values (?)").argLocalDate(feb1900).insert(1);
+ assertEquals(feb1900,
+ db.toSelect("select testdate from dbtest where testdate=?").argLocalDate(feb1900).queryLocalDateOrNull());
+
+ LocalDate mar1900 = LocalDate.of(1900, Month.MARCH, 1);
+ db.toInsert("insert into dbtest (testdate) values (?)").argLocalDate(mar1900).insert(1);
+ assertEquals(mar1900,
+ db.toSelect("select testdate from dbtest where testdate=?").argLocalDate(mar1900).queryLocalDateOrNull());
+
+ // Now try Feb 28, 29, and March 1 of 2000. This was a leap year
+ LocalDate feb2000 = LocalDate.of(2000, Month.FEBRUARY, 28);
+ db.toInsert("insert into dbtest (testdate) values (?)").argLocalDate(feb2000).insert(1);
+ assertEquals(feb2000,
+ db.toSelect("select testdate from dbtest where testdate=?").argLocalDate(feb2000).queryLocalDateOrNull());
+
+ LocalDate febLeap2000 = LocalDate.of(2000, Month.FEBRUARY, 29);
+ db.toInsert("insert into dbtest (testdate) values (?)").argLocalDate(febLeap2000).insert(1);
+ assertEquals(febLeap2000,
+ db.toSelect("select testdate from dbtest where testdate=?").argLocalDate(febLeap2000).queryLocalDateOrNull());
+
+ LocalDate mar2000 = LocalDate.of(2000, Month.MARCH, 1);
+ db.toInsert("insert into dbtest (testdate) values (?)").argLocalDate(mar2000).insert(1);
+ assertEquals(mar2000,
+ db.toSelect("select testdate from dbtest where testdate=?").argLocalDate(mar2000).queryLocalDateOrNull());
+ }
+
+ @Test
+ public void argIntegerMinMax() {
+ new Schema().addTable("dbtest").addColumn("i").asInteger().schema().execute(db);
+
+ db.toInsert("insert into dbtest (i) values (?)").argInteger(Integer.MIN_VALUE).insert(1);
+ assertEquals(Integer.valueOf(Integer.MIN_VALUE),
+ db.toSelect("select i from dbtest where i=?").argInteger(Integer.MIN_VALUE).queryIntegerOrNull());
+
+ db.toInsert("insert into dbtest (i) values (?)").argInteger(Integer.MAX_VALUE).insert(1);
+ assertEquals(Integer.valueOf(Integer.MAX_VALUE),
+ db.toSelect("select i from dbtest where i=?").argInteger(Integer.MAX_VALUE).queryIntegerOrNull());
+ }
+
+ @Test
+ public void argLongMinMax() {
+ new Schema().addTable("dbtest").addColumn("i").asLong().schema().execute(db);
+
+ db.toInsert("insert into dbtest (i) values (?)").argLong(Long.MIN_VALUE).insert(1);
+ assertEquals(Long.valueOf(Long.MIN_VALUE),
+ db.toSelect("select i from dbtest where i=?").argLong(Long.MIN_VALUE).queryLongOrNull());
+
+ db.toInsert("insert into dbtest (i) values (?)").argLong(Long.MAX_VALUE).insert(1);
+ assertEquals(Long.valueOf(Long.MAX_VALUE),
+ db.toSelect("select i from dbtest where i=?").argLong(Long.MAX_VALUE).queryLongOrNull());
+ }
+
+ @Test
+ public void argFloatMinMax() {
+ new Schema().addTable("dbtest").addColumn("i").asFloat().schema().execute(db);
+
+ db.toInsert("insert into dbtest (i) values (?)").argFloat(Float.MIN_VALUE).insert(1);
+ assertEquals(Float.valueOf(Float.MIN_VALUE),
+ db.toSelect("select i from dbtest where i=?").argFloat(Float.MIN_VALUE).queryFloatOrNull());
+
+ db.toInsert("insert into dbtest (i) values (?)").argFloat(Float.MAX_VALUE).insert(1);
+ assertEquals(Float.valueOf(Float.MAX_VALUE),
+ db.toSelect("select i from dbtest where i=?").argFloat(Float.MAX_VALUE).queryFloatOrNull());
+ }
+
+ @Test
+ public void argFloatNaN() {
+ new Schema().addTable("dbtest").addColumn("i").asFloat().schema().execute(db);
+
+ db.toInsert("insert into dbtest (i) values (?)").argFloat(Float.NaN).insert(1);
+ assertEquals(Float.valueOf(Float.NaN),
+ db.toSelect("select i from dbtest where i=?").argFloat(Float.NaN).queryFloatOrNull());
+ }
+
+ @Test
+ public void argFloatInfinity() {
+ new Schema().addTable("dbtest").addColumn("i").asFloat().schema().execute(db);
+
+ db.toInsert("insert into dbtest (i) values (?)").argFloat(Float.NEGATIVE_INFINITY).insert(1);
+ assertEquals(Float.valueOf(Float.NEGATIVE_INFINITY),
+ db.toSelect("select i from dbtest where i=?").argFloat(Float.NEGATIVE_INFINITY).queryFloatOrNull());
+
+ db.toInsert("insert into dbtest (i) values (?)").argFloat(Float.POSITIVE_INFINITY).insert(1);
+ assertEquals(Float.valueOf(Float.POSITIVE_INFINITY),
+ db.toSelect("select i from dbtest where i=?").argFloat(Float.POSITIVE_INFINITY).queryFloatOrNull());
+ }
+
+ @Test
+ public void argFloatZero() {
+ new Schema().addTable("dbtest").addColumn("i").asFloat().schema().execute(db);
+
+ db.toInsert("insert into dbtest (i) values (?)").argFloat(0f).insert(1);
+ assertEquals(Float.valueOf(0f),
+ db.toSelect("select i from dbtest where i=?").argFloat(0f).queryFloatOrNull());
+ }
+
+ @Test
+ public void argFloatNegativeZero() {
+ new Schema().addTable("dbtest").addColumn("i").asFloat().schema().execute(db);
+
+ db.toInsert("insert into dbtest (i) values (?)").argFloat(-0f).insert(1);
+ assertEquals(Float.valueOf(-0f),
+ db.toSelect("select i from dbtest where i=?").argFloat(-0f).queryFloatOrNull());
+ }
+
+ @Test
+ public void argDoubleMinMax() {
+ new Schema().addTable("dbtest").addColumn("i").asDouble().schema().execute(db);
+
+ db.toInsert("insert into dbtest (i) values (?)").argDouble(Double.MIN_VALUE).insert(1);
+ assertEquals(Double.valueOf(Double.MIN_VALUE),
+ db.toSelect("select i from dbtest where i=?").argDouble(Double.MIN_VALUE).queryDoubleOrNull());
+
+ db.toInsert("insert into dbtest (i) values (?)").argDouble(Double.MAX_VALUE).insert(1);
+ assertEquals(Double.valueOf(Double.MAX_VALUE),
+ db.toSelect("select i from dbtest where i=?").argDouble(Double.MAX_VALUE).queryDoubleOrNull());
+ }
+
+ @Test
+ public void argDoubleNaN() {
+ new Schema().addTable("dbtest").addColumn("i").asDouble().schema().execute(db);
+
+ db.toInsert("insert into dbtest (i) values (?)").argDouble(Double.NaN).insert(1);
+ assertEquals(Double.valueOf(Double.NaN),
+ db.toSelect("select i from dbtest where i=?").argDouble(Double.NaN).queryDoubleOrNull());
+ }
+
+ @Test
+ public void argDoubleInfinity() {
+ new Schema().addTable("dbtest").addColumn("i").asDouble().schema().execute(db);
+
+ db.toInsert("insert into dbtest (i) values (?)").argDouble(Double.NEGATIVE_INFINITY).insert(1);
+ assertEquals(Double.valueOf(Double.NEGATIVE_INFINITY),
+ db.toSelect("select i from dbtest where i=?").argDouble(Double.NEGATIVE_INFINITY).queryDoubleOrNull());
+
+ db.toInsert("insert into dbtest (i) values (?)").argDouble(Double.POSITIVE_INFINITY).insert(1);
+ assertEquals(Double.valueOf(Double.POSITIVE_INFINITY),
+ db.toSelect("select i from dbtest where i=?").argDouble(Double.POSITIVE_INFINITY).queryDoubleOrNull());
+ }
+
+ @Test
+ public void argDoubleZero() {
+ new Schema().addTable("dbtest").addColumn("i").asDouble().schema().execute(db);
+
+ db.toInsert("insert into dbtest (i) values (?)").argDouble(0d).insert(1);
+ assertEquals(Double.valueOf(0d),
+ db.toSelect("select i from dbtest where i=?").argDouble(0d).queryDoubleOrNull());
+ }
+
+ @Test
+ public void argDoubleNegativeZero() {
+ new Schema().addTable("dbtest").addColumn("i").asDouble().schema().execute(db);
+
+ db.toInsert("insert into dbtest (i) values (?)").argDouble(-0d).insert(1);
+ assertEquals(Double.valueOf(-0d),
+ db.toSelect("select i from dbtest where i=?").argDouble(-0d).queryDoubleOrNull());
+ }
+
+ @Test
+ public void argBigDecimal38Precision0() {
+ new Schema().addTable("dbtest").addColumn("i").asBigDecimal(38, 0).schema().execute(db);
+
+ BigDecimal value = new BigDecimal("99999999999999999999999999999999999999"); // 38 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 argBigDecimal38Precision1() {
+ new Schema().addTable("dbtest").addColumn("i").asBigDecimal(38, 1).schema().execute(db);
+
+ BigDecimal value = new BigDecimal("9999999999999999999999999999999999999.9"); // 38 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 argBigDecimal38Precision37() {
+ new Schema().addTable("dbtest").addColumn("i").asBigDecimal(38, 37).schema().execute(db);
+
+ BigDecimal value = new BigDecimal("9.9999999999999999999999999999999999999"); // 38 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 argBigDecimal38Precision38() {
+ new Schema().addTable("dbtest").addColumn("i").asBigDecimal(38, 38).schema().execute(db);
+
+ BigDecimal value = new BigDecimal("0.99999999999999999999999999999999999999"); // 38 digits
+ db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1);
+ System.out.println(db.toSelect("select i from dbtest").queryBigDecimalOrNull());
+ assertEquals(value,
+ db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull());
+ }
+
+ @Test
+ public void dropTableQuietly() {
+ db.dropTableQuietly("dbtest");
+ new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db);
+ db.dropTableQuietly("dbtest");
+ // Verify the quietly part really kicks in, since the table might have existed above
+ db.dropTableQuietly("dbtest");
+ new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db);
+ }
+
+ @Test
+ public void dropSequenceQuietly() {
+ db.dropSequenceQuietly("dbtest_seq");
+ // Verify the quietly part really kicks in, since the sequence might have existed above
+ db.dropSequenceQuietly("dbtest_seq");
+ }
+
+ @Test
+ public void insertReturningPkSeq() {
+ db.dropSequenceQuietly("dbtest_seq");
+
+ db.ddl("create table dbtest (pk numeric)").execute();
+ db.ddl("create sequence dbtest_seq start with 1").execute();
+
+ assertEquals(Long.valueOf(1L), db.toInsert("insert into dbtest (pk) values (:seq)")
+ .argPkSeq(":seq", "dbtest_seq").insertReturningPkSeq("pk"));
+ assertEquals(Long.valueOf(2L), db.toInsert("insert into dbtest (pk) values (:seq)")
+ .argPkSeq(":seq", "dbtest_seq").insertReturningPkSeq("pk"));
+ }
+
+ @Test
+ public void insertReturningAppDate() {
+ db.dropSequenceQuietly("dbtest_seq");
+
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("d").asDate().table().schema()
+ .addSequence("dbtest_seq").schema()
+ .execute(db);
+
+ db.toInsert("insert into dbtest (pk, d) values (:seq, :d)")
+ .argPkSeq(":seq", "dbtest_seq")
+ .argDateNowPerApp(":d")
+ .insertReturning("dbtest", "pk", rs -> {
+ assertTrue(rs.next());
+ assertEquals(Long.valueOf(1L), rs.getLongOrNull(1));
+ assertThat(rs.getDateOrNull(2), equalTo(now));
+ assertFalse(rs.next());
+ return null;
+ }, "d");
+ assertEquals(Long.valueOf(1L), db.toSelect("select count(*) from dbtest where d=?").argDate(now).queryLongOrNull());
+ }
+
+ @Test
+ public void quickQueries() {
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("d").asDate().table()
+ .addColumn("d2").asDate().table()
+ .addColumn("d3").asLocalDate().table()
+ .addColumn("d4").asLocalDate().table()
+ .addColumn("s").asString(5).table()
+ .addColumn("s2").asString(5).table()
+ .addColumn("i").asInteger().table().schema()
+ .execute(db);
+
+ db.toInsert("insert into dbtest (pk, d, d3, s) values (?,?,?,?)")
+ .argLong(1L).argDateNowPerApp().argLocalDate(localDateNow).argString("foo").insert(1);
+
+ assertEquals(Long.valueOf(1L), db.toSelect("select pk from dbtest").queryLongOrNull());
+ assertNull(db.toSelect("select pk from dbtest where 1=0").queryLongOrNull());
+ assertNull(db.toSelect("select i from dbtest").queryLongOrNull());
+ assertEquals(1L, db.toSelect("select pk from dbtest").queryLongOrZero());
+ assertEquals(0L, db.toSelect("select pk from dbtest where 1=0").queryLongOrZero());
+ assertEquals(0L, db.toSelect("select i from dbtest").queryLongOrZero());
+ assertEquals(1L, (long) db.toSelect("select pk from dbtest").queryLongs().get(0));
+ assertTrue(db.toSelect("select pk from dbtest where 1=0").queryLongs().isEmpty());
+ assertTrue(db.toSelect("select i from dbtest").queryLongs().isEmpty());
+
+ assertEquals(Integer.valueOf(1), db.toSelect("select pk from dbtest").queryIntegerOrNull());
+ assertNull(db.toSelect("select pk from dbtest where 1=0").queryIntegerOrNull());
+ assertNull(db.toSelect("select i from dbtest").queryIntegerOrNull());
+ assertEquals(1, db.toSelect("select pk from dbtest").queryIntegerOrZero());
+ assertEquals(0, db.toSelect("select pk from dbtest where 1=0").queryIntegerOrZero());
+ assertEquals(0, db.toSelect("select i from dbtest").queryIntegerOrZero());
+ assertEquals(1L, (int) db.toSelect("select pk from dbtest").queryIntegers().get(0));
+ assertTrue(db.toSelect("select pk from dbtest where 1=0").queryIntegers().isEmpty());
+ assertTrue(db.toSelect("select i from dbtest").queryIntegers().isEmpty());
+
+ assertEquals("foo", db.toSelect("select s from dbtest").queryStringOrNull());
+ assertNull(db.toSelect("select s from dbtest where 1=0").queryStringOrNull());
+ assertNull(db.toSelect("select s2 from dbtest").queryStringOrNull());
+ assertEquals("foo", db.toSelect("select s from dbtest").queryStringOrEmpty());
+ assertEquals("", db.toSelect("select s from dbtest where 1=0").queryStringOrEmpty());
+ assertEquals("", db.toSelect("select s2 from dbtest").queryStringOrEmpty());
+ assertEquals("foo", db.toSelect("select s from dbtest").queryStrings().get(0));
+ assertTrue(db.toSelect("select s from dbtest where 1=0").queryStrings().isEmpty());
+ assertTrue(db.toSelect("select s2 from dbtest").queryStrings().isEmpty());
+
+ assertEquals(now, db.toSelect("select d from dbtest").queryDateOrNull());
+ assertNull(db.toSelect("select d from dbtest where 1=0").queryDateOrNull());
+ assertNull(db.toSelect("select d2 from dbtest").queryDateOrNull());
+ assertEquals(db.toSelect("select d from dbtest").queryDates().get(0), now);
+ assertTrue(db.toSelect("select d from dbtest where 1=0").queryDates().isEmpty());
+ assertTrue(db.toSelect("select d2 from dbtest").queryDates().isEmpty());
+
+ assertEquals(localDateNow, db.toSelect("select d3 from dbtest").queryLocalDateOrNull());
+ assertNull(db.toSelect("select d3 from dbtest where 1=0").queryLocalDateOrNull());
+ assertEquals(db.toSelect("select d3 from dbtest").queryLocalDates().get(0), localDateNow);
+ assertEquals(Long.valueOf(1L),
+ db.toSelect("select count(*) from dbtest where d3=?").argLocalDate(localDateNow).queryLongOrNull());
+
+ assertNull(db.toSelect("select d4 from dbtest").queryLocalDateOrNull());
+ assertNull(db.toSelect("select d4 from dbtest where 1=0").queryLocalDateOrNull());
+ assertTrue(db.toSelect("select d4 from dbtest").queryLocalDates().isEmpty());
+ }
+
+ @Test
+ public void rowHandlerQueries() {
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("pk").primaryKey().schema()
+ .execute(db);
+
+ db.toInsert("insert into dbtest (pk) values (?)").argLong(1L).insert(1);
+ db.toInsert("insert into dbtest (pk) values (?)").argLong(2L).insert(1);
+
+ RowHandler rowHandler = Row::getLongOrNull;
+
+ List many = db.toSelect("select pk from dbtest").queryMany(rowHandler);
+ assertEquals(2, many.size());
+
+ assertEquals(Long.valueOf(1), db.toSelect("select pk from dbtest where pk=1").queryOneOrNull(rowHandler));
+ assertNull(db.toSelect("select pk from dbtest where pk=9").queryOneOrNull(rowHandler));
+ try {
+ db.toSelect("select pk from dbtest").queryOneOrNull(rowHandler);
+ fail("Should have thrown an exception");
+ } catch (ConstraintViolationException e) {
+ assertEquals("Expected exactly one row to be returned but found multiple", e.getCause().getMessage());
+ }
+ try {
+ db.toSelect("select pk from dbtest where pk=9").queryOneOrThrow(rowHandler);
+ fail("Should have thrown an exception");
+ } catch (ConstraintViolationException e) {
+ assertEquals("Expected exactly one row to be returned but found none", e.getMessage());
+ }
+
+ assertEquals(Long.valueOf(1), db.toSelect("select pk from dbtest where pk=1").queryFirstOrNull(rowHandler));
+ assertEquals(Long.valueOf(1), db.toSelect("select pk from dbtest order by 1").queryFirstOrNull(rowHandler));
+ assertNull(db.toSelect("select pk from dbtest where pk=9").queryFirstOrNull(rowHandler));
+ try {
+ db.toSelect("select pk from dbtest where pk=9").queryFirstOrThrow(rowHandler);
+ fail("Should have thrown an exception");
+ } catch (ConstraintViolationException e) {
+ assertEquals("Expected one or more rows to be returned but found none", e.getMessage());
+ }
+ }
+
+ @Test
+ public void nextSequenceValue() {
+ db.dropSequenceQuietly("dbtest_seq");
+ new Schema()
+ .addSequence("dbtest_seq").schema()
+ .execute(db);
+
+ assertEquals(Long.valueOf(1L), db.nextSequenceValue("dbtest_seq"));
+ }
+
+ @Test
+ public void insertReturningDbDate() {
+ db.dropSequenceQuietly("dbtest_seq");
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("d").asDate().table().schema()
+ .addSequence("dbtest_seq").schema()
+ .execute(db);
+ Date dbNow = db.toInsert("insert into dbtest (pk, d) values (:seq, :d)")
+ .argPkSeq(":seq", "dbtest_seq")
+ .argDateNowPerDb(":d")
+ .insertReturning("dbtest", "pk", rs -> {
+ assertTrue(rs.next());
+ assertEquals(Long.valueOf(1L), rs.getLongOrNull(1));
+ Date dbDate = rs.getDateOrNull(2);
+ assertFalse(rs.next());
+ return dbDate;
+ }, "d");
+ assertEquals(Long.valueOf(1L), db.toSelect("select count(*) from dbtest where d=?").argDate(dbNow).queryLongOrNull());
+ }
+
+ @Test
+ public void daylightSavings() {
+ LocalDate lastStdDateSpring = LocalDate.of(2019, Month.MARCH, 9);
+ LocalDate firstDSTDateSpring = LocalDate.of(2019, Month.MARCH, 10);
+ // Verify that the original LocalDate matches the driver SQL LocalDate generated.
+ StatementAdaptor adaptor = new StatementAdaptor(new OptionsDefault(db.flavor()));
+ assertEquals(lastStdDateSpring.toString(), adaptor.nullLocalDate(lastStdDateSpring).toString());
+ assertEquals(firstDSTDateSpring.toString(), adaptor.nullLocalDate(firstDSTDateSpring).toString());
+ }
+
+ @Test
+ public void insertLocalDate() {
+ // Date without time
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("d").asLocalDate().table().schema()
+ .execute(db);
+ LocalDate dateOfBirth = LocalDate.of(1951, Month.AUGUST, 9);
+ db.toInsert("insert into dbtest (d) values (?)")
+ .argLocalDate(dateOfBirth)
+ .insert(1);
+ LocalDate testDate = db.toSelect("select d from dbtest").queryLocalDateOrNull();
+ assertEquals(dateOfBirth, testDate);
+ }
+
+ @Test
+ public void localDateRoundTrip() {
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("d1").asLocalDate().table()
+ .addColumn("d2").asLocalDate().table().schema()
+ .execute(db);
+ // Store current time as per the database
+ db.toInsert("insert into dbtest (d1) values (?)")
+ .argLocalDate(localDateNow)
+ .insert(1);
+ // Now pull it out, put it back in, and verify it matches in the database
+ LocalDate queryRsDate = db.toSelect("select d1 from dbtest").queryLocalDateOrNull();
+ db.toUpdate("update dbtest set d2=?")
+ .argLocalDate(queryRsDate)
+ .update(1);
+ assertEquals(Long.valueOf(1L), db.toSelect("select count(*) from dbtest where d1=d2").queryLongOrNull());
+ }
+
+ /**
+ * Make sure database times are inserted with at least millisecond precision.
+ * This test is non-deterministic since it is checking the timestamp provided
+ * by the database, so we use a retry to give it up to ten attempts.
+ */
+ @Test
+ public void dateMillis() {
+ for (int attempts = 1; attempts <= 10; attempts++) {
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("d").asDate().table().schema()
+ .execute(db);
+ db.toInsert("insert into dbtest (d) values (?)")
+ .argDateNowPerDb()
+ .insert(1);
+ Date dbNow = db.toSelect("select d from dbtest").queryDateOrNull();
+ if (dbNow != null && dbNow.getTime() % 10 != 0) {
+ return;
+ }
+ System.out.println("Zero in least significant digit (attempt " + attempts + ")");
+ db.dropTableQuietly(TEST_TABLE_NAME);
+ }
+ fail("Timestamp had zero in the least significant digit");
+ }
+
+ @Test
+ public void dateRoundTrip() {
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("d1").asDate().table()
+ .addColumn("d2").asDate().table().schema()
+ .execute(db);
+ // Store current time as per the database
+ db.toInsert("insert into dbtest (d1) values (?)")
+ .argDateNowPerDb()
+ .insert(1);
+ // Now pull it out, put it back in, and verify it matches in the database
+ Date dbNow = db.toSelect("select d1 from dbtest").queryDateOrNull();
+ db.toUpdate("update dbtest set d2=?")
+ .argDate(dbNow)
+ .update(1);
+
+ assertEquals(Long.valueOf(1L), db.toSelect("select count(*) from dbtest where d1=d2").queryLongOrNull());
+ }
+
+ @Test
+ public void dateRoundTripTimezones() {
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("d").asDate().table().schema()
+ .execute(db);
+ Date date = new Date(166656789L);
+ TimeZone defaultTZ = TimeZone.getDefault();
+ try {
+ TimeZone.setDefault(TimeZone.getTimeZone("GMT-4:00"));
+ db.toInsert("insert into dbtest (d) values (?)").argDate(date).insert(1);
+ assertEquals(date, db.toSelect("select d from dbtest").queryDateOrNull());
+ assertEquals("1970-01-02 18:17:36.789000-0400", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000Z").format(
+ db.toSelect("select d from dbtest").queryDateOrNull()));
+ db.toDelete("delete from dbtest where d=?").argDate(date).update(1);
+ TimeZone.setDefault(TimeZone.getTimeZone("GMT+4:00"));
+ db.toInsert("insert into dbtest (d) values (?)").argDate(date).insert(1);
+ assertEquals(date, db.toSelect("select d from dbtest").queryDateOrNull());
+ assertEquals("1970-01-03 02:17:36.789000+0400", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000Z").format(
+ db.toSelect("select d from dbtest").queryDateOrNull()));
+ db.toDelete("delete from dbtest where d=?").argDate(date).update(1);
+ } finally {
+ TimeZone.setDefault(defaultTZ);
+ }
+ }
+
+ /**
+ * Verify the appropriate database flavor can correctly convert a {@code Date}
+ * into a SQL function representing a conversion from string to timestamp. This
+ * function is used to write debug SQL to the log in a way that could be manually
+ * executed if desired.
+ */
+ @Test
+ public void stringDateFunctions() {
+ Date date = new Date(166656789L);
+ System.out.println("Date: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000Z").format(date));
+ TimeZone defaultTZ = TimeZone.getDefault();
+ try {
+ TimeZone.setDefault(TimeZone.getTimeZone("GMT-4:00"));
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("d").asDate().schema().execute(db);
+ db.toInsert("insert into dbtest (d) values ("
+ + db.flavor().dateAsSqlFunction(date, db.options().calendarForTimestamps()).replace(":", "::") + ")")
+ .insert(1);
+ assertEquals("1970-01-02 18:17:36.789000-0400", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000Z").format(
+ db.toSelect("select d from dbtest").queryDateOrNull()));
+ // Now do some client operations in a different time zone
+ TimeZone.setDefault(TimeZone.getTimeZone("GMT+4:00"));
+ // Verify regular arg maps date the same way even though our TimeZone is now different
+ db.toDelete("delete from dbtest where d=?").argDate(date).update(1);
+ db.toInsert("insert into dbtest (d) values ("
+ + db.flavor().dateAsSqlFunction(date, db.options().calendarForTimestamps()).replace(":", "::") + ")")
+ .insert(1);
+ assertEquals("1970-01-03 02:17:36.789000+0400", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000Z").format(
+ db.toSelect("select d from dbtest").queryDateOrNull()));
+ // Verify the function maps correctly for equals operations as well
+ db.toDelete("delete from dbtest where d=" + db.flavor().dateAsSqlFunction(date,
+ db.options().calendarForTimestamps()).replace(":", "::")).update(1);
+ } finally {
+ TimeZone.setDefault(defaultTZ);
+ }
+ }
+
+ @Test
+ public void mixPositionalAndNamedParameters() {
+ new Schema()
+ .addTable("dbtest")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("d").asDate().table()
+ .addColumn("a").asInteger().table().schema()
+ .execute(db);
+
+ db.toSelect("select pk as \"time:: now??\" from dbtest where a=? and d=:now")
+ .argInteger(1).argDateNowPerDb("now").query(rs -> {
+ assertFalse(rs.next());
+ return null;
+ });
+ }
+
+ public String readerToString(Reader reader) throws IOException {
+ char[] buffer = new char[1024];
+ StringBuilder out = new StringBuilder();
+ int byteCount;
+ while ((byteCount = reader.read(buffer, 0, buffer.length)) >= 0) {
+ out.append(buffer, 0, byteCount);
+ }
+ return out.toString();
+ }
+
+ public byte[] inputStreamToString(InputStream inputStream) throws IOException {
+ byte[] buffer = new byte[1024];
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ int byteCount;
+ while ((byteCount = inputStream.read(buffer, 0, buffer.length)) >= 0) {
+ out.write(buffer, 0, byteCount);
+ }
+ return out.toByteArray();
+ }
+}
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
new file mode 100644
index 0000000..f0f47d8
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/ConfigTest.java
@@ -0,0 +1,250 @@
+package org.xbib.jdbc.query.test;
+
+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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for the configuration classes.
+ */
+public class ConfigTest {
+
+ @Test
+ public void testSystemProperties() {
+ System.setProperty("foo", "bar");
+ Config config = ConfigFrom.firstOf().systemProperties().get();
+ assertEquals("bar", config.getString("foo"));
+ assertNull(config.getString("unknown"));
+ assertEquals("default", config.getString("unknown", "default"));
+ }
+
+ @Test
+ public void testProperties() {
+ Properties properties = new Properties();
+ properties.setProperty("foo", "bar");
+
+ Config config = ConfigFrom.firstOf().properties(properties).get();
+
+ assertEquals("bar", config.getString("foo"));
+ assertNull(config.getString("unknown"));
+ assertEquals("default", config.getString("unknown", "default"));
+ }
+
+ @Test
+ public void testPropertyFiles() throws Exception {
+ Properties properties = new Properties();
+
+ properties.setProperty("foo", "1");
+ String filename1 = "build/ConfigTest-properties-1.properties";
+ properties.store(new FileWriter(filename1), null);
+
+ properties.setProperty("foo", "2");
+ properties.setProperty("foo2", "-2");
+ String filename2 = "build/ConfigTest-properties-2.properties";
+ 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();
+
+ assertEquals(Integer.valueOf(1), config.getInteger("foo"));
+ assertEquals(Integer.valueOf(-2), config.getInteger("foo2"));
+ assertNull(config.getInteger("unknown"));
+ assertEquals(5, config.getInteger("unknown", 5));
+
+ // Now flip the order and verify precedence works
+ config = ConfigFrom.firstOf().propertyFile(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();
+
+ assertEquals(Integer.valueOf(1), config.getInteger("foo"));
+ assertEquals(Integer.valueOf(-2), config.getInteger("foo2"));
+ assertNull(config.getInteger("unknown"));
+ 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();
+ assertEquals(Integer.valueOf(2), config.getInteger("foo"));
+ assertEquals(Integer.valueOf(-2), config.getInteger("foo2"));
+ }
+
+ @Test
+ public void testNested() {
+ Config config = ConfigFrom.firstOf()
+ .config(ConfigFrom.firstOf().custom(key -> key.equals("foo") ? "a" : null))
+ .config(ConfigFrom.firstOf().custom(key -> key.equals("foo") ? "b" : null)).get();
+
+ assertEquals("a", config.getString("foo"));
+
+ // Re-mapping prefix in nested config
+ config = ConfigFrom.firstOf()
+ .config(ConfigFrom.firstOf().custom(key -> key.equals("a.foo") ? "a" : null).removePrefix("a."))
+ .config(ConfigFrom.firstOf().custom(key -> key.equals("foo") ? "b" : null)).get();
+
+ assertEquals("a", config.getString("foo"));
+
+ // Excluding nested config, should skip to next
+ config = ConfigFrom.firstOf()
+ .config(ConfigFrom.firstOf().custom(key -> key.equals("a.foo") ? "a" : null).removePrefix("a.").excludeRegex("fo{2}"))
+ .config(ConfigFrom.firstOf().custom(key -> key.equals("foo") ? "b" : null)).get();
+
+ assertEquals("b", config.getString("foo"));
+ assertNull(config.getString("foooo"));
+
+ config = ConfigFrom.firstOf()
+ .config(ConfigFrom.firstOf().custom(key -> key.equals("a.foo") ? "a" : null).excludePrefix("a.", "other."))
+ .config(ConfigFrom.firstOf().custom(key -> key.equals("foo") ? "b" : null).addPrefix("a.")).get();
+
+ assertEquals("b", config.getString("a.foo"));
+ assertNull(config.getString("foo"));
+
+ config = ConfigFrom.firstOf()
+ .config(ConfigFrom.firstOf().custom(key -> key.equals("a.foo") ? "a" : null).includePrefix("other."))
+ .config(ConfigFrom.firstOf().custom(key -> key.equals("foo") ? "b" : null).addPrefix("a.").includeRegex("a.*f.*")).get();
+
+ assertEquals("b", config.getString("a.foo"));
+ assertNull(config.getString("foo"));
+ assertNull(config.getString("other.foo"));
+ }
+
+ @Test
+ public void testStripPrefixConflict() {
+ Config config = ConfigFrom.firstOf().value("a.foo", "a").value("foo", "bar").removePrefix("a.").get();
+
+ assertEquals("bar", config.getString("foo"));
+ }
+
+ @Test
+ public void testException() {
+ Config config = ConfigFrom.firstOf().custom(key -> {
+ throw new SecurityException("Pretending security policy is in place");
+ }).get();
+
+ // We do this call twice, but you should see only one warning in the log
+ assertEquals("default", config.getString("foo", "default"));
+ assertEquals("default", config.getString("foo", "default"));
+ }
+
+ @Test
+ public void testTidyValues() {
+ Config config = ConfigFrom.firstOf().value("foo", " a ").get();
+
+ // Strip whitespace
+ assertEquals("a", config.getString("foo"));
+
+ config = ConfigFrom.firstOf().value("foo", " ").value("foo", "").value("foo", null).value("foo", "a").get();
+
+ // Skip over the garbage ones
+ assertEquals("a", config.getString("foo"));
+ }
+
+ @Test
+ public void testBoolean() {
+ // Case insensitive, allow either true/false or yes/no
+ Config config = ConfigFrom.firstOf().value("foo", "tRuE").get();
+
+ assertTrue(config.getBooleanOrFalse("foo"));
+ assertTrue(config.getBooleanOrTrue("foo"));
+ assertFalse(config.getBooleanOrFalse("unknown"));
+ assertTrue(config.getBooleanOrTrue("unknown"));
+
+ config = ConfigFrom.firstOf().value("foo", "yEs").get();
+
+ assertTrue(config.getBooleanOrFalse("foo"));
+ assertTrue(config.getBooleanOrTrue("foo"));
+ assertFalse(config.getBooleanOrFalse("unknown"));
+ assertTrue(config.getBooleanOrTrue("unknown"));
+
+ config = ConfigFrom.firstOf().value("foo", "fAlSe").get();
+
+ assertFalse(config.getBooleanOrFalse("foo"));
+ assertFalse(config.getBooleanOrTrue("foo"));
+ assertFalse(config.getBooleanOrFalse("unknown"));
+ assertTrue(config.getBooleanOrTrue("unknown"));
+
+ config = ConfigFrom.firstOf().value("foo", "nO").get();
+
+ assertFalse(config.getBooleanOrFalse("foo"));
+ assertFalse(config.getBooleanOrTrue("foo"));
+ assertFalse(config.getBooleanOrFalse("unknown"));
+ assertTrue(config.getBooleanOrTrue("unknown"));
+
+ config = ConfigFrom.firstOf().value("foo", "bad value").get();
+
+ assertFalse(config.getBooleanOrFalse("foo"));
+ assertTrue(config.getBooleanOrTrue("foo"));
+ assertFalse(config.getBooleanOrFalse("unknown"));
+ assertTrue(config.getBooleanOrTrue("unknown"));
+ }
+
+ @Test
+ public void testInteger() {
+ Config config = ConfigFrom.firstOf().value("good", "123").value("bad", "hi").get();
+ assertEquals(Integer.valueOf(123), config.getInteger("good"));
+ assertNull(config.getInteger("bad"));
+ assertNull(config.getInteger("missing"));
+ assertEquals(123, config.getInteger("good", 5));
+ assertEquals(5, config.getInteger("bad", 5));
+ assertEquals(5, config.getInteger("missing", 5));
+ }
+
+ @Test
+ public void testLong() {
+ Config config = ConfigFrom.firstOf().value("good", "123").value("bad", "hi").get();
+
+ assertEquals(Long.valueOf(123), config.getLong("good"));
+ assertNull(config.getLong("bad"));
+ assertNull(config.getLong("missing"));
+ assertEquals(123, config.getLong("good", 5));
+ assertEquals(5, config.getLong("bad", 5));
+ assertEquals(5, config.getLong("missing", 5));
+ }
+
+ @Test
+ public void testFloat() {
+ Config config = ConfigFrom.firstOf().value("good", "123.45").value("bad", "hi").get();
+
+ assertEquals(Float.valueOf(123.45f), config.getFloat("good"));
+ assertNull(config.getFloat("bad"));
+ assertNull(config.getFloat("missing"));
+ assertEquals(123.45, config.getFloat("good", 5.45f), 0.001);
+ assertEquals(5.45, config.getFloat("bad", 5.45f), 0.001);
+ assertEquals(5.45, config.getFloat("missing", 5.45f), 0.001);
+ }
+
+ @Test
+ public void testDouble() {
+ Config config = ConfigFrom.firstOf().value("good", "123.45").value("bad", "hi").get();
+
+ assertEquals(Double.valueOf(123.45), config.getDouble("good"));
+ assertNull(config.getDouble("bad"));
+ assertNull(config.getDouble("missing"));
+ assertEquals(123.45, config.getDouble("good", 5.45), 0.001);
+ assertEquals(5.45, config.getDouble("bad", 5.45), 0.001);
+ assertEquals(5.45, config.getDouble("missing", 5.45), 0.001);
+ }
+
+ @Test
+ public void testBigDecimal() {
+ Config config = ConfigFrom.firstOf().value("good", "123.45").value("bad", "hi").get();
+
+ assertEquals(new BigDecimal("123.45"), config.getBigDecimal("good"));
+ assertNull(config.getBigDecimal("bad"));
+ assertNull(config.getBigDecimal("missing"));
+ assertEquals(new BigDecimal("123.45"), config.getBigDecimal("good", new BigDecimal("5.45")));
+ assertEquals(new BigDecimal("5.45"), config.getBigDecimal("bad", new BigDecimal("5.45")));
+ assertEquals(new BigDecimal("5.45"), config.getBigDecimal("missing", new BigDecimal("5.45")));
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DatabaseMock.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DatabaseMock.java
new file mode 100644
index 0000000..ea73424
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DatabaseMock.java
@@ -0,0 +1,18 @@
+package org.xbib.jdbc.query.test;
+
+/**
+ * Convenience class to intercept calls to Connection and return stubbed results
+ * for testing purposes.
+ */
+public interface DatabaseMock {
+
+ RowStub query(String executeSql, String debugSql);
+
+ Integer insert(String executeSql, String debugSql);
+
+ Long insertReturningPk(String executeSql, String debugSql);
+
+ RowStub insertReturning(String executeSql, String debugSql);
+
+ Integer update(String executeSql, String debugSql);
+}
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
new file mode 100644
index 0000000..d16d542
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DerbyTest.java
@@ -0,0 +1,145 @@
+package org.xbib.jdbc.query.test;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.xbib.jdbc.query.DatabaseProvider;
+import org.xbib.jdbc.query.OptionsOverride;
+import org.xbib.jdbc.query.Schema;
+
+import java.math.BigDecimal;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/**
+ * Exercise Database functionality with a real database (Derby).
+ */
+public class DerbyTest extends CommonTest {
+
+ private static final Logger logger = Logger.getLogger(DerbyTest.class.getName());
+
+ static {
+ System.setProperty("derby.stream.error.file", System.getProperty("user.dir") + "/build/derby-errors.log");
+ }
+
+ @Override
+ protected DatabaseProvider createDatabaseProvider(OptionsOverride options) {
+ return DatabaseProvider.fromDriverManager("jdbc:derby:build/testdb;create=true")
+ .withSqlParameterLogging().withSqlInExceptionMessages().withOptions(options).create();
+ }
+
+ // TODO fix this test
+ @Disabled("Not sure why this fails on the build servers right now...")
+ @Test
+ public void clockSync() {
+ super.clockSync();
+ }
+
+ @Disabled("Derby prohibits NaN and Infinity (https://issues.apache.org/jira/browse/DERBY-3290)")
+ @Test
+ public void argFloatNaN() {
+ super.argFloatNaN();
+ }
+
+ @Disabled("Derby prohibits NaN and Infinity (https://issues.apache.org/jira/browse/DERBY-3290)")
+ @Test
+ public void argFloatInfinity() {
+ super.argFloatInfinity();
+ }
+
+ @Disabled("Derby prohibits NaN and Infinity (https://issues.apache.org/jira/browse/DERBY-3290)")
+ @Test
+ public void argDoubleNaN() {
+ super.argDoubleNaN();
+ }
+
+ @Disabled("Derby prohibits NaN and Infinity (https://issues.apache.org/jira/browse/DERBY-3290)")
+ @Test
+ public void argDoubleInfinity() {
+ super.argDoubleInfinity();
+ }
+
+ @Disabled("Current Derby behavior is to convert -0f to 0f")
+ @Test
+ public void argFloatNegativeZero() {
+ super.argFloatNegativeZero();
+ }
+
+ @Disabled("Current Derby behavior is to convert -0d to 0d")
+ @Test
+ public void argDoubleNegativeZero() {
+ super.argDoubleNegativeZero();
+ }
+
+ @Disabled("Derby does not support timestamp intervals")
+ @Test
+ public void intervals() {
+ 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);
+ System.out.println(db.toSelect("select i from dbtest").queryBigDecimalOrNull());
+ assertEquals(value,
+ db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull());
+ }
+
+ @Disabled("Derby limits out at precision 31")
+ @Test
+ public void argBigDecimal38Precision0() {
+ super.argBigDecimal38Precision0();
+ }
+
+ @Disabled("Derby limits out at precision 31")
+ @Test
+ public void argBigDecimal38Precision1() {
+ super.argBigDecimal38Precision1();
+ }
+
+ @Disabled("Derby limits out at precision 31")
+ @Test
+ public void argBigDecimal38Precision37() {
+ super.argBigDecimal38Precision37();
+ }
+
+ @Disabled("Derby limits out at precision 31")
+ @Test
+ public void argBigDecimal38Precision38() {
+ super.argBigDecimal38Precision38();
+ }
+}
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
new file mode 100644
index 0000000..a11a53e
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/HsqldbTest.java
@@ -0,0 +1,177 @@
+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;
+import org.xbib.jdbc.query.Config;
+import org.xbib.jdbc.query.ConfigFrom;
+import org.xbib.jdbc.query.DatabaseProvider;
+import org.xbib.jdbc.query.OptionsOverride;
+import org.xbib.jdbc.query.Schema;
+import org.xbib.jdbc.query.Sql;
+import org.xbib.jdbc.query.SqlArgs;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Exercise database functionality with a real HyperSQL database.
+ */
+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");
+ Config config = ConfigFrom.firstOf()
+ .systemProperties()
+ .propertyFile(propertiesFile)
+ .excludePrefix("database.")
+ .removePrefix("hsqldb.").get();
+ logger.log(Level.INFO, "config = " + config);
+ return DatabaseProvider.fromDriverManager(config)
+ .withSqlParameterLogging()
+ .withSqlInExceptionMessages()
+ .withOptions(options).create();
+ }
+
+ @Test
+ public void noDatabaseAccess() throws Exception {
+ DatabaseProvider provider = createDatabaseProvider(new OptionsOverride());
+ provider.transact(dbp -> {
+ // Do nothing, just making sure no exception is thrown
+ });
+ provider.transact((dbp, tx) -> {
+ // Do nothing, just making sure no exception is thrown
+ });
+ provider.transact((dbp, tx) -> {
+ tx.setRollbackOnError(true);
+ // Do nothing, just making sure no exception is thrown
+ });
+ provider.transact((dbp, tx) -> {
+ tx.setRollbackOnly(true);
+ // Do nothing, just making sure no exception is thrown
+ });
+ }
+
+ @Disabled("LocalDate implementations should be TimeZone agnostic, but HSQLDB implementation has a bug.")
+ @Test
+ public void argLocalDateTimeZones() {
+ // See bug: https://bugs.documentfoundation.org/show_bug.cgi?id=63566
+ super.argLocalDateTimeZones();
+ }
+
+ /**
+ * This one is adjusted in that the float values are passed as double, because
+ * the database stores them both as double and there doesn't appear to be a way
+ * to tell that one was actually declared as a float.
+ */
+ @Test
+ public void saveResultAsTable() {
+ new Schema().addTable("dbtest")
+ .addColumn("nbr_integer").asInteger().primaryKey().table()
+ .addColumn("nbr_long").asLong().table()
+ .addColumn("nbr_float").asFloat().table()
+ .addColumn("nbr_double").asDouble().table()
+ .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table()
+ .addColumn("str_varchar").asString(80).table()
+ .addColumn("str_fixed").asStringFixed(1).table()
+ .addColumn("str_lob").asClob().table()
+ .addColumn("bin_blob").asBlob().table()
+ .addColumn("boolean_flag").asBoolean().table()
+ .addColumn("date_millis").asDate().table()
+ .addColumn("local_date").asLocalDate().schema().execute(db);
+
+ 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);
+
+ 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);
+
+ String expectedSchema = new Schema().addTable("dbtest2")
+ .addColumn("nbr_integer").asInteger().table()
+ .addColumn("nbr_long").asLong().table()
+ .addColumn("nbr_float").asFloat().table()
+ .addColumn("nbr_double").asDouble().table()
+ .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table()
+ .addColumn("str_varchar").asString(80).table()
+ .addColumn("str_fixed").asStringFixed(1).table()
+ .addColumn("str_lob").asClob().table()
+ .addColumn("bin_blob").asBlob().table()
+ .addColumn("boolean_flag").asBoolean().table()
+ .addColumn("date_millis").asDate().table()
+ .addColumn("local_date").asLocalDate().schema().print(db.flavor());
+
+ List args = db.toSelect("select 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 from dbtest")
+ .query(rs -> {
+ List result = new ArrayList<>();
+ while (rs.next()) {
+ if (result.size() == 0) {
+ db.dropTableQuietly("dbtest2");
+ Schema schema = new Schema().addTableFromRow("dbtest2", rs).schema();
+ assertEquals(expectedSchema, schema.print(db.flavor()));
+ schema.execute(db);
+ }
+ result.add(SqlArgs.readRow(rs));
+ }
+ return result;
+ });
+
+ db.toInsert(Sql.insert("dbtest2", args)).insertBatch();
+
+ assertEquals(2, db.toSelect("select count(*) from dbtest2").queryIntegerOrZero());
+ assertEquals(db.toSelect("select 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 from dbtest order by 1")
+ .queryMany(SqlArgs::readRow),
+ db.toSelect("select 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 from dbtest2 order by 1")
+ .queryMany(SqlArgs::readRow));
+ assertEquals(Arrays.asList(
+ new SqlArgs()
+ .argInteger("nbr_integer", Integer.MIN_VALUE)
+ .argLong("nbr_long", Long.MIN_VALUE)
+ .argDouble("nbr_float", 0.000001d)
+ .argDouble("nbr_double", Double.MIN_VALUE)
+ .argBigDecimal("nbr_big_decimal", new BigDecimal("-123.456"))
+ .argString("str_varchar", "goodbye")
+ .argString("str_fixed", "A")
+ .argClobString("str_lob", "bye again")
+ .argBlobBytes("bin_blob", new byte[]{'3', '4'})
+ .argString("boolean_flag", "N")//.argBoolean("boolean_flag", false)
+ .argDate("date_millis", now)
+ .argLocalDate("local_date", localDateNow),
+ new SqlArgs()
+ .argInteger("nbr_integer", Integer.MAX_VALUE)
+ .argLong("nbr_long", Long.MAX_VALUE)
+ .argDouble("nbr_float", (double) Float.MAX_VALUE)
+ .argDouble("nbr_double", Double.MAX_VALUE)
+ .argBigDecimal("nbr_big_decimal", new BigDecimal("123.456"))
+ .argString("str_varchar", "hello")
+ .argString("str_fixed", "Z")
+ .argClobString("str_lob", "hello again")
+ .argBlobBytes("bin_blob", new byte[]{'1', '2'})
+ .argString("boolean_flag", "Y")//.argBoolean("boolean_flag", true)
+ .argDate("date_millis", now)
+ .argLocalDate("local_date", localDateNow)),
+ db.toSelect("select 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 from dbtest2 order by 1")
+ .queryMany(SqlArgs::readRow));
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/OracleTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/OracleTest.java
new file mode 100644
index 0000000..76cbaeb
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/OracleTest.java
@@ -0,0 +1,43 @@
+package org.xbib.jdbc.query.test;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.OracleContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.xbib.jdbc.query.DatabaseProvider;
+import org.xbib.jdbc.query.OptionsOverride;
+
+import java.io.FileReader;
+import java.util.Properties;
+
+/**
+ * Exercise Database functionality with a real Oracle database.
+ */
+public class OracleTest extends CommonTest {
+
+ @Container
+ public OracleContainer oracleContainer = new OracleContainer("");
+
+ @Override
+ protected DatabaseProvider createDatabaseProvider(OptionsOverride options) throws Exception {
+ Properties properties = new Properties();
+ properties.load(new FileReader(System.getProperty("local.properties", "local.properties")));
+ return DatabaseProvider.fromDriverManager(
+ properties.getProperty("database.url"),
+ properties.getProperty("database.user"),
+ properties.getProperty("database.password")
+ ).withSqlParameterLogging().withSqlInExceptionMessages().withOptions(options).create();
+ }
+
+ @Disabled("Current Oracle behavior is to convert -0f to 0f")
+ @Test
+ public void argFloatNegativeZero() {
+ super.argFloatNegativeZero();
+ }
+
+ @Disabled("Current Oracle behavior is to convert -0d to 0d")
+ @Test
+ public void argDoubleNegativeZero() {
+ super.argDoubleNegativeZero();
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/PostgreSqlTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/PostgreSqlTest.java
new file mode 100644
index 0000000..818588c
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/PostgreSqlTest.java
@@ -0,0 +1,50 @@
+package org.xbib.jdbc.query.test;
+
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+import org.xbib.jdbc.query.DatabaseProvider;
+import org.xbib.jdbc.query.OptionsOverride;
+import org.xbib.jdbc.query.Schema;
+
+import java.io.FileReader;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+/**
+ * Exercise Database functionality with a real PostgreSQL database.
+ */
+public class PostgreSqlTest extends CommonTest {
+
+ @Override
+ protected DatabaseProvider createDatabaseProvider(OptionsOverride options) throws IOException {
+ Properties properties = new Properties();
+ properties.load(new FileReader(System.getProperty("local.properties", "local.properties")));
+ return DatabaseProvider.fromDriverManager(
+ properties.getProperty("postgres.database.url"),
+ properties.getProperty("postgres.database.user"),
+ properties.getProperty("postgres.database.password")
+ ).withOptions(options).withSqlParameterLogging().withSqlInExceptionMessages().create();
+ }
+
+ /**
+ * PostgreSQL seems to have different behavior in that is does not convert
+ * column names to uppercase (it actually converts them to lowercase).
+ * I haven't figured out how to smooth over this difference, since all databases
+ * seem to respect the provided case when it is inside quotes, but don't provide
+ * a way to tell whether a particular parameter was quoted.
+ */
+ @Override
+ @Test
+ public void metadataColumnNames() {
+ db.dropTableQuietly("dbtest");
+
+ new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db);
+
+ db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\" from dbtest")
+ .query(rs -> {
+ assertArrayEquals(new String[]{"pk", "foo", "Foo"}, rs.getColumnLabels());
+ return null;
+ });
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/RowStub.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/RowStub.java
new file mode 100644
index 0000000..9723db8
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/RowStub.java
@@ -0,0 +1,799 @@
+package org.xbib.jdbc.query.test;
+
+import org.xbib.jdbc.query.DatabaseException;
+import org.xbib.jdbc.query.Rows;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.sql.ResultSetMetaData;
+import java.sql.Types;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Convenience for specifying hard-coded values for the Rows object. Useful for testing,
+ * especially with Mock libraries.
+ */
+public class RowStub {
+
+ private final List rows = new ArrayList<>();
+
+ private String[] columnNames;
+
+ public RowStub withColumnNames(String... names) {
+ columnNames = names;
+ return this;
+ }
+
+ public RowStub addRow(Object... columns) {
+ rows.add(columns);
+ return this;
+ }
+
+ public Rows toRows() {
+ return new Rows() {
+ private int row = -1;
+ private int col = -1;
+
+ @Override
+ public boolean next() {
+ col = -1;
+ return !rows.isEmpty() && ++row < rows.size();
+ }
+
+
+ @Override
+ public String[] getColumnLabels() {
+ requireColumnNames();
+ return columnNames;
+ }
+
+
+ @Override
+ public ResultSetMetaData getMetadata() {
+ requireColumnNames();
+ return new ResultSetMetaData() {
+ @Override
+ public int getColumnCount() {
+ return columnNames.length;
+ }
+
+ @Override
+ public boolean isAutoIncrement(int column) {
+ return false;
+ }
+
+ @Override
+ public boolean isCaseSensitive(int column) {
+ return false;
+ }
+
+ @Override
+ public boolean isSearchable(int column) {
+ return false;
+ }
+
+ @Override
+ public boolean isCurrency(int column) {
+ return false;
+ }
+
+ @Override
+ public int isNullable(int column) {
+ return columnNullable;
+ }
+
+ @Override
+ public boolean isSigned(int column) {
+ return false;
+ }
+
+ @Override
+ public int getColumnDisplaySize(int column) {
+ return 4000;
+ }
+
+ @Override
+ public String getColumnLabel(int column) {
+ return columnNames[column - 1];
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ return columnNames[column - 1];
+ }
+
+ @Override
+ public String getSchemaName(int column) {
+ return "";
+ }
+
+ @Override
+ public int getPrecision(int column) {
+ return 4000;
+ }
+
+ @Override
+ public int getScale(int column) {
+ return 0;
+ }
+
+ @Override
+ public String getTableName(int column) {
+ return "";
+ }
+
+ @Override
+ public String getCatalogName(int column) {
+ return "";
+ }
+
+ @Override
+ public int getColumnType(int column) {
+ return Types.VARCHAR;
+ }
+
+ @Override
+ public String getColumnTypeName(int column) {
+ return "VARCHAR";
+ }
+
+ @Override
+ public boolean isReadOnly(int column) {
+ return false;
+ }
+
+ @Override
+ public boolean isWritable(int column) {
+ return false;
+ }
+
+ @Override
+ public boolean isDefinitelyWritable(int column) {
+ return false;
+ }
+
+ @Override
+ public String getColumnClassName(int column) {
+ return String.class.getName();
+ }
+
+ @Override
+ public T unwrap(Class iface) {
+ return null;
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) {
+ return false;
+ }
+ };
+ }
+
+
+ @Override
+ public Boolean getBooleanOrNull() {
+ return toBoolean(rows.get(row)[++col]);
+ }
+
+
+ @Override
+ public Boolean getBooleanOrNull(int columnOneBased) {
+ col = columnOneBased;
+ return toBoolean(rows.get(row)[columnOneBased - 1]);
+ }
+
+
+ @Override
+ public Boolean getBooleanOrNull(String columnName) {
+ col = columnIndexByName(columnName) + 1;
+ return toBoolean(rows.get(row)[columnIndexByName(columnName)]);
+ }
+
+ @Override
+ public boolean getBooleanOrFalse() {
+ Boolean i = getBooleanOrNull();
+ return i != null && i;
+ }
+
+ @Override
+ public boolean getBooleanOrFalse(int columnOneBased) {
+ Boolean i = getBooleanOrNull(columnOneBased);
+ return i != null && i;
+ }
+
+ @Override
+ public boolean getBooleanOrFalse(String columnName) {
+ Boolean i = getBooleanOrNull(columnName);
+ return i != null && i;
+ }
+
+ @Override
+ public boolean getBooleanOrTrue() {
+ Boolean i = getBooleanOrNull();
+ return i == null || i;
+ }
+
+ @Override
+ public boolean getBooleanOrTrue(int columnOneBased) {
+ col = columnOneBased;
+ Boolean i = getBooleanOrNull(columnOneBased);
+ return i == null || i;
+ }
+
+ @Override
+ public boolean getBooleanOrTrue(String columnName) {
+ Boolean i = getBooleanOrNull(columnName);
+ return i == null || i;
+ }
+
+
+ @Override
+ public Integer getIntegerOrNull() {
+ return toInteger(rows.get(row)[++col]);
+ }
+
+
+ @Override
+ public Integer getIntegerOrNull(int columnOneBased) {
+ col = columnOneBased;
+ return toInteger(rows.get(row)[columnOneBased - 1]);
+ }
+
+
+ @Override
+ public Integer getIntegerOrNull(String columnName) {
+ col = columnIndexByName(columnName) + 1;
+ return toInteger(rows.get(row)[columnIndexByName(columnName)]);
+ }
+
+ @Override
+ public int getIntegerOrZero() {
+ Integer i = getIntegerOrNull();
+ return i == null ? 0 : i;
+ }
+
+ @Override
+ public int getIntegerOrZero(int columnOneBased) {
+ Integer i = getIntegerOrNull(columnOneBased);
+ return i == null ? 0 : i;
+ }
+
+ @Override
+ public int getIntegerOrZero(String columnName) {
+ Integer i = getIntegerOrNull(columnName);
+ return i == null ? 0 : i;
+ }
+
+
+ @Override
+ public Long getLongOrNull() {
+ return toLong(rows.get(row)[++col]);
+ }
+
+
+ @Override
+ public Long getLongOrNull(int columnOneBased) {
+ col = columnOneBased;
+ return toLong(rows.get(row)[columnOneBased - 1]);
+ }
+
+
+ @Override
+ public Long getLongOrNull(String columnName) {
+ col = columnIndexByName(columnName) + 1;
+ return toLong(rows.get(row)[columnIndexByName(columnName)]);
+ }
+
+ @Override
+ public long getLongOrZero() {
+ Long i = getLongOrNull();
+ return i == null ? 0 : i;
+ }
+
+ @Override
+ public long getLongOrZero(int columnOneBased) {
+ Long i = getLongOrNull(columnOneBased);
+ return i == null ? 0 : i;
+ }
+
+ @Override
+ public long getLongOrZero(String columnName) {
+ Long i = getLongOrNull(columnName);
+ return i == null ? 0 : i;
+ }
+
+
+ @Override
+ public Float getFloatOrNull() {
+ return toFloat(rows.get(row)[++col]);
+ }
+
+
+ @Override
+ public Float getFloatOrNull(int columnOneBased) {
+ col = columnOneBased;
+ return toFloat(rows.get(row)[columnOneBased - 1]);
+ }
+
+
+ @Override
+ public Float getFloatOrNull(String columnName) {
+ col = columnIndexByName(columnName) + 1;
+ return toFloat(rows.get(row)[columnIndexByName(columnName)]);
+ }
+
+ @Override
+ public float getFloatOrZero() {
+ Float i = getFloatOrNull();
+ return i == null ? 0 : i;
+ }
+
+ @Override
+ public float getFloatOrZero(int columnOneBased) {
+ Float i = getFloatOrNull(columnOneBased);
+ return i == null ? 0 : i;
+ }
+
+ @Override
+ public float getFloatOrZero(String columnName) {
+ Float i = getFloatOrNull(columnName);
+ return i == null ? 0 : i;
+ }
+
+
+ @Override
+ public Double getDoubleOrNull() {
+ return toDouble(rows.get(row)[++col]);
+ }
+
+
+ @Override
+ public Double getDoubleOrNull(int columnOneBased) {
+ col = columnOneBased;
+ return toDouble(rows.get(row)[columnOneBased - 1]);
+ }
+
+
+ @Override
+ public Double getDoubleOrNull(String columnName) {
+ col = columnIndexByName(columnName) + 1;
+ return toDouble(rows.get(row)[columnIndexByName(columnName)]);
+ }
+
+ @Override
+ public double getDoubleOrZero() {
+ Double i = getDoubleOrNull();
+ return i == null ? 0 : i;
+ }
+
+ @Override
+ public double getDoubleOrZero(int columnOneBased) {
+ Double i = getDoubleOrNull(columnOneBased);
+ return i == null ? 0 : i;
+ }
+
+ @Override
+ public double getDoubleOrZero(String columnName) {
+ Double i = getDoubleOrNull(columnName);
+ return i == null ? 0 : i;
+ }
+
+
+ @Override
+ public BigDecimal getBigDecimalOrNull() {
+ return toBigDecimal(rows.get(row)[++col]);
+ }
+
+
+ @Override
+ public BigDecimal getBigDecimalOrNull(int columnOneBased) {
+ col = columnOneBased;
+ return toBigDecimal(rows.get(row)[columnOneBased - 1]);
+ }
+
+
+ @Override
+ public BigDecimal getBigDecimalOrNull(String columnName) {
+ col = columnIndexByName(columnName) + 1;
+ return toBigDecimal(rows.get(row)[columnIndexByName(columnName)]);
+ }
+
+
+ @Override
+ public BigDecimal getBigDecimalOrZero() {
+ BigDecimal i = getBigDecimalOrNull();
+ return i == null ? new BigDecimal(0) : i;
+ }
+
+
+ @Override
+ public BigDecimal getBigDecimalOrZero(int columnOneBased) {
+ BigDecimal i = getBigDecimalOrNull(columnOneBased);
+ return i == null ? new BigDecimal(0) : i;
+ }
+
+
+ @Override
+ public BigDecimal getBigDecimalOrZero(String columnName) {
+ BigDecimal i = getBigDecimalOrNull(columnName);
+ return i == null ? new BigDecimal(0) : i;
+ }
+
+
+ @Override
+ public String getStringOrNull() {
+ return toString(rows.get(row)[++col]);
+ }
+
+
+ @Override
+ public String getStringOrNull(int columnOneBased) {
+ col = columnOneBased;
+ return toString(rows.get(row)[columnOneBased - 1]);
+ }
+
+
+ @Override
+ public String getStringOrNull(String columnName) {
+ col = columnIndexByName(columnName) + 1;
+ return toString(rows.get(row)[columnIndexByName(columnName)]);
+ }
+
+
+ @Override
+ public String getStringOrEmpty() {
+ String i = getStringOrNull();
+ return i == null ? "" : i;
+ }
+
+
+ @Override
+ public String getStringOrEmpty(int columnOneBased) {
+ String i = getStringOrNull(columnOneBased);
+ return i == null ? "" : i;
+ }
+
+
+ @Override
+ public String getStringOrEmpty(String columnName) {
+ String i = getStringOrNull(columnName);
+ return i == null ? "" : i;
+ }
+
+
+ @Override
+ public String getClobStringOrNull() {
+ return getStringOrNull();
+ }
+
+
+ @Override
+ public String getClobStringOrNull(int columnOneBased) {
+ return getStringOrNull(columnOneBased);
+ }
+
+
+ @Override
+ public String getClobStringOrNull(String columnName) {
+ return getStringOrNull(columnName);
+ }
+
+
+ @Override
+ public String getClobStringOrEmpty() {
+ return getStringOrEmpty();
+ }
+
+
+ @Override
+ public String getClobStringOrEmpty(int columnOneBased) {
+ return getStringOrEmpty(columnOneBased);
+ }
+
+
+ @Override
+ public String getClobStringOrEmpty(String columnName) {
+ return getStringOrEmpty(columnName);
+ }
+
+
+ @Override
+ public Reader getClobReaderOrNull() {
+ String s = getStringOrNull();
+ return s == null ? null : new StringReader(s);
+ }
+
+
+ @Override
+ public Reader getClobReaderOrNull(int columnOneBased) {
+ String s = getStringOrNull(columnOneBased);
+ return s == null ? null : new StringReader(s);
+ }
+
+
+ @Override
+ public Reader getClobReaderOrNull(String columnName) {
+ String s = getStringOrNull(columnName);
+ return s == null ? null : new StringReader(s);
+ }
+
+
+ @Override
+ public Reader getClobReaderOrEmpty() {
+ return new StringReader(getStringOrEmpty());
+ }
+
+
+ @Override
+ public Reader getClobReaderOrEmpty(int columnOneBased) {
+ return new StringReader(getStringOrEmpty(columnOneBased));
+ }
+
+
+ @Override
+ public Reader getClobReaderOrEmpty(String columnName) {
+ return new StringReader(getStringOrEmpty(columnName));
+ }
+
+
+ @Override
+ public byte[] getBlobBytesOrNull() {
+ return toBytes(rows.get(row)[++col]);
+ }
+
+
+ @Override
+ public byte[] getBlobBytesOrNull(int columnOneBased) {
+ col = columnOneBased;
+ return toBytes(rows.get(row)[columnOneBased - 1]);
+ }
+
+
+ @Override
+ public byte[] getBlobBytesOrNull(String columnName) {
+ col = columnIndexByName(columnName) + 1;
+ return toBytes(rows.get(row)[columnIndexByName(columnName)]);
+ }
+
+
+ @Override
+ public byte[] getBlobBytesOrZeroLen() {
+ byte[] a = getBlobBytesOrNull();
+ return a == null ? new byte[0] : a;
+ }
+
+
+ @Override
+ public byte[] getBlobBytesOrZeroLen(int columnOneBased) {
+ byte[] a = getBlobBytesOrNull(columnOneBased);
+ return a == null ? new byte[0] : a;
+ }
+
+
+ @Override
+ public byte[] getBlobBytesOrZeroLen(String columnName) {
+ byte[] a = getBlobBytesOrNull(columnName);
+ return a == null ? new byte[0] : a;
+ }
+
+
+ @Override
+ public InputStream getBlobInputStreamOrNull() {
+ byte[] a = getBlobBytesOrNull();
+ return a == null ? null : new ByteArrayInputStream(a);
+ }
+
+
+ @Override
+ public InputStream getBlobInputStreamOrNull(int columnOneBased) {
+ byte[] a = getBlobBytesOrNull(columnOneBased);
+ return a == null ? null : new ByteArrayInputStream(a);
+ }
+
+
+ @Override
+ public InputStream getBlobInputStreamOrNull(String columnName) {
+ byte[] a = getBlobBytesOrNull(columnName);
+ return a == null ? null : new ByteArrayInputStream(a);
+ }
+
+
+ @Override
+ public InputStream getBlobInputStreamOrEmpty() {
+ return new ByteArrayInputStream(getBlobBytesOrZeroLen());
+ }
+
+
+ @Override
+ public InputStream getBlobInputStreamOrEmpty(int columnOneBased) {
+ return new ByteArrayInputStream(getBlobBytesOrZeroLen(columnOneBased));
+ }
+
+
+ @Override
+ public InputStream getBlobInputStreamOrEmpty(String columnName) {
+ return new ByteArrayInputStream(getBlobBytesOrZeroLen(columnName));
+ }
+
+
+ @Override
+ public Date getDateOrNull() {
+ return toDate(rows.get(row)[++col]);
+ }
+
+
+ @Override
+ public Date getDateOrNull(int columnOneBased) {
+ col = columnOneBased;
+ return toDate(rows.get(row)[columnOneBased - 1]);
+ }
+
+
+ @Override
+ public Date getDateOrNull(String columnName) {
+ col = columnIndexByName(columnName) + 1;
+ return toDate(rows.get(row)[columnIndexByName(columnName)]);
+ }
+
+ /**
+ * Returns a java.time.LocalDate. It will have no timezone or other time data.
+ * If you require time, use the Date APIs instead.
+ */
+
+ @Override
+ public LocalDate getLocalDateOrNull() {
+ return toLocalDate(rows.get(row)[++col]);
+ }
+
+ /**
+ * Returns a java.time.LocalDate. It will have no timezone or other time data.
+ * If you require time, use the Date APIs instead.
+ */
+
+ @Override
+ public LocalDate getLocalDateOrNull(int columnOneBased) {
+ col = columnOneBased;
+ return toLocalDate(rows.get(row)[columnOneBased - 1]);
+ }
+
+ /**
+ * Returns a java.time.LocalDate. It will have no timezone or other time data.
+ * If you require time, use the Date APIs instead.
+ */
+
+ @Override
+ public LocalDate getLocalDateOrNull(String columnName) {
+ col = columnIndexByName(columnName) + 1;
+ return toLocalDate(rows.get(row)[columnIndexByName(columnName)]);
+ }
+
+ private void requireColumnNames() {
+ if (columnNames == null) {
+ throw new DatabaseException("Column names were not provided for this stub");
+ }
+ }
+
+ private int columnIndexByName(String columnName) {
+ requireColumnNames();
+ for (int i = 0; i < columnNames.length; i++) {
+ if (columnName.equals(columnNames[i])) {
+ return i;
+ }
+ }
+ throw new DatabaseException("Column name '" + columnName + "' not found");
+ }
+
+ private Boolean toBoolean(Object o) {
+ if (o instanceof String) {
+ if ("Y".equals(o)) {
+ return Boolean.TRUE;
+ } else if ("N".equals(o)) {
+ return Boolean.FALSE;
+ } else {
+ throw new DatabaseException("Value returned for boolean was not 'Y' or 'N'");
+ }
+ }
+ return (Boolean) o;
+ }
+
+ private Integer toInteger(Object o) {
+ return (Integer) o;
+ }
+
+ private Long toLong(Object o) {
+ if (o instanceof Integer) {
+ return ((Integer) o).longValue();
+ }
+ return (Long) o;
+ }
+
+ private Float toFloat(Object o) {
+ if (o instanceof Integer) {
+ return ((Integer) o).floatValue();
+ }
+ return (Float) o;
+ }
+
+ private Double toDouble(Object o) {
+ if (o instanceof Integer) {
+ return ((Integer) o).doubleValue();
+ }
+ if (o instanceof Float) {
+ return ((Float) o).doubleValue();
+ }
+ return (Double) o;
+ }
+
+ private BigDecimal toBigDecimal(Object o) {
+ if (o instanceof Integer) {
+ return BigDecimal.valueOf(((Integer) o).longValue());
+ }
+ if (o instanceof Long) {
+ return BigDecimal.valueOf((Long) o);
+ }
+ if (o instanceof Float) {
+ return BigDecimal.valueOf(((Float) o).doubleValue());
+ }
+ if (o instanceof Double) {
+ return BigDecimal.valueOf((Double) o);
+ }
+ return (BigDecimal) o;
+ }
+
+ private byte[] toBytes(Object o) {
+ return (byte[]) o;
+ }
+
+ private String toString(Object o) {
+ return (String) o;
+ }
+
+ /**
+ * Returns a java.util.Date. It may be used for dates or times.
+ */
+ private Date toDate(Object o) {
+ if (o instanceof String) {
+ String s = (String) o;
+ if (s.length() == "yyyy-MM-dd".length()) {
+ try {
+ return new SimpleDateFormat("yyyy-MM-dd").parse(s);
+ } catch (ParseException e) {
+ throw new DatabaseException("Could not parse date as yyyy-MM-dd for " + s);
+ }
+ }
+ if (s.length() == "yyyy-MM-ddThh:mm:ss".length()) {
+ try {
+ return new SimpleDateFormat("yyyy-MM-ddThh:mm:ss").parse(s);
+ } catch (ParseException e) {
+ throw new DatabaseException("Could not parse date as yyyy-MM-ddThh:mm:ss for " + s);
+ }
+ }
+ throw new DatabaseException("Didn't understand date string: " + s);
+ }
+ return (Date) o;
+ }
+
+ /**
+ * Returns a LocalDate (no time).
+ * If the object is a String, it should be in ISO 8601 format.
+ *
+ * @return a LocalDate representation of the object
+ */
+ private LocalDate toLocalDate(Object o) {
+ if (o instanceof String) {
+ return LocalDate.parse((String) o);
+ }
+
+ return (LocalDate) o;
+ }
+ };
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlArgsTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlArgsTest.java
new file mode 100644
index 0000000..6f2e31a
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlArgsTest.java
@@ -0,0 +1,24 @@
+package org.xbib.jdbc.query.test;
+
+import org.junit.jupiter.api.Test;
+import org.xbib.jdbc.query.SqlArgs;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+/**
+ * Unit tests for the SqlArgs class.
+ */
+public class SqlArgsTest {
+
+ @Test
+ public void testTidyColumnNames() {
+ assertArrayEquals(new String[]{"column_1", "column_2", "a", "a_2", "a_3", "a1"},
+ SqlArgs.tidyColumnNames(new String[]{null, "", " a ", "a ", "#!@#$_a", "#!@#$_1"}));
+
+ check("TheBest", "the_best");
+ }
+
+ private void check(String input, String output) {
+ assertArrayEquals(new String[]{output}, SqlArgs.tidyColumnNames(new String[]{input}));
+ }
+}
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
new file mode 100644
index 0000000..bb77373
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlServerTest.java
@@ -0,0 +1,86 @@
+package org.xbib.jdbc.query.test;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.xbib.jdbc.query.Config;
+import org.xbib.jdbc.query.ConfigFrom;
+import org.xbib.jdbc.query.DatabaseProvider;
+import org.xbib.jdbc.query.OptionsOverride;
+import org.xbib.jdbc.query.Schema;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+/**
+ * Exercise Database functionality with a real Oracle database.
+ */
+public class SqlServerTest extends CommonTest {
+ @Override
+ protected DatabaseProvider createDatabaseProvider(OptionsOverride options) throws Exception {
+ String propertiesFile = System.getProperty("local.properties", "local.properties");
+ Config config = ConfigFrom.firstOf()
+ .systemProperties()
+ .propertyFile(propertiesFile)
+ .excludePrefix("database.")
+ .removePrefix("sqlserver.").get();
+ return DatabaseProvider.fromDriverManager(config)
+ .withSqlParameterLogging()
+ .withSqlInExceptionMessages()
+ .withOptions(options).create();
+ }
+
+ @Disabled("SQL Server prohibits NaN and Infinity")
+ @Test
+ public void argFloatNaN() {
+ super.argFloatNaN();
+ }
+
+ @Disabled("SQL Server prohibits NaN and Infinity")
+ @Test
+ public void argFloatInfinity() {
+ super.argFloatInfinity();
+ }
+
+ @Disabled("SQL Server prohibits NaN and Infinity")
+ @Test
+ public void argDoubleNaN() {
+ super.argDoubleNaN();
+ }
+
+ @Disabled("SQL Server prohibits NaN and Infinity")
+ @Test
+ public void argDoubleInfinity() {
+ super.argDoubleInfinity();
+ }
+
+ @Disabled("SQL Server seems to have incorrect min value for float (rounds to zero)")
+ @Test
+ public void argFloatMinMax() {
+ super.argFloatMinMax();
+ }
+
+ @Disabled("SQL Server doesn't support the interval syntax for date arithmetic")
+ @Test
+ public void intervals() {
+ super.intervals();
+ }
+
+ /**
+ * SQL Server seems to have different behavior in that is does not convert
+ * column names to uppercase (it preserves the case).
+ * I haven't figured out how to smooth over this difference, since all databases
+ * seem to respect the provided case when it is inside quotes, but don't provide
+ * a way to tell whether a particular parameter was quoted.
+ */
+ @Override
+ @Test
+ public void metadataColumnNames() {
+ db.dropTableQuietly("dbtest");
+
+ new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db);
+
+ db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\" from dbtest").query(rs -> {
+ assertArrayEquals(new String[]{"Pk", "Foo", "Foo"}, rs.getColumnLabels());
+ return null;
+ });
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DerbyExample.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DerbyExample.java
new file mode 100644
index 0000000..cfc3eb6
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DerbyExample.java
@@ -0,0 +1,35 @@
+package org.xbib.jdbc.query.test.example;
+
+import org.xbib.jdbc.query.Database;
+import org.xbib.jdbc.query.DatabaseProvider;
+
+/**
+ * Demo of using some com.github.susom.database classes with Derby.
+ */
+public abstract class DerbyExample {
+
+ void example(Database db, String[] args) {
+ // For subclasses to override
+ }
+
+ void example(DatabaseProvider.Builder dbb, final String[] args) {
+ dbb.transact(db -> {
+ example(db.get(), args);
+ });
+ }
+
+ public void println(String s) {
+ System.out.println(s);
+ }
+
+ public final void launch(final String[] args) {
+ try {
+ System.setProperty("derby.stream.error.file", "java.lang.System.err");
+ String url = "jdbc:derby:target/testdb;create=true";
+ example(DatabaseProvider.fromDriverManager(url), args);
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DynamicSql.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DynamicSql.java
new file mode 100644
index 0000000..c1b3125
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DynamicSql.java
@@ -0,0 +1,52 @@
+package org.xbib.jdbc.query.test.example;
+
+import org.xbib.jdbc.query.Database;
+import org.xbib.jdbc.query.Schema;
+import org.xbib.jdbc.query.Sql;
+
+/**
+ * Demo of how to use the Sql helper class to dynamically build queries.
+ */
+public class DynamicSql extends DerbyExample {
+ public static void main(String[] args) {
+ new DynamicSql().launch(args);
+ }
+
+ void example(Database db, String[] args) {
+ // Drops in case we are running this multiple times
+ db.dropTableQuietly("t");
+
+ // Create and populate a simple table
+ new Schema()
+ .addTable("t")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("s").asString(80).schema().execute(db);
+ db.toInsert("insert into t (pk,s) values (?,?)")
+ .argLong(1L).argString("Hi").insert(1);
+ db.toInsert("insert into t (pk,s) values (?,?)")
+ .argLong(2L).argString("Hi").insert(1);
+
+ // Construct various dynamic queries and execute them
+ println("Rows with none: " + countByPkOrS(db, null, null));
+ println("Rows with pk=1: " + countByPkOrS(db, 1L, null));
+ println("Rows with s=Hi: " + countByPkOrS(db, null, "Hi"));
+ }
+
+ Long countByPkOrS(Database db, Long pk, String s) {
+ Sql sql = new Sql("select count(*) from t");
+ boolean where = true;
+ if (pk != null) {
+ where = false;
+ sql.append(" where pk=?").argLong(pk);
+ }
+ if (s != null) {
+ if (where) {
+ sql.append(" where ");
+ } else {
+ sql.append(" and ");
+ }
+ sql.append("s=?").argString(s);
+ }
+ return db.toSelect(sql).queryLongOrNull();
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/FakeBuilder.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/FakeBuilder.java
new file mode 100644
index 0000000..0940134
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/FakeBuilder.java
@@ -0,0 +1,65 @@
+package org.xbib.jdbc.query.test.example;
+
+import org.xbib.jdbc.query.DatabaseException;
+import org.xbib.jdbc.query.DatabaseProvider;
+import org.xbib.jdbc.query.Schema;
+
+/**
+ * Demo of how to use the {@code DatabaseProvider.fakeBuilder()} to control
+ * transactions for testing purposes.
+ */
+public class FakeBuilder extends DerbyExample {
+ public static void main(String[] args) {
+ new FakeBuilder().launch(args);
+ }
+
+ void example(DatabaseProvider.Builder dbb, String[] args) {
+ DatabaseProvider realDbp = null;
+
+ try {
+ realDbp = dbb.create();
+
+ dbb.transact(db -> {
+ // Drops in case we are running this multiple times
+ db.get().dropTableQuietly("t");
+
+ // Create and populate a simple table
+ new Schema().addTable("t").addColumn("pk").primaryKey().schema().execute(db.get());
+ });
+
+ DatabaseProvider.Builder fakeBuilder = realDbp.fakeBuilder();
+
+ // Trying all three transact methods, just for completeness
+ fakeBuilder.transact(db -> {
+ db.get().toInsert("insert into t (pk) values (?)").argLong(1L).insert(1);
+ });
+ fakeBuilder.transact((db, tx) -> {
+ db.get().toInsert("insert into t (pk) values (?)").argLong(2L).insert(1);
+ });
+
+ fakeBuilder.transact(db -> {
+ println("Rows before rollback: " + db.get().toSelect("select count(*) from t").queryLongOrZero());
+ });
+
+ realDbp.rollbackAndClose();
+
+ // Can't use fakeBuilder after close
+ try {
+ fakeBuilder.transact(db -> {
+ db.get().tableExists("foo");
+ println("Eeek...shouldn't get here!");
+ });
+ } catch (DatabaseException e) {
+ println("Correctly threw exception: " + e.getMessage());
+ }
+
+ dbb.transact(db -> {
+ println("Rows after rollback: " + db.get().toSelect("select count(*) from t").queryLongOrZero());
+ });
+ } finally {
+ if (realDbp != null) {
+ realDbp.rollbackAndClose();
+ }
+ }
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloAny.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloAny.java
new file mode 100644
index 0000000..b9144df
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloAny.java
@@ -0,0 +1,47 @@
+package org.xbib.jdbc.query.test.example;
+
+import org.xbib.jdbc.query.Database;
+import org.xbib.jdbc.query.DatabaseProvider;
+
+/**
+ * Example with database info provided from command line. To use this, set properties like this:
+ *
+ *
+ * -Ddatabase.url=... Database connect string (required)
+ * -Ddatabase.user=... Authenticate as this user (optional if provided in url)
+ * -Ddatabase.password=... User password (optional if user and password provided in
+ * url; prompted on standard input if user is provided and
+ * password is not)
+ * -Ddatabase.flavor=... What kind of database it is (optional, will guess based
+ * on the url if this is not provided)
+ * -Ddatabase.driver=... The Java class of the JDBC driver to load (optional, will
+ * guess based on the flavor if this is not provided)
+ *
+ */
+public class HelloAny {
+ public static void main(final String[] args) {
+ try {
+ new HelloAny().run();
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ public void run() {
+ DatabaseProvider.fromSystemProperties().transact(dbp -> {
+ Database db = dbp.get();
+ db.dropTableQuietly("t");
+ db.ddl("create table t (a numeric)").execute();
+ db.toInsert("insert into t (a) values (?)")
+ .argInteger(32)
+ .insert(1);
+ db.toUpdate("update t set a=:val")
+ .argInteger("val", 23)
+ .update(1);
+
+ Long rows = db.toSelect("select count(1) from t ").queryLongOrNull();
+ System.out.println("Rows: " + rows);
+ });
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloDerby.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloDerby.java
new file mode 100644
index 0000000..13678de
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloDerby.java
@@ -0,0 +1,46 @@
+package org.xbib.jdbc.query.test.example;
+
+import org.xbib.jdbc.query.Database;
+import org.xbib.jdbc.query.DatabaseProvider;
+
+import java.io.File;
+
+/**
+ * Demo of using some com.github.susom.database classes with Derby.
+ */
+public class HelloDerby {
+ public static void main(final String[] args) {
+ try {
+ new HelloDerby().run();
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ public void run() {
+ // Put all Derby related files inside ./build to keep our working copy clean
+ File directory = new File("target").getAbsoluteFile();
+ if (directory.exists() || directory.mkdirs()) {
+ System.setProperty("derby.stream.error.file", new File(directory, "derby.log").getAbsolutePath());
+ }
+
+ String url = "jdbc:derby:target/testdb;create=true";
+ DatabaseProvider.fromDriverManager(url).transact(dbp -> {
+ Database db = dbp.get();
+ db.ddl("drop table t").executeQuietly();
+ db.ddl("create table t (a numeric)").execute();
+ db.toInsert("insert into t (a) values (?)").argInteger(32).insert(1);
+ db.toUpdate("update t set a=:val")
+ .argInteger("val", 23)
+ .update(1);
+
+ Long rows = db.toSelect("select count(1) from t ").queryLongOrNull();
+ println("Rows: " + rows);
+ });
+ }
+
+ public void println(String s) {
+ System.out.println(s);
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/InsertReturning.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/InsertReturning.java
new file mode 100644
index 0000000..7d174be
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/InsertReturning.java
@@ -0,0 +1,42 @@
+package org.xbib.jdbc.query.test.example;
+
+import org.xbib.jdbc.query.Database;
+import org.xbib.jdbc.query.Schema;
+
+/**
+ * Demo of using some com.github.susom.database classes with Derby.
+ */
+public class InsertReturning extends DerbyExample {
+
+ public static void main(String[] args) {
+ new InsertReturning().launch(args);
+ }
+
+ void example(Database db, String[] args) {
+ // Drops in case we are running this multiple times
+ db.dropTableQuietly("t");
+ db.dropSequenceQuietly("pk_seq");
+
+ // Create a table and a sequence
+ new Schema()
+ .addTable("t")
+ .addColumn("pk").primaryKey().table()
+ .addColumn("d").asDate().table()
+ .addColumn("s").asString(80).schema()
+ .addSequence("pk_seq").schema().execute(db);
+
+ // Insert a row into the table, populating the primary key from a sequence,
+ // and the date based on current database time. Observe that this will work
+ // on Derby, where it results in a query for the sequence value, followed by
+ // the insert. On databases like Oracle, this will be optimized into a single
+ // statement that does the insert and also returns the primary key.
+ Long pk = db.toInsert(
+ "insert into t (pk,d,s) values (?,?,?)")
+ .argPkSeq("pk_seq")
+ .argDateNowPerDb()
+ .argString("Hi")
+ .insertReturningPkSeq("pk");
+
+ println("Inserted row with pk=" + pk);
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/Sample.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/Sample.java
new file mode 100644
index 0000000..4c2d887
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/Sample.java
@@ -0,0 +1,49 @@
+package org.xbib.jdbc.query.test.example;
+
+import java.util.Date;
+
+/**
+ * Simple bean for use with SampleDao.
+ */
+public class Sample {
+
+ private Long sampleId;
+
+ private String name;
+
+ private Integer updateSequence;
+
+ private Date updateTime;
+
+ public Long getSampleId() {
+ return sampleId;
+ }
+
+ public void setSampleId(Long sampleId) {
+ this.sampleId = sampleId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Integer getUpdateSequence() {
+ return updateSequence;
+ }
+
+ public void setUpdateSequence(Integer updateSequence) {
+ this.updateSequence = updateSequence;
+ }
+
+ public Date getUpdateTime() {
+ return updateTime;
+ }
+
+ public void setUpdateTime(Date updateTime) {
+ this.updateTime = updateTime;
+ }
+}
diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/SampleDao.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/SampleDao.java
new file mode 100644
index 0000000..eb8b573
--- /dev/null
+++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/SampleDao.java
@@ -0,0 +1,109 @@
+package org.xbib.jdbc.query.test.example;
+
+import org.xbib.jdbc.query.Database;
+
+import java.util.Date;
+import java.util.function.Supplier;
+
+/**
+ * Create, read, update, and delete sample database objects.
+ */
+public class SampleDao {
+
+ private final Supplier dbp;
+
+ public SampleDao(Supplier dbp) {
+ this.dbp = dbp;
+ }
+
+ public void createSample(final Sample sample, Long userIdMakingChange) {
+ Database db = dbp.get();
+
+ Date updateTime = db.nowPerApp();
+ Long sampleId = db.toInsert(
+ "insert into sample (sample_id, sample_name, update_sequence, update_time) values (?,?,0,?)")
+ .argPkSeq("id_seq")
+ .argString(sample.getName())
+ .argDate(updateTime)
+ .insertReturningPkSeq("sample_id");
+
+ db.toInsert("insert into sample_history (sample_id, sample_name, update_sequence, update_time, update_user_id,"
+ + " is_deleted) values (?,?,0,?,?,'N')")
+ .argLong(sampleId)
+ .argString(sample.getName())
+ .argDate(updateTime)
+ .argLong(userIdMakingChange)
+ .insert(1);
+
+ // Update the object in memory
+ sample.setSampleId(sampleId);
+ sample.setUpdateSequence(0);
+ sample.setUpdateTime(updateTime);
+ }
+
+ public Sample findSampleById(final Long sampleId, boolean lockRow) {
+ return dbp.get().toSelect("select sample_name, update_sequence, update_time from sample where sample_id=?"
+ + (lockRow ? " for update" : ""))
+ .argLong(sampleId).queryOneOrNull(r -> {
+ Sample result = new Sample();
+ result.setSampleId(sampleId);
+ result.setName(r.getStringOrNull());
+ result.setUpdateSequence(r.getIntegerOrNull());
+ result.setUpdateTime(r.getDateOrNull());
+ return result;
+ });
+ }
+
+ public void updateSample(Sample sample, Long userIdMakingChange) {
+ Database db = dbp.get();
+
+ // Insert the history row first, so it will fail (non-unique sample_id + update_sequence)
+ // if someone else modified the row. This is an optimistic locking strategy.
+ int newUpdateSequence = sample.getUpdateSequence() + 1;
+ Date newUpdateTime = db.nowPerApp();
+ db.toInsert("insert into sample_history (sample_id, sample_name, update_sequence, update_time, update_user_id,"
+ + " is_deleted) values (?,?,?,?,?,'N')")
+ .argLong(sample.getSampleId())
+ .argString(sample.getName())
+ .argInteger(newUpdateSequence)
+ .argDate(newUpdateTime)
+ .argLong(userIdMakingChange)
+ .insert(1);
+
+ db.toUpdate("update sample set sample_name=?, update_sequence=?, update_time=? where sample_id=?")
+ .argString(sample.getName())
+ .argInteger(newUpdateSequence)
+ .argDate(newUpdateTime)
+ .argLong(sample.getSampleId())
+ .update(1);
+
+ // Make sure the object in memory matches the database.
+ sample.setUpdateSequence(newUpdateSequence);
+ sample.setUpdateTime(newUpdateTime);
+ }
+
+ public void deleteSample(Sample sample, Long userIdMakingChange) {
+ Database db = dbp.get();
+
+ // Insert the history row first, so it will fail (non-unique sample_id + update_sequence)
+ // if someone else modified the row. This is an optimistic locking strategy.
+ int newUpdateSequence = sample.getUpdateSequence() + 1;
+ Date newUpdateTime = db.nowPerApp();
+ db.toInsert("insert into sample_history (sample_id, sample_name, update_sequence, update_time, update_user_id,"
+ + " is_deleted) values (?,?,?,?,?,'Y')")
+ .argLong(sample.getSampleId())
+ .argString(sample.getName())
+ .argInteger(newUpdateSequence)
+ .argDate(newUpdateTime)
+ .argLong(userIdMakingChange)
+ .insert(1);
+
+ db.toDelete("delete from sample where sample_id=?")
+ .argLong(sample.getSampleId())
+ .update(1);
+
+ // Make sure the object in memory matches the database.
+ sample.setUpdateSequence(newUpdateSequence);
+ sample.setUpdateTime(newUpdateTime);
+ }
+}
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..c48c38f
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+include 'jdbc-connection-pool'
+include 'jdbc-query'