package org.davidmoten.rx.jdbc;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLSyntaxErrorException;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.davidmoten.rx.jdbc.annotations.Column;
import org.davidmoten.rx.jdbc.annotations.Index;
import org.davidmoten.rx.jdbc.annotations.Query;
import org.davidmoten.rx.jdbc.exceptions.AnnotationsNotFoundException;
import org.davidmoten.rx.jdbc.exceptions.AutomappedInterfaceInaccessibleException;
import org.davidmoten.rx.jdbc.exceptions.CannotForkTransactedConnection;
import org.davidmoten.rx.jdbc.exceptions.ColumnIndexOutOfRangeException;
import org.davidmoten.rx.jdbc.exceptions.ColumnNotFoundException;
import org.davidmoten.rx.jdbc.exceptions.MoreColumnsRequestedThanExistException;
import org.davidmoten.rx.jdbc.exceptions.NamedParameterFoundButSqlDoesNotHaveNamesException;
import org.davidmoten.rx.jdbc.exceptions.NamedParameterMissingException;
import org.davidmoten.rx.jdbc.exceptions.QueryAnnotationMissingException;
import org.davidmoten.rx.jdbc.exceptions.SQLRuntimeException;
import org.davidmoten.rx.jdbc.internal.DelegatedConnection;
import org.davidmoten.rx.jdbc.pool.DatabaseCreator;
import org.davidmoten.rx.jdbc.pool.DatabaseType;
import org.davidmoten.rx.jdbc.pool.NonBlockingConnectionPool;
import org.davidmoten.rx.jdbc.pool.Pools;
import org.davidmoten.rx.jdbc.tuple.Tuple2;
import org.davidmoten.rx.jdbc.tuple.Tuple3;
import org.davidmoten.rx.jdbc.tuple.Tuple4;
import org.davidmoten.rx.jdbc.tuple.Tuple5;
import org.davidmoten.rx.jdbc.tuple.Tuple6;
import org.davidmoten.rx.jdbc.tuple.Tuple7;
import org.davidmoten.rx.jdbc.tuple.TupleN;
import org.davidmoten.rx.pool.PoolClosedException;
import org.h2.jdbc.JdbcSQLException;
import org.hsqldb.jdbc.JDBCBlobFile;
import org.hsqldb.jdbc.JDBCClobFile;
import org.junit.Assert;
import org.junit.FixMethodOrder;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.davidmoten.guavamini.Lists;
import com.github.davidmoten.guavamini.Sets;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.reactivex.Completable;
import io.reactivex.Flowable;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.Single;
import io.reactivex.exceptions.UndeliverableException;
import io.reactivex.functions.Predicate;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.schedulers.TestScheduler;
import io.reactivex.subscribers.TestSubscriber;

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class DatabaseTest {

    private static final long FRED_REGISTERED_TIME = 1442515672690L;
    private static final int NAMES_COUNT_BIG = 5163;
    private static final Logger log = LoggerFactory.getLogger(DatabaseTest.class);
    private static final int TIMEOUT_SECONDS = 10;

    private static Database db() {
        return DatabaseCreator.create(1);
    }

    private static Database blocking() {
        return DatabaseCreator.createBlocking();
    }

    private static Database db(int poolSize) {
        return DatabaseCreator.create(poolSize);
    }

    private static Database big(int poolSize) {
        return DatabaseCreator.create(poolSize, true, Schedulers.computation());
    }

    @Test
    public void testSelectUsingQuestionMark() {
        try (Database db = db()) {
            db.select("select score from person where name=?") //
                    .parameters("FRED", "JOSEPH") //
                    .getAs(Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34) //
                    .assertComplete();
        }
    }

    @Test
    public void testDemonstrateDbClosureOnTerminate() {
        Database db = db();
        db.select("select score from person where name=?") //
                .parameters("FRED", "JOSEPH") //
                .getAs(Integer.class) //
                .doOnTerminate(() -> db.close()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues(21, 34) //
                .assertComplete();
    }

    @Test
    public void testFromDataSource() {
        ConnectionProvider cp = DatabaseCreator.connectionProvider();
        AtomicInteger count = new AtomicInteger();
        Set<Connection> connections = new HashSet<>();
        AtomicInteger closed = new AtomicInteger();
        Database db = Database.fromBlocking(new ConnectionProvider() {

            @Override
            public Connection get() {
                count.incrementAndGet();
                Connection c = cp.get();
                connections.add(c);
                return new DelegatedConnection() {

                    @Override
                    public Connection con() {
                        return c;
                    }

                    @Override
                    public void close() {
                        closed.incrementAndGet();
                    }
                };
            }

            @Override
            public void close() {
                // do nothing
            }
        });
        db.select("select count(*) from person") //
                .getAs(Integer.class) //
                .blockingSubscribe();
        db.select("select count(*) from person") //
                .getAs(Integer.class) //
                .blockingSubscribe();
        assertEquals(2, count.get());
        assertEquals(2, connections.size());
        assertEquals(2, closed.get());
    }

    @Test
    public void testBlockingForEachWhenError() {
        try {
            Flowable //
                    .error(new RuntimeException("boo")) //
                    .blockingForEach(System.out::println);
        } catch (RuntimeException e) {
            assertEquals("boo", e.getMessage());
        }
    }

    @Test
    public void testSelectUsingQuestionMarkAndInClauseIssue10() {
        Database.test() //
                .select("select score from person where name in (?) order by score") //
                .parameters("FRED", "JOSEPH") //
                .getAs(Integer.class) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues(21, 34) //
                .assertComplete();
    }

    @Test
    public void testSelectUsingQuestionMarkAndInClauseWithSetParameter() {
        Database.test() //
                .select("select score from person where name in (?) order by score") //
                .parameter(Sets.newHashSet("FRED", "JOSEPH")) //
                .getAs(Integer.class) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues(21, 34) //
                .assertComplete();
    }

    @Test
    public void testUpdateWithInClauseBatchSize0() {
        Database.test() //
                .update("update person set score=50 where name in (?)") //
                .batchSize(0) //
                .parameter(Sets.newHashSet("FRED", "JOSEPH")) //
                .counts() //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.DAYS) //
                .assertComplete() //
                .assertValues(2);
    }

    @Test
    public void testUpdateWithInClauseBatchSize10() {
        Database.test() //
                .update("update person set score=50 where name in (?)") //
                .batchSize(10) //
                .parameter(Sets.newHashSet("FRED", "JOSEPH")) //
                .counts() //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.DAYS) //
                .assertComplete() //
                .assertValues(2);
    }

    @Test
    public void testSelectUsingQuestionMarkAndInClauseWithSetParameterUsingParametersMethod() {
        Database.test() //
                .select("select score from person where name in (?) order by score") //
                .parameters(Sets.newHashSet("FRED", "JOSEPH")) //
                .getAs(Integer.class) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues(21, 34) //
                .assertComplete();
    }

    @Test
    public void testSelectUsingInClauseWithListParameter() {
        Database.test() //
                .select("select score from person where score > ? and name in (?) order by score") //
                .parameters(0, Lists.newArrayList("FRED", "JOSEPH")) //
                .getAs(Integer.class) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues(21, 34) //
                .assertComplete();
    }

    @Test
    public void testSelectUsingInClauseWithNamedListParameter() {
        Database.test() //
                .select("select score from person where score > :score and name in (:names) order by score") //
                .parameter("score", 0) //
                .parameter("names", Lists.newArrayList("FRED", "JOSEPH")) //
                .getAs(Integer.class) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues(21, 34) //
                .assertComplete();
    }

    @Test
    public void testSelectUsingInClauseWithRepeatedNamedListParameter() {
        Database.test() //
                .select("select score from person where name in (:names) and name in (:names) order by score") //
                .parameter("names", Lists.newArrayList("FRED", "JOSEPH")) //
                .getAs(Integer.class) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues(21, 34) //
                .assertComplete();
    }

    @Test
    public void testSelectUsingInClauseWithRepeatedNamedListParameterAndRepeatedNonListParameter() {
        Database.test() //
                .select("select score from person where name in (:names) and score >:score and name in (:names) and score >:score order by score") //
                .parameter("names", Lists.newArrayList("FRED", "JOSEPH")) //
                .parameter("score", 0) //
                .getAs(Integer.class) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues(21, 34) //
                .assertComplete();
    }

    @Test
    public void testSelectUsingNamedParameterList() {
        try (Database db = db()) {
            db.select("select score from person where name=:name") //
                    .parameters(Parameter.named("name", "FRED").value("JOSEPH").list()) //
                    .getAs(Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectUsingQuestionMarkFlowableParameters() {
        try (Database db = db()) {
            db.select("select score from person where name=?") //
                    .parameterStream(Flowable.just("FRED", "JOSEPH")) //
                    .getAs(Integer.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectUsingQuestionMarkFlowableParametersInLists() {
        try (Database db = db()) {
            db.select("select score from person where name=?") //
                    .parameterListStream(
                            Flowable.just(Arrays.asList("FRED"), Arrays.asList("JOSEPH"))) //
                    .getAs(Integer.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34) //
                    .assertComplete();
        }
    }

    @Test
    public void testDrivingSelectWithoutParametersUsingParameterStream() {
        try (Database db = db()) {
            db.select("select count(*) from person") //
                    .parameters(1, 2, 3) //
                    .getAs(Integer.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(3, 3, 3) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectUsingQuestionMarkFlowableParametersTwoParametersPerQuery() {
        try (Database db = db()) {
            db.select("select score from person where name=? and score = ?") //
                    .parameterStream(Flowable.just("FRED", 21, "JOSEPH", 34)) //
                    .getAs(Integer.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectUsingQuestionMarkFlowableParameterListsTwoParametersPerQuery() {
        try (Database db = db()) {
            db.select("select score from person where name=? and score = ?") //
                    .parameterListStream(
                            Flowable.just(Arrays.asList("FRED", 21), Arrays.asList("JOSEPH", 34))) //
                    .getAs(Integer.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectUsingQuestionMarkWithPublicTestingDatabase() {
        try (Database db = Database.test()) {
            db //
                    .select("select score from person where name=?") //
                    .parameters("FRED", "JOSEPH") //
                    .getAs(Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectWithFetchSize() {
        try (Database db = db()) {
            db.select("select score from person order by name") //
                    .fetchSize(2) //
                    .getAs(Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34, 25) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectWithFetchSizeZero() {
        try (Database db = db()) {
            db.select("select score from person order by name") //
                    .fetchSize(0) //
                    .getAs(Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34, 25) //
                    .assertComplete();
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSelectWithFetchSizeNegative() {
        try (Database db = db()) {
            db.select("select score from person order by name") //
                    .fetchSize(-1);
        }
    }

    @Test
    public void testSelectUsingNonBlockingBuilder() {
        NonBlockingConnectionPool pool = Pools //
                .nonBlocking() //
                .connectionProvider(DatabaseCreator.connectionProvider()) //
                .maxIdleTime(1, TimeUnit.MINUTES) //
                .idleTimeBeforeHealthCheck(1, TimeUnit.MINUTES) //
                .connectionRetryInterval(1, TimeUnit.SECONDS) //
                .healthCheck(c -> c.prepareStatement("select 1").execute()) //
                .maxPoolSize(3) //
                .build();

        try (Database db = Database.from(pool)) {
            db.select("select score from person where name=?") //
                    .parameters("FRED", "JOSEPH") //
                    .getAs(Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectSpecifyingHealthCheck() {
        try (Database db = Database//
                .nonBlocking() //
                .connectionProvider(DatabaseCreator.connectionProvider()) //
                .maxIdleTime(1, TimeUnit.MINUTES) //
                .idleTimeBeforeHealthCheck(1, TimeUnit.MINUTES) //
                .connectionRetryInterval(1, TimeUnit.SECONDS) //
                .healthCheck(DatabaseType.H2) //
                .maxPoolSize(3) //
                .build()) {
            db.select("select score from person where name=?") //
                    .parameters("FRED", "JOSEPH") //
                    .getAs(Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34) //
                    .assertComplete();
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testCreateTestDatabaseWithZeroSizePoolThrows() {
        Database.test(0);
    }

    @Test
    public void testSelectSpecifyingHealthCheckAsSql() {
        final AtomicInteger success = new AtomicInteger();
        NonBlockingConnectionPool pool = Pools //
                .nonBlocking() //
                .connectionProvider(DatabaseCreator.connectionProvider()) //
                .maxIdleTime(1, TimeUnit.MINUTES) //
                .healthCheck(DatabaseType.H2) //
                .idleTimeBeforeHealthCheck(1, TimeUnit.MINUTES) //
                .connectionRetryInterval(1, TimeUnit.SECONDS) //
                .healthCheck("select 1") //
                .connectionListener(error -> {
                    if (error.isPresent()) {
                        success.set(Integer.MIN_VALUE);
                    } else {
                        success.incrementAndGet();
                    }
                }) //
                .maxPoolSize(3) //
                .build();

        try (Database db = Database.from(pool)) {
            db.select("select score from person where name=?") //
                    .parameters("FRED", "JOSEPH") //
                    .getAs(Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34) //
                    .assertComplete();
        }
        assertEquals(1, success.get());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testNonBlockingPoolWithTrampolineSchedulerThrows() {
        Pools.nonBlocking().scheduler(Schedulers.trampoline());
    }

    @Test(timeout = 40000)
    public void testSelectUsingNonBlockingBuilderConcurrencyTest()
            throws InterruptedException, TimeoutException {
        info();
        try {
            try (Database db = db(3)) {
                Scheduler scheduler = Schedulers.from(Executors.newFixedThreadPool(50));
                int n = 10000;
                CountDownLatch latch = new CountDownLatch(n);
                AtomicInteger count = new AtomicInteger();
                for (int i = 0; i < n; i++) {
                    db.select("select score from person where name=?") //
                            .parameters("FRED", "JOSEPH") //
                            .getAs(Integer.class) //
                            .subscribeOn(scheduler) //
                            .toList() //
                            .doOnSuccess(x -> {
                                if (!x.equals(Lists.newArrayList(21, 34))) {
                                    throw new RuntimeException("run broken");
                                } else {
                                    // log.debug(iCopy + " succeeded");
                                }
                            }) //
                            .doOnSuccess(x -> {
                                count.incrementAndGet();
                                latch.countDown();
                            }) //
                            .doOnError(x -> latch.countDown()) //
                            .subscribe();
                }
                if (!latch.await(20, TimeUnit.SECONDS)) {
                    throw new TimeoutException("timeout");
                }
                assertEquals(n, count.get());
            }
        } finally {
            debug();
        }
    }

    @Test(timeout = 5000)
    public void testSelectConcurrencyTest() throws InterruptedException, TimeoutException {
        debug();
        try {
            try (Database db = db(1)) {
                Scheduler scheduler = Schedulers.from(Executors.newFixedThreadPool(2));
                int n = 2;
                CountDownLatch latch = new CountDownLatch(n);
                AtomicInteger count = new AtomicInteger();
                for (int i = 0; i < n; i++) {
                    db.select("select score from person where name=?") //
                            .parameters("FRED", "JOSEPH") //
                            .getAs(Integer.class) //
                            .subscribeOn(scheduler) //
                            .toList() //
                            .doOnSuccess(x -> {
                                if (!x.equals(Lists.newArrayList(21, 34))) {
                                    throw new RuntimeException("run broken");
                                }
                            }) //
                            .doOnSuccess(x -> {
                                count.incrementAndGet();
                                latch.countDown();
                            }) //
                            .doOnError(x -> latch.countDown()) //
                            .subscribe();
                    log.info("submitted " + i);
                }
                if (!latch.await(5000, TimeUnit.SECONDS)) {
                    throw new TimeoutException("timeout");
                }
                assertEquals(n, count.get());
            }
        } finally {
            debug();
        }
    }

    @Test
    public void testDatabaseClose() {
        try (Database db = db()) {
            db.select("select score from person where name=?") //
                    .parameters("FRED", "JOSEPH") //
                    .getAs(Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues(21, 34) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectUsingName() {
        try (Database db = db()) {
            db //
                    .select("select score from person where name=:name") //
                    .parameter("name", "FRED") //
                    .parameter("name", "JOSEPH") //
                    .getAs(Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(21, 34) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectUsingNameNotGiven() {
        try (Database db = db()) {
            db //
                    .select("select score from person where name=:name and name<>:name2") //
                    .parameter("name", "FRED") //
                    .parameter("name", "JOSEPH") //
                    .getAs(Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertError(NamedParameterMissingException.class) //
                    .assertNoValues();
        }
    }

    @Test
    public void testSelectUsingParameterNameNullNameWhenSqlHasNoNames() {
        db() //
                .select("select score from person where name=?") //
                .parameter(Parameter.create("name", "FRED")) //
                .getAs(Integer.class) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertError(NamedParameterFoundButSqlDoesNotHaveNamesException.class) //
                .assertNoValues();
    }

    @Test
    public void testUpdateWithNullNamedParameter() {
        try (Database db = db()) {
            db //
                    .update("update person set date_of_birth = :dob") //
                    .parameter(Parameter.create("dob", null)) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(3) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateWithNullParameter() {
        try (Database db = db()) {
            db //
                    .update("update person set date_of_birth = ?") //
                    .parameter(null) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(3) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateWithNullStreamParameter() {
        try (Database db = db()) {
            db //
                    .update("update person set date_of_birth = ?") //
                    .parameterStream(Flowable.just(Parameter.NULL)) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(3) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateWithTestDatabaseForReadme() {
        try (Database db = db()) {
            db //
                    .update("update person set date_of_birth = ?") //
                    .parameterStream(Flowable.just(Parameter.NULL)) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(3) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateClobWithNull() {
        try (Database db = db()) {
            insertNullClob(db);
            db //
                    .update("update person_clob set document = :doc") //
                    .parameter("doc", Database.NULL_CLOB) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateClobWithClob() throws SQLException {
        try (Database db = db()) {
            Clob clob = new JDBCClobFile(new File("src/test/resources/big.txt"));
            insertNullClob(db);
            db //
                    .update("update person_clob set document = :doc") //
                    .parameter("doc", clob) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateClobWithReader() throws FileNotFoundException {
        try (Database db = db()) {
            Reader reader = new FileReader(new File("src/test/resources/big.txt"));
            insertNullClob(db);
            db //
                    .update("update person_clob set document = :doc") //
                    .parameter("doc", reader) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateBlobWithBlob() throws SQLException {
        try (Database db = db()) {
            Blob blob = new JDBCBlobFile(new File("src/test/resources/big.txt"));
            insertPersonBlob(db);
            db //
                    .update("update person_blob set document = :doc") //
                    .parameter("doc", blob) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateBlobWithNull() {
        try (Database db = db()) {
            insertPersonBlob(db);
            db //
                    .update("update person_blob set document = :doc") //
                    .parameter("doc", Database.NULL_BLOB) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
        }
    }

    @Test(expected = NullPointerException.class)
    public void testSelectUsingNullNameInParameter() {
        try (Database db = db()) {
            db //
                    .select("select score from person where name=:name") //
                    .parameter(null, "FRED"); //
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSelectUsingNameDoesNotExist() {
        try (Database db = db()) {
            db //
                    .select("select score from person where name=:name") //
                    .parameters("nam", "FRED");
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSelectUsingNameWithoutSpecifyingNameThrowsImmediately() {
        try (Database db = db()) {
            db //
                    .select("select score from person where name=:name") //
                    .parameters("FRED", "JOSEPH");
        }
    }

    @Test
    public void testSelectTransacted() {
        try (Database db = db()) {
            db //
                    .select("select score from person where name=?") //
                    .parameters("FRED", "JOSEPH") //
                    .transacted() //
                    .getAs(Integer.class) //
                    .doOnNext(tx -> log
                            .debug(tx.isComplete() ? "complete" : String.valueOf(tx.value()))) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(3) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectAutomappedAnnotatedTransacted() {
        try (Database db = db()) {
            db //
                    .select(Person10.class) //
                    .transacted() //
                    .valuesOnly() //
                    .get().test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(3) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectAutomappedTransactedValuesOnly() {
        try (Database db = db()) {
            db //
                    .select("select name, score from person") //
                    .transacted() //
                    .valuesOnly() //
                    .autoMap(Person2.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(3) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectAutomappedTransacted() {
        try (Database db = db()) {
            db //
                    .select("select name, score from person") //
                    .transacted() //
                    .autoMap(Person2.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(4) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectTransactedTuple2() {
        try (Database db = db()) {
            Tx<Tuple2<String, Integer>> t = db //
                    .select("select name, score from person where name=?") //
                    .parameters("FRED") //
                    .transacted() //
                    .getAs(String.class, Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(2) //
                    .assertComplete() //
                    .values().get(0);
            assertEquals("FRED", t.value()._1());
            assertEquals(21, (int) t.value()._2());
        }
    }

    @Test
    public void testSelectTransactedTuple3() {
        try (Database db = db()) {
            Tx<Tuple3<String, Integer, String>> t = db //
                    .select("select name, score, name from person where name=?") //
                    .parameters("FRED") //
                    .transacted() //
                    .getAs(String.class, Integer.class, String.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(2) //
                    .assertComplete() //
                    .values().get(0);
            assertEquals("FRED", t.value()._1());
            assertEquals(21, (int) t.value()._2());
            assertEquals("FRED", t.value()._3());
        }
    }

    @Test
    public void testSelectTransactedTuple4() {
        try (Database db = db()) {
            Tx<Tuple4<String, Integer, String, Integer>> t = db //
                    .select("select name, score, name, score from person where name=?") //
                    .parameters("FRED") //
                    .transacted() //
                    .getAs(String.class, Integer.class, String.class, Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(2) //
                    .assertComplete() //
                    .values().get(0);
            assertEquals("FRED", t.value()._1());
            assertEquals(21, (int) t.value()._2());
            assertEquals("FRED", t.value()._3());
            assertEquals(21, (int) t.value()._4());
        }
    }

    @Test
    public void testSelectTransactedTuple5() {
        try (Database db = db()) {
            Tx<Tuple5<String, Integer, String, Integer, String>> t = db //
                    .select("select name, score, name, score, name from person where name=?") //
                    .parameters("FRED") //
                    .transacted() //
                    .getAs(String.class, Integer.class, String.class, Integer.class, String.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(2) //
                    .assertComplete() //
                    .values().get(0);
            assertEquals("FRED", t.value()._1());
            assertEquals(21, (int) t.value()._2());
            assertEquals("FRED", t.value()._3());
            assertEquals(21, (int) t.value()._4());
            assertEquals("FRED", t.value()._5());
        }
    }

    @Test
    public void testSelectTransactedTuple6() {
        try (Database db = db()) {
            Tx<Tuple6<String, Integer, String, Integer, String, Integer>> t = db //
                    .select("select name, score, name, score, name, score from person where name=?") //
                    .parameters("FRED") //
                    .transacted() //
                    .getAs(String.class, Integer.class, String.class, Integer.class, String.class,
                            Integer.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(2) //
                    .assertComplete() //
                    .values().get(0);
            assertEquals("FRED", t.value()._1());
            assertEquals(21, (int) t.value()._2());
            assertEquals("FRED", t.value()._3());
            assertEquals(21, (int) t.value()._4());
            assertEquals("FRED", t.value()._5());
            assertEquals(21, (int) t.value()._6());
        }
    }

    @Test
    public void testSelectTransactedTuple7() {
        try (Database db = db()) {
            Tx<Tuple7<String, Integer, String, Integer, String, Integer, String>> t = db //
                    .select("select name, score, name, score, name, score, name from person where name=?") //
                    .parameters("FRED") //
                    .transacted() //
                    .getAs(String.class, Integer.class, String.class, Integer.class, String.class,
                            Integer.class, String.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(2) //
                    .assertComplete() //
                    .values().get(0);
            assertEquals("FRED", t.value()._1());
            assertEquals(21, (int) t.value()._2());
            assertEquals("FRED", t.value()._3());
            assertEquals(21, (int) t.value()._4());
            assertEquals("FRED", t.value()._5());
            assertEquals(21, (int) t.value()._6());
            assertEquals("FRED", t.value()._7());
        }
    }

    @Test
    public void testSelectTransactedTupleN() {
        try (Database db = db()) {
            List<Tx<TupleN<Object>>> list = db //
                    .select("select name, score from person where name=?") //
                    .parameters("FRED") //
                    .transacted() //
                    .getTupleN() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(2) //
                    .assertComplete() //
                    .values();
            assertEquals("FRED", list.get(0).value().values().get(0));
            assertEquals(21, (int) list.get(0).value().values().get(1));
            assertTrue(list.get(1).isComplete());
            assertEquals(2, list.size());
        }
    }

    @Test
    public void testSelectTransactedCount() {
        try (Database db = db()) {
            db //
                    .select("select name, score, name, score, name, score, name from person where name=?") //
                    .parameters("FRED") //
                    .transacted() //
                    .count() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(1) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectTransactedGetAs() {
        try (Database db = db()) {
            db //
                    .select("select name from person") //
                    .transacted() //
                    .getAs(String.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(4) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectTransactedGetAsOptional() {
        try (Database db = db()) {
            List<Tx<Optional<String>>> list = db //
                    .select("select name from person where name=?") //
                    .parameters("FRED") //
                    .transacted() //
                    .getAsOptional(String.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(2) //
                    .assertComplete() //
                    .values();
            assertTrue(list.get(0).isValue());
            assertEquals("FRED", list.get(0).value().get());
            assertTrue(list.get(1).isComplete());
        }
    }

    @Test
    public void testDatabaseFrom() {
        Database.from(DatabaseCreator.nextUrl(), 3) //
                .select("select name from person") //
                .getAs(String.class) //
                .count() //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertError(JdbcSQLException.class);
    }

    @Test
    public void testSelectTransactedChained() throws Exception {
        try (Database db = db()) {
            db //
                    .select("select score from person where name=?") //
                    .parameters("FRED", "JOSEPH") //
                    .transacted() //
                    .transactedValuesOnly() //
                    .getAs(Integer.class) //
                    .doOnNext(tx -> log
                            .debug(tx.isComplete() ? "complete" : String.valueOf(tx.value())))//
                    .flatMap(tx -> tx //
                            .select("select name from person where score = ?") //
                            .parameter(tx.value()) //
                            .valuesOnly() //
                            .getAs(String.class)) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues("FRED", "JOSEPH") //
                    .assertComplete();
        }
    }

    @Test
    public void databaseIsAutoCloseable() {
        try (Database db = db()) {
            log.debug(db.toString());
        }
    }

    private static void println(Object o) {
        log.debug("{}", o);
    }

    @Test
    public void testSelectChained() {
        try (Database db = db(1)) {
            // we can do this with 1 connection only!
            db.select("select score from person where name=?") //
                    .parameters("FRED", "JOSEPH") //
                    .getAs(Integer.class) //
                    .doOnNext(DatabaseTest::println) //
                    .concatMap(score -> {
                        log.info("score={}", score);
                        return db //
                                .select("select name from person where score = ?") //
                                .parameter(score) //
                                .getAs(String.class) //
                                .doOnComplete(
                                        () -> log.info("completed select where score=" + score));
                    }) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoErrors() //
                    .assertValues("FRED", "JOSEPH") //
                    .assertComplete(); //
        }
    }

    @Test
    @SuppressFBWarnings
    public void testReadMeFragment1() {
        try (Database db = db()) {
            db.select("select name from person") //
                    .getAs(String.class) //
                    .forEach(DatabaseTest::println);
        }
    }

    @Test
    public void testReadMeFragmentColumnDoesNotExistEmitsSqlSyntaxErrorException() {
        try (Database db = Database.test()) {
            db.select("select nam from person") //
                    .getAs(String.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoValues() //
                    .assertError(SQLSyntaxErrorException.class);
        }
    }

    @Test
    public void testReadMeFragmentDerbyHealthCheck() {
        try (Database db = Database.test()) {
            db.select("select 'a' from sysibm.sysdummy1") //
                    .getAs(String.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue("a") //
                    .assertComplete();
        }
    }

    @Test
    @SuppressFBWarnings
    public void testTupleSupport() {
        try (Database db = db()) {
            db.select("select name, score from person") //
                    .getAs(String.class, Integer.class) //
                    .forEach(DatabaseTest::println);
        }
    }

    @Test
    public void testDelayedCallsAreNonBlocking() throws InterruptedException {
        List<String> list = new CopyOnWriteArrayList<String>();
        try (Database db = db(1)) { //
            db.select("select score from person where name=?") //
                    .parameter("FRED") //
                    .getAs(Integer.class) //
                    .doOnNext(x -> Thread.sleep(1000)) //
                    .subscribeOn(Schedulers.io()) //
                    .subscribe();
            Thread.sleep(100);
            CountDownLatch latch = new CountDownLatch(1);
            db.select("select score from person where name=?") //
                    .parameter("FRED") //
                    .getAs(Integer.class) //
                    .doOnNext(x -> list.add("emitted")) //
                    .doOnNext(x -> log.debug("emitted on " + Thread.currentThread().getName())) //
                    .doOnNext(x -> latch.countDown()) //
                    .subscribe();
            list.add("subscribed");
            assertTrue(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS));
            assertEquals(Arrays.asList("subscribed", "emitted"), list);
        }
    }

    @Test
    public void testAutoMapToInterface() {
        try (Database db = db()) {
            db //
                    .select("select name from person") //
                    .autoMap(Person.class) //
                    .map(p -> p.name()) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(3) //
                    .assertComplete();
        }
    }

    @Test
    public void testAutoMapToInterfaceWithoutAnnotationstsError() {
        try (Database db = db()) {
            db //
                    .select("select name from person") //
                    .autoMap(PersonNoAnnotation.class) //
                    .map(p -> p.name()) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoValues() //
                    .assertError(AnnotationsNotFoundException.class);
        }
    }

    @Test
    public void testAutoMapToInterfaceWithTwoMethods() {
        try (Database db = db()) {
            db //
                    .select("select name, score from person order by name") //
                    .autoMap(Person2.class) //
                    .firstOrError() //
                    .map(Person2::score) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(21) //
                    .assertComplete();
        }
    }

    @Test
    public void testAutoMapToInterfaceWithExplicitColumnName() {
        try (Database db = db()) {
            db //
                    .select("select name, score from person order by name") //
                    .autoMap(Person3.class) //
                    .firstOrError() //
                    .map(Person3::examScore) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(21) //
                    .assertComplete();
        }
    }

    @Test
    public void testAutoMapToInterfaceWithExplicitColumnNameThatDoesNotExist() {
        try (Database db = db()) {
            db //
                    .select("select name, score from person order by name") //
                    .autoMap(Person4.class) //
                    .firstOrError() //
                    .map(Person4::examScore) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoValues() //
                    .assertError(ColumnNotFoundException.class);
        }
    }

    @Test
    public void testAutoMapToInterfaceWithIndex() {
        try (Database db = db()) {
            db //
                    .select("select name, score from person order by name") //
                    .autoMap(Person5.class) //
                    .firstOrError() //
                    .map(Person5::examScore) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(21) //
                    .assertComplete();
        }
    }

    @Test
    public void testAutoMapToInterfaceWithIndexTooLarge() {
        try (Database db = db()) {
            db //
                    .select("select name, score from person order by name") //
                    .autoMap(Person6.class) //
                    .firstOrError() //
                    .map(Person6::examScore) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoValues() //
                    .assertError(ColumnIndexOutOfRangeException.class);
        }
    }

    @Test
    public void testAutoMapToInterfaceWithIndexTooSmall() {
        try (Database db = db()) {
            db //
                    .select("select name, score from person order by name") //
                    .autoMap(Person7.class) //
                    .firstOrError() //
                    .map(Person7::examScore) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoValues() //
                    .assertError(ColumnIndexOutOfRangeException.class);
        }
    }

    @Test
    public void testAutoMapWithUnmappableColumnType() {
        try (Database db = db()) {
            db //
                    .select("select name from person order by name") //
                    .autoMap(Person8.class) //
                    .map(p -> p.name()) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoValues() //
                    .assertError(ClassCastException.class);
        }
    }

    @Test
    public void testAutoMapWithMixIndexAndName() {
        try (Database db = db()) {
            db //
                    .select("select name, score from person order by name") //
                    .autoMap(Person9.class) //
                    .firstOrError() //
                    .map(Person9::score) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(21) //
                    .assertComplete();
        }
    }

    @Test
    public void testAutoMapWithQueryInAnnotation() {
        try (Database db = db()) {
            db.select(Person10.class) //
                    .get() //
                    .firstOrError() //
                    .map(Person10::score) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(21) //
                    .assertComplete();
        }
    }

    @Test
    @Ignore
    public void testAutoMapForReadMe() {
        try (Database db = Database.test()) {
            db.select(Person10.class) //
                    .get(Person10::name) //
                    .blockingForEach(DatabaseTest::println);
        }
    }

    @Test(expected = QueryAnnotationMissingException.class)
    public void testAutoMapWithoutQueryInAnnotation() {
        try (Database db = db()) {
            db.select(Person.class);
        }
    }

    @Test
    public void testSelectWithoutWhereClause() {
        try (Database db = db()) {
            Assert.assertEquals(3, (long) db.select("select name from person") //
                    .count() //
                    .blockingGet());
        }
    }

    @Test
    public void testTuple3() {
        try (Database db = db()) {
            db //
                    .select("select name, score, name from person order by name") //
                    .getAs(String.class, Integer.class, String.class) //
                    .firstOrError() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertComplete() //
                    .assertValue(Tuple3.create("FRED", 21, "FRED")); //
        }
    }

    @Test
    public void testTuple4() {
        try (Database db = db()) {
            db //
                    .select("select name, score, name, score from person order by name") //
                    .getAs(String.class, Integer.class, String.class, Integer.class) //
                    .firstOrError() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertComplete() //
                    .assertValue(Tuple4.create("FRED", 21, "FRED", 21)); //
        }
    }

    @Test
    public void testTuple5() {
        try (Database db = db()) {
            db //
                    .select("select name, score, name, score, name from person order by name") //
                    .getAs(String.class, Integer.class, String.class, Integer.class, String.class) //
                    .firstOrError() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertComplete().assertValue(Tuple5.create("FRED", 21, "FRED", 21, "FRED")); //
        }
    }

    @Test
    public void testTuple6() {
        try (Database db = db()) {
            db //
                    .select("select name, score, name, score, name, score from person order by name") //
                    .getAs(String.class, Integer.class, String.class, Integer.class, String.class,
                            Integer.class) //
                    .firstOrError() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertComplete()
                    .assertValue(Tuple6.create("FRED", 21, "FRED", 21, "FRED", 21)); //
        }
    }

    @Test
    public void testTuple7() {
        try (Database db = db()) {
            db //
                    .select("select name, score, name, score, name, score, name from person order by name") //
                    .getAs(String.class, Integer.class, String.class, Integer.class, String.class,
                            Integer.class, String.class) //
                    .firstOrError() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertComplete()
                    .assertValue(Tuple7.create("FRED", 21, "FRED", 21, "FRED", 21, "FRED")); //
        }
    }

    @Test
    public void testTupleN() {
        try (Database db = db()) {
            db //
                    .select("select name, score, name from person order by name") //
                    .getTupleN() //
                    .firstOrError().test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertComplete() //
                    .assertValue(TupleN.create("FRED", 21, "FRED")); //
        }
    }

    @Test
    public void testTupleNWithClass() {
        try (Database db = db()) {
            db //
                    .select("select score a, score b from person order by name") //
                    .getTupleN(Integer.class) //
                    .firstOrError() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertComplete() //
                    .assertValue(TupleN.create(21, 21)); //
        }
    }

    @Test
    public void testTupleNWithClassInTransaction() {
        try (Database db = db()) {
            db //
                    .select("select score a, score b from person order by name") //
                    .transactedValuesOnly() //
                    .getTupleN(Integer.class) //
                    .map(x -> x.value()) //
                    .firstOrError() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertComplete() //
                    .assertValue(TupleN.create(21, 21)); //
        }
    }

    @Test
    public void testHealthCheck() throws InterruptedException {
        AtomicBoolean once = new AtomicBoolean(true);
        testHealthCheck(c -> {
            log.debug("doing health check");
            return !once.compareAndSet(true, false);
        });
    }

    @Test
    public void testHealthCheckThatThrows() throws InterruptedException {
        AtomicBoolean once = new AtomicBoolean(true);
        testHealthCheck(c -> {
            log.debug("doing health check");
            if (!once.compareAndSet(true, false))
                return true;
            else
                throw new RuntimeException("health check failed");
        });
    }

    @Test
    public void testUpdateOneRow() {
        try (Database db = db()) {
            db.update("update person set score=20 where name='FRED'") //
                    .counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateThreeRows() {
        try (Database db = db()) {
            db.update("update person set score=20") //
                    .counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(3) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateWithParameter() {
        try (Database db = db()) {
            db.update("update person set score=20 where name=?") //
                    .parameter("FRED").counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateWithParameterTwoRuns() {
        try (Database db = db()) {
            db.update("update person set score=20 where name=?") //
                    .parameters("FRED", "JOSEPH").counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(1, 1) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateAllWithParameterFourRuns() {
        try (Database db = db()) {
            db.update("update person set score=?") //
                    .parameters(1, 2, 3, 4) //
                    .counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(3, 3, 3, 3) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateWithBatchSize2() {
        try (Database db = db()) {
            db.update("update person set score=?") //
                    .batchSize(2) //
                    .parameters(1, 2, 3, 4) //
                    .counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(3, 3, 3, 3) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateWithBatchSize3GreaterThanNumRecords() {
        try (Database db = db()) {
            db.update("update person set score=?") //
                    .batchSize(3) //
                    .parameters(1, 2, 3, 4) //
                    .counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(3, 3, 3, 3) //
                    .assertComplete();
        }
    }

    @Test
    public void testInsert() {
        try (Database db = db()) {
            db.update("insert into person(name, score) values(?,?)") //
                    .parameters("DAVE", 12, "ANNE", 18) //
                    .counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(1, 1) //
                    .assertComplete();
            List<Tuple2<String, Integer>> list = db.select("select name, score from person") //
                    .getAs(String.class, Integer.class) //
                    .toList() //
                    .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .blockingGet();
            assertTrue(list.contains(Tuple2.create("DAVE", 12)));
            assertTrue(list.contains(Tuple2.create("ANNE", 18)));
        }
    }

    @Test
    public void testReturnGeneratedKeys() {
        try (Database db = db()) {
            // note is a table with auto increment
            db.update("insert into note(text) values(?)") //
                    .parameters("HI", "THERE") //
                    .returnGeneratedKeys() //
                    .getAs(Integer.class)//
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(1, 2) //
                    .assertComplete();

            db.update("insert into note(text) values(?)") //
                    .parameters("ME", "TOO") //
                    .returnGeneratedKeys() //
                    .getAs(Integer.class)//
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(3, 4) //
                    .assertComplete();
        }
    }

    @Test
    public void testReturnGeneratedKeysDerby() {
        Database db = DatabaseCreator.createDerby(1);

        // note is a table with auto increment
        db.update("insert into note2(text) values(?)") //
                .parameters("HI", "THERE") //
                .returnGeneratedKeys() //
                .getAs(Integer.class)//
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors().assertValues(1, 3) //
                .assertComplete();

        db.update("insert into note2(text) values(?)") //
                .parameters("ME", "TOO") //
                .returnGeneratedKeys() //
                .getAs(Integer.class)//
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValues(5, 7) //
                .assertComplete();
    }

    @Test(expected = IllegalArgumentException.class)
    public void testReturnGeneratedKeysWithBatchSizeShouldThrow() {
        try (Database db = db()) {
            // note is a table with auto increment
            db.update("insert into note(text) values(?)") //
                    .parameters("HI", "THERE") //
                    .batchSize(2) //
                    .returnGeneratedKeys();
        }
    }

    @Test
    public void testTransactedReturnGeneratedKeys() {
        try (Database db = db()) {
            // note is a table with auto increment
            db.update("insert into note(text) values(?)") //
                    .parameters("HI", "THERE") //
                    .transacted() //
                    .returnGeneratedKeys() //
                    .valuesOnly() //
                    .getAs(Integer.class)//
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(1, 2) //
                    .assertComplete();

            db.update("insert into note(text) values(?)") //
                    .parameters("ME", "TOO") //
                    .transacted() //
                    .returnGeneratedKeys() //
                    .valuesOnly() //
                    .getAs(Integer.class)//
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(3, 4) //
                    .assertComplete();
        }
    }

    @Test
    public void testTransactedReturnGeneratedKeys2() {
        try (Database db = db()) {
            // note is a table with auto increment
            Flowable<Integer> a = db.update("insert into note(text) values(?)") //
                    .parameters("HI", "THERE") //
                    .transacted() //
                    .returnGeneratedKeys() //
                    .valuesOnly() //
                    .getAs(Integer.class);

            db.update("insert into note(text) values(?)") //
                    .parameters("ME", "TOO") //
                    .transacted() //
                    .returnGeneratedKeys() //
                    .valuesOnly() //
                    .getAs(Integer.class)//
                    .startWith(a) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(1, 2, 3, 4) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateWithinTransaction() {
        try (Database db = db()) {
            db //
                    .select("select name from person") //
                    .transactedValuesOnly() //
                    .getAs(String.class) //
                    .doOnNext(DatabaseTest::println) //
                    .flatMap(tx -> tx//
                            .update("update person set score=-1 where name=:name") //
                            .batchSize(1) //
                            .parameter("name", tx.value()) //
                            .valuesOnly() //
                            .counts()) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(1, 1, 1) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectDependsOnFlowable() {
        try (Database db = db()) {
            Flowable<Integer> a = db.update("update person set score=100 where name=?") //
                    .parameter("FRED") //
                    .counts();
            db.select("select score from person where name=?") //
                    .parameter("FRED") //
                    .dependsOn(a) //
                    .getAs(Integer.class)//
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(100) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectDependsOnObservable() {
        try (Database db = db()) {
            Observable<Integer> a = db.update("update person set score=100 where name=?") //
                    .parameter("FRED") //
                    .counts().toObservable();
            db.select("select score from person where name=?") //
                    .parameter("FRED") //
                    .dependsOn(a) //
                    .getAs(Integer.class)//
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(100) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectDependsOnOnSingle() {
        try (Database db = db()) {
            Single<Long> a = db.update("update person set score=100 where name=?") //
                    .parameter("FRED") //
                    .counts().count();
            db.select("select score from person where name=?") //
                    .parameter("FRED") //
                    .dependsOn(a) //
                    .getAs(Integer.class)//
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(100) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectDependsOnCompletable() {
        try (Database db = db()) {
            Completable a = db.update("update person set score=100 where name=?") //
                    .parameter("FRED") //
                    .counts().ignoreElements();
            db.select("select score from person where name=?") //
                    .parameter("FRED") //
                    .dependsOn(a) //
                    .getAs(Integer.class)//
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(100) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateWithinTransactionBatchSize0() {
        try (Database db = db()) {
            db //
                    .select("select name from person") //
                    .transactedValuesOnly() //
                    .getAs(String.class) //
                    .doOnNext(DatabaseTest::println) //
                    .flatMap(tx -> tx//
                            .update("update person set score=-1 where name=:name") //
                            .batchSize(0) //
                            .parameter("name", tx.value()) //
                            .valuesOnly() //
                            .counts()) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(1, 1, 1) //
                    .assertComplete();
        }
    }

    private static void info() {
        LogManager.getRootLogger().setLevel(Level.INFO);
    }

    private static void debug() {
        LogManager.getRootLogger().setLevel(Level.INFO);
    }

    @Test
    public void testCreateBig() {
        info();
        big(5).select("select count(*) from person") //
                .getAs(Integer.class) //
                .test().awaitDone(20, TimeUnit.SECONDS) //
                .assertValue(5163) //
                .assertComplete();
        debug();
    }

    @Test
    public void testTxWithBig() {
        info();
        big(1) //
                .select("select name from person") //
                .transactedValuesOnly() //
                .getAs(String.class) //
                .flatMap(tx -> tx//
                        .update("update person set score=-1 where name=:name") //
                        .batchSize(1) //
                        .parameter("name", tx.value()) //
                        .valuesOnly() //
                        .counts()) //
                .count() //
                .test().awaitDone(20, TimeUnit.SECONDS) //
                .assertValue((long) NAMES_COUNT_BIG) //
                .assertComplete();
        debug();
    }

    @Test
    public void testTxWithBigInputBatchSize2000() {
        info();
        big(1) //
                .select("select name from person") //
                .transactedValuesOnly() //
                .getAs(String.class) //
                .flatMap(tx -> tx//
                        .update("update person set score=-1 where name=:name") //
                        .batchSize(2000) //
                        .parameter("name", tx.value()) //
                        .valuesOnly() //
                        .counts()) //
                .count() //
                .test().awaitDone(20, TimeUnit.SECONDS) //
                .assertValue((long) NAMES_COUNT_BIG) //
                .assertComplete();
        debug();
    }

    @Test
    public void testInsertNullClobAndReadClobAsString() {
        try (Database db = db()) {
            insertNullClob(db);
            db.select("select document from person_clob where name='FRED'") //
                    .getAsOptional(String.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(Optional.<String>empty()) //
                    .assertComplete();
        }
    }
    
    @Test
    public void testAutomapClobIssue32() {
        try (Database db = db()) {
            db.update("insert into person_clob(name, document) values(?, ? )") //
                    .parameters("fred", "hello there") //
                    .complete() //
                    .blockingAwait(TIMEOUT_SECONDS, TimeUnit.SECONDS);
            db.select("select * from person_clob") //
                    .autoMap(PersonClob.class) //
                    .map(pc -> {
                        System.out.println(pc);
                        return pc.document();
                    }) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValueCount(1) //
                    .assertComplete();
        }
    }
    
    private static interface PersonClob {
        @Column
        String name();
        @Column
        String document();
    }

    private static void insertNullClob(Database db) {
        db.update("insert into person_clob(name,document) values(?,?)") //
                .parameters("FRED", Database.NULL_CLOB) //
                .counts() //
                .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValue(1) //
                .assertComplete();
    }

    @Test
    public void testClobMethod() {
        assertEquals(Database.NULL_CLOB, Database.clob(null));
    }

    @Test
    public void testBlobMethod() {
        assertEquals(Database.NULL_BLOB, Database.blob(null));
    }

    @Test
    public void testClobMethodPresent() {
        assertEquals("a", Database.clob("a"));
    }

    @Test
    public void testBlobMethodPresent() {
        byte[] b = new byte[1];
        assertEquals(b, Database.blob(b));
    }

    @Test
    public void testDateOfBirthNullableForReadMe() {
        Database.test() //
                .select("select date_of_birth from person where name='FRED'") //
                .getAsOptional(Instant.class) //
                .blockingForEach(DatabaseTest::println);
    }

    @Test
    public void testInsertNullClobAndReadClobAsTuple2() {
        try (Database db = db()) {
            insertNullClob(db);
            db.select("select document, document from person_clob where name='FRED'") //
                    .getAs(String.class, String.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(Tuple2.create(null, null)) //
                    .assertComplete();
        }
    }

    @Test
    public void testInsertClobAndReadClobAsString() {
        try (Database db = db()) {
            db.update("insert into person_clob(name,document) values(?,?)") //
                    .parameters("FRED", "some text here") //
                    .counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
            db.select("select document from person_clob where name='FRED'") //
                    .getAs(String.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) // .assertValue("some
                                                                         // text
                                                                         // here")
                                                                         // //
                    .assertComplete();
        }
    }

    @Test
    public void testInsertClobAndReadClobUsingReader() {
        try (Database db = db()) {
            db.update("insert into person_clob(name,document) values(?,?)") //
                    .parameters("FRED", "some text here") //
                    .counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
            db.select("select document from person_clob where name='FRED'") //
                    .getAs(Reader.class) //
                    .map(r -> read(r)).test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue("some text here") //
                    .assertComplete();
        }
    }

    @Test
    public void testInsertBlobAndReadBlobAsByteArray() {
        try (Database db = db()) {
            insertPersonBlob(db);
            db.select("select document from person_blob where name='FRED'") //
                    .getAs(byte[].class) //
                    .map(b -> new String(b)) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue("some text here") //
                    .assertComplete();
        }
    }

    private static void insertPersonBlob(Database db) {
        byte[] bytes = "some text here".getBytes();
        db.update("insert into person_blob(name,document) values(?,?)") //
                .parameters("FRED", bytes) //
                .counts() //
                .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValue(1) //
                .assertComplete();
    }

    @Test
    public void testInsertBlobAndReadBlobAsInputStream() {
        try (Database db = db()) {
            byte[] bytes = "some text here".getBytes();
            db.update("insert into person_blob(name,document) values(?,?)") //
                    .parameters("FRED", new ByteArrayInputStream(bytes)) //
                    .counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
            db.select("select document from person_blob where name='FRED'") //
                    .getAs(InputStream.class) //
                    .map(is -> read(is)) //
                    .map(b -> new String(b)) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue("some text here") //
                    .assertComplete();
        }
    }

    private static String read(Reader reader) throws IOException {
        StringBuffer s = new StringBuffer();
        char[] ch = new char[128];
        int n = 0;
        while ((n = reader.read(ch)) != -1) {
            s.append(ch, 0, n);
        }
        reader.close();
        return s.toString();
    }

    private static byte[] read(InputStream is) throws IOException {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        byte[] b = new byte[128];
        int n = 0;
        while ((n = is.read(b)) != -1) {
            bytes.write(b, 0, n);
        }
        is.close();
        return bytes.toByteArray();
    }

    private void testHealthCheck(Predicate<Connection> healthy) throws InterruptedException {
        TestScheduler scheduler = new TestScheduler();

        NonBlockingConnectionPool pool = Pools //
                .nonBlocking() //
                .connectionProvider(DatabaseCreator.connectionProvider()) //
                .maxIdleTime(10, TimeUnit.MINUTES) //
                .idleTimeBeforeHealthCheck(0, TimeUnit.MINUTES) //
                .healthCheck(healthy) //
                .scheduler(scheduler) //
                .maxPoolSize(1) //
                .build();

        try (Database db = Database.from(pool)) {
            TestSubscriber<Integer> ts0 = db.select( //
                    "select score from person where name=?") //
                    .parameter("FRED") //
                    .getAs(Integer.class) //
                    .test();
            ts0.assertValueCount(0) //
                    .assertNotComplete();
            scheduler.advanceTimeBy(1, TimeUnit.MINUTES);
            ts0.assertValueCount(1) //
                    .assertComplete();
            TestSubscriber<Integer> ts = db.select( //
                    "select score from person where name=?") //
                    .parameter("FRED") //
                    .getAs(Integer.class) //
                    .test() //
                    .assertValueCount(0);
            log.debug("done2");
            scheduler.advanceTimeBy(1, TimeUnit.MINUTES);
            Thread.sleep(200);
            ts.assertValueCount(1);
            Thread.sleep(200);
            ts.assertValue(21) //
                    .assertComplete();
        }
    }

    @Test
    public void testShutdownBeforeUse() {
        NonBlockingConnectionPool pool = Pools //
                .nonBlocking() //
                .connectionProvider(DatabaseCreator.connectionProvider()) //
                .scheduler(Schedulers.io()) //
                .maxPoolSize(1) //
                .build();
        pool.close();
        Database.from(pool) //
                .select("select score from person where name=?") //
                .parameter("FRED") //
                .getAs(Integer.class) //
                .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoValues() //
                .assertError(PoolClosedException.class);
    }

    @Test
    public void testFewerColumnsMappedThanAvailable() {
        try (Database db = db()) {
            db.select("select name, score from person where name='FRED'") //
                    .getAs(String.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues("FRED") //
                    .assertComplete();
        }
    }

    @Test
    public void testMoreColumnsMappedThanAvailable() {
        try (Database db = db()) {
            db //
                    .select("select name, score from person where name='FRED'") //
                    .getAs(String.class, Integer.class, String.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertNoValues() //
                    .assertError(MoreColumnsRequestedThanExistException.class);
        }
    }

    @Test
    public void testSelectTimestamp() {
        try (Database db = db()) {
            db //
                    .select("select registered from person where name='FRED'") //
                    .getAs(Long.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(FRED_REGISTERED_TIME) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectTimestampAsDate() {
        try (Database db = db()) {
            db //
                    .select("select registered from person where name='FRED'") //
                    .getAs(Date.class) //
                    .map(d -> d.getTime()) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(FRED_REGISTERED_TIME) //
                    .assertComplete();
        }
    }

    @Test
    public void testSelectTimestampAsInstant() {
        try (Database db = db()) {
            db //
                    .select("select registered from person where name='FRED'") //
                    .getAs(Instant.class) //
                    .map(d -> d.toEpochMilli()) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(FRED_REGISTERED_TIME) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateCalendarParameter() {
        Calendar c = GregorianCalendar.from(ZonedDateTime.parse("2017-03-25T15:37Z"));
        try (Database db = db()) {
            db.update("update person set registered=?") //
                    .parameter(c) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(3) //
                    .assertComplete();
            db.select("select registered from person") //
                    .getAs(Long.class) //
                    .firstOrError() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(c.getTimeInMillis()) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateTimeParameter() {
        try (Database db = db()) {
            Time t = new Time(1234);
            db.update("update person set registered=?") //
                    .parameter(t) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(3) //
                    .assertComplete();
            db.select("select registered from person") //
                    .getAs(Long.class) //
                    .firstOrError() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1234L) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateTimestampParameter() {
        try (Database db = db()) {
            Timestamp t = new Timestamp(1234);
            db.update("update person set registered=?") //
                    .parameter(t) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(3) //
                    .assertComplete();
            db.select("select registered from person") //
                    .getAs(Long.class) //
                    .firstOrError() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1234L) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateSqlDateParameter() {
        try (Database db = db()) {
            java.sql.Date t = new java.sql.Date(1234);

            db.update("update person set registered=?") //
                    .parameter(t) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(3) //
                    .assertComplete();
            db.select("select registered from person") //
                    .getAs(Long.class) //
                    .firstOrError() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    // TODO make a more accurate comparison using the current TZ
                    .assertValue(x -> Math.abs(x - 1234) <= TimeUnit.HOURS.toMillis(24)) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateUtilDateParameter() {
        try (Database db = db()) {
            Date d = new Date(1234);
            db.update("update person set registered=?") //
                    .parameter(d) //
                    .counts() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(3) //
                    .assertComplete();
            db.select("select registered from person") //
                    .getAs(Long.class) //
                    .firstOrError() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1234L) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateTimestampAsInstant() {
        try (Database db = db()) {
            db.update("update person set registered=? where name='FRED'") //
                    .parameter(Instant.ofEpochMilli(FRED_REGISTERED_TIME)) //
                    .counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
            db.select("select registered from person where name='FRED'") //
                    .getAs(Long.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(FRED_REGISTERED_TIME) //
                    .assertComplete();
        }
    }

    @Test
    public void testUpdateTimestampAsZonedDateTime() {
        try (Database db = db()) {
            db.update("update person set registered=? where name='FRED'") //
                    .parameter(ZonedDateTime.ofInstant(Instant.ofEpochMilli(FRED_REGISTERED_TIME),
                            ZoneOffset.UTC.normalized())) //
                    .counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
            db.select("select registered from person where name='FRED'") //
                    .getAs(Long.class) //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(FRED_REGISTERED_TIME) //
                    .assertComplete();
        }
    }

    @Test
    public void testCompleteCompletes() {
        try (Database db = db(1)) {
            db //
                    .update("update person set score=-3 where name='FRED'") //
                    .complete() //
                    .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .blockingAwait();

            int score = db.select("select score from person where name='FRED'") //
                    .getAs(Integer.class) //
                    .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .blockingFirst();
            assertEquals(-3, score);
        }
    }

    @Test
    public void testComplete() throws InterruptedException {
        try (Database db = db(1)) {
            Completable a = db //
                    .update("update person set score=-3 where name='FRED'") //
                    .complete();
            db.update("update person set score=-4 where score = -3") //
                    .dependsOn(a) //
                    .counts() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(1) //
                    .assertComplete();
        }
    }

    @Test
    public void testCountsOnlyInTransaction() {
        try (Database db = db()) {
            db.update("update person set score = -3") //
                    .transacted() //
                    .countsOnly() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues(3) //
                    .assertComplete();
        }
    }

    @Test
    public void testCountsInTransaction() {
        try (Database db = db()) {
            db.update("update person set score = -3") //
                    .transacted() //
                    .counts() //
                    .doOnNext(DatabaseTest::println) //
                    .toList() //
                    .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValue(list -> list.get(0).isValue() && list.get(0).value() == 3
                            && list.get(1).isComplete() && list.size() == 2) //
                    .assertComplete();
        }
    }

    @Test
    public void testTx() throws InterruptedException {
        Database db = db(3);
        Flowable<Tx<?>> transaction = db //
                .update("update person set score=-3 where name='FRED'") //
                .transaction();

        transaction //
                .doOnCancel(() -> log.debug("disposing")) //
                .doOnNext(DatabaseTest::println) //
                .flatMap(tx -> {
                    log.debug("flatmapping");
                    return tx //
                            .update("update person set score = -4 where score = -3") //
                            .countsOnly() //
                            .doOnSubscribe(s -> log.debug("subscribed")) //
                            .doOnNext(num -> log.debug("num=" + num));
                }) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValue(1) //
                .assertComplete();
    }

    @Test
    public void testTxAfterSelect() {
        Database db = db(3);
        Single<Tx<Integer>> transaction = db //
                .select("select score from person where name='FRED'") //
                .transactedValuesOnly() //
                .getAs(Integer.class) //
                .firstOrError();

        transaction //
                .doOnDispose(() -> log.debug("disposing")) //
                .doOnSuccess(DatabaseTest::println) //
                .flatMapPublisher(tx -> {
                    log.debug("flatmapping");
                    return tx //
                            .update("update person set score = -4 where score = ?") //
                            .parameter(tx.value()) //
                            .countsOnly() //
                            .doOnSubscribe(s -> log.debug("subscribed")) //
                            .doOnNext(num -> log.debug("num=" + num));
                }) //
                .test().awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValue(1) //
                .assertComplete();
    }

    @Test
    public void testUseTxOnComplete() {
        db(1) //
                .select(Person10.class) //
                .transacted() //
                .get() //
                .lastOrError() //
                .map(tx -> tx.select("select count(*) from person") //
                        .count().blockingGet()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertError(CannotForkTransactedConnection.class) //
                .assertValueCount(0);
    }

    @Test
    public void testSingleFlatMap() {
        Single.just(1).flatMapPublisher(n -> Flowable.just(1)).test(1).assertValue(1)
                .assertComplete();
    }

    @Test
    public void testAutomappedInstanceHasMeaningfulToStringMethod() {
        info();
        String s = Database.test() //
                .select("select name, score from person where name=?") //
                .parameterStream(Flowable.just("FRED")) //
                .autoMap(Person2.class) //
                .map(x -> x.toString()) //
                .blockingSingle();
        assertEquals("Person2[name=FRED, score=21]", s);
    }

    @Test
    public void testAutomappedEquals() {
        boolean b = Database.test() //
                .select("select name, score from person where name=?") //
                .parameterStream(Flowable.just("FRED")) //
                .autoMap(Person2.class) //
                .map(x -> x.equals(x)) //
                .blockingSingle();
        assertTrue(b);
    }

    @Test
    public void testAutomappedDoesNotEqualNull() {
        boolean b = Database.test() //
                .select("select name, score from person where name=?") //
                .parameterStream(Flowable.just("FRED")) //
                .autoMap(Person2.class) //
                .map(x -> x.equals(null)) //
                .blockingSingle();
        assertFalse(b);
    }

    @Test
    public void testAutomappedDoesNotEqual() {
        boolean b = Database.test() //
                .select("select name, score from person where name=?") //
                .parameterStream(Flowable.just("FRED")) //
                .autoMap(Person2.class) //
                .map(x -> x.equals(new Object())) //
                .blockingSingle();
        assertFalse(b);
    }

    @Test
    public void testAutomappedHashCode() {
        Person2 p = Database.test() //
                .select("select name, score from person where name=?") //
                .parameterStream(Flowable.just("FRED")) //
                .autoMap(Person2.class) //
                .blockingSingle();
        assertTrue(p.hashCode() != 0);
    }

    @Test
    public void testAutomappedWithParametersThatProvokesMoreThanOneQuery() {
        Database.test() //
                .select("select name, score from person where name=?") //
                .parameters("FRED", "FRED") //
                .autoMap(Person2.class) //
                .doOnNext(DatabaseTest::println) //
                .test() //
                .awaitDone(2000, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValueCount(2) //
                .assertComplete();
    }

    @Test
    public void testAutomappedObjectsEqualsAndHashCodeIsDistinctOnValues() {
        Database.test() //
                .select("select name, score from person where name=?") //
                .parameters("FRED", "FRED") //
                .autoMap(Person2.class) //
                .distinct() //
                .doOnNext(DatabaseTest::println) //
                .test() //
                .awaitDone(2000, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValueCount(1) //
                .assertComplete();
    }

    @Test
    public void testAutomappedObjectsEqualsDifferentiatesDifferentInterfacesWithSameMethodNamesAndValues() {
        PersonDistinct1 p1 = Database.test() //
                .select("select name, score from person where name=?") //
                .parameters("FRED") //
                .autoMap(PersonDistinct1.class) //
                .blockingFirst();

        PersonDistinct2 p2 = Database.test() //
                .select("select name, score from person where name=?") //
                .parameters("FRED") //
                .autoMap(PersonDistinct2.class) //
                .blockingFirst();

        assertNotEquals(p1, p2);
    }

    @Test
    public void testAutomappedObjectsWhenDefaultMethodInvoked() {
        // only run test if java 8
        if (System.getProperty("java.version").startsWith("1.8.")) {
            PersonWithDefaultMethod p = Database.test() //
                    .select("select name, score from person where name=?") //
                    .parameters("FRED") //
                    .autoMap(PersonWithDefaultMethod.class) //
                    .blockingFirst();
            assertEquals("fred", p.nameLower());
        }
    }

    @Test(expected = AutomappedInterfaceInaccessibleException.class)
    public void testAutomappedObjectsWhenDefaultMethodInvokedAndIsNonPublicThrows() {
        PersonWithDefaultMethodNonPublic p = Database.test() //
                .select("select name, score from person where name=?") //
                .parameters("FRED") //
                .autoMap(PersonWithDefaultMethodNonPublic.class) //
                .blockingFirst();
        p.nameLower();
    }

    @Test
    public void testBlockingDatabase() {
        Database db = blocking();
        db.select("select score from person where name=?") //
                .parameters("FRED", "JOSEPH") //
                .getAs(Integer.class) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues(21, 34) //
                .assertComplete();
    }

    @Test
    public void testBlockingDatabaseTransacted() {
        Database db = blocking();
        db.select("select score from person where name=?") //
                .parameters("FRED", "JOSEPH") //
                .transactedValuesOnly() //
                .getAs(Integer.class) //
                .map(x -> x.value()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues(21, 34) //
                .assertComplete();
    }

    @Test
    public void testBlockingDatabaseTransactedNested() {
        Database db = blocking();
        db.select("select score from person where name=?") //
                .parameters("FRED", "JOSEPH") //
                .transactedValuesOnly() //
                .getAs(Integer.class) //
                .flatMap(tx -> tx.select("select name from person where score=?") //
                        .parameter(tx.value()) //
                        .valuesOnly() //
                        .getAs(String.class))
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues("FRED", "JOSEPH") //
                .assertComplete();
    }

    @Test
    public void testUsingNormalJDBCApi() {
        Database db = db(1);
        db.apply(con -> {
            try (PreparedStatement stmt = con
                    .prepareStatement("select count(*) from person where name='FRED'");
                    ResultSet rs = stmt.executeQuery()) {
                rs.next();
                return rs.getInt(1);
            }
        }) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValue(1) //
                .assertComplete();

        // now check that the connection was returned to the pool
        db.select("select count(*) from person where name='FRED'") //
                .getAs(Integer.class) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValue(1) //
                .assertComplete();
    }

    @Test
    public void testUsingNormalJDBCApiCompletable() {
        Database db = db(1);
        db.apply(con -> {
            try (PreparedStatement stmt = con
                    .prepareStatement("select count(*) from person where name='FRED'");
                    ResultSet rs = stmt.executeQuery()) {
                rs.next();
            }
        }) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertComplete();

        // now check that the connection was returned to the pool
        db.select("select count(*) from person where name='FRED'") //
                .getAs(Integer.class) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValue(1) //
                .assertComplete();
    }

    @Test
    public void testCallableStatement() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db.apply(con -> {
            try (Statement stmt = con.createStatement()) {
                CallableStatement st = con.prepareCall("call getPersonCount(?, ?)");
                st.setInt(1, 0);
                st.registerOutParameter(2, Types.INTEGER);
                st.execute();
                assertEquals(2, st.getInt(2));
            }
        }).blockingAwait(TIMEOUT_SECONDS, TimeUnit.SECONDS);
    }

    @Test
    public void testCallableStatementReturningResultSets() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db.apply(con -> {
            try (Statement stmt = con.createStatement()) {
                CallableStatement st = con.prepareCall("call in1out0rs2(?)");
                st.setInt(1, 0);
                st.execute();
                ResultSet rs1 = st.getResultSet();
                st.getMoreResults(Statement.KEEP_CURRENT_RESULT);
                ResultSet rs2 = st.getResultSet();
                rs1.next();
                assertEquals("FRED", rs1.getString(1));
                rs2.next();
                assertEquals("SARAH", rs2.getString(1));
            }
        }).blockingAwait(TIMEOUT_SECONDS, TimeUnit.SECONDS);
    }

    @Test
    public void testCallableApiNoParameters() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call zero()") //
                .once() //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertComplete();
    }

    @Test
    public void testCallableApiNoParametersTransacted() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call zero()") //
                .transacted() //
                .once() //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValueCount(1) //
                .assertValue(x -> x.isComplete()) //
                .assertComplete();
    }

    @Test
    public void testCallableApiOneInOutParameter() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call inout1(?)") //
                .inOut(Type.INTEGER, Integer.class) //
                .input(4) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValue(5) //
                .assertComplete();
    }

    @Test
    public void testCallableApiOneInOutParameterTransacted() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call inout1(?)") //
                .transacted() //
                .inOut(Type.INTEGER, Integer.class) //
                .input(4) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValue(5) //
                .assertComplete();
    }

    @Test
    public void testCallableApiTwoInOutParameters() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call inout2(?, ?)") //
                .inOut(Type.INTEGER, Integer.class) //
                .inOut(Type.INTEGER, Integer.class) //
                .input(4, 10) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValue(x -> x._1() == 5 && x._2() == 12) //
                .assertComplete();
    }

    @Test
    public void testCallableApiTwoInOutParametersTransacted() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call inout2(?, ?)") //
                .transacted() //
                .inOut(Type.INTEGER, Integer.class) //
                .inOut(Type.INTEGER, Integer.class) //
                .input(4, 10) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValue(x -> x._1() == 5 && x._2() == 12) //
                .assertComplete();
    }

    @Test
    public void testCallableApiThreeInOutParameters() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call inout3(?, ?, ?)") //
                .inOut(Type.INTEGER, Integer.class) //
                .inOut(Type.INTEGER, Integer.class) //
                .inOut(Type.INTEGER, Integer.class) //
                .input(4, 10, 13) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValue(x -> x._1() == 5 && x._2() == 12 && x._3() == 16) //
                .assertComplete();
    }

    @Test
    public void testCallableApiThreeInOutParametersTransacted() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call inout3(?, ?, ?)") //
                .transacted() //
                .inOut(Type.INTEGER, Integer.class) //
                .inOut(Type.INTEGER, Integer.class) //
                .inOut(Type.INTEGER, Integer.class) //
                .input(4, 10, 13) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValue(x -> x._1() == 5 && x._2() == 12 && x._3() == 16) //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningOneOutParameter() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out1(?,?)") //
                .in() //
                .out(Type.INTEGER, Integer.class) //
                .input(0, 10, 20) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValues(0, 10, 20) //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningOneOutParameterTransacted() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out1(?,?)") //
                .transacted() //
                .in() //
                .out(Type.INTEGER, Integer.class) //
                .input(0, 10, 20) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValues(0, 10, 20) //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTwoOutParameters() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out2(?,?,?)") //
                .in() //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .input(0, 10, 20) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValueCount(3) //
                .assertValueAt(0, x -> x._1() == 0 && x._2() == 1) //
                .assertValueAt(1, x -> x._1() == 10 && x._2() == 11) //
                .assertValueAt(2, x -> x._1() == 20 && x._2() == 21) //
                .assertComplete();

        db.call("call in1out2(?,?,?)").in().out(Type.INTEGER, Integer.class)
                .out(Type.INTEGER, Integer.class).input(0, 10, 20)
                .blockingForEach(System.out::println);
    }

    @Test
    public void testCallableApiReturningTwoOutParametersTransacted() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out2(?,?,?)") //
                .transacted() //
                .in() //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .input(0, 10, 20) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValueCount(3) //
                .assertValueAt(0, x -> x._1() == 0 && x._2() == 1) //
                .assertValueAt(1, x -> x._1() == 10 && x._2() == 11) //
                .assertValueAt(2, x -> x._1() == 20 && x._2() == 21) //
                .assertComplete();

        db.call("call in1out2(?,?,?)") //
                .in() //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .input(0, 10, 20) //
                .blockingForEach(System.out::println);
    }

    @Test
    public void testCallableApiReturningThreeOutParameters() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out3(?,?,?,?)") //
                .in() //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .input(0, 10, 20) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValueCount(3) //
                .assertValueAt(0, x -> x._1() == 0 && x._2() == 1 && x._3() == 2) //
                .assertValueAt(1, x -> x._1() == 10 && x._2() == 11 && x._3() == 12) //
                .assertValueAt(2, x -> x._1() == 20 && x._2() == 21 && x._3() == 22) //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningThreeOutParametersTransacted() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out3(?,?,?,?)") //
                .transacted() //
                .in() //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .input(0, 10, 20) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValueCount(3) //
                .assertValueAt(0, x -> x._1() == 0 && x._2() == 1 && x._3() == 2) //
                .assertValueAt(1, x -> x._1() == 10 && x._2() == 11 && x._3() == 12) //
                .assertValueAt(2, x -> x._1() == 20 && x._2() == 21 && x._3() == 22) //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningOneResultSet() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in0out0rs1()") //
                .autoMap(Person2.class) //
                .input(0, 10, 20) //
                .doOnNext(x -> {
                    assertTrue(x.outs().isEmpty());
                }) //
                .flatMap(x -> x.results()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValueAt(0, p -> "FRED".equalsIgnoreCase(p.name()) && p.score() == 24)
                .assertValueAt(1, p -> "SARAH".equalsIgnoreCase(p.name()) && p.score() == 26)
                .assertValueAt(2, p -> "FRED".equalsIgnoreCase(p.name()) && p.score() == 24)
                .assertValueAt(3, p -> "SARAH".equalsIgnoreCase(p.name()) && p.score() == 26)
                .assertValueAt(4, p -> "FRED".equalsIgnoreCase(p.name()) && p.score() == 24)
                .assertValueAt(5, p -> "SARAH".equalsIgnoreCase(p.name()) && p.score() == 26)
                .assertValueCount(6) //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningOneResultSetTransacted() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in0out0rs1()") //
                .transacted() //
                .autoMap(Person2.class) //
                .input(0, 10, 20) //
                .flatMap(Tx.flattenToValuesOnly()).doOnNext(x -> {
                    assertTrue(x.outs().isEmpty());
                }) //
                .flatMap(x -> x.results()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValueAt(0, p -> "FRED".equalsIgnoreCase(p.name()) && p.score() == 24)
                .assertValueAt(1, p -> "SARAH".equalsIgnoreCase(p.name()) && p.score() == 26)
                .assertValueAt(2, p -> "FRED".equalsIgnoreCase(p.name()) && p.score() == 24)
                .assertValueAt(3, p -> "SARAH".equalsIgnoreCase(p.name()) && p.score() == 26)
                .assertValueAt(4, p -> "FRED".equalsIgnoreCase(p.name()) && p.score() == 24)
                .assertValueAt(5, p -> "SARAH".equalsIgnoreCase(p.name()) && p.score() == 26)
                .assertValueCount(6) //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTwoResultSetsWithAutoMap() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out0rs2(?)") //
                .in() //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .input(0, 10, 20) //
                .doOnNext(x -> assertTrue(x.outs().isEmpty())) //
                .flatMap(x -> x.results1().zipWith(x.results2(), (y, z) -> y.name() + z.name())) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValues("FREDSARAH", "SARAHFRED", "FREDSARAH", "SARAHFRED", "FREDSARAH",
                        "SARAHFRED") //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTwoResultSetsWithAutoMapTransacted()
            throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out0rs2(?)") //
                .transacted() //
                .in() //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .input(0, 10, 20) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .doOnNext(x -> assertTrue(x.outs().isEmpty())) //
                .flatMap(x -> x.results1().zipWith(x.results2(), (y, z) -> y.name() + z.name())) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValues("FREDSARAH", "SARAHFRED", "FREDSARAH", "SARAHFRED", "FREDSARAH",
                        "SARAHFRED") //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTwoResultSetsWithGet() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out0rs2(?)") //
                .in() //
                .getAs(String.class, Integer.class) //
                .getAs(String.class, Integer.class)//
                .input(0, 10, 20) //
                .flatMap(x -> x.results1().zipWith(x.results2(), (y, z) -> y._1() + z._1())) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValues("FREDSARAH", "SARAHFRED", "FREDSARAH", "SARAHFRED", "FREDSARAH",
                        "SARAHFRED") //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTwoResultSetsWithGetTransacted()
            throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out0rs2(?)") //
                .transacted() //
                .in() //
                .getAs(String.class, Integer.class) //
                .getAs(String.class, Integer.class)//
                .input(0, 10, 20) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .flatMap(x -> x.results1().zipWith(x.results2(), (y, z) -> y._1() + z._1())) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValues("FREDSARAH", "SARAHFRED", "FREDSARAH", "SARAHFRED", "FREDSARAH",
                        "SARAHFRED") //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTwoResultSetsSwitchOrder1() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out0rs2(?)") //
                .autoMap(Person2.class) //
                .in() //
                .autoMap(Person2.class) //
                .input(0, 10, 20) //
                .doOnNext(x -> assertTrue(x.outs().isEmpty())) //
                .flatMap(x -> x.results1().zipWith(x.results2(), (y, z) -> y.name() + z.name())) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValues("FREDSARAH", "SARAHFRED", "FREDSARAH", "SARAHFRED", "FREDSARAH",
                        "SARAHFRED") //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTwoResultSetsSwitchOrder1Transacted()
            throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out0rs2(?)") //
                .transacted() //
                .autoMap(Person2.class) //
                .in() //
                .autoMap(Person2.class) //
                .input(0, 10, 20) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .doOnNext(x -> assertTrue(x.outs().isEmpty())) //
                .flatMap(x -> x.results1().zipWith(x.results2(), (y, z) -> y.name() + z.name())) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValues("FREDSARAH", "SARAHFRED", "FREDSARAH", "SARAHFRED", "FREDSARAH",
                        "SARAHFRED") //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTwoResultSetsSwitchOrder2() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out0rs2(?)") //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .in() //
                .input(0, 10, 20) //
                .doOnNext(x -> assertTrue(x.outs().isEmpty())) //
                .flatMap(x -> x.results1().zipWith(x.results2(), (y, z) -> y.name() + z.name())) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValues("FREDSARAH", "SARAHFRED", "FREDSARAH", "SARAHFRED", "FREDSARAH",
                        "SARAHFRED") //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTwoResultSetsSwitchOrder2Transacted()
            throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in1out0rs2(?)") //
                .transacted() //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .in() //
                .input(0, 10, 20) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .doOnNext(x -> assertTrue(x.outs().isEmpty())) //
                .flatMap(x -> x.results1().zipWith(x.results2(), (y, z) -> y.name() + z.name())) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValues("FREDSARAH", "SARAHFRED", "FREDSARAH", "SARAHFRED", "FREDSARAH",
                        "SARAHFRED") //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTwoOutputThreeResultSets() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in0out2rs3(?, ?)") //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .input(0, 10, 20) //
                .doOnNext(x -> {
                    assertEquals(2, x.outs().size());
                    assertEquals(1, x.outs().get(0));
                    assertEquals(2, x.outs().get(1));
                }) //
                .flatMap(x -> x.results1().zipWith(x.results2(), (y, z) -> y.name() + z.name())
                        .zipWith(x.results3(), (y, z) -> y + z.name())) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues("FREDSARAHFRED", "SARAHFREDSARAH", "FREDSARAHFRED", "SARAHFREDSARAH",
                        "FREDSARAHFRED", "SARAHFREDSARAH") //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTwoOutputThreeResultSetsTransacted()
            throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in0out2rs3(?, ?)") //
                .transacted() //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .input(0, 10, 20) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .doOnNext(x -> {
                    assertEquals(2, x.outs().size());
                    assertEquals(1, x.outs().get(0));
                    assertEquals(2, x.outs().get(1));
                }) //
                .flatMap(x -> x.results1().zipWith(x.results2(), (y, z) -> y.name() + z.name())
                        .zipWith(x.results3(), (y, z) -> y + z.name())) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues("FREDSARAHFRED", "SARAHFREDSARAH", "FREDSARAHFRED", "SARAHFREDSARAH",
                        "FREDSARAHFRED", "SARAHFREDSARAH") //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTenOutParameters() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call out10(?,?,?,?,?,?,?,?,?,?)") //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .input(0, 10) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValueCount(2) //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTenOutParametersTransacted() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call out10(?,?,?,?,?,?,?,?,?,?)") //
                .transacted() //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .out(Type.INTEGER, Integer.class) //
                .input(0, 10) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValueCount(2) //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTenResultSets() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call rs10()") //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .input(0, 10) //
                .doOnNext(x -> {
                    assertEquals(0, x.outs().size());
                    assertEquals(10, x.results().size());
                }) //
                   // just zip the first and last result sets
                .flatMap(x -> x.results(0) //
                        .zipWith(x.results(9), //
                                (y, z) -> ((Person2) y).name() + ((Person2) z).name()))
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues("FREDSARAH", "SARAHFRED", "FREDSARAH", "SARAHFRED") //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningTenResultSetsTransacted() {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call rs10()") //
                .transacted() //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .autoMap(Person2.class) //
                .input(0, 10) //
                .flatMap(Tx.flattenToValuesOnly()) //
                .doOnNext(x -> {
                    assertEquals(0, x.outs().size());
                    assertEquals(10, x.results().size());
                }) //
                   // just zip the first and last result sets
                .flatMap(x -> x.results(0) //
                        .zipWith(x.results(9), //
                                (y, z) -> ((Person2) y).name() + ((Person2) z).name()))
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertNoErrors() //
                .assertValues("FREDSARAH", "SARAHFRED", "FREDSARAH", "SARAHFRED") //
                .assertComplete();
    }

    @Test
    public void testCallableApiReturningOneResultSetGetAs() throws InterruptedException {
        Database db = DatabaseCreator.createDerbyWithStoredProcs(1);
        db //
                .call("call in0out0rs1()") //
                .getAs(String.class, Integer.class) //
                .input(1) //
                .flatMap(x -> x.results()) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValueAt(0, p -> "FRED".equalsIgnoreCase(p._1()) && p._2() == 24) //
                .assertValueAt(1, p -> "SARAH".equalsIgnoreCase(p._1()) && p._2() == 26) //
                .assertComplete();
    }

    @Test
    public void testH2InClauseWithoutSetArray() {
        db().apply(con -> {
            try (PreparedStatement ps = con
                    .prepareStatement("select count(*) from person where name in (?, ?)")) {
                ps.setString(1, "FRED");
                ps.setString(2, "JOSEPH");
                ResultSet rs = ps.executeQuery();
                rs.next();
                return rs.getInt(1);
            }
        }).test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS).assertComplete() // ;
                .assertValue(2); //
    }

    @Test
    @Ignore
    public void testH2InClauseWithSetArray() {
        db().apply(con -> {
            try (PreparedStatement ps = con
                    .prepareStatement("select count(*) from person where name in (?)")) {
                ps.setArray(1, con.createArrayOf("VARCHAR", new String[] { "FRED", "JOSEPH" }));
                ResultSet rs = ps.executeQuery();
                rs.next();
                return rs.getInt(1);
            }
        }).test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS).assertComplete() // ;
                .assertValue(2); //
    }

    @Test
    public void testUpdateTxPerformed() {
        Database db = db(1);
        db.update("update person set score = 1000") //
                .transacted() //
                .counts() //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertComplete() //
                .assertValueCount(2); // value and complete

        db.select("select count(*) from person where score=1000") //
                .getAs(Integer.class) //
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertComplete() //
                .assertValue(3);

    }

    @Test
    public void testIssue20AutoCommitEnabledAndConnectionThrowsOnCommit() {
        ConnectionProvider cp = DatabaseCreator.connectionProvider();
        Database db = Database.fromBlocking(new ConnectionProvider() {

            @Override
            public Connection get() {
                Connection c = cp.get();
                try {
                    c.setAutoCommit(true);
                } catch (SQLException e) {
                    throw new SQLRuntimeException(e);
                }
                return new DelegatedConnection() {

                    @Override
                    public Connection con() {
                        return c;
                    }

                    @Override
                    public void commit() throws SQLException {
                        System.out.println("COMMITTING");
                        if (this.getAutoCommit()) {
                            throw new SQLException("cannot commit when autoCommit is true");
                        } else {
                            con().commit();
                        }
                    }

                };
            }

            @Override
            public void close() {
                // do nothing
            }
        });
        db.update("insert into note(text) values(?)") //
                .parameters("HI", "THERE") //
                .returnGeneratedKeys() //
                .getAs(Integer.class)//
                .test() //
                .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                .assertValues(1, 2) //
                .assertComplete();
    }
    
    @Test
    public void testMapInTransactionIssue35() {
        Database.test() //
                .select(Person10.class) //
                .transacted() //
                .valuesOnly() //
                .get() //
                .map(p -> p.name()) //
                .blockingForEach(System.out::println);
    }

    private static final class Plugins {

        private static final List<Throwable> list = new CopyOnWriteArrayList<Throwable>();

        static void reset() {
            list.clear();
            RxJavaPlugins.setErrorHandler(e -> list.add(e));
        }

        static Throwable getSingleError() {
            assertEquals(1, list.size());
            RxJavaPlugins.reset();
            return list.get(0);
        }
    }

    @Test
    public void testIssue27ConnectionErrorReportedToRxJavaPlugins() {
        try (Database db = Database.nonBlocking()
                .url("jdbc:driverdoesnotexist://doesnotexist:1527/notThere").build()) {
            Plugins.reset();
            db.select("select count(*) from person") //
                    .getAs(Long.class) //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS / 5, TimeUnit.SECONDS) //
                    .assertNoValues() //
                    .assertNotTerminated() //
                    .cancel();
            Throwable e = Plugins.getSingleError();
            assertTrue(e instanceof UndeliverableException);
            assertTrue(e.getMessage().toLowerCase().contains("no suitable driver"));
        }
    }
    
    @Test
    public void testAutoMapInTransactionIssue35() {
        try (Database db = Database.test()) {
            db.select(Person10.class) //
                    .transacted() //
                    .valuesOnly() //
                    .get(x -> x.name()) //
                    .sorted() //
                    .test() //
                    .awaitDone(TIMEOUT_SECONDS, TimeUnit.SECONDS) //
                    .assertValues("FRED", "JOSEPH", "MARMADUKE") //
                    .assertComplete();
        }
    }

    public interface PersonWithDefaultMethod {
        @Column
        String name();

        @Column
        int score();

        public default String nameLower() {
            return name().toLowerCase();
        }
    }

    interface PersonWithDefaultMethodNonPublic {
        @Column
        String name();

        @Column
        int score();

        default String nameLower() {
            return name().toLowerCase();
        }
    }

    interface PersonDistinct1 {
        @Column
        String name();

        @Column
        int score();

    }

    interface PersonDistinct2 {
        @Column
        String name();

        @Column
        int score();

    }

    interface Score {
        @Column
        int score();
    }

    interface Person {
        @Column
        String name();
    }

    interface Person2 {
        @Column
        String name();

        @Column
        int score();
    }

    interface Person3 {
        @Column("name")
        String fullName();

        @Column("score")
        int examScore();
    }

    interface Person4 {
        @Column("namez")
        String fullName();

        @Column("score")
        int examScore();
    }

    interface Person5 {
        @Index(1)
        String fullName();

        @Index(2)
        int examScore();
    }

    interface Person6 {
        @Index(1)
        String fullName();

        @Index(3)
        int examScore();
    }

    interface Person7 {
        @Index(1)
        String fullName();

        @Index(0)
        int examScore();
    }

    interface Person8 {
        @Column
        int name();
    }

    interface Person9 {
        @Column
        String name();

        @Index(2)
        int score();
    }

    interface PersonNoAnnotation {
        String name();
    }

    @Query("select name, score from person order by name")
    interface Person10 {

        @Column
        String name();

        @Column
        int score();
    }

}