package com.github.davidmoten.rx.jdbc; import static com.github.davidmoten.rx.Functions.constant; import static com.github.davidmoten.rx.RxUtil.log; import static com.github.davidmoten.rx.RxUtil.toEmpty; import static com.github.davidmoten.rx.jdbc.DatabaseCreator.connectionProvider; import static com.github.davidmoten.rx.jdbc.DatabaseCreator.createDatabase; import static com.github.davidmoten.rx.jdbc.DatabaseCreator.nextUrl; import static com.github.davidmoten.rx.jdbc.TestingUtil.countDown; import static java.util.Arrays.asList; import static org.easymock.EasyMock.createMock; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static rx.Observable.just; import java.io.IOException; import java.io.Reader; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.easymock.EasyMock; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.davidmoten.rx.RxUtil; import com.github.davidmoten.rx.jdbc.annotations.Column; import com.github.davidmoten.rx.jdbc.annotations.Index; import com.github.davidmoten.rx.jdbc.annotations.Query; import com.github.davidmoten.rx.jdbc.exceptions.TransactionAlreadyOpenException; import com.github.davidmoten.rx.jdbc.tuple.Tuple2; import com.github.davidmoten.rx.jdbc.tuple.Tuple3; import com.github.davidmoten.rx.jdbc.tuple.Tuple4; import com.github.davidmoten.rx.jdbc.tuple.Tuple5; import com.github.davidmoten.rx.jdbc.tuple.Tuple6; import com.github.davidmoten.rx.jdbc.tuple.Tuple7; import com.github.davidmoten.rx.jdbc.tuple.TupleN; import com.zaxxer.hikari.HikariDataSource; import rx.Observable; import rx.Observable.Transformer; import rx.Observer; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; import rx.observables.MathObservable; import rx.observers.TestSubscriber; public abstract class DatabaseTestBase { /** * <p> * Timeout used for latch.await() and similar. If too short then short * machine lockups on build server (which is known to happen on CloudBees * Jenkins infrastructure) will cause undesired test failures. * </p> * * <p> * While your test is still failing use a lower value of course so that you * get rapid turnaround. However, once passing switch the timeout to this * standard timeout value. * </p> */ private static final int TIMEOUT_SECONDS = 3; private static final Logger log = LoggerFactory.getLogger(DatabaseTestBase.class); private final boolean async; public DatabaseTestBase(boolean async) { this.async = async; } Database db() { if (async) return DatabaseCreator.db().asynchronous(); else return DatabaseCreator.db(); } @Test public void testSimpleExample() { Observable<String> names = db().select("select name from person order by name") .getAs(String.class); // convert the names to a list for unit test List<String> list = names.toList().toBlocking().single(); log.debug("list=" + list); assertEquals(asList("FRED", "JOSEPH", "MARMADUKE"), list); } @Test public void testCountQuery() { int count = db() // select names .select("select name from person where name >?") // set name parameter .parameter("ALEX") // count results .count() // get count .first() // block till finished .toBlocking().single(); assertEquals(3, count); } @Test public void testTransactionUsingCount() { Database db = db(); Func1<? super Integer, Boolean> isZero = new Func1<Integer, Boolean>() { @Override public Boolean call(Integer t1) { return t1 == 0; } }; Observable<Integer> existingRows = db // select names .select("select name from person where name=?") // set name parameter .parameter("FRED") // is part of transaction .dependsOn(db.beginTransaction()) // get result count .count() // return empty if count = 0 .filter(isZero); Observable<Integer> update = db // insert record if does not exist .update("insert into person(name,score) values(?,0)") // get parameters from last query .parameters(existingRows.map(constant("FRED"))) // return num rows affected .count(); boolean committed = db.commit(update).toBlocking().single(); assertTrue(committed); } @Test public void testTransactionOnCommit() { Database db = db(); Observable<Boolean> begin = db.beginTransaction(); Observable<Integer> updateCount = db // set everyones score to 99 .update("update person set score=?") // is within transaction .dependsOn(begin) // new score .parameter(99) // execute .count(); Observable<Boolean> commit = db.commit(updateCount); long count = db.select("select count(*) from person where score=?") // set score .parameter(99) // depends on .dependsOn(commit) // return as Long .getAs(Long.class) // log .doOnEach(RxUtil.log()) // get answer .toBlocking().single(); assertEquals(3, count); } @Test public void testSelectErrorResetsTransactionContextInDatabaseClass() { Database db = db(); Observable<Integer> select = db // select names .select("select namez from person where name=?") // set name parameter .parameter("FRED") // is part of transaction .dependsOn(db.beginTransaction()) // get result count .count(); final AtomicBoolean transactionClosed = new AtomicBoolean(true); db.commit(select).doOnError(new Action1<Throwable>() { @Override public void call(Throwable t) { System.out.println(t.getMessage()); if (t instanceof TransactionAlreadyOpenException) transactionClosed.set(false); } }).retry(1).subscribe(ignore()); assertTrue(transactionClosed.get()); } @Test public void testUpdateErrorResetsTransactionContextInDatabaseClass() { Database db = db(); Observable<Integer> update = db // select names .update("zzz") // set name parameter .parameter("FRED") // is part of transaction .dependsOn(db.beginTransaction()) // get result count .count(); final AtomicBoolean transactionClosed = new AtomicBoolean(true); db.commit(update).doOnError(new Action1<Throwable>() { @Override public void call(Throwable t) { System.out.println(t.getMessage()); if (t instanceof TransactionAlreadyOpenException) transactionClosed.set(false); } }).retry(1).subscribe(ignore()); assertTrue(transactionClosed.get()); } private static <T> Observer<T> ignore() { return new Observer<T>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(T t) { } }; } @Test public void testPushEmptyList() { Database db = db(); Observable<Integer> rowsAffected = Observable // generate two integers .range(1, 2) // replace the integers with empty lists .map(toEmpty()) // execute the update .compose( db.update("update person set score = score + 1").parameterListTransformer()) // flatten .compose(RxUtil.<Integer> flatten()) // total the affected records .compose(SUM_INTEGER); assertIs(6, rowsAffected); } @Test public void testRunScript() { Observable<String> commands = just("create table temp1(id integer)", "drop table temp1"); db().run(commands).count().toBlocking().single(); } @Test public void testTransactionOnCommitDoesntOccurUnlessSubscribedTo() { Database db = db(); Observable<Boolean> begin = db.beginTransaction(); Observable<Integer> u = db.update("update person set score=?").dependsOn(begin) .parameter(99).count(); db.commit(u); // note that last transaction was not listed as a dependency of the next // query long count = db.select("select count(*) from person where score=?").parameter(99) .getAs(Long.class).toBlocking().single(); assertEquals(0, count); } @Test public void testTransactionOnRollback() { Database db = db(); Observable<Boolean> begin = db.beginTransaction(); Observable<Integer> updateCount = db.update("update person set score=?").dependsOn(begin) .parameter(99).count(); db.rollback(updateCount); long count = db.select("select count(*) from person where score=?").parameter(99) .dependsOnLastTransaction().getAs(Long.class).toBlocking().single(); assertEquals(0, count); } @Test public void testUpdateAndSelectWithTransaction() { Database db = db(); Observable<Boolean> begin = db.beginTransaction(); Observable<Integer> updateCount = db // update everyone's score to 99 .update("update person set score=?") // in transaction .dependsOn(begin) // new score .parameter(99) // execute .count(); long count = db.select("select count(*) from person where score=?") // where score = 99 .parameter(99) // depends on .dependsOn(updateCount) // as long value .getAs(Long.class).toBlocking().single(); assertEquals(3, count); } @Test public void testUseParameterObservable() { int count = db().select("select name from person where name >?") .parameters(Observable.just("ALEX")).count().toBlocking().single(); assertEquals(3, count); } @Test public void testTwoParameters() { List<String> list = db().select("select name from person where name > ? and name < ?") .parameter("ALEX").parameter("LOUIS").getAs(String.class).toList().toBlocking() .single(); assertEquals(asList("FRED", "JOSEPH"), list); } @Test public void testTakeFewerThanAvailable() { int count = db().select("select name from person where name >?").parameter("ALEX") .get(new ResultSetMapper<Integer>() { @Override public Integer call(ResultSet rs) throws SQLException { return 1; } }).take(2).count().first().toBlocking().single(); assertEquals(2, count); } @Test public void testJdbcObservableCountLettersInAllNames() { int count = MathObservable .sumInteger(db() // select .select("select name from person where name >?") // set name .parameter("ALEX") // count letters .get(COUNT_LETTERS_IN_NAME)) // first result .first() // block and get result .toBlocking().single(); assertEquals(19, count); } private static final ResultSetMapper<Integer> COUNT_LETTERS_IN_NAME = new ResultSetMapper<Integer>() { @Override public Integer call(ResultSet rs) throws SQLException { return rs.getString("name").length(); } }; @Test public void testTransformToTuple2AndTestActionsPrintln() { Tuple2<String, Integer> tuple = db() .select("select name,score from person where name >? order by name") .parameter("ALEX").getAs(String.class, Integer.class).last().toBlocking().single(); assertEquals("MARMADUKE", tuple.value1()); assertEquals(25, (int) tuple.value2()); } @Test public void testTransformToTupleN() { TupleN<String> tuple = db().select("select name, lower(name) from person order by name") .getTupleN(String.class).first().toBlocking().single(); assertEquals("FRED", tuple.values().get(0)); assertEquals("fred", tuple.values().get(1)); } @Test public void testMultipleSetsOfParameters() { List<Integer> list = db().select("select score from person where name=?") // first param .parameter("FRED") // second param .parameter("JOSEPH") // score as integer .getAs(Integer.class) // log .doOnEach(log()) // sort .toSortedList() // block and get .toBlocking().single(); assertEquals(asList(21, 34), list); } @Test public void testNoParams() { List<Tuple2<String, Integer>> tuples = db() .select("select name, score from person where name=? order by name") .getAs(String.class, Integer.class).toList().toBlocking().single(); assertEquals(0, tuples.size()); } @Test public void testCreateFromScript() { Database db = Database.from(DatabaseCreator.nextUrl()); Observable<Integer> create = db .run(DatabaseTestBase.class.getResourceAsStream("/db-creation-script.sql"), ";"); Observable<Integer> count = db.select("select name from person").dependsOn(create) .getAs(String.class).count(); assertIs(3, count); } @Test public void testComposition2() { log.debug("running testComposition2"); Func1<Integer, Boolean> isZero = new Func1<Integer, Boolean>() { @Override public Boolean call(Integer count) { return count == 0; } }; Database db = db(); Observable<Integer> existingRows = db.select("select name from person where name=?") .parameter("FRED").getAs(String.class).count().filter(isZero); List<Integer> counts = db.update("insert into person(name,score) values(?,?)") .parameters(existingRows).count().toList().toBlocking().single(); assertEquals(0, counts.size()); } @Test public void testEmptyResultSet() { int count = db().select("select name from person where name >?") .parameters(Observable.just("ZZTOP")).count().first().toBlocking().single(); assertEquals(0, count); } @Test public void testMixingExplicitAndObservableParameters() { String name = db() .select("select name from person where name > ? and score < ? order by name") .parameter("BARRY").parameters(Observable.just(100)).getAs(String.class).first() .toBlocking().single(); assertEquals("FRED", name); } @Test public void testInstantiateDatabaseWithUrl() throws SQLException { Database db = Database.from("jdbc:h2:mem:testa1"); Connection con = db.queryContext().connectionProvider().get(); con.close(); } @Test public void testComposition() { // use composition to find the first person alphabetically with // a score less than the person with the last name alphabetically // whose name is not XAVIER. Two threads and connections will be used. Database db = db(); Observable<Integer> score = db .select("select score from person where name <> ? order by name") .parameter("XAVIER").getAs(Integer.class).last(); Observable<String> name = db.select("select name from person where score < ? order by name") .parameters(score).getAs(String.class).first(); assertIs("FRED", name); } @Test public void testCompositionUsingCompose() { // use composition to find the first person alphabetically with // a score less than the person with the last name alphabetically // whose name is not XAVIER. Two threads and connections will be used. Database db = db(); Observable<String> name = db .select("select score from person where name <> ? order by name") .parameter("XAVIER").getAs(Integer.class).last() .compose(db.select("select name from person where score < ? order by name") .parameterTransformer().getAs(String.class)) .first(); assertIs("FRED", name); } @Test public void testCompositionTwoLevels() { Database db = db(); Observable<String> names = db.select("select name from person order by name") .getAs(String.class); Observable<String> names2 = db.select("select name from person where name<>? order by name") .parameters(names).parameters(names).getAs(String.class); List<String> list = db.select("select name from person where name>?").parameters(names2) .getAs(String.class).toList().toBlocking().single(); System.out.println(list); assertEquals(12, list.size()); } @Test(expected = RuntimeException.class) public void testSqlProblem() { String name = db().select("select name from pperson where name >?").parameter("ALEX") .getAs(String.class).first().toBlocking().single(); log.debug(name); } @Test(expected = ClassCastException.class) public void testException() { Integer name = db().select("select name from person where name >?").parameter("ALEX") .getAs(Integer.class).first().toBlocking().single(); log.debug("name=" + name); } @Test public void testAutoMapWillMapStringToStringAndIntToDouble() { Person person = db().select("select name,score,dob,registered from person order by name") .autoMap(Person.class).first().toBlocking().single(); assertEquals("FRED", person.getName()); assertEquals(21, person.getScore(), 0.001); assertNull(person.getDateOfBirth()); } @Test(expected = RuntimeException.class) public void testAutoMapCannotFindConstructorWithEnoughParameters() { db().select("select name,score,dob,registered,name from person order by name") .autoMap(Person.class).first().toBlocking().single(); } @Test public void testGetTimestamp() { Database db = db(); java.sql.Timestamp registered = new java.sql.Timestamp(100); Observable<Integer> u = db.update("update person set registered=? where name=?") .parameter(registered).parameter("FRED").count(); Date regTime = db.select("select registered from person order by name").dependsOn(u) .getAs(Date.class).first().toBlocking().single(); assertEquals(100, regTime.getTime()); } @Test public void insertClobAndReadAsString() throws SQLException { Database db = db(); insertClob(db); // read clob as string String text = db.select("select document from person_clob").getAs(String.class).first() .toBlocking().single(); assertTrue(text.contains("about Fred")); } @Test public void insertNullClobAndReadAsString() throws SQLException { Database db = db(); insertClob(db, null); // read clob as string String text = db.select("select document from person_clob").getAs(String.class).first() .toBlocking().single(); assertNull(text); } @Test public void insertNullBlobAndReadAsByteArray() throws SQLException { Database db = db(); insertBlob(db, null); // read clob as string byte[] bytes = db.select("select document from person_blob").getAs(byte[].class).first() .toBlocking().single(); assertNull(bytes); } private static void insertClob(Database db) { insertClob(db, "A description about Fred that is rather long and needs a Clob to store it"); } private static void insertClob(Database db, String value) { Observable<Integer> count = db.update("insert into person_clob(name,document) values(?,?)") .parameter("FRED").parameterClob(value).count(); assertIs(1, count); } private static void insertBlob(Database db, byte[] bytes) { Observable<Integer> count = db.update("insert into person_blob(name,document) values(?,?)") .parameter("FRED").parameterBlob(bytes).count(); assertIs(1, count); } @Test public void insertClobAndReadAsReader() throws SQLException, IOException { Database db = db(); insertClob(db); // read clob as Reader String text = db.select("select document from person_clob").getAs(Reader.class) .map(Util.READER_TO_STRING).first().toBlocking().single(); assertTrue(text.contains("about Fred")); } @Test public void insertBlobAndReadAsByteArray() throws SQLException { Database db = db(); insertBlob(db); // read clob as string byte[] bytes = db.select("select document from person_blob").getAs(byte[].class).first() .toBlocking().single(); assertTrue(new String(bytes).contains("about Fred")); } @Test public void testInsertNull() { Observable<Integer> count = db().update("insert into person(name,score,dob) values(?,?,?)") .parameters("JACK", 42, null).count(); assertIs(1, count); } @Test public void testRC4() { Observable.<Object> empty().concatWith(just(10, 20, 30)).buffer(3) .concatMap(new Func1<List<Object>, Observable<Object>>() { @Override public Observable<Object> call(List<Object> list) { return Observable.from(list); } }).count().toBlocking().single(); // Observable<Integer> count = db() // .update("insert into person(name,score,dob) values(?,?,?)") // .parameters("JACK", 42, null).count(); // assertIs(1, count); } @Test public void testAutoMap() { TimeZone current = TimeZone.getDefault(); try { TimeZone.setDefault(TimeZone.getTimeZone("AEST")); Database db = db(); Date dob = new Date(100); long now = System.currentTimeMillis(); java.sql.Timestamp registered = new java.sql.Timestamp(now); Observable<Integer> u = db.update("update person set dob=?, registered=? where name=?") .parameter(dob).parameter(registered).parameter("FRED").count(); Person person = db.select("select name,score,dob,registered from person order by name") .dependsOn(u).autoMap(Person.class).first().toBlocking().single(); assertEquals("FRED", person.getName()); assertEquals(21, person.getScore(), 0.001); // Dates are truncated to start of day assertEquals(0, (long) person.getDateOfBirth()); assertEquals(now, (long) person.getRegistered()); } finally { TimeZone.setDefault(current); } } @Test public void testLastTransactionWithoutTransaction() { assertIs(0, db().lastTransactionResult().count()); } @Test public void testTuple3() { Tuple3<String, Integer, String> tuple = db() .select("select name,1,lower(name) from person order by name") .getAs(String.class, Integer.class, String.class).first().toBlocking().single(); assertEquals("FRED", tuple.value1()); assertEquals(1, (int) tuple.value2()); assertEquals("fred", tuple.value3()); } @Test public void testTuple4() { Tuple4<String, Integer, String, Integer> tuple = db() .select("select name,1,lower(name),2 from person order by name") .getAs(String.class, Integer.class, String.class, Integer.class).first() .toBlocking().single(); assertEquals("FRED", tuple.value1()); assertEquals(1, (int) tuple.value2()); assertEquals("fred", tuple.value3()); assertEquals(2, (int) tuple.value4()); } @Test public void testTuple5() { Tuple5<String, Integer, String, Integer, String> tuple = db() .select("select name,1,lower(name),2,name from person order by name") .getAs(String.class, Integer.class, String.class, Integer.class, String.class) .first().toBlocking().single(); assertEquals("FRED", tuple.value1()); assertEquals(1, (int) tuple.value2()); assertEquals("fred", tuple.value3()); assertEquals(2, (int) tuple.value4()); assertEquals("FRED", tuple.value5()); } @Test public void testTuple6() { Tuple6<String, Integer, String, Integer, String, Integer> tuple = db() .select("select name,1,lower(name),2,name,3 from person order by name") .getAs(String.class, Integer.class, String.class, Integer.class, String.class, Integer.class) .first().toBlocking().single(); assertEquals("FRED", tuple.value1()); assertEquals(1, (int) tuple.value2()); assertEquals("fred", tuple.value3()); assertEquals(2, (int) tuple.value4()); assertEquals("FRED", tuple.value5()); assertEquals(3, (int) tuple.value6()); } @Test public void testTuple7() { Tuple7<String, Integer, String, Integer, String, Integer, Integer> tuple = db() .select("select name,1,lower(name),2,name,3,4 from person order by name") .getAs(String.class, Integer.class, String.class, Integer.class, String.class, Integer.class, Integer.class) .first().toBlocking().single(); assertEquals("FRED", tuple.value1()); assertEquals(1, (int) tuple.value2()); assertEquals("fred", tuple.value3()); assertEquals(2, (int) tuple.value4()); assertEquals("FRED", tuple.value5()); assertEquals(3, (int) tuple.value6()); assertEquals(4, (int) tuple.value7()); } @Test public void testAutoMapClob() { Database db = db(); insertClob(db); List<PersonClob> list = db.select("select name, document from person_clob") .autoMap(PersonClob.class).toList().toBlocking().single(); assertEquals(1, list.size()); assertEquals("FRED", list.get(0).getName()); assertTrue(list.get(0).getDocument().contains("rather long")); } @Test public void testAutoMapBlob() { Database db = db(); insertBlob(db); List<PersonBlob> list = db.select("select name, document from person_blob") .autoMap(PersonBlob.class).toList().toBlocking().single(); assertEquals(1, list.size()); assertEquals("FRED", list.get(0).getName()); assertTrue(new String(list.get(0).getDocument()).contains("rather long")); } private void insertBlob(Database db) { insertBlob(db, "A description about Fred that is rather long and needs a Clob to store it" .getBytes()); } @Test public void testCalendarParameter() throws SQLException { Database db = db(); Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(0); Observable<Integer> update = db.update("update person set registered=? where name=?") .parameters(cal, "FRED").count(); Timestamp t = db.select("select registered from person where name=?").parameter("FRED") .dependsOn(update).getAs(Timestamp.class).first().toBlocking().single(); assertEquals(0, t.getTime()); } @Test public void testDatabaseBuilder() { Database.builder().connectionProvider(connectionProvider()) .nonTransactionalSchedulerOnCurrentThread().build(); } @Test public void testConnectionPool() { ConnectionProviderPooled cp = new ConnectionProviderPooled(nextUrl(), 0, 10); Database db = createDatabase(cp); int count = db.select("select name from person order by name").count().toBlocking() .single(); assertEquals(3, count); cp.close(); // and again to test idempotentcy cp.close(); } @Test(expected = RuntimeException.class) public void testConnectionPoolWhenExceptionThrown() throws SQLException { HikariDataSource pool = new HikariDataSource(); pool.setJdbcUrl("invalid"); new ConnectionProviderPooled(pool).get(); } @Test public void testConnectionPoolDoesNotRunOutOfConnectionsWhenQueryRunRepeatedly() throws SQLException { ConnectionProviderPooled cp = new ConnectionProviderPooled(nextUrl(), 0, 1); Database db = new Database(cp); Connection con = cp.get(); DatabaseCreator.createDatabase(con); con.close(); assertIs(100, db.select("select name from person where name=?") .parameters(Observable.range(0, 100).map(constant("FRED"))).count()); } // TODO add unit test to check that resources closed (connection etc) before // onComplete or onError called on either select or update @Test public void testDatabaseBuilderWithPool() { Database.builder().url(nextUrl()).pool(0, 5).build().close(); } @Test public void testDatabaseBuilderWithPoolAndConfigured() { Database.builder().url(nextUrl()).pool(0, 5).configure(new Action1<HikariDataSource>() { @Override public void call(HikariDataSource ds) { ds.setConnectionTimeout(10000); }}).build().close(); } @Test(expected=IllegalArgumentException.class) public void testDatabaseBuilderWithPoolAndIllegallyConfiguredThrows() { Database.builder().url(nextUrl()).pool(0, 5).configure(new Action1<HikariDataSource>() { @Override public void call(HikariDataSource ds) { //too short! ds.setConnectionTimeout(1); }}).build().close(); } @Test public void testOneConnectionOpenAndClosedAfterOneSelect() throws InterruptedException { CountDownConnectionProvider cp = new CountDownConnectionProvider(1, 1); Database db = new Database(cp); db.select("select name from person").count().toBlocking().single(); cp.closesLatch().await(); cp.getsLatch().await(); } @Test public void testOneConnectionOpenAndClosedAfterOneUpdate() throws InterruptedException { CountDownConnectionProvider cp = new CountDownConnectionProvider(1, 1); Database db = new Database(cp); db.update("update person set score=? where name=?").parameters(23, "FRED").count() .toBlocking().single(); cp.closesLatch().await(TIMEOUT_SECONDS, TimeUnit.SECONDS); cp.getsLatch().await(TIMEOUT_SECONDS, TimeUnit.SECONDS); } @Test public void testComposeWithParameters() { int score = just("FRED").compose(db().select("select score from person where name=?") .parameterTransformer().getAs(Integer.class)).toBlocking().single(); assertEquals(21, score); } @Test public void testcomposeWithManyParameters() { int score = Observable // range .range(1, 3) // log .doOnEach(log()) // to parameter .map(constant("FRED")) .compose(db() // select .select("select score from person where name=?") // push parameters .parameterTransformer() // get score as integer .getAs(Integer.class)) // sum values .compose(SUM_INTEGER) // block and get .toBlocking().single(); assertEquals(3 * 21, score); } private final Transformer<Integer, Integer> SUM_INTEGER = new Transformer<Integer, Integer>() { @Override public Observable<Integer> call(Observable<Integer> source) { return MathObservable.sumInteger(source); } }; @Test public void testDetector() throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); Observable.range(1, 10).doOnUnsubscribe(countDown(latch)).take(1).toBlocking().single(); assertTrue(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)); } @Test public void testUnsubscribeOfBufferAndFlatMap() throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); Observable.interval(10, TimeUnit.MILLISECONDS).doOnUnsubscribe(countDown(latch)).buffer(2) .flatMap(constant(just(1L))).take(6).toList().toBlocking().single(); assertTrue(latch.await(3, TimeUnit.SECONDS)); } @Test public void testParametersAreUnsubscribedIfUnsubscribedPostParameterOperatorcompose() throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); Observable.interval(100, TimeUnit.MILLISECONDS).doOnEach(log()).map(constant("FRED")) .doOnUnsubscribe(countDown(latch)) .compose(db().select("select score from person where name=?").parameterTransformer() .getAs(Integer.class)) .take(1).subscribe(log()); assertTrue(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)); } @Test public void testParametersAreUnsubscribed() throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); Observable<String> params = Observable.interval(100, TimeUnit.MILLISECONDS).doOnEach(log()) .map(constant("FRED")).doOnUnsubscribe(countDown(latch)); db().select("select score from person where name=?").parameters(params).getAs(Integer.class) .take(1).subscribe(log()); assertTrue(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)); } @Test public void testTakeShouldNotHang() { assertEquals(1, (int) Observable.<Integer> empty().concatWith(Observable.just(1)).take(1) .toBlocking().single()); } @Test public void testcomposeSelectWithDependencies() { Database db = db(); Observable<Integer> count = db.update("update person set score=? where name=?") .parameters(4, "FRED").count() .compose(db.select("select score from person where name=?").parameters("FRED") .dependsOnTransformer().getAs(Integer.class)); assertIs(4, count); } @Test public void testcomposeUpdateWithParameters() { Database db = db(); Observable<Integer> count = just(4, "FRED").compose( db.update("update person set score=? where name=?").parameterTransformer()); assertIs(1, count); } @Test public void testcomposeUpdateWithDependencies() { Database db = db(); Observable<Integer> score = Observable // parameters for coming update .just(4, "FRED") // update Fred's score to 4 .compose(db.update("update person set score=? where name=?").parameterTransformer()) // update everyone with score of 4 to 14 .compose(db.update("update person set score=? where score=?").parameters(14, 4) .dependsOnTransformer()) // get Fred's score .compose(db.select("select score from person where name=?").parameters("FRED") .dependsOnTransformer().getAs(Integer.class)); assertIs(14, score); } static <T> void assertIs(T t, Observable<T> observable) { assertEquals(t, observable.toBlocking().single()); } @Test public void testTwoConnectionsOpenedAndClosedAfterTwoSelects() throws InterruptedException { CountDownConnectionProvider cp = new CountDownConnectionProvider(2, 2); Database db = new Database(cp); db.select("select name from person").count().toBlocking().single(); db.select("select name from person").count().toBlocking().single(); assertTrue(cp.getsLatch().await(60, TimeUnit.SECONDS)); assertTrue(cp.closesLatch().await(60, TimeUnit.SECONDS)); } @Test public void testTwoConnectionsOpenedAndClosedAfterTwoUpdates() throws InterruptedException { CountDownConnectionProvider cp = new CountDownConnectionProvider(2, 2); Database db = new Database(cp); db.update("update person set score=? where name=?").parameters(23, "FRED").count() .toBlocking().single(); db.update("update person set score=? where name=?").parameters(25, "JOHN").count() .toBlocking().single(); assertTrue(cp.getsLatch().await(60, TimeUnit.SECONDS)); assertTrue(cp.closesLatch().await(60, TimeUnit.SECONDS)); } @Test public void testOneConnectionOpenedAndClosedAfterTwoSelectsWithinTransaction() throws InterruptedException { CountDownConnectionProvider cp = new CountDownConnectionProvider(1, 1); Database db = new Database(cp); Observable<Boolean> begin = db.beginTransaction(); Observable<Integer> count = db.select("select name from person").dependsOn(begin).count(); Observable<Integer> count2 = db.select("select name from person").dependsOn(count).count(); int result = db.commit(count2).count().toBlocking().single(); log.info("committed " + result); cp.getsLatch().await(); log.info("gets ok"); cp.closesLatch().await(); log.info("closes ok"); } @Test public void testOneConnectionOpenedAndClosedAfterTwoUpdatesWithinTransaction() throws InterruptedException { CountDownConnectionProvider cp = new CountDownConnectionProvider(1, 1); Database db = new Database(cp); Observable<Boolean> begin = db.beginTransaction(); Observable<Integer> count = db.update("update person set score=? where name=?") .dependsOn(begin).parameters(23, "FRED").count(); Observable<Integer> count2 = db.update("update person set score=? where name=?") .dependsOn(count).parameters(25, "JOHN").count(); int result = db.commit(count2).count().toBlocking().single(); log.info("committed " + result); cp.getsLatch().await(); log.info("gets ok"); cp.closesLatch().await(); log.info("closes ok"); } @Test public void testCloseDatabaseClosesConnectionProvider() { ConnectionProvider cp = createMock(ConnectionProvider.class); cp.close(); EasyMock.expectLastCall().once(); EasyMock.replay(cp); new Database(cp).close(); EasyMock.verify(cp); } @Test public void testCloseAutoCommittingConnectionProviderClosesInternalConnectionProvider() { ConnectionProvider cp = createMock(ConnectionProvider.class); cp.close(); EasyMock.expectLastCall().once(); EasyMock.replay(cp); new ConnectionProviderAutoCommitting(cp).close(); EasyMock.verify(cp); } @Test public void testCloseSingletonManualCommitConnectionProviderClosesInternalConnectionProvider() { ConnectionProvider cp = createMock(ConnectionProvider.class); cp.close(); EasyMock.expectLastCall().once(); EasyMock.replay(cp); new ConnectionProviderSingletonManualCommit(cp).close(); EasyMock.verify(cp); } @Test public void testCloseConnectionProviderFromUrlClosesInternalConnectionProvider() { db().close(); } @Test(expected = RuntimeException.class) public void testCannotPassObservableAsSingleParameter() { db().select("anything").parameter(Observable.just(123)); } @Test public void testConnectionsReleasedByUpdateStatementBeforeOnNext() throws InterruptedException { final CountDownConnectionProvider cp = new CountDownConnectionProvider(1, 1); Database db = new Database(cp); Observable<Integer> result = db.update("update person set score = 1 where name=?") .parameter("FRED").count(); checkConnectionsReleased(cp, result); } @Test public void testConnectionsReleasedByCommitBeforeOnNext() throws InterruptedException { final CountDownConnectionProvider cp = new CountDownConnectionProvider(1, 1); Database db = new Database(cp); Observable<Boolean> begin = db.beginTransaction(); Observable<Integer> result = db.update("update person set score = 1 where name=?") .dependsOn(begin).parameter("FRED").count(); checkConnectionsReleased(cp, db.commit(result)); } @Test public void testConnectionsReleasedByRollbackBeforeOnNext() throws InterruptedException { final CountDownConnectionProvider cp = new CountDownConnectionProvider(1, 1); Database db = new Database(cp); Observable<Boolean> begin = db.beginTransaction(); Observable<Integer> result = db.update("update person set score = 1 where name=?") .dependsOn(begin).parameter("FRED").count(); checkConnectionsReleased(cp, db.rollback(result)); } private void checkConnectionsReleased(final CountDownConnectionProvider cp, Observable<?> result) throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); result.subscribe(new Action1<Object>() { @Override public void call(Object obj) { try { if (cp.closesLatch().await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } } }); assertTrue(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)); } @Test public void testCanChainUpdateStatementsWithinTransaction() { Database db = db(); Observable<Boolean> begin = db.beginTransaction(); Observable<Integer> updates = Observable // set name parameter .just("FRED") // push into update .compose(db.update("update person set score=1 where name=?").dependsOn(begin) .parameterTransformer()) // map num rows affected to JOHN .map(constant("JOHN")) // push into second update .compose( db.update("update person set score=2 where name=?").parameterTransformer()); db.commit(updates).toBlocking().single(); } @Test public void testCommitOperator() { Database db = db(); Observable<Boolean> begin = db.beginTransaction(); String name = Observable // set name parameter .just("FRED") // push into update .compose(db.update("update person set score=1 where name=?") // .dependsOn(begin) // .parameterTransformer()) // map num rows affected to JOHN .compose(db.commit_()) // select query .compose(db.select("select name from person where score=1") // depends on commit .dependsOnTransformer() // return names .getAs(String.class)) // return first name .first() // block to get make everything run .toBlocking().single(); assertEquals("FRED", name); } @Test public void testTryCatch() { try (Connection con = DatabaseCreator.nextConnection(); PreparedStatement ps = con.prepareStatement( "select name from person where name > ? order by name");) { ps.setObject(1, "ALEX"); List<String> list = new ArrayList<String>(); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(rs.getString(1)); } } System.out.println(list); } catch (SQLException e) { throw new RuntimeException(e); } } @Test public void testChainSelectUsingOperators() { Database db = db(); List<Integer> scores = db.select("select name from person") // get name .getAs(String.class) // push name as parameter to next select .compose(db // select scores .select("select score from person where name=?") // parameters are pushed .parameterTransformer() // get score as integer .getAs(Integer.class)) // sort scores .toSortedList() // block to get result .toBlocking().single(); assertEquals(asList(21, 25, 34), scores); } @Test public void testBeginTransactionEmitsOneItem() { Database db = db(); Boolean value = db.beginTransaction().toBlocking().single(); assertTrue(value); } @Test public void testCommitOnLastOperator() { Database db = db(); long count = db // start transaction .beginTransaction() // push parameters .concatMap(constant(just(99, 88))) // log .doOnEach(log()) // update twice .compose(db.update("update person set score=?") // push parameters .parameterTransformer()) // commit on last .compose(db.commitOnComplete_()) // get count of 88s .compose(db.select("select count(*) from person where score=88") // depends on previous .dependsOnTransformer() // count as Long .getAs(Long.class)) // block and get result .toBlocking().single(); assertEquals(3, count); } @Test public void testRollbackOnLastOperator() { Database db = db(); long count = db // start transaction .beginTransaction() // push parameters .concatMap(constant(just(99, 88))) // log .doOnEach(log()) // update twice .compose(db.update("update person set score=?") // push parameters .parameterTransformer()) // commit on last .compose(db.rollbackOnComplete_()) // get count of 88s .compose(db.select("select count(*) from person where score=88") // depends on previous .dependsOnTransformer() // count as Long .getAs(Long.class)) // block and get result .toBlocking().single(); assertEquals(0, count); } @Test public void testBeginTransactionOnNextForThreePasses() { Database db = db(); Observable<Integer> min = Observable // do 3 times .just(11, 12, 13) // begin transaction for each item .compose(db.beginTransactionOnNext_()) // update all scores to the item .compose(db.update("update person set score=?").parameterTransformer()) // to empty parameter list .map(toEmpty()) // increase score .compose(db.update("update person set score=score + 5").parameterListTransformer()) // only expect one result so can flatten .compose(RxUtil.<Integer> flatten()) // commit transaction .compose(db.commitOnNext_()) // to empty lists .map(toEmpty()) // return count .compose(db.select("select min(score) from person").dependsOnTransformer() .getAs(Integer.class)); assertIs(18, min); } @Test public void testParameterListOperator() { Database db = db(); @SuppressWarnings("unchecked") int count = // parameters grouped in lists objects(objects(1), objects(2)) // log .doOnEach(log()) // begin trans .compose(db.<Observable<Object>> beginTransactionOnNext_()) // log .doOnEach(log()) // update .compose(db.update("update person set score = ?") // push lists of parameters .parameterListTransformer()) // log .doOnEach(log()) // commit .compose(db.<Integer> commitOnNextList_()) // total rows affected .count() // block and get result .toBlocking().single(); assertEquals(2, count); } @Test public void testParameterListOperatorWhenQueryNeedsTwoParameters() { Database db = db(); @SuppressWarnings("unchecked") int count = // parameters grouped in lists objects(objects(1, "FRED", 3, "JOHN"), objects(2, "JOSEPH")) // log .doOnEach(log()) // begin trans .compose(db.<Observable<Object>> beginTransactionOnNext_()) // log .doOnEach(log()) // update .compose(db.update("update person set score = ? where name=?") // push lists of parameters .parameterListTransformer()) // log .doOnEach(log()) // commit .compose(db.<Integer> commitOnNextList_()) // total rows affected .count() // block and get result .toBlocking().single(); assertEquals(2, count); } @Test public void testCanExecuteCreateSchema() { Database db = db(); int count = db.update("create schema if not exists special_user").count().toBlocking() .single(); assertEquals(0, count); } @Test public void testCanExecuteCreateTable() { Database db = db(); int count = db.update("create table mytemp(name varchar2(100) primary key)").count() .toBlocking().single(); assertEquals(0, count); } private static Observable<Object> objects(Object... objects) { return Observable.from(objects); } private static Observable<Observable<Object>> objects( @SuppressWarnings("unchecked") Observable<Object>... objects) { return Observable.from(objects); } @Test public void testDatabaseFromConnectionCanUseConnectionTwiceWithoutItBeingClosedInReality() throws SQLException { ConnectionProvider cp = DatabaseCreator.connectionProvider(); DatabaseCreator.createDatabase(cp); Connection con = cp.get(); Database db = Database.from(con); Observable<Integer> count = db // get names .select("select name from person") // as string .getAs(String.class) // count names .count() // do something else .compose(db // get max score .select("select max(score) from person") // run the previous statement first .dependsOnTransformer() // as integer .getAs(Integer.class)); assertIs(34, count); con.close(); } @Test public void testNonTransactionalMultipleQueries() { // get a synchronous database Database db = DatabaseCreator.db(); final Set<String> set = Collections.newSetFromMap(new HashMap<String, Boolean>()); Observable<Integer> count = Observable.just(1, 2, 3, 4, 5) // select .compose(db.select("select name from person where score >?") // push parameters to this query .parameterTransformer() // get name as string .getAs(String.class)) // record thread name .doOnNext(new Action1<String>() { @Override public void call(String name) { set.add(Thread.currentThread().getName()); } }) // count .count(); assertIs(5 * 3, count); System.out.println("threads=" + set); assertEquals(1, set.size()); } @Test public void testTransaction() { Database db = db(); Observable<Boolean> begin = db.beginTransaction(); Observable<Integer> count = Observable // generate 1,2,3 .just(1, 2, 3) // update score with that value .compose(db.update("update person set score = ?") // participates in a transaction .dependsOn(begin) // parameters are pushed to this update statement .parameterTransformer()) // commit transaction .compose(db.commitOnComplete_()) // count names with score 3 .compose(db.select("select count(name) from person where score=3") // must commit first .dependsOnTransformer().getAs(Integer.class)); assertIs(3, count); } @Test public void testTwoConnectionsOpenedAndClosedWhenTakeOneUsedWithSelectThatReturnsOneRow() throws InterruptedException { Action0 completed = new Action0() { @Override public void call() { System.out.println("completed"); } }; CountDownConnectionProvider cp = new CountDownConnectionProvider(1, 1); Database db = new Database(cp); db.select("select count(*) from person").getAs(Long.class).doOnCompleted(completed).take(1) .toBlocking().single(); assertTrue(cp.getsLatch().await(6, TimeUnit.SECONDS)); assertTrue(cp.closesLatch().await(6, TimeUnit.SECONDS)); } @Test public void testAutoMapInterface() { // test dynamic proxying List<NameScore> list = db().select("select name, score from person order by name") .autoMap(NameScore.class).toList().toBlocking().single(); assertEquals(3, list.size()); assertEquals("FRED", list.get(0).name()); assertEquals(21, list.get(0).score()); assertEquals("JOSEPH", list.get(1).name()); assertEquals(34, list.get(1).score()); } static interface NameScore { @Index(1) String name(); @Column("score") int score(); } @Test public void testAutoMapConvertsCamelCaseToUnderscoreColumnNames() { // test dynamic proxying List<Address> list = db().select("select address_id, full_address from address") .autoMap(Address.class).toList().toBlocking().single(); assertEquals(1, list.size()); assertEquals(1, list.get(0).addressId()); assertTrue(list.get(0).fullAddress().contains("Something")); } static interface Address { @Column int addressId(); @Column String fullAddress(); } @Test public void testAutoMapWithQueryAnnotation() { List<NameScore2> list = db().select().autoMap(NameScore2.class).toList().toBlocking() .single(); assertEquals(3, list.size()); assertEquals("FRED", list.get(0).name()); assertEquals(21, list.get(0).score()); assertEquals("JOSEPH", list.get(1).name()); assertEquals(34, list.get(1).score()); } @Query("select name, score from person order by name") static interface NameScore2 { @Index(1) String name(); @Column("score") int score(); } @Test(expected = RuntimeException.class) public void testAutoMapThrowsExceptionIfMappedInterfaceColumnMethodHasParameters() { // test dynamic proxying db().select("select address_id, full_address from address").autoMap(Address2.class).toList() .toBlocking().single(); } static interface Address2 { @Column int addressId(String suburb); @Column String fullAddress(); } @Test public void testCustomMapper() { String name = db().select("select name from person order by name") .get(new ResultSetMapper<String>() { @Override public String call(ResultSet rs) throws SQLException { return rs.getString(1); } }).first().toBlocking().single(); assertEquals("FRED", name); } @Test public void testReturnGeneratedKeysForOneInsertedValue() { // h2 only returns the last generated key List<Integer> list = db() // .update("insert into note(text) values(?)") // .parameters("something") // .returnGeneratedKeys() // .getAs(Integer.class) // .toList().toBlocking().single(); assertEquals(Arrays.asList(1), list); } @Test public void testReturnGeneratedKeysForMultipleInsertedValuesInOneStatement() { // h2 only returns the last generated key List<Integer> list = db() // .update("insert into note(text) values(?),(?)") // .parameters("something", "again") // .returnGeneratedKeys() // .getAs(Integer.class) // .toList().toBlocking().single(); assertEquals(Arrays.asList(2), list); } @Test public void testReturnGeneratedKeysForMultipleCallsOfInsert() { // h2 only returns the last generated key List<Integer> list = db() // .update("insert into note(text) values(?)") // .parameters("something", "again") // .returnGeneratedKeys() // .getAs(Integer.class) // .toList().toBlocking().single(); assertEquals(Arrays.asList(1, 2), list); } @Test public void testNamedParameters() { String name = db() // .select("select name from person where score >= :min and score <=:max") // .parameter("min", 24) // .parameter("max", 26) // .getAs(String.class).toBlocking().single(); assertEquals("MARMADUKE", name); } @Test public void testNamedParametersWithMapParameter() { Map<String, Integer> map = new HashMap<String, Integer>(); map.put("min", 24); map.put("max", 26); String name = db() // .select("select name from person where score >= :min and score <=:max") // .parameters(map) // .getAs(String.class).toBlocking().single(); assertEquals("MARMADUKE", name); } @Test public void testNamedParametersWithMapParameterInObservable() { Map<String, Integer> map = new HashMap<String, Integer>(); map.put("min", 24); map.put("max", 26); String name = db() // .select("select name from person where score >= :min and score <=:max") // .parameters(Observable.just(map)) // .getAs(String.class).toBlocking().single(); assertEquals("MARMADUKE", name); } @Test public void testNamedParametersWithUpdateStatement() { int count = db() // .update("update person set score = :newScore where score >= :min and score <=:max") // .parameter("newScore", 25) // .parameter("min", 24) // .parameter("max", 26) // .count().toBlocking().single(); assertEquals(1, count); } @Test(expected = RuntimeException.class) public void testNamedParametersOneMissingParameterShouldThrowException() { db().select("select name from person where name = :name and score = :score") .parameter("name", "FRED").count().toBlocking().single(); } @Test(expected = RuntimeException.class) public void testNamedParametersWithMapParameterNoNamesInSql() { Map<String, Integer> map = new HashMap<String, Integer>(); map.put("min", 24); map.put("max", 26); db() // .select("select name from person where score >= ? and score <= ?") // .parameters(Observable.just(map)) // .getAs(String.class).toBlocking().single(); } @Test public void testNoParameters() { int count = db().select("select name from person").count().toBlocking().single(); assertEquals(3, count); } @Test public void testAutoMapInterfaceWithPrimitives() { List<NameScorePrimitive> list = db() .select("select name, score from person where name='FRED'") .autoMap(NameScorePrimitive.class).toList().toBlocking().single(); assertEquals(21, list.get(0).score()); } static interface NameScorePrimitive { @Column String name(); @Column int score(); } @Test public void testResultSetTransformSetOnDatabase() { final AtomicInteger count = new AtomicInteger(); Func1<ResultSet, ? extends ResultSet> transform = new Func1<ResultSet, ResultSet>() { @Override public ResultSet call(ResultSet rs) { count.incrementAndGet(); return rs; } }; TestSubscriber<Integer> ts = TestSubscriber.create(); Database db = Database.builder().connectionProvider(db().connectionProvider()) .resultSetTransform(transform).build(); db.select("select name from person").count().subscribe(ts); ts.awaitTerminalEvent(); assertEquals(1, count.get()); } @Test public void testResultSetTransformSetOnQuery() { final AtomicInteger count = new AtomicInteger(); Func1<ResultSet, ? extends ResultSet> transform = new Func1<ResultSet, ResultSet>() { @Override public ResultSet call(ResultSet rs) { count.incrementAndGet(); return rs; } }; TestSubscriber<Integer> ts = TestSubscriber.create(); db().select("select name from person").resultSetTransform(transform).count().subscribe(ts); ts.awaitTerminalEvent(10, TimeUnit.SECONDS); assertEquals(1, count.get()); } @Test public void testResultSetTransformSetOnQueryCompoundsWithDatabaseTransform() { final List<Integer> list = new CopyOnWriteArrayList<>(); Func1<ResultSet, ? extends ResultSet> transform1 = new Func1<ResultSet, ResultSet>() { @Override public ResultSet call(ResultSet rs) { list.add(1); return rs; } }; Func1<ResultSet, ? extends ResultSet> transform2 = new Func1<ResultSet, ResultSet>() { @Override public ResultSet call(ResultSet rs) { list.add(2); return rs; } }; Database db = Database.builder().connectionProvider(db().connectionProvider()) .resultSetTransform(transform1).build(); TestSubscriber<Integer> ts = TestSubscriber.create(); db.select("select name from person").resultSetTransform(transform2).count().subscribe(ts); ts.awaitTerminalEvent(10, TimeUnit.SECONDS); assertEquals(Arrays.asList(1, 2), list); } /******************************************************** ** Utility classes ********************************************************/ private static class CountDownConnectionProvider implements ConnectionProvider { private final ConnectionProvider cp; private final CountDownLatch closesLatch; private final CountDownLatch getsLatch; CountDownConnectionProvider(int expectedGets, int expectedCloses) { this.cp = connectionProvider(); DatabaseCreator.createDatabase(cp.get()); this.closesLatch = new CountDownLatch(expectedCloses); this.getsLatch = new CountDownLatch(expectedGets); } CountDownLatch closesLatch() { return closesLatch; } CountDownLatch getsLatch() { return getsLatch; } @Override public Connection get() { getsLatch.countDown(); Connection inner = cp.get(); return new CountingConnection(inner, closesLatch); } @Override public void close() { cp.close(); } } static class PersonClob { private final String name; private final String document; public PersonClob(String name, String document) { this.name = name; this.document = document; } public String getName() { return name; } public String getDocument() { return document; } } static class PersonBlob { private final String name; private final byte[] document; public PersonBlob(String name, byte[] document) { this.name = name; this.document = document; } public String getName() { return name; } public byte[] getDocument() { return document; } } static class Person { private final String name; private final double score; private final Long dateOfBirthEpochMs; private final Long registered; Person(String name, double score, Long dateOfBirthEpochMs, Long registered) { this.name = name; this.score = score; this.dateOfBirthEpochMs = dateOfBirthEpochMs; this.registered = registered; } public String getName() { return name; } public double getScore() { return score; } public Long getDateOfBirth() { return dateOfBirthEpochMs; } public Long getRegistered() { return registered; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("Pair [name="); builder.append(name); builder.append(", score="); builder.append(score); builder.append("]"); return builder.toString(); } } }