From 21556e171e25699e1aeb6e46c8f28cd074700846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Thu, 10 Feb 2022 18:41:26 +0100 Subject: [PATCH] initial commit --- .gitignore | 13 + LICENSE.txt | 191 ++ NOTICE.txt | 7 + build.gradle | 32 + gradle.properties | 11 + gradle/compile/java.gradle | 43 + gradle/documentation/asciidoc.gradle | 55 + gradle/ide/idea.gradle | 13 + gradle/publish.gradle | 0 gradle/publishing/publication.gradle | 64 + gradle/publishing/sonatype.gradle | 11 + gradle/test/junit5.gradle | 27 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 234 +++ gradlew.bat | 89 + jdbc-connection-pool/NOTICE.txt | 26 + jdbc-connection-pool/build.gradle | 4 + .../src/main/java/module-info.java | 6 + .../jdbc/connection/pool/IsolationLevel.java | 20 + .../org/xbib/jdbc/connection/pool/Pool.java | 1076 +++++++++++ .../xbib/jdbc/connection/pool/PoolConfig.java | 666 +++++++ .../jdbc/connection/pool/PoolDataSource.java | 224 +++ .../xbib/jdbc/connection/pool/PoolEntry.java | 176 ++ .../connection/pool/PoolEntryException.java | 9 + .../pool/PoolInitializationException.java | 14 + .../pool/ProxyCallableStatement.java | 595 ++++++ .../jdbc/connection/pool/ProxyConnection.java | 697 +++++++ .../pool/ProxyDatabaseMetaData.java | 1056 ++++++++++ .../jdbc/connection/pool/ProxyFactory.java | 68 + .../jdbc/connection/pool/ProxyLeakTask.java | 77 + .../connection/pool/ProxyLeakTaskFactory.java | 32 + .../pool/ProxyPreparedStatement.java | 329 ++++ .../jdbc/connection/pool/ProxyResultSet.java | 1033 ++++++++++ .../jdbc/connection/pool/ProxyStatement.java | 394 ++++ .../xbib/jdbc/connection/pool/util/Bag.java | 333 ++++ .../jdbc/connection/pool/util/BagEntry.java | 14 + .../pool/util/BagStateListener.java | 6 + .../connection/pool/util/ClockSource.java | 153 ++ .../pool/util/ClockSourceFactory.java | 10 + .../pool/util/DefaultThreadFactory.java | 22 + .../pool/util/DriverDataSource.java | 161 ++ .../jdbc/connection/pool/util/FastList.java | 357 ++++ .../pool/util/NanosecondClockSource.java | 77 + .../java/org/xbib/io/pool/jdbc/BagTest.java | 78 + .../jdbc/ConcurrentCloseConnectionTest.java | 39 + .../jdbc/ConnectionCloseBlockingTest.java | 69 + .../jdbc/ConnectionPoolSizeVsThreadsTest.java | 141 ++ .../jdbc/ConnectionRaceConditionTest.java | 59 + .../io/pool/jdbc/ConnectionStateTest.java | 134 ++ .../org/xbib/io/pool/jdbc/ConnectionTest.java | 552 ++++++ .../pool/jdbc/ConnectionTimeoutRetryTest.java | 178 ++ .../io/pool/jdbc/HouseKeeperCleanupTest.java | 57 + .../org/xbib/io/pool/jdbc/IsolationTest.java | 48 + .../org/xbib/io/pool/jdbc/JdbcDriverTest.java | 59 + .../java/org/xbib/io/pool/jdbc/PoolTest.java | 107 + .../xbib/io/pool/jdbc/PoolTestExtension.java | 70 + .../org/xbib/io/pool/jdbc/ProxiesTest.java | 247 +++ .../org/xbib/io/pool/jdbc/RampUpDownTest.java | 40 + .../io/pool/jdbc/SaturatedPoolTest830.java | 111 ++ .../org/xbib/io/pool/jdbc/ShutdownTest.java | 262 +++ .../org/xbib/io/pool/jdbc/StatementTest.java | 98 + .../org/xbib/io/pool/jdbc/UnwrapTest.java | 59 + .../io/pool/jdbc/mock/MockDataSource.java | 96 + .../io/pool/jdbc/mock/StubBaseConnection.java | 33 + .../io/pool/jdbc/mock/StubConnection.java | 522 +++++ .../io/pool/jdbc/mock/StubDataSource.java | 127 ++ .../xbib/io/pool/jdbc/mock/StubDriver.java | 78 + .../pool/jdbc/mock/StubPreparedStatement.java | 746 +++++++ .../xbib/io/pool/jdbc/mock/StubResultSet.java | 1467 ++++++++++++++ .../xbib/io/pool/jdbc/mock/StubStatement.java | 418 ++++ jdbc-query/build.gradle | 7 + jdbc-query/src/main/java/module-info.java | 4 + .../main/java/org/xbib/jdbc/query/Config.java | 127 ++ .../java/org/xbib/jdbc/query/ConfigFrom.java | 102 + .../org/xbib/jdbc/query/ConfigFromImpl.java | 295 +++ .../java/org/xbib/jdbc/query/ConfigImpl.java | 252 +++ .../jdbc/query/ConfigInvalidException.java | 22 + .../jdbc/query/ConfigMissingException.java | 20 + .../query/ConstraintViolationException.java | 23 + .../java/org/xbib/jdbc/query/Database.java | 211 ++ .../xbib/jdbc/query/DatabaseException.java | 37 + .../org/xbib/jdbc/query/DatabaseImpl.java | 280 +++ .../org/xbib/jdbc/query/DatabaseProvider.java | 1107 +++++++++++ .../main/java/org/xbib/jdbc/query/DbCode.java | 18 + .../java/org/xbib/jdbc/query/DbCodeTx.java | 19 + .../java/org/xbib/jdbc/query/DbCodeTyped.java | 19 + .../org/xbib/jdbc/query/DbCodeTypedTx.java | 19 + .../main/java/org/xbib/jdbc/query/Ddl.java | 19 + .../java/org/xbib/jdbc/query/DdlImpl.java | 97 + .../main/java/org/xbib/jdbc/query/Flavor.java | 993 ++++++++++ .../xbib/jdbc/query/MixedParameterSql.java | 133 ++ .../java/org/xbib/jdbc/query/Options.java | 130 ++ .../org/xbib/jdbc/query/OptionsDefault.java | 84 + .../org/xbib/jdbc/query/OptionsOverride.java | 108 ++ .../jdbc/query/QueryTimedOutException.java | 13 + .../main/java/org/xbib/jdbc/query/Row.java | 435 +++++ .../java/org/xbib/jdbc/query/RowHandler.java | 8 + .../main/java/org/xbib/jdbc/query/Rows.java | 8 + .../java/org/xbib/jdbc/query/RowsAdaptor.java | 835 ++++++++ .../java/org/xbib/jdbc/query/RowsHandler.java | 9 + .../main/java/org/xbib/jdbc/query/Schema.java | 949 +++++++++ .../java/org/xbib/jdbc/query/SecretArg.java | 19 + .../main/java/org/xbib/jdbc/query/Sql.java | 443 +++++ .../java/org/xbib/jdbc/query/SqlArgs.java | 773 ++++++++ .../java/org/xbib/jdbc/query/SqlInsert.java | 152 ++ .../org/xbib/jdbc/query/SqlInsertImpl.java | 708 +++++++ .../java/org/xbib/jdbc/query/SqlNull.java | 14 + .../java/org/xbib/jdbc/query/SqlSelect.java | 289 +++ .../org/xbib/jdbc/query/SqlSelectImpl.java | 766 ++++++++ .../java/org/xbib/jdbc/query/SqlUpdate.java | 165 ++ .../org/xbib/jdbc/query/SqlUpdateImpl.java | 334 ++++ .../org/xbib/jdbc/query/StatementAdaptor.java | 220 +++ .../java/org/xbib/jdbc/query/Transaction.java | 56 + .../org/xbib/jdbc/query/TransactionImpl.java | 30 + .../main/java/org/xbib/jdbc/query/When.java | 75 + .../query/WrongNumberOfRowsException.java | 13 + .../org/xbib/jdbc/query/util/DebugSql.java | 175 ++ .../jdbc/query/util/InternalStringReader.java | 21 + .../java/org/xbib/jdbc/query/util/Metric.java | 243 +++ .../org/xbib/jdbc/query/util/RewriteArg.java | 14 + .../org/xbib/jdbc/query/test/CommonTest.java | 1715 +++++++++++++++++ .../org/xbib/jdbc/query/test/ConfigTest.java | 250 +++ .../xbib/jdbc/query/test/DatabaseMock.java | 18 + .../org/xbib/jdbc/query/test/DerbyTest.java | 145 ++ .../org/xbib/jdbc/query/test/HsqldbTest.java | 177 ++ .../org/xbib/jdbc/query/test/OracleTest.java | 43 + .../xbib/jdbc/query/test/PostgreSqlTest.java | 50 + .../org/xbib/jdbc/query/test/RowStub.java | 799 ++++++++ .../org/xbib/jdbc/query/test/SqlArgsTest.java | 24 + .../xbib/jdbc/query/test/SqlServerTest.java | 86 + .../jdbc/query/test/example/DerbyExample.java | 35 + .../jdbc/query/test/example/DynamicSql.java | 52 + .../jdbc/query/test/example/FakeBuilder.java | 65 + .../jdbc/query/test/example/HelloAny.java | 47 + .../jdbc/query/test/example/HelloDerby.java | 46 + .../query/test/example/InsertReturning.java | 42 + .../xbib/jdbc/query/test/example/Sample.java | 49 + .../jdbc/query/test/example/SampleDao.java | 109 ++ settings.gradle | 2 + 140 files changed, 28972 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 NOTICE.txt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/compile/java.gradle create mode 100644 gradle/documentation/asciidoc.gradle create mode 100644 gradle/ide/idea.gradle create mode 100644 gradle/publish.gradle create mode 100644 gradle/publishing/publication.gradle create mode 100644 gradle/publishing/sonatype.gradle create mode 100644 gradle/test/junit5.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 jdbc-connection-pool/NOTICE.txt create mode 100644 jdbc-connection-pool/build.gradle create mode 100644 jdbc-connection-pool/src/main/java/module-info.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/IsolationLevel.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/Pool.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolConfig.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolDataSource.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntry.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntryException.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolInitializationException.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyCallableStatement.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyConnection.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyDatabaseMetaData.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyFactory.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTask.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTaskFactory.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyPreparedStatement.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyResultSet.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyStatement.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/Bag.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/BagEntry.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/BagStateListener.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/ClockSource.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/ClockSourceFactory.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/DefaultThreadFactory.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/DriverDataSource.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/FastList.java create mode 100644 jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/NanosecondClockSource.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/BagTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConcurrentCloseConnectionTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionCloseBlockingTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionPoolSizeVsThreadsTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionRaceConditionTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionStateTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionTimeoutRetryTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/HouseKeeperCleanupTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/IsolationTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/JdbcDriverTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/PoolTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/PoolTestExtension.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ProxiesTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/RampUpDownTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/SaturatedPoolTest830.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ShutdownTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/StatementTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/UnwrapTest.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/MockDataSource.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubBaseConnection.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubConnection.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubDataSource.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubDriver.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubPreparedStatement.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubResultSet.java create mode 100644 jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubStatement.java create mode 100644 jdbc-query/build.gradle create mode 100644 jdbc-query/src/main/java/module-info.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/Config.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFrom.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFromImpl.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigImpl.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigInvalidException.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigMissingException.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/ConstraintViolationException.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/Database.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseException.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseImpl.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseProvider.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/DbCode.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTx.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTyped.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTypedTx.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/Ddl.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/DdlImpl.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/Flavor.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/MixedParameterSql.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/Options.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsDefault.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsOverride.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/QueryTimedOutException.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/Row.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/RowHandler.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/Rows.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/RowsAdaptor.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/RowsHandler.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/Schema.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/SecretArg.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/Sql.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/SqlArgs.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsert.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsertImpl.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/SqlNull.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelect.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelectImpl.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdate.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdateImpl.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/StatementAdaptor.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/Transaction.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/TransactionImpl.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/When.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/WrongNumberOfRowsException.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/util/DebugSql.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/util/InternalStringReader.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/util/Metric.java create mode 100644 jdbc-query/src/main/java/org/xbib/jdbc/query/util/RewriteArg.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/CommonTest.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/ConfigTest.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/DatabaseMock.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/DerbyTest.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/HsqldbTest.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/OracleTest.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/PostgreSqlTest.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/RowStub.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlArgsTest.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlServerTest.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DerbyExample.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DynamicSql.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/FakeBuilder.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloAny.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloDerby.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/InsertReturning.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/Sample.java create mode 100644 jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/SampleDao.java create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..168396e --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +/data +/work +/logs +/.idea +/target +.DS_Store +*.iml +/.settings +/.classpath +/.project +/.gradle +build +out diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..37ec93a --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..adfaf00 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,7 @@ +This work is based upon + +https://github.com/susom/database + +as of 28 Dec 2021 + +License: Apache 2.0 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..2292756 --- /dev/null +++ b/build.gradle @@ -0,0 +1,32 @@ +plugins { + id "de.marcphilipp.nexus-publish" version "0.4.0" + id "io.codearte.nexus-staging" version "0.21.1" +} + +wrapper { + gradleVersion = "${project.property('gradle.wrapper.version')}" + distributionType = Wrapper.DistributionType.ALL +} + +ext { + user = 'jprante' + name = 'database' + description = 'JDBC connection pool and utilities' + inceptionYear = '2018' + url = 'https://github.com/' + user + '/' + name + scmUrl = 'https://github.com/' + user + '/' + name + scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' + scmDeveloperConnection = 'scm:git:ssh://git@github.com:' + user + '/' + name + '.git' + issueManagementSystem = 'Github' + issueManagementUrl = ext.scmUrl + '/issues' + licenseName = 'The Apache License, Version 2.0' + licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' +} + +subprojects { + apply plugin: 'java-library' + apply from: rootProject.file('gradle/ide/idea.gradle') + apply from: rootProject.file('gradle/compile/java.gradle') + apply from: rootProject.file('gradle/test/junit5.gradle') + apply from: rootProject.file('gradle/publishing/publication.gradle') +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..bf36966 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,11 @@ +group = org.xbib +name = database +version = 0.0.1 + +org.gradle.warning.mode = ALL +gradle.wrapper.version = 7.3.2 +h2.version = 1.4.200 +mockito.version = 3.3.3 +testcontainers.version = 1.16.2 +derby.version = 10.15.2.0 +oracle-client.version = 21.4.0.0 diff --git a/gradle/compile/java.gradle b/gradle/compile/java.gradle new file mode 100644 index 0000000..c9bba7f --- /dev/null +++ b/gradle/compile/java.gradle @@ -0,0 +1,43 @@ + +apply plugin: 'java-library' + +java { + modularity.inferModulePath.set(true) +} + +compileJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +jar { + manifest { + attributes('Implementation-Version': project.version) + } +} + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier 'javadoc' +} + +artifacts { + archives sourcesJar, javadocJar +} + +tasks.withType(JavaCompile) { + options.compilerArgs << '-Xlint:all,-fallthrough' +} + +javadoc { + options.addStringOption('Xdoclint:none', '-quiet') +} diff --git a/gradle/documentation/asciidoc.gradle b/gradle/documentation/asciidoc.gradle new file mode 100644 index 0000000..87ba22e --- /dev/null +++ b/gradle/documentation/asciidoc.gradle @@ -0,0 +1,55 @@ +apply plugin: 'org.xbib.gradle.plugin.asciidoctor' + +configurations { + asciidoclet +} + +dependencies { + asciidoclet "org.asciidoctor:asciidoclet:${project.property('asciidoclet.version')}" +} + + +asciidoctor { + backends 'html5' + outputDir = file("${rootProject.projectDir}/docs") + separateOutputDirs = false + attributes 'source-highlighter': 'coderay', + idprefix: '', + idseparator: '-', + toc: 'left', + doctype: 'book', + icons: 'font', + encoding: 'utf-8', + sectlink: true, + sectanchors: true, + linkattrs: true, + imagesdir: 'img', + stylesheet: "${projectDir}/src/docs/asciidoc/css/foundation.css" +} + + +/*javadoc { +options.docletpath = configurations.asciidoclet.files.asType(List) +options.doclet = 'org.asciidoctor.Asciidoclet' +//options.overview = "src/docs/asciidoclet/overview.adoc" +options.addStringOption "-base-dir", "${projectDir}" +options.addStringOption "-attribute", + "name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}" +configure(options) { + noTimestamp = true +} +}*/ + + +/*javadoc { + options.docletpath = configurations.asciidoclet.files.asType(List) + options.doclet = 'org.asciidoctor.Asciidoclet' + options.overview = "${rootProject.projectDir}/src/docs/asciidoclet/overview.adoc" + options.addStringOption "-base-dir", "${projectDir}" + options.addStringOption "-attribute", + "name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}" + options.destinationDirectory(file("${projectDir}/docs/javadoc")) + configure(options) { + noTimestamp = true + } +}*/ diff --git a/gradle/ide/idea.gradle b/gradle/ide/idea.gradle new file mode 100644 index 0000000..64e2167 --- /dev/null +++ b/gradle/ide/idea.gradle @@ -0,0 +1,13 @@ +apply plugin: 'idea' + +idea { + module { + outputDir file('build/classes/java/main') + testOutputDir file('build/classes/java/test') + } +} + +if (project.convention.findPlugin(JavaPluginConvention)) { + //sourceSets.main.output.classesDirs = file("build/classes/java/main") + //sourceSets.test.output.classesDirs = file("build/classes/java/test") +} diff --git a/gradle/publish.gradle b/gradle/publish.gradle new file mode 100644 index 0000000..e69de29 diff --git a/gradle/publishing/publication.gradle b/gradle/publishing/publication.gradle new file mode 100644 index 0000000..c35fcb9 --- /dev/null +++ b/gradle/publishing/publication.gradle @@ -0,0 +1,64 @@ + +apply plugin: "de.marcphilipp.nexus-publish" + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + pom { + name = project.name + description = rootProject.ext.description + url = rootProject.ext.url + inceptionYear = rootProject.ext.inceptionYear + packaging = 'jar' + organization { + name = 'xbib' + url = 'https://xbib.org' + } + developers { + developer { + id = 'jprante' + name = 'Jörg Prante' + email = 'joergprante@gmail.com' + url = 'https://github.com/jprante' + } + } + scm { + url = rootProject.ext.scmUrl + connection = rootProject.ext.scmConnection + developerConnection = rootProject.ext.scmDeveloperConnection + } + issueManagement { + system = rootProject.ext.issueManagementSystem + url = rootProject.ext.issueManagementUrl + } + licenses { + license { + name = rootProject.ext.licenseName + url = rootProject.ext.licenseUrl + distribution = 'repo' + } + } + } + } + } +} + +if (project.hasProperty("signing.keyId")) { + apply plugin: 'signing' + signing { + sign publishing.publications.mavenJava + } +} + +nexusPublishing { + repositories { + sonatype { + username = project.property('ossrhUsername') + password = project.property('ossrhPassword') + packageGroup = "org.xbib" + } + } +} diff --git a/gradle/publishing/sonatype.gradle b/gradle/publishing/sonatype.gradle new file mode 100644 index 0000000..e1813f3 --- /dev/null +++ b/gradle/publishing/sonatype.gradle @@ -0,0 +1,11 @@ + +if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) { + + apply plugin: 'io.codearte.nexus-staging' + + nexusStaging { + username = project.property('ossrhUsername') + password = project.property('ossrhPassword') + packageGroup = "org.xbib" + } +} diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle new file mode 100644 index 0000000..ee630fd --- /dev/null +++ b/gradle/test/junit5.gradle @@ -0,0 +1,27 @@ + +def junitVersion = project.hasProperty('junit.version')?project.property('junit.version'):'5.6.2' +def hamcrestVersion = project.hasProperty('hamcrest.version')?project.property('hamcrest.version'):'2.2' + +dependencies { + testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}" + testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}" + testImplementation "org.hamcrest:hamcrest-library:${hamcrestVersion}" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}" +} + +test { + useJUnitPlatform() + failFast = false + testLogging { + events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED' + } + afterSuite { desc, result -> + if (!desc.parent) { + println "\nTest result: ${result.resultType}" + println "Test summary: ${result.testCount} tests, " + + "${result.successfulTestCount} succeeded, " + + "${result.failedTestCount} failed, " + + "${result.skippedTestCount} skipped" + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa GIT binary patch literal 59536 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac0b842 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jdbc-connection-pool/NOTICE.txt b/jdbc-connection-pool/NOTICE.txt new file mode 100644 index 0000000..1aeb36c --- /dev/null +++ b/jdbc-connection-pool/NOTICE.txt @@ -0,0 +1,26 @@ + +This connection pool implementation is a derived work from HikariCP Version 3.4.5 (May 2020) + +https://github.com/brettwooldridge/HikariCP + +published under Apache 2.0 License. + +Motivations for the derived work: + +- remove all dependencies +- remove everything with metrics +- remove everything with JMX +- remove everything with slf4j logging +- remove everything with OSGI, Hibernate, Spring, JNDI +- remove everything with javassist +- remove everything with suspend/resume +- fix module-info.java +- remove MacOS "milli second" clock +- clean up source code, packages, inheritances, inner classes refactoring, get rid of helper classes +- no system property dark magic +- no addDataSourceProperty magic, pass a Properties object always to PoolConfig which contains JDBC driver properties +- Java 11+ +- JUnit 5+ +- Gradle 6.4+ + +The result is an 88k jar. diff --git a/jdbc-connection-pool/build.gradle b/jdbc-connection-pool/build.gradle new file mode 100644 index 0000000..c0675ac --- /dev/null +++ b/jdbc-connection-pool/build.gradle @@ -0,0 +1,4 @@ +dependencies { + testImplementation "com.h2database:h2:${project.property('h2.version')}" + testImplementation "org.mockito:mockito-core:${project.property('mockito.version')}" +} diff --git a/jdbc-connection-pool/src/main/java/module-info.java b/jdbc-connection-pool/src/main/java/module-info.java new file mode 100644 index 0000000..c22579b --- /dev/null +++ b/jdbc-connection-pool/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module org.xbib.jdbc.connection.pool { + requires java.logging; + requires transitive java.sql; + exports org.xbib.jdbc.connection.pool; + exports org.xbib.jdbc.connection.pool.util; +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/IsolationLevel.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/IsolationLevel.java new file mode 100644 index 0000000..cebb7e9 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/IsolationLevel.java @@ -0,0 +1,20 @@ +package org.xbib.jdbc.connection.pool; + +public enum IsolationLevel { + TRANSACTION_NONE(0), + TRANSACTION_READ_UNCOMMITTED(1), + TRANSACTION_READ_COMMITTED(2), + TRANSACTION_REPEATABLE_READ(4), + TRANSACTION_SERIALIZABLE(8), + TRANSACTION_SQL_SERVER_SNAPSHOT_ISOLATION_LEVEL(4096); + + private final int levelId; + + IsolationLevel(int levelId) { + this.levelId = levelId; + } + + public int getLevelId() { + return levelId; + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/Pool.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/Pool.java new file mode 100644 index 0000000..8b79b8f --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/Pool.java @@ -0,0 +1,1076 @@ +package org.xbib.jdbc.connection.pool; + +import org.xbib.jdbc.connection.pool.util.ClockSource; +import org.xbib.jdbc.connection.pool.util.Bag; +import org.xbib.jdbc.connection.pool.util.DefaultThreadFactory; +import org.xbib.jdbc.connection.pool.util.DriverDataSource; +import org.xbib.jdbc.connection.pool.util.BagStateListener; +import org.xbib.jdbc.connection.pool.util.BagEntry; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLTransientConnectionException; +import java.sql.Statement; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.sql.DataSource; + +/** + * This is the primary connection pool class that provides the basic pooling. + */ +public class Pool implements BagStateListener { + + private static final Logger logger = Logger.getLogger(Pool.class.getName()); + + private static final int POOL_NORMAL = 0; + + private static final int POOL_SHUTDOWN = 2; + + private volatile int poolState; + + private static final String EVICTED_CONNECTION_MESSAGE = "(connection was evicted)"; + + private static final String DEAD_CONNECTION_MESSAGE = "(connection is dead)"; + + private final PoolEntryCreator poolEntryCreator = new PoolEntryCreator(null); + + private final PoolEntryCreator postFillPoolEntryCreator = new PoolEntryCreator("after adding "); + + private final Collection addConnectionQueueReadOnlyView; + + private final ThreadPoolExecutor addConnectionExecutor; + + private final ThreadPoolExecutor closeConnectionExecutor; + + private final Bag bag; + + private final ProxyLeakTaskFactory leakTaskFactory; + + private final ScheduledExecutorService houseKeepingExecutorService; + + private ScheduledFuture houseKeeperTask; + + private final PoolConfig config; + + private final String poolName; + + private String catalog; + + private final AtomicReference lastConnectionFailure; + + private long connectionTimeout; + + private long validationTimeout; + + private static final int UNINITIALIZED = -1; + + private static final int TRUE = 1; + + private static final int FALSE = 0; + + private int networkTimeout; + + private int isNetworkTimeoutSupported; + + private int isQueryTimeoutSupported; + + private int defaultTransactionIsolation; + + private int transactionIsolation; + + private DataSource dataSource; + + private final String schema; + + private final boolean isReadOnly; + + private final boolean isAutoCommit; + + private final boolean isUseJdbc4Validation; + + private final boolean isIsolateInternalQueries; + + private volatile boolean isValidChecked; + + /** + * Construct a {@link Pool} with the specified configuration. + * + * @param config the config + */ + public Pool(PoolConfig config) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + config.validate(); + this.config = config; + logger.log(Level.INFO, () -> "starting new pool: " + config.getPoolName()); + this.networkTimeout = UNINITIALIZED; + this.catalog = config.getCatalog(); + this.schema = config.getSchema(); + this.isReadOnly = config.isReadOnly(); + this.isAutoCommit = config.isAutoCommit(); + this.transactionIsolation = getTransactionIsolation(config.getTransactionIsolation()); + this.isQueryTimeoutSupported = UNINITIALIZED; + this.isNetworkTimeoutSupported = UNINITIALIZED; + this.isUseJdbc4Validation = config.getConnectionTestQuery() == null; + this.isIsolateInternalQueries = config.isIsolateInternalQueries(); + this.poolName = config.getPoolName(); + this.connectionTimeout = config.getConnectionTimeout(); + this.validationTimeout = config.getValidationTimeout(); + this.lastConnectionFailure = new AtomicReference<>(); + initializeDataSource(); + this.bag = new Bag<>(this); + this.houseKeepingExecutorService = initializeHouseKeepingExecutorService(); + long initializationTimeout = config.getInitializationFailTimeout(); + if (initializationTimeout >= 0) { + checkFailFast(initializationTimeout); + } + ThreadFactory threadFactory = config.getThreadFactory(); + int maxPoolSize = config.getMaximumPoolSize(); + LinkedBlockingQueue addConnectionQueue = new LinkedBlockingQueue<>(maxPoolSize); + this.addConnectionQueueReadOnlyView = Collections.unmodifiableCollection(addConnectionQueue); + this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardOldestPolicy()); + this.closeConnectionExecutor = createThreadPoolExecutor(maxPoolSize, poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy()); + this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService); + this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, config.getHousekeepingPeriodMs(), TimeUnit.MILLISECONDS); + if (Boolean.getBoolean("pool.jdbc.blockUntilFilled") && config.getInitializationFailTimeout() > 1) { + addConnectionExecutor.setCorePoolSize(Math.min(16, Runtime.getRuntime().availableProcessors())); + addConnectionExecutor.setMaximumPoolSize(Math.min(16, Runtime.getRuntime().availableProcessors())); + final long startTime = ClockSource.currentTime(); + while (ClockSource.elapsedMillis(startTime) < config.getInitializationFailTimeout() && getTotalConnections() < config.getMinimumIdle()) { + quietlySleep(TimeUnit.MILLISECONDS.toMillis(100)); + } + addConnectionExecutor.setCorePoolSize(1); + addConnectionExecutor.setMaximumPoolSize(1); + } + } + + public PoolConfig getConfig() { + return config; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return poolName; + } + + public void quietlyCloseConnection(Connection connection, String closureReason) { + if (connection != null) { + try { + logger.log(Level.FINE, () -> MessageFormat.format("{0} closing connection {1} {2}", poolName, connection, closureReason)); + try (connection) { + setNetworkTimeout(connection, TimeUnit.SECONDS.toMillis(15)); + } catch (SQLException e) { + // ignore + } + } catch (Exception e) { + logger.log(Level.WARNING, "closing connection failed: " + poolName + " " + connection, e); + } + } + } + + public boolean isConnectionAlive(Connection connection) { + try { + try { + setNetworkTimeout(connection, validationTimeout); + final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000; + if (isUseJdbc4Validation) { + return connection.isValid(validationSeconds); + } + try (Statement statement = connection.createStatement()) { + if (isNetworkTimeoutSupported != TRUE) { + setQueryTimeout(statement, validationSeconds); + } + statement.execute(config.getConnectionTestQuery()); + } + } finally { + setNetworkTimeout(connection, networkTimeout); + if (isIsolateInternalQueries && !isAutoCommit) { + connection.rollback(); + } + } + return true; + } catch (Exception e) { + lastConnectionFailure.set(e); + logger.log(Level.WARNING, "failed to validate connection, possibly consider using a shorter maxLifetime value: " + + poolName + " " + connection + " " + e.getMessage(), e); + return false; + } + } + + public Exception getLastConnectionFailure() { + return lastConnectionFailure.get(); + } + + public DataSource getUnwrappedDataSource() { + return dataSource; + } + + public PoolEntry newPoolEntry() throws Exception { + return new PoolEntry(newConnection(), this, isReadOnly, isAutoCommit); + } + + public long getLoginTimeout() { + try { + return (dataSource != null) ? dataSource.getLoginTimeout() : TimeUnit.SECONDS.toSeconds(5); + } catch (SQLException e) { + return TimeUnit.SECONDS.toSeconds(5); + } + } + + /** + * Get a connection from the pool, or timeout after connectionTimeout milliseconds. + * + * @return a java.sql.Connection instance + * @throws SQLException thrown if a timeout occurs trying to obtain a connection + */ + public Connection getConnection() throws SQLException { + return getConnection(connectionTimeout); + } + + /** + * Get a connection from the pool, or timeout after the specified number of milliseconds. + * + * @param hardTimeout the maximum time to wait for a connection from the pool + * @return a java.sql.Connection instance + * @throws SQLException thrown if a timeout occurs trying to obtain a connection + */ + public Connection getConnection(long hardTimeout) throws SQLException { + long startTime = ClockSource.currentTime(); + try { + long timeout = hardTimeout; + do { + PoolEntry poolEntry = bag.borrow(timeout, TimeUnit.MILLISECONDS); + if (poolEntry == null) { + break; + } + long now = ClockSource.currentTime(); + if (poolEntry.isMarkedEvicted() || + (ClockSource.elapsedMillis(poolEntry.getLastAccessed(), now) > config.getAliveBypassWindowMs() && + !isConnectionAlive(poolEntry.getConnection()))) { + closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE); + timeout = hardTimeout - ClockSource.elapsedMillis(startTime); + } else { + return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now); + } + } while (timeout > 0L); + throw createTimeoutException(startTime); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new SQLException(poolName + " - Interrupted during connection acquisition", e); + } + } + + /** + * Shutdown the pool, closing all idle connections and aborting or closing + * active connections. + * + * @throws InterruptedException thrown if the thread is interrupted during shutdown + */ + public synchronized void shutdown() throws InterruptedException { + try { + poolState = POOL_SHUTDOWN; + if (addConnectionExecutor == null) { + return; + } + logPoolState("before shutdown"); + if (houseKeeperTask != null) { + houseKeeperTask.cancel(false); + houseKeeperTask = null; + } + softEvictConnections(); + addConnectionExecutor.shutdown(); + addConnectionExecutor.awaitTermination(getLoginTimeout(), TimeUnit.SECONDS); + destroyHouseKeepingExecutorService(); + bag.close(); + final ExecutorService assassinExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connection assassinator", + config.getThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); + try { + final long start = ClockSource.currentTime(); + do { + abortActiveConnections(assassinExecutor); + softEvictConnections(); + } while (getTotalConnections() > 0 && ClockSource.elapsedMillis(start) < TimeUnit.SECONDS.toMillis(10)); + } finally { + assassinExecutor.shutdown(); + assassinExecutor.awaitTermination(10L, TimeUnit.SECONDS); + } + closeConnectionExecutor.shutdown(); + closeConnectionExecutor.awaitTermination(10L, TimeUnit.SECONDS); + } finally { + logPoolState("after shutdown"); + } + } + + /** + * Evict a Connection from the pool. + * + * @param connection the Connection to evict (actually a {@link ProxyConnection}) + */ + public void evictConnection(Connection connection) { + ProxyConnection proxyConnection = (ProxyConnection) connection; + proxyConnection.cancelLeakTask(); + try { + softEvictConnection(proxyConnection.getPoolEntry(), "(connection evicted by user)", !connection.isClosed()); + } catch (SQLException e) { + // unreachable + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addBagItem(final int waiting) { + final boolean shouldAdd = waiting - addConnectionQueueReadOnlyView.size() >= 0; + if (shouldAdd) { + addConnectionExecutor.submit(poolEntryCreator); + } else { + logger.log(Level.FINE, () -> "add connection elided, waiting, queue: " + + poolName + " " + waiting + " " + addConnectionQueueReadOnlyView.size()); + } + } + + public int getActiveConnections() { + return bag.getCount(BagEntry.STATE_IN_USE); + } + + public int getIdleConnections() { + return bag.getCount(BagEntry.STATE_NOT_IN_USE); + } + + public int getTotalConnections() { + return bag.size(); + } + + public int getThreadsAwaitingConnection() { + return bag.getWaitingThreadCount(); + } + + public void softEvictConnections() { + bag.values().forEach(poolEntry -> softEvictConnection(poolEntry, "(connection evicted)", false)); + } + + /** + * Log the current pool state. + * + * @param prefix an optional prefix to prepend the log message + */ + public void logPoolState(String... prefix) { + logger.log(Level.FINE, () -> MessageFormat.format("{0} {1} stats: total={2} active={3} idle={4} waiting={5}", + poolName, (prefix.length > 0 ? prefix[0] : ""), + getTotalConnections(), getActiveConnections(), getIdleConnections(), getThreadsAwaitingConnection())); + } + + /** + * Recycle PoolEntry (add back to the pool) + * + * @param poolEntry the PoolEntry to recycle + */ + public void recycle(final PoolEntry poolEntry) { + bag.requite(poolEntry); + } + + /** + * Permanently close the real (underlying) connection (eat any exception). + * + * @param poolEntry poolEntry having the connection to close + * @param closureReason reason to close + */ + public void closeConnection(PoolEntry poolEntry, String closureReason) { + if (bag.remove(poolEntry)) { + final Connection connection = poolEntry.close(); + closeConnectionExecutor.execute(() -> { + quietlyCloseConnection(connection, closureReason); + if (poolState == POOL_NORMAL) { + fillPool(); + } + }); + } + } + + private void initializeDataSource() + throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + String jdbcUrl = config.getProperties().getProperty("url"); + String dsClassName = config.getDataSourceClassName(); + DataSource ds = config.getDataSource(); + if (ds == null) { + if (dsClassName != null) { + Class clazz = Class.forName(dsClassName, true, ClassLoader.getSystemClassLoader()); + ds = (DataSource) clazz.getDeclaredConstructor().newInstance(); + } else if (jdbcUrl != null) { + ds = new DriverDataSource(jdbcUrl, config.getDriverClassName(), config.getProperties(), config.getUsername(), config.getPassword()); + } + } + logger.log(Level.INFO, () -> "got data source, setting props = " + config.getProperties()); + setTargetFromProperties(ds, config.getProperties()); + setLoginTimeout(ds); + this.dataSource = ds; + } + + private Connection newConnection() throws Exception { + Connection connection = null; + try { + String username = config.getUsername(); + String password = config.getPassword(); + connection = username == null ? + dataSource.getConnection() : dataSource.getConnection(username, password); + if (connection == null) { + throw new SQLTransientConnectionException("dataSource returned null unexpectedly"); + } + setupConnection(connection); + lastConnectionFailure.set(null); + return connection; + } catch (Exception e) { + if (connection != null) { + quietlyCloseConnection(connection, "(failed to create/setup connection)"); + } else if (getLastConnectionFailure() == null) { + logger.log(Level.FINE, () -> "failed to create/setup connection:" + + poolName + " " + e.getMessage()); + } + lastConnectionFailure.set(e); + throw e; + } + } + + /** + * Setup a connection initial state. + * + * @param connection a Connection + * @throws PoolEntryException thrown if any exception is encountered + */ + private void setupConnection(final Connection connection) throws PoolEntryException { + try { + if (networkTimeout == UNINITIALIZED) { + networkTimeout = getAndSetNetworkTimeout(connection, validationTimeout); + } else { + setNetworkTimeout(connection, validationTimeout); + } + if (connection.isReadOnly() != isReadOnly) { + connection.setReadOnly(isReadOnly); + } + if (connection.getAutoCommit() != isAutoCommit) { + connection.setAutoCommit(isAutoCommit); + } + checkDriverSupport(connection); + if (transactionIsolation != defaultTransactionIsolation) { + connection.setTransactionIsolation(transactionIsolation); + } + if (catalog != null) { + connection.setCatalog(catalog); + } + if (schema != null) { + connection.setSchema(schema); + } + executeSql(connection, config.getConnectionInitSql(), true); + setNetworkTimeout(connection, networkTimeout); + } catch (SQLException e) { + throw new PoolEntryException(e); + } + } + + /** + * Execute isValid() or connection test query. + * + * @param connection a Connection to check + */ + private void checkDriverSupport(final Connection connection) throws SQLException { + if (!isValidChecked) { + checkValidationSupport(connection); + checkDefaultIsolation(connection); + isValidChecked = true; + } + } + + /** + * Check whether Connection.isValid() is supported, or that the user has test query configured. + * + * @param connection a Connection to check + * @throws SQLException rethrown from the driver + */ + private void checkValidationSupport(final Connection connection) throws SQLException { + try { + if (isUseJdbc4Validation) { + connection.isValid(1); + } else { + executeSql(connection, config.getConnectionTestQuery(), false); + } + } catch (Exception | AbstractMethodError e) { + logger.log(Level.SEVERE, () -> "failed to execute connection test query: " + + poolName + " " + (isUseJdbc4Validation ? " isValid() for connection, configure" : "") + " " + e.getMessage()); + throw e; + } + } + + /** + * Check the default transaction isolation of the Connection. + * + * @param connection a Connection to check + * @throws SQLException rethrown from the driver + */ + private void checkDefaultIsolation(final Connection connection) throws SQLException { + try { + defaultTransactionIsolation = connection.getTransactionIsolation(); + if (transactionIsolation == -1) { + transactionIsolation = defaultTransactionIsolation; + } + } catch (SQLException e) { + logger.log(Level.WARNING, () -> "default transaction isolation level detection failed: " + + poolName + " " + e.getMessage()); + if (e.getSQLState() != null && !e.getSQLState().startsWith("08")) { + throw e; + } + } + } + + /** + * Set the query timeout, if it is supported by the driver. + * + * @param statement a statement to set the query timeout on + * @param timeoutSec the number of seconds before timeout + */ + private void setQueryTimeout(final Statement statement, final int timeoutSec) { + if (isQueryTimeoutSupported != FALSE) { + try { + statement.setQueryTimeout(timeoutSec); + isQueryTimeoutSupported = TRUE; + } catch (Exception e) { + if (isQueryTimeoutSupported == UNINITIALIZED) { + isQueryTimeoutSupported = FALSE; + logger.log(Level.INFO, () -> "failed to set query timeout for statement: " + + poolName + " " + e.getMessage()); + } + } + } + } + + /** + * Set the network timeout. Return the pre-existing value of the network timeout. + * + * @param connection the connection to set the network timeout on + * @param timeoutMs the number of milliseconds before timeout + * @return the pre-existing network timeout value + */ + private int getAndSetNetworkTimeout(final Connection connection, final long timeoutMs) { + if (isNetworkTimeoutSupported != FALSE) { + try { + final int originalTimeout = connection.getNetworkTimeout(); + connection.setNetworkTimeout(Runnable::run, (int) timeoutMs); + isNetworkTimeoutSupported = TRUE; + return originalTimeout; + } catch (Exception | AbstractMethodError e) { + if (isNetworkTimeoutSupported == UNINITIALIZED) { + isNetworkTimeoutSupported = FALSE; + logger.log(Level.INFO, () -> "driver does not support get/set network timeout for connections: " + + poolName + " " + e.getMessage()); + if (validationTimeout < TimeUnit.SECONDS.toMillis(1)) { + logger.log(Level.WARNING, () -> "a validationTimeout of less than 1 second cannot be honored on drivers without setNetworkTimeout() support: " + poolName); + } else if (validationTimeout % TimeUnit.SECONDS.toMillis(1) != 0) { + logger.log(Level.WARNING, () -> "a validationTimeout with fractional second granularity cannot be honored on drivers without setNetworkTimeout() support: " + poolName); + } + } + } + } + return 0; + } + + /** + * Set the network timeout, + * @param connection the connection to set the network timeout on + * @param timeoutMs the number of milliseconds before timeout + * @throws SQLException throw if the connection.setNetworkTimeout() call throws + */ + private void setNetworkTimeout(final Connection connection, final long timeoutMs) throws SQLException { + if (isNetworkTimeoutSupported == TRUE) { + connection.setNetworkTimeout(Runnable::run, (int) timeoutMs); + } + } + + /** + * Execute the user-specified init SQL. + * + * @param connection the connection to initialize + * @param sql the SQL to execute + * @param isCommit whether to commit the SQL after execution or not + * @throws SQLException throws if the init SQL execution fails + */ + private void executeSql(final Connection connection, final String sql, final boolean isCommit) throws SQLException { + if (sql != null) { + try (Statement statement = connection.createStatement()) { + // connection was created a few milliseconds before, so set query timeout is omitted (we assume it will succeed) + statement.execute(sql); + } + if (isIsolateInternalQueries && !isAutoCommit) { + if (isCommit) { + connection.commit(); + } else { + connection.rollback(); + } + } + } + } + + /** + * Set the loginTimeout on the specified DataSource. + * + * @param dataSource the DataSource + */ + private void setLoginTimeout(final DataSource dataSource) { + if (connectionTimeout != Integer.MAX_VALUE) { + try { + dataSource.setLoginTimeout(Math.max(1, (int) TimeUnit.MILLISECONDS.toSeconds(500L + connectionTimeout))); + } catch (Exception e) { + logger.log(Level.INFO, () -> "failed to set login timeout for data source: " + + poolName + " " + e.getMessage()); + } + } + } + + /** + * Get the int value of a transaction isolation level by name. + * + * @param transactionIsolationName the name of the transaction isolation level + * @return the int value of the isolation level or -1 + */ + private int getTransactionIsolation(String transactionIsolationName) { + if (transactionIsolationName != null) { + try { + // use the english locale to avoid the infamous turkish locale bug + final String upperCaseIsolationLevelName = transactionIsolationName.toUpperCase(Locale.ENGLISH); + return IsolationLevel.valueOf(upperCaseIsolationLevelName).getLevelId(); + } catch (IllegalArgumentException e) { + // legacy support for passing an integer version of the isolation level + try { + final int level = Integer.parseInt(transactionIsolationName); + for (IsolationLevel iso : IsolationLevel.values()) { + if (iso.getLevelId() == level) { + return iso.getLevelId(); + } + } + throw new IllegalArgumentException("Invalid transaction isolation value: " + transactionIsolationName); + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException("Invalid transaction isolation value: " + transactionIsolationName, nfe); + } + } + } + return -1; + } + + /** + * Creating new poolEntry. If maxLifetime is configured, create a future End-of-life task with 2.5% variance from + * the maxLifetime time to ensure there is no massive die-off of Connections in the pool. + */ + private PoolEntry createPoolEntry() { + try { + final PoolEntry poolEntry = newPoolEntry(); + final long maxLifetime = config.getMaxLifetime(); + if (maxLifetime > 0) { + final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong(maxLifetime / 40) : 0; + final long lifetime = maxLifetime - variance; + poolEntry.setFutureEol(houseKeepingExecutorService.schedule(() -> { + logger.log(Level.FINE, () -> "end-of life check, lifetime = " + lifetime + " variance = " + variance); + if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false)) { + logger.log(Level.FINE, () -> "end-of life check: connection has passed life time"); + addBagItem(bag.getWaitingThreadCount()); + } + }, lifetime, TimeUnit.MILLISECONDS)); + } else { + logger.log(Level.FINE, () -> "max life time is 0 or less, ignoring"); + } + return poolEntry; + } catch (PoolEntryException e) { + if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently + logger.log(Level.SEVERE, "error thrown while acquiring connection from data source: " + poolName + " " + e.getCause()); + lastConnectionFailure.set(e); + } + } catch (Exception e) { + if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently + logger.log(Level.FINE, () -> "can not acquire connection from data source: " + poolName + " " + e.getMessage()); + } + } + return null; + } + + /** + * Fill pool up from current idle connections (as they are perceived at the point of execution) to minimumIdle connections. + */ + private synchronized void fillPool() { + int connectionsToAdd = Math.min(config.getMaximumPoolSize() - getTotalConnections(), config.getMinimumIdle() - getIdleConnections()) + - addConnectionQueueReadOnlyView.size(); + if (connectionsToAdd <= 0) { + logger.log(Level.FINE, () -> "fill pool skipped, pool is at sufficient level: " + poolName); + } + for (int i = 0; i < connectionsToAdd; i++) { + addConnectionExecutor.submit((i < connectionsToAdd - 1) ? poolEntryCreator : postFillPoolEntryCreator); + } + } + + /** + * Attempt to abort or close active connections. + * + * @param assassinExecutor the ExecutorService to pass to Connection.abort() + */ + private void abortActiveConnections(final ExecutorService assassinExecutor) { + for (PoolEntry poolEntry : bag.values(BagEntry.STATE_IN_USE)) { + Connection connection = poolEntry.close(); + try { + connection.abort(assassinExecutor); + } catch (Throwable e) { + quietlyCloseConnection(connection, "(connection aborted during shutdown)"); + } finally { + bag.remove(poolEntry); + } + } + } + + /** + * If initializationFailFast is configured, check that we have DB connectivity. + * @param initializationTimeout initialization timeout + * @throws PoolInitializationException if fails to create or validate connection + * @see PoolConfig#setInitializationFailTimeout(long) + */ + private void checkFailFast(long initializationTimeout) { + final long startTime = ClockSource.currentTime(); + do { + PoolEntry poolEntry = createPoolEntry(); + if (poolEntry != null) { + if (config.getMinimumIdle() >= 0) { + bag.add(poolEntry); + logger.log(Level.FINE, () -> MessageFormat.format("{0} added connection: {1}", + poolName, poolEntry.getConnection())); + } else { + quietlyCloseConnection(poolEntry.close(), "(initialization check complete and minimumIdle is zero)"); + } + return; + } + if (getLastConnectionFailure() instanceof PoolEntryException) { + throwPoolInitializationException(getLastConnectionFailure().getCause()); + } + quietlySleep(TimeUnit.SECONDS.toMillis(1)); + } while (ClockSource.elapsedMillis(startTime) < initializationTimeout); + if (initializationTimeout > 0) { + throwPoolInitializationException(getLastConnectionFailure()); + } + } + + /** + * Log the Throwable that caused pool initialization to fail, and then throw a PoolInitializationException with + * that cause attached. + * + * @param t the Throwable that caused the pool to fail to initialize (possibly null) + */ + private void throwPoolInitializationException(Throwable t) { + logger.log(Level.SEVERE, "exception during pool initialization: " + poolName + " " + t.getMessage(), t); + destroyHouseKeepingExecutorService(); + throw new PoolInitializationException(t); + } + + /** + * "Soft" evict a Connection (/PoolEntry) from the pool. If this method is being called by the user directly + * through {@link PoolDataSource#evictConnection(Connection)} then {@code owner} is {@code true}. + * If the caller is the owner, or if the Connection is idle (i.e. can be "reserved" in the {@link Bag}), + * then we can close the connection immediately. Otherwise, we leave it "marked" for eviction so that it is evicted + * the next time someone tries to acquire it from the pool. + * + * @param poolEntry the PoolEntry (/Connection) to "soft" evict from the pool + * @param reason the reason that the connection is being evicted + * @param owner true if the caller is the owner of the connection, false otherwise + * @return true if the connection was evicted (closed), false if it was merely marked for eviction + */ + private boolean softEvictConnection(final PoolEntry poolEntry, final String reason, final boolean owner) { + poolEntry.markEvicted(); + if (owner || bag.reserve(poolEntry)) { + closeConnection(poolEntry, reason); + return true; + } + return false; + } + + /** + * Create/initialize the Housekeeping service {@link ScheduledExecutorService}. If the user specified an Executor + * to be used in the {@link PoolConfig}, then we use that. If no Executor was specified (typical), then create + * an Executor and configure it. + * + * @return either the user specified {@link ScheduledExecutorService}, or the one we created + */ + private ScheduledExecutorService initializeHouseKeepingExecutorService() { + if (config.getScheduledExecutor() == null) { + ThreadFactory threadFactory = Optional.ofNullable(config.getThreadFactory()).orElseGet(() -> + new DefaultThreadFactory(poolName + "-housekeeper", true)); + ScheduledThreadPoolExecutor executor = + new ScheduledThreadPoolExecutor(1, threadFactory, + new ThreadPoolExecutor.DiscardPolicy()); + executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + executor.setRemoveOnCancelPolicy(true); + return executor; + } else { + return config.getScheduledExecutor(); + } + } + + /** + * Destroy (/shutdown) the Housekeeping service Executor, if it was the one that we created. + */ + private void destroyHouseKeepingExecutorService() { + if (config.getScheduledExecutor() == null) { + houseKeepingExecutorService.shutdownNow(); + } + } + + /** + * Create a timeout exception (specifically, {@link SQLTransientConnectionException}) to be thrown, because a + * timeout occurred when trying to acquire a Connection from the pool. If there was an underlying cause for the + * timeout, e.g. a SQLException thrown by the driver while trying to create a new Connection, then use the + * SQL State from that exception as our own and additionally set that exception as the "next" SQLException inside + * of our exception. + * As a side-effect, log the timeout failure at DEBUG, and record the timeout failure in the metrics tracker. + * + * @param startTime the start time (timestamp) of the acquisition attempt + * @return a SQLException to be thrown from {@link #getConnection()} + */ + private SQLException createTimeoutException(long startTime) { + logPoolState("timeout failure"); + String sqlState = null; + Throwable originalException = getLastConnectionFailure(); + if (originalException instanceof SQLException) { + sqlState = ((SQLException) originalException).getSQLState(); + } + SQLException connectionException = new SQLTransientConnectionException(poolName + + " connection is not available, request timed out after " + + ClockSource.elapsedMillis(startTime) + " ms", sqlState, originalException); + if (originalException instanceof SQLException) { + connectionException.setNextException((SQLException) originalException); + } + return connectionException; + } + + /** + * Create a ThreadPoolExecutor. + * + * @param queueSize the queue size + * @param threadName the thread name + * @param threadFactory an optional ThreadFactory + * @param policy the RejectedExecutionHandler policy + * @return a ThreadPoolExecutor + */ + private ThreadPoolExecutor createThreadPoolExecutor(int queueSize, + String threadName, + ThreadFactory threadFactory, + RejectedExecutionHandler policy) { + if (threadFactory == null) { + threadFactory = new DefaultThreadFactory(threadName, true); + } + LinkedBlockingQueue queue = new LinkedBlockingQueue<>(queueSize); + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, + 5, TimeUnit.SECONDS, queue, threadFactory, policy); + executor.allowCoreThreadTimeOut(true); + return executor; + } + + /** + * Create a ThreadPoolExecutor. + * + * @param queue the BlockingQueue to use + * @param threadName the thread name + * @param threadFactory an optional ThreadFactory + * @param policy the RejectedExecutionHandler policy + * @return a ThreadPoolExecutor + */ + private ThreadPoolExecutor createThreadPoolExecutor(BlockingQueue queue, + String threadName, + ThreadFactory threadFactory, + RejectedExecutionHandler policy) { + if (threadFactory == null) { + threadFactory = new DefaultThreadFactory(threadName, true); + } + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, + 5, TimeUnit.SECONDS, queue, threadFactory, policy); + executor.allowCoreThreadTimeOut(true); + return executor; + } + + /** + * Sleep and suppress InterruptedException (but re-signal it). + * + * @param millis the number of milliseconds to sleep + */ + private static void quietlySleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static void setTargetFromProperties(Object target, Properties properties) { + if (target == null || properties == null) { + return; + } + List methods = Arrays.asList(target.getClass().getMethods()); + properties.forEach((key, value) -> setProperty(target, key.toString(), value, methods)); + } + + private static void setProperty(Object target, String propName, Object propValue, List methods) { + String methodName = "set" + propName.substring(0, 1).toUpperCase(Locale.ENGLISH) + propName.substring(1); + Method writeMethod = methods.stream() + .filter(m -> m.getName().equals(methodName) && m.getParameterCount() == 1) + .findFirst().orElse(null); + if (writeMethod == null) { + String methodName2 = "set" + propName.toUpperCase(Locale.ENGLISH); + writeMethod = methods.stream() + .filter(m -> m.getName().equals(methodName2) && m.getParameterCount() == 1) + .findFirst().orElse(null); + } + if (writeMethod == null) { + logger.log(Level.SEVERE, "property does not exist on target: " + propName + " " + target.getClass()); + throw new RuntimeException(String.format("property %s does not exist on target %s", propName, target.getClass())); + } + try { + Class paramClass = writeMethod.getParameterTypes()[0]; + if (paramClass == int.class) { + writeMethod.invoke(target, Integer.parseInt(propValue.toString())); + } else if (paramClass == long.class) { + writeMethod.invoke(target, Long.parseLong(propValue.toString())); + } else if (paramClass == boolean.class || paramClass == Boolean.class) { + writeMethod.invoke(target, Boolean.parseBoolean(propValue.toString())); + } else if (paramClass == String.class) { + logger.log(Level.FINE, () -> + MessageFormat.format("write method {0} {1}", target, propValue)); + writeMethod.invoke(target, propValue.toString()); + } else { + try { + logger.log(Level.FINE, () -> + MessageFormat.format("try to create a new instance of {0}", propValue)); + writeMethod.invoke(target, Class.forName(propValue.toString()).getDeclaredConstructor().newInstance()); + } catch (InstantiationException | ClassNotFoundException e) { + logger.log(Level.FINE, () -> + MessageFormat.format("class not found or could not instantiate it (Default constructor): {0}", + propValue)); + writeMethod.invoke(target, propValue); + } + } + } catch (Exception e) { + logger.log(Level.WARNING, () -> + MessageFormat.format("failed to set property {0} on target {1}", propName, target.getClass())); + throw new RuntimeException(e); + } + } + + /** + * Creating and adding poolEntries (connections) to the pool. + */ + private final class PoolEntryCreator implements Callable { + + private final String loggingPrefix; + + private PoolEntryCreator(String loggingPrefix) { + this.loggingPrefix = loggingPrefix; + } + + @Override + public Boolean call() { + long sleepBackoff = 250L; + while (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) { + final PoolEntry poolEntry = createPoolEntry(); + if (poolEntry != null) { + bag.add(poolEntry); + logger.log(Level.FINE, () -> MessageFormat.format("{0}: added connection {1} ", + poolName, poolEntry.getConnection())); + if (loggingPrefix != null) { + logPoolState(loggingPrefix); + } + return Boolean.TRUE; + } + if (loggingPrefix != null) { + logger.log(Level.FINE, () -> "connection add failed, sleeping with backoff" + poolName); + } + quietlySleep(sleepBackoff); + sleepBackoff = Math.min(TimeUnit.SECONDS.toMillis(10), Math.min(connectionTimeout, (long) (sleepBackoff * 1.5))); + } + return Boolean.FALSE; + } + + /** + * We only create connections if we need another idle connection or have threads still waiting + * for a new connection. Otherwise we bail out of the request to create. + * + * @return true if we should create a connection, false if the need has disappeared + */ + private synchronized boolean shouldCreateAnotherConnection() { + return getTotalConnections() < config.getMaximumPoolSize() && + (bag.getWaitingThreadCount() > 0 || getIdleConnections() < config.getMinimumIdle()); + } + } + + /** + * The house keeping task to retire and maintain minimum idle connections. + */ + private final class HouseKeeper implements Runnable { + + private volatile long previous = ClockSource.plusMillis(ClockSource.currentTime(), -config.getHousekeepingPeriodMs()); + + @Override + public void run() { + try { + logger.log(Level.FINE, () -> "housekeeper running"); + connectionTimeout = config.getConnectionTimeout(); + validationTimeout = config.getValidationTimeout(); + leakTaskFactory.updateLeakDetectionThreshold(config.getLeakDetectionThreshold()); + catalog = (config.getCatalog() != null && !config.getCatalog().equals(catalog)) ? config.getCatalog() : catalog; + final long idleTimeout = config.getIdleTimeout(); + final long now = ClockSource.currentTime(); + // allowing +128ms as per NTP spec + if (ClockSource.plusMillis(now, 128) < ClockSource.plusMillis(previous, config.getHousekeepingPeriodMs())) { + logger.log(Level.WARNING, "retrograde clock change detected (housekeeper delta=), soft-evicting connections from pool: " + + poolName + " " + ClockSource.elapsedDisplayString(previous, now)); + previous = now; + softEvictConnections(); + return; + } else if (now > ClockSource.plusMillis(previous, (3 * config.getHousekeepingPeriodMs()) / 2)) { + logger.log(Level.WARNING, "thread starvation or clock leap detected: " + + poolName + " housekeeper delta=" + ClockSource.elapsedDisplayString(previous, now)); + } + previous = now; + if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) { + logPoolState("before cleanup"); + final List notInUse = bag.values(BagEntry.STATE_NOT_IN_USE); + int toRemove = notInUse.size() - config.getMinimumIdle(); + for (PoolEntry entry : notInUse) { + if (toRemove > 0 && ClockSource.elapsedMillis(entry.getLastAccessed(), now) > idleTimeout && bag.reserve(entry)) { + closeConnection(entry, "(connection has passed idleTimeout)"); + toRemove--; + } + } + logPoolState("after cleanup"); + } else { + logPoolState("pool"); + } + fillPool(); + } catch (Exception e) { + logger.log(Level.SEVERE, "unexpected exception in housekeeping task: " + e.getMessage(), e); + } + } + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolConfig.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolConfig.java new file mode 100644 index 0000000..1a9bdac --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolConfig.java @@ -0,0 +1,666 @@ +package org.xbib.jdbc.connection.pool; + +import java.sql.Connection; +import java.util.Properties; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.sql.DataSource; + +public class PoolConfig { + + private static final Logger logger = Logger.getLogger(PoolConfig.class.getName()); + + private static final AtomicLong POOL_COUNTER = new AtomicLong(); + + private static final long CONNECTION_TIMEOUT = TimeUnit.SECONDS.toMillis(30); + + private static final long VALIDATION_TIMEOUT = TimeUnit.SECONDS.toMillis(5); + + private static final long IDLE_TIMEOUT = TimeUnit.MINUTES.toMillis(10); + + private static final long MAX_LIFETIME = TimeUnit.MINUTES.toMillis(30); + + private static final int DEFAULT_POOL_SIZE = 8; + + private final Properties properties; + + private volatile long connectionTimeout; + + private volatile long validationTimeout; + + private volatile long idleTimeout; + + private volatile long leakDetectionThreshold; + + private volatile long maxLifetime; + + private volatile int maxPoolSize; + + private volatile int minIdle; + + private volatile String username; + + private volatile String password; + + private long initializationFailTimeout; + + private String connectionInitSql; + + private String connectionTestQuery; + + private String dataSourceClassName; + + private String driverClassName; + + private String jdbcUrl; + + private String poolName; + + private String catalog; + + private String schema; + + private String transactionIsolationName; + + private boolean isAutoCommit; + + private boolean isReadOnly; + + private boolean isIsolateInternalQueries; + + private boolean isAllowPoolSuspension; + + private long aliveBypassWindowMs; + + private long housekeepingPeriodMs; + + private DataSource dataSource; + + private ThreadFactory threadFactory; + + private ScheduledExecutorService scheduledExecutor; + + /** + * Default constructor + */ + public PoolConfig() { + this(new Properties()); + } + + /** + * Construct a {@link PoolConfig} from the specified properties object. + * + * @param properties the name of the property file + */ + public PoolConfig(Properties properties) { + this.properties = properties; + this.minIdle = -1; + this.maxPoolSize = -1; + this.maxLifetime = MAX_LIFETIME; + this.connectionTimeout = CONNECTION_TIMEOUT; + this.validationTimeout = VALIDATION_TIMEOUT; + this.idleTimeout = IDLE_TIMEOUT; + this.initializationFailTimeout = -1; + this.isAutoCommit = true; + this.jdbcUrl = properties.getProperty("url"); + this.aliveBypassWindowMs = TimeUnit.MILLISECONDS.toMillis(500); + this.housekeepingPeriodMs = TimeUnit.SECONDS.toMillis(30); + } + + public String getCatalog() { + return catalog; + } + + public void setCatalog(String catalog) { + this.catalog = catalog; + } + + public long getConnectionTimeout() { + return connectionTimeout; + } + + public void setConnectionTimeout(long connectionTimeoutMs) { + if (connectionTimeoutMs == 0) { + this.connectionTimeout = Integer.MAX_VALUE; + } else if (connectionTimeoutMs < 250) { + throw new IllegalArgumentException("connectionTimeout cannot be less than 250ms"); + } else { + this.connectionTimeout = connectionTimeoutMs; + } + } + + public long getIdleTimeout() { + return idleTimeout; + } + + public void setIdleTimeout(long idleTimeoutMs) { + if (idleTimeoutMs < 0) { + throw new IllegalArgumentException("idleTimeout cannot be negative"); + } + this.idleTimeout = idleTimeoutMs; + } + + public long getLeakDetectionThreshold() { + return leakDetectionThreshold; + } + + public void setLeakDetectionThreshold(long leakDetectionThresholdMs) { + this.leakDetectionThreshold = leakDetectionThresholdMs; + } + + public long getMaxLifetime() { + return maxLifetime; + } + + public void setMaxLifetime(long maxLifetimeMs) { + this.maxLifetime = maxLifetimeMs; + } + + public int getMaximumPoolSize() { + return maxPoolSize; + } + + public void setMaximumPoolSize(int maxPoolSize) { + if (maxPoolSize < 1) { + throw new IllegalArgumentException("maxPoolSize cannot be less than 1"); + } + this.maxPoolSize = maxPoolSize; + } + + public int getMinimumIdle() { + return minIdle; + } + + public void setMinimumIdle(int minIdle) { + if (minIdle < 0) { + throw new IllegalArgumentException("minimumIdle cannot be negative"); + } + this.minIdle = minIdle; + } + + /** + * Get the default password to use for DataSource.getConnection(username, password) calls. + * + * @return the password + */ + public String getPassword() { + return password; + } + + /** + * Set the default password to use for DataSource.getConnection(username, password) calls. + * + * @param password the password + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Get the default username used for DataSource.getConnection(username, password) calls. + * + * @return the username + */ + public String getUsername() { + return username; + } + + /** + * Set the default username used for DataSource.getConnection(username, password) calls. + * + * @param username the username + */ + public void setUsername(String username) { + this.username = username; + } + + public long getValidationTimeout() { + return validationTimeout; + } + + public void setValidationTimeout(long validationTimeoutMs) { + if (validationTimeoutMs < 250) { + throw new IllegalArgumentException("validationTimeout cannot be less than 250ms"); + } + this.validationTimeout = validationTimeoutMs; + } + + /** + * Get the SQL query to be executed to test the validity of connections. + * + * @return the SQL query string, or null + */ + public String getConnectionTestQuery() { + return connectionTestQuery; + } + + /** + * Set the SQL query to be executed to test the validity of connections. Using + * the JDBC4 Connection.isValid() method to test connection validity can + * be more efficient on some databases and is recommended. + * + * @param connectionTestQuery a SQL query string + */ + public void setConnectionTestQuery(String connectionTestQuery) { + this.connectionTestQuery = connectionTestQuery; + } + + /** + * Get the SQL string that will be executed on all new connections when they are + * created, before they are added to the pool. + * + * @return the SQL to execute on new connections, or null + */ + public String getConnectionInitSql() { + return connectionInitSql; + } + + /** + * Set the SQL string that will be executed on all new connections when they are + * created, before they are added to the pool. If this query fails, it will be + * treated as a failed connection attempt. + * + * @param connectionInitSql the SQL to execute on new connections + */ + public void setConnectionInitSql(String connectionInitSql) { + this.connectionInitSql = connectionInitSql; + } + + /** + * Get the {@link DataSource} that has been explicitly specified to be wrapped by the + * pool. + * + * @return the {@link DataSource} instance, or null + */ + public DataSource getDataSource() { + return dataSource; + } + + /** + * Set a {@link DataSource} for the pool to explicitly wrap. This setter is not + * available through property file based initialization. + * + * @param dataSource a specific {@link DataSource} to be wrapped by the pool + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Get the name of the JDBC {@link DataSource} class used to create Connections. + * + * @return the fully qualified name of the JDBC {@link DataSource} class + */ + public String getDataSourceClassName() { + return dataSourceClassName; + } + + /** + * Set the fully qualified class name of the JDBC {@link DataSource} that will be used create Connections. + * + * @param className the fully qualified name of the JDBC {@link DataSource} class + */ + public void setDataSourceClassName(String className) { + this.dataSourceClassName = className; + } + + public Properties getProperties() { + return properties; + } + + public String getDriverClassName() { + return driverClassName; + } + + public void setDriverClassName(String driverClassName) { + Class driverClass = attemptFromContextLoader(driverClassName); + try { + if (driverClass == null) { + driverClass = this.getClass().getClassLoader().loadClass(driverClassName); + logger.log(Level.FINE, () -> "driver class found in the PoolConfig class classloader: " + driverClassName + " " + this.getClass().getClassLoader()); + } + } catch (ClassNotFoundException e) { + logger.log(Level.SEVERE, "failed to load driver class from PoolConfig class classloader: " + driverClassName + " " + this.getClass().getClassLoader()); + } + if (driverClass == null) { + throw new RuntimeException("failed to load driver class " + driverClassName + " in either of PoolConfig class loader or Thread context classloader"); + } + try { + driverClass.getConstructor().newInstance(); + this.driverClassName = driverClassName; + } catch (Exception e) { + throw new RuntimeException("Failed to instantiate class " + driverClassName, e); + } + } + + /** + * Get the default auto-commit behavior of connections in the pool. + * + * @return the default auto-commit behavior of connections + */ + public boolean isAutoCommit() { + return isAutoCommit; + } + + /** + * Set the default auto-commit behavior of connections in the pool. + * + * @param isAutoCommit the desired auto-commit default for connections + */ + public void setAutoCommit(boolean isAutoCommit) { + this.isAutoCommit = isAutoCommit; + } + + /** + * Get the pool suspension behavior (allowed or disallowed). + * + * @return the pool suspension behavior + */ + public boolean isAllowPoolSuspension() { + return isAllowPoolSuspension; + } + + /** + * Set whether or not pool suspension is allowed. There is a performance + * impact when pool suspension is enabled. Unless you need it (for a + * redundancy system for example) do not enable it. + * + * @param isAllowPoolSuspension the desired pool suspension allowance + */ + public void setAllowPoolSuspension(boolean isAllowPoolSuspension) { + this.isAllowPoolSuspension = isAllowPoolSuspension; + } + + /** + * Get the pool initialization failure timeout. See {@code #setInitializationFailTimeout(long)} + * for details. + * + * @return the number of milliseconds before the pool initialization fails + * @see PoolConfig#setInitializationFailTimeout(long) + */ + public long getInitializationFailTimeout() { + return initializationFailTimeout; + } + + /** + * Set the pool initialization failure timeout. This setting applies to pool + * initialization when {@link PoolDataSource} is constructed with a {@link PoolConfig}, + * or when {@link PoolDataSource} is constructed using the no-arg constructor + * and {@link PoolDataSource#getConnection()} is called. + *
    + *
  • Any value greater than zero will be treated as a timeout for pool initialization. + * The calling thread will be blocked from continuing until a successful connection + * to the database, or until the timeout is reached. If the timeout is reached, then + * a {@code PoolInitializationException} will be thrown.
  • + *
  • A value of zero will not prevent the pool from starting in the + * case that a connection cannot be obtained. However, upon start the pool will + * attempt to obtain a connection and validate that the {@code connectionTestQuery} + * and {@code connectionInitSql} are valid. If those validations fail, an exception + * will be thrown. If a connection cannot be obtained, the validation is skipped + * and the the pool will start and continue to try to obtain connections in the + * background. This can mean that callers to {@code DataSource#getConnection()} may + * encounter exceptions.
  • + *
  • A value less than zero will bypass any connection attempt and validation during + * startup, and therefore the pool will start immediately. The pool will continue to + * try to obtain connections in the background. This can mean that callers to + * {@code DataSource#getConnection()} may encounter exceptions.
  • + *
+ * Note that if this timeout value is greater than or equal to zero (0), and therefore an + * initial connection validation is performed, this timeout does not override the + * {@code connectionTimeout} or {@code validationTimeout}; they will be honored before this + * timeout is applied. The default value is one millisecond. + * + * @param initializationFailTimeout the number of milliseconds before the + * pool initialization fails, or 0 to validate connection setup but continue with + * pool start, or less than zero to skip all initialization checks and start the + * pool without delay. + */ + public void setInitializationFailTimeout(long initializationFailTimeout) { + this.initializationFailTimeout = initializationFailTimeout; + } + + /** + * Determine whether internal pool queries, principally aliveness checks, will be isolated in their own transaction + * via {@link Connection#rollback()}. Defaults to {@code false}. + * + * @return {@code true} if internal pool queries are isolated, {@code false} if not + */ + public boolean isIsolateInternalQueries() { + return isIsolateInternalQueries; + } + + /** + * Configure whether internal pool queries, principally aliveness checks, will be isolated in their own transaction + * via {@link Connection#rollback()}. Defaults to {@code false}. + * + * @param isolate {@code true} if internal pool queries should be isolated, {@code false} if not + */ + public void setIsolateInternalQueries(boolean isolate) { + this.isIsolateInternalQueries = isolate; + } + + /** + * Determine whether the Connections in the pool are in read-only mode. + * + * @return {@code true} if the Connections in the pool are read-only, {@code false} if not + */ + public boolean isReadOnly() { + return isReadOnly; + } + + /** + * Configures the Connections to be added to the pool as read-only Connections. + * + * @param readOnly {@code true} if the Connections in the pool are read-only, {@code false} if not + */ + public void setReadOnly(boolean readOnly) { + this.isReadOnly = readOnly; + } + + public String getPoolName() { + return poolName; + } + + /** + * Set the name of the connection pool. This is primarily used for the MBean + * to uniquely identify the pool configuration. + * + * @param poolName the name of the connection pool to use + */ + public void setPoolName(String poolName) { + this.poolName = poolName; + } + + /** + * Get the ScheduledExecutorService used for housekeeping. + * + * @return the executor + */ + public ScheduledExecutorService getScheduledExecutor() { + return scheduledExecutor; + } + + /** + * Set the ScheduledExecutorService used for housekeeping. + * + * @param executor the ScheduledExecutorService + */ + public void setScheduledExecutor(ScheduledExecutorService executor) { + this.scheduledExecutor = executor; + } + + public String getTransactionIsolation() { + return transactionIsolationName; + } + + /** + * Get the default schema name to be set on connections. + * + * @return the default schema name + */ + public String getSchema() { + return schema; + } + + /** + * Set the default schema name to be set on connections. + * + * @param schema the name of the default schema + */ + public void setSchema(String schema) { + this.schema = schema; + } + + /** + * Set the default transaction isolation level. The specified value is the + * constant name from the Connection class, eg. + * TRANSACTION_REPEATABLE_READ. + * + * @param isolationLevel the name of the isolation level + */ + public void setTransactionIsolation(String isolationLevel) { + this.transactionIsolationName = isolationLevel; + } + + public void setAliveBypassWindowMs(long aliveBypassWindowMs) { + this.aliveBypassWindowMs = aliveBypassWindowMs; + } + + public long getAliveBypassWindowMs() { + return aliveBypassWindowMs; + } + + public void setHousekeepingPeriodMs(long housekeepingPeriodMs) { + this.housekeepingPeriodMs = housekeepingPeriodMs; + } + + public long getHousekeepingPeriodMs() { + return housekeepingPeriodMs; + } + + /** + * Get the thread factory used to create threads. + * + * @return the thread factory (may be null, in which case the default thread factory is used) + */ + public ThreadFactory getThreadFactory() { + return threadFactory; + } + + /** + * Set the thread factory to be used to create threads. + * + * @param threadFactory the thread factory (setting to null causes the default thread factory to be used) + */ + public void setThreadFactory(ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + } + + private Class attemptFromContextLoader(final String driverClassName) { + final ClassLoader threadContextClassLoader = Thread.currentThread().getContextClassLoader(); + if (threadContextClassLoader != null) { + try { + final Class driverClass = threadContextClassLoader.loadClass(driverClassName); + logger.log(Level.FINE, "Driver class found in Thread context class loader:" + + driverClassName + " " + threadContextClassLoader); + return driverClass; + } catch (ClassNotFoundException e) { + logger.log(Level.FINE, "Driver class not found in Thread context class loader, trying classloader: " + + driverClassName + " " + threadContextClassLoader + " " + this.getClass().getClassLoader()); + } + } + + return null; + } + + @SuppressWarnings("StatementWithEmptyBody") + public void validate() { + if (poolName == null) { + poolName = generatePoolName(); + } + catalog = getNullIfEmpty(catalog); + connectionInitSql = getNullIfEmpty(connectionInitSql); + connectionTestQuery = getNullIfEmpty(connectionTestQuery); + transactionIsolationName = getNullIfEmpty(transactionIsolationName); + dataSourceClassName = getNullIfEmpty(dataSourceClassName); + driverClassName = getNullIfEmpty(driverClassName); + jdbcUrl = getNullIfEmpty(jdbcUrl); + if (dataSource != null) { + if (dataSourceClassName != null) { + logger.log(Level.WARNING, "using dataSource and ignoring dataSourceClassName: " + poolName); + } + } else if (dataSourceClassName != null) { + if (driverClassName != null) { + logger.log(Level.SEVERE, "cannot use driverClassName and dataSourceClassName together: " + poolName); + throw new IllegalStateException("cannot use driverClassName and dataSourceClassName together."); + } else if (jdbcUrl != null) { + logger.log(Level.WARNING, "using dataSourceClassName and ignoring jdbcUrl: " + poolName); + } + } else if (jdbcUrl != null) { + // ok + } else if (driverClassName != null) { + logger.log(Level.SEVERE, "jdbcUrl is required with driverClassName: " + poolName); + throw new IllegalArgumentException("jdbcUrl is required with driverClassName."); + } else { + logger.log(Level.SEVERE, "dataSource or dataSourceClassName or jdbcUrl is required: " + poolName); + throw new IllegalArgumentException("dataSource or dataSourceClassName or jdbcUrl is required."); + } + validateNumerics(); + } + + /** + * @return null if string is null or empty + */ + private static String getNullIfEmpty(final String text) { + return text == null ? null : text.trim().isEmpty() ? null : text.trim(); + } + + private void validateNumerics() { + if (maxLifetime != 0 && maxLifetime < TimeUnit.SECONDS.toMillis(30)) { + logger.log(Level.WARNING, "maxLifetime is less than 30000ms, setting to default ms: " + + poolName + " " + MAX_LIFETIME); + maxLifetime = MAX_LIFETIME; + } + if (leakDetectionThreshold > 0) { + if (leakDetectionThreshold < TimeUnit.SECONDS.toMillis(2) || (leakDetectionThreshold > maxLifetime && maxLifetime > 0)) { + logger.log(Level.WARNING, "leakDetectionThreshold is less than 2000ms or more than maxLifetime, disabling it: " + + poolName); + leakDetectionThreshold = 0; + } + } + if (connectionTimeout < 250) { + logger.log(Level.WARNING, "connectionTimeout is less than 250ms, setting to ms: " + + poolName + " " + CONNECTION_TIMEOUT); + connectionTimeout = CONNECTION_TIMEOUT; + } + if (validationTimeout < 250) { + logger.log(Level.WARNING, "validationTimeout is less than 250ms, setting to ms" + + poolName + " " + VALIDATION_TIMEOUT); + validationTimeout = VALIDATION_TIMEOUT; + } + if (maxPoolSize < 1) { + maxPoolSize = DEFAULT_POOL_SIZE; + } + if (minIdle < 0 || minIdle > maxPoolSize) { + minIdle = maxPoolSize; + } + if (idleTimeout + TimeUnit.SECONDS.toMillis(1) > maxLifetime && maxLifetime > 0 && minIdle < maxPoolSize) { + logger.log(Level.WARNING, "idleTimeout is close to or more than maxLifetime, disabling it:" + poolName); + idleTimeout = 0; + } else if (idleTimeout != 0 && idleTimeout < TimeUnit.SECONDS.toMillis(10) && minIdle < maxPoolSize) { + logger.log(Level.WARNING, "idleTimeout is less than 10000ms, setting to default ms: " + + poolName + " " + IDLE_TIMEOUT); + idleTimeout = IDLE_TIMEOUT; + } else if (idleTimeout != IDLE_TIMEOUT && idleTimeout != 0 && minIdle == maxPoolSize) { + logger.log(Level.WARNING, "idleTimeout has been set but has no effect because the pool is operating as a fixed size pool: " + poolName); + } + } + + private static String generatePoolName() { + return "xbib-pool-jdbc-" + POOL_COUNTER.getAndIncrement(); + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolDataSource.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolDataSource.java new file mode 100644 index 0000000..a0d2bab --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolDataSource.java @@ -0,0 +1,224 @@ +package org.xbib.jdbc.connection.pool; + +import java.io.Closeable; +import java.io.PrintWriter; +import java.lang.reflect.InvocationTargetException; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.sql.DataSource; + +/** + * The pooled DataSource. + */ +public class PoolDataSource implements DataSource, Closeable { + + private static final Logger logger = Logger.getLogger(PoolDataSource.class.getName()); + + private final AtomicBoolean isShutdown = new AtomicBoolean(); + + private final PoolConfig configuration; + + private Pool pool; + + /** + * Construct a {@link PoolDataSource} with the specified configuration. The + * {@link PoolConfig} is copied and the pool is started by invoking this + * constructor. + * The {@link PoolConfig} can be modified without affecting the DataSource + * and used to initialize another DataSource instance. + * + * @param configuration a config instance + */ + public PoolDataSource(PoolConfig configuration) + throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + this.configuration = configuration; + pool = new Pool(configuration); + } + + public Pool getPool() { + return pool; + } + + /** + * {@inheritDoc} + */ + @Override + public Connection getConnection() throws SQLException { + if (isClosed()) { + throw new SQLException("PoolDataSource " + this + " has been closed"); + } + Pool result = pool; + if (result == null) { + synchronized (this) { + result = pool; + if (result == null) { + configuration.validate(); + logger.log(Level.INFO, "Starting: " + configuration.getPoolName()); + try { + pool = result = new Pool(configuration); + } catch (Exception pie) { + throw new SQLException(pie); + } + logger.log(Level.INFO, "Start completed: " + configuration.getPoolName()); + } + } + } + return result.getConnection(); + } + + /** + * {@inheritDoc} + */ + @Override + public Connection getConnection(String username, String password) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + /** + * {@inheritDoc} + */ + @Override + public PrintWriter getLogWriter() throws SQLException { + Pool p = pool; + return (p != null ? p.getUnwrappedDataSource().getLogWriter() : null); + } + + /** + * {@inheritDoc} + */ + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + Pool p = pool; + if (p != null) { + p.getUnwrappedDataSource().setLogWriter(out); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void setLoginTimeout(int seconds) throws SQLException { + Pool p = pool; + if (p != null) { + p.getUnwrappedDataSource().setLoginTimeout(seconds); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int getLoginTimeout() throws SQLException { + Pool p = pool; + return (p != null ? p.getUnwrappedDataSource().getLoginTimeout() : 0); + } + + /** + * {@inheritDoc} + */ + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public T unwrap(Class iface) throws SQLException { + if (iface.isInstance(this)) { + return (T) this; + } + Pool p = pool; + if (p != null) { + final DataSource unwrappedDataSource = p.getUnwrappedDataSource(); + if (iface.isInstance(unwrappedDataSource)) { + return (T) unwrappedDataSource; + } + if (unwrappedDataSource != null) { + return unwrappedDataSource.unwrap(iface); + } + } + throw new SQLException("wrapped DataSource is not an instance of " + iface); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + if (iface.isInstance(this)) { + return true; + } + Pool p = pool; + if (p != null) { + final DataSource unwrappedDataSource = p.getUnwrappedDataSource(); + if (iface.isInstance(unwrappedDataSource)) { + return true; + } + + if (unwrappedDataSource != null) { + return unwrappedDataSource.isWrapperFor(iface); + } + } + return false; + } + + /** + * Evict a connection from the pool. If the connection has already been closed (returned to the pool) + * this may result in a "soft" eviction; the connection will be evicted sometime in the future if it is + * currently in use. If the connection has not been closed, the eviction is immediate. + * + * @param connection the connection to evict from the pool + */ + public void evictConnection(Connection connection) { + Pool p; + if (!isClosed() && (p = pool) != null) { + p.evictConnection(connection); + } + } + + /** + * Shutdown the DataSource and its associated pool. + */ + @Override + public void close() { + if (isShutdown.getAndSet(true)) { + return; + } + Pool p = pool; + if (p != null) { + try { + logger.log(Level.INFO, () -> "shutdown initiated: " + configuration.getPoolName()); + p.shutdown(); + logger.log(Level.INFO, () -> "shutdown completed: " + configuration.getPoolName()); + } catch (InterruptedException e) { + logger.log(Level.WARNING, "interrupted during closing: " + configuration.getPoolName() + " " + e.getMessage()); + Thread.currentThread().interrupt(); + } + } + } + + /** + * Determine whether the DataSource has been closed. + * + * @return true if the DataSource has been closed, false otherwise + */ + public boolean isClosed() { + return isShutdown.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "PoolDataSource (" + pool + ")"; + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntry.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntry.java new file mode 100644 index 0000000..fa53695 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntry.java @@ -0,0 +1,176 @@ +package org.xbib.jdbc.connection.pool; + +import org.xbib.jdbc.connection.pool.util.ClockSource; +import org.xbib.jdbc.connection.pool.util.FastList; +import org.xbib.jdbc.connection.pool.util.BagEntry; +import java.sql.Connection; +import java.sql.Statement; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Pool entry used in the Bag to track Connection instances. + */ +public class PoolEntry implements BagEntry { + + private static final Logger logger = Logger.getLogger(PoolEntry.class.getName()); + + private static final AtomicIntegerFieldUpdater stateUpdater = + AtomicIntegerFieldUpdater.newUpdater(PoolEntry.class, "state"); + + private Connection connection; + + private long lastAccessed; + + private long lastBorrowed; + + private volatile int state = 0; + + private volatile boolean evict; + + private volatile ScheduledFuture endOfLife; + + private final FastList openStatements; + + private final Pool pool; + + private final boolean isReadOnly; + + private final boolean isAutoCommit; + + public PoolEntry(Connection connection, Pool pool, final boolean isReadOnly, final boolean isAutoCommit) { + this.connection = connection; + this.pool = pool; + this.isReadOnly = isReadOnly; + this.isAutoCommit = isAutoCommit; + this.lastAccessed = ClockSource.currentTime(); + this.openStatements = new FastList<>(Statement.class, 16); + } + + public Connection getConnection() { + return connection; + } + + public Pool getPool() { + return pool; + } + + public long getLastAccessed() { + return lastAccessed; + } + + public long getLastBorrowed() { + return lastBorrowed; + } + + /** + * Release this entry back to the pool. + * + * @param lastAccessed last access time-stamp + */ + public void recycle(final long lastAccessed) { + if (connection != null) { + this.lastAccessed = lastAccessed; + pool.recycle(this); + } + } + + /** + * Set the end of life {@link ScheduledFuture}. + * + * @param endOfLife this PoolEntry/Connection's end of life {@link ScheduledFuture} + */ + public void setFutureEol(final ScheduledFuture endOfLife) { + this.endOfLife = endOfLife; + } + + public Connection createProxyConnection(final ProxyLeakTask leakTask, final long now) { + return ProxyFactory.getProxyConnection(this, connection, openStatements, leakTask, now, isReadOnly, isAutoCommit); + } + + public String getPoolName() { + return pool.toString(); + } + + public boolean isMarkedEvicted() { + return evict; + } + + public void markEvicted() { + this.evict = true; + } + + public void evict(final String closureReason) { + pool.closeConnection(this, closureReason); + } + + /** + * Returns millis since lastBorrowed + */ + public long getMillisSinceBorrowed() { + return ClockSource.elapsedMillis(lastBorrowed); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final long now = ClockSource.currentTime(); + return connection + + ", accessed " + ClockSource.elapsedDisplayString(lastAccessed, now) + " ago, " + + stateToString(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getState() { + return stateUpdater.get(this); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean compareAndSet(int expect, int update) { + return stateUpdater.compareAndSet(this, expect, update); + } + + /** + * {@inheritDoc} + */ + @Override + public void setState(int update) { + stateUpdater.set(this, update); + } + + public Connection close() { + ScheduledFuture eol = endOfLife; + if (eol != null && !eol.isDone() && !eol.cancel(false)) { + logger.log(Level.WARNING, "maxLifeTime expiration task cancellation unexpectedly returned false for connection " + getPoolName() + " " + connection); + } + Connection con = connection; + connection = null; + endOfLife = null; + return con; + } + + private String stateToString() { + switch (state) { + case STATE_IN_USE: + return "IN_USE"; + case STATE_NOT_IN_USE: + return "NOT_IN_USE"; + case STATE_REMOVED: + return "REMOVED"; + case STATE_RESERVED: + return "RESERVED"; + default: + return "Invalid"; + } + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntryException.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntryException.java new file mode 100644 index 0000000..60563cd --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolEntryException.java @@ -0,0 +1,9 @@ +package org.xbib.jdbc.connection.pool; + +@SuppressWarnings("serial") +public class PoolEntryException extends Exception { + + public PoolEntryException(Throwable t) { + super(t); + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolInitializationException.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolInitializationException.java new file mode 100644 index 0000000..d44a269 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/PoolInitializationException.java @@ -0,0 +1,14 @@ +package org.xbib.jdbc.connection.pool; + +@SuppressWarnings("serial") +public class PoolInitializationException extends RuntimeException { + + /** + * Construct an exception, possibly wrapping the provided Throwable as the cause. + * + * @param t the Throwable to wrap + */ + public PoolInitializationException(Throwable t) { + super("Failed to initialize pool: " + t.getMessage(), t); + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyCallableStatement.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyCallableStatement.java new file mode 100644 index 0000000..b7921ed --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyCallableStatement.java @@ -0,0 +1,595 @@ +package org.xbib.jdbc.connection.pool; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +/** + * This is the proxy class for java.sql.CallableStatement. + */ +public class ProxyCallableStatement extends ProxyPreparedStatement implements CallableStatement { + + public ProxyCallableStatement(ProxyConnection connection, CallableStatement statement) { + super(connection, statement); + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException { + ((CallableStatement) delegate).registerOutParameter(parameterIndex, sqlType); + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException { + ((CallableStatement) delegate).registerOutParameter(parameterIndex, sqlType, scale); + } + + @Override + public boolean wasNull() throws SQLException { + return ((CallableStatement) delegate).wasNull(); + } + + @Override + public String getString(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getString(parameterIndex); + } + + @Override + public boolean getBoolean(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getBoolean(parameterIndex); + } + + @Override + public byte getByte(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getByte(parameterIndex); + } + + @Override + public short getShort(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getShort(parameterIndex); + } + + @Override + public int getInt(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getInt(parameterIndex); + } + + @Override + public long getLong(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getLong(parameterIndex); + } + + @Override + public float getFloat(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getFloat(parameterIndex); + } + + @Override + public double getDouble(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getDouble(parameterIndex); + } + + @Override + public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException { + return ((CallableStatement) delegate).getBigDecimal(parameterIndex, scale); + } + + @Override + public byte[] getBytes(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getBytes(parameterIndex); + } + + @Override + public Date getDate(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getDate(parameterIndex); + } + + @Override + public Time getTime(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getTime(parameterIndex); + } + + @Override + public Timestamp getTimestamp(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getTimestamp(parameterIndex); + } + + @Override + public Object getObject(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getObject(parameterIndex); + } + + @Override + public BigDecimal getBigDecimal(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getBigDecimal(parameterIndex); + } + + @Override + public Object getObject(int parameterIndex, Map> map) throws SQLException { + return ((CallableStatement) delegate).getObject(parameterIndex, map); + } + + @Override + public Ref getRef(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getRef(parameterIndex); + } + + @Override + public Blob getBlob(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getBlob(parameterIndex); + } + + @Override + public Clob getClob(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getClob(parameterIndex); + } + + @Override + public Array getArray(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getArray(parameterIndex); + } + + @Override + public Date getDate(int parameterIndex, Calendar cal) throws SQLException { + return ((CallableStatement) delegate).getDate(parameterIndex, cal); + } + + @Override + public Time getTime(int parameterIndex, Calendar cal) throws SQLException { + return ((CallableStatement) delegate).getTime(parameterIndex, cal); + } + + @Override + public Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException { + return ((CallableStatement) delegate).getTimestamp(parameterIndex, cal); + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType, String typeName) throws SQLException { + ((CallableStatement) delegate).registerOutParameter(parameterIndex, sqlType, typeName); + } + + @Override + public void registerOutParameter(String parameterName, int sqlType) throws SQLException { + ((CallableStatement) delegate).registerOutParameter(parameterName, sqlType); + } + + @Override + public void registerOutParameter(String parameterName, int sqlType, int scale) throws SQLException { + ((CallableStatement) delegate).registerOutParameter(parameterName, sqlType, scale); + } + + @Override + public void registerOutParameter(String parameterName, int sqlType, String typeName) throws SQLException { + ((CallableStatement) delegate).registerOutParameter(parameterName, sqlType, typeName); + } + + @Override + public URL getURL(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getURL(parameterIndex); + } + + @Override + public void setURL(String parameterName, URL val) throws SQLException { + ((CallableStatement) delegate).setURL(parameterName, val); + } + + @Override + public void setNull(String parameterName, int sqlType) throws SQLException { + ((CallableStatement) delegate).setNull(parameterName, sqlType); + } + + @Override + public void setBoolean(String parameterName, boolean x) throws SQLException { + ((CallableStatement) delegate).setBoolean(parameterName, x); + } + + @Override + public void setByte(String parameterName, byte x) throws SQLException { + ((CallableStatement) delegate).setByte(parameterName, x); + } + + @Override + public void setShort(String parameterName, short x) throws SQLException { + ((CallableStatement) delegate).setShort(parameterName, x); + } + + @Override + public void setInt(String parameterName, int x) throws SQLException { + ((CallableStatement) delegate).setInt(parameterName, x); + } + + @Override + public void setLong(String parameterName, long x) throws SQLException { + ((CallableStatement) delegate).setLong(parameterName, x); + } + + @Override + public void setFloat(String parameterName, float x) throws SQLException { + ((CallableStatement) delegate).setFloat(parameterName, x); + } + + @Override + public void setDouble(String parameterName, double x) throws SQLException { + ((CallableStatement) delegate).setDouble(parameterName, x); + } + + @Override + public void setBigDecimal(String parameterName, BigDecimal x) throws SQLException { + ((CallableStatement) delegate).setBigDecimal(parameterName, x); + } + + @Override + public void setString(String parameterName, String x) throws SQLException { + ((CallableStatement) delegate).setString(parameterName, x); + } + + @Override + public void setBytes(String parameterName, byte[] x) throws SQLException { + ((CallableStatement) delegate).setBytes(parameterName, x); + } + + @Override + public void setDate(String parameterName, Date x) throws SQLException { + ((CallableStatement) delegate).setDate(parameterName, x); + } + + @Override + public void setTime(String parameterName, Time x) throws SQLException { + ((CallableStatement) delegate).setTime(parameterName, x); + } + + @Override + public void setTimestamp(String parameterName, Timestamp x) throws SQLException { + ((CallableStatement) delegate).setTimestamp(parameterName, x); + } + + @Override + public void setAsciiStream(String parameterName, InputStream x, int length) throws SQLException { + ((CallableStatement) delegate).setAsciiStream(parameterName, x, length); + } + + @Override + public void setBinaryStream(String parameterName, InputStream x, int length) throws SQLException { + ((CallableStatement) delegate).setBinaryStream(parameterName, x, length); + } + + @Override + public void setObject(String parameterName, Object x, int targetSqlType, int scale) throws SQLException { + ((CallableStatement) delegate).setObject(parameterName, x, targetSqlType, scale); + } + + @Override + public void setObject(String parameterName, Object x, int targetSqlType) throws SQLException { + ((CallableStatement) delegate).setObject(parameterName, x, targetSqlType); + } + + @Override + public void setObject(String parameterName, Object x) throws SQLException { + ((CallableStatement) delegate).setObject(parameterName, x); + } + + @Override + public void setCharacterStream(String parameterName, Reader reader, int length) throws SQLException { + ((CallableStatement) delegate).setCharacterStream(parameterName, reader, length); + } + + @Override + public void setDate(String parameterName, Date x, Calendar cal) throws SQLException { + ((CallableStatement) delegate).setDate(parameterName, x, cal); + } + + @Override + public void setTime(String parameterName, Time x, Calendar cal) throws SQLException { + ((CallableStatement) delegate).setTime(parameterName, x, cal); + } + + @Override + public void setTimestamp(String parameterName, Timestamp x, Calendar cal) throws SQLException { + ((CallableStatement) delegate).setTimestamp(parameterName, x, cal); + } + + @Override + public void setNull(String parameterName, int sqlType, String typeName) throws SQLException { + ((CallableStatement) delegate).setNull(parameterName, sqlType, typeName); + } + + @Override + public String getString(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getString(parameterName); + } + + @Override + public boolean getBoolean(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getBoolean(parameterName); + } + + @Override + public byte getByte(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getByte(parameterName); + } + + @Override + public short getShort(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getShort(parameterName); + } + + @Override + public int getInt(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getInt(parameterName); + } + + @Override + public long getLong(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getLong(parameterName); + } + + @Override + public float getFloat(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getFloat(parameterName); + } + + @Override + public double getDouble(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getDouble(parameterName); + } + + @Override + public byte[] getBytes(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getBytes(parameterName); + } + + @Override + public Date getDate(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getDate(parameterName); + } + + @Override + public Time getTime(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getTime(parameterName); + } + + @Override + public Timestamp getTimestamp(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getTimestamp(parameterName); + } + + @Override + public Object getObject(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getObject(parameterName); + } + + @Override + public BigDecimal getBigDecimal(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getBigDecimal(parameterName); + } + + @Override + public Object getObject(String parameterName, Map> map) throws SQLException { + return ((CallableStatement) delegate).getObject(parameterName, map); + } + + @Override + public Ref getRef(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getRef(parameterName); + } + + @Override + public Blob getBlob(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getBlob(parameterName); + } + + @Override + public Clob getClob(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getClob(parameterName); + } + + @Override + public Array getArray(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getArray(parameterName); + } + + @Override + public Date getDate(String parameterName, Calendar cal) throws SQLException { + return ((CallableStatement) delegate).getDate(parameterName, cal); + } + + @Override + public Time getTime(String parameterName, Calendar cal) throws SQLException { + return ((CallableStatement) delegate).getTime(parameterName, cal); + } + + @Override + public Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException { + return ((CallableStatement) delegate).getTimestamp(parameterName, cal); + } + + @Override + public URL getURL(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getURL(parameterName); + } + + @Override + public RowId getRowId(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getRowId(parameterIndex); + } + + @Override + public RowId getRowId(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getRowId(parameterName); + } + + @Override + public void setRowId(String parameterName, RowId x) throws SQLException { + ((CallableStatement) delegate).setRowId(parameterName, x); + } + + @Override + public void setNString(String parameterName, String value) throws SQLException { + ((CallableStatement) delegate).setNString(parameterName, value); + } + + @Override + public void setNCharacterStream(String parameterName, Reader value, long length) throws SQLException { + ((CallableStatement) delegate).setNCharacterStream(parameterName, value, length); + } + + @Override + public void setNClob(String parameterName, NClob value) throws SQLException { + ((CallableStatement) delegate).setNClob(parameterName, value); + } + + @Override + public void setClob(String parameterName, Reader reader, long length) throws SQLException { + ((CallableStatement) delegate).setClob(parameterName, reader, length); + } + + @Override + public void setBlob(String parameterName, InputStream inputStream, long length) throws SQLException { + ((CallableStatement) delegate).setBlob(parameterName, inputStream, length); + } + + @Override + public void setNClob(String parameterName, Reader reader, long length) throws SQLException { + ((CallableStatement) delegate).setNClob(parameterName, reader, length); + } + + @Override + public NClob getNClob(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getNClob(parameterIndex); + } + + @Override + public NClob getNClob(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getNClob(parameterName); + } + + @Override + public void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException { + ((CallableStatement) delegate).setSQLXML(parameterName, xmlObject); + } + + @Override + public SQLXML getSQLXML(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getSQLXML(parameterIndex); + } + + @Override + public SQLXML getSQLXML(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getSQLXML(parameterName); + } + + @Override + public String getNString(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getNString(parameterIndex); + } + + @Override + public String getNString(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getNString(parameterName); + } + + @Override + public Reader getNCharacterStream(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getNCharacterStream(parameterIndex); + } + + @Override + public Reader getNCharacterStream(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getNCharacterStream(parameterName); + } + + @Override + public Reader getCharacterStream(int parameterIndex) throws SQLException { + return ((CallableStatement) delegate).getCharacterStream(parameterIndex); + } + + @Override + public Reader getCharacterStream(String parameterName) throws SQLException { + return ((CallableStatement) delegate).getCharacterStream(parameterName); + } + + @Override + public void setBlob(String parameterName, Blob x) throws SQLException { + ((CallableStatement) delegate).setBlob(parameterName, x); + } + + @Override + public void setClob(String parameterName, Clob x) throws SQLException { + ((CallableStatement) delegate).setClob(parameterName, x); + } + + @Override + public void setAsciiStream(String parameterName, InputStream x, long length) throws SQLException { + ((CallableStatement) delegate).setAsciiStream(parameterName, x, length); + } + + @Override + public void setBinaryStream(String parameterName, InputStream x, long length) throws SQLException { + ((CallableStatement) delegate).setBinaryStream(parameterName, x, length); + } + + @Override + public void setCharacterStream(String parameterName, Reader reader, long length) throws SQLException { + ((CallableStatement) delegate).setCharacterStream(parameterName, reader, length); + } + + @Override + public void setAsciiStream(String parameterName, InputStream x) throws SQLException { + ((CallableStatement) delegate).setAsciiStream(parameterName, x); + } + + @Override + public void setBinaryStream(String parameterName, InputStream x) throws SQLException { + ((CallableStatement) delegate).setBinaryStream(parameterName, x); + } + + @Override + public void setCharacterStream(String parameterName, Reader reader) throws SQLException { + ((CallableStatement) delegate).setCharacterStream(parameterName, reader); + } + + @Override + public void setNCharacterStream(String parameterName, Reader value) throws SQLException { + ((CallableStatement) delegate).setNCharacterStream(parameterName, value); + } + + @Override + public void setClob(String parameterName, Reader reader) throws SQLException { + ((CallableStatement) delegate).setClob(parameterName, reader); + } + + @Override + public void setBlob(String parameterName, InputStream inputStream) throws SQLException { + ((CallableStatement) delegate).setBlob(parameterName, inputStream); + } + + @Override + public void setNClob(String parameterName, Reader reader) throws SQLException { + ((CallableStatement) delegate).setNClob(parameterName, reader); + } + + @Override + public T getObject(int parameterIndex, Class type) throws SQLException { + return ((CallableStatement) delegate).getObject(parameterIndex, type); + } + + @Override + public T getObject(String parameterName, Class type) throws SQLException { + return ((CallableStatement) delegate).getObject(parameterName, type); + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyConnection.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyConnection.java new file mode 100644 index 0000000..4093259 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyConnection.java @@ -0,0 +1,697 @@ +package org.xbib.jdbc.connection.pool; + +import org.xbib.jdbc.connection.pool.util.ClockSource; +import org.xbib.jdbc.connection.pool.util.FastList; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLTimeoutException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This is the proxy class for java.sql.Connection. + */ +public class ProxyConnection implements Connection { + + private static final Logger logger = Logger.getLogger(ProxyConnection.class.getName()); + + private static final int DIRTY_BIT_READONLY = 0b000001; + + private static final int DIRTY_BIT_AUTOCOMMIT = 0b000010; + + private static final int DIRTY_BIT_ISOLATION = 0b000100; + + private static final int DIRTY_BIT_CATALOG = 0b001000; + + private static final int DIRTY_BIT_NETTIMEOUT = 0b010000; + + private static final int DIRTY_BIT_SCHEMA = 0b100000; + + private static final Set ERROR_STATES; + + private static final Set ERROR_CODES; + + private Connection delegate; + + private final PoolEntry poolEntry; + + private final ProxyLeakTask leakTask; + + private final FastList openStatements; + + private int dirtyBits; + + private long lastAccess; + + private boolean isCommitStateDirty; + + private boolean isReadOnly; + + private boolean isAutoCommit; + + private int networkTimeout; + + private int transactionIsolation; + + private String dbcatalog; + + private String dbschema; + + static { + ERROR_STATES = new HashSet<>(); + ERROR_STATES.add("0A000"); // FEATURE UNSUPPORTED + ERROR_STATES.add("57P01"); // ADMIN SHUTDOWN + ERROR_STATES.add("57P02"); // CRASH SHUTDOWN + ERROR_STATES.add("57P03"); // CANNOT CONNECT NOW + ERROR_STATES.add("01002"); // SQL92 disconnect error + ERROR_STATES.add("JZ0C0"); // Sybase disconnect error + ERROR_STATES.add("JZ0C1"); // Sybase disconnect error + + ERROR_CODES = new HashSet<>(); + ERROR_CODES.add(500150); + ERROR_CODES.add(2399); + } + + public ProxyConnection(PoolEntry poolEntry, + Connection connection, + FastList openStatements, + ProxyLeakTask leakTask, + long now, + boolean isReadOnly, + boolean isAutoCommit) { + this.poolEntry = poolEntry; + this.delegate = connection; + this.openStatements = openStatements; + this.leakTask = leakTask; + this.lastAccess = now; + this.isReadOnly = isReadOnly; + this.isAutoCommit = isAutoCommit; + } + + public boolean isCommitStateDirty() { + return isCommitStateDirty; + } + + /** + * + * {@inheritDoc} + */ + @Override + public final String toString() { + return getClass().getSimpleName() + '@' + System.identityHashCode(this) + " wrapping " + delegate; + } + + public boolean getAutoCommitState() { + return isAutoCommit; + } + + public String getCatalogState() { + return dbcatalog; + } + + public String getSchemaState() { + return dbschema; + } + + public int getTransactionIsolationState() { + return transactionIsolation; + } + + public boolean getReadOnlyState() { + return isReadOnly; + } + + public int getNetworkTimeoutState() { + return networkTimeout; + } + + public PoolEntry getPoolEntry() { + return poolEntry; + } + + public SQLException checkException(SQLException sqle) { + boolean evict = false; + SQLException nse = sqle; + for (int depth = 0; delegate != CLOSED_CONNECTION && nse != null && depth < 10; depth++) { + final String sqlState = nse.getSQLState(); + if (sqlState != null && sqlState.startsWith("08") + || nse instanceof SQLTimeoutException + || ERROR_STATES.contains(sqlState) + || ERROR_CODES.contains(nse.getErrorCode())) { + // broken connection + evict = true; + break; + } else { + nse = nse.getNextException(); + } + } + if (evict) { + logger.log(Level.WARNING, "Connection marked as broken because of SQLSTATE(), ErrorCode(): " + + poolEntry.getPoolName() + " " + delegate + " " + nse.getSQLState() + " " + nse.getErrorCode(), nse); + leakTask.cancel(); + poolEntry.evict("(connection is broken)"); + delegate = CLOSED_CONNECTION; + } + return sqle; + } + + public synchronized void untrackStatement(final Statement statement) { + openStatements.remove(statement); + } + + public void markCommitStateDirty() { + if (isAutoCommit) { + lastAccess = ClockSource.currentTime(); + } else { + isCommitStateDirty = true; + } + } + + public void cancelLeakTask() { + leakTask.cancel(); + } + + private synchronized T trackStatement(final T statement) { + openStatements.add(statement); + return statement; + } + + @SuppressWarnings("try") + private synchronized void closeStatements() { + final int size = openStatements.size(); + if (size > 0) { + for (int i = 0; i < size && delegate != CLOSED_CONNECTION; i++) { + try (Statement ignored = openStatements.get(i)) { + // automatic resource cleanup + } catch (SQLException e) { + logger.log(Level.WARNING, "Connection marked as broken because of an exception closing open statements during Connection.close(): " + + poolEntry.getPoolName() + " " + delegate); + leakTask.cancel(); + poolEntry.evict("(exception closing Statements during Connection.close())"); + delegate = CLOSED_CONNECTION; + } + } + openStatements.clear(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws SQLException { + closeStatements(); + if (delegate != CLOSED_CONNECTION) { + leakTask.cancel(); + try { + if (isCommitStateDirty && !isAutoCommit) { + delegate.rollback(); + lastAccess = ClockSource.currentTime(); + logger.log(Level.FINE, "Executed rollback on connection due to dirty commit state on close(): " + poolEntry.getPoolName() + " " + delegate); + } + if (dirtyBits != 0) { + //poolEntry.resetConnectionState(this, dirtyBits); + resetConnectionState(poolEntry.getConnection(), dirtyBits, + poolEntry.getPool().getConfig().getCatalog() , + poolEntry.getPool().getConfig().getSchema()); + lastAccess = ClockSource.currentTime(); + } + delegate.clearWarnings(); + } catch (SQLException e) { + // when connections are aborted, exceptions are often thrown that should not reach the application + if (!poolEntry.isMarkedEvicted()) { + throw checkException(e); + } + } finally { + delegate = CLOSED_CONNECTION; + poolEntry.recycle(lastAccess); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isClosed() throws SQLException { + return delegate == CLOSED_CONNECTION; + } + + /** + * {@inheritDoc} + */ + @Override + public Statement createStatement() throws SQLException { + return ProxyFactory.getProxyStatement(this, trackStatement(delegate.createStatement())); + } + + /** + * {@inheritDoc} + */ + @Override + public Statement createStatement(int resultSetType, int concurrency) throws SQLException { + return ProxyFactory.getProxyStatement(this, trackStatement(delegate.createStatement(resultSetType, concurrency))); + } + + /** + * {@inheritDoc} + */ + @Override + public Statement createStatement(int resultSetType, int concurrency, int holdability) throws SQLException { + return ProxyFactory.getProxyStatement(this, trackStatement(delegate.createStatement(resultSetType, concurrency, holdability))); + } + + + /** + * {@inheritDoc} + */ + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + return ProxyFactory.getProxyCallableStatement(this, trackStatement(delegate.prepareCall(sql))); + } + + @Override + public String nativeSQL(String sql) throws SQLException { + return delegate.nativeSQL(sql); + } + + /** + * {@inheritDoc} + */ + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int concurrency) throws SQLException { + return ProxyFactory.getProxyCallableStatement(this, trackStatement(delegate.prepareCall(sql, resultSetType, concurrency))); + } + + @Override + public Map> getTypeMap() throws SQLException { + return delegate.getTypeMap(); + } + + @Override + public void setTypeMap(Map> map) throws SQLException { + delegate.setTypeMap(map); + } + + @Override + public void setHoldability(int holdability) throws SQLException { + delegate.setHoldability(holdability); + } + + @Override + public int getHoldability() throws SQLException { + return delegate.getHoldability(); + } + + @Override + public Savepoint setSavepoint() throws SQLException { + return delegate.setSavepoint(); + } + + @Override + public Savepoint setSavepoint(String name) throws SQLException { + return delegate.setSavepoint(name); + } + + /** + * {@inheritDoc} + */ + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int concurrency, int holdability) throws SQLException { + return ProxyFactory.getProxyCallableStatement(this, trackStatement(delegate.prepareCall(sql, resultSetType, concurrency, holdability))); + } + + /** + * {@inheritDoc} + */ + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql))); + } + + /** + * {@inheritDoc} + */ + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, autoGeneratedKeys))); + } + + /** + * {@inheritDoc} + */ + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int concurrency) throws SQLException { + return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, resultSetType, concurrency))); + } + + /** + * {@inheritDoc} + */ + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int concurrency, int holdability) throws SQLException { + return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, resultSetType, concurrency, holdability))); + } + + /** + * {@inheritDoc} + */ + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, columnIndexes))); + } + + /** + * {@inheritDoc} + */ + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, columnNames))); + } + + @Override + public Clob createClob() throws SQLException { + return delegate.createClob(); + } + + @Override + public Blob createBlob() throws SQLException { + return delegate.createBlob(); + } + + @Override + public NClob createNClob() throws SQLException { + return delegate.createNClob(); + } + + @Override + public SQLXML createSQLXML() throws SQLException { + return delegate.createSQLXML(); + } + + @Override + public boolean isValid(int timeout) throws SQLException { + return delegate.isValid(timeout); + } + + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException { + delegate.setClientInfo(name, value); + } + + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException { + delegate.setClientInfo(properties); + } + + @Override + public String getClientInfo(String name) throws SQLException { + return delegate.getClientInfo(name); + } + + @Override + public Properties getClientInfo() throws SQLException { + return delegate.getClientInfo(); + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + return delegate.createArrayOf(typeName, elements); + } + + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + return delegate.createStruct(typeName, attributes); + } + + /** + * {@inheritDoc} + */ + @Override + public DatabaseMetaData getMetaData() throws SQLException { + markCommitStateDirty(); + return ProxyFactory.getProxyDatabaseMetaData(this, delegate.getMetaData()); + } + + /** + * {@inheritDoc} + */ + @Override + public void commit() throws SQLException { + delegate.commit(); + isCommitStateDirty = false; + lastAccess = ClockSource.currentTime(); + } + + /** + * {@inheritDoc} + */ + @Override + public void rollback() throws SQLException { + delegate.rollback(); + isCommitStateDirty = false; + lastAccess = ClockSource.currentTime(); + } + + /** + * {@inheritDoc} + */ + @Override + public void rollback(Savepoint savepoint) throws SQLException { + delegate.rollback(savepoint); + isCommitStateDirty = false; + lastAccess = ClockSource.currentTime(); + } + + @Override + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + delegate.releaseSavepoint(savepoint); + } + + /** + * {@inheritDoc} + */ + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + delegate.setAutoCommit(autoCommit); + isAutoCommit = autoCommit; + dirtyBits |= DIRTY_BIT_AUTOCOMMIT; + } + + @Override + public boolean getAutoCommit() throws SQLException { + return delegate.getAutoCommit(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + delegate.setReadOnly(readOnly); + isReadOnly = readOnly; + isCommitStateDirty = false; + dirtyBits |= DIRTY_BIT_READONLY; + } + + @Override + public boolean isReadOnly() throws SQLException { + return delegate.isReadOnly(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setTransactionIsolation(int level) throws SQLException { + delegate.setTransactionIsolation(level); + transactionIsolation = level; + dirtyBits |= DIRTY_BIT_ISOLATION; + } + + @Override + public int getTransactionIsolation() throws SQLException { + return delegate.getTransactionIsolation(); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return delegate.getWarnings(); + } + + @Override + public void clearWarnings() throws SQLException { + delegate.clearWarnings(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setCatalog(String catalog) throws SQLException { + delegate.setCatalog(catalog); + dbcatalog = catalog; + dirtyBits |= DIRTY_BIT_CATALOG; + } + + @Override + public String getCatalog() throws SQLException { + return delegate.getCatalog(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + delegate.setNetworkTimeout(executor, milliseconds); + networkTimeout = milliseconds; + dirtyBits |= DIRTY_BIT_NETTIMEOUT; + } + + @Override + public int getNetworkTimeout() throws SQLException { + return delegate.getNetworkTimeout(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setSchema(String schema) throws SQLException { + delegate.setSchema(schema); + dbschema = schema; + dirtyBits |= DIRTY_BIT_SCHEMA; + } + + @Override + public String getSchema() throws SQLException { + return delegate.getSchema(); + } + + @Override + public void abort(Executor executor) throws SQLException { + delegate.abort(executor); + } + + /** + * {@inheritDoc} + */ + @Override + public final boolean isWrapperFor(Class iface) throws SQLException { + return iface.isInstance(delegate) || (delegate != null && delegate.isWrapperFor(iface)); + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public T unwrap(Class iface) throws SQLException { + if (iface.isInstance(delegate)) { + return (T) delegate; + } else if (delegate != null) { + return delegate.unwrap(iface); + } + throw new SQLException("Wrapped connection is not an instance of " + iface); + } + + static final Connection CLOSED_CONNECTION = getClosedConnection(); + + private static Connection getClosedConnection() { + InvocationHandler handler = (proxy, method, args) -> { + final String methodName = method.getName(); + if ("isClosed".equals(methodName)) { + return Boolean.TRUE; + } else if ("isValid".equals(methodName)) { + return Boolean.FALSE; + } + if ("abort".equals(methodName)) { + return Void.TYPE; + } + if ("close".equals(methodName)) { + return Void.TYPE; + } else if ("toString".equals(methodName)) { + return ProxyConnection.class.getCanonicalName(); + } + throw new SQLException("connection is closed"); + }; + return (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), + new Class[] { Connection.class }, handler); + } + + private void resetConnectionState(Connection connection, int dirtyBits, String catalog, String schema) throws SQLException { + int resetBits = 0; + if ((dirtyBits & DIRTY_BIT_READONLY) != 0 && getReadOnlyState() != isReadOnly) { + connection.setReadOnly(isReadOnly); + resetBits |= DIRTY_BIT_READONLY; + } + if ((dirtyBits & DIRTY_BIT_AUTOCOMMIT) != 0 && getAutoCommitState() != isAutoCommit) { + connection.setAutoCommit(isAutoCommit); + resetBits |= DIRTY_BIT_AUTOCOMMIT; + } + if ((dirtyBits & DIRTY_BIT_ISOLATION) != 0 && getTransactionIsolationState() != transactionIsolation) { + connection.setTransactionIsolation(transactionIsolation); + resetBits |= DIRTY_BIT_ISOLATION; + } + if ((dirtyBits & DIRTY_BIT_CATALOG) != 0 && catalog != null && !catalog.equals(getCatalogState())) { + connection.setCatalog(catalog); + resetBits |= DIRTY_BIT_CATALOG; + } + if ((dirtyBits & DIRTY_BIT_NETTIMEOUT) != 0 && getNetworkTimeoutState() != networkTimeout) { + connection.setNetworkTimeout(Runnable::run, networkTimeout); + resetBits |= DIRTY_BIT_NETTIMEOUT; + } + if ((dirtyBits & DIRTY_BIT_SCHEMA) != 0 && schema != null && !schema.equals(getSchemaState())) { + connection.setSchema(schema); + resetBits |= DIRTY_BIT_SCHEMA; + } + if (resetBits != 0 && logger.isLoggable(Level.FINE)) { + final String string = stringFromResetBits(resetBits); + logger.log(Level.FINE, () -> "reset on connection: " + string + " " + connection); + } + } + + /** + * This will create a string for debug logging. Given a set of "reset bits", this + * method will return a concatenated string, for example + * Input : 0b00110 + * Output: "autoCommit, isolation" + * + * @param bits a set of "reset bits" + * @return a string of which states were reset + */ + private String stringFromResetBits(final int bits) { + final StringBuilder sb = new StringBuilder(); + for (int ndx = 0; ndx < RESET_STATES.length; ndx++) { + if ((bits & (0b1 << ndx)) != 0) { + sb.append(RESET_STATES[ndx]).append(", "); + } + } + sb.setLength(sb.length() - 2); // trim trailing comma + return sb.toString(); + } + + private static final String[] RESET_STATES = {"readOnly", "autoCommit", "isolation", "catalog", "netTimeout", "schema"}; + +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyDatabaseMetaData.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyDatabaseMetaData.java new file mode 100644 index 0000000..7f099e6 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyDatabaseMetaData.java @@ -0,0 +1,1056 @@ +package org.xbib.jdbc.connection.pool; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.RowIdLifetime; +import java.sql.SQLException; +import java.sql.Statement; + +public class ProxyDatabaseMetaData implements DatabaseMetaData { + + protected final ProxyConnection connection; + + private final DatabaseMetaData delegate; + + public ProxyDatabaseMetaData(ProxyConnection connection, DatabaseMetaData metaData) { + this.connection = connection; + this.delegate = metaData; + } + + @SuppressWarnings("unused") + public SQLException checkException(SQLException e) { + return connection.checkException(e); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final String delegateToString = delegate.toString(); + return this.getClass().getSimpleName() + '@' + System.identityHashCode(this) + " wrapping " + delegateToString; + } + + /** + * {@inheritDoc} + */ + @Override + public Connection getConnection() { + return connection; + } + + @Override + public boolean supportsSavepoints() throws SQLException { + return delegate.supportsSavepoints(); + } + + @Override + public boolean supportsNamedParameters() throws SQLException { + return delegate.supportsNamedParameters(); + } + + @Override + public boolean supportsMultipleOpenResults() throws SQLException { + return delegate.supportsMultipleOpenResults(); + } + + @Override + public boolean supportsGetGeneratedKeys() throws SQLException { + return delegate.supportsGetGeneratedKeys(); + } + + @Override + public boolean allProceduresAreCallable() throws SQLException { + return delegate.allProceduresAreCallable(); + } + + @Override + public boolean allTablesAreSelectable() throws SQLException { + return delegate.allTablesAreSelectable(); + } + + @Override + public String getURL() throws SQLException { + return delegate.getURL(); + } + + @Override + public String getUserName() throws SQLException { + return delegate.getUserName(); + } + + @Override + public boolean isReadOnly() throws SQLException { + return delegate.isReadOnly(); + } + + @Override + public boolean nullsAreSortedHigh() throws SQLException { + return delegate.nullsAreSortedHigh(); + } + + @Override + public boolean nullsAreSortedLow() throws SQLException { + return delegate.nullsAreSortedLow(); + } + + @Override + public boolean nullsAreSortedAtStart() throws SQLException { + return delegate.nullsAreSortedAtStart(); + } + + @Override + public boolean nullsAreSortedAtEnd() throws SQLException { + return delegate.nullsAreSortedAtEnd(); + } + + @Override + public String getDatabaseProductName() throws SQLException { + return delegate.getDatabaseProductName(); + } + + @Override + public String getDatabaseProductVersion() throws SQLException { + return delegate.getDatabaseProductVersion(); + } + + @Override + public String getDriverName() throws SQLException { + return delegate.getDriverName(); + } + + @Override + public String getDriverVersion() throws SQLException { + return delegate.getDriverVersion(); + } + + @Override + public int getDriverMajorVersion() { + return delegate.getDriverMajorVersion(); + } + + @Override + public int getDriverMinorVersion() { + return delegate.getDriverMinorVersion(); + } + + @Override + public boolean usesLocalFiles() throws SQLException { + return delegate.usesLocalFiles(); + } + + @Override + public boolean usesLocalFilePerTable() throws SQLException { + return delegate.usesLocalFilePerTable(); + } + + @Override + public boolean supportsMixedCaseIdentifiers() throws SQLException { + return delegate.supportsMixedCaseIdentifiers(); + } + + @Override + public boolean storesUpperCaseIdentifiers() throws SQLException { + return delegate.storesUpperCaseIdentifiers(); + } + + @Override + public boolean storesLowerCaseIdentifiers() throws SQLException { + return delegate.storesLowerCaseIdentifiers(); + } + + @Override + public boolean storesMixedCaseIdentifiers() throws SQLException { + return delegate.supportsMixedCaseIdentifiers(); + } + + @Override + public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException { + return delegate.supportsMixedCaseQuotedIdentifiers(); + } + + @Override + public boolean storesUpperCaseQuotedIdentifiers() throws SQLException { + return delegate.storesUpperCaseQuotedIdentifiers(); + } + + @Override + public boolean storesLowerCaseQuotedIdentifiers() throws SQLException { + return delegate.storesLowerCaseQuotedIdentifiers(); + } + + @Override + public boolean storesMixedCaseQuotedIdentifiers() throws SQLException { + return delegate.storesMixedCaseQuotedIdentifiers(); + } + + @Override + public String getIdentifierQuoteString() throws SQLException { + return delegate.getIdentifierQuoteString(); + } + + @Override + public String getSQLKeywords() throws SQLException { + return delegate.getSQLKeywords(); + } + + @Override + public String getNumericFunctions() throws SQLException { + return delegate.getNumericFunctions(); + } + + @Override + public String getStringFunctions() throws SQLException { + return delegate.getStringFunctions(); + } + + @Override + public String getSystemFunctions() throws SQLException { + return delegate.getSystemFunctions(); + } + + @Override + public String getTimeDateFunctions() throws SQLException { + return delegate.getTimeDateFunctions(); + } + + @Override + public String getSearchStringEscape() throws SQLException { + return delegate.getSearchStringEscape(); + } + + @Override + public String getExtraNameCharacters() throws SQLException { + return delegate.getExtraNameCharacters(); + } + + @Override + public boolean supportsAlterTableWithAddColumn() throws SQLException { + return delegate.supportsAlterTableWithAddColumn(); + } + + @Override + public boolean supportsAlterTableWithDropColumn() throws SQLException { + return delegate.supportsAlterTableWithDropColumn(); + } + + @Override + public boolean supportsColumnAliasing() throws SQLException { + return delegate.supportsColumnAliasing(); + } + + @Override + public boolean nullPlusNonNullIsNull() throws SQLException { + return delegate.nullPlusNonNullIsNull(); + } + + @Override + public boolean supportsConvert() throws SQLException { + return delegate.supportsConvert(); + } + + @Override + public boolean supportsConvert(int fromType, int toType) throws SQLException { + return delegate.supportsConvert(fromType, toType); + } + + @Override + public boolean supportsTableCorrelationNames() throws SQLException { + return delegate.supportsTableCorrelationNames(); + } + + @Override + public boolean supportsDifferentTableCorrelationNames() throws SQLException { + return delegate.supportsDifferentTableCorrelationNames(); + } + + @Override + public boolean supportsExpressionsInOrderBy() throws SQLException { + return delegate.supportsExpressionsInOrderBy(); + } + + @Override + public boolean supportsOrderByUnrelated() throws SQLException { + return delegate.supportsOrderByUnrelated(); + } + + @Override + public boolean supportsGroupBy() throws SQLException { + return delegate.supportsGroupBy(); + } + + @Override + public boolean supportsGroupByUnrelated() throws SQLException { + return delegate.supportsGroupByUnrelated(); + } + + @Override + public boolean supportsGroupByBeyondSelect() throws SQLException { + return delegate.supportsGroupByBeyondSelect(); + } + + @Override + public boolean supportsLikeEscapeClause() throws SQLException { + return delegate.supportsLikeEscapeClause(); + } + + @Override + public boolean supportsMultipleResultSets() throws SQLException { + return delegate.supportsMultipleResultSets(); + } + + @Override + public boolean supportsMultipleTransactions() throws SQLException { + return delegate.supportsMultipleTransactions(); + } + + @Override + public boolean supportsNonNullableColumns() throws SQLException { + return delegate.supportsNonNullableColumns(); + } + + @Override + public boolean supportsMinimumSQLGrammar() throws SQLException { + return delegate.supportsMinimumSQLGrammar(); + } + + @Override + public boolean supportsCoreSQLGrammar() throws SQLException { + return delegate.supportsCoreSQLGrammar(); + } + + @Override + public boolean supportsExtendedSQLGrammar() throws SQLException { + return delegate.supportsExtendedSQLGrammar(); + } + + @Override + public boolean supportsANSI92EntryLevelSQL() throws SQLException { + return delegate.supportsANSI92EntryLevelSQL(); + } + + @Override + public boolean supportsANSI92IntermediateSQL() throws SQLException { + return delegate.supportsANSI92IntermediateSQL(); + } + + @Override + public boolean supportsANSI92FullSQL() throws SQLException { + return delegate.supportsANSI92FullSQL(); + } + + @Override + public boolean supportsIntegrityEnhancementFacility() throws SQLException { + return delegate.supportsIntegrityEnhancementFacility(); + } + + @Override + public boolean supportsOuterJoins() throws SQLException { + return delegate.supportsOuterJoins(); + } + + @Override + public boolean supportsFullOuterJoins() throws SQLException { + return delegate.supportsFullOuterJoins(); + } + + @Override + public boolean supportsLimitedOuterJoins() throws SQLException { + return delegate.supportsLimitedOuterJoins(); + } + + @Override + public String getSchemaTerm() throws SQLException { + return delegate.getSchemaTerm(); + } + + @Override + public String getProcedureTerm() throws SQLException { + return delegate.getProcedureTerm(); + } + + @Override + public String getCatalogTerm() throws SQLException { + return delegate.getCatalogTerm(); + } + + @Override + public boolean isCatalogAtStart() throws SQLException { + return delegate.isCatalogAtStart(); + } + + @Override + public String getCatalogSeparator() throws SQLException { + return delegate.getCatalogSeparator(); + } + + @Override + public boolean supportsSchemasInDataManipulation() throws SQLException { + return delegate.supportsSchemasInDataManipulation(); + } + + @Override + public boolean supportsSchemasInProcedureCalls() throws SQLException { + return delegate.supportsSchemasInProcedureCalls(); + } + + @Override + public boolean supportsSchemasInTableDefinitions() throws SQLException { + return delegate.supportsSchemasInTableDefinitions(); + } + + @Override + public boolean supportsSchemasInIndexDefinitions() throws SQLException { + return delegate.supportsSchemasInIndexDefinitions(); + } + + @Override + public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException { + return delegate.supportsSchemasInPrivilegeDefinitions(); + } + + @Override + public boolean supportsCatalogsInDataManipulation() throws SQLException { + return delegate.supportsCatalogsInDataManipulation(); + } + + @Override + public boolean supportsCatalogsInProcedureCalls() throws SQLException { + return delegate.supportsCatalogsInProcedureCalls(); + } + + @Override + public boolean supportsCatalogsInTableDefinitions() throws SQLException { + return delegate.supportsCatalogsInTableDefinitions(); + } + + @Override + public boolean supportsCatalogsInIndexDefinitions() throws SQLException { + return delegate.supportsCatalogsInIndexDefinitions(); + } + + @Override + public boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException { + return delegate.supportsCatalogsInPrivilegeDefinitions(); + } + + @Override + public boolean supportsPositionedDelete() throws SQLException { + return delegate.supportsPositionedDelete(); + } + + @Override + public boolean supportsPositionedUpdate() throws SQLException { + return delegate.supportsPositionedUpdate(); + } + + @Override + public boolean supportsSelectForUpdate() throws SQLException { + return delegate.supportsSelectForUpdate(); + } + + @Override + public boolean supportsStoredProcedures() throws SQLException { + return delegate.supportsStoredProcedures(); + } + + @Override + public boolean supportsSubqueriesInComparisons() throws SQLException { + return delegate.supportsSubqueriesInComparisons(); + } + + @Override + public boolean supportsSubqueriesInExists() throws SQLException { + return delegate.supportsSubqueriesInExists(); + } + + @Override + public boolean supportsSubqueriesInIns() throws SQLException { + return delegate.supportsSubqueriesInIns(); + } + + @Override + public boolean supportsSubqueriesInQuantifieds() throws SQLException { + return delegate.supportsSubqueriesInQuantifieds(); + } + + @Override + public boolean supportsCorrelatedSubqueries() throws SQLException { + return delegate.supportsCorrelatedSubqueries(); + } + + @Override + public boolean supportsUnion() throws SQLException { + return delegate.supportsUnion(); + } + + @Override + public boolean supportsUnionAll() throws SQLException { + return delegate.supportsUnionAll(); + } + + @Override + public boolean supportsOpenCursorsAcrossCommit() throws SQLException { + return delegate.supportsOpenCursorsAcrossCommit(); + } + + @Override + public boolean supportsOpenCursorsAcrossRollback() throws SQLException { + return delegate.supportsOpenCursorsAcrossRollback(); + } + + @Override + public boolean supportsOpenStatementsAcrossCommit() throws SQLException { + return delegate.supportsOpenStatementsAcrossCommit(); + } + + @Override + public boolean supportsOpenStatementsAcrossRollback() throws SQLException { + return delegate.supportsOpenStatementsAcrossRollback(); + } + + @Override + public int getMaxBinaryLiteralLength() throws SQLException { + return delegate.getMaxBinaryLiteralLength(); + } + + @Override + public int getMaxCharLiteralLength() throws SQLException { + return delegate.getMaxCharLiteralLength(); + } + + @Override + public int getMaxColumnNameLength() throws SQLException { + return delegate.getMaxColumnNameLength(); + } + + @Override + public int getMaxColumnsInGroupBy() throws SQLException { + return delegate.getMaxColumnsInGroupBy(); + } + + @Override + public int getMaxColumnsInIndex() throws SQLException { + return delegate.getMaxColumnsInIndex(); + } + + @Override + public int getMaxColumnsInOrderBy() throws SQLException { + return delegate.getMaxColumnsInOrderBy(); + } + + @Override + public int getMaxColumnsInSelect() throws SQLException { + return delegate.getMaxColumnsInSelect(); + } + + @Override + public int getMaxColumnsInTable() throws SQLException { + return delegate.getMaxColumnsInTable(); + } + + @Override + public int getMaxConnections() throws SQLException { + return delegate.getMaxConnections(); + } + + @Override + public int getMaxCursorNameLength() throws SQLException { + return delegate.getMaxCursorNameLength(); + } + + @Override + public int getMaxIndexLength() throws SQLException { + return delegate.getMaxIndexLength(); + } + + @Override + public int getMaxSchemaNameLength() throws SQLException { + return delegate.getMaxSchemaNameLength(); + } + + @Override + public int getMaxProcedureNameLength() throws SQLException { + return delegate.getMaxProcedureNameLength(); + } + + @Override + public int getMaxCatalogNameLength() throws SQLException { + return delegate.getMaxCatalogNameLength(); + } + + @Override + public int getMaxRowSize() throws SQLException { + return delegate.getMaxRowSize(); + } + + @Override + public boolean doesMaxRowSizeIncludeBlobs() throws SQLException { + return delegate.doesMaxRowSizeIncludeBlobs(); + } + + @Override + public int getMaxStatementLength() throws SQLException { + return delegate.getMaxStatementLength(); + } + + @Override + public int getMaxStatements() throws SQLException { + return delegate.getMaxStatements(); + } + + @Override + public int getMaxTableNameLength() throws SQLException { + return delegate.getMaxTableNameLength(); + } + + @Override + public int getMaxTablesInSelect() throws SQLException { + return delegate.getMaxTablesInSelect(); + } + + @Override + public int getMaxUserNameLength() throws SQLException { + return delegate.getMaxUserNameLength(); + } + + @Override + public int getDefaultTransactionIsolation() throws SQLException { + return delegate.getDefaultTransactionIsolation(); + } + + @Override + public boolean supportsTransactions() throws SQLException { + return delegate.supportsTransactions(); + } + + @Override + public boolean supportsTransactionIsolationLevel(int level) throws SQLException { + return delegate.supportsTransactionIsolationLevel(level); + } + + @Override + public boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException { + return delegate.supportsDataDefinitionAndDataManipulationTransactions(); + } + + @Override + public boolean supportsDataManipulationTransactionsOnly() throws SQLException { + return delegate.supportsDataManipulationTransactionsOnly(); + } + + @Override + public boolean dataDefinitionCausesTransactionCommit() throws SQLException { + return delegate.dataDefinitionCausesTransactionCommit(); + } + + @Override + public boolean dataDefinitionIgnoredInTransactions() throws SQLException { + return delegate.dataDefinitionIgnoredInTransactions(); + } + + @Override + public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { + ResultSet resultSet = delegate.getProcedures(catalog, schemaPattern, procedureNamePattern); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException { + ResultSet resultSet = delegate.getProcedureColumns(catalog, schemaPattern, procedureNamePattern, columnNamePattern); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) throws SQLException { + ResultSet resultSet = delegate.getTables(catalog, schemaPattern, tableNamePattern, types); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getSchemas() throws SQLException { + ResultSet resultSet = delegate.getSchemas(); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getCatalogs() throws SQLException { + ResultSet resultSet = delegate.getCatalogs(); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getTableTypes() throws SQLException { + ResultSet resultSet = delegate.getTableTypes(); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + ResultSet resultSet = delegate.getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException { + ResultSet resultSet = delegate.getColumnPrivileges(catalog, schema, table, columnNamePattern); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + ResultSet resultSet = delegate.getTablePrivileges(catalog, schemaPattern, tableNamePattern); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException { + ResultSet resultSet = delegate.getBestRowIdentifier(catalog, schema, table, scope, nullable); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { + ResultSet resultSet = delegate.getVersionColumns(catalog, schema, table); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { + ResultSet resultSet = delegate.getPrimaryKeys(catalog, schema, table); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { + ResultSet resultSet = delegate.getImportedKeys(catalog, schema, table); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { + ResultSet resultSet = delegate.getExportedKeys(catalog, schema, table); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) throws SQLException { + ResultSet resultSet = delegate.getCrossReference(parentCatalog, parentSchema, parentTable, foreignCatalog, foreignSchema, foreignTable); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getTypeInfo() throws SQLException { + ResultSet resultSet = delegate.getTypeInfo(); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException { + ResultSet resultSet = delegate.getIndexInfo(catalog, schema, table, unique, approximate); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public boolean supportsResultSetType(int type) throws SQLException { + return delegate.supportsResultSetType(type); + } + + @Override + public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { + return delegate.supportsResultSetConcurrency(type, concurrency); + } + + @Override + public boolean ownUpdatesAreVisible(int type) throws SQLException { + return delegate.ownUpdatesAreVisible(type); + } + + @Override + public boolean ownDeletesAreVisible(int type) throws SQLException { + return delegate.ownDeletesAreVisible(type); + } + + @Override + public boolean ownInsertsAreVisible(int type) throws SQLException { + return delegate.ownInsertsAreVisible(type); + } + + @Override + public boolean othersUpdatesAreVisible(int type) throws SQLException { + return delegate.othersUpdatesAreVisible(type); + } + + @Override + public boolean othersDeletesAreVisible(int type) throws SQLException { + return delegate.othersDeletesAreVisible(type); + } + + @Override + public boolean othersInsertsAreVisible(int type) throws SQLException { + return delegate.othersInsertsAreVisible(type); + } + + @Override + public boolean updatesAreDetected(int type) throws SQLException { + return delegate.updatesAreDetected(type); + } + + @Override + public boolean deletesAreDetected(int type) throws SQLException { + return delegate.deletesAreDetected(type); + } + + @Override + public boolean insertsAreDetected(int type) throws SQLException { + return delegate.insertsAreDetected(type); + } + + @Override + public boolean supportsBatchUpdates() throws SQLException { + return delegate.supportsBatchUpdates(); + } + + @Override + public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { + ResultSet resultSet = delegate.getUDTs(catalog, schemaPattern, typeNamePattern, types); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { + ResultSet resultSet = delegate.getSuperTypes(catalog, schemaPattern, typeNamePattern); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + ResultSet resultSet = delegate.getSuperTables(catalog, schemaPattern, tableNamePattern); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) throws SQLException { + ResultSet resultSet = delegate.getAttributes(catalog, schemaPattern, typeNamePattern, attributeNamePattern); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public boolean supportsResultSetHoldability(int holdability) throws SQLException { + return delegate.supportsResultSetHoldability(holdability); + } + + @Override + public int getResultSetHoldability() throws SQLException { + return delegate.getResultSetHoldability(); + } + + @Override + public int getDatabaseMajorVersion() throws SQLException { + return delegate.getDatabaseMajorVersion(); + } + + @Override + public int getDatabaseMinorVersion() throws SQLException { + return delegate.getDatabaseMinorVersion(); + } + + @Override + public int getJDBCMajorVersion() throws SQLException { + return delegate.getJDBCMajorVersion(); + } + + @Override + public int getJDBCMinorVersion() throws SQLException { + return delegate.getJDBCMinorVersion(); + } + + @Override + public int getSQLStateType() throws SQLException { + return delegate.getSQLStateType(); + } + + @Override + public boolean locatorsUpdateCopy() throws SQLException { + return delegate.locatorsUpdateCopy(); + } + + @Override + public boolean supportsStatementPooling() throws SQLException { + return delegate.supportsStatementPooling(); + } + + @Override + public RowIdLifetime getRowIdLifetime() throws SQLException { + return delegate.getRowIdLifetime(); + } + + @Override + public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { + ResultSet resultSet = delegate.getSchemas(catalog, schemaPattern); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { + return delegate.supportsStoredFunctionsUsingCallSyntax(); + } + + @Override + public boolean autoCommitFailureClosesAllResultSets() throws SQLException { + return delegate.autoCommitFailureClosesAllResultSets(); + } + + @Override + public ResultSet getClientInfoProperties() throws SQLException { + ResultSet resultSet = delegate.getClientInfoProperties(); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException { + ResultSet resultSet = delegate.getFunctions(catalog, schemaPattern, functionNamePattern); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) throws SQLException { + ResultSet resultSet = delegate.getFunctionColumns(catalog, schemaPattern, functionNamePattern, columnNamePattern); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + ResultSet resultSet = delegate.getPseudoColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern); + Statement statement = resultSet.getStatement(); + if (statement != null) { + statement = ProxyFactory.getProxyStatement(connection, statement); + } + return ProxyFactory.getProxyResultSet(connection, (ProxyStatement) statement, resultSet); + } + + @Override + public boolean generatedKeyAlwaysReturned() throws SQLException { + return delegate.generatedKeyAlwaysReturned(); + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public T unwrap(Class iface) throws SQLException { + if (iface.isInstance(delegate)) { + return (T) delegate; + } else if (delegate != null) { + return delegate.unwrap(iface); + } + throw new SQLException("Wrapped DatabaseMetaData is not an instance of " + iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return delegate.isWrapperFor(iface); + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyFactory.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyFactory.java new file mode 100644 index 0000000..c09325f --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyFactory.java @@ -0,0 +1,68 @@ +package org.xbib.jdbc.connection.pool; + +import org.xbib.jdbc.connection.pool.util.FastList; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; + +/** + * A factory class that produces proxies around instances of the standard JDBC interfaces. + */ +@SuppressWarnings("unused") +public class ProxyFactory { + + private ProxyFactory() { + // unconstructable + } + + /** + * Create a proxy for the specified {@link Connection} instance. + * + * @param poolEntry the PoolEntry holding pool state + * @param connection the raw database Connection + * @param openStatements a reusable list to track open Statement instances + * @param leakTask the ProxyLeakTask for this connection + * @param now the current timestamp + * @param isReadOnly the default readOnly state of the connection + * @param isAutoCommit the default autoCommit state of the connection + * @return a proxy that wraps the specified {@link Connection} + */ + public static Connection getProxyConnection(PoolEntry poolEntry, + Connection connection, + FastList openStatements, + ProxyLeakTask leakTask, + long now, + boolean isReadOnly, + boolean isAutoCommit) { + return new ProxyConnection(poolEntry, connection, openStatements, leakTask, now, isReadOnly, isAutoCommit); + } + + public static Statement getProxyStatement(ProxyConnection connection, + Statement statement) { + return new ProxyStatement(connection, statement); + } + + public static CallableStatement getProxyCallableStatement(ProxyConnection connection, + CallableStatement statement) { + return new ProxyCallableStatement(connection, statement); + } + + public static PreparedStatement getProxyPreparedStatement(ProxyConnection connection, + PreparedStatement statement) { + return new ProxyPreparedStatement(connection, statement); + } + + public static ResultSet getProxyResultSet(ProxyConnection connection, + ProxyStatement statement, + ResultSet resultSet) { + return new ProxyResultSet(connection, statement, resultSet); + } + + public static DatabaseMetaData getProxyDatabaseMetaData(ProxyConnection connection, + DatabaseMetaData metaData) { + return new ProxyDatabaseMetaData(connection, metaData); + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTask.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTask.java new file mode 100644 index 0000000..0ae99ac --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTask.java @@ -0,0 +1,77 @@ +package org.xbib.jdbc.connection.pool; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A Runnable that is scheduled in the future to report leaks. + * The ScheduledFuture is cancelled if the connection is closed before the leak time expires. + */ +public class ProxyLeakTask implements Runnable { + + private static final Logger logger = Logger.getLogger(ProxyLeakTask.class.getName()); + + public static final ProxyLeakTask NO_LEAK; + + private ScheduledFuture scheduledFuture; + + private String connectionName; + + private Exception exception; + + private String threadName; + + private boolean isLeaked; + + static { + NO_LEAK = new ProxyLeakTask() { + @Override + public void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold) { + } + + @Override + public void run() { + } + + @Override + public void cancel() { + } + }; + } + + public ProxyLeakTask(final PoolEntry poolEntry) { + this.exception = new Exception("Apparent connection leak detected"); + this.threadName = Thread.currentThread().getName(); + this.connectionName = poolEntry.getConnection().toString(); + } + + private ProxyLeakTask() { + } + + public void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold) { + scheduledFuture = executorService.schedule(this, leakDetectionThreshold, TimeUnit.MILLISECONDS); + } + + /** + * {@inheritDoc} + */ + @Override + public void run() { + isLeaked = true; + final StackTraceElement[] stackTrace = exception.getStackTrace(); + final StackTraceElement[] trace = new StackTraceElement[stackTrace.length - 5]; + System.arraycopy(stackTrace, 5, trace, 0, trace.length); + exception.setStackTrace(trace); + logger.log(Level.WARNING, "Connection leak detection triggered for on thread, stack trace follows: " + connectionName + " " + threadName, exception); + } + + public void cancel() { + scheduledFuture.cancel(false); + if (isLeaked) { + logger.log(Level.INFO, "Previously reported leaked connection on thread was returned to the pool (unleaked: )" + connectionName + " " + threadName); + } + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTaskFactory.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTaskFactory.java new file mode 100644 index 0000000..b89c410 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyLeakTaskFactory.java @@ -0,0 +1,32 @@ +package org.xbib.jdbc.connection.pool; + +import java.util.concurrent.ScheduledExecutorService; + +/** + * A factory for {@link ProxyLeakTask} Runnables that are scheduled in the future to report leaks. + */ +public class ProxyLeakTaskFactory { + + private final ScheduledExecutorService executorService; + + private long leakDetectionThreshold; + + public ProxyLeakTaskFactory(final long leakDetectionThreshold, final ScheduledExecutorService executorService) { + this.executorService = executorService; + this.leakDetectionThreshold = leakDetectionThreshold; + } + + public ProxyLeakTask schedule(final PoolEntry poolEntry) { + return (leakDetectionThreshold == 0) ? ProxyLeakTask.NO_LEAK : scheduleNewTask(poolEntry); + } + + public void updateLeakDetectionThreshold(final long leakDetectionThreshold) { + this.leakDetectionThreshold = leakDetectionThreshold; + } + + private ProxyLeakTask scheduleNewTask(PoolEntry poolEntry) { + ProxyLeakTask task = new ProxyLeakTask(poolEntry); + task.schedule(executorService, leakDetectionThreshold); + return task; + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyPreparedStatement.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyPreparedStatement.java new file mode 100644 index 0000000..bf77159 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyPreparedStatement.java @@ -0,0 +1,329 @@ +package org.xbib.jdbc.connection.pool; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; + +/** + * This is the proxy class for java.sql.PreparedStatement. + */ +public class ProxyPreparedStatement extends ProxyStatement implements PreparedStatement { + + public ProxyPreparedStatement(ProxyConnection connection, PreparedStatement statement) { + super(connection, statement); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute() throws SQLException { + connection.markCommitStateDirty(); + return ((PreparedStatement) delegate).execute(); + } + + @Override + public void addBatch() throws SQLException { + ((PreparedStatement) delegate).addBatch(); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + ((PreparedStatement) delegate).setCharacterStream(parameterIndex, reader, length); + } + + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException { + ((PreparedStatement) delegate).setRef(parameterIndex, x); + } + + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException { + ((PreparedStatement) delegate).setBlob(parameterIndex, x); + } + + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException { + ((PreparedStatement) delegate).setClob(parameterIndex, x); + } + + @Override + public void setArray(int parameterIndex, Array x) throws SQLException { + ((PreparedStatement) delegate).setArray(parameterIndex, x); + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + return ((PreparedStatement) delegate).getMetaData(); + } + + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + ((PreparedStatement) delegate).setDate(parameterIndex, x); + } + + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + ((PreparedStatement) delegate).setTime(parameterIndex, x); + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + ((PreparedStatement) delegate).setTimestamp(parameterIndex, x, cal); + } + + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + ((PreparedStatement) delegate).setNull(parameterIndex, sqlType, typeName); + } + + @Override + public void setURL(int parameterIndex, URL x) throws SQLException { + ((PreparedStatement) delegate).setURL(parameterIndex, x); + } + + @Override + public ParameterMetaData getParameterMetaData() throws SQLException { + return ((PreparedStatement) delegate).getParameterMetaData(); + } + + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException { + ((PreparedStatement) delegate).setRowId(parameterIndex, x); + } + + @Override + public void setNString(int parameterIndex, String value) throws SQLException { + ((PreparedStatement) delegate).setNString(parameterIndex, value); + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + ((PreparedStatement) delegate).setNCharacterStream(parameterIndex, value, length); + } + + @Override + public void setNClob(int parameterIndex, NClob value) throws SQLException { + ((PreparedStatement) delegate).setNClob(parameterIndex, value); + } + + @Override + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + ((PreparedStatement) delegate).setClob(parameterIndex, reader, length); + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + ((PreparedStatement) delegate).setBlob(parameterIndex, inputStream, length); + } + + @Override + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + ((PreparedStatement) delegate).setNClob(parameterIndex, reader, length); + } + + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + ((PreparedStatement) delegate).setSQLXML(parameterIndex, xmlObject); + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + ((PreparedStatement) delegate).setObject(parameterIndex, x, targetSqlType, scaleOrLength); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + ((PreparedStatement) delegate).setAsciiStream(parameterIndex, x, length); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + ((PreparedStatement) delegate).setBinaryStream(parameterIndex, x, length); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + ((PreparedStatement) delegate).setCharacterStream(parameterIndex, reader, length); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + ((PreparedStatement) delegate).setAsciiStream(parameterIndex, x); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + ((PreparedStatement) delegate).setBinaryStream(parameterIndex, x); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + ((PreparedStatement) delegate).setCharacterStream(parameterIndex, reader); + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + ((PreparedStatement) delegate).setNCharacterStream(parameterIndex, value); + } + + @Override + public void setClob(int parameterIndex, Reader reader) throws SQLException { + ((PreparedStatement) delegate).setClob(parameterIndex, reader); + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + ((PreparedStatement) delegate).setBlob(parameterIndex, inputStream); + } + + @Override + public void setNClob(int parameterIndex, Reader reader) throws SQLException { + ((PreparedStatement) delegate).setNClob(parameterIndex, reader); + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSet executeQuery() throws SQLException { + connection.markCommitStateDirty(); + ResultSet resultSet = ((PreparedStatement) getDelegate()).executeQuery(); + return ProxyFactory.getProxyResultSet(connection, this, resultSet); + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate() throws SQLException { + connection.markCommitStateDirty(); + return ((PreparedStatement) getDelegate()).executeUpdate(); + } + + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + ((PreparedStatement) delegate).setNull(parameterIndex, sqlType); + } + + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + ((PreparedStatement) delegate).setBoolean(parameterIndex, x); + } + + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + ((PreparedStatement) delegate).setByte(parameterIndex, x); + } + + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + ((PreparedStatement) delegate).setShort(parameterIndex, x); + } + + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + ((PreparedStatement) delegate).setInt(parameterIndex, x); + } + + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + ((PreparedStatement) delegate).setLong(parameterIndex, x); + } + + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + ((PreparedStatement) delegate).setFloat(parameterIndex, x); + } + + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + ((PreparedStatement) delegate).setDouble(parameterIndex, x); + } + + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + ((PreparedStatement) delegate).setBigDecimal(parameterIndex, x); + } + + @Override + public void setString(int parameterIndex, String x) throws SQLException { + ((PreparedStatement) delegate).setString(parameterIndex, x); + } + + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException { + ((PreparedStatement) delegate).setBytes(parameterIndex, x); + } + + @Override + public void setDate(int parameterIndex, Date x) throws SQLException { + ((PreparedStatement) delegate).setDate(parameterIndex, x); + } + + @Override + public void setTime(int parameterIndex, Time x) throws SQLException { + ((PreparedStatement) delegate).setTime(parameterIndex, x); + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + ((PreparedStatement) delegate).setTimestamp(parameterIndex, x); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + ((PreparedStatement) delegate).setAsciiStream(parameterIndex, x, length); + } + + @Override + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + ((PreparedStatement) delegate).setUnicodeStream(parameterIndex, x, length); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + ((PreparedStatement) delegate).setBinaryStream(parameterIndex, x, length); + } + + @Override + public void clearParameters() throws SQLException { + ((PreparedStatement) delegate).clearParameters(); + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + ((PreparedStatement) delegate).setObject(parameterIndex, x, targetSqlType); + } + + @Override + public void setObject(int parameterIndex, Object x) throws SQLException { + ((PreparedStatement) delegate).setObject(parameterIndex, x); + } + + /** + * {@inheritDoc} + */ + @Override + public long executeLargeUpdate() throws SQLException { + connection.markCommitStateDirty(); + return ((PreparedStatement) getDelegate()).executeLargeUpdate(); + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyResultSet.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyResultSet.java new file mode 100644 index 0000000..aaecb95 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyResultSet.java @@ -0,0 +1,1033 @@ +package org.xbib.jdbc.connection.pool; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +/** + * This is the proxy class for java.sql.ResultSet. + */ +public class ProxyResultSet implements ResultSet { + + private final ProxyConnection connection; + + private final ProxyStatement statement; + + private final ResultSet delegate; + + public ProxyResultSet(ProxyConnection connection, ProxyStatement statement, ResultSet resultSet) { + this.connection = connection; + this.statement = statement; + this.delegate = resultSet; + } + + public ResultSet getDelegate() { + return delegate; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return this.getClass().getSimpleName() + '@' + System.identityHashCode(this) + " wrapping " + delegate; + } + + /** + * {@inheritDoc} + */ + @Override + public Statement getStatement() throws SQLException { + return statement; + } + + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { + return delegate.getObject(columnIndex, map); + } + + @Override + public Ref getRef(int columnIndex) throws SQLException { + return delegate.getRef(columnIndex); + } + + @Override + public Blob getBlob(int columnIndex) throws SQLException { + return delegate.getBlob(columnIndex); + } + + @Override + public Clob getClob(int columnIndex) throws SQLException { + return delegate.getClob(columnIndex); + } + + @Override + public Array getArray(int columnIndex) throws SQLException { + return delegate.getArray(columnIndex); + } + + @Override + public Object getObject(String columnLabel, Map> map) throws SQLException { + return delegate.getObject(columnLabel, map); + } + + @Override + public Ref getRef(String columnLabel) throws SQLException { + return delegate.getRef(columnLabel); + } + + @Override + public Blob getBlob(String columnLabel) throws SQLException { + return delegate.getBlob(columnLabel); + } + + @Override + public Clob getClob(String columnLabel) throws SQLException { + return delegate.getClob(columnLabel); + } + + @Override + public Array getArray(String columnLabel) throws SQLException { + return delegate.getArray(columnLabel); + } + + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + return delegate.getDate(columnIndex, cal); + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + return delegate.getDate(columnLabel, cal); + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + return delegate.getTime(columnIndex, cal); + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + return delegate.getTime(columnLabel, cal); + } + + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + return delegate.getTimestamp(columnIndex, cal); + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + return delegate.getTimestamp(columnLabel, cal); + } + + @Override + public URL getURL(int columnIndex) throws SQLException { + return delegate.getURL(columnIndex); + } + + @Override + public URL getURL(String columnLabel) throws SQLException { + return delegate.getURL(columnLabel); + } + + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + delegate.updateRef(columnIndex, x); + } + + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + delegate.updateRef(columnLabel, x); + } + + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + delegate.updateBlob(columnIndex, x); + } + + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + delegate.updateBlob(columnLabel, x); + } + + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + delegate.updateClob(columnIndex, x); + } + + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + delegate.updateClob(columnLabel, x); + } + + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + delegate.updateArray(columnIndex, x); + } + + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + delegate.updateArray(columnLabel, x); + } + + @Override + public RowId getRowId(int columnIndex) throws SQLException { + return delegate.getRowId(columnIndex); + } + + @Override + public RowId getRowId(String columnLabel) throws SQLException { + return delegate.getRowId(columnLabel); + } + + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + delegate.updateRowId(columnIndex, x); + } + + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + delegate.updateRowId(columnLabel, x); + } + + @Override + public int getHoldability() throws SQLException { + return delegate.getHoldability(); + } + + @Override + public boolean isClosed() throws SQLException { + return delegate.isClosed(); + } + + @Override + public void updateNString(int columnIndex, String nString) throws SQLException { + delegate.updateNString(columnIndex, nString); + } + + @Override + public void updateNString(String columnLabel, String nString) throws SQLException { + delegate.updateNString(columnLabel, nString); + } + + @Override + public void updateNClob(int columnIndex, NClob nClob) throws SQLException { + delegate.updateNClob(columnIndex, nClob); + } + + @Override + public void updateNClob(String columnLabel, NClob nClob) throws SQLException { + delegate.updateNClob(columnLabel, nClob); + } + + @Override + public NClob getNClob(int columnIndex) throws SQLException { + return delegate.getNClob(columnIndex); + } + + @Override + public NClob getNClob(String columnLabel) throws SQLException { + return delegate.getNClob(columnLabel); + } + + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException { + return delegate.getSQLXML(columnIndex); + } + + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException { + return delegate.getSQLXML(columnLabel); + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + delegate.updateSQLXML(columnIndex, xmlObject); + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + delegate.updateSQLXML(columnLabel, xmlObject); + } + + @Override + public String getNString(int columnIndex) throws SQLException { + return delegate.getNString(columnIndex); + } + + @Override + public String getNString(String columnLabel) throws SQLException { + return delegate.getNString(columnLabel); + } + + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException { + return delegate.getNCharacterStream(columnIndex); + } + + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException { + return delegate.getNCharacterStream(columnLabel); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + delegate.updateNCharacterStream(columnIndex, x, length); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + delegate.updateNCharacterStream(columnLabel, reader, length); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + delegate.updateAsciiStream(columnIndex, x, length); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + delegate.updateBinaryStream(columnIndex, x, length); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + delegate.updateCharacterStream(columnIndex, x, length); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { + delegate.updateAsciiStream(columnLabel, x, length); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { + delegate.updateBinaryStream(columnLabel, x, length); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + delegate.updateCharacterStream(columnLabel, reader, length); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { + delegate.updateBlob(columnIndex, inputStream, length); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { + delegate.updateBlob(columnLabel, inputStream, length); + } + + @Override + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { + delegate.updateClob(columnIndex, reader, length); + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + delegate.updateClob(columnLabel, reader, length); + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { + delegate.updateNClob(columnIndex, reader, length); + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { + delegate.updateNClob(columnLabel, reader, length); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + delegate.updateNCharacterStream(columnIndex, x); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + delegate.updateNCharacterStream(columnLabel, reader); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + delegate.updateAsciiStream(columnIndex, x); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + delegate.updateBinaryStream(columnIndex, x); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + delegate.updateCharacterStream(columnIndex, x); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + delegate.updateAsciiStream(columnLabel, x); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + delegate.updateBinaryStream(columnLabel, x); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + delegate.updateCharacterStream(columnLabel, reader); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { + delegate.updateBlob(columnIndex, inputStream); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + delegate.updateBlob(columnLabel, inputStream); + } + + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException { + delegate.updateClob(columnIndex, reader); + } + + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException { + delegate.updateClob(columnLabel, reader); + } + + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + delegate.updateNClob(columnIndex, reader); + } + + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException { + delegate.updateNClob(columnLabel, reader); + } + + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + return delegate.getObject(columnIndex, type); + } + + @Override + public T getObject(String columnLabel, Class type) throws SQLException { + return delegate.getObject(columnLabel, type); + } + + /** + * {@inheritDoc} + */ + @Override + public void updateRow() throws SQLException { + connection.markCommitStateDirty(); + delegate.updateRow(); + } + + @Override + public boolean next() throws SQLException { + return delegate.next(); + } + + @Override + public void close() throws SQLException { + delegate.close(); + } + + @Override + public boolean wasNull() throws SQLException { + return delegate.wasNull(); + } + + @Override + public String getString(int columnIndex) throws SQLException { + return delegate.getString(columnIndex); + } + + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + return delegate.getBoolean(columnIndex); + } + + @Override + public byte getByte(int columnIndex) throws SQLException { + return delegate.getByte(0); + } + + @Override + public short getShort(int columnIndex) throws SQLException { + return delegate.getShort(columnIndex); + } + + @Override + public int getInt(int columnIndex) throws SQLException { + return delegate.getInt(columnIndex); + } + + @Override + public long getLong(int columnIndex) throws SQLException { + return delegate.getInt(columnIndex); + } + + @Override + public float getFloat(int columnIndex) throws SQLException { + return delegate.getFloat(columnIndex); + } + + @Override + public double getDouble(int columnIndex) throws SQLException { + return delegate.getDouble(columnIndex); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { + return delegate.getBigDecimal(columnIndex); + } + + @Override + public byte[] getBytes(int columnIndex) throws SQLException { + return delegate.getBytes(columnIndex); + } + + @Override + public Date getDate(int columnIndex) throws SQLException { + return delegate.getDate(columnIndex); + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + return delegate.getTime(columnIndex); + } + + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + return delegate.getTimestamp(columnIndex); + } + + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException { + return delegate.getAsciiStream(columnIndex); + } + + @Override + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + return delegate.getUnicodeStream(columnIndex); + } + + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException { + return delegate.getBinaryStream(columnIndex); + } + + @Override + public String getString(String columnLabel) throws SQLException { + return delegate.getString(columnLabel); + } + + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + return delegate.getBoolean(columnLabel); + } + + @Override + public byte getByte(String columnLabel) throws SQLException { + return delegate.getByte(columnLabel); + } + + @Override + public short getShort(String columnLabel) throws SQLException { + return delegate.getShort(columnLabel); + } + + @Override + public int getInt(String columnLabel) throws SQLException { + return delegate.getInt(columnLabel); + } + + @Override + public long getLong(String columnLabel) throws SQLException { + return delegate.getLong(columnLabel); + } + + @Override + public float getFloat(String columnLabel) throws SQLException { + return delegate.getFloat(columnLabel); + } + + @Override + public double getDouble(String columnLabel) throws SQLException { + return delegate.getDouble(columnLabel); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + return delegate.getBigDecimal(columnLabel, scale); + } + + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + return delegate.getBytes(columnLabel); + } + + @Override + public Date getDate(String columnLabel) throws SQLException { + return delegate.getDate(columnLabel); + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + return delegate.getTime(columnLabel); + } + + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + return delegate.getTimestamp(columnLabel); + } + + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + return delegate.getAsciiStream(columnLabel); + } + + @Override + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + return delegate.getUnicodeStream(columnLabel); + } + + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + return delegate.getBinaryStream(columnLabel); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return delegate.getWarnings(); + } + + @Override + public void clearWarnings() throws SQLException { + delegate.clearWarnings(); + } + + @Override + public String getCursorName() throws SQLException { + return delegate.getCursorName(); + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + return delegate.getMetaData(); + } + + @Override + public Object getObject(int columnIndex) throws SQLException { + return delegate.getObject(columnIndex); + } + + @Override + public Object getObject(String columnLabel) throws SQLException { + return delegate.getObject(columnLabel); + } + + @Override + public int findColumn(String columnLabel) throws SQLException { + return delegate.findColumn(columnLabel); + } + + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException { + return delegate.getCharacterStream(columnIndex); + } + + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException { + return delegate.getCharacterStream(columnLabel); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + return delegate.getBigDecimal(columnIndex); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + return delegate.getBigDecimal(columnLabel); + } + + @Override + public boolean isBeforeFirst() throws SQLException { + return delegate.isBeforeFirst(); + } + + @Override + public boolean isAfterLast() throws SQLException { + return delegate.isAfterLast(); + } + + @Override + public boolean isFirst() throws SQLException { + return delegate.isFirst(); + } + + @Override + public boolean isLast() throws SQLException { + return delegate.isLast(); + } + + @Override + public void beforeFirst() throws SQLException { + delegate.beforeFirst(); + } + + @Override + public void afterLast() throws SQLException { + delegate.afterLast(); + } + + @Override + public boolean first() throws SQLException { + return delegate.first(); + } + + @Override + public boolean last() throws SQLException { + return delegate.last(); + } + + @Override + public int getRow() throws SQLException { + return delegate.getRow(); + } + + @Override + public boolean absolute(int row) throws SQLException { + return delegate.absolute(row); + } + + @Override + public boolean relative(int rows) throws SQLException { + return delegate.relative(rows); + } + + @Override + public boolean previous() throws SQLException { + return delegate.previous(); + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + delegate.setFetchSize(direction); + } + + @Override + public int getFetchDirection() throws SQLException { + return delegate.getFetchDirection(); + } + + @Override + public void setFetchSize(int rows) throws SQLException { + delegate.setFetchSize(rows); + } + + @Override + public int getFetchSize() throws SQLException { + return delegate.getFetchSize(); + } + + @Override + public int getType() throws SQLException { + return delegate.getType(); + } + + @Override + public int getConcurrency() throws SQLException { + return delegate.getConcurrency(); + } + + @Override + public boolean rowUpdated() throws SQLException { + return delegate.rowUpdated(); + } + + @Override + public boolean rowInserted() throws SQLException { + return delegate.rowInserted(); + } + + @Override + public boolean rowDeleted() throws SQLException { + return delegate.rowDeleted(); + } + + @Override + public void updateNull(int columnIndex) throws SQLException { + delegate.updateNull(columnIndex); + } + + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + delegate.updateBoolean(columnIndex, x); + } + + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + delegate.updateByte(columnIndex, x); + } + + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + delegate.updateShort(columnIndex, x); + } + + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + delegate.updateInt(columnIndex, x); + } + + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + delegate.updateLong(columnIndex, x); + } + + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + delegate.updateFloat(columnIndex, x); + } + + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + delegate.updateDouble(columnIndex, x); + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + delegate.updateBigDecimal(columnIndex, x); + } + + @Override + public void updateString(int columnIndex, String x) throws SQLException { + delegate.updateString(columnIndex, x); + } + + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + delegate.updateBytes(columnIndex, x); + } + + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + delegate.updateDate(columnIndex, x); + } + + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + delegate.updateTime(columnIndex, x); + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + delegate.updateTimestamp(columnIndex, x); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + delegate.updateAsciiStream(columnIndex, x, length); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + delegate.updateBinaryStream(columnIndex, x, length); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + delegate.updateCharacterStream(columnIndex, x, length); + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + delegate.updateObject(columnIndex, x, scaleOrLength); + } + + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + delegate.updateObject(columnIndex, x); + } + + @Override + public void updateNull(String columnLabel) throws SQLException { + delegate.updateNull(columnLabel); + } + + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + delegate.updateBoolean(columnLabel, x); + } + + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + delegate.updateByte(columnLabel, x); + } + + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + delegate.updateShort(columnLabel, x); + } + + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + delegate.updateInt(columnLabel, x); + } + + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + delegate.updateLong(columnLabel, x); + } + + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + delegate.updateFloat(columnLabel, x); + } + + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + delegate.updateDouble(columnLabel, x); + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + delegate.updateBigDecimal(columnLabel, x); + } + + @Override + public void updateString(String columnLabel, String x) throws SQLException { + delegate.updateString(columnLabel, x); + } + + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + delegate.updateBytes(columnLabel, x); + } + + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + delegate.updateDate(columnLabel, x); + } + + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + delegate.updateTime(columnLabel, x); + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + delegate.updateTimestamp(columnLabel, x); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + delegate.updateAsciiStream(columnLabel, x, length); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { + delegate.updateBinaryStream(columnLabel, x, length); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { + delegate.updateCharacterStream(columnLabel, reader, length); + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + delegate.updateObject(columnLabel, x, scaleOrLength); + } + + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + delegate.updateObject(columnLabel, x); + } + + /** + * {@inheritDoc} + */ + @Override + public void insertRow() throws SQLException { + connection.markCommitStateDirty(); + delegate.insertRow(); + } + + /** + * {@inheritDoc} + */ + @Override + public void deleteRow() throws SQLException { + connection.markCommitStateDirty(); + delegate.deleteRow(); + } + + @Override + public void refreshRow() throws SQLException { + delegate.refreshRow(); + } + + @Override + public void cancelRowUpdates() throws SQLException { + delegate.cancelRowUpdates(); + } + + @Override + public void moveToInsertRow() throws SQLException { + delegate.moveToInsertRow(); + } + + @Override + public void moveToCurrentRow() throws SQLException { + delegate.moveToCurrentRow(); + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public final T unwrap(Class iface) throws SQLException { + if (iface.isInstance(delegate)) { + return (T) delegate; + } else if (delegate != null) { + return delegate.unwrap(iface); + } + + throw new SQLException("Wrapped ResultSet is not an instance of " + iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return delegate.isWrapperFor(iface); + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyStatement.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyStatement.java new file mode 100644 index 0000000..04cf67b --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/ProxyStatement.java @@ -0,0 +1,394 @@ +package org.xbib.jdbc.connection.pool; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.Statement; + +/** + * This is the proxy class for java.sql.Statement. + */ +public class ProxyStatement implements Statement { + + protected final ProxyConnection connection; + + protected final Statement delegate; + + private boolean isClosed; + + private ResultSet proxyResultSet; + + public ProxyStatement(ProxyConnection connection, Statement statement) { + this.connection = connection; + this.delegate = statement; + } + + public Statement getDelegate() { + return delegate; + } + + @SuppressWarnings("unused") + public SQLException checkException(SQLException e) { + return connection.checkException(e); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final String delegateToString = delegate.toString(); + return this.getClass().getSimpleName() + '@' + System.identityHashCode(this) + " wrapping " + delegateToString; + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws SQLException { + synchronized (this) { + if (isClosed) { + return; + } + isClosed = true; + } + connection.untrackStatement(delegate); + try { + delegate.close(); + } catch (SQLException e) { + throw connection.checkException(e); + } + } + + @Override + public int getMaxFieldSize() throws SQLException { + return delegate.getMaxFieldSize(); + } + + @Override + public void setMaxFieldSize(int max) throws SQLException { + delegate.setMaxFieldSize(max); + } + + @Override + public int getMaxRows() throws SQLException { + return delegate.getMaxRows(); + } + + @Override + public void setMaxRows(int max) throws SQLException { + delegate.setMaxRows(max); + } + + @Override + public void setEscapeProcessing(boolean enable) throws SQLException { + delegate.setEscapeProcessing(enable); + } + + @Override + public int getQueryTimeout() throws SQLException { + return delegate.getQueryTimeout(); + } + + @Override + public void setQueryTimeout(int seconds) throws SQLException { + delegate.setQueryTimeout(seconds); + } + + @Override + public void cancel() throws SQLException { + delegate.cancel(); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return delegate.getWarnings(); + } + + @Override + public void clearWarnings() throws SQLException { + delegate.clearWarnings(); + } + + @Override + public void setCursorName(String name) throws SQLException { + delegate.setCursorName(name); + } + + /** + * {@inheritDoc} + */ + @Override + public Connection getConnection() throws SQLException { + return connection; + } + + @Override + public boolean getMoreResults(int current) throws SQLException { + return delegate.getMoreResults(current); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute(String sql) throws SQLException { + connection.markCommitStateDirty(); + return delegate.execute(sql); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + connection.markCommitStateDirty(); + return delegate.execute(sql, autoGeneratedKeys); + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSet executeQuery(String sql) throws SQLException { + connection.markCommitStateDirty(); + ResultSet resultSet = delegate.executeQuery(sql); + return ProxyFactory.getProxyResultSet(connection, this, resultSet); + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate(String sql) throws SQLException { + connection.markCommitStateDirty(); + return delegate.executeUpdate(sql); + } + + /** + * {@inheritDoc} + */ + @Override + public int[] executeBatch() throws SQLException { + connection.markCommitStateDirty(); + return delegate.executeBatch(); + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + connection.markCommitStateDirty(); + return delegate.executeUpdate(sql, autoGeneratedKeys); + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + connection.markCommitStateDirty(); + return delegate.executeUpdate(sql, columnIndexes); + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + connection.markCommitStateDirty(); + return delegate.executeUpdate(sql, columnNames); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + connection.markCommitStateDirty(); + return delegate.execute(sql, columnIndexes); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + connection.markCommitStateDirty(); + return delegate.execute(sql, columnNames); + } + + @Override + public int getResultSetHoldability() throws SQLException { + return delegate.getResultSetHoldability(); + } + + @Override + public boolean isClosed() throws SQLException { + return delegate.isClosed(); + } + + @Override + public void setPoolable(boolean poolable) throws SQLException { + delegate.setPoolable(poolable); + } + + @Override + public boolean isPoolable() throws SQLException { + return delegate.isPoolable(); + } + + @Override + public void closeOnCompletion() throws SQLException { + delegate.closeOnCompletion(); + } + + @Override + public boolean isCloseOnCompletion() throws SQLException { + return delegate.isCloseOnCompletion(); + } + + /** + * {@inheritDoc} + */ + @Override + public long[] executeLargeBatch() throws SQLException { + connection.markCommitStateDirty(); + return delegate.executeLargeBatch(); + } + + /** + * {@inheritDoc} + */ + @Override + public long executeLargeUpdate(String sql) throws SQLException { + connection.markCommitStateDirty(); + return delegate.executeLargeUpdate(sql); + } + + /** + * {@inheritDoc} + */ + @Override + public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + connection.markCommitStateDirty(); + return delegate.executeLargeUpdate(sql, autoGeneratedKeys); + } + + /** + * {@inheritDoc} + */ + @Override + public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException { + connection.markCommitStateDirty(); + return delegate.executeLargeUpdate(sql, columnIndexes); + } + + /** + * {@inheritDoc} + */ + @Override + public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException { + connection.markCommitStateDirty(); + return delegate.executeLargeUpdate(sql, columnNames); + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSet getResultSet() throws SQLException { + final ResultSet resultSet = delegate.getResultSet(); + if (resultSet != null) { + if (proxyResultSet == null || ((ProxyResultSet) proxyResultSet).getDelegate() != resultSet) { + proxyResultSet = ProxyFactory.getProxyResultSet(connection, this, resultSet); + } + } else { + proxyResultSet = null; + } + return proxyResultSet; + } + + @Override + public int getUpdateCount() throws SQLException { + return delegate.getUpdateCount(); + } + + @Override + public boolean getMoreResults() throws SQLException { + return delegate.getMoreResults(); + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + delegate.setFetchDirection(direction); + } + + @Override + public int getFetchDirection() throws SQLException { + return delegate.getFetchDirection(); + } + + @Override + public void setFetchSize(int rows) throws SQLException { + delegate.setFetchSize(rows); + } + + @Override + public int getFetchSize() throws SQLException { + return delegate.getFetchSize(); + } + + @Override + public int getResultSetConcurrency() throws SQLException { + return delegate.getResultSetConcurrency(); + } + + @Override + public int getResultSetType() throws SQLException { + return delegate.getResultSetType(); + } + + @Override + public void addBatch(String sql) throws SQLException { + delegate.addBatch(sql); + } + + @Override + public void clearBatch() throws SQLException { + delegate.clearBatch(); + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSet getGeneratedKeys() throws SQLException { + ResultSet resultSet = delegate.getGeneratedKeys(); + if (proxyResultSet == null || ((ProxyResultSet) proxyResultSet).getDelegate() != resultSet) { + proxyResultSet = ProxyFactory.getProxyResultSet(connection, this, resultSet); + } + return proxyResultSet; + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public final T unwrap(Class iface) throws SQLException { + if (iface.isInstance(delegate)) { + return (T) delegate; + } else if (delegate != null) { + return delegate.unwrap(iface); + } + throw new SQLException("Wrapped statement is not an instance of " + iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return delegate.isWrapperFor(iface); + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/Bag.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/Bag.java new file mode 100644 index 0000000..9a367e5 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/Bag.java @@ -0,0 +1,333 @@ +package org.xbib.jdbc.connection.pool.util; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.AbstractQueuedLongSynchronizer; +import java.util.concurrent.locks.LockSupport; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * This is a specialized concurrent bag that achieves superior performance + * to {@link java.util.concurrent.LinkedBlockingQueue} and + * {@link java.util.concurrent.LinkedTransferQueue} for the purposes of a + * connection pool. It uses {@link ThreadLocal} storage when possible to avoid + * locks, but resorts to scanning a common collection if there are no + * available items in the {@link ThreadLocal} list. Not-in-use items in the + * {@link ThreadLocal} lists can be "stolen" when the borrowing thread has none + * of its own. It is a "lock-less" implementation using a specialized + * {@link AbstractQueuedLongSynchronizer} to manage cross-thread signaling. + * Note that items that are "borrowed" from the bag are not actually + * removed from any collection, so garbage collection will not occur + * even if the reference is abandoned. Thus care must be taken to + * {@link Bag#requite(T)} borrowed objects otherwise a memory leak will result. + * Only the {@link Bag#remove(T)} method can completely remove an object. + * + * @param the templated type to store in the bag + */ +public class Bag implements AutoCloseable { + + private static final Logger logger = Logger.getLogger(Bag.class.getName()); + + private final CopyOnWriteArrayList sharedList; + + private final boolean weakThreadLocals; + + private final ThreadLocal> threadList; + + private final BagStateListener listener; + + private final AtomicInteger waiters; + + private volatile boolean closed; + + private final SynchronousQueue handoffQueue; + + private String lastMessage; + + /** + * Construct a Bag with the specified listener. + * + * @param listener the IBagStateListener to attach to this bag + */ + public Bag(BagStateListener listener) { + this.listener = listener; + this.weakThreadLocals = useWeakThreadLocals(); + this.handoffQueue = new SynchronousQueue<>(true); + this.waiters = new AtomicInteger(); + this.sharedList = new CopyOnWriteArrayList<>(); + if (weakThreadLocals) { + this.threadList = ThreadLocal.withInitial(() -> new ArrayList<>(16)); + } else { + this.threadList = ThreadLocal.withInitial(() -> new FastList<>(BagEntry.class, 16)); + } + } + + public String getLastMessage() { + return lastMessage; + } + + /** + * The method will borrow a BagEntry from the bag, blocking for the + * specified timeout if none are available. + * + * @param timeout how long to wait before giving up, in units of unit + * @param timeUnit a TimeUnit determining how to interpret the timeout parameter + * @return a borrowed instance from the bag or null if a timeout occurs + * @throws InterruptedException if interrupted while waiting + */ + public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException { + // Try the thread-local list first + final List list = threadList.get(); + for (int i = list.size() - 1; i >= 0; i--) { + final Object entry = list.remove(i); + @SuppressWarnings("unchecked") + final T bagEntry = weakThreadLocals ? ((WeakReference) entry).get() : (T) entry; + if (bagEntry != null && bagEntry.compareAndSet(BagEntry.STATE_NOT_IN_USE, BagEntry.STATE_IN_USE)) { + return bagEntry; + } + } + final int waiting = waiters.incrementAndGet(); + try { + for (T bagEntry : sharedList) { + if (bagEntry.compareAndSet(BagEntry.STATE_NOT_IN_USE, BagEntry.STATE_IN_USE)) { + if (waiting > 1) { + listener.addBagItem(waiting - 1); + } + return bagEntry; + } + } + listener.addBagItem(waiting); + timeout = timeUnit.toNanos(timeout); + do { + final long start = ClockSource.currentTime(); + final T bagEntry = handoffQueue.poll(timeout, TimeUnit.NANOSECONDS); + if (bagEntry == null || bagEntry.compareAndSet(BagEntry.STATE_NOT_IN_USE, BagEntry.STATE_IN_USE)) { + return bagEntry; + } + + timeout -= ClockSource.elapsedNanos(start); + } while (timeout > 10_000); + return null; + } finally { + waiters.decrementAndGet(); + } + } + + /** + * This method will return a borrowed object to the bag. Objects + * that are borrowed from the bag but never "requited" will result + * in a memory leak. + * + * @param bagEntry the value to return to the bag + * @throws NullPointerException if value is null + * @throws IllegalStateException if the bagEntry was not borrowed from the bag + */ + public void requite(final T bagEntry) { + bagEntry.setState(BagEntry.STATE_NOT_IN_USE); + for (int i = 0; waiters.get() > 0; i++) { + if (bagEntry.getState() != BagEntry.STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) { + return; + } else if ((i & 0xff) == 0xff) { + LockSupport.parkNanos(TimeUnit.MICROSECONDS.toNanos(10)); + } else { + Thread.yield(); + } + } + final List threadLocalList = threadList.get(); + if (threadLocalList.size() < 50) { + threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry); + } + } + + /** + * Add a new object to the bag for others to borrow. + * + * @param bagEntry an object to add to the bag + */ + public void add(final T bagEntry) { + if (closed) { + lastMessage = "Bag has been closed, ignoring add()"; + logger.info(lastMessage); + throw new IllegalStateException("Bag has been closed, ignoring add()"); + } + sharedList.add(bagEntry); + // spin until a thread takes it or none are waiting + while (waiters.get() > 0 && bagEntry.getState() == BagEntry.STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) { + Thread.yield(); + } + } + + /** + * Remove a value from the bag. This method should only be called + * with objects obtained by {@link Bag#borrow(long, TimeUnit)} or + * {@link Bag#reserve(T)}. + * + * @param bagEntry the value to remove + * @return true if the entry was removed, false otherwise + * @throws IllegalStateException if an attempt is made to remove an object + * from the bag that was not borrowed or reserved first + */ + public boolean remove(final T bagEntry) { + if (!bagEntry.compareAndSet(BagEntry.STATE_IN_USE, BagEntry.STATE_REMOVED) && + !bagEntry.compareAndSet(BagEntry.STATE_RESERVED, BagEntry.STATE_REMOVED) && !closed) { + lastMessage = "attempt to remove an object from the bag that was not borrowed or reserved: " + bagEntry; + logger.warning(lastMessage); + return false; + } + boolean removed = sharedList.remove(bagEntry); + if (!removed && !closed) { + lastMessage = "attempt to remove an object from the bag that does not exist: " + bagEntry; + logger.warning(lastMessage); + } + threadList.get().remove(bagEntry); + return removed; + } + + /** + * Close the bag to further adds. + */ + @Override + public void close() { + closed = true; + } + + /** + * This method provides a "snapshot" in time of the BagEntry + * items in the bag in the specified state. It does not "lock" + * or reserve items in any way. Call {@link Bag#reserve(T)} + * on items in list before performing any action on them. + * + * @param state one of the {@link BagEntry} states + * @return a possibly empty list of objects having the state specified + */ + public List values(int state) { + final List list = sharedList.stream() + .filter(e -> e.getState() == state) + .collect(Collectors.toList()); + Collections.reverse(list); + return list; + } + + /** + * This method provides a "snapshot" in time of the bag items. It + * does not "lock" or reserve items in any way. Call {@link Bag#reserve(T)} + * on items in the list, or understand the concurrency implications of + * modifying items, before performing any action on them. + * + * @return a possibly empty list of (all) bag items + */ + @SuppressWarnings("unchecked") + public List values() { + return (List) sharedList.clone(); + } + + /** + * The method is used to make an item in the bag "unavailable" for + * borrowing. It is primarily used when wanting to operate on items + * returned by the {@link Bag#values(int)} method. Items that are + * reserved can be removed from the bag via {@link Bag#remove(T)} + * without the need to unreserve them. Items that are not removed + * from the bag can be make available for borrowing again by calling + * the {@link Bag#unreserve(BagEntry)} method. + * + * @param bagEntry the item to reserve + * @return true if the item was able to be reserved, false otherwise + */ + public boolean reserve(T bagEntry) { + return bagEntry.compareAndSet(BagEntry.STATE_NOT_IN_USE, BagEntry.STATE_RESERVED); + } + + /** + * This method is used to make an item reserved via {@link Bag#reserve(T)} + * available again for borrowing. + * + * @param bagEntry the item to unreserve + */ + @SuppressWarnings("SpellCheckingInspection") + public void unreserve(final T bagEntry) { + if (bagEntry.compareAndSet(BagEntry.STATE_RESERVED, BagEntry.STATE_NOT_IN_USE)) { + // spin until a thread takes it or none are waiting + while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) { + Thread.yield(); + } + } else { + lastMessage = "attempt to relinquish an object to the bag that was not reserved: " + bagEntry; + logger.warning(lastMessage); + } + } + + /** + * Get the number of threads pending (waiting) for an item from the + * bag to become available. + * + * @return the number of threads waiting for items from the bag + */ + public int getWaitingThreadCount() { + return waiters.get(); + } + + /** + * Get a count of the number of items in the specified state at the time of this call. + * + * @param state the state of the items to count + * @return a count of how many items in the bag are in the specified state + */ + public int getCount(final int state) { + int count = 0; + for (BagEntry e : sharedList) { + if (e.getState() == state) { + count++; + } + } + return count; + } + + public int[] getStateCounts() { + final int[] states = new int[6]; + for (BagEntry e : sharedList) { + ++states[e.getState()]; + } + states[4] = sharedList.size(); + states[5] = waiters.get(); + + return states; + } + + /** + * Get the total number of items in the bag. + * + * @return the number of items in the bag + */ + public int size() { + return sharedList.size(); + } + + public void dumpState() { + sharedList.forEach(entry -> logger.info(entry.toString())); + } + + /** + * Determine whether to use WeakReferences based on whether there is a + * custom ClassLoader implementation sitting between this class and the + * System ClassLoader. + * + * @return true if we should use WeakReferences in our ThreadLocals, false otherwise + */ + private boolean useWeakThreadLocals() { + try { + if (System.getProperty("pool.jdbc.useWeakReferences") != null) { + return Boolean.getBoolean("pool.jdbc.useWeakReferences"); + } + return getClass().getClassLoader() != ClassLoader.getSystemClassLoader(); + } catch (SecurityException se) { + return true; + } + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/BagEntry.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/BagEntry.java new file mode 100644 index 0000000..d1ed2df --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/BagEntry.java @@ -0,0 +1,14 @@ +package org.xbib.jdbc.connection.pool.util; + +public interface BagEntry { + int STATE_NOT_IN_USE = 0; + int STATE_IN_USE = 1; + int STATE_REMOVED = -1; + int STATE_RESERVED = -2; + + boolean compareAndSet(int expectState, int newState); + + void setState(int newState); + + int getState(); +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/BagStateListener.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/BagStateListener.java new file mode 100644 index 0000000..3f48f59 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/BagStateListener.java @@ -0,0 +1,6 @@ +package org.xbib.jdbc.connection.pool.util; + +public interface BagStateListener { + + void addBagItem(int waiting); +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/ClockSource.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/ClockSource.java new file mode 100644 index 0000000..0c8bc05 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/ClockSource.java @@ -0,0 +1,153 @@ +package org.xbib.jdbc.connection.pool.util; + +import java.util.concurrent.TimeUnit; + +/** + * A resolution-independent provider of current time-stamps and elapsed time + * calculations. + */ +public interface ClockSource { + ClockSource CLOCK = ClockSourceFactory.create(); + + /** + * Get the current time-stamp (resolution is opaque). + * + * @return the current time-stamp + */ + static long currentTime() { + return CLOCK.currentTime0(); + } + + long currentTime0(); + + /** + * Convert an opaque time-stamp returned by currentTime() into + * milliseconds. + * + * @param time an opaque time-stamp returned by an instance of this class + * @return the time-stamp in milliseconds + */ + static long toMillis(long time) { + return CLOCK.toMillis0(time); + } + + long toMillis0(long time); + + /** + * Convert an opaque time-stamp returned by currentTime() into + * nanoseconds. + * + * @param time an opaque time-stamp returned by an instance of this class + * @return the time-stamp in nanoseconds + */ + static long toNanos(long time) { + return CLOCK.toNanos0(time); + } + + long toNanos0(long time); + + /** + * Convert an opaque time-stamp returned by currentTime() into an + * elapsed time in milliseconds, based on the current instant in time. + * + * @param startTime an opaque time-stamp returned by an instance of this class + * @return the elapsed time between startTime and now in milliseconds + */ + static long elapsedMillis(long startTime) { + return CLOCK.elapsedMillis0(startTime); + } + + long elapsedMillis0(long startTime); + + /** + * Get the difference in milliseconds between two opaque time-stamps returned + * by currentTime(). + * + * @param startTime an opaque time-stamp returned by an instance of this class + * @param endTime an opaque time-stamp returned by an instance of this class + * @return the elapsed time between startTime and endTime in milliseconds + */ + static long elapsedMillis(long startTime, long endTime) { + return CLOCK.elapsedMillis0(startTime, endTime); + } + + long elapsedMillis0(long startTime, long endTime); + + /** + * Convert an opaque time-stamp returned by currentTime() into an + * elapsed time in milliseconds, based on the current instant in time. + * + * @param startTime an opaque time-stamp returned by an instance of this class + * @return the elapsed time between startTime and now in milliseconds + */ + static long elapsedNanos(long startTime) { + return CLOCK.elapsedNanos0(startTime); + } + + long elapsedNanos0(long startTime); + + /** + * Get the difference in nanoseconds between two opaque time-stamps returned + * by currentTime(). + * + * @param startTime an opaque time-stamp returned by an instance of this class + * @param endTime an opaque time-stamp returned by an instance of this class + * @return the elapsed time between startTime and endTime in nanoseconds + */ + static long elapsedNanos(long startTime, long endTime) { + return CLOCK.elapsedNanos0(startTime, endTime); + } + + long elapsedNanos0(long startTime, long endTime); + + /** + * Return the specified opaque time-stamp plus the specified number of milliseconds. + * + * @param time an opaque time-stamp + * @param millis milliseconds to add + * @return a new opaque time-stamp + */ + static long plusMillis(long time, long millis) { + return CLOCK.plusMillis0(time, millis); + } + + long plusMillis0(long time, long millis); + + TimeUnit getSourceTimeUnit0(); + + /** + * Get a String representation of the elapsed time in appropriate magnitude terminology. + * + * @param startTime an opaque time-stamp + * @param endTime an opaque time-stamp + * @return a string representation of the elapsed time interval + */ + static String elapsedDisplayString(long startTime, long endTime) { + return CLOCK.elapsedDisplayString0(startTime, endTime); + } + + default String elapsedDisplayString0(long startTime, long endTime) { + long elapsedNanos = elapsedNanos0(startTime, endTime); + + StringBuilder sb = new StringBuilder(elapsedNanos < 0 ? "-" : ""); + elapsedNanos = Math.abs(elapsedNanos); + + for (TimeUnit unit : TIMEUNITS_DESCENDING) { + long converted = unit.convert(elapsedNanos, TimeUnit.NANOSECONDS); + if (converted > 0) { + sb.append(converted).append(TIMEUNIT_DISPLAY_VALUES[unit.ordinal()]); + elapsedNanos -= TimeUnit.NANOSECONDS.convert(converted, unit); + } + } + + return sb.toString(); + } + + TimeUnit[] TIMEUNITS_DESCENDING = {TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MINUTES, + TimeUnit.SECONDS, TimeUnit.MILLISECONDS, TimeUnit.MICROSECONDS, + TimeUnit.NANOSECONDS}; + + String[] TIMEUNIT_DISPLAY_VALUES = {"ns", "µs", "ms", "s", "m", "h", "d"}; + + +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/ClockSourceFactory.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/ClockSourceFactory.java new file mode 100644 index 0000000..8c5e7eb --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/ClockSourceFactory.java @@ -0,0 +1,10 @@ +package org.xbib.jdbc.connection.pool.util; + +/** + * Factory class used to create a platform-specific ClockSource. + */ +public class ClockSourceFactory { + public static ClockSource create() { + return new NanosecondClockSource(); + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/DefaultThreadFactory.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/DefaultThreadFactory.java new file mode 100644 index 0000000..5e5f49d --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/DefaultThreadFactory.java @@ -0,0 +1,22 @@ +package org.xbib.jdbc.connection.pool.util; + +import java.util.concurrent.ThreadFactory; + +public class DefaultThreadFactory implements ThreadFactory { + + private final String threadName; + + private final boolean daemon; + + public DefaultThreadFactory(String threadName, boolean daemon) { + this.threadName = threadName; + this.daemon = daemon; + } + + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, threadName); + thread.setDaemon(daemon); + return thread; + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/DriverDataSource.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/DriverDataSource.java new file mode 100644 index 0000000..b949a22 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/DriverDataSource.java @@ -0,0 +1,161 @@ +package org.xbib.jdbc.connection.pool.util; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Enumeration; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.sql.DataSource; + +public class DriverDataSource implements DataSource { + + private static final Logger logger = Logger.getLogger(DriverDataSource.class.getName()); + + private static final String PASSWORD = "password"; + + private static final String USER = "user"; + + private String jdbcUrl; + + private final Properties driverProperties; + + private Driver driver; + + public DriverDataSource(String jdbcUrl, String driverClassName, Properties properties, String username, String password) { + this.jdbcUrl = jdbcUrl; + this.driverProperties = new Properties(); + for (Entry entry : properties.entrySet()) { + driverProperties.setProperty(entry.getKey().toString(), entry.getValue().toString()); + } + if (username != null) { + setUser(username); + } + if (password != null) { + setPassword(password); + } + if (driverClassName != null) { + Enumeration drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + Driver d = drivers.nextElement(); + if (d.getClass().getName().equals(driverClassName)) { + driver = d; + break; + } + } + if (driver == null) { + logger.warning("Registered driver with driverClassName was not found, trying direct instantiation: " + driverClassName); + Class driverClass = null; + ClassLoader threadContextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + if (threadContextClassLoader != null) { + try { + driverClass = threadContextClassLoader.loadClass(driverClassName); + logger.fine("Driver class found in Thread context class loader: " + driverClassName + " " + threadContextClassLoader); + } catch (ClassNotFoundException e) { + logger.fine("Driver class not found in Thread context class loader, trying classloader: " + + driverClassName + " " + threadContextClassLoader + " " + this.getClass().getClassLoader()); + } + } + if (driverClass == null) { + driverClass = this.getClass().getClassLoader().loadClass(driverClassName); + logger.fine("Driver class found in the PoolConfig class classloader:" + driverClassName + " " + this.getClass().getClassLoader()); + } + } catch (ClassNotFoundException e) { + logger.fine("Failed to load driver class from PoolConfig class classloader: " + driverClassName + " " + this.getClass().getClassLoader()); + } + if (driverClass != null) { + try { + driver = (Driver) driverClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to create instance of driver class, trying jdbcUrl resolution: " + driverClassName + " " + e.getMessage(), e); + } + } + } + } + final String sanitizedUrl = jdbcUrl.replaceAll("([?&;]password=)[^&#;]*(.*)", "$1$2"); + try { + if (driver == null) { + driver = DriverManager.getDriver(jdbcUrl); + logger.fine("Loaded driver with class name for jdbcUrl: " + driver.getClass().getName() + " " + sanitizedUrl); + } else if (!driver.acceptsURL(jdbcUrl)) { + throw new RuntimeException("Driver " + driverClassName + " claims to not accept jdbcUrl, " + sanitizedUrl); + } + } catch (SQLException e) { + throw new RuntimeException("Failed to get driver instance for jdbcUrl=" + sanitizedUrl, e); + } + } + + @Override + public Connection getConnection() throws SQLException { + return driver.connect(jdbcUrl, driverProperties); + } + + public void setUrl(String url) { + this.jdbcUrl = url; + } + + public void setUser(String user) { + driverProperties.put(USER, driverProperties.getProperty("user", user)); + } + + public void setPassword(String password) { + driverProperties.put(PASSWORD, driverProperties.getProperty("password", password)); + } + + @Override + public Connection getConnection(final String username, final String password) throws SQLException { + final Properties cloned = (Properties) driverProperties.clone(); + if (username != null) { + cloned.put("user", username); + if (cloned.containsKey("username")) { + cloned.put("username", username); + } + } + if (password != null) { + cloned.put("password", password); + } + + return driver.connect(jdbcUrl, cloned); + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setLogWriter(PrintWriter logWriter) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setLoginTimeout(int seconds) { + DriverManager.setLoginTimeout(seconds); + } + + @Override + public int getLoginTimeout() { + return DriverManager.getLoginTimeout(); + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return driver.getParentLogger(); + } + + @Override + public T unwrap(Class iface) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isWrapperFor(Class iface) { + return false; + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/FastList.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/FastList.java new file mode 100644 index 0000000..d6d4607 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/FastList.java @@ -0,0 +1,357 @@ +package org.xbib.jdbc.connection.pool.util; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.RandomAccess; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + * Fast list without range checking. + */ +public final class FastList implements List, RandomAccess { + + private final Class clazz; + + private T[] elementData; + + private int size; + + /** + * Construct a FastList with a default size of 32. + * + * @param clazz the Class stored in the collection + */ + @SuppressWarnings("unchecked") + public FastList(Class clazz) { + this.elementData = (T[]) Array.newInstance(clazz, 32); + this.clazz = clazz; + } + + /** + * Construct a FastList with a specified size. + * + * @param clazz the Class stored in the collection + * @param capacity the initial size of the FastList + */ + @SuppressWarnings("unchecked") + public FastList(Class clazz, int capacity) { + this.elementData = (T[]) Array.newInstance(clazz, capacity); + this.clazz = clazz; + } + + /** + * Add an element to the tail of the FastList. + * + * @param element the element to add + */ + @Override + public boolean add(T element) { + if (size < elementData.length) { + elementData[size++] = element; + } else { + // overflow-conscious code + final int oldCapacity = elementData.length; + final int newCapacity = oldCapacity << 1; + @SuppressWarnings("unchecked") + final T[] newElementData = (T[]) Array.newInstance(clazz, newCapacity); + System.arraycopy(elementData, 0, newElementData, 0, oldCapacity); + newElementData[size++] = element; + elementData = newElementData; + } + return true; + } + + /** + * Get the element at the specified index. + * + * @param index the index of the element to get + * @return the element, or ArrayIndexOutOfBounds is thrown if the index is invalid + */ + @Override + public T get(int index) { + return elementData[index]; + } + + /** + * Remove the last element from the list. No bound check is performed, so if this + * method is called on an empty list and ArrayIndexOutOfBounds exception will be + * thrown. + * + * @return the last element of the list + */ + public T removeLast() { + T element = elementData[--size]; + elementData[size] = null; + return element; + } + + /** + * This remove method is most efficient when the element being removed + * is the last element. Equality is identity based, not equals() based. + * Only the first matching element is removed. + * + * @param element the element to remove + */ + @Override + public boolean remove(Object element) { + for (int index = size - 1; index >= 0; index--) { + if (element == elementData[index]) { + final int numMoved = size - index - 1; + if (numMoved > 0) { + System.arraycopy(elementData, index + 1, elementData, index, numMoved); + } + elementData[--size] = null; + return true; + } + } + + return false; + } + + /** + * Clear the FastList. + */ + @Override + public void clear() { + for (int i = 0; i < size; i++) { + elementData[i] = null; + } + size = 0; + } + + /** + * Get the current number of elements in the FastList. + * + * @return the number of current elements + */ + @Override + public int size() { + return size; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEmpty() { + return size == 0; + } + + /** + * {@inheritDoc} + */ + @Override + public T set(int index, T element) { + T old = elementData[index]; + elementData[index] = element; + return old; + } + + /** + * {@inheritDoc} + */ + @Override + public T remove(int index) { + if (size == 0) { + return null; + } + final T old = elementData[index]; + final int numMoved = size - index - 1; + if (numMoved > 0) { + System.arraycopy(elementData, index + 1, elementData, index, numMoved); + } + elementData[--size] = null; + return old; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(Object o) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator iterator() { + return new Iterator<>() { + private int index; + + @Override + public boolean hasNext() { + return index < size; + } + + @Override + public T next() { + if (index < size) { + return elementData[index++]; + } + throw new NoSuchElementException("No more elements in FastList"); + } + }; + } + + /** + * {@inheritDoc} + */ + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public E[] toArray(E[] a) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public void add(int index, T element) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public int indexOf(Object o) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public int lastIndexOf(Object o) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public ListIterator listIterator() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public ListIterator listIterator(int index) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public Object clone() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public void forEach(Consumer action) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public Spliterator spliterator() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean removeIf(Predicate filter) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public void replaceAll(UnaryOperator operator) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public void sort(Comparator c) { + throw new UnsupportedOperationException(); + } +} diff --git a/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/NanosecondClockSource.java b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/NanosecondClockSource.java new file mode 100644 index 0000000..aac3114 --- /dev/null +++ b/jdbc-connection-pool/src/main/java/org/xbib/jdbc/connection/pool/util/NanosecondClockSource.java @@ -0,0 +1,77 @@ +package org.xbib.jdbc.connection.pool.util; + +import java.util.concurrent.TimeUnit; + +public class NanosecondClockSource implements ClockSource { + /** + * {@inheritDoc} + */ + @Override + public long currentTime0() { + return System.nanoTime(); + } + + /** + * {@inheritDoc} + */ + @Override + public long toMillis0(final long time) { + return TimeUnit.NANOSECONDS.toMillis(time); + } + + /** + * {@inheritDoc} + */ + @Override + public long toNanos0(final long time) { + return time; + } + + /** + * {@inheritDoc} + */ + @Override + public long elapsedMillis0(final long startTime) { + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime); + } + + /** + * {@inheritDoc} + */ + @Override + public long elapsedMillis0(final long startTime, final long endTime) { + return TimeUnit.NANOSECONDS.toMillis(endTime - startTime); + } + + /** + * {@inheritDoc} + */ + @Override + public long elapsedNanos0(final long startTime) { + return System.nanoTime() - startTime; + } + + /** + * {@inheritDoc} + */ + @Override + public long elapsedNanos0(final long startTime, final long endTime) { + return endTime - startTime; + } + + /** + * {@inheritDoc} + */ + @Override + public long plusMillis0(final long time, final long millis) { + return time + TimeUnit.MILLISECONDS.toNanos(millis); + } + + /** + * {@inheritDoc} + */ + @Override + public TimeUnit getSourceTimeUnit0() { + return TimeUnit.NANOSECONDS; + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/BagTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/BagTest.java new file mode 100644 index 0000000..ada6dcd --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/BagTest.java @@ -0,0 +1,78 @@ +package org.xbib.io.pool.jdbc; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.jdbc.connection.pool.util.Bag; +import java.util.concurrent.CompletableFuture; +import org.xbib.jdbc.connection.pool.Pool; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; +import org.xbib.jdbc.connection.pool.PoolEntry; + +@ExtendWith(PoolTestExtension.class) +public class BagTest { + + private static PoolDataSource ds; + + private static Pool pool; + + @BeforeAll + public static void setup() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(2); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + ds = new PoolDataSource(config); + pool = ds.getPool(); + } + + @AfterAll + public static void teardown() + { + ds.close(); + } + + @Test + public void testBag() throws Exception { + try (Bag bag = new Bag<>((x) -> CompletableFuture.completedFuture(Boolean.TRUE))) { + assertEquals(0, bag.values(8).size()); + PoolEntry reserved = pool.newPoolEntry(); + bag.add(reserved); + bag.reserve(reserved); + PoolEntry inuse = pool.newPoolEntry(); + bag.add(inuse); + bag.borrow(2, MILLISECONDS); + PoolEntry notinuse = pool.newPoolEntry(); + bag.add(notinuse); + bag.dumpState(); + bag.requite(reserved); + bag.remove(notinuse); + assertTrue(bag.getLastMessage().contains("not borrowed or reserved")); + bag.unreserve(notinuse); + assertTrue(bag.getLastMessage().contains("was not reserved")); + bag.remove(inuse); + bag.remove(inuse); + assertTrue(bag.getLastMessage().contains("not borrowed or reserved")); + bag.close(); + try { + PoolEntry bagEntry = pool.newPoolEntry(); + bag.add(bagEntry); + assertNotEquals(bagEntry, bag.borrow(100, MILLISECONDS)); + } + catch (IllegalStateException e) { + assertTrue(bag.getLastMessage().contains("ignoring add()")); + } + assertNotNull(notinuse.toString()); + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConcurrentCloseConnectionTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConcurrentCloseConnectionTest.java new file mode 100644 index 0000000..023073f --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConcurrentCloseConnectionTest.java @@ -0,0 +1,39 @@ +package org.xbib.io.pool.jdbc; + +import org.junit.jupiter.api.Test; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +public class ConcurrentCloseConnectionTest +{ + @Test + public void testConcurrentClose() throws Exception + { + PoolConfig config = new PoolConfig(); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config); + final Connection connection = ds.getConnection()) { + ExecutorService executorService = Executors.newFixedThreadPool(10); + List> futures = new ArrayList<>(); + for (int i = 0; i < 500; i++) { + final PreparedStatement preparedStatement = connection.prepareStatement(""); + futures.add(executorService.submit((Callable) () -> { + preparedStatement.close(); + return null; + })); + } + executorService.shutdown(); + for (Future future : futures) { + future.get(); + } + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionCloseBlockingTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionCloseBlockingTest.java new file mode 100644 index 0000000..b72133e --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionCloseBlockingTest.java @@ -0,0 +1,69 @@ +package org.xbib.io.pool.jdbc; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; +import java.sql.Connection; +import java.sql.SQLException; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.stubbing.Answer; +import org.xbib.io.pool.jdbc.mock.MockDataSource; +import org.xbib.jdbc.connection.pool.util.ClockSource; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +/** + * Test for cases when db network connectivity goes down and close is called on + * existing connections. By default we block longer than getMaximumTimeout + * (it can hang for a lot of time depending on driver timeout settings). + * Closing the connections asynchronously fixes this issue. + */ +@ExtendWith(PoolTestExtension.class) +public class ConnectionCloseBlockingTest { + + private static volatile boolean shouldFail = false; + + @Disabled + @Test + public void testConnectionCloseBlocking() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(1500); + config.setDataSource(new CustomMockDataSource()); + long start = ClockSource.currentTime(); + try (PoolDataSource ds = new PoolDataSource(config); + Connection connection = ds.getConnection()) { + connection.close(); + PoolTestExtension.quietlySleep(1100L); + shouldFail = true; + try (Connection connection2 = ds.getConnection()) { + assertTrue((ClockSource.elapsedMillis(start) < config.getConnectionTimeout()), + "waited longer than timeout: " + config.getConnectionTimeout()); + } + } catch (SQLException e) { + assertTrue((ClockSource.elapsedMillis(start) < config.getConnectionTimeout()), + "getConnection failed because close connection took longer than timeout"); + } + } + + private static class CustomMockDataSource extends MockDataSource { + @Override + public Connection getConnection() throws SQLException { + Connection mockConnection = super.getConnection(); + when(mockConnection.isValid(anyInt())).thenReturn(!shouldFail); + doAnswer((Answer) invocation -> { + if (shouldFail) { + SECONDS.sleep(2); + } + return null; + }).when(mockConnection).close(); + return mockConnection; + } + } + +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionPoolSizeVsThreadsTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionPoolSizeVsThreadsTest.java new file mode 100644 index 0000000..4dbd059 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionPoolSizeVsThreadsTest.java @@ -0,0 +1,141 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.xbib.jdbc.connection.pool.util.ClockSource.currentTime; +import static org.xbib.jdbc.connection.pool.util.ClockSource.elapsedMillis; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.io.pool.jdbc.mock.StubDataSource; +import java.sql.Connection; +import java.text.MessageFormat; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; +import org.xbib.jdbc.connection.pool.Pool; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +@ExtendWith(PoolTestExtension.class) +public class ConnectionPoolSizeVsThreadsTest { + + private static final Logger LOGGER = Logger.getLogger(ConnectionPoolSizeVsThreadsTest.class.getName()); + + private static final int ITERATIONS = 50_000; + + @Test + public void testPoolSizeAboutSameSizeAsThreadCount() throws Exception { + final int threadCount = 50; + final Counts counts = testPoolSize(2, 100, + threadCount, 1, 0, 20, + ITERATIONS, TimeUnit.SECONDS.toMillis(2)); + assertEquals(threadCount, counts.maxActive, 15); + assertEquals(threadCount, counts.maxTotal, 5); + } + + @Test + public void testSlowConnectionTimeBurstyWork() throws Exception { + final int threadCount = 50; + final int workItems = threadCount * 100; + final int workTimeMs = 0; + final int connectionAcquisitionTimeMs = 250; + final Counts counts = testPoolSize(2, 100, + threadCount, workTimeMs, 0, connectionAcquisitionTimeMs, + workItems, TimeUnit.SECONDS.toMillis(3)); + final long totalWorkTime = workItems * workTimeMs; + final long connectionMax = totalWorkTime / connectionAcquisitionTimeMs; + assertTrue(connectionMax <= counts.maxActive); + assertEquals(connectionMax, counts.maxTotal, 2 + 2); + } + + private Counts testPoolSize(final int minIdle, + final int maxPoolSize, + final int threadCount, + final long workTimeMs, + final long restTimeMs, + final long connectionAcquisitionTimeMs, + final int iterations, + final long postTestTimeMs) throws Exception { + LOGGER.info(MessageFormat.format("Starting test (minIdle={0}, maxPoolSize={1}, threadCount={2}, workTimeMs={3}, restTimeMs={4}, connectionAcquisitionTimeMs={5}, iterations={6}, postTestTimeMs={7})", + minIdle, maxPoolSize, threadCount, workTimeMs, restTimeMs, connectionAcquisitionTimeMs, iterations, postTestTimeMs)); + final PoolConfig config = new PoolConfig(); + config.setMinimumIdle(minIdle); + config.setMaximumPoolSize(maxPoolSize); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTimeout(2500); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + final AtomicReference ref = new AtomicReference<>(null); + try (final PoolDataSource ds = new PoolDataSource(config)) { + final StubDataSource stubDataSource = ds.unwrap(StubDataSource.class); + // connection acquisition takes more than 0 ms in a real system + stubDataSource.setConnectionAcquistionTime(connectionAcquisitionTimeMs); + final ExecutorService threadPool = Executors.newFixedThreadPool(threadCount); + final CountDownLatch allThreadsDone = new CountDownLatch(iterations); + for (int i = 0; i < iterations; i++) { + threadPool.submit(() -> { + if (ref.get() == null) { + PoolTestExtension.quietlySleep(restTimeMs); + try (Connection c2 = ds.getConnection()) { + PoolTestExtension.quietlySleep(workTimeMs); + } + catch (Exception e) { + ref.set(e); + } + } + allThreadsDone.countDown(); + }); + } + final Pool pool = ds.getPool(); + final Counts underLoad = new Counts(); + while (allThreadsDone.getCount() > 0 || pool.getTotalConnections() < minIdle) { + PoolTestExtension.quietlySleep(50); + underLoad.updateMaxCounts(pool); + } + LOGGER.info(MessageFormat.format("test over, waiting for post delay time {0} ms ", postTestTimeMs)); + PoolTestExtension.quietlySleep(connectionAcquisitionTimeMs + workTimeMs + restTimeMs); + final Counts postLoad = new Counts(); + final long start = currentTime(); + while (elapsedMillis(start) < postTestTimeMs) { + PoolTestExtension.quietlySleep(50); + postLoad.updateMaxCounts(pool); + } + allThreadsDone.await(); + threadPool.shutdown(); + threadPool.awaitTermination(30, TimeUnit.SECONDS); + if (ref.get() != null) { + LOGGER.severe("task failed: " + ref.get()); + fail("task failed"); + } + LOGGER.info("under load... " + underLoad); + LOGGER.info("post load.... " + postLoad); + if (postTestTimeMs > 0) { + if (postLoad.maxActive != 0) { + fail("max active was greater than 0 after test was done"); + } + final int createdAfterWorkAllFinished = postLoad.maxTotal - underLoad.maxTotal; + assertEquals(0, createdAfterWorkAllFinished, 1, + "connections were created when there was no waiting consumers"); + } + return underLoad; + } + } + + private static class Counts { + int maxTotal = 0; + int maxActive = 0; + + void updateMaxCounts(Pool pool) { + maxTotal = Math.max(pool.getTotalConnections(), maxTotal); + maxActive = Math.max(pool.getActiveConnections(), maxActive); + } + + @Override + public String toString() { + return "counts{" + "max total=" + maxTotal + ", max active=" + maxActive + '}'; + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionRaceConditionTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionRaceConditionTest.java new file mode 100644 index 0000000..203c7a6 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionRaceConditionTest.java @@ -0,0 +1,59 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Test; +import java.sql.Connection; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +public class ConnectionRaceConditionTest { + + private static final Logger logger = Logger.getLogger(ConnectionRaceConditionTest.class.getName()); + + public static final int ITERATIONS = 10_000; + + @Test + public void testRaceCondition() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(10); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTimeout(5000); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + final AtomicReference ref = new AtomicReference<>(null); + try (final PoolDataSource ds = new PoolDataSource(config)) { + ExecutorService threadPool = Executors.newFixedThreadPool(2); + for (int i = 0; i < ITERATIONS; i++) { + threadPool.submit(new Callable() { + @Override + public Exception call() throws Exception { + if (ref.get() == null) { + Connection c2; + try { + c2 = ds.getConnection(); + ds.evictConnection(c2); + } catch (Exception e) { + ref.set(e); + } + } + return null; + } + }); + } + threadPool.shutdown(); + threadPool.awaitTermination(30, TimeUnit.SECONDS); + if (ref.get() != null) { + logger.severe("task failed: " + ref.get()); + fail("task failed"); + } + } catch (Exception e) { + throw e; + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionStateTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionStateTest.java new file mode 100644 index 0000000..c8922b0 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionStateTest.java @@ -0,0 +1,134 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.Properties; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; +import org.xbib.jdbc.connection.pool.ProxyConnection; + +public class ConnectionStateTest { + + @Test + public void testAutoCommit() throws Exception { + Properties properties = new Properties(); + properties.put("user", "bar"); + properties.put("password", "secret"); + properties.put("url", "baf"); + properties.put("loginTimeout", "10"); + PoolConfig config = new PoolConfig(properties); + config.setAutoCommit(true); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + try (Connection connection = ds.getConnection()) { + Connection unwrap = connection.unwrap(Connection.class); + unwrap.setAutoCommit(false); + connection.close(); + assertFalse(unwrap.getAutoCommit()); + } + } + } + + @Test + public void testTransactionIsolation() throws Exception { + Properties properties = new Properties(); + PoolConfig config = new PoolConfig(properties); + config.setTransactionIsolation("TRANSACTION_READ_COMMITTED"); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + try (Connection connection = ds.getConnection()) { + Connection unwrap = connection.unwrap(Connection.class); + unwrap.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); + connection.close(); + assertEquals(Connection.TRANSACTION_READ_UNCOMMITTED, unwrap.getTransactionIsolation()); + } + } + } + + @Test + public void testIsolation() { + PoolConfig config = new PoolConfig(); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + config.setTransactionIsolation("TRANSACTION_REPEATABLE_READ"); + config.validate(); + int transactionIsolation = PoolTestExtension.getTransactionIsolation(config.getTransactionIsolation()); + assertSame(Connection.TRANSACTION_REPEATABLE_READ, transactionIsolation); + } + + @Test + public void testReadOnly() throws Exception { + PoolConfig config = new PoolConfig(); + config.setCatalog("test"); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + try (Connection connection = ds.getConnection()) { + Connection unwrap = connection.unwrap(Connection.class); + connection.setReadOnly(true); + connection.close(); + assertFalse(unwrap.isReadOnly()); + } + } + } + + @Test + public void testCatalog() throws Exception { + PoolConfig config = new PoolConfig(); + config.setCatalog("test"); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + try (Connection connection = ds.getConnection()) { + Connection unwrap = connection.unwrap(Connection.class); + connection.setCatalog("other"); + connection.close(); + assertEquals("test", unwrap.getCatalog()); + } + } + } + + @Test + public void testCommitTracking() throws Exception { + PoolConfig config = new PoolConfig(); + config.setAutoCommit(false); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + try (Connection connection = ds.getConnection()) { + Statement statement = connection.createStatement(); + statement.execute("SELECT something"); + assertTrue(((ProxyConnection)connection).isCommitStateDirty()); + connection.commit(); + assertFalse(((ProxyConnection)connection).isCommitStateDirty()); + statement.execute("SELECT something", Statement.NO_GENERATED_KEYS); + assertTrue(((ProxyConnection)connection).isCommitStateDirty()); + connection.rollback(); + assertFalse(((ProxyConnection)connection).isCommitStateDirty()); + ResultSet resultSet = statement.executeQuery("SELECT something"); + assertTrue(((ProxyConnection)connection).isCommitStateDirty()); + connection.rollback(null); + assertFalse(((ProxyConnection)connection).isCommitStateDirty()); + resultSet.updateRow(); + assertTrue(((ProxyConnection)connection).isCommitStateDirty()); + } + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionTest.java new file mode 100644 index 0000000..fcd00a0 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionTest.java @@ -0,0 +1,552 @@ +package org.xbib.io.pool.jdbc; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.io.pool.jdbc.mock.StubConnection; +import org.xbib.io.pool.jdbc.mock.StubDataSource; +import org.xbib.io.pool.jdbc.mock.StubStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLTransientConnectionException; +import java.sql.Statement; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.xbib.jdbc.connection.pool.Pool; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; +import org.xbib.jdbc.connection.pool.PoolInitializationException; + +@ExtendWith(PoolTestExtension.class) +public class ConnectionTest { + + @Test + public void testCreate() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setConnectionInitSql("SELECT 1"); + config.setReadOnly(true); + config.setConnectionTimeout(2500); + config.setLeakDetectionThreshold(TimeUnit.SECONDS.toMillis(30)); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + ds.setLoginTimeout(10); + assertSame(10, ds.getLoginTimeout()); + Pool pool = ds.getPool(); + ds.getConnection().close(); + assertSame(1, pool.getTotalConnections(), "total connections not as expected"); + assertSame(1, pool.getIdleConnections(), "idle connections not as expected"); + try (Connection connection = ds.getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT * FROM device WHERE device_id=?")) { + assertNotNull(connection); + assertNotNull(statement); + assertSame(1, pool.getTotalConnections(), "total connections not as expected"); + assertSame(0, pool.getIdleConnections(), "idle connections not as expected"); + statement.setInt(1, 0); + try (ResultSet resultSet = statement.executeQuery()) { + assertNotNull(resultSet); + assertFalse(resultSet.next()); + } + } + assertSame(1, pool.getTotalConnections(), "Total connections not as expected"); + assertSame(1, pool.getIdleConnections(), "idle connections not as expected"); + } + } + + @Test + public void testMaxLifetime() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(1500); + config.setConnectionTestQuery("VALUES 1"); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + config.setHousekeepingPeriodMs(2000L); + config.setMaxLifetime(30000L); // must be 30s or higher + try (PoolDataSource ds = new PoolDataSource(config)) { + Pool pool = ds.getPool(); + assertSame(1, pool.getTotalConnections(), "Total connections not as expected"); + assertSame(1, pool.getIdleConnections(), "Idle connections not as expected"); + Connection unwrap; + Connection unwrap2; + try (Connection connection = ds.getConnection()) { + unwrap = connection.unwrap(Connection.class); + assertNotNull(connection); + assertSame(1, pool.getTotalConnections(), "Second total connections not as expected"); + assertSame(0, pool.getIdleConnections(), "Second idle connections not as expected"); + } + assertSame(1, pool.getIdleConnections(), "Idle connections not as expected"); + try (Connection connection = ds.getConnection()) { + unwrap2 = connection.unwrap(Connection.class); + assertSame(unwrap, unwrap2); + assertSame(1, pool.getTotalConnections(), "Second total connections not as expected"); + assertSame(0, pool.getIdleConnections(), "Second idle connections not as expected"); + //pool.evictConnection(connection); + } + Thread.sleep(31000L); + try (Connection connection = ds.getConnection()) { + unwrap2 = connection.unwrap(Connection.class); + assertNotSame(unwrap, unwrap2, "Expected a different connection"); + } + assertSame(1, pool.getTotalConnections(), "Post total connections not as expected"); + assertSame(1, pool.getIdleConnections(), "Post idle connections not as expected"); + } + } + + @Test + public void testMaxLifetime2() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + config.setHousekeepingPeriodMs(100L); + try (PoolDataSource ds = new PoolDataSource(config)) { + ds.getPool().getConfig().setMaxLifetime(700); + Pool pool = ds.getPool(); + assertSame(0, pool.getTotalConnections(), "Total connections not as expected"); + assertSame(0, pool.getIdleConnections(), "Idle connections not as expected"); + Connection unwrap; + Connection unwrap2; + try (Connection connection = ds.getConnection()) { + unwrap = connection.unwrap(Connection.class); + assertNotNull(connection); + assertSame(1, pool.getTotalConnections(), "Second total connections not as expected"); + assertSame(0, pool.getIdleConnections(), "Second idle connections not as expected"); + } + assertSame(1, pool.getIdleConnections(), "Idle connections not as expected"); + try (Connection connection = ds.getConnection()) { + unwrap2 = connection.unwrap(Connection.class); + assertSame(unwrap, unwrap2); + assertSame(1, pool.getTotalConnections(), "Second total connections not as expected"); + assertSame( 0, pool.getIdleConnections(), "Second idle connections not as expected"); + } + PoolTestExtension.quietlySleep(800); + try (Connection connection = ds.getConnection()) { + unwrap2 = connection.unwrap(Connection.class); + assertNotSame(unwrap, unwrap2, "Expected a different connection"); + } + assertSame( 1, pool.getTotalConnections(), "Post total connections not as expected"); + assertSame(1, pool.getIdleConnections(), "Post idle connections not as expected"); + } + } + + @Test + public void testDoubleClose() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config); + Connection connection = ds.getConnection()) { + connection.close(); + connection.abort(null); + assertTrue(connection.isClosed(), "Connection should have closed"); + assertFalse(connection.isValid(5), "Connection should have closed"); + assertTrue(connection.toString().contains("ProxyConnection"), "Expected to contain ProxyConnection, but was " + connection); + } + } + + @Test + public void testEviction() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(5); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + Connection connection = ds.getConnection(); + Pool pool = ds.getPool(); + assertEquals(1, pool.getTotalConnections()); + ds.evictConnection(connection); + assertEquals(0, pool.getTotalConnections()); + } + } + + @Test + public void testEviction2() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMaximumPoolSize(5); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + //config.setExceptionOverrideClassName(OverrideHandler.class.getName()); + try (PoolDataSource ds = new PoolDataSource(config)) { + Pool pool = ds.getPool(); + while (pool.getTotalConnections() < 5) { + PoolTestExtension.quietlySleep(100L); + } + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + PreparedStatement statement = connection.prepareStatement("SELECT some, thing FROM somewhere WHERE something=?"); + assertNotNull(statement); + ResultSet resultSet = statement.executeQuery(); + assertNotNull(resultSet); + try { + statement.getMaxFieldSize(); + } catch (Exception e) { + assertSame(SQLException.class, e.getClass()); + } + } + assertEquals(5, pool.getTotalConnections(), "Total connections not as expected"); + assertEquals(5, pool.getIdleConnections(), "Idle connections not as expected"); + } + } + + @Test + public void testEviction3() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMaximumPoolSize(5); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + Pool pool = ds.getPool(); + while (pool.getTotalConnections() < 5) { + PoolTestExtension.quietlySleep(100L); + } + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + PreparedStatement statement = connection.prepareStatement("SELECT some, thing FROM somewhere WHERE something=?"); + assertNotNull(statement); + ResultSet resultSet = statement.executeQuery(); + assertNotNull(resultSet); + try { + statement.getMaxFieldSize(); + } catch (Exception e) { + assertSame(SQLException.class, e.getClass()); + } + } + assertEquals(5, pool.getTotalConnections(), "Total connections not as expected"); + assertEquals(5, pool.getIdleConnections(), "Idle connections not as expected"); + } + } + + @Test + public void testEvictAllRefill() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(10); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + config.setHousekeepingPeriodMs(100L); + try (PoolDataSource ds = new PoolDataSource(config)) { + Pool pool = ds.getPool(); + for (int i = 0; i < 5; i++) { + final Connection conn = ds.getConnection(); + ds.evictConnection(conn); + } + PoolTestExtension.quietlySleep(SECONDS.toMillis(2)); + int count = 0; + while (pool.getIdleConnections() < 5 && count++ < 20) { + PoolTestExtension.quietlySleep(100); + } + assertEquals(5, pool.getIdleConnections(), + "after eviction, refill did not reach expected 5 connections"); + } + } + + @Test + public void testBackfill() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(4); + config.setConnectionTimeout(1000); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + StubConnection.slowCreate = true; + try (PoolDataSource ds = new PoolDataSource(config)) { + Pool pool = ds.getPool(); + PoolTestExtension.quietlySleep(1250); + assertSame(1, pool.getTotalConnections(), "Total connections not as expected"); + assertSame(1, pool.getIdleConnections(), "Idle connections not as expected"); + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + assertSame(1, pool.getTotalConnections(), "Total connections not as expected"); + assertSame(0, pool.getIdleConnections(), "Idle connections not as expected"); + PreparedStatement statement = connection.prepareStatement("SELECT some, thing FROM somewhere WHERE something=?"); + assertNotNull(statement); + ResultSet resultSet = statement.executeQuery(); + assertNotNull(resultSet); + try { + statement.getMaxFieldSize(); + fail(); + } catch (Exception e) { + assertSame(SQLException.class, e.getClass()); + } + pool.logPoolState("testBackfill() before close..."); + } + assertSame(1, pool.getTotalConnections(), "Total connections not as expected"); + pool.logPoolState("testBackfill() after close..."); + PoolTestExtension.quietlySleep(1250); + assertSame(1, pool.getTotalConnections(), "Total connections not as expected"); + assertSame( 1, pool.getIdleConnections(), "Idle connections not as expected"); + } finally { + StubConnection.slowCreate = false; + } + } + + @Test + public void testMaximumPoolLimit() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(4); + config.setConnectionTimeout(20000); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + final AtomicReference ref = new AtomicReference<>(); + StubConnection.count.set(0); // reset counter + try (final PoolDataSource ds = new PoolDataSource(config)) { + final Pool pool = ds.getPool(); + Thread[] threads = new Thread[20]; + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(() -> { + try { + pool.logPoolState("Before acquire "); + try (Connection ignored = ds.getConnection()) { + pool.logPoolState("After acquire "); + PoolTestExtension.quietlySleep(500); + } + } catch (Exception e) { + ref.set(e); + } + }); + } + for (Thread thread : threads) { + thread.start(); + } + for (Thread thread : threads) { + thread.join(); + } + pool.logPoolState("before check "); + assertNull(ref.get(), (ref.get() != null ? ref.get().toString() : "")); + assertSame(4, StubConnection.count.get(), "StubConnection count not as expected"); + } + } + + @Test + public void testOldDriver() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + StubConnection.oldDriver = true; + StubStatement.oldDriver = true; + try (PoolDataSource ds = new PoolDataSource(config)) { + PoolTestExtension.quietlySleep(500); + try (Connection ignored = ds.getConnection()) { + // close + } + PoolTestExtension.quietlySleep(500); + try (Connection ignored = ds.getConnection()) { + // close + } + } finally { + StubConnection.oldDriver = false; + StubStatement.oldDriver = false; + } + } + + @Test + public void testInitializationFailure1() throws Exception { + StubDataSource stubDataSource = new StubDataSource(); + stubDataSource.setThrowException(new SQLException("Connection refused")); + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSource(stubDataSource); + try (PoolDataSource ds = new PoolDataSource(config)) { + try (Connection ignored = ds.getConnection()) { + fail("Initialization should have failed"); + } catch (SQLException e) { + // passed + } + } + } + + @Test + public void testInitializationFailure2() throws Exception { + StubDataSource stubDataSource = new StubDataSource(); + stubDataSource.setThrowException(new SQLException("Connection refused")); + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSource(stubDataSource); + try (PoolDataSource ds = new PoolDataSource(config); + Connection ignored = ds.getConnection()) { + fail("Initialization should have failed"); + } catch (SQLTransientConnectionException e) { + // passed + } + } + + @Test + public void testInvalidConnectionTestQuery() throws Exception { + class BadConnection extends StubConnection { + @Override + public Statement createStatement() throws SQLException { + throw new SQLException("Simulated exception in createStatement()"); + } + } + StubDataSource stubDataSource = new StubDataSource() { + @Override + public Connection getConnection() { + return new BadConnection(); + } + }; + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(2); + config.setConnectionTimeout(TimeUnit.SECONDS.toMillis(3)); + config.setConnectionTestQuery("VALUES 1"); + config.setInitializationFailTimeout(TimeUnit.SECONDS.toMillis(2)); + config.setDataSource(stubDataSource); + try (PoolDataSource ds = new PoolDataSource(config)) { + try (Connection ignored = ds.getConnection()) { + fail("getConnection() should have failed"); + } catch (SQLException e) { + assertSame("Simulated exception in createStatement()", e.getNextException().getMessage()); + } + } catch (PoolInitializationException e) { + assertSame("Simulated exception in createStatement()", e.getCause().getMessage()); + } + config.setInitializationFailTimeout(0); + try (PoolDataSource ignored = new PoolDataSource(config)) { + fail("Initialization should have failed"); + } catch (PoolInitializationException e) { + // passed + } + } + + @Test + public void testDataSourceRaisesErrorWhileInitializationTestQuery() throws Exception { + StubDataSourceWithErrorSwitch stubDataSource = new StubDataSourceWithErrorSwitch(); + stubDataSource.setErrorOnConnection(true); + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSource(stubDataSource); + try (PoolDataSource ds = new PoolDataSource(config); + Connection ignored = ds.getConnection()) { + fail("Initialization should have failed"); + } catch (SQLTransientConnectionException e) { + // passed + } + } + + @Disabled + @Test + public void testDataSourceRaisesErrorAfterInitializationTestQuery() throws Exception { + StubDataSourceWithErrorSwitch stubDataSource = new StubDataSourceWithErrorSwitch(); + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(2); + config.setConnectionTimeout(TimeUnit.SECONDS.toMillis(3)); + config.setConnectionTestQuery("VALUES 1"); + config.setInitializationFailTimeout(TimeUnit.SECONDS.toMillis(2)); + config.setDataSource(stubDataSource); + try (PoolDataSource ds = new PoolDataSource(config)) { + try (Connection ignored = ds.getConnection()) { + stubDataSource.setErrorOnConnection(true); + fail("SQLException should occur!"); + } catch (Exception e) { + // request will get timed-out + assertTrue(e.getMessage().contains("request timed out")); + } + } + } + + @Test + public void testPopulationSlowAcquisition() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMaximumPoolSize(20); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + config.setHousekeepingPeriodMs(1000L); + StubConnection.slowCreate = true; + try (PoolDataSource ds = new PoolDataSource(config)) { + ds.getPool().getConfig().setIdleTimeout(3000); + SECONDS.sleep(2); + Pool pool = ds.getPool(); + assertSame(1, pool.getTotalConnections(), "Total connections not as expected"); + assertSame(1, pool.getIdleConnections(), "Idle connections not as expected"); + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + SECONDS.sleep(20); + assertSame(20, pool.getTotalConnections(), "Second total connections not as expected"); + assertSame(19, pool.getIdleConnections(), "Second idle connections not as expected"); + } + assertSame(20, pool.getIdleConnections(), "Idle connections not as expected"); + SECONDS.sleep(5); + assertSame(20, pool.getTotalConnections(), "Third total connections not as expected"); + assertSame(20, pool.getIdleConnections(), "Third idle connections not as expected"); + } finally { + StubConnection.slowCreate = false; + } + } + + @Test + public void testMinimumIdleZero() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(5); + config.setConnectionTimeout(1000L); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config); + Connection ignored = ds.getConnection()) { + // passed + } catch (SQLTransientConnectionException sqle) { + fail("Failed to obtain connection"); + } + } + + private static class StubDataSourceWithErrorSwitch extends StubDataSource { + + private boolean errorOnConnection = false; + + @Override + public Connection getConnection() { + if (!errorOnConnection) { + return new StubConnection(); + } + throw new RuntimeException("bad thing happens on datasource"); + } + + public void setErrorOnConnection(boolean errorOnConnection) { + this.errorOnConnection = errorOnConnection; + } + } + + /*public static class OverrideHandler implements SQLExceptionOverride { + @java.lang.Override + public Override adjudicate(SQLException sqlException) { + return (sqlException.getSQLState().equals("08999")) ? Override.DO_NOT_EVICT : Override.CONTINUE_EVICT; + } + }*/ +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionTimeoutRetryTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionTimeoutRetryTest.java new file mode 100644 index 0000000..edd94b0 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ConnectionTimeoutRetryTest.java @@ -0,0 +1,178 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Test; +import org.xbib.io.pool.jdbc.mock.StubConnection; +import org.xbib.io.pool.jdbc.mock.StubDataSource; +import org.xbib.jdbc.connection.pool.util.ClockSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.xbib.jdbc.connection.pool.Pool; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +public class ConnectionTimeoutRetryTest { + + @Test + public void testConnectionRetries() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(2800); + config.setValidationTimeout(2800); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + StubDataSource stubDataSource = ds.unwrap(StubDataSource.class); + stubDataSource.setThrowException(new SQLException("Connection refused")); + long start = ClockSource.currentTime(); + try (Connection connection = ds.getConnection()) { + connection.close(); + fail("Should not have been able to get a connection."); + } catch (SQLException e) { + long elapsed = ClockSource.elapsedMillis(start); + long timeout = config.getConnectionTimeout(); + assertTrue(elapsed >= timeout, "Didn't wait long enough for timeout"); + } + } + } + + @Test + public void testConnectionRetries2() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(2800); + config.setValidationTimeout(2800); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + final StubDataSource stubDataSource = ds.unwrap(StubDataSource.class); + stubDataSource.setThrowException(new SQLException("Connection refused")); + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduler.schedule(() -> stubDataSource.setThrowException(null), 300, TimeUnit.MILLISECONDS); + long start = ClockSource.currentTime(); + try { + try (Connection connection = ds.getConnection()) { + // close immediately + } + long elapsed = ClockSource.elapsedMillis(start); + assertTrue(elapsed < config.getConnectionTimeout(), "waited too long to get a connection"); + } catch (SQLException e) { + fail("Should not have timed out: " + e.getMessage()); + } finally { + scheduler.shutdownNow(); + } + } + } + + @Test + public void testConnectionRetries3() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(2); + config.setConnectionTimeout(2800); + config.setValidationTimeout(2800); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + final Connection connection1 = ds.getConnection(); + final Connection connection2 = ds.getConnection(); + assertNotNull(connection1); + assertNotNull(connection2); + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); + scheduler.schedule(() -> { + try { + connection1.close(); + } catch (Exception e) { + e.printStackTrace(System.err); + } + }, 800, TimeUnit.MILLISECONDS); + + long start = ClockSource.currentTime(); + try { + try (Connection connection3 = ds.getConnection()) { + // close immediately + } + long elapsed = ClockSource.elapsedMillis(start); + assertTrue((elapsed >= 700) && (elapsed < 950), "waited too long to get a connection"); + } catch (SQLException e) { + fail("Should not have timed out."); + } finally { + scheduler.shutdownNow(); + } + } + } + + @Test + public void testConnectionRetries5() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(2); + config.setConnectionTimeout(1000); + config.setValidationTimeout(1000); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + final Connection connection1 = ds.getConnection(); + long start = ClockSource.currentTime(); + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); + scheduler.schedule(() -> { + try { + connection1.close(); + } catch (Exception e) { + e.printStackTrace(System.err); + } + }, 250, TimeUnit.MILLISECONDS); + StubDataSource stubDataSource = ds.unwrap(StubDataSource.class); + stubDataSource.setThrowException(new SQLException("Connection refused")); + try { + try (Connection connection2 = ds.getConnection()) { + // close immediately + } + long elapsed = ClockSource.elapsedMillis(start); + assertTrue((elapsed >= 250) && (elapsed < config.getConnectionTimeout()), "Waited too long to get a connection"); + } catch (SQLException e) { + fail("Should not have timed out."); + } finally { + scheduler.shutdownNow(); + } + } + } + + @Test + public void testConnectionIdleFill() throws Exception { + StubConnection.slowCreate = false; + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(10); + config.setConnectionTimeout(2000); + config.setValidationTimeout(2000); + config.setConnectionTestQuery("VALUES 2"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + config.setHousekeepingPeriodMs(400); + try (PoolDataSource ds = new PoolDataSource(config)) { + Pool pool = ds.getPool(); + try (Connection connection1 = ds.getConnection(); + Connection connection2 = ds.getConnection(); + Connection connection3 = ds.getConnection(); + Connection connection4 = ds.getConnection(); + Connection connection5 = ds.getConnection(); + Connection connection6 = ds.getConnection(); + Connection connection7 = ds.getConnection()) { + Thread.sleep(1300); + assertSame(10, pool.getTotalConnections(), "Total connections not as expected"); + assertSame(3, pool.getIdleConnections(), "Idle connections not as expected"); + } + assertSame(10, pool.getTotalConnections(), "Total connections not as expected"); + assertSame(10, pool.getIdleConnections(), "Idle connections not as expected"); + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/HouseKeeperCleanupTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/HouseKeeperCleanupTest.java new file mode 100644 index 0000000..902fbe4 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/HouseKeeperCleanupTest.java @@ -0,0 +1,57 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.jdbc.connection.pool.util.DefaultThreadFactory; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +public class HouseKeeperCleanupTest { + + private ScheduledThreadPoolExecutor executor; + + @BeforeEach + public void before() throws Exception { + ThreadFactory threadFactory = new DefaultThreadFactory("global-housekeeper", true); + executor = new ScheduledThreadPoolExecutor(1, threadFactory, + new ThreadPoolExecutor.DiscardPolicy()); + executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + executor.setRemoveOnCancelPolicy(true); + } + + @Test + public void testHouseKeeperCleanupWithCustomExecutor() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(10); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTimeout(2500); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + config.setScheduledExecutor(executor); + PoolConfig config2 = new PoolConfig(); + config2.setMinimumIdle(0); + config2.setMaximumPoolSize(10); + config2.setInitializationFailTimeout(Long.MAX_VALUE); + config2.setConnectionTimeout(2500); + config2.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + config2.setScheduledExecutor(executor); + try ( + final PoolDataSource ds1 = new PoolDataSource(config); + final PoolDataSource ds2 = new PoolDataSource(config2)) { + assertEquals(4, executor.getQueue().size(), "scheduled tasks count not as expected"); + } + assertEquals( 0, executor.getQueue().size(), "scheduled tasks count not as expected"); + } + + @AfterEach + public void after() throws Exception { + executor.shutdown(); + executor.awaitTermination(5, TimeUnit.SECONDS); + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/IsolationTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/IsolationTest.java new file mode 100644 index 0000000..6fb818a --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/IsolationTest.java @@ -0,0 +1,48 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import org.junit.jupiter.api.Test; +import java.sql.Connection; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +public class IsolationTest { + + @Test + public void testIsolation() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setIsolateInternalQueries(true); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + try (Connection connection = ds.getConnection()) { + connection.close(); + try (Connection connection2 = ds.getConnection()) { + connection2.close(); + assertNotSame(connection, connection2); + assertSame(connection.unwrap(Connection.class), connection2.unwrap(Connection.class)); + } + } + } + } + + @Test + public void testNonIsolation() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setIsolateInternalQueries(false); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + try (Connection connection = ds.getConnection()) { + connection.close(); + try (Connection connection2 = ds.getConnection()) { + connection2.close(); + assertSame(connection.unwrap(Connection.class), connection2.unwrap(Connection.class)); + } + } + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/JdbcDriverTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/JdbcDriverTest.java new file mode 100644 index 0000000..11e9a75 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/JdbcDriverTest.java @@ -0,0 +1,59 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.xbib.jdbc.connection.pool.util.DriverDataSource; +import java.sql.Connection; +import java.util.Properties; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +public class JdbcDriverTest { + + private PoolDataSource ds; + + @AfterEach + public void teardown() { + if (ds != null) { + ds.close(); + } + } + + @Test + public void driverTest1() throws Exception { + Properties properties = new Properties(); + properties.put("url", "jdbc:stub"); + properties.put("user", "bart"); + properties.put("password", "simpson"); + PoolConfig config = new PoolConfig(properties); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDriverClassName("org.xbib.io.pool.jdbc.mock.StubDriver"); + ds = new PoolDataSource(config); + assertTrue(ds.isWrapperFor(DriverDataSource.class)); + DriverDataSource unwrap = ds.unwrap(DriverDataSource.class); + assertNotNull(unwrap); + try (Connection connection = ds.getConnection()) { + // test that getConnection() succeeds + } + } + + @Test + public void driverTest2() throws Exception { + Properties properties = new Properties(); + properties.put("url", "jdbc:invalid"); + PoolConfig config = new PoolConfig(properties); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDriverClassName("org.xbib.io.pool.jdbc.mock.StubDriver"); + try { + ds = new PoolDataSource(config); + } catch (RuntimeException e) { + assertTrue(e.getMessage().contains("claims to not accept")); + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/PoolTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/PoolTest.java new file mode 100644 index 0000000..5985ec4 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/PoolTest.java @@ -0,0 +1,107 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import java.sql.Connection; +import java.sql.Statement; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.jdbc.connection.pool.Pool; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +@ExtendWith(PoolTestExtension.class) +public class PoolTest { + + private static final Logger logger = Logger.getLogger(PoolTest.class.getName()); + + @BeforeAll + static void setup() throws Exception { + Properties properties = new Properties(); + properties.put("url", "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"); + PoolConfig config = new PoolConfig(properties); + config.setMinimumIdle(1); + config.setMaximumPoolSize(2); + config.setConnectionTestQuery("SELECT 1"); + config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); + try (PoolDataSource ds = new PoolDataSource(config); + Connection conn = ds.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.executeUpdate("DROP TABLE IF EXISTS basic_pool_test"); + stmt.executeUpdate("CREATE TABLE basic_pool_test (" + + "id INTEGER NOT NULL IDENTITY PRIMARY KEY, " + + "timestamp TIMESTAMP, " + + "string VARCHAR(128), " + + "string_from_number NUMERIC " + + ")"); + } + } + + @Test + public void testIdleTimeout() throws Exception { + Properties properties = new Properties(); + properties.put("url", "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"); + PoolConfig config = new PoolConfig(properties); + config.setMinimumIdle(5); + config.setMaximumPoolSize(10); + config.setConnectionTestQuery("SELECT 1"); + config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); + System.setProperty("pool.jdbc.housekeeping.periodMs", "1000"); + try (PoolDataSource ds = new PoolDataSource(config)) { + System.clearProperty("pool.jdbc.housekeeping.periodMs"); + TimeUnit.SECONDS.sleep(1); + Pool pool = ds.getPool(); + config.setIdleTimeout(3000); + assertEquals(5, pool.getTotalConnections(), "Total connections not as expected"); + assertEquals(5, pool.getIdleConnections(), "Idle connections not as expected"); + try (Connection connection = ds.getConnection()) { + logger.log(Level.INFO, "got connection " + connection); + assertNotNull(connection); + TimeUnit.MILLISECONDS.sleep(1500); + //assertEquals(6, pool.getTotalConnections(), "Second total connections not as expected"); + //assertEquals(5, pool.getIdleConnections(), "Second idle connections not as expected"); + assertEquals(5, pool.getTotalConnections(), "Second total connections not as expected"); + assertEquals(4, pool.getIdleConnections(), "Second idle connections not as expected"); + } + //assertEquals(6, pool.getIdleConnections(), "Idle connections not as expected"); + assertEquals(5, pool.getIdleConnections(), "Idle connections not as expected"); + TimeUnit.SECONDS.sleep(2); + assertEquals(5, pool.getTotalConnections(), "Third total connections not as expected"); + assertEquals(5, pool.getIdleConnections(), "Third idle connections not as expected"); + } + } + + @Test + public void testIdleTimeout2() throws Exception { + Properties properties = new Properties(); + properties.put("url", "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"); + PoolConfig config = new PoolConfig(properties); + config.setMaximumPoolSize(50); + config.setConnectionTestQuery("SELECT 1"); + config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); + System.setProperty("pool.jdbc.housekeeping.periodMs", "1000"); + try (PoolDataSource ds = new PoolDataSource(config)) { + System.clearProperty("pool.jdbc.housekeeping.periodMs"); + TimeUnit.SECONDS.sleep(1); + Pool pool = ds.getPool(); + config.setIdleTimeout(3000); + assertEquals(50, pool.getTotalConnections(), "Total connections not as expected"); + assertEquals(50, pool.getIdleConnections(), "Idle connections not as expected"); + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + TimeUnit.MILLISECONDS.sleep(1500); + assertEquals(50, pool.getTotalConnections(), "Second total connections not as expected"); + assertEquals(49, pool.getIdleConnections(), "Second idle connections not as expected"); + } + assertEquals(50, pool.getIdleConnections(), "Idle connections not as expected"); + TimeUnit.SECONDS.sleep(3); + assertEquals(50, pool.getTotalConnections(), "Third total connections not as expected"); + assertEquals(50, pool.getIdleConnections(), "Third idle connections not as expected"); + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/PoolTestExtension.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/PoolTestExtension.java new file mode 100644 index 0000000..2fe50b1 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/PoolTestExtension.java @@ -0,0 +1,70 @@ +package org.xbib.io.pool.jdbc; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import java.util.Locale; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; +import org.xbib.jdbc.connection.pool.IsolationLevel; + +public class PoolTestExtension implements BeforeAllCallback { + + @Override + public void beforeAll(ExtensionContext context) { + //Level level = Level.INFO; + Level level = Level.ALL; + System.setProperty("java.util.logging.SimpleFormatter.format", + "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n"); + LogManager.getLogManager().reset(); + Logger rootLogger = LogManager.getLogManager().getLogger(""); + Handler handler = new ConsoleHandler(); + handler.setFormatter(new SimpleFormatter()); + rootLogger.addHandler(handler); + rootLogger.setLevel(level); + for (Handler h : rootLogger.getHandlers()) { + handler.setFormatter(new SimpleFormatter()); + h.setLevel(level); + } + } + + public static void quietlySleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** + * Get the int value of a transaction isolation level by name. + * + * @param transactionIsolationName the name of the transaction isolation level + * @return the int value of the isolation level or -1 + */ + public static int getTransactionIsolation(final String transactionIsolationName) { + if (transactionIsolationName != null) { + try { + final String upperCaseIsolationLevelName = transactionIsolationName.toUpperCase(Locale.ENGLISH); + return IsolationLevel.valueOf(upperCaseIsolationLevelName).getLevelId(); + } catch (IllegalArgumentException e) { + try { + final int level = Integer.parseInt(transactionIsolationName); + for (IsolationLevel iso : IsolationLevel.values()) { + if (iso.getLevelId() == level) { + return iso.getLevelId(); + } + } + throw new IllegalArgumentException("Invalid transaction isolation value: " + transactionIsolationName); + } + catch (NumberFormatException nfe) { + throw new IllegalArgumentException("Invalid transaction isolation value: " + transactionIsolationName, nfe); + } + } + } + return -1; + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ProxiesTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ProxiesTest.java new file mode 100644 index 0000000..f4c9e25 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ProxiesTest.java @@ -0,0 +1,247 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.io.pool.jdbc.mock.StubConnection; +import org.xbib.io.pool.jdbc.mock.StubStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.concurrent.TimeUnit; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +@ExtendWith(PoolTestExtension.class) +public class ProxiesTest { + + @Test + public void testProxyCreation() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + Connection conn = ds.getConnection(); + assertNotNull(conn.createStatement(ResultSet.FETCH_FORWARD, ResultSet.TYPE_SCROLL_INSENSITIVE)); + assertNotNull(conn.createStatement(ResultSet.FETCH_FORWARD, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.HOLD_CURSORS_OVER_COMMIT)); + assertNotNull(conn.prepareCall("some sql")); + assertNotNull(conn.prepareCall("some sql", ResultSet.FETCH_FORWARD, ResultSet.TYPE_SCROLL_INSENSITIVE)); + assertNotNull(conn.prepareCall("some sql", ResultSet.FETCH_FORWARD, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.HOLD_CURSORS_OVER_COMMIT)); + assertNotNull(conn.prepareStatement("some sql", PreparedStatement.NO_GENERATED_KEYS)); + assertNotNull(conn.prepareStatement("some sql", new int[3])); + assertNotNull(conn.prepareStatement("some sql", new String[3])); + assertNotNull(conn.prepareStatement("some sql", ResultSet.FETCH_FORWARD, ResultSet.TYPE_SCROLL_INSENSITIVE)); + assertNotNull(conn.prepareStatement("some sql", ResultSet.FETCH_FORWARD, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.HOLD_CURSORS_OVER_COMMIT)); + assertNotNull(conn.toString()); + assertTrue(conn.isWrapperFor(Connection.class)); + assertTrue(conn.isValid(10)); + assertFalse(conn.isClosed()); + assertNotNull(conn.unwrap(StubConnection.class)); + try { + conn.unwrap(ProxiesTest.class); + fail(); + } catch (SQLException e) { + // pass + } + } + } + + @Test + public void testStatementProxy() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + Connection conn = ds.getConnection(); + PreparedStatement stmt = conn.prepareStatement("some sql"); + stmt.executeQuery(); + stmt.executeQuery("some sql"); + assertFalse(stmt.isClosed()); + assertNotNull(stmt.getGeneratedKeys()); + assertNotNull(stmt.getResultSet()); + assertNotNull(stmt.getConnection()); + assertNotNull(stmt.unwrap(StubStatement.class)); + try { + stmt.unwrap(ProxiesTest.class); + fail(); + } catch (SQLException e) { + // pass + } + } + } + + @Test + public void testStatementExceptions() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(TimeUnit.SECONDS.toMillis(1)); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + Connection conn = ds.getConnection(); + StubConnection stubConnection = conn.unwrap(StubConnection.class); + stubConnection.throwException = true; + try { + conn.createStatement(); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.createStatement(0, 0); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.createStatement(0, 0, 0); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.prepareCall(""); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.prepareCall("", 0, 0); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.prepareCall("", 0, 0, 0); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.prepareStatement(""); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.prepareStatement("", 0); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.prepareStatement("", new int[0]); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.prepareStatement("", new String[0]); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.prepareStatement("", 0, 0); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.prepareStatement("", 0, 0, 0); + fail(); + } catch (SQLException e) { + // pass + } + } + } + + @Test + public void testOtherExceptions() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + try (Connection conn = ds.getConnection()) { + StubConnection stubConnection = conn.unwrap(StubConnection.class); + stubConnection.throwException = true; + try { + conn.setTransactionIsolation(Connection.TRANSACTION_NONE); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.isReadOnly(); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.setReadOnly(false); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.setCatalog(""); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.setAutoCommit(false); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.clearWarnings(); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.isValid(0); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.isWrapperFor(getClass()); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.unwrap(getClass()); + fail(); + } catch (SQLException e) { + // pass + } + try { + conn.close(); + fail(); + } catch (SQLException e) { + // pass + } + try { + assertFalse(conn.isValid(0)); + } catch (SQLException e) { + fail(); + } + } + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/RampUpDownTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/RampUpDownTest.java new file mode 100644 index 0000000..c78e6d4 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/RampUpDownTest.java @@ -0,0 +1,40 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertSame; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import java.sql.Connection; +import org.xbib.jdbc.connection.pool.Pool; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +@ExtendWith(PoolTestExtension.class) +public class RampUpDownTest { + + @Test + public void rampUpDownTest() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(60); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + config.setIdleTimeout(1000); + config.setHousekeepingPeriodMs(250L); + try (PoolDataSource ds = new PoolDataSource(config)) { + Pool pool = ds.getPool(); + assertSame(1, pool.getTotalConnections(), "total connections not as expected"); + PoolTestExtension.quietlySleep(500); + Connection[] connections = new Connection[ds.getPool().getConfig().getMaximumPoolSize()]; + for (int i = 0; i < connections.length; i++) { + connections[i] = ds.getConnection(); + } + assertSame(60, pool.getTotalConnections(), "total connections not as expected"); + for (Connection connection : connections) { + connection.close(); + } + PoolTestExtension.quietlySleep(500); + assertSame(60, pool.getIdleConnections(), "idle connections not as expected"); + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/SaturatedPoolTest830.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/SaturatedPoolTest830.java new file mode 100644 index 0000000..330370f --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/SaturatedPoolTest830.java @@ -0,0 +1,111 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.io.pool.jdbc.mock.StubConnection; +import org.xbib.io.pool.jdbc.mock.StubStatement; +import org.xbib.jdbc.connection.pool.util.ClockSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +@ExtendWith(PoolTestExtension.class) +public class SaturatedPoolTest830 { + + private static final Logger logger = Logger.getLogger(SaturatedPoolTest830.class.getName()); + + private static final int MAX_POOL_SIZE = 10; + + @Test + public void saturatedPoolTest() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(MAX_POOL_SIZE); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTimeout(1000); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + config.setHousekeepingPeriodMs(5000L); + StubConnection.slowCreate = true; + StubStatement.setSimulatedQueryTime(1000); + final long start = ClockSource.currentTime(); + try (PoolDataSource ds = new PoolDataSource(config)) { + LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + ThreadPoolExecutor threadPool = new ThreadPoolExecutor( 50, 50, + 2, TimeUnit.SECONDS, queue, new ThreadPoolExecutor.CallerRunsPolicy()); + threadPool.allowCoreThreadTimeOut(true); + AtomicInteger windowIndex = new AtomicInteger(); + boolean[] failureWindow = new boolean[100]; + Arrays.fill(failureWindow, true); + for (int i = 0; i < 50; i++) { + threadPool.execute(() -> { + try (Connection conn = ds.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute("SELECT bogus FROM imaginary"); + } + catch (SQLException e) { + logger.info(e.getMessage()); + } + }); + } + long sleep = 80; + outer: while (true) { + PoolTestExtension.quietlySleep(sleep); + if (ClockSource.elapsedMillis(start) > TimeUnit.SECONDS.toMillis(12) && sleep < 100) { + sleep = 100; + logger.warning("switching to 100ms sleep"); + } + else if (ClockSource.elapsedMillis(start) > TimeUnit.SECONDS.toMillis(6) && sleep < 90) { + sleep = 90; + logger.warning("switching to 90ms sleep"); + } + threadPool.execute(() -> { + int ndx = windowIndex.incrementAndGet() % failureWindow.length; + try (Connection conn = ds.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute("SELECT bogus FROM imaginary"); + failureWindow[ndx] = false; + } + catch (SQLException e) { + logger.info(e.getMessage()); + failureWindow[ndx] = true; + } + }); + for (boolean b : failureWindow) { + if (b) { + if (ClockSource.elapsedMillis(start) % (TimeUnit.SECONDS.toMillis(1) - sleep) < sleep) { + logger.info(MessageFormat.format("active threads {0}, submissions per second {1}, waiting threads {2}", + threadPool.getActiveCount(), + TimeUnit.SECONDS.toMillis(1) / sleep, + ds.getPool().getThreadsAwaitingConnection())); + } + continue outer; + } + } + logger.info(MessageFormat.format("active threads {0}, submissions per second {1}, waiting threads {2}", + threadPool.getActiveCount(), + TimeUnit.SECONDS.toMillis(1) / sleep, + ds.getPool().getThreadsAwaitingConnection())); + break; + } + logger.info("waiting for completion of active tasks: " + threadPool.getActiveCount()); + while (ds.getPool().getActiveConnections() > 0) { + PoolTestExtension.quietlySleep(50); + } + assertEquals(TimeUnit.SECONDS.toMillis(1) / sleep, 10L, "Rate not in balance at 10req/s"); + } + finally { + StubStatement.setSimulatedQueryTime(0); + StubConnection.slowCreate = false; + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ShutdownTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ShutdownTest.java new file mode 100644 index 0000000..1f6aa60 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/ShutdownTest.java @@ -0,0 +1,262 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.io.pool.jdbc.mock.StubConnection; +import org.xbib.jdbc.connection.pool.util.ClockSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.concurrent.TimeUnit; +import org.xbib.jdbc.connection.pool.Pool; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +@ExtendWith(PoolTestExtension.class) +public class ShutdownTest { + + @BeforeEach + public void beforeTest() { + StubConnection.count.set(0); + } + + @AfterEach + public void afterTest() { + StubConnection.slowCreate = false; + } + + @Test + public void testShutdown1() throws Exception { + assertSame(0, StubConnection.count.get(), "StubConnection count not as expected"); + StubConnection.slowCreate = true; + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(10); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + Pool pool = ds.getPool(); + Thread[] threads = new Thread[10]; + for (int i = 0; i < 10; i++) { + threads[i] = new Thread(() -> { + try { + if (ds.getConnection() != null) { + PoolTestExtension.quietlySleep(TimeUnit.SECONDS.toMillis(1)); + } + } + catch (SQLException e) { + // + } + }); + threads[i].setDaemon(true); + } + for (int i = 0; i < 10; i++) { + threads[i].start(); + } + PoolTestExtension.quietlySleep(1800L); + assertTrue(pool.getTotalConnections() > 0, "total connection count not as expected"); + ds.close(); + assertSame(0, pool.getActiveConnections(), "active connection count not as expected"); + assertSame( 0, pool.getIdleConnections(), "idle connection count not as expected"); + assertSame(0, pool.getTotalConnections(), "total connection count not as expected"); + assertTrue(ds.isClosed()); + } + } + + @Test + public void testShutdown2() throws Exception { + assertSame( 0, StubConnection.count.get(), "StubConnection count not as expected"); + StubConnection.slowCreate = true; + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(10); + config.setMaximumPoolSize(10); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + Pool pool = ds.getPool(); + PoolTestExtension.quietlySleep(1200L); + assertTrue( pool.getTotalConnections() > 0, "total connection count not as expected"); + ds.close(); + assertSame(0, pool.getActiveConnections(), "active connection count not as expected"); + assertSame(0, pool.getIdleConnections(), "idle connection count not as expected"); + assertSame(0, pool.getTotalConnections(), "Total connection count not as expected"); + assertTrue(ds.toString().startsWith("PoolDataSource (") && ds.toString().endsWith(")")); + } + } + + @Test + public void testShutdown3() throws Exception { + assertSame(0, StubConnection.count.get(), "StubConnection count not as expected"); + StubConnection.slowCreate = false; + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(5); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + Pool pool = ds.getPool(); + PoolTestExtension.quietlySleep(1200L); + assertEquals(5, pool.getTotalConnections(), "total connection count not as expected"); + ds.close(); + assertSame(0, pool.getActiveConnections(), "active connection count not as expected"); + assertSame( 0, pool.getIdleConnections(), "idle connection count not as expected"); + assertSame(0, pool.getTotalConnections(), "total connection count not as expected"); + } + } + + @Test + public void testShutdown4() throws Exception { + StubConnection.slowCreate = true; + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(10); + config.setMaximumPoolSize(10); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + PoolTestExtension.quietlySleep(500L); + ds.close(); + long startTime = ClockSource.currentTime(); + while (ClockSource.elapsedMillis(startTime) < TimeUnit.SECONDS.toMillis(5) && threadCount() > 0) { + PoolTestExtension.quietlySleep(250); + } + assertSame(0, ds.getPool().getTotalConnections(), "unreleased connections after shutdown"); + } + } + + @Test + public void testShutdown5() throws Exception { + assertSame( 0, StubConnection.count.get(), "StubConnection count not as expected"); + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(5); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + Pool pool = ds.getPool(); + for (int i = 0; i < 5; i++) { + ds.getConnection(); + } + assertEquals(5, pool.getTotalConnections(), "total connection count not as expected, "); + ds.close(); + assertSame(0, pool.getActiveConnections(), "active connection count not as expected"); + assertSame(0, pool.getIdleConnections(), "idle connection count not as expected"); + assertSame(0, pool.getTotalConnections(), "total connection count not as expected"); + } + } + + @Test + public void testAfterShutdown() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(5); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + ds.close(); + try { + ds.getConnection(); + } + catch (SQLException e) { + assertTrue(e.getMessage().contains("has been closed")); + } + } + } + + @Test + public void testShutdownDuringInit() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(5); + config.setConnectionTimeout(1000); + config.setValidationTimeout(1000); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + StubConnection.slowCreate = true; + PoolTestExtension.quietlySleep(3000L); + } + } + + @Test + public void testThreadedShutdown() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(5); + config.setConnectionTimeout(1000); + config.setValidationTimeout(1000); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + for (int i = 0; i < 4; i++) { + try (PoolDataSource ds = new PoolDataSource(config)) { + Thread t = new Thread(() -> { + try (Connection connection = ds.getConnection()) { + for (int i1 = 0; i1 < 10; i1++) { + Connection connection2 = null; + try { + connection2 = ds.getConnection(); + PreparedStatement stmt = connection2.prepareStatement("SOMETHING"); + PoolTestExtension.quietlySleep(20); + stmt.getMaxFieldSize(); + } + catch (SQLException e) { + try { + if (connection2 != null) { + connection2.close(); + } + } + catch (SQLException e2) { + if (e2.getMessage().contains("shutdown") || e2.getMessage().contains("evicted")) { + break; + } + } + } + } + } + catch (Exception e) { + fail(e.getMessage()); + } + finally { + ds.close(); + } + }); + t.start(); + Thread t2 = new Thread(() -> { + PoolTestExtension.quietlySleep(100); + try { + ds.close(); + } + catch (IllegalStateException e) { + fail(e.getMessage()); + } + }); + t2.start(); + t.join(); + t2.join(); + } + } + } + + private int threadCount() { + Thread[] threads = new Thread[Thread.activeCount() * 2]; + Thread.enumerate(threads); + int count = 0; + for (Thread thread : threads) { + count += (thread != null && thread.getName().startsWith("Pool")) ? 1 : 0; + } + return count; + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/StatementTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/StatementTest.java new file mode 100644 index 0000000..62d2e9e --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/StatementTest.java @@ -0,0 +1,98 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import org.xbib.jdbc.connection.pool.Pool; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +public class StatementTest { + + private PoolDataSource ds; + + @BeforeEach + public void setup() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(2); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + ds = new PoolDataSource(config); + } + + @AfterEach + public void teardown() { + ds.close(); + } + + @Test + public void testStatementClose() throws SQLException { + ds.getConnection().close(); + Pool pool = ds.getPool(); + assertTrue(pool.getTotalConnections() >= 1, "total connections not as expected"); + assertTrue(pool.getIdleConnections() >= 1, "idle connections not as expected"); + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + assertTrue(pool.getTotalConnections() >= 1, "total connections not as expected"); + assertTrue(pool.getIdleConnections() >= 0, "idle connections not as expected"); + Statement statement = connection.createStatement(); + assertNotNull(statement); + connection.close(); + assertTrue(statement.isClosed()); + } + } + + @Test + public void testAutoStatementClose() throws SQLException { + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + Statement statement1 = connection.createStatement(); + assertNotNull(statement1); + Statement statement2 = connection.createStatement(); + assertNotNull(statement2); + connection.close(); + assertTrue(statement1.isClosed()); + assertTrue(statement2.isClosed()); + } + } + + @Test + public void testStatementResultSetProxyClose() throws SQLException { + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + Statement statement1 = connection.createStatement(); + assertNotNull(statement1); + Statement statement2 = connection.createStatement(); + assertNotNull(statement2); + statement1.getResultSet().getStatement().close(); + statement2.getGeneratedKeys().getStatement().close(); + assertTrue(statement1.isClosed()); + assertTrue(statement2.isClosed()); + } + } + + @Test + public void testDoubleStatementClose() throws SQLException { + try (Connection connection = ds.getConnection(); + Statement statement1 = connection.createStatement()) { + statement1.close(); + statement1.close(); + } + } + + @Test + public void testOutOfOrderStatementClose() throws SQLException { + try (Connection connection = ds.getConnection(); + Statement statement1 = connection.createStatement(); + Statement statement2 = connection.createStatement()) { + statement1.close(); + statement2.close(); + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/UnwrapTest.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/UnwrapTest.java new file mode 100644 index 0000000..61ef008 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/UnwrapTest.java @@ -0,0 +1,59 @@ +package org.xbib.io.pool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import org.xbib.io.pool.jdbc.mock.StubConnection; +import org.xbib.io.pool.jdbc.mock.StubDataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; + +public class UnwrapTest { + + @Test + public void testUnwrapConnection() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + ds.getConnection().close(); + assertSame(1, ds.getPool().getIdleConnections(), "Idle connections not as expected"); + Connection connection = ds.getConnection(); + assertNotNull(connection); + StubConnection unwrapped = connection.unwrap(StubConnection.class); + assertNotNull(unwrapped, "unwrapped connection is not instance of StubConnection: " + unwrapped); + } + } + + @Test + public void testUnwrapDataSource() throws Exception { + PoolConfig config = new PoolConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource"); + try (PoolDataSource ds = new PoolDataSource(config)) { + StubDataSource unwrap = ds.unwrap(StubDataSource.class); + assertNotNull(unwrap); + assertTrue(ds.isWrapperFor(PoolDataSource.class)); + assertNotNull(ds.unwrap(PoolDataSource.class)); + assertFalse(ds.isWrapperFor(getClass())); + try { + ds.unwrap(getClass()); + } catch (SQLException e) { + Logger.getAnonymousLogger().log(Level.INFO, e.getMessage()); + assertTrue(e.getMessage().contains("wrapped DataSource")); + } + } + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/MockDataSource.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/MockDataSource.java new file mode 100644 index 0000000..a5288f0 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/MockDataSource.java @@ -0,0 +1,96 @@ +package org.xbib.io.pool.jdbc.mock; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import java.io.PrintWriter; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Statement; +import java.util.logging.Logger; +import javax.sql.DataSource; + +public class MockDataSource implements DataSource { + @Override + public Connection getConnection() throws SQLException { + return createMockConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return getConnection(); + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return null; + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + } + + @Override + public int getLoginTimeout() throws SQLException { + return 0; + } + + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return null; + } + + @Override + public T unwrap(Class iface) throws SQLException { + return null; + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } + + public static Connection createMockConnection() throws SQLException { + final Connection mockConnection = mock(Connection.class); + when(mockConnection.getAutoCommit()).thenReturn(true); + Statement statement = mock(Statement.class); + when(mockConnection.createStatement()).thenReturn(statement); + when(mockConnection.createStatement(anyInt(), anyInt())).thenReturn(statement); + when(mockConnection.createStatement(anyInt(), anyInt(), anyInt())).thenReturn(statement); + when(mockConnection.isValid(anyInt())).thenReturn(true); + PreparedStatement mockPreparedStatement = mock(PreparedStatement.class); + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + when(mockConnection.prepareStatement(anyString(), anyInt())).thenReturn(mockPreparedStatement); + when(mockConnection.prepareStatement(anyString(), any(int[].class))).thenReturn(mockPreparedStatement); + when(mockConnection.prepareStatement(anyString(), any(String[].class))).thenReturn(mockPreparedStatement); + when(mockConnection.prepareStatement(anyString(), anyInt(), anyInt())).thenReturn(mockPreparedStatement); + when(mockConnection.prepareStatement(anyString(), anyInt(), anyInt(), anyInt())).thenReturn(mockPreparedStatement); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + return null; + } + }).doNothing().when(mockPreparedStatement).setInt(anyInt(), anyInt()); + ResultSet mockResultSet = mock(ResultSet.class); + when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet); + when(mockResultSet.getString(anyInt())).thenReturn("aString"); + when(mockResultSet.next()).thenReturn(true); + CallableStatement mockCallableStatement = mock(CallableStatement.class); + when(mockConnection.prepareCall(anyString())).thenReturn(mockCallableStatement); + when(mockConnection.prepareCall(anyString(), anyInt(), anyInt())).thenReturn(mockCallableStatement); + when(mockConnection.prepareCall(anyString(), anyInt(), anyInt(), anyInt())).thenReturn(mockCallableStatement); + return mockConnection; + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubBaseConnection.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubBaseConnection.java new file mode 100644 index 0000000..42c91c7 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubBaseConnection.java @@ -0,0 +1,33 @@ +package org.xbib.io.pool.jdbc.mock; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +public abstract class StubBaseConnection implements Connection { + + public volatile boolean throwException; + + /** + * {@inheritDoc} + */ + @Override + public Statement createStatement() throws SQLException { + if (throwException) { + throw new SQLException(); + } + return new StubStatement(this); + } + + /** + * {@inheritDoc} + */ + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return new StubPreparedStatement(this); + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubConnection.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubConnection.java new file mode 100644 index 0000000..29a775f --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubConnection.java @@ -0,0 +1,522 @@ +package org.xbib.io.pool.jdbc.mock; + +import org.xbib.io.pool.jdbc.PoolTestExtension; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +public class StubConnection extends StubBaseConnection implements Connection { + + public static final AtomicInteger count = new AtomicInteger(); + + public static volatile boolean slowCreate; + + public static volatile boolean oldDriver; + + private static final long foo; + + private boolean autoCommit; + + private int isolation = Connection.TRANSACTION_READ_COMMITTED; + + private String catalog; + + static { + foo = System.currentTimeMillis(); + } + + public StubConnection() { + count.incrementAndGet(); + if (slowCreate) { + PoolTestExtension.quietlySleep(1000); + } + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + @Override + public T unwrap(Class iface) throws SQLException { + if (throwException) { + throw new SQLException(); + } + + if (iface.isInstance(this)) { + return (T) this; + } + + throw new SQLException("Wrapped connection is not an instance of " + iface); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public String nativeSQL(String sql) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + if (throwException) { + throw new SQLException(); + } + this.autoCommit = autoCommit; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getAutoCommit() throws SQLException { + return autoCommit; + } + + /** + * {@inheritDoc} + */ + @Override + public void commit() throws SQLException { + + } + + /** + * {@inheritDoc} + */ + @Override + public void rollback() throws SQLException { + + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws SQLException { + + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isClosed() throws SQLException { + if (throwException) { + throw new SQLException(); + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public DatabaseMetaData getMetaData() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + if (throwException) { + throw new SQLException(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isReadOnly() throws SQLException { + if (throwException) { + throw new SQLException(); + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void setCatalog(String catalog) throws SQLException { + if (throwException) { + throw new SQLException(); + } + this.catalog = catalog; + } + + /** + * {@inheritDoc} + */ + @Override + public String getCatalog() throws SQLException { + return catalog; + } + + /** + * {@inheritDoc} + */ + @Override + public void setTransactionIsolation(int level) throws SQLException { + if (throwException) { + throw new SQLException(); + } + this.isolation = level; + } + + /** + * {@inheritDoc} + */ + @Override + public int getTransactionIsolation() throws SQLException { + return isolation; + } + + /** + * {@inheritDoc} + */ + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void clearWarnings() throws SQLException { + if (throwException) { + throw new SQLException(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return new StubPreparedStatement(this); + } + + /** + * {@inheritDoc} + */ + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Map> getTypeMap() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void setTypeMap(Map> map) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setHoldability(int holdability) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public int getHoldability() throws SQLException { + return (int) foo; + } + + /** + * {@inheritDoc} + */ + @Override + public Savepoint setSavepoint() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Savepoint setSavepoint(String name) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void rollback(Savepoint savepoint) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return new StubPreparedStatement(this); + } + + /** + * {@inheritDoc} + */ + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return new StubPreparedStatement(this); + } + + /** + * {@inheritDoc} + */ + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return new StubPreparedStatement(this); + } + + /** + * {@inheritDoc} + */ + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return new StubPreparedStatement(this); + } + + /** + * {@inheritDoc} + */ + @Override + public Clob createClob() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Blob createBlob() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public NClob createNClob() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public SQLXML createSQLXML() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isValid(int timeout) throws SQLException { + if (throwException) { + throw new SQLException(); + } + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException { + } + + /** + * {@inheritDoc} + */ + @Override + public String getClientInfo(String name) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Properties getClientInfo() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + public void setSchema(String schema) throws SQLException { + } + + /** + * {@inheritDoc} + */ + public String getSchema() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + public void abort(Executor executor) throws SQLException { + throw new SQLException("Intentional exception during abort"); + } + + /** + * {@inheritDoc} + */ + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + } + + /** + * {@inheritDoc} + */ + public int getNetworkTimeout() throws SQLException { + if (oldDriver) { + throw new AbstractMethodError(); + } + + return 0; + } + +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubDataSource.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubDataSource.java new file mode 100644 index 0000000..3375766 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubDataSource.java @@ -0,0 +1,127 @@ +package org.xbib.io.pool.jdbc.mock; + +import org.xbib.io.pool.jdbc.PoolTestExtension; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.logging.Logger; +import javax.sql.DataSource; + +public class StubDataSource implements DataSource { + private String user; + private String password; + private PrintWriter logWriter; + private SQLException throwException; + private long connectionAcquistionTime = 0; + private int loginTimeout; + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setURL(String url) { + // we don't care + } + + /** + * {@inheritDoc} + */ + @Override + public PrintWriter getLogWriter() throws SQLException { + return logWriter; + } + + /** + * {@inheritDoc} + */ + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + this.logWriter = out; + } + + /** + * {@inheritDoc} + */ + @Override + public void setLoginTimeout(int seconds) throws SQLException { + this.loginTimeout = seconds; + } + + /** + * {@inheritDoc} + */ + @Override + public int getLoginTimeout() throws SQLException { + return loginTimeout; + } + + /** + * {@inheritDoc} + */ + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return null; + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + @Override + public T unwrap(Class iface) throws SQLException { + if (iface.isInstance(this)) { + return (T) this; + } + throw new SQLException("wrapped DataSource is not an instance of " + iface); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public Connection getConnection() throws SQLException { + if (throwException != null) { + throw throwException; + } + if (connectionAcquistionTime > 0) { + PoolTestExtension.quietlySleep(connectionAcquistionTime); + } + return new StubConnection(); + } + + /** + * {@inheritDoc} + */ + @Override + public Connection getConnection(String username, String password) throws SQLException { + return new StubConnection(); + } + + public void setThrowException(SQLException e) { + this.throwException = e; + } + + public void setConnectionAcquistionTime(long connectionAcquisitionTime) { + this.connectionAcquistionTime = connectionAcquisitionTime; + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubDriver.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubDriver.java new file mode 100644 index 0000000..fbadba5 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubDriver.java @@ -0,0 +1,78 @@ +package org.xbib.io.pool.jdbc.mock; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Properties; +import java.util.logging.Logger; + +public class StubDriver implements Driver { + private static final Driver driver; + + static { + driver = new StubDriver(); + try { + DriverManager.registerDriver(driver); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Connection connect(String url, Properties info) throws SQLException { + return new StubConnection(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean acceptsURL(String url) throws SQLException { + return "jdbc:stub".equals(url); + } + + /** + * {@inheritDoc} + */ + @Override + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public int getMajorVersion() { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int getMinorVersion() { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean jdbcCompliant() { + return true; + } + + /** + * {@inheritDoc} + */ + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return null; + } +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubPreparedStatement.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubPreparedStatement.java new file mode 100644 index 0000000..d2e00d9 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubPreparedStatement.java @@ -0,0 +1,746 @@ +package org.xbib.io.pool.jdbc.mock; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; + +@SuppressWarnings("RedundantThrows") +public class StubPreparedStatement extends StubStatement implements PreparedStatement { + StubPreparedStatement(Connection connection) { + super(connection); + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSet executeQuery(String sql) throws SQLException { + return new StubResultSet(); + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate(String sql) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int getMaxFieldSize() throws SQLException { + throw new SQLException("Simulated disconnection error", "08999"); + } + + /** + * {@inheritDoc} + */ + @Override + public void setMaxFieldSize(int max) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public int getMaxRows() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void setMaxRows(int max) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setEscapeProcessing(boolean enable) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public int getQueryTimeout() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void setQueryTimeout(int seconds) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void cancel() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void clearWarnings() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setCursorName(String name) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute(String sql) throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSet getResultSet() throws SQLException { + return new StubResultSet(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getUpdateCount() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getMoreResults() throws SQLException { + if (isClosed()) { + throw new SQLException("Connection is closed"); + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void setFetchDirection(int direction) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public int getFetchDirection() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void setFetchSize(int rows) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public int getFetchSize() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int getResultSetConcurrency() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int getResultSetType() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void addBatch(String sql) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void clearBatch() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public int[] executeBatch() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getMoreResults(int current) throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSet getGeneratedKeys() throws SQLException { + return new StubResultSet(); + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public int getResultSetHoldability() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void setPoolable(boolean poolable) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isPoolable() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void closeOnCompletion() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isCloseOnCompletion() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + @Override + public T unwrap(Class iface) throws SQLException { + if (iface.isInstance(this)) { + return (T) this; + } + + throw new SQLException("Wrapped connection is not an instance of " + iface); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSet executeQuery() throws SQLException { + return new StubResultSet(); + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setString(int parameterIndex, String x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setDate(int parameterIndex, Date x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setTime(int parameterIndex, Time x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("deprecation") + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void clearParameters() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setObject(int parameterIndex, Object x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void addBatch() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setArray(int parameterIndex, Array x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSetMetaData getMetaData() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setURL(int parameterIndex, URL x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public ParameterMetaData getParameterMetaData() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setNString(int parameterIndex, String value) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setNClob(int parameterIndex, NClob value) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setClob(int parameterIndex, Reader reader) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void setNClob(int parameterIndex, Reader reader) throws SQLException { + } + +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubResultSet.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubResultSet.java new file mode 100644 index 0000000..fabca10 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubResultSet.java @@ -0,0 +1,1467 @@ +package org.xbib.io.pool.jdbc.mock; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +@SuppressWarnings("RedundantThrows") +public class StubResultSet implements ResultSet { + private int counter; + private boolean closed; + + /** + * {@inheritDoc} + */ + @Override + public T unwrap(Class iface) throws SQLException { + + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean next() throws SQLException { + return (counter > 100000); + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws SQLException { + closed = true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean wasNull() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public String getString(int columnIndex) throws SQLException { + return "aString"; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public byte getByte(int columnIndex) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public short getShort(int columnIndex) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int getInt(int columnIndex) throws SQLException { + return ++counter; + } + + /** + * {@inheritDoc} + */ + @Override + public long getLong(int columnIndex) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public float getFloat(int columnIndex) throws SQLException { + throw new SQLException("Simulated disconnection error", "08999"); + } + + /** + * {@inheritDoc} + */ + @Override + public double getDouble(int columnIndex) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("deprecation") + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] getBytes(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Date getDate(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Time getTime(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("deprecation") + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public String getString(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public byte getByte(String columnLabel) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public short getShort(String columnLabel) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int getInt(String columnLabel) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public long getLong(String columnLabel) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public float getFloat(String columnLabel) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public double getDouble(String columnLabel) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("deprecation") + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Date getDate(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Time getTime(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("deprecation") + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void clearWarnings() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public String getCursorName() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSetMetaData getMetaData() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getObject(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getObject(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public int findColumn(String columnLabel) throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBeforeFirst() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isAfterLast() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isFirst() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isLast() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void beforeFirst() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void afterLast() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public boolean first() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean last() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public int getRow() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean absolute(int row) throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean relative(int rows) throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean previous() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void setFetchDirection(int direction) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public int getFetchDirection() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void setFetchSize(int rows) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public int getFetchSize() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int getType() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int getConcurrency() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean rowUpdated() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean rowInserted() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean rowDeleted() throws SQLException { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNull(int columnIndex) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateString(int columnIndex, String x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNull(String columnLabel) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateString(String columnLabel, String x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void insertRow() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateRow() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void deleteRow() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void refreshRow() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void cancelRowUpdates() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void moveToInsertRow() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void moveToCurrentRow() throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public Statement getStatement() throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Ref getRef(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Blob getBlob(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Clob getClob(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Array getArray(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getObject(String columnLabel, Map> map) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Ref getRef(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Blob getBlob(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Clob getClob(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Array getArray(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public URL getURL(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public URL getURL(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public RowId getRowId(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public RowId getRowId(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public int getHoldability() throws SQLException { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isClosed() throws SQLException { + return closed; + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNString(int columnIndex, String nString) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNString(String columnLabel, String nString) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNClob(int columnIndex, NClob nClob) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNClob(String columnLabel, NClob nClob) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public NClob getNClob(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public NClob getNClob(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public String getNString(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public String getNString(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + } + + /** + * {@inheritDoc} + */ + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException { + } + + /** + * {@inheritDoc} + */ + public T getObject(int columnIndex, Class type) throws SQLException { + return null; + } + + /** + * {@inheritDoc} + */ + public T getObject(String columnLabel, Class type) throws SQLException { + return null; + } + +} diff --git a/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubStatement.java b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubStatement.java new file mode 100644 index 0000000..5c4df31 --- /dev/null +++ b/jdbc-connection-pool/src/test/java/org/xbib/io/pool/jdbc/mock/StubStatement.java @@ -0,0 +1,418 @@ +package org.xbib.io.pool.jdbc.mock; + +import org.xbib.io.pool.jdbc.PoolTestExtension; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLWarning; +import java.sql.Statement; + +public class StubStatement implements Statement { + public static volatile boolean oldDriver; + + private static volatile long simulatedQueryTime; + private boolean closed; + private final Connection connection; + + public StubStatement(Connection connection) { + this.connection = connection; + } + + public static void setSimulatedQueryTime(long time) { + simulatedQueryTime = time; + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + @Override + public T unwrap(Class iface) throws SQLException { + checkClosed(); + return (T) this; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + checkClosed(); + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSet executeQuery(String sql) throws SQLException { + checkClosed(); + return new StubResultSet(); + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate(String sql) throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws SQLException { + closed = true; + } + + /** + * {@inheritDoc} + */ + @Override + public int getMaxFieldSize() throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void setMaxFieldSize(int max) throws SQLException { + checkClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getMaxRows() throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void setMaxRows(int max) throws SQLException { + checkClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setEscapeProcessing(boolean enable) throws SQLException { + checkClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getQueryTimeout() throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void setQueryTimeout(int seconds) throws SQLException { + if (oldDriver) { + throw new SQLFeatureNotSupportedException(); + } + + checkClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public void cancel() throws SQLException { + checkClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public SQLWarning getWarnings() throws SQLException { + checkClosed(); + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void clearWarnings() throws SQLException { + checkClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setCursorName(String name) throws SQLException { + checkClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute(String sql) throws SQLException { + checkClosed(); + if (simulatedQueryTime > 0) { + PoolTestExtension.quietlySleep(simulatedQueryTime); + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSet getResultSet() throws SQLException { + checkClosed(); + return new StubResultSet(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getUpdateCount() throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getMoreResults() throws SQLException { + checkClosed(); + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void setFetchDirection(int direction) throws SQLException { + checkClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getFetchDirection() throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void setFetchSize(int rows) throws SQLException { + checkClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getFetchSize() throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int getResultSetConcurrency() throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int getResultSetType() throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void addBatch(String sql) throws SQLException { + checkClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public void clearBatch() throws SQLException { + checkClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public int[] executeBatch() throws SQLException { + checkClosed(); + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Connection getConnection() throws SQLException { + checkClosed(); + return connection; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getMoreResults(int current) throws SQLException { + checkClosed(); + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public ResultSet getGeneratedKeys() throws SQLException { + checkClosed(); + return new StubResultSet(); + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + checkClosed(); + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + checkClosed(); + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + checkClosed(); + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public int getResultSetHoldability() throws SQLException { + checkClosed(); + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isClosed() throws SQLException { + return closed; + } + + /** + * {@inheritDoc} + */ + @Override + public void setPoolable(boolean poolable) throws SQLException { + checkClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isPoolable() throws SQLException { + checkClosed(); + return false; + } + + /** + * {@inheritDoc} + */ + public void closeOnCompletion() throws SQLException { + checkClosed(); + } + + /** + * {@inheritDoc} + */ + public boolean isCloseOnCompletion() throws SQLException { + checkClosed(); + return false; + } + + private void checkClosed() throws SQLException { + if (closed) { + throw new SQLException("Statement is closed"); + } + } +} diff --git a/jdbc-query/build.gradle b/jdbc-query/build.gradle new file mode 100644 index 0000000..ae01f31 --- /dev/null +++ b/jdbc-query/build.gradle @@ -0,0 +1,7 @@ +dependencies { + api project(":jdbc-connection-pool") + testImplementation "org.apache.derby:derby:${project.property('derby.version')}" + testImplementation "org.testcontainers:testcontainers:${project.property('testcontainers.version')}" + testImplementation "org.testcontainers:junit-jupiter:${project.property('testcontainers.version')}" + testImplementation "org.testcontainers:oracle-xe:${project.property('testcontainers.version')}" +} diff --git a/jdbc-query/src/main/java/module-info.java b/jdbc-query/src/main/java/module-info.java new file mode 100644 index 0000000..f5e3439 --- /dev/null +++ b/jdbc-query/src/main/java/module-info.java @@ -0,0 +1,4 @@ +module org.xbib.jdbc.query { + requires transitive org.xbib.jdbc.connection.pool; + exports org.xbib.jdbc.query; +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Config.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Config.java new file mode 100644 index 0000000..6bd33d2 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Config.java @@ -0,0 +1,127 @@ +package org.xbib.jdbc.query; + +import java.math.BigDecimal; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Entry point for getting configuration parameters. This isn't intended as + * a be-all, end-all configuration solution. Just a way of easily specifying + * multiple read-only sources for configuration with a nice fluent syntax. + */ +public interface Config extends Function, Supplier { + /** + * Convenience method for fluent syntax. + * + * @return a builder for specifying from where configuration should be loaded + */ + static ConfigFrom from() { + return new ConfigFromImpl(); + } + + // TODO add: String originalKey(String key) to find out the key before prefixing or other manipulation + + /** + * @return a trimmed, non-empty string, or null + */ + String getString(String key); + + /** + * @return a trimmed, non-empty string + * @throws ConfigMissingException if no value could be read for the specified key + */ + String getStringOrThrow(String key); + + String getString(String key, String defaultValue); + + /** + * Same as {@link #getString(String)}. Useful for passing configs around + * without static dependencies. + */ + @Override + default String apply(String key) { + return getString(key); + } + + @Override + default Config get() { + return this; + } + + + Integer getInteger(String key); + + int getInteger(String key, int defaultValue); + + /** + * @throws ConfigMissingException if no value could be read for the specified key + */ + int getIntegerOrThrow(String key); + + + Long getLong(String key); + + long getLong(String key, long defaultValue); + + /** + * @throws ConfigMissingException if no value could be read for the specified key + */ + long getLongOrThrow(String key); + + + Float getFloat(String key); + + float getFloat(String key, float defaultValue); + + /** + * @throws ConfigMissingException if no value could be read for the specified key + */ + float getFloatOrThrow(String key); + + + Double getDouble(String key); + + double getDouble(String key, double defaultValue); + + /** + * @throws ConfigMissingException if no value could be read for the specified key + */ + double getDoubleOrThrow(String key); + + + BigDecimal getBigDecimal(String key); + + + BigDecimal getBigDecimal(String key, BigDecimal defaultValue); + + /** + * @throws ConfigMissingException if no value could be read for the specified key + */ + + BigDecimal getBigDecimalOrThrow(String key); + + /** + * Read a boolean value from the configuration. The value is not case-sensitivie, + * and may be either true/false or yes/no. If no value was provided or an invalid + * value is provided, false will be returned. + */ + boolean getBooleanOrFalse(String key); + + /** + * Read a boolean value from the configuration. The value is not case-sensitivie, + * and may be either true/false or yes/no. If no value was provided or an invalid + * value is provided, true will be returned. + */ + boolean getBooleanOrTrue(String key); + + /** + * @throws ConfigMissingException if no value could be read for the specified key + */ + boolean getBooleanOrThrow(String key); + + /** + * Show where configuration is coming from. This is useful to drop in your logs + * for troubleshooting. + */ + String sources(); +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFrom.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFrom.java new file mode 100644 index 0000000..a297726 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFrom.java @@ -0,0 +1,102 @@ +package org.xbib.jdbc.query; + +import java.io.File; +import java.nio.charset.CharsetDecoder; +import java.util.Properties; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Pull configuration properties from various sources and filter/manipulate them. + */ +public interface ConfigFrom extends Supplier { + /** + * Convenience method for fluent syntax. + * + * @return a builder for specifying from where configuration should be loaded + */ + static ConfigFrom firstOf() { + return new ConfigFromImpl(); + } + + static Config other(Function other) { + if (other instanceof Config) { + return (Config) other; + } + return new ConfigFromImpl().custom(other::apply).get(); + } + + ConfigFrom custom(Function keyValueLookup); + + ConfigFrom value(String key, String value); + + ConfigFrom systemProperties(); + + ConfigFrom env(); + + ConfigFrom properties(Properties properties); + + ConfigFrom config(Config config); + + ConfigFrom config(Supplier config); + + /** + * Adds a set of properties files to read from, which can be overridden by a system property "properties". + * Equivalent to: + *
+     *   defaultPropertyFiles("properties", "conf/app.properties", "local.properties", "sample.properties")
+     * 
+ */ + ConfigFrom defaultPropertyFiles(); + + /** + * Adds a set of properties files to read from, which can be overridden by a specified system property. + * Equivalent to: + *
+     *   defaultPropertyFiles(systemPropertyKey, Charset.defaultCharset().newDecoder(), filenames)
+     * 
+ */ + ConfigFrom defaultPropertyFiles(String systemPropertyKey, String... filenames); + + /** + * Adds a set of properties files to read from, which can be overridden by a specified system property. + * Equivalent to: + *
+     *   propertyFile(Charset.defaultCharset().newDecoder(),
+     *       System.getProperty(systemPropertyKey, String.join(File.pathSeparator, filenames))
+     *       .split(File.pathSeparator));
+     * 
+ */ + ConfigFrom defaultPropertyFiles(String systemPropertyKey, CharsetDecoder decoder, String... filenames); + + ConfigFrom propertyFile(String... filenames); + + ConfigFrom propertyFile(CharsetDecoder decoder, String... filenames); + + ConfigFrom propertyFile(File... files); + + ConfigFrom propertyFile(CharsetDecoder decoder, File... files); + + ConfigFrom rename(String key, String newKey); + + ConfigFrom includeKeys(String... keys); + + ConfigFrom includePrefix(String... prefixes); + + ConfigFrom includeRegex(String regex); + + ConfigFrom excludeKeys(String... keys); + + ConfigFrom excludePrefix(String... prefixes); + + ConfigFrom excludeRegex(String regex); + + ConfigFrom removePrefix(String... prefixes); + + ConfigFrom addPrefix(String prefix); + + ConfigFrom substitutions(Config config); + + Config get(); + +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFromImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFromImpl.java new file mode 100644 index 0000000..cc96b03 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigFromImpl.java @@ -0,0 +1,295 @@ +package org.xbib.jdbc.query; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Access configuration properties from a variety of standard sources, + * and provide some basic filtering and mapping of property keys. + */ +public class ConfigFromImpl implements ConfigFrom { + + private final List searchPath = new ArrayList<>(); + + public ConfigFromImpl() { + super(); + } + + public ConfigFromImpl(Config first) { + searchPath.add(first); + } + + @Override + public ConfigFrom custom(Function keyValueLookup) { + return custom(keyValueLookup, "custom()"); + } + + private ConfigFrom custom(Function keyValueLookup, String source) { + searchPath.add(new ConfigImpl(keyValueLookup, source)); + return this; + } + + @Override + public ConfigFrom value(String key, String value) { + return custom(k -> k.equals(key) ? value : null, "value(" + key + ")"); + } + + @Override + public ConfigFrom config(Config config) { + searchPath.add(config); + return this; + } + + @Override + public ConfigFrom config(Supplier config) { + return config(config.get()); + } + + @Override + public ConfigFrom systemProperties() { + return custom(System::getProperty, "systemProperties()"); + } + + @Override + public ConfigFrom env() { + return custom(System::getenv, "env()"); + } + + @Override + public ConfigFrom properties(Properties properties) { + return custom(properties::getProperty, "properties()"); + } + + @Override + public ConfigFrom defaultPropertyFiles() { + return defaultPropertyFiles("properties", "conf/app.properties", "local.properties", "sample.properties"); + } + + @Override + public ConfigFrom defaultPropertyFiles(String systemPropertyKey, String... filenames) { + return defaultPropertyFiles(systemPropertyKey, Charset.defaultCharset().newDecoder(), filenames); + } + + @Override + public ConfigFrom defaultPropertyFiles(String systemPropertyKey, CharsetDecoder decoder, String... filenames) { + String properties = System.getProperty(systemPropertyKey, String.join(File.pathSeparator, filenames)); + return propertyFile(Charset.defaultCharset().newDecoder(), properties.split(File.pathSeparator)); + } + + @Override + public ConfigFrom propertyFile(String... filenames) { + return propertyFile(Charset.defaultCharset().newDecoder(), filenames); + } + + @Override + public ConfigFrom propertyFile(CharsetDecoder decoder, String... filenames) { + for (String filename : filenames) { + if (filename != null) { + propertyFile(decoder, new File(filename)); + } + } + return this; + } + + @Override + public ConfigFrom propertyFile(File... files) { + return propertyFile(Charset.defaultCharset().newDecoder(), files); + } + + @Override + public ConfigFrom propertyFile(CharsetDecoder decoder, File... files) { + for (File file : files) { + if (file != null) { + try { + Properties properties = new Properties(); + try ( + FileInputStream fis = new FileInputStream(file); + InputStreamReader reader = new InputStreamReader(fis, decoder) + ) { + properties.load(reader); + } + searchPath.add(new ConfigImpl(properties::getProperty, "propertyFile(" + file.getAbsolutePath() + ")")); + } catch (Exception e) { + // Put a "fake" provider in so we can see it failed + String fileName = file.getName(); + try { + fileName = file.getAbsolutePath(); + } catch (Exception ignored) { + // Fall back to relative name + } + custom(k -> null, "Ignored: propertyFile(" + fileName + ") " + e.getClass().getSimpleName()); + } + } + } + return this; + } + + @Override + public ConfigFrom rename(String configKey, String newKey) { + return new ConfigFromImpl(new ConfigImpl(key -> { + if (key.equals(configKey)) { + return null; + } + if (key.equals(newKey)) { + return lookup(configKey); + } + return lookup(key); + }, indentedSources("rename(" + configKey + " -> " + newKey + ")"))); + } + + @Override + public ConfigFrom includeKeys(String... keys) { + return new ConfigFromImpl(new ConfigImpl(key -> { + for (String k : keys) { + if (key.equals(k)) { + return lookup(key); + } + } + return null; + }, indentedSources("includeKeys" + Arrays.asList(keys)))); + } + + @Override + public ConfigFrom includePrefix(String... prefixes) { + return new ConfigFromImpl(new ConfigImpl(key -> { + for (String prefix : prefixes) { + if (key.startsWith(prefix)) { + return lookup(key); + } + } + return null; + }, indentedSources("includePrefix" + Arrays.asList(prefixes)))); + } + + @Override + public ConfigFrom includeRegex(String regex) { + return new ConfigFromImpl(new ConfigImpl(key -> { + if (key.matches(regex)) { + return lookup(key); + } + return null; + }, indentedSources("includeRegex(" + regex + ")"))); + } + + @Override + public ConfigFrom excludeKeys(String... keys) { + return new ConfigFromImpl(new ConfigImpl(key -> { + for (String k : keys) { + if (key.equals(k)) { + return null; + } + } + return lookup(key); + }, indentedSources("excludeKeys" + Arrays.asList(keys)))); + } + + @Override + public ConfigFrom excludePrefix(String... prefixes) { + return new ConfigFromImpl(new ConfigImpl(key -> { + for (String prefix : prefixes) { + if (key.startsWith(prefix)) { + return null; + } + } + return lookup(key); + }, indentedSources("excludePrefix" + Arrays.asList(prefixes)))); + } + + @Override + public ConfigFrom excludeRegex(String regex) { + return new ConfigFromImpl(new ConfigImpl(key -> { + if (key.matches(regex)) { + return null; + } + return lookup(key); + }, indentedSources("excludeRegex(" + regex + ")"))); + } + + @Override + public ConfigFrom removePrefix(String... prefixes) { + return new ConfigFromImpl(new ConfigImpl(key -> { + // Give precedence to ones that already lacked the prefix, + // do an include*() first if you don't want that + String value = lookup(key); + if (value != null) { + return value; + } + + for (String prefix : prefixes) { + value = lookup(prefix + key); + if (value != null) { + return value; + } + } + return null; + }, indentedSources("removePrefix" + Arrays.asList(prefixes)))); + } + + @Override + public ConfigFrom addPrefix(String prefix) { + return new ConfigFromImpl(new ConfigImpl(key -> { + if (key.startsWith(prefix)) { + return lookup(key.substring(prefix.length())); + } else { + return null; + } + }, indentedSources("addPrefix(" + prefix + ")"))); + } + + @Override + public ConfigFrom substitutions(Config config) { + return new ConfigFromImpl(new ConfigImpl(key -> { + String value = lookup(key); + if (value != null) { + // matches ${ENV_VAR_NAME} or $ENV_VAR_NAME + Pattern p = Pattern.compile("(? provider; + + private final String sources; + + private final Set failedKeys = new HashSet<>(); + + public ConfigImpl(Function provider, String sources) { + this.provider = provider; + this.sources = sources; + } + + @Override + public String getString(String key) { + return cleanString(key); + } + + + @Override + public String getStringOrThrow(String key) { + return nonnull(key, getString(key)); + } + + + @Override + public String getString(String key, String defaultValue) { + String stringValue = cleanString(key); + if (stringValue != null) { + return stringValue; + } + // Make sure the default value is tidied the same way a value would be + defaultValue = defaultValue.trim(); + if (defaultValue.length() == 0) { + throw new IllegalArgumentException("Your default value is empty or just whitespace"); + } + return defaultValue; + } + + @Override + public Integer getInteger(String key) { + String stringValue = cleanString(key); + try { + return stringValue == null ? null : Integer.parseInt(stringValue); + } catch (Exception e) { + if (!failedKeys.contains(key)) { + log.log(Level.SEVERE, "Could not load config value for key (this message will only be logged once): " + key, e); + failedKeys.add(key); + } + return null; + } + } + + @Override + public int getInteger(String key, int defaultValue) { + Integer value = getInteger(key); + return value == null ? defaultValue : value; + } + + @Override + public int getIntegerOrThrow(String key) { + return nonnull(key, getInteger(key)); + } + + + @Override + public Long getLong(String key) { + String stringValue = cleanString(key); + try { + return stringValue == null ? null : Long.parseLong(stringValue); + } catch (Exception e) { + if (!failedKeys.contains(key)) { + log.log(Level.SEVERE, "Could not load config value for key (this message will only be logged once): " + key, e); + failedKeys.add(key); + } + return null; + } + } + + @Override + public long getLong(String key, long defaultValue) { + Long value = getLong(key); + return value == null ? defaultValue : value; + } + + @Override + public long getLongOrThrow(String key) { + return nonnull(key, getLong(key)); + } + + + @Override + public Float getFloat(String key) { + String stringValue = cleanString(key); + try { + return stringValue == null ? null : Float.parseFloat(stringValue); + } catch (Exception e) { + if (!failedKeys.contains(key)) { + log.log(Level.SEVERE, "Could not load config value for key (this message will only be logged once): " + key, e); + failedKeys.add(key); + } + return null; + } + } + + @Override + public float getFloat(String key, float defaultValue) { + Float value = getFloat(key); + return value == null ? defaultValue : value; + } + + @Override + public float getFloatOrThrow(String key) { + return nonnull(key, getFloat(key)); + } + + + @Override + public Double getDouble(String key) { + String stringValue = cleanString(key); + try { + return stringValue == null ? null : Double.parseDouble(stringValue); + } catch (Exception e) { + if (!failedKeys.contains(key)) { + log.log(Level.SEVERE, "Could not load config value for key (this message will only be logged once): " + key, e); + failedKeys.add(key); + } + return null; + } + } + + @Override + public double getDouble(String key, double defaultValue) { + Double value = getDouble(key); + return value == null ? defaultValue : value; + } + + @Override + public double getDoubleOrThrow(String key) { + return nonnull(key, getDouble(key)); + } + + + @Override + public BigDecimal getBigDecimal(String key) { + String stringValue = cleanString(key); + try { + return stringValue == null ? null : new BigDecimal(stringValue); + } catch (Exception e) { + if (!failedKeys.contains(key)) { + log.log(Level.SEVERE, "Could not load config value for key (this message will only be logged once): " + key, e); + failedKeys.add(key); + } + return null; + } + } + + + @Override + public BigDecimal getBigDecimal(String key, BigDecimal defaultValue) { + BigDecimal value = getBigDecimal(key); + return value == null ? defaultValue : value; + } + + + @Override + public BigDecimal getBigDecimalOrThrow(String key) { + return nonnull(key, getBigDecimal(key)); + } + + @Override + public boolean getBooleanOrFalse(String key) { + return parseBoolean(cleanString(key), false); + } + + @Override + public boolean getBooleanOrTrue(String key) { + return parseBoolean(cleanString(key), true); + } + + @Override + public boolean getBooleanOrThrow(String key) { + String value = nonnull(key, cleanString(key)); + value = value.toLowerCase(); + if (value.equals("yes") || value.equals("true")) { + return true; + } + if (value.equals("no") || value.equals("false")) { + return false; + } + throw new ConfigMissingException("Unrecognized boolean value for config key: " + key); + } + + @Override + public String sources() { + return sources; + } + + private T nonnull(String key, T value) { + if (value == null) { + throw new ConfigMissingException("No value for config key: " + key); + } + return value; + } + + private boolean parseBoolean(String value, boolean defaultValue) { + if (value != null) { + value = value.toLowerCase(); + if (value.equals("yes") || value.equals("true")) { + return true; + } + if (value.equals("no") || value.equals("false")) { + return false; + } + } + return defaultValue; + } + + private String cleanString(String key) { + String value = null; + try { + value = provider.apply(key); + if (value != null) { + value = value.trim(); + if (value.length() == 0) { + value = null; + } + } + } catch (Exception e) { + if (!failedKeys.contains(key)) { + log.log(Level.SEVERE, "Could not load config value for key (this message will only be logged once): " + key, e); + failedKeys.add(key); + } + } + return value; + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigInvalidException.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigInvalidException.java new file mode 100644 index 0000000..97256a4 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigInvalidException.java @@ -0,0 +1,22 @@ +package org.xbib.jdbc.query; + +/** + * Indicates that a configuration value is present but not in a usable format. + * For example, if the value must be an integer and the configuration value + * is a non-numeric string. + */ +@SuppressWarnings("serial") +public class ConfigInvalidException extends DatabaseException { + + public ConfigInvalidException(String message) { + super(message); + } + + public ConfigInvalidException(Throwable cause) { + super(cause); + } + + public ConfigInvalidException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigMissingException.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigMissingException.java new file mode 100644 index 0000000..a8f7c34 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConfigMissingException.java @@ -0,0 +1,20 @@ +package org.xbib.jdbc.query; + +/** + * Indicates that a configuration value is required but was not present. + */ +@SuppressWarnings("serial") +public class ConfigMissingException extends DatabaseException { + + public ConfigMissingException(String message) { + super(message); + } + + public ConfigMissingException(Throwable cause) { + super(cause); + } + + public ConfigMissingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/ConstraintViolationException.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConstraintViolationException.java new file mode 100644 index 0000000..e25fc06 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/ConstraintViolationException.java @@ -0,0 +1,23 @@ +package org.xbib.jdbc.query; + +/** + * This exception will be thrown when a condition arises that violates + * a stated invariant regarding the database. This might be a database + * schema "constraint violated" as thrown by the database, or could be + * caused by a violation of constraints enforced only within the code. + */ +@SuppressWarnings("serial") +public class ConstraintViolationException extends DatabaseException { + + public ConstraintViolationException(String message) { + super(message); + } + + public ConstraintViolationException(Throwable cause) { + super(cause); + } + + public ConstraintViolationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Database.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Database.java new file mode 100644 index 0000000..30a9c6a --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Database.java @@ -0,0 +1,211 @@ +package org.xbib.jdbc.query; + +import java.sql.Connection; +import java.util.Date; +import java.util.function.Supplier; + +/** + * Primary class for accessing a relational (SQL) database. + */ +public interface Database extends Supplier { + /** + * Create a SQL "insert" statement for further manipulation and execution. + * Note this call does not actually execute the SQL. + * + * @param sql the SQL to execute, optionally containing indexed ("?") or + * named (":foo") parameters. To include the characters '?' or ':' + * in the SQL you must escape them with two ("??" or "::"). You + * MUST be careful not to pass untrusted strings in as SQL, since + * this will be executed in the database. + * @return an interface for further manipulating the statement; never null + */ + SqlInsert toInsert(String sql); + + SqlInsert toInsert( Sql sql); + + /** + * Create a SQL "select" statement for further manipulation and execution. + * Note this call does not actually execute the SQL. + * + * @param sql the SQL to execute, optionally containing indexed ("?") or + * named (":foo") parameters. To include the characters '?' or ':' + * in the SQL you must escape them with two ("??" or "::"). You + * MUST be careful not to pass untrusted strings in as SQL, since + * this will be executed in the database. + * @return an interface for further manipulating the statement; never null + */ + SqlSelect toSelect(String sql); + + SqlSelect toSelect( Sql sql); + + /** + * Create a SQL "update" statement for further manipulation and execution. + * Note this call does not actually execute the SQL. + * + * @param sql the SQL to execute, optionally containing indexed ("?") or + * named (":foo") parameters. To include the characters '?' or ':' + * in the SQL you must escape them with two ("??" or "::"). You + * MUST be careful not to pass untrusted strings in as SQL, since + * this will be executed in the database. + * @return an interface for further manipulating the statement; never null + */ + SqlUpdate toUpdate(String sql); + + SqlUpdate toUpdate( Sql sql); + + /** + * Create a SQL "delete" statement for further manipulation and execution. + * Note this call does not actually execute the SQL. + * + * @param sql the SQL to execute, optionally containing indexed ("?") or + * named (":foo") parameters. To include the characters '?' or ':' + * in the SQL you must escape them with two ("??" or "::"). You + * MUST be careful not to pass untrusted strings in as SQL, since + * this will be executed in the database. + * @return an interface for further manipulating the statement; never null + */ + SqlUpdate toDelete(String sql); + + SqlUpdate toDelete( Sql sql); + + /** + * Create a DDL (schema modifying) statement for further manipulation and execution. + * Note this call does not actually execute the SQL. + * + * @param sql the SQL to execute, optionally containing indexed ("?") or + * named (":foo") parameters. To include the characters '?' or ':' + * in the SQL you must escape them with two ("??" or "::"). You + * MUST be careful not to pass untrusted strings in as SQL, since + * this will be executed in the database. + * @return an interface for further manipulating the statement; never null + */ + Ddl ddl(String sql); + + /** + * Read the next value from a sequence. This method helps smooth over the + * syntax differences across databases. + */ + Long nextSequenceValue( String sequenceName); + + /** + * Get the value that would be used if you specify an argNowPerApp() parameter. + */ + Date nowPerApp(); + + /** + * Cause the underlying connection to commit its transaction immediately. This + * must be explicitly enabled (see {@link Options}, + * or it will throw a {@link DatabaseException}. + */ + void commitNow(); + + /** + * Cause the underlying connection to roll back its transaction immediately. This + * must be explicitly enabled (see {@link Options}, + * or it will throw a {@link DatabaseException}. + */ + void rollbackNow(); + + /** + *

Obtain direct access to the connection being used by this instance. Be very + * careful as this is highly likely to be unsafe and cause you great pain and + * suffering. This method is included to help ease into the library in large + * codebases where some parts still rely on direct JDBC access.

+ * + *

By default this method will throw a {@link DatabaseException}. If you want + * to use this method you must explicitly enable it via + * {@link Options#allowConnectionAccess()}

+ */ + Connection underlyingConnection(); + + Options options(); + + /** + * Access information about what kind of database we are dealing with. + */ + Flavor flavor(); + + /** + *

A little syntax sugar to make it easier to customize your SQL based on the + * specific database. For example:

+ * + *
"select 1" + db.when().oracle(" from dual")
+ *
"select " + db.when().postgres("date_trunc('day',").other("trunc(") + ") ..."
+ * + * @return an interface for chaining or terminating the conditionals + */ + When when(); + + /** + * Convenience method to deal with mutually incompatible syntax for this. For example: + * + *

Oracle: 'drop sequence x'

+ *

Derby: 'drop sequence x restrict'

" + */ + void dropSequenceQuietly(String sequenceName); + + /** + * Convenience method to deal with dropping tables that may or may not exist. Some + * databases make it hard to check and conditionally drop things, so we will just + * try to drop it and ignore the errors. + * + * @param tableName the table to be dropped + */ + void dropTableQuietly(String tableName); + + /** + * Convenience method to check if a table or view exists so that caller can decide + * whether to create or update a table. The table name's case is normalized using + * the database's convention unless tableName is enclosed in double quotes. + * The default catalog and schema from the DB connection will be used. + * + * @param tableName the table to be checked + * @return true if the table or view exists + */ + boolean tableExists( String tableName); + + /** + * Convenience method to check whether a table or view exists or not. + * The table name's case is normalized using the database's convention + * unless tableName is enclosed in double quotes. The default catalog + * from the DB connection will be used. + * + * @param tableName the table to be checked + * @param schemaName the schema expected to contain the table + * @return true if the table or view exists + */ + boolean tableExists( String tableName, String schemaName); + + /** + * Return the DB table name in the normalized form in which it is stored. + * Databases like Oracle, Derby, HSQL store their tables in upper case. + * Databases like postgres and sqlserver use lower case unless configured otherwise. + * If the caller passes in a quoted string, we will leave the name as is, removing + * the quotes. + * + * @param tableName this should be a name, not a pattern + * @return table name in appropriate format for DB lookup - original case, uppercase, or lowercase + */ + String normalizeTableName(String tableName); + + /** + * Check the JVM time (and timezone) against the database and log a warning + * or throw an error if they are too far apart. It is a good idea to do this + * before you store and dates, and maybe make it part of your health checks. + * If the clocks differ by more than an hour, a DatabaseException is thrown + * suggesting you check the timezones (under the assumptions the JVM and + * database are running in different timezones). + * + * @param millisToWarn if the clocks disagree by more than this and less than + * millisToError, a warning will be dropped in the log + * @param millisToError if the clocks disagree by more than this a + * DatabaseEception will be thrown + */ + void assertTimeSynchronized(long millisToWarn, long millisToError); + + /** + * Convenience method, same as {@link #assertTimeSynchronized(long, long)} + * with millisToWarn=10000 and millisToError=30000. + */ + void assertTimeSynchronized(); +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseException.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseException.java new file mode 100644 index 0000000..26ed63b --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseException.java @@ -0,0 +1,37 @@ +package org.xbib.jdbc.query; + +/** + * Indicates something went wrong accessing the database. Most often this is + * used to wrap SQLException to avoid declaring checked exceptions. + */ +@SuppressWarnings("serial") +public class DatabaseException extends RuntimeException { + + public DatabaseException(String message) { + super(message); + } + + public DatabaseException(Throwable cause) { + super(cause); + } + + public DatabaseException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Wrap an exception with a DatabaseException, taking into account all known + * subtypes such that we wrap subtypes in a matching type (so we don't obscure + * the type available to catch clauses). + * + * @param message the new wrapping exception will have this message + * @param cause the exception to be wrapped + * @return the exception you should throw + */ + public static DatabaseException wrap(String message, Throwable cause) { + if (cause instanceof ConstraintViolationException) { + return new ConstraintViolationException(message, cause); + } + return new DatabaseException(message, cause); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseImpl.java new file mode 100644 index 0000000..705b0b2 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseImpl.java @@ -0,0 +1,280 @@ +package org.xbib.jdbc.query; + +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.logging.Logger; + +/** + * Primary class for accessing a relational (SQL) database. + */ +public class DatabaseImpl implements Database { + + private static final Logger log = Logger.getLogger(Database.class.getName()); + + private final Connection connection; + + private final Options options; + + public DatabaseImpl(Connection connection, Options options) { + this.connection = connection; + this.options = options; + } + + public DatabaseImpl(Flavor flavor) { + this(new OptionsDefault(flavor)); + } + + public DatabaseImpl(Options options) { + this.connection = null; + this.options = options; + } + + @Override + public DatabaseImpl get() { + return this; + } + + @Override + public SqlInsert toInsert(String sql) { + return new SqlInsertImpl(connection, sql, options); + } + + @Override + public SqlInsert toInsert(Sql sql) { + return new SqlInsertImpl(connection, sql.sql(), options).apply(sql); + } + + @Override + public SqlSelect toSelect(String sql) { + return new SqlSelectImpl(connection, sql, options); + } + + @Override + public SqlSelect toSelect(Sql sql) { + return new SqlSelectImpl(connection, sql.sql(), options).apply(sql); + } + + @Override + public SqlUpdate toUpdate(String sql) { + return new SqlUpdateImpl(connection, sql, options); + } + + @Override + public SqlUpdate toUpdate(Sql sql) { + return new SqlUpdateImpl(connection, sql.sql(), options).apply(sql); + } + + @Override + public SqlUpdate toDelete(String sql) { + return new SqlUpdateImpl(connection, sql, options); + } + + @Override + public SqlUpdate toDelete(Sql sql) { + return new SqlUpdateImpl(connection, sql.sql(), options).apply(sql); + } + + @Override + public Ddl ddl(String sql) { + return new DdlImpl(connection, sql, options); + } + + @Override + public Long nextSequenceValue(/*@Untainted*/ String sequenceName) { + return toSelect(flavor().sequenceSelectNextVal(sequenceName)).queryLongOrNull(); + } + + @Override + public Date nowPerApp() { + return options.currentDate(); + } + + public void commitNow() { + if (options.ignoreTransactionControl()) { + log.fine("Ignoring call to commitNow()"); + return; + } + if (!options.allowTransactionControl()) { + throw new DatabaseException("Calls to commitNow() are not allowed"); + } + try { + connection.commit(); + } catch (Exception e) { + throw new DatabaseException("Unable to commit transaction", e); + } + } + + public void rollbackNow() { + if (options.ignoreTransactionControl()) { + log.fine("Ignoring call to rollbackNow()"); + return; + } + if (!options.allowTransactionControl()) { + throw new DatabaseException("Calls to rollbackNow() are not allowed"); + } + try { + connection.rollback(); + } catch (Exception e) { + throw new DatabaseException("Unable to rollback transaction", e); + } + } + + @Override + public Connection underlyingConnection() { + if (!options.allowConnectionAccess()) { + throw new DatabaseException("Calls to underlyingConnection() are not allowed"); + } + + return connection; + } + + @Override + public Options options() { + return options; + } + + @Override + public Flavor flavor() { + return options.flavor(); + } + + + @Override + public When when() { + return new When(options.flavor()); + } + + @Override + public void dropSequenceQuietly(/*@Untainted*/ String sequenceName) { + ddl(flavor().sequenceDrop(sequenceName)).executeQuietly(); + } + + @Override + public void dropTableQuietly(/*@Untainted*/ String tableName) { + if (flavor() == Flavor.postgresql || flavor() == Flavor.hsqldb) { + ddl("drop table if exists " + tableName).executeQuietly(); + } else { + ddl("drop table " + tableName).executeQuietly(); + } + } + + @Override + public boolean tableExists(String tableName) throws DatabaseException { + + String schemaName = null; + Method getSchema = null; + + try { + // Use reflections to see if connection.getSchema API exists. It should exist for any JDBC7 or later implementation + // We still support Oracle 11 with odbc6, however, so we can't assume it's there. + getSchema = connection.getClass().getDeclaredMethod("getSchema"); + } catch (NoSuchMethodException noMethodExc) { + // Expected if method does not exist - just let it go + } + + try { + if (getSchema != null) { + schemaName = ((String) getSchema.invoke(connection, new Object[0])); + } else if (flavor() == Flavor.oracle) { + // Oracle defaults to user name schema - use that. + log.warning("Connection getSchema API was not found. Defaulting to Oracle user name schema." + + "If this is not appropriate, please use tableExists(tableName, schemaName) API or upgrade to ojdbc7 or later"); + schemaName = connection.getMetaData().getUserName(); + } + if (schemaName == null) { + // connection.getSchema API was supported starting at JDK1.7. Method should not be null. + throw new NullPointerException("Unable to retrieve schema name."); + } + + } catch (Exception exc) { + throw new DatabaseException("Unable to determine the schema. " + + "Please use tableExists(tableName, schemaName API) or upgrade to a JDBC7 driver or later.", exc); + } + + return tableExists(tableName, schemaName); + } + + + @Override + public boolean tableExists(String tableName, String schemaName) throws DatabaseException { + if (tableName != null) { + try { + DatabaseMetaData metadata = connection.getMetaData(); + String normalizedTable = normalizeTableName(tableName); + ResultSet resultSet = + metadata.getTables(connection.getCatalog(), schemaName, normalizedTable, new String[]{"TABLE", "VIEW"}); + + while (resultSet.next()) { + if (normalizedTable.equals(resultSet.getString("TABLE_NAME"))) { + return true; + } + } + } catch (SQLException exc) { + throw new DatabaseException("Unable to look up table " + tableName + + " in schema " + schemaName + " : " + exc.getMessage(), + exc); + } + } + + return false; + } + + @Override + public String normalizeTableName(String tableName) { + if (tableName == null) { + return tableName; + } + + // If user gave us a quoted string, leave it alone for look up + if (tableName.length() > 2) { + if (tableName.startsWith("\"") && tableName.endsWith("\"")) { + // Remove quotes and return as is. + return tableName.substring(1, tableName.length() - 1); + } + } + + if (flavor().isNormalizedUpperCase()) { + return tableName.toUpperCase(); + } + + return tableName.toLowerCase(); + } + + @Override + public void assertTimeSynchronized(long millisToWarn, long millisToError) { + toSelect("select ?" + flavor().fromAny()) + .argDateNowPerDb().queryFirstOrNull(r -> { + Date appDate = nowPerApp(); + Date dbDate = r.getDateOrNull(); + if (dbDate == null) { + throw new DatabaseException("Expecting a date in the result"); + } + if (Math.abs(appDate.getTime() - dbDate.getTime()) > 3600000) { + throw new DatabaseException("App and db time are over an hour apart (check your timezones) app: " + + DateTimeFormatter.ISO_INSTANT.format(appDate.toInstant()) + " db: " + + DateTimeFormatter.ISO_INSTANT.format(dbDate.toInstant())); + } + if (Math.abs(appDate.getTime() - dbDate.getTime()) > millisToError) { + throw new DatabaseException("App and db time over " + millisToError + " millis apart (check your clocks) app: " + + DateTimeFormatter.ISO_INSTANT.format(appDate.toInstant()) + " db: " + + DateTimeFormatter.ISO_INSTANT.format(dbDate.toInstant())); + } + if (Math.abs(appDate.getTime() - dbDate.getTime()) > millisToWarn) { + log.warning("App and db time are over " + millisToWarn + " millis apart (check your clocks) app: " + + DateTimeFormatter.ISO_INSTANT.format(appDate.toInstant()) + " db: " + + DateTimeFormatter.ISO_INSTANT.format(dbDate.toInstant())); + } + return null; + }); + } + + @Override + public void assertTimeSynchronized() { + assertTimeSynchronized(10000, 30000); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseProvider.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseProvider.java new file mode 100644 index 0000000..409c416 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseProvider.java @@ -0,0 +1,1107 @@ +package org.xbib.jdbc.query; + +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; +import org.xbib.jdbc.query.util.Metric; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This is a lazy provider for Database instances. It helps avoid allocating connection + * or transaction resources until (or if) we actually need a Database. As a consequence + * of this laziness, the underlying resources require explicit cleanup by calling either + * commitAndClose() or rollbackAndClose(). + */ +public final class DatabaseProvider implements Supplier { + + private static final Logger log = Logger.getLogger(DatabaseProvider.class.getName()); + + private static final AtomicInteger poolNameCounter = new AtomicInteger(1); + + private final Options options; + + private DatabaseProvider delegateTo = null; + + private Supplier connectionProvider; + + private Connection connection = null; + + private Database database = null; + + public DatabaseProvider(Supplier connectionProvider, Options options) { + if (connectionProvider == null) { + throw new IllegalArgumentException("Connection provider cannot be null"); + } + this.connectionProvider = connectionProvider; + this.options = options; + } + + private DatabaseProvider(DatabaseProvider delegateTo) { + this.delegateTo = delegateTo; + this.options = delegateTo.options; + } + + /** + * Configure the database from the following properties read from the provided configuration: + *
+ *
+     *   database.url=...       Database connect string (required)
+     *   database.user=...      Authenticate as this user (optional if provided in url)
+     *   database.password=...  User password (optional if user and password provided in
+     *                          url; prompted on standard input if user is provided and
+     *                          password is not)
+     *   database.pool.size=... How many connections in the connection pool (default 10).
+     *   database.driver.class  The driver to initialize with Class.forName(). This will
+     *                          be guessed from the database.url if not provided.
+     *   database.flavor        One of the enumerated values in {@link Flavor}. If this
+     *                          is not provided the flavor will be guessed based on the
+     *                          value for database.url, if possible.
+     * 
+ * + *

The database flavor will be guessed based on the URL.

+ * + *

A database pool will be created using HikariCP.

+ */ + public static Builder pooledBuilder(Config config) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { + return fromPoolDataSource(createPoolDataSource(config), getFlavor(config)); + } + + /** + * Use an externally configured DataSource and a Flavor. + */ + + public static Builder fromPoolDataSource(PoolDataSource ds, Flavor flavor) { + return new BuilderImpl(ds, () -> { + try { + return ds.getConnection(); + } catch (Exception e) { + throw new DatabaseException("Unable to obtain a connection from the DataSource", e); + } + }, new OptionsDefault(flavor)); + } + + /** + * Builder method to create and initialize an instance of this class using + * the JDBC standard DriverManager method. The url parameter will be inspected + * to determine the Flavor for this database. + */ + + public static Builder fromDriverManager(String url) { + return fromDriverManager(url, Flavor.fromJdbcUrl(url), null, null, null); + } + + /** + * Builder method to create and initialize an instance of this class using + * the JDBC standard DriverManager method. + * + * @param flavor use this flavor rather than guessing based on the url + */ + + public static Builder fromDriverManager(String url, Flavor flavor) { + return fromDriverManager(url, flavor, null, null, null); + } + + public static Builder fromDriverManager(Config config) { + return fromDriverManager(config.getString("database.url"), + config.getString("database.user"), + config.getString("database.password")); + } + + /** + * Builder method to create and initialize an instance of this class using + * the JDBC standard DriverManager method. The url parameter will be inspected + * to determine the Flavor for this database. + */ + + public static Builder fromDriverManager(String url, Properties info) { + return fromDriverManager(url, Flavor.fromJdbcUrl(url), info, null, null); + } + + /** + * Builder method to create and initialize an instance of this class using + * the JDBC standard DriverManager method. + * + * @param flavor use this flavor rather than guessing based on the url + */ + + public static Builder fromDriverManager(String url, Flavor flavor, Properties info) { + return fromDriverManager(url, flavor, info, null, null); + } + + /** + * Builder method to create and initialize an instance of this class using + * the JDBC standard DriverManager method. The url parameter will be inspected + * to determine the Flavor for this database. + */ + + public static Builder fromDriverManager(String url, String user, String password) { + return fromDriverManager(url, Flavor.fromJdbcUrl(url), null, user, password); + } + + /** + * Builder method to create and initialize an instance of this class using + * the JDBC standard DriverManager method. + * + * @param flavor use this flavor rather than guessing based on the url + */ + + public static Builder fromDriverManager(String url, + Flavor flavor, + String user, + String password) { + return fromDriverManager(url, flavor, null, user, password); + } + + private static Builder fromDriverManager(String url, + Flavor flavor, + Properties info, + String user, + String password) { + Options options = new OptionsDefault(flavor); + try { + DriverManager.getDriver(url); + } catch (SQLException e) { + try { + Class.forName(Flavor.driverForJdbcUrl(url)); + } catch (ClassNotFoundException e1) { + throw new DatabaseException("Couldn't locate JDBC driver - try setting -Djdbc.drivers=some.Driver", e1); + } + } + return new BuilderImpl(null, () -> { + try { + if (info != null) { + return DriverManager.getConnection(url, info); + } else if (user != null) { + return DriverManager.getConnection(url, user, password); + } + return DriverManager.getConnection(url); + } catch (Exception e) { + throw new DatabaseException("Unable to obtain a connection from DriverManager", e); + } + }, options); + } + + /** + * Configure the database from up to five properties read from a file: + *
+ *
+     *   database.url=...      Database connect string (required)
+     *   database.user=...     Authenticate as this user (optional if provided in url)
+     *   database.password=... User password (optional if user and password provided in
+     *                         url; prompted on standard input if user is provided and
+     *                         password is not)
+     *   database.flavor=...   What kind of database it is (optional, will guess based
+     *                         on the url if this is not provided)
+     *   database.driver=...   The Java class of the JDBC driver to load (optional, will
+     *                         guess based on the flavor if this is not provided)
+     * 
+ *

This will use the JVM default character encoding to read the property file.

+ * + * @param filename path to the properties file we will attempt to read + * @throws DatabaseException if the property file could not be read for any reason + */ + public static Builder fromPropertyFile(String filename) { + return fromPropertyFile(filename, Charset.defaultCharset().newDecoder()); + } + + /** + * Configure the database from up to five properties read from a file: + *
+ *
+     *   database.url=...      Database connect string (required)
+     *   database.user=...     Authenticate as this user (optional if provided in url)
+     *   database.password=... User password (optional if user and password provided in
+     *                         url; prompted on standard input if user is provided and
+     *                         password is not)
+     *   database.flavor=...   What kind of database it is (optional, will guess based
+     *                         on the url if this is not provided)
+     *   database.driver=...   The Java class of the JDBC driver to load (optional, will
+     *                         guess based on the flavor if this is not provided)
+     * 
+ * + * @param filename path to the properties file we will attempt to read + * @param decoder character encoding to use when reading the property file + * @throws DatabaseException if the property file could not be read for any reason + */ + public static Builder fromPropertyFile(String filename, CharsetDecoder decoder) { + Properties properties = new Properties(); + if (filename != null && filename.length() > 0) { + try ( + FileInputStream fis = new FileInputStream(filename); + InputStreamReader reader = new InputStreamReader(fis, decoder) + ) { + properties.load(reader); + } catch (Exception e) { + throw new DatabaseException("Unable to read properties file: " + filename, e); + } + } + return fromProperties(properties, "", true); + } + + /** + * Configure the database from up to five properties read from a file: + *
+ *
+     *   database.url=...      Database connect string (required)
+     *   database.user=...     Authenticate as this user (optional if provided in url)
+     *   database.password=... User password (optional if user and password provided in
+     *                         url; prompted on standard input if user is provided and
+     *                         password is not)
+     *   database.flavor=...   What kind of database it is (optional, will guess based
+     *                         on the url if this is not provided)
+     *   database.driver=...   The Java class of the JDBC driver to load (optional, will
+     *                         guess based on the flavor if this is not provided)
+     * 
+ *

This will use the JVM default character encoding to read the property file.

+ * + * @param filename path to the properties file we will attempt to read + * @param propertyPrefix if this is null or empty the properties above will be read; + * if a value is provided it will be prefixed to each property + * (exactly, so if you want to use "my.database.url" you must + * pass "my." as the prefix) + * @throws DatabaseException if the property file could not be read for any reason + */ + public static Builder fromPropertyFile(String filename, String propertyPrefix) { + return fromPropertyFile(filename, propertyPrefix, Charset.defaultCharset().newDecoder()); + } + + /** + * Configure the database from up to five properties read from a file: + *
+ *
+     *   database.url=...      Database connect string (required)
+     *   database.user=...     Authenticate as this user (optional if provided in url)
+     *   database.password=... User password (optional if user and password provided in
+     *                         url; prompted on standard input if user is provided and
+     *                         password is not)
+     *   database.flavor=...   What kind of database it is (optional, will guess based
+     *                         on the url if this is not provided)
+     *   database.driver=...   The Java class of the JDBC driver to load (optional, will
+     *                         guess based on the flavor if this is not provided)
+     * 
+ * + * @param filename path to the properties file we will attempt to read + * @param propertyPrefix if this is null or empty the properties above will be read; + * if a value is provided it will be prefixed to each property + * (exactly, so if you want to use "my.database.url" you must + * pass "my." as the prefix) + * @param decoder character encoding to use when reading the property file + * @throws DatabaseException if the property file could not be read for any reason + */ + public static Builder fromPropertyFile(String filename, String propertyPrefix, CharsetDecoder decoder) { + Properties properties = new Properties(); + if (filename != null && filename.length() > 0) { + try ( + FileInputStream fis = new FileInputStream(filename); + InputStreamReader reader = new InputStreamReader(fis, decoder) + ) { + properties.load(reader); + } catch (Exception e) { + throw new DatabaseException("Unable to read properties file: " + filename, e); + } + } + return fromProperties(properties, propertyPrefix, true); + } + + /** + * Configure the database from up to five properties read from the provided properties: + *
+ *
+     *   database.url=...      Database connect string (required)
+     *   database.user=...     Authenticate as this user (optional if provided in url)
+     *   database.password=... User password (optional if user and password provided in
+     *                         url; prompted on standard input if user is provided and
+     *                         password is not)
+     *   database.flavor=...   What kind of database it is (optional, will guess based
+     *                         on the url if this is not provided)
+     *   database.driver=...   The Java class of the JDBC driver to load (optional, will
+     *                         guess based on the flavor if this is not provided)
+     * 
+ * + * @param properties properties will be read from here + * @throws DatabaseException if the property file could not be read for any reason + */ + public static Builder fromProperties(Properties properties) { + return fromProperties(properties, "", false); + } + + /** + * Configure the database from up to five properties read from the provided properties: + *
+ *
+     *   database.url=...      Database connect string (required)
+     *   database.user=...     Authenticate as this user (optional if provided in url)
+     *   database.password=... User password (optional if user and password provided in
+     *                         url; prompted on standard input if user is provided and
+     *                         password is not)
+     *   database.flavor=...   What kind of database it is (optional, will guess based
+     *                         on the url if this is not provided)
+     *   database.driver=...   The Java class of the JDBC driver to load (optional, will
+     *                         guess based on the flavor if this is not provided)
+     * 
+ * + * @param properties properties will be read from here + * @param propertyPrefix if this is null or empty the properties above will be read; + * if a value is provided it will be prefixed to each property + * (exactly, so if you want to use "my.database.url" you must + * pass "my." as the prefix) + * @throws DatabaseException if the property file could not be read for any reason + */ + public static Builder fromProperties(Properties properties, String propertyPrefix) { + return fromProperties(properties, propertyPrefix, false); + } + + /** + * Configure the database from up to five properties read from the specified + * properties file, or from the system properties (system properties will take + * precedence over the file): + *
+ *
+     *   database.url=...      Database connect string (required)
+     *   database.user=...     Authenticate as this user (optional if provided in url)
+     *   database.password=... User password (optional if user and password provided in
+     *                         url; prompted on standard input if user is provided and
+     *                         password is not)
+     *   database.flavor=...   What kind of database it is (optional, will guess based
+     *                         on the url if this is not provided)
+     *   database.driver=...   The Java class of the JDBC driver to load (optional, will
+     *                         guess based on the flavor if this is not provided)
+     * 
+ *

This will use the JVM default character encoding to read the property file.

+ * + * @param filename path to the properties file we will attempt to read; if the file + * cannot be read for any reason (e.g. does not exist) a debug level + * log entry will be entered, but it will attempt to proceed using + * solely the system properties + */ + public static Builder fromPropertyFileOrSystemProperties(String filename) { + return fromPropertyFileOrSystemProperties(filename, Charset.defaultCharset().newDecoder()); + } + + /** + * Configure the database from up to five properties read from the specified + * properties file, or from the system properties (system properties will take + * precedence over the file): + *
+ *
+     *   database.url=...      Database connect string (required)
+     *   database.user=...     Authenticate as this user (optional if provided in url)
+     *   database.password=... User password (optional if user and password provided in
+     *                         url; prompted on standard input if user is provided and
+     *                         password is not)
+     *   database.flavor=...   What kind of database it is (optional, will guess based
+     *                         on the url if this is not provided)
+     *   database.driver=...   The Java class of the JDBC driver to load (optional, will
+     *                         guess based on the flavor if this is not provided)
+     * 
+ * + * @param filename path to the properties file we will attempt to read; if the file + * cannot be read for any reason (e.g. does not exist) a debug level + * log entry will be entered, but it will attempt to proceed using + * solely the system properties + * @param decoder character encoding to use when reading the property file + */ + public static Builder fromPropertyFileOrSystemProperties(String filename, CharsetDecoder decoder) { + Properties properties = new Properties(); + if (filename != null && filename.length() > 0) { + try ( + FileInputStream fis = new FileInputStream(filename); + InputStreamReader reader = new InputStreamReader(fis, decoder) + ) { + properties.load(reader); + } catch (Exception e) { + log.fine("Trying system properties - unable to read properties file: " + filename); + } + } + return fromProperties(properties, "", true); + } + + /** + * Configure the database from up to five properties read from the specified + * properties file, or from the system properties (system properties will take + * precedence over the file): + *
+ *
+     *   database.url=...      Database connect string (required)
+     *   database.user=...     Authenticate as this user (optional if provided in url)
+     *   database.password=... User password (optional if user and password provided in
+     *                         url; prompted on standard input if user is provided and
+     *                         password is not)
+     *   database.flavor=...   What kind of database it is (optional, will guess based
+     *                         on the url if this is not provided)
+     *   database.driver=...   The Java class of the JDBC driver to load (optional, will
+     *                         guess based on the flavor if this is not provided)
+     * 
+ *

This will use the JVM default character encoding to read the property file.

+ * + * @param filename path to the properties file we will attempt to read; if the file + * cannot be read for any reason (e.g. does not exist) a debug level + * log entry will be entered, but it will attempt to proceed using + * solely the system properties + * @param propertyPrefix if this is null or empty the properties above will be read; + * if a value is provided it will be prefixed to each property + * (exactly, so if you want to use "my.database.url" you must + * pass "my." as the prefix) + */ + public static Builder fromPropertyFileOrSystemProperties(String filename, String propertyPrefix) { + return fromPropertyFileOrSystemProperties(filename, propertyPrefix, Charset.defaultCharset().newDecoder()); + } + + /** + * Configure the database from up to five properties read from the specified + * properties file, or from the system properties (system properties will take + * precedence over the file): + *
+ *
+     *   database.url=...      Database connect string (required)
+     *   database.user=...     Authenticate as this user (optional if provided in url)
+     *   database.password=... User password (optional if user and password provided in
+     *                         url; prompted on standard input if user is provided and
+     *                         password is not)
+     *   database.flavor=...   What kind of database it is (optional, will guess based
+     *                         on the url if this is not provided)
+     *   database.driver=...   The Java class of the JDBC driver to load (optional, will
+     *                         guess based on the flavor if this is not provided)
+     * 
+ * + * @param filename path to the properties file we will attempt to read; if the file + * cannot be read for any reason (e.g. does not exist) a debug level + * log entry will be entered, but it will attempt to proceed using + * solely the system properties + * @param propertyPrefix if this is null or empty the properties above will be read; + * if a value is provided it will be prefixed to each property + * (exactly, so if you want to use "my.database.url" you must + * pass "my." as the prefix) + * @param decoder character encoding to use when reading the property file + */ + public static Builder fromPropertyFileOrSystemProperties(String filename, String propertyPrefix, + CharsetDecoder decoder) { + Properties properties = new Properties(); + if (filename != null && filename.length() > 0) { + try ( + FileInputStream fis = new FileInputStream(filename); + InputStreamReader reader = new InputStreamReader(fis, decoder) + ) { + properties.load(reader); + } catch (Exception e) { + log.fine("Trying system properties - unable to read properties file: " + filename); + } + } + return fromProperties(properties, propertyPrefix, true); + } + + /** + * Configure the database from up to five system properties: + *
+ *
+     *   -Ddatabase.url=...      Database connect string (required)
+     *   -Ddatabase.user=...     Authenticate as this user (optional if provided in url)
+     *   -Ddatabase.password=... User password (optional if user and password provided in
+     *                           url; prompted on standard input if user is provided and
+     *                           password is not)
+     *   -Ddatabase.flavor=...   What kind of database it is (optional, will guess based
+     *                           on the url if this is not provided)
+     *   -Ddatabase.driver=...   The Java class of the JDBC driver to load (optional, will
+     *                           guess based on the flavor if this is not provided)
+     * 
+ */ + + public static Builder fromSystemProperties() { + return fromProperties(null, "", true); + } + + /** + * Configure the database from up to five system properties: + *
+ *
+     *   -D{prefix}database.url=...      Database connect string (required)
+     *   -D{prefix}database.user=...     Authenticate as this user (optional if provided in url)
+     *   -D{prefix}database.password=... User password (optional if user and password provided in
+     *                                   url; prompted on standard input if user is provided and
+     *                                   password is not)
+     *   -D{prefix}database.flavor=...   What kind of database it is (optional, will guess based
+     *                                   on the url if this is not provided)
+     *   -D{prefix}database.driver=...   The Java class of the JDBC driver to load (optional, will
+     *                                   guess based on the flavor if this is not provided)
+     * 
+ * + * @param propertyPrefix a prefix to attach to each system property - be sure to include the + * dot if desired (e.g. "mydb." for properties like -Dmydb.database.url) + */ + + public static Builder fromSystemProperties(String propertyPrefix) { + return fromProperties(null, propertyPrefix, true); + } + + private static Builder fromProperties(Properties properties, String propertyPrefix, boolean useSystemProperties) { + if (propertyPrefix == null) { + propertyPrefix = ""; + } + + String driver; + String flavorStr; + String url; + String user; + String password; + if (useSystemProperties) { + if (properties == null) { + properties = new Properties(); + } + driver = System.getProperty(propertyPrefix + "database.driver", + properties.getProperty(propertyPrefix + "database.driver")); + flavorStr = System.getProperty(propertyPrefix + "database.flavor", + properties.getProperty(propertyPrefix + "database.flavor")); + url = System.getProperty(propertyPrefix + "database.url", + properties.getProperty(propertyPrefix + "database.url")); + user = System.getProperty(propertyPrefix + "database.user", + properties.getProperty(propertyPrefix + "database.user")); + password = System.getProperty(propertyPrefix + "database.password", + properties.getProperty(propertyPrefix + "database.password")); + } else { + if (properties == null) { + throw new DatabaseException("No properties were provided"); + } + driver = properties.getProperty(propertyPrefix + "database.driver"); + flavorStr = properties.getProperty(propertyPrefix + "database.flavor"); + url = properties.getProperty(propertyPrefix + "database.url"); + user = properties.getProperty(propertyPrefix + "database.user"); + password = properties.getProperty(propertyPrefix + "database.password"); + } + + if (url == null) { + throw new DatabaseException("You must use -D" + propertyPrefix + "database.url=..."); + } + + if (user != null && password == null) { + System.out.println("Enter database password for user " + user + ":"); + byte[] input = new byte[256]; + try { + int bytesRead = System.in.read(input); + password = new String(input, 0, bytesRead - 1, Charset.defaultCharset()); + } catch (IOException e) { + throw new DatabaseException("Error reading password from standard input", e); + } + } + + Flavor flavor; + if (flavorStr != null) { + flavor = Flavor.valueOf(flavorStr); + } else { + flavor = Flavor.fromJdbcUrl(url); + } + + if (driver == null) { + if (flavor == Flavor.oracle) { + driver = "oracle.jdbc.OracleDriver"; + } else if (flavor == Flavor.postgresql) { + driver = "org.postgresql.Driver"; + } else if (flavor == Flavor.derby) { + driver = "org.apache.derby.jdbc.EmbeddedDriver"; + } + } + if (driver != null) { + try { + Class.forName(driver).getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new DatabaseException("Unable to load JDBC driver: " + driver, e); + } + } + if (user == null) { + return fromDriverManager(url, flavor); + } else { + return fromDriverManager(url, flavor, user, password); + } + } + + public static PoolDataSource createPoolDataSource(Config config) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { + String url = config.getString("database.url"); + if (url == null) { + throw new DatabaseException("You must provide database.url"); + } + PoolConfig poolConfig = new PoolConfig(); + poolConfig.setPoolName(config.getString("database.pool.name", "pool-" + poolNameCounter.getAndAdd(1))); + String driverClassName = config.getString("database.driver.class", Flavor.driverForJdbcUrl(url)); + poolConfig.setDriverClassName(driverClassName); + poolConfig.setUsername(config.getString("database.user")); + poolConfig.setPassword(config.getString("database.password")); + int poolSize = config.getInteger("database.pool.size", 8); + poolConfig.setMaximumPoolSize(poolSize); + poolConfig.setAutoCommit(false); + return new PoolDataSource(poolConfig); + } + + public static Flavor getFlavor(Config config) { + String url = config.getString("database.url"); + if (url == null) { + throw new DatabaseException("You must provide database.url"); + } + Flavor flavor; + String flavorString = config.getString("database.flavor"); + if (flavorString != null) { + flavor = Flavor.valueOf(flavorString); + } else { + flavor = Flavor.fromJdbcUrl(url); + } + return flavor; + } + + public void transact(final DbCode code) { + boolean complete = false; + try { + code.run(this); + complete = true; + } catch (ThreadDeath | DatabaseException t) { + throw t; + } catch (Throwable t) { + throw new DatabaseException("Error during transaction", t); + } finally { + if (!complete) { + rollbackAndClose(); + } else { + commitAndClose(); + } + } + } + + public T transactReturning(final DbCodeTyped code) { + T result; + boolean complete = false; + try { + result = code.run(this); + complete = true; + } catch (ThreadDeath | DatabaseException t) { + throw t; + } catch (Throwable t) { + throw new DatabaseException("Error during transaction", t); + } finally { + if (!complete) { + rollbackAndClose(); + } else { + commitAndClose(); + } + } + return result; + } + + public void transact(final DbCodeTx code) { + Transaction tx = new TransactionImpl(); + tx.setRollbackOnError(true); + tx.setRollbackOnly(false); + boolean complete = false; + try { + code.run(this, tx); + complete = true; + } catch (ThreadDeath | DatabaseException t) { + throw t; + } catch (Throwable t) { + throw new DatabaseException("Error during transaction", t); + } finally { + if ((!complete && tx.isRollbackOnError()) || tx.isRollbackOnly()) { + rollbackAndClose(); + } else { + commitAndClose(); + } + } + } + + public Database get() { + if (delegateTo != null) { + return delegateTo.get(); + } + + if (database != null) { + return database; + } + + if (connectionProvider == null) { + throw new DatabaseException("Called get() on a DatabaseProvider after close()"); + } + + Metric metric = new Metric(log.isLoggable(Level.FINE)); + try { + connection = connectionProvider.get(); + metric.checkpoint("getConn"); + try { + // JDBC specifies that autoCommit is the default for all new connections. + // Don't try to be clever about clearing it conditionally. + if (!options.flavor().autoCommitOnly()) { + connection.setAutoCommit(false); + metric.checkpoint("setAutoCommit"); + } + } catch (SQLException e) { + throw new DatabaseException("Unable to set autoCommit for the connection", e); + } + database = new DatabaseImpl(connection, options); + metric.checkpoint("dbInit"); + } catch (RuntimeException e) { + metric.checkpoint("fail"); + throw e; + } finally { + metric.done(); + if (log.isLoggable(Level.FINE)) { + StringBuilder buf = new StringBuilder("Get ").append(options.flavor()).append(" database: "); + metric.printMessage(buf); + log.fine(buf.toString()); + } + } + return database; + } + + public Builder fakeBuilder() { + return new Builder() { + @Override + public Builder withOptions(OptionsOverride optionsOverride) { + return this; + } + + @Override + public Builder withSqlParameterLogging() { + return this; + } + + @Override + public Builder withSqlInExceptionMessages() { + return this; + } + + @Override + public Builder withDatePerAppOnly() { + return this; + } + + @Override + public Builder withTransactionControl() { + return this; + } + + @Override + public Builder withTransactionControlSilentlyIgnored() { + return this; + } + + @Override + public Builder withConnectionAccess() { + return this; + } + + @Override + public DatabaseProvider create() { + return new DatabaseProvider(DatabaseProvider.this); + } + + @Override + public void transact(DbCode tx) { + create().transact(tx); + } + + @Override + public T transactReturning(DbCodeTyped tx) { + return create().transactReturning(tx); + } + + @Override + public void transact(DbCodeTx tx) { + create().transact(tx); + } + + @Override + public void close() { + log.fine("Ignoring close call on fakeBuilder"); + } + }; + } + + public void commitAndClose() { + if (delegateTo != null) { + log.fine("Ignoring commitAndClose() because this is a fake provider"); + return; + } + + if (connection != null) { + try { + if (!options.flavor().autoCommitOnly()) { + connection.commit(); + } + } catch (Exception e) { + throw new DatabaseException("Unable to commit the transaction", e); + } + close(); + } + } + + public void rollbackAndClose() { + if (delegateTo != null) { + log.fine("Ignoring rollbackAndClose() because this is a fake provider"); + return; + } + + if (connection != null) { + try { + if (!options.flavor().autoCommitOnly()) { + connection.rollback(); + } + } catch (Exception e) { + log.log(Level.SEVERE, "Unable to rollback the transaction", e); + } + close(); + } + } + + private void close() { + if (connection != null) { + try { + connection.close(); + } catch (Exception e) { + log.log(Level.SEVERE, "Unable to close the database connection", e); + } + } + connection = null; + database = null; + connectionProvider = null; + } + + /** + * This builder is immutable, so setting various options does not affect + * the previous instance. This is intended to make it safe to pass builders + * around without risk someone will reconfigure it. + */ + public interface Builder { + + Builder withOptions(OptionsOverride options); + + /** + * Enable logging of parameter values along with the SQL. + */ + + Builder withSqlParameterLogging(); + + /** + * Include SQL in exception messages. This will also include parameters in the + * exception messages if SQL parameter logging is enabled. This is handy for + * development, but be careful as this is an information disclosure risk, + * dependent on how the exception are caught and handled. + */ + + Builder withSqlInExceptionMessages(); + + /** + * Wherever argDateNowPerDb() is specified, use argDateNowPerApp() instead. This is + * useful for testing purposes as you can use OptionsOverride to provide your + * own system clock that will be used for time travel. + */ + + Builder withDatePerAppOnly(); + + /** + * Allow provided Database instances to explicitly control transactions using the + * commitNow() and rollbackNow() methods. Otherwise calling those methods would + * throw an exception. + */ + + Builder withTransactionControl(); + + /** + * This can be useful when testing code, as it can pretend to use transactions, + * while giving you control over whether it actually commits or rolls back. + */ + + Builder withTransactionControlSilentlyIgnored(); + + /** + * Allow direct access to the underlying database connection. Normally this is + * not allowed, and is a bad idea, but it can be helpful when migrating from + * legacy code that works with raw JDBC. + */ + + Builder withConnectionAccess(); + + /** + * WARNING: You should try to avoid using this method. If you use it more + * that once or twice in your entire codebase you are probably doing + * something wrong. + * + *

If you use this method you are responsible for managing + * the transaction and commit/rollback/close.

+ */ + + DatabaseProvider create(); + + /** + * This is a convenience method to eliminate the need for explicitly + * managing the resources (and error handling) for this class. After + * the run block is complete the transaction will commit unless the + * {@link DbCode#run(Supplier) run(Supplier)} method threw a {@link Throwable}. + * + *

Here is a typical usage: + *

+         *   dbp.transact(dbs -> {
+         *     List r = dbs.get().toSelect("select a from b where c=?").argInteger(1).queryStrings();
+         *     ... do something with the results ...
+         *   });
+         * 
+ *

+ * + * @param code the code you want to run as a transaction with a Database + * @see #transact(DbCodeTx) + */ + void transact(DbCode code); + + /** + * This method is the same as {@link #transact(DbCode)} but allows a return value. + * + *

Here is a typical usage: + *

+         *   List r = dbp.transact(dbs -> {
+         *     return dbs.get().toSelect("select a from b where c=?").argInteger(1).queryStrings();
+         *   });
+         * 
+ *

+ */ + T transactReturning(DbCodeTyped code); + + /** + * This is a convenience method to eliminate the need for explicitly + * managing the resources (and error handling) for this class. After + * the run block is complete commit() will be called unless either the + * {@link DbCodeTx#run(Supplier, Transaction)} method threw a {@link Throwable} + * while {@link Transaction#isRollbackOnError()} returns true, or + * {@link Transaction#isRollbackOnly()} returns a true value. + * + *

Here is a typical usage: + *

+         *   dbp.transact((dbs, tx) -> {
+         *     tx.setRollbackOnError(false);
+         *     dbs.get().toInsert("...").argInteger(1).insert(1);
+         *     ...some stuff that might fail...
+         *   });
+         * 
+ *

+ * + * @param code the code you want to run as a transaction with a Database + */ + void transact(DbCodeTx code); + + void close(); + } + + private static class BuilderImpl implements Builder { + + private PoolDataSource ds; + + private final Supplier connectionProvider; + + private final Options options; + + private BuilderImpl(PoolDataSource ds, Supplier connectionProvider, Options options) { + this.ds = ds; + this.connectionProvider = connectionProvider; + this.options = options; + } + + @Override + public Builder withOptions(OptionsOverride options) { + return new BuilderImpl(ds, connectionProvider, options.withParent(this.options)); + } + + @Override + public Builder withSqlParameterLogging() { + return new BuilderImpl(ds, connectionProvider, new OptionsOverride() { + @Override + public boolean isLogParameters() { + return true; + } + }.withParent(this.options)); + } + + @Override + public Builder withSqlInExceptionMessages() { + return new BuilderImpl(ds, connectionProvider, new OptionsOverride() { + @Override + public boolean isDetailedExceptions() { + return true; + } + }.withParent(this.options)); + } + + @Override + public Builder withDatePerAppOnly() { + return new BuilderImpl(ds, connectionProvider, new OptionsOverride() { + @Override + public boolean useDatePerAppOnly() { + return true; + } + }.withParent(this.options)); + } + + @Override + public Builder withTransactionControl() { + return new BuilderImpl(ds, connectionProvider, new OptionsOverride() { + @Override + public boolean allowTransactionControl() { + return true; + } + }.withParent(this.options)); + } + + @Override + public Builder withTransactionControlSilentlyIgnored() { + return new BuilderImpl(ds, connectionProvider, new OptionsOverride() { + @Override + public boolean ignoreTransactionControl() { + return true; + } + }.withParent(this.options)); + } + + @Override + public Builder withConnectionAccess() { + return new BuilderImpl(ds, connectionProvider, new OptionsOverride() { + @Override + public boolean allowConnectionAccess() { + return true; + } + }.withParent(this.options)); + } + + @Override + public DatabaseProvider create() { + return new DatabaseProvider(connectionProvider, options); + } + + @Override + public void transact(DbCode tx) { + create().transact(tx); + } + + @Override + public T transactReturning(DbCodeTyped tx) { + return create().transactReturning(tx); + } + + @Override + public void transact(DbCodeTx tx) { + create().transact(tx); + } + + @Override + public void close() { + if (ds != null) { + ds.close(); + ds = null; + } + } + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCode.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCode.java new file mode 100644 index 0000000..f1df0dd --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCode.java @@ -0,0 +1,18 @@ +package org.xbib.jdbc.query; + +import java.util.function.Supplier; + +/** + * A block of runnable code using a transacted Database. + */ +public interface DbCode { + /** + * Implement this method to provide a block of code that uses the provided database + * and is transacted. Whether the transaction will commit or rollback is typically + * controlled by the code that invokes this method. + * + *

If a {@link Throwable} is thrown from this method, it will be caught, wrapped in + * a DatabaseException (if it is not already one), and then propagated.

+ */ + void run(Supplier dbs) throws Exception; +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTx.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTx.java new file mode 100644 index 0000000..6ec3ade --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTx.java @@ -0,0 +1,19 @@ +package org.xbib.jdbc.query; + +import java.util.function.Supplier; + +/** + * A block of runnable code using a transacted Database. + */ +@FunctionalInterface +public interface DbCodeTx { + /** + * Implement this method to provide a block of code that uses the provided database + * and is transacted. Whether the transaction will commit or rollback is typically + * controlled by the code that invokes this method. + * + *

If a {@link Throwable} is thrown from this method, it will be caught, wrapped in + * a DatabaseException (if it is not already one), and then propagated.

+ */ + void run(Supplier db, Transaction tx) throws Exception; +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTyped.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTyped.java new file mode 100644 index 0000000..b3fbb77 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTyped.java @@ -0,0 +1,19 @@ +package org.xbib.jdbc.query; + +import java.util.function.Supplier; + +/** + * A block of runnable code using a transacted Database. + */ +@FunctionalInterface +public interface DbCodeTyped { + /** + * Implement this method to provide a block of code that uses the provided database + * and is transacted. Whether the transaction will commit or rollback is typically + * controlled by the code that invokes this method. + * + *

If a {@link Throwable} is thrown from this method, it will be caught, wrapped in + * a DatabaseException (if it is not already one), and then propagated.

+ */ + T run(Supplier dbs) throws Exception; +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTypedTx.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTypedTx.java new file mode 100644 index 0000000..76ecb87 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DbCodeTypedTx.java @@ -0,0 +1,19 @@ +package org.xbib.jdbc.query; + +import java.util.function.Supplier; + +/** + * A block of runnable code using a transacted Database. + */ +@FunctionalInterface +public interface DbCodeTypedTx { + /** + * Implement this method to provide a block of code that uses the provided database + * and is transacted. Whether the transaction will commit or rollback is typically + * controlled by the code that invokes this method. + * + *

If a {@link Throwable} is thrown from this method, it will be caught, wrapped in + * a DatabaseException (if it is not already one), and then propagated.

+ */ + T run(Supplier db, Transaction tx) throws Exception; +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Ddl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Ddl.java new file mode 100644 index 0000000..fff4b8e --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Ddl.java @@ -0,0 +1,19 @@ +package org.xbib.jdbc.query; + +/** + * Interface for executing a chunk of DDL within the database. + */ +public interface Ddl { + /** + * Execute the DDL statement. All checked SQLExceptions get wrapped in DatabaseExceptions. + */ + void execute(); + + /** + * This just does an execute() call and silently discards any DatabaseException + * that might occur. This can be useful for things like drop statements, where + * some databases don't make it easy to conditionally drop things only if they + * exist. + */ + void executeQuietly(); +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DdlImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DdlImpl.java new file mode 100644 index 0000000..abd6640 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DdlImpl.java @@ -0,0 +1,97 @@ +package org.xbib.jdbc.query; + +import org.xbib.jdbc.query.util.DebugSql; +import org.xbib.jdbc.query.util.Metric; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.Statement; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DdlImpl implements Ddl { + + private static final Logger log = Logger.getLogger(Database.class.getName()); + + private static final Logger logQuiet = Logger.getLogger(Database.class.getName() + ".quiet"); + + private final Connection connection; + + private final String sql; + + private final Options options; + + DdlImpl(Connection connection, String sql, Options options) { + this.connection = connection; + this.sql = sql; + this.options = options; + } + + private void updateInternal(boolean quiet) { + CallableStatement ps = null; + Metric metric = new Metric(log.isLoggable(Level.FINE)); + + boolean isSuccess = false; + String errorCode = null; + Exception logEx = null; + try { + ps = connection.prepareCall(sql); + + metric.checkpoint("prep"); + ps.execute(); + metric.checkpoint("exec"); + isSuccess = true; + } catch (Exception e) { + errorCode = options.generateErrorCode(); + logEx = e; + throw DatabaseException.wrap(DebugSql.exceptionMessage(sql, null, errorCode, options), e); + } finally { + close(ps); + metric.checkpoint("close"); + // PostgreSQL requires explicit commit since we are running with setAutoCommit(false) + commit(connection); + metric.done("commit"); + if (isSuccess) { + DebugSql.logSuccess("DDL", log, metric, sql, null, options); + } else if (quiet) { + DebugSql.logWarning("DDL", logQuiet, metric, errorCode, sql, null, options, logEx); + } else { + DebugSql.logError("DDL", log, metric, errorCode, sql, null, options, logEx); + } + } + } + + @Override + public void execute() { + updateInternal(false); + } + + @Override + public void executeQuietly() { + try { + updateInternal(true); + } catch (DatabaseException e) { + // Ignore, as requested + } + } + + private void close(Statement s) { + if (s != null) { + try { + s.close(); + } catch (Exception e) { + log.log(Level.SEVERE, "Caught exception closing the Statement", e); + } + } + } + + private void commit(Connection c) { + if (c != null) { + try { + c.commit(); + } catch (Exception e) { + log.log(Level.SEVERE, "Caught exception on commit", e); + } + } + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Flavor.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Flavor.java new file mode 100644 index 0000000..8eb8ea9 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Flavor.java @@ -0,0 +1,993 @@ +package org.xbib.jdbc.query; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +/** + * Enumeration of supported databases with various compatibility settings. + */ +public enum Flavor { + + derby { + @Override + public boolean isNormalizedUpperCase() { + return true; + } + + @Override + public String typeInteger() { + return "integer"; + } + + @Override + public String typeBoolean() { + return "char(1)"; + } + + @Override + public String typeLong() { + return "bigint"; + } + + @Override + public String typeFloat() { + return "real"; + } + + @Override + public String typeDouble() { + return "double"; + } + + @Override + public String typeBigDecimal(int size, int precision) { + return "numeric(" + size + "," + precision + ")"; + } + + @Override + public String typeStringVar(int length) { + return "varchar(" + length + ")"; + } + + @Override + public String typeStringFixed(int length) { + return "char(" + length + ")"; + } + + @Override + public String typeClob() { + return "clob"; + } + + @Override + public String typeBlob() { + return "blob"; + } + + @Override + public String typeDate() { + return "timestamp"; + } + + @Override + public String typeLocalDate() { + return "date"; + } + + @Override + public boolean useStringForClob() { + return false; + } + + @Override + public boolean useBytesForBlob() { + return false; + } + + @Override + public String sequenceNextVal(String sequenceName) { + return "next value for " + sequenceName; + } + + @Override + public String sequenceSelectNextVal(String sequenceName) { + return "values next value for " + sequenceName; + } + + @Override + public String sequenceDrop(String dbtestSeq) { + return "drop sequence " + dbtestSeq + " restrict"; + } + + @Override + public boolean supportsInsertReturning() { + return false; + } + + @Override + public String sequenceCacheClause(int nbrValuesToCache) { + return ""; + } + + @Override + public String sequenceOrderClause(boolean order) { + return ""; + } + + @Override + public String sequenceCycleClause(boolean cycle) { + return cycle ? " cycle" : " no cycle"; + } + + @Override + public String dbTimeMillis() { + return "current_timestamp"; + } + + @Override + public String fromAny() { + return " from sysibm.sysdummy1"; + } + + @Override + public String dateAsSqlFunction(Date date, Calendar calendar) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000"); + dateFormat.setCalendar(calendar); + return "timestamp('" + dateFormat.format(date) + "')"; + } + + @Override + public String localDateAsSqlFunction(Date date) { + return "'" + date.toString() + "'"; + } + + @Override + public String sequenceOptions() { + return " as bigint"; + } + + @Override + public boolean autoCommitOnly() { + return false; + } + }, + sqlserver { + @Override + public boolean isNormalizedUpperCase() { + return false; + } + + @Override + public String typeFloat() { + return "float(24)"; + } + + @Override + public String typeDouble() { + return "float(53)"; + } + + @Override + public String typeBigDecimal(int size, int precision) { + return "numeric(" + size + "," + precision + ")"; + } + + @Override + public String typeInteger() { + return "numeric(10)"; + } + + @Override + public String typeBoolean() { + return "char(1)"; + } + + @Override + public String typeLong() { + return "numeric(19)"; + } + + @Override + public String typeDate() { + return "datetime2(3)"; + } + + @Override + public String typeLocalDate() { + return "date"; + } + + @Override + public boolean useStringForClob() { + return false; + } + + @Override + public boolean useBytesForBlob() { + return false; + } + + @Override + public String sequenceNextVal(String sequenceName) { + return "next value for " + sequenceName; + } + + @Override + public String sequenceSelectNextVal(String sequenceName) { + return "select next value for " + sequenceName; + } + + @Override + public String sequenceDrop(String dbtestSeq) { + return "drop sequence " + dbtestSeq; + } + + @Override + public String typeStringVar(int length) { + return "varchar(" + length + ")"; + } + + @Override + public String typeStringFixed(int length) { + return "char(" + length + ")"; + } + + @Override + public String typeClob() { + return "varchar(max)"; + } + + @Override + public String typeBlob() { + return "varbinary(max)"; + } + + @Override + public String sequenceOrderClause(boolean order) { + // Not supported + return ""; + } + + @Override + public String sequenceCycleClause(boolean cycle) { + return cycle ? " cycle" : " no cycle"; + } + + @Override + public boolean supportsInsertReturning() { + // TODO it probably does, but I haven't figure it out yet + return false; + } + + @Override + public String dbTimeMillis() { + return "current_timestamp"; + } + + @Override + public String sequenceCacheClause(int nbrValuesToCache) { + if (nbrValuesToCache < 2) { + return " no cache"; + } + return " cache " + nbrValuesToCache; + } + + @Override + public String fromAny() { + return ""; + } + + @Override + public String dateAsSqlFunction(Date date, Calendar calendar) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000"); + dateFormat.setCalendar(calendar); + return "cast('" + dateFormat.format(date) + "' as datetime2(3))"; + } + + @Override + public String localDateAsSqlFunction(Date date) { + return "'" + date.toString() + "'"; + } + + @Override + public String sequenceOptions() { + return ""; + } + + @Override + public boolean autoCommitOnly() { + return false; + } + }, + oracle { + @Override + public boolean isNormalizedUpperCase() { + return true; + } + + @Override + public String typeFloat() { + return "binary_float"; + } + + @Override + public String typeDouble() { + return "binary_double"; + } + + @Override + public String typeBigDecimal(int size, int precision) { + return "numeric(" + size + "," + precision + ")"; + } + + @Override + public String typeInteger() { + return "numeric(10)"; + } + + @Override + public String typeBoolean() { + return "char(1 char)"; + } + + @Override + public String typeLong() { + return "numeric(19)"; + } + + @Override + public String typeDate() { + return "timestamp(3)"; + } + + @Override + public String typeLocalDate() { + return "date"; + } + + @Override + public boolean useStringForClob() { + return false; + } + + @Override + public boolean useBytesForBlob() { + return false; + } + + @Override + public String sequenceNextVal(String sequenceName) { + return sequenceName + ".nextval"; + } + + @Override + public String sequenceSelectNextVal(String sequenceName) { + return "select " + sequenceName + ".nextval from dual"; + } + + @Override + public String sequenceDrop(String dbtestSeq) { + return "drop sequence " + dbtestSeq; + } + + @Override + public String typeStringVar(int length) { + return "varchar2(" + length + " char)"; + } + + @Override + public String typeStringFixed(int length) { + return "char(" + length + " char)"; + } + + @Override + public String typeClob() { + return "clob"; + } + + @Override + public String typeBlob() { + return "blob"; + } + + @Override + public String sequenceOrderClause(boolean order) { + return order ? " order" : " noorder"; + } + + @Override + public String sequenceCycleClause(boolean cycle) { + return cycle ? " cycle" : " nocycle"; + } + + @Override + public boolean supportsInsertReturning() { + return true; + } + + @Override + public String dbTimeMillis() { + return "systimestamp(3)"; + } + + @Override + public String sequenceCacheClause(int nbrValuesToCache) { + if (nbrValuesToCache < 2) { + return " nocache"; + } + return " cache " + nbrValuesToCache; + } + + @Override + public String fromAny() { + return " from dual"; + } + + @Override + public String dateAsSqlFunction(Date date, Calendar calendar) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000"); + dateFormat.setCalendar(calendar); + return "timestamp '" + dateFormat.format(date) + "'"; + } + + @Override + public String localDateAsSqlFunction(Date date) { + return "to_date('" + date.toString() + "', 'yyyy-mm-dd')"; + } + + @Override + public String sequenceOptions() { + return ""; + } + + @Override + public boolean autoCommitOnly() { + return false; + } + }, + postgresql { + @Override + public boolean isNormalizedUpperCase() { + return false; + } + + @Override + public String typeInteger() { + return "integer"; + } + + @Override + public String typeBoolean() { + return "char(1)"; + } + + @Override + public String typeLong() { + return "bigint"; + } + + @Override + public String typeFloat() { + return "real"; + } + + @Override + public String typeDouble() { + return "double precision"; + } + + @Override + public String typeBigDecimal(int size, int precision) { + return "numeric(" + size + "," + precision + ")"; + } + + @Override + public String typeStringVar(int length) { + return "varchar(" + length + ")"; + } + + @Override + public String typeStringFixed(int length) { + return "char(" + length + ")"; + } + + @Override + public String typeClob() { + return "text"; + } + + @Override + public String typeBlob() { + return "bytea"; + } + + @Override + public String typeDate() { + return "timestamp(3)"; + } + + @Override + public String typeLocalDate() { + return "date"; + } + + @Override + public boolean useStringForClob() { + return true; + } + + @Override + public boolean useBytesForBlob() { + return true; + } + + @Override + public String sequenceNextVal(String sequenceName) { + return "nextval('" + sequenceName + "')"; + } + + @Override + public String sequenceSelectNextVal(String sequenceName) { + return "select nextval('" + sequenceName + "')"; + } + + @Override + public String sequenceDrop(String dbtestSeq) { + return "drop sequence " + dbtestSeq; + } + + @Override + public String sequenceOrderClause(boolean order) { + return ""; + } + + @Override + public String sequenceCycleClause(boolean cycle) { + return cycle ? " cycle" : " no cycle"; + } + + @Override + public String fromAny() { + return ""; + } + + @Override + public boolean supportsInsertReturning() { + return true; + } + + @Override + public String dbTimeMillis() { + return "date_trunc('milliseconds',localtimestamp)"; + } + + @Override + public String sequenceCacheClause(int nbrValuesToCache) { + return " cache " + nbrValuesToCache; + } + + @Override + public String dateAsSqlFunction(Date date, Calendar calendar) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000"); + dateFormat.setCalendar(calendar); + return "'" + dateFormat.format(date) + " GMT'::timestamp"; + } + + @Override + public String localDateAsSqlFunction(Date date) { + return "'" + date.toString() + "'"; + } + + @Override + public String sequenceOptions() { + return ""; + } + + @Override + public boolean autoCommitOnly() { + return false; + } + }, + hsqldb { + @Override + public boolean isNormalizedUpperCase() { + return true; + } + + @Override + public String typeInteger() { + return "integer"; + } + + @Override + public String typeBoolean() { + return "char(1)"; + } + + @Override + public String typeLong() { + return "bigint"; + } + + @Override + public String typeFloat() { + return "double"; + } + + @Override + public String typeDouble() { + return "double"; + } + + @Override + public String typeBigDecimal(int size, int precision) { + return "numeric(" + size + "," + precision + ")"; + } + + @Override + public String typeStringVar(int length) { + return "varchar(" + length + ")"; + } + + @Override + public String typeStringFixed(int length) { + return "char(" + length + ")"; + } + + @Override + public String typeClob() { + return "clob(2G)"; + } + + @Override + public String typeBlob() { + return "blob(2G)"; + } + + @Override + public String typeDate() { + return "timestamp(3)"; + } + + @Override + public String typeLocalDate() { + return "date"; + } + + @Override + public boolean useStringForClob() { + return true; + } + + @Override + public boolean useBytesForBlob() { + return true; + } + + @Override + public String sequenceNextVal(String sequenceName) { + return "next value for " + sequenceName + ""; + } + + @Override + public String sequenceSelectNextVal(String sequenceName) { + return "select " + sequenceNextVal(sequenceName) + fromAny(); + } + + @Override + public String sequenceDrop(String dbtestSeq) { + return "drop sequence if exists " + dbtestSeq; + } + + @Override + public String sequenceOrderClause(boolean order) { + return ""; + } + + @Override + public String sequenceCycleClause(boolean cycle) { + return cycle ? " cycle" : " no cycle"; + } + + @Override + public String fromAny() { + return " from (values(0))"; + } + + @Override + public boolean supportsInsertReturning() { + return false; + } + + @Override + public String dbTimeMillis() { + return "localtimestamp"; + } + + @Override + public String sequenceCacheClause(int nbrValuesToCache) { + return ""; + } + + @Override + public String dateAsSqlFunction(Date date, Calendar calendar) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000XXX"); + dateFormat.setCalendar(calendar); + return "cast(timestamp '" + dateFormat.format(date) + "' as timestamp without time zone)"; + } + + @Override + public String localDateAsSqlFunction(Date date) { + return "'" + date.toString() + "'"; + } + + @Override + public String sequenceOptions() { + return " as bigint"; + } + + @Override + public boolean autoCommitOnly() { + return false; + } + }, + bigquery { + @Override + public boolean isNormalizedUpperCase() { + return false; + } + + @Override + public String typeInteger() { + return "int64"; + } + + @Override + public String typeBoolean() { + // BigQuery has a native boolean type, but we're not trying to use it + return "string"; + } + + @Override + public String typeLong() { + return "int64"; + } + + @Override + public String typeFloat() { + return "float64"; + } + + @Override + public String typeDouble() { + return "float64"; + } + + @Override + public String typeBigDecimal(int size, int precision) { + return "numeric"; + } + + @Override + public String typeStringVar(int length) { + return "string"; + } + + @Override + public String typeStringFixed(int length) { + return "string"; + } + + @Override + public String typeClob() { + return "string"; + } + + @Override + public String typeBlob() { + return "bytes"; + } + + @Override + public String typeDate() { + return "datetime"; + } + + @Override + public String typeLocalDate() { + return "date"; + } + + @Override + public boolean useStringForClob() { + return true; + } + + @Override + public boolean useBytesForBlob() { + return true; + } + + @Override + public String sequenceNextVal(String sequenceName) { + throw new UnsupportedOperationException(); + } + + @Override + public String sequenceSelectNextVal(String sequenceName) { + throw new UnsupportedOperationException(); + } + + @Override + public String sequenceDrop(String dbtestSeq) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean supportsInsertReturning() { + return false; + } + + @Override + public String dbTimeMillis() { + return "current_timestamp()"; + } + + @Override + public String sequenceCacheClause(int nbrValuesToCache) { + throw new UnsupportedOperationException(); + } + + @Override + public String sequenceOrderClause(boolean order) { + throw new UnsupportedOperationException(); + } + + @Override + public String sequenceCycleClause(boolean cycle) { + throw new UnsupportedOperationException(); + } + + @Override + public String fromAny() { + return ""; + } + + @Override + public String dateAsSqlFunction(Date date, Calendar calendar) { + // Construct a datetime literal + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000"); + dateFormat.setCalendar(calendar); + return String.format("datetime '%s'", dateFormat.format(date)); + } + + @Override + public String localDateAsSqlFunction(Date date) { + // Construct a datetime literal + return String.format("datetime '%s'", date.toString()); + } + + @Override + public String sequenceOptions() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean autoCommitOnly() { + return true; + } + }; + + public static Flavor fromJdbcUrl(String url) { + if (url == null) { + throw new DatabaseException("url must not be null"); + } + if (url.startsWith("jdbc:postgresql:")) { + return postgresql; + } else if (url.startsWith("jdbc:oracle:")) { + return oracle; + } else if (url.startsWith("jdbc:sqlserver:")) { + return sqlserver; + } else if (url.startsWith("jdbc:hsqldb:")) { + return hsqldb; + } else if (url.startsWith("jdbc:derby:")) { + return derby; + } else if (url.startsWith("jdbc:bigquery:")) { + return bigquery; + } else { + throw new DatabaseException("Cannot determine database flavor from url"); + } + } + + public static String driverForJdbcUrl(String url) { + if (url == null) { + throw new DatabaseException("url must not be null"); + } + if (url.startsWith("jdbc:postgresql:")) { + return "org.postgresql.Driver"; + } else if (url.startsWith("jdbc:oracle:")) { + return "oracle.jdbc.OracleDriver"; + } else if (url.startsWith("jdbc:sqlserver:")) { + return "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + } else if (url.startsWith("jdbc:hsqldb:")) { + return "org.hsqldb.jdbc.JDBCDriver"; + } else if (url.startsWith("jdbc:derby:")) { + return "org.apache.derby.jdbc.EmbeddedDriver"; + } else if (url.startsWith("jdbc:bigquery:")) { + return "com.simba.googlebigquery.jdbc42.Driver"; + } else { + throw new DatabaseException("Cannot determine database driver class from url"); + } + } + + // Returns true if DB normalizes to upper case names for ids like tables and columns + // See https://github.com/ontop/ontop/wiki/Case-sensitivity-for-SQL-identifiers + public abstract boolean isNormalizedUpperCase(); + + public abstract String typeInteger(); + + public abstract String typeBoolean(); + + public abstract String typeLong(); + + public abstract String typeFloat(); + + public abstract String typeDouble(); + + public abstract String typeBigDecimal(int size, int precision); + + public abstract String typeStringVar(int length); + + public abstract String typeStringFixed(int length); + + public abstract String typeClob(); + + public abstract String typeBlob(); + + public abstract String typeDate(); + + public abstract String typeLocalDate(); + + public abstract boolean useStringForClob(); + + public abstract boolean useBytesForBlob(); + + public abstract String sequenceNextVal(String sequenceName); + + public abstract String sequenceSelectNextVal(String sequenceName); + + public abstract String sequenceDrop(String dbtestSeq); + + public abstract boolean supportsInsertReturning(); + + public abstract String dbTimeMillis(); + + public abstract String sequenceCacheClause(int nbrValuesToCache); + + public abstract String sequenceOrderClause(boolean order); + + public abstract String sequenceCycleClause(boolean cycle); + + /** + * Indicate what should follow a constant select statement. For example, "select 1" + * works on some databases, while Oracle requires "select 1 from dual". For Oracle + * this function should return " from dual" (including the leading space). + */ + public abstract String fromAny(); + + /** + * Return a SQL function representing the specified date. For example, in PostgreSQL this + * looks like "'1970-01-02 02:17:36.789000 GMT'::timestamp". + */ + public abstract String dateAsSqlFunction(Date date, Calendar calendar); + + /** + * Return a SQL function representing the specified date without time. + */ + public abstract String localDateAsSqlFunction(Date date); + + public abstract String sequenceOptions(); + + public abstract boolean autoCommitOnly(); +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/MixedParameterSql.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/MixedParameterSql.java new file mode 100644 index 0000000..ab6c6c7 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/MixedParameterSql.java @@ -0,0 +1,133 @@ +package org.xbib.jdbc.query; + +import org.xbib.jdbc.query.util.RewriteArg; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Convenience class to allow use of (:mylabel) for SQL parameters in addition to + * positional (?) parameters. This doesn't do any smart parsing of the SQL, it is just + * looking for ':' and '?' characters. If the SQL needs to include an actual ':' or '?' + * character, use two of them ('::' or '??'), and they will be replaced with a + * single ':' or '?'. + */ +public class MixedParameterSql { + + private final String sqlToExecute; + + private final Object[] args; + + public MixedParameterSql(String sql, List positionalArgs, Map nameToArg) { + if (positionalArgs == null) { + positionalArgs = new ArrayList<>(); + } + if (nameToArg == null) { + nameToArg = new HashMap<>(); + } + + StringBuilder newSql = new StringBuilder(sql.length()); + List argNamesList = new ArrayList<>(); + List rewrittenArgs = new ArrayList<>(); + List argsList = new ArrayList<>(); + int searchIndex = 0; + int currentPositionalArg = 0; + while (searchIndex < sql.length()) { + int nextColonIndex = sql.indexOf(':', searchIndex); + int nextQmIndex = sql.indexOf('?', searchIndex); + + if (nextColonIndex < 0 && nextQmIndex < 0) { + newSql.append(sql.substring(searchIndex)); + break; + } + + if (nextColonIndex >= 0 && (nextQmIndex == -1 || nextColonIndex < nextQmIndex)) { + // The next parameter we found is a named parameter (":foo") + if (nextColonIndex > sql.length() - 2) { + // Probably illegal sql, but handle boundary condition + break; + } + + // Allow :: as escape for : + if (sql.charAt(nextColonIndex + 1) == ':') { + newSql.append(sql, searchIndex, nextColonIndex + 1); + searchIndex = nextColonIndex + 2; + continue; + } + + int endOfNameIndex = nextColonIndex + 1; + while (endOfNameIndex < sql.length() && Character.isJavaIdentifierPart(sql.charAt(endOfNameIndex))) { + endOfNameIndex++; + } + newSql.append(sql, searchIndex, nextColonIndex); + String paramName = sql.substring(nextColonIndex + 1, endOfNameIndex); + boolean secretParam = paramName.startsWith("secret"); + Object arg = nameToArg.get(paramName); + if (arg instanceof RewriteArg) { + newSql.append(((RewriteArg) arg).getSql()); + rewrittenArgs.add(paramName); + } else { + newSql.append('?'); + if (nameToArg.containsKey(paramName)) { + argsList.add(secretParam ? new SecretArg(arg) : arg); + } else { + throw new DatabaseException("The SQL requires parameter ':" + paramName + "' but no value was provided"); + } + argNamesList.add(paramName); + } + searchIndex = endOfNameIndex; + } else { + // The next parameter we found is a positional parameter ("?") + + // Allow ?? as escape for ? + if (nextQmIndex < sql.length() - 1 && sql.charAt(nextQmIndex + 1) == '?') { + newSql.append(sql, searchIndex, nextQmIndex + 1); + searchIndex = nextQmIndex + 2; + continue; + } + + newSql.append(sql, searchIndex, nextQmIndex); + if (currentPositionalArg >= positionalArgs.size()) { + throw new DatabaseException("Not enough positional parameters (" + positionalArgs.size() + ") were provided"); + } + if (positionalArgs.get(currentPositionalArg) instanceof RewriteArg) { + newSql.append(((RewriteArg) positionalArgs.get(currentPositionalArg)).getSql()); + } else { + newSql.append('?'); + argsList.add(positionalArgs.get(currentPositionalArg)); + } + currentPositionalArg++; + searchIndex = nextQmIndex + 1; + } + } + this.sqlToExecute = newSql.toString(); + args = argsList.toArray(new Object[argsList.size()]); + + // Sanity check number of arguments to provide a better error message + if (currentPositionalArg != positionalArgs.size()) { + throw new DatabaseException("Wrong number of positional parameters were provided (expected: " + + currentPositionalArg + ", actual: " + positionalArgs.size() + ")"); + } + if (nameToArg.size() > args.length - Math.max(0, positionalArgs.size() - 1) + rewrittenArgs.size()) { + Set unusedNames = new HashSet<>(nameToArg.keySet()); + unusedNames.removeAll(argNamesList); + unusedNames.removeAll(rewrittenArgs); + if (!unusedNames.isEmpty()) { + throw new DatabaseException("These named parameters do not exist in the query: " + unusedNames); + } + } + } + + public String getSqlToExecute() { + return sqlToExecute; + } + + public Object[] getArgs() { + return args; + } + +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Options.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Options.java new file mode 100644 index 0000000..9667b6b --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Options.java @@ -0,0 +1,130 @@ +package org.xbib.jdbc.query; + +import java.util.Calendar; +import java.util.Date; + +/** + * Control various optional behavior for the database interactions. + */ +public interface Options { + /** + * Control whether the Database object will allow calls to commitNow() + * and rollbackNow(). By default it will throw exceptions if you try to + * call those. + */ + boolean allowTransactionControl(); + + /** + * Useful for testing code that explicitly controls transactions, and you + * don't really want it to commit/rollback. Disabled by default, meaning + * calls will be allowed or throw exceptions depending on allowTransctionControl(). + * The value of allowTranscationControl() has no affect if this returns true. + */ + boolean ignoreTransactionControl(); + + /** + * Control whether the Database object will allow calls to underlyingConnection(). + * By default that method will throw an exception. + */ + boolean allowConnectionAccess(); + + /** + * If this is false, log messages will look something like: + * + *
+     *   ...select a from b where c=?
+     * 
+ *

+ * If this is true, log messages will look something like: + * + *

+     *   ...select a from b where c=?|select a from b where c='abc'
+     * 
+ * + * @return true if parameter values should be logged along with SQL, false otherwise + */ + boolean isLogParameters(); + + /** + * If true, text of the SQL and possibly parameter values (depending on @{#isLogParameters()}) + * will be included in exception messages. This can be very helpful for debugging, but poses + * some disclosure risks. + * + * @return true to add possibly sensitive data in exception messages, false otherwise + */ + boolean isDetailedExceptions(); + + /** + * In cases where exceptions are thrown, use this method to provide a common + * code that will be included in the exception message and the log message + * so they can be searched and correlated later. + * + * @return an arbitrary, fairly unique, speakable over the phone, without whitespace + */ + String generateErrorCode(); + + /** + * Indicate whether to use the Blob functionality of the underlying database driver, + * or whether to use setBytes() methods instead. Using Blobs is preferred, but is not + * supported by all drivers. + * + *

The default behavior of this method is to delegate to flavor().useBytesForBlob(), + * but it is provided on this interface so the behavior can be controlled. + * + * @return true to avoid using Blob functionality, false otherwise + */ + boolean useBytesForBlob(); + + /** + * Indicate whether to use the Clob functionality of the underlying database driver, + * or whether to use setString() methods instead. Using Clobs is preferred, but is not + * supported by all drivers. + * + *

The default behavior of this method is to delegate to flavor().useStringForClob(), + * but it is provided on this interface so the behavior can be controlled. + * + * @return true to avoid using Clob functionality, false otherwise + */ + boolean useStringForClob(); + + /** + * Access compatibility information for the underlying database. The + * Flavor class enumerates the known databases and tries to smooth over + * some of the variations in features and syntax. + */ + Flavor flavor(); + + /** + * The value returned by this method will be used for argDateNowPerApp() calls. It + * may also be used for argDateNowPerDb() calls if you have enabled that. + */ + Date currentDate(); + + /** + * Wherever argDateNowPerDb() is specified, use argDateNowPerApp() instead. This is + * useful for testing purposes as you can use OptionsOverride to provide your + * own system clock that will be used for time travel. + */ + boolean useDatePerAppOnly(); + + /** + * This calendar will be used for conversions when storing and retrieving timestamps + * from the database. By default this is the JVM default with TimeZone explicitly set + * to GMT (so timestamps will be stored in the database as GMT). + * + *

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

+ * + *

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

+ */ + Calendar calendarForTimestamps(); + + /** + * The maximum number of characters to print in debug SQL for a given String type + * insert/update/query parameter. If it exceeds this length, the parameter value + * will be truncated at the max and a "..." will be appended. Note this affects + * both {@code argString()} and {@code argClobString()} methods. + */ + int maxStringLengthParam(); +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsDefault.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsDefault.java new file mode 100644 index 0000000..44a4911 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsDefault.java @@ -0,0 +1,84 @@ +package org.xbib.jdbc.query; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +/** + * Control various optional behavior for the database interactions. + */ +public class OptionsDefault implements Options { + + private final Flavor flavor; + + public OptionsDefault(Flavor flavor) { + this.flavor = flavor; + } + + @Override + public boolean allowTransactionControl() { + return false; + } + + @Override + public boolean ignoreTransactionControl() { + return false; + } + + @Override + public boolean allowConnectionAccess() { + return false; + } + + @Override + public boolean isLogParameters() { + return false; + } + + @Override + public boolean isDetailedExceptions() { + return false; + } + + @Override + public String generateErrorCode() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd:H:m:s"); + return sdf.format(new Date()) + "-" + Math.round(Math.random() * 1000000); + } + + @Override + public boolean useBytesForBlob() { + return flavor().useBytesForBlob(); + } + + @Override + public boolean useStringForClob() { + return flavor().useStringForClob(); + } + + @Override + public Flavor flavor() { + return flavor; + } + + @Override + public Date currentDate() { + return new Date(); + } + + @Override + public boolean useDatePerAppOnly() { + return false; + } + + @Override + public Calendar calendarForTimestamps() { + return Calendar.getInstance(TimeZone.getDefault()); + } + + @Override + public int maxStringLengthParam() { + return 4000; + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsOverride.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsOverride.java new file mode 100644 index 0000000..6994a0a --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/OptionsOverride.java @@ -0,0 +1,108 @@ +package org.xbib.jdbc.query; + +import java.util.Calendar; +import java.util.Date; + +/** + * Base class for selectively overriding another Options object. + */ +public class OptionsOverride implements Options { + + private Options parent; + + /** + * Wrap another {@code Options} and defer to it for anything we choose not + * to override. + */ + public OptionsOverride(Options parent) { + this.parent = parent; + } + + /** + * Defer to OptionsDefault for anything that is not specified, and use postgresql flavor. + */ + public OptionsOverride() { + parent = new OptionsDefault(Flavor.postgresql); + } + + /** + * Defer to OptionsDefault for anything that is not specified, using the specified flavor. + */ + public OptionsOverride(Flavor flavor) { + parent = new OptionsDefault(flavor); + } + + public void setParent(Options parent) { + this.parent = parent; + } + + public OptionsOverride withParent(Options parent) { + this.parent = parent; + return this; + } + + @Override + public boolean allowTransactionControl() { + return parent.allowTransactionControl(); + } + + @Override + public boolean ignoreTransactionControl() { + return parent.ignoreTransactionControl(); + } + + @Override + public boolean allowConnectionAccess() { + return parent.allowConnectionAccess(); + } + + @Override + public boolean isLogParameters() { + return parent.isLogParameters(); + } + + @Override + public boolean isDetailedExceptions() { + return parent.isDetailedExceptions(); + } + + @Override + public String generateErrorCode() { + return parent.generateErrorCode(); + } + + @Override + public boolean useBytesForBlob() { + return parent.useBytesForBlob(); + } + + @Override + public boolean useStringForClob() { + return parent.useStringForClob(); + } + + @Override + public Flavor flavor() { + return parent.flavor(); + } + + @Override + public Date currentDate() { + return parent.currentDate(); + } + + @Override + public boolean useDatePerAppOnly() { + return parent.useDatePerAppOnly(); + } + + @Override + public Calendar calendarForTimestamps() { + return parent.calendarForTimestamps(); + } + + @Override + public int maxStringLengthParam() { + return parent.maxStringLengthParam(); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/QueryTimedOutException.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/QueryTimedOutException.java new file mode 100644 index 0000000..1ba9796 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/QueryTimedOutException.java @@ -0,0 +1,13 @@ +package org.xbib.jdbc.query; + +/** + * Thrown when a query is interrupted because a timeout was exceeded or it was + * explicitly cancelled. + */ +@SuppressWarnings("serial") +public class QueryTimedOutException extends DatabaseException { + + public QueryTimedOutException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Row.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Row.java new file mode 100644 index 0000000..3f99ebe --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Row.java @@ -0,0 +1,435 @@ +package org.xbib.jdbc.query; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.sql.ResultSetMetaData; +import java.time.LocalDate; +import java.util.Date; + +/** + * Interface for reading results from a database query. + */ +public interface Row { + /** + * Obtain the names of the columns in the database. You probably want to + * avoid this method if possible, as the way column names are handled varies + * by database and driver. For example, Derby and Oracle normally convert + * column names to uppercase, while PostgreSQL normally converts column + * names to lowercase. If you do use this method, you might want to either + * call toUppercase()/toLowercase() or ensure the SQL explicitly specifies + * parameters with AS "FOO" (including quotes) to ensure your desired name + * will be honored. + */ + String[] getColumnLabels(); + + /** + * Get raw access to the underlying JDBC metadata. + */ + ResultSetMetaData getMetadata(); + + /** + * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database, + * typically in a {@code CHAR(1)} column. This reads the value and converts it + * to {@code Boolean} or {@code null} as appropriate. + * + *

This is a short-hand method that reads columns in order, starting + * with the first, and automatically incrementing the column index.

+ * + *

If you call one of the methods using an explicit column index or column name before + * calling this method, it will pick up at the next column following the explicit one. + * For example:

+ * + *
+     * getX();  // column 1
+     * getX(5); // or getX("foo") if foo is column 5
+     * getX();  // column 6
+     * 
+ * + * @return true if the value was "Y", false if it was "N", or null + * @throws DatabaseException if the value was something other than Y, N, or null + */ + Boolean getBooleanOrNull(); + + /** + * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database, + * typically in a {@code CHAR(1)} column. This reads the value and converts it + * to {@code Boolean} or {@code null} as appropriate. + * + * @param columnOneBased column number to read (1 is the first column) + * @return true if the value was "Y", false if it was "N", or null + * @throws DatabaseException if the value was something other than Y, N, or null + */ + Boolean getBooleanOrNull(int columnOneBased); + + /** + * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database, + * typically in a {@code CHAR(1)} column. This reads the value and converts it + * to {@code Boolean} or {@code null} as appropriate. + * + * @param columnName SQL alias of the column to read (use all lowercase) + * @return true if the value was "Y", false if it was "N", or null + * @throws DatabaseException if the value was something other than Y, N, or null + */ + Boolean getBooleanOrNull(String columnName); + + /** + * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database, + * typically in a {@code CHAR(1)} column. This reads the value and converts it + * to a {@code boolean}. If the value is {@code null}, it will be converted to {@code false}. + * + *

This is a short-hand method that reads columns in order, starting + * with the first, and automatically incrementing the column index.

+ * + *

If you call one of the methods using an explicit column index or column name before + * calling this method, it will pick up at the next column following the explicit one. + * For example:

+ * + *
+     * getX();  // column 1
+     * getX(5); // or getX("foo") if foo is column 5
+     * getX();  // column 6
+     * 
+ * + * @return true if the value was "Y", false if it was either "N" or null + * @throws DatabaseException if the value was something other than Y, N, or null + */ + boolean getBooleanOrFalse(); + + /** + * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database, + * typically in a {@code CHAR(1)} column. This reads the value and converts it + * to a {@code boolean}. If the value is {@code null}, it will be converted to {@code false}. + * + *

This is a short-hand method that reads columns in order, starting + * with the first, and automatically incrementing the column index.

+ * + * @param columnOneBased column number to read (1 is the first column) + * @return true if the value was "Y", false if it was either "N" or null + * @throws DatabaseException if the value was something other than Y, N, or null + */ + boolean getBooleanOrFalse(int columnOneBased); + + /** + * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database, + * typically in a {@code CHAR(1)} column. This reads the value and converts it + * to a {@code boolean}. If the value is {@code null}, it will be converted to {@code false}. + * + *

This is a short-hand method that reads columns in order, starting + * with the first, and automatically incrementing the column index.

+ * + * @param columnName SQL alias of the column to read (use all lowercase) + * @return true if the value was "Y", false if it was either "N" or null + * @throws DatabaseException if the value was something other than Y, N, or null + */ + boolean getBooleanOrFalse(String columnName); + + /** + * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database, + * typically in a {@code CHAR(1)} column. This reads the value and converts it + * to a {@code boolean}. If the value is {@code null}, it will be converted to {@code true}. + * + *

This is a short-hand method that reads columns in order, starting + * with the first, and automatically incrementing the column index.

+ * + *

If you call one of the methods using an explicit column index or column name before + * calling this method, it will pick up at the next column following the explicit one. + * For example:

+ * + *
+     * getX();  // column 1
+     * getX(5); // or getX("foo") if foo is column 5
+     * getX();  // column 6
+     * 
+ * + * @return true if the value was either "Y" or null, false if it was "N" + * @throws DatabaseException if the value was something other than Y, N, or null + */ + boolean getBooleanOrTrue(); + + /** + * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database, + * typically in a {@code CHAR(1)} column. This reads the value and converts it + * to a {@code boolean}. If the value is {@code null}, it will be converted to {@code true}. + * + *

This is a short-hand method that reads columns in order, starting + * with the first, and automatically incrementing the column index.

+ * + * @param columnOneBased column number to read (1 is the first column) + * @return true if the value was either "Y" or null, false if it was "N" + * @throws DatabaseException if the value was something other than Y, N, or null + */ + boolean getBooleanOrTrue(int columnOneBased); + + /** + * Boolean values are represented as strings {@code "Y"} or {@code "N"} in the database, + * typically in a {@code CHAR(1)} column. This reads the value and converts it + * to a {@code boolean}. If the value is {@code null}, it will be converted to {@code true}. + * + *

This is a short-hand method that reads columns in order, starting + * with the first, and automatically incrementing the column index.

+ * + * @param columnName SQL alias of the column to read (use all lowercase) + * @return true if the value was either "Y" or null, false if it was "N" + * @throws DatabaseException if the value was something other than Y, N, or null + */ + boolean getBooleanOrTrue(String columnName); + + Integer getIntegerOrNull(); + + Integer getIntegerOrNull(int columnOneBased); + + Integer getIntegerOrNull(String columnName); + + int getIntegerOrZero(); + + int getIntegerOrZero(int columnOneBased); + + int getIntegerOrZero(String columnName); + + Long getLongOrNull(); + + Long getLongOrNull(int columnOneBased); + + Long getLongOrNull(String columnName); + + long getLongOrZero(); + + long getLongOrZero(int columnOneBased); + + long getLongOrZero(String columnName); + + Float getFloatOrNull(); + + Float getFloatOrNull(int columnOneBased); + + Float getFloatOrNull(String columnName); + + float getFloatOrZero(); + + float getFloatOrZero(int columnOneBased); + + float getFloatOrZero(String columnName); + + Double getDoubleOrNull(); + + Double getDoubleOrNull(int columnOneBased); + + Double getDoubleOrNull(String columnName); + + double getDoubleOrZero(); + + double getDoubleOrZero(int columnOneBased); + + double getDoubleOrZero(String columnName); + + /** + * Note this method attempts to correct for "artifical" scale due to the database + * representation. Some databases will pad the number out to "full precision". This + * method tries to reduce scale if there is zero padding to the right of the decimal. + */ + + BigDecimal getBigDecimalOrNull(); + + + BigDecimal getBigDecimalOrNull(int columnOneBased); + + + BigDecimal getBigDecimalOrNull(String columnName); + + + BigDecimal getBigDecimalOrZero(); + + + BigDecimal getBigDecimalOrZero(int columnOneBased); + + + BigDecimal getBigDecimalOrZero(String columnName); + + /** + * @return the value, or null if it is SQL null; never returns the empty string + */ + + String getStringOrNull(); + + /** + * @return the value, or null if it is SQL null; never returns the empty string + */ + + String getStringOrNull(int columnOneBased); + + /** + * @return the value, or null if it is SQL null; never returns the empty string + */ + + String getStringOrNull(String columnName); + + /** + * @return the value, or the empty string if it is SQL null; never returns null + */ + + String getStringOrEmpty(); + + /** + * @return the value, or the empty string if it is SQL null; never returns null + */ + + String getStringOrEmpty(int columnOneBased); + + /** + * @return the value, or the empty string if it is SQL null; never returns null + */ + + String getStringOrEmpty(String columnName); + + /** + * @return the value, or null if it is SQL null; never returns the empty string + */ + + String getClobStringOrNull(); + + /** + * @return the value, or null if it is SQL null; never returns the empty string + */ + + String getClobStringOrNull(int columnOneBased); + + /** + * @return the value, or null if it is SQL null; never returns the empty string + */ + + String getClobStringOrNull(String columnName); + + /** + * @return the value, or the empty string if it is SQL null; never returns null + */ + + String getClobStringOrEmpty(); + + /** + * @return the value, or the empty string if it is SQL null; never returns null + */ + + String getClobStringOrEmpty(int columnOneBased); + + /** + * @return the value, or the empty string if it is SQL null; never returns null + */ + + String getClobStringOrEmpty(String columnName); + + /** + * @return the value, or null if it is SQL null + */ + + Reader getClobReaderOrNull(); + + /** + * @return the value, or null if it is SQL null + */ + + Reader getClobReaderOrNull(int columnOneBased); + + /** + * @return the value, or null if it is SQL null + */ + + Reader getClobReaderOrNull(String columnName); + + /** + * @return the value, or a StringReader containing the empty string if it is SQL null + */ + + Reader getClobReaderOrEmpty(); + + /** + * @return the value, or a StringReader containing the empty string if it is SQL null + */ + + Reader getClobReaderOrEmpty(int columnOneBased); + + /** + * @return the value, or a StringReader containing the empty string if it is SQL null + */ + + Reader getClobReaderOrEmpty(String columnName); + + + byte[] getBlobBytesOrNull(); + + + byte[] getBlobBytesOrNull(int columnOneBased); + + + byte[] getBlobBytesOrNull(String columnName); + + + byte[] getBlobBytesOrZeroLen(); + + + byte[] getBlobBytesOrZeroLen(int columnOneBased); + + + byte[] getBlobBytesOrZeroLen(String columnName); + + + InputStream getBlobInputStreamOrNull(); + + + InputStream getBlobInputStreamOrNull(int columnOneBased); + + + InputStream getBlobInputStreamOrNull(String columnName); + + + InputStream getBlobInputStreamOrEmpty(); + + + InputStream getBlobInputStreamOrEmpty(int columnOneBased); + + + InputStream getBlobInputStreamOrEmpty(String columnName); + + /** + * Return the millisecond precision Date, which should be represented as a TIMESTAMP + * in the database. The nanoseconds are truncated. + */ + + Date getDateOrNull(); + + /** + * Return the millisecond precision Date, which should be represented as a TIMESTAMP + * in the database. The nanoseconds are truncated. + */ + + Date getDateOrNull(int columnOneBased); + + + Date getDateOrNull(String columnName); + + /** + * Retrieve column as LocalDate, .i.e, date with no time. + * + * @return LocalDate of the database column value + */ + + LocalDate getLocalDateOrNull(); + + /** + * Get the Date field, with no timestamp + * + * @param columnOneBased column number starting at 1, not 0 + * @return LocalDate of the column value + */ + + LocalDate getLocalDateOrNull(int columnOneBased); + + /** + * Get the Date field, with no timestamp + * + * @param columnName column name to retrieve + * @return LocalDate of the column value + */ + + LocalDate getLocalDateOrNull(String columnName); +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/RowHandler.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/RowHandler.java new file mode 100644 index 0000000..47c13bb --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/RowHandler.java @@ -0,0 +1,8 @@ +package org.xbib.jdbc.query; + +/** + * Type-safe callback to read query results. + */ +public interface RowHandler { + T process(Row r) throws Exception; +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Rows.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Rows.java new file mode 100644 index 0000000..595be05 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Rows.java @@ -0,0 +1,8 @@ +package org.xbib.jdbc.query; + +/** + * Interface for reading results from a database query. + */ +public interface Rows extends Row { + boolean next(); +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/RowsAdaptor.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/RowsAdaptor.java new file mode 100644 index 0000000..4b66444 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/RowsAdaptor.java @@ -0,0 +1,835 @@ +package org.xbib.jdbc.query; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.util.Date; + +/** + * Safely wrap a ResultSet and provide access to the data it contains. + */ +class RowsAdaptor implements Rows { + + private final ResultSet rs; + + private final Options options; + + private int column = 1; + + public RowsAdaptor(ResultSet rs, Options options) { + this.rs = rs; + this.options = options; + } + + @Override + public boolean next() { + try { + column = 1; + return rs.next(); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + + @Override + public String[] getColumnLabels() { + try { + ResultSetMetaData metaData = rs.getMetaData(); + String[] names = new String[metaData.getColumnCount()]; + for (int i = 0; i < names.length; i++) { + names[i] = metaData.getColumnLabel(i + 1); + } + return names; + } catch (SQLException e) { + throw new DatabaseException("Unable to retrieve metadata from ResultSet", e); + } + } + + + @Override + public ResultSetMetaData getMetadata() { + try { + return rs.getMetaData(); + } catch (SQLException e) { + throw new DatabaseException("Unable to retrieve metadata from ResultSet", e); + } + } + + + @Override + public Boolean getBooleanOrNull() { + return getBooleanOrNull(column++); + } + + + @Override + public Boolean getBooleanOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + return toBoolean(rs, columnOneBased); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + + @Override + public Boolean getBooleanOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + return toBoolean(rs, columnName); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public boolean getBooleanOrFalse() { + return getBooleanOrFalse(column++); + } + + @Override + public boolean getBooleanOrFalse(int columnOneBased) { + Boolean result = getBooleanOrNull(columnOneBased); + if (result == null) { + result = Boolean.FALSE; + } + return result; + } + + @Override + public boolean getBooleanOrFalse(String columnName) { + Boolean result = getBooleanOrNull(columnName); + if (result == null) { + result = Boolean.FALSE; + } + return result; + } + + @Override + public boolean getBooleanOrTrue() { + return getBooleanOrTrue(column++); + } + + @Override + public boolean getBooleanOrTrue(int columnOneBased) { + Boolean result = getBooleanOrNull(columnOneBased); + if (result == null) { + result = Boolean.TRUE; + } + return result; + } + + @Override + public boolean getBooleanOrTrue(String columnName) { + Boolean result = getBooleanOrNull(columnName); + if (result == null) { + result = Boolean.TRUE; + } + return result; + } + + + @Override + public Integer getIntegerOrNull() { + return getIntegerOrNull(column++); + } + + @Override + public Integer getIntegerOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + return toInteger(rs, columnOneBased); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public Integer getIntegerOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + return toInteger(rs, columnName); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public int getIntegerOrZero() { + return getIntegerOrZero(column++); + } + + @Override + public int getIntegerOrZero(int columnOneBased) { + Integer result = getIntegerOrNull(columnOneBased); + if (result == null) { + result = 0; + } + return result; + } + + @Override + public int getIntegerOrZero(String columnName) { + Integer result = getIntegerOrNull(columnName); + if (result == null) { + result = 0; + } + return result; + } + + + @Override + public Long getLongOrNull() { + return getLongOrNull(column++); + } + + @Override + public Long getLongOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + return toLong(rs, columnOneBased); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public Long getLongOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + return toLong(rs, columnName); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public long getLongOrZero() { + return getLongOrZero(column++); + } + + @Override + public long getLongOrZero(int columnOneBased) { + Long result = getLongOrNull(columnOneBased); + if (result == null) { + result = 0L; + } + return result; + } + + @Override + public long getLongOrZero(String columnName) { + Long result = getLongOrNull(columnName); + if (result == null) { + result = 0L; + } + return result; + } + + + @Override + public Float getFloatOrNull() { + return getFloatOrNull(column++); + } + + @Override + public Float getFloatOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + return toFloat(rs, columnOneBased); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public Float getFloatOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + return toFloat(rs, columnName); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public float getFloatOrZero() { + return getFloatOrZero(column++); + } + + @Override + public float getFloatOrZero(int columnOneBased) { + Float result = getFloatOrNull(columnOneBased); + if (result == null) { + result = 0f; + } + return result; + } + + @Override + public float getFloatOrZero(String columnName) { + Float result = getFloatOrNull(columnName); + if (result == null) { + result = 0f; + } + return result; + } + + + @Override + public Double getDoubleOrNull() { + return getDoubleOrNull(column++); + } + + @Override + public Double getDoubleOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + return toDouble(rs, columnOneBased); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public Double getDoubleOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + return toDouble(rs, columnName); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public double getDoubleOrZero() { + return getDoubleOrZero(column++); + } + + @Override + public double getDoubleOrZero(int columnOneBased) { + Double result = getDoubleOrNull(columnOneBased); + if (result == null) { + result = 0d; + } + return result; + } + + @Override + public double getDoubleOrZero(String columnName) { + Double result = getDoubleOrNull(columnName); + if (result == null) { + result = 0d; + } + return result; + } + + + @Override + public BigDecimal getBigDecimalOrNull() { + return getBigDecimalOrNull(column++); + } + + @Override + public BigDecimal getBigDecimalOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + return toBigDecimal(rs, columnOneBased); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public BigDecimal getBigDecimalOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + return toBigDecimal(rs, columnName); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + + @Override + public BigDecimal getBigDecimalOrZero() { + return getBigDecimalOrZero(column++); + } + + + @Override + public BigDecimal getBigDecimalOrZero(int columnOneBased) { + BigDecimal result = getBigDecimalOrNull(columnOneBased); + if (result == null) { + result = BigDecimal.ZERO; + } + return result; + } + + + @Override + public BigDecimal getBigDecimalOrZero(String columnName) { + BigDecimal result = getBigDecimalOrNull(columnName); + if (result == null) { + result = BigDecimal.ZERO; + } + return result; + } + + + @Override + public String getStringOrNull() { + return getStringOrNull(column++); + } + + @Override + public String getStringOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + String result = rs.getString(columnOneBased); + if (result != null && result.length() == 0) { + result = null; + } + return result; + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public String getStringOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + String result = rs.getString(columnName); + if (result != null && result.length() == 0) { + result = null; + } + return result; + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + + @Override + public String getStringOrEmpty() { + return getStringOrEmpty(column++); + } + + + @Override + public String getStringOrEmpty(int columnOneBased) { + String result = getStringOrNull(columnOneBased); + if (result == null) { + result = ""; + } + return result; + } + + + @Override + public String getStringOrEmpty(String columnName) { + String result = getStringOrNull(columnName); + if (result == null) { + result = ""; + } + return result; + } + + + @Override + public String getClobStringOrNull() { + return getClobStringOrNull(column++); + } + + @Override + public String getClobStringOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + String result = rs.getString(columnOneBased); + if (result != null && result.length() == 0) { + result = null; + } + return result; + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public String getClobStringOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + String result = rs.getString(columnName); + if (result != null && result.length() == 0) { + result = null; + } + return result; + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + + @Override + public String getClobStringOrEmpty() { + return getClobStringOrEmpty(column++); + } + + + @Override + public String getClobStringOrEmpty(int columnOneBased) { + String result = getClobStringOrNull(columnOneBased); + if (result == null) { + result = ""; + } + return result; + } + + + @Override + public String getClobStringOrEmpty(String columnName) { + String result = getClobStringOrNull(columnName); + if (result == null) { + result = ""; + } + return result; + } + + + @Override + public Reader getClobReaderOrNull() { + return getClobReaderOrNull(column++); + } + + @Override + public Reader getClobReaderOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + return rs.getCharacterStream(columnOneBased); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public Reader getClobReaderOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + return rs.getCharacterStream(columnName); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + + @Override + public Reader getClobReaderOrEmpty() { + return getClobReaderOrEmpty(column++); + } + + + @Override + public Reader getClobReaderOrEmpty(int columnOneBased) { + Reader result = getClobReaderOrNull(columnOneBased); + if (result == null) { + result = new StringReader(""); + } + return result; + } + + + @Override + public Reader getClobReaderOrEmpty(String columnName) { + Reader result = getClobReaderOrNull(columnName); + if (result == null) { + result = new StringReader(""); + } + return result; + } + + + @Override + public byte[] getBlobBytesOrNull() { + return getBlobBytesOrNull(column++); + } + + @Override + public byte[] getBlobBytesOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + return rs.getBytes(columnOneBased); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public byte[] getBlobBytesOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + return rs.getBytes(columnName); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + + @Override + public byte[] getBlobBytesOrZeroLen() { + return getBlobBytesOrZeroLen(column++); + } + + + @Override + public byte[] getBlobBytesOrZeroLen(int columnOneBased) { + byte[] result = getBlobBytesOrNull(columnOneBased); + if (result == null) { + result = new byte[0]; + } + return result; + } + + + @Override + public byte[] getBlobBytesOrZeroLen(String columnName) { + byte[] result = getBlobBytesOrNull(columnName); + if (result == null) { + result = new byte[0]; + } + return result; + } + + + @Override + public InputStream getBlobInputStreamOrNull() { + return getBlobInputStreamOrNull(column++); + } + + @Override + public InputStream getBlobInputStreamOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + return rs.getBinaryStream(columnOneBased); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + @Override + public InputStream getBlobInputStreamOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + return rs.getBinaryStream(columnName); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + + @Override + public InputStream getBlobInputStreamOrEmpty() { + return getBlobInputStreamOrEmpty(column++); + } + + + @Override + public InputStream getBlobInputStreamOrEmpty(int columnOneBased) { + InputStream result = getBlobInputStreamOrNull(columnOneBased); + if (result == null) { + result = new ByteArrayInputStream(new byte[0]); + } + return result; + } + + + @Override + public InputStream getBlobInputStreamOrEmpty(String columnName) { + InputStream result = getBlobInputStreamOrNull(columnName); + if (result == null) { + result = new ByteArrayInputStream(new byte[0]); + } + return result; + } + + + @Override + public Date getDateOrNull() { + return getDateOrNull(column++); + } + + + @Override + public Date getDateOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + return toDate(rs, columnOneBased); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + + @Override + public Date getDateOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + return toDate(rs, columnName); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + + @Override + public LocalDate getLocalDateOrNull() { + return getLocalDateOrNull(column++); + } + + + @Override + public LocalDate getLocalDateOrNull(int columnOneBased) { + try { + column = columnOneBased + 1; + return toLocalDate(rs, columnOneBased); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + + @Override + public LocalDate getLocalDateOrNull(String columnName) { + try { + column = rs.findColumn(columnName) + 1; + return toLocalDate(rs, columnName); + } catch (SQLException e) { + throw new DatabaseException(e); + } + } + + /** + * Make sure the Timestamp will return getTime() accurate to the millisecond + * (if possible) and truncate away nanoseconds. + */ + private Date timestampToDate(Timestamp ts) { + long millis = ts.getTime(); + int nanos = ts.getNanos(); + return new Date(millis / 1000 * 1000 + nanos / 1000000); + } + + private Date toDate(ResultSet rs, int col) throws SQLException { + Timestamp val = rs.getTimestamp(col, options.calendarForTimestamps()); + return val == null ? null : timestampToDate(val); + } + + private Date toDate(ResultSet rs, String col) throws SQLException { + Timestamp val = rs.getTimestamp(col, options.calendarForTimestamps()); + return val == null ? null : timestampToDate(val); + } + + private LocalDate toLocalDate(ResultSet rs, int col) throws SQLException { + java.sql.Date val = rs.getDate(col); + return val == null ? null : val.toLocalDate(); + } + + private LocalDate toLocalDate(ResultSet rs, String col) throws SQLException { + java.sql.Date val = rs.getDate(col); + return val == null ? null : val.toLocalDate(); + } + + private Boolean toBoolean(ResultSet rs, int col) throws SQLException { + String val = rs.getString(col); + if (val == null) { + return null; + } else if (val.equals("Y") || val.equals("1")) { + return Boolean.TRUE; + } else if (val.equals("N") || val.equals("0")) { + return Boolean.FALSE; + } else { + throw new DatabaseException("Reading boolean from column " + col + " but the value was not 'Y' or 'N'"); + } + } + + private Boolean toBoolean(ResultSet rs, String col) throws SQLException { + String val = rs.getString(col); + if (val == null) { + return null; + } else if (val.equals("Y") || val.equals("1")) { + return Boolean.TRUE; + } else if (val.equals("N") || val.equals("0")) { + return Boolean.FALSE; + } else { + throw new DatabaseException("Reading boolean from column \"" + col + "\" but the value was not 'Y' or 'N'"); + } + } + + private Integer toInteger(ResultSet rs, int col) throws SQLException { + int val = rs.getInt(col); + return rs.wasNull() ? null : val; + } + + private Integer toInteger(ResultSet rs, String col) throws SQLException { + int val = rs.getInt(col); + return rs.wasNull() ? null : val; + } + + private Long toLong(ResultSet rs, int col) throws SQLException { + long val = rs.getLong(col); + return rs.wasNull() ? null : val; + } + + private Long toLong(ResultSet rs, String col) throws SQLException { + long val = rs.getLong(col); + return rs.wasNull() ? null : val; + } + + private Float toFloat(ResultSet rs, int col) throws SQLException { + float val = rs.getFloat(col); + return rs.wasNull() ? null : val; + } + + private Float toFloat(ResultSet rs, String col) throws SQLException { + float val = rs.getFloat(col); + return rs.wasNull() ? null : val; + } + + private Double toDouble(ResultSet rs, int col) throws SQLException { + double val = rs.getDouble(col); + return rs.wasNull() ? null : val; + } + + private Double toDouble(ResultSet rs, String col) throws SQLException { + double val = rs.getDouble(col); + return rs.wasNull() ? null : val; + } + + private BigDecimal fixBigDecimal(BigDecimal val) { + if (val.scale() > 0) { + val = val.stripTrailingZeros(); + if (val.scale() < 0) { + val = val.setScale(0); + } + } + return val; + } + + private BigDecimal toBigDecimal(ResultSet rs, int col) throws SQLException { + BigDecimal val = rs.getBigDecimal(col); + return val == null ? null : fixBigDecimal(val); + } + + private BigDecimal toBigDecimal(ResultSet rs, String col) throws SQLException { + BigDecimal val = rs.getBigDecimal(col); + return val == null ? null : fixBigDecimal(val); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/RowsHandler.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/RowsHandler.java new file mode 100644 index 0000000..968229b --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/RowsHandler.java @@ -0,0 +1,9 @@ +package org.xbib.jdbc.query; + +/** + * Type-safe callback to read query results. + */ +@FunctionalInterface +public interface RowsHandler { + T process(Rows rs) throws Exception; +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Schema.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Schema.java new file mode 100644 index 0000000..0b5809c --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Schema.java @@ -0,0 +1,949 @@ +package org.xbib.jdbc.query; + +import org.xbib.jdbc.query.Schema.Table.Check; +import org.xbib.jdbc.query.Schema.Table.Column; +import org.xbib.jdbc.query.Schema.Table.ForeignKey; +import org.xbib.jdbc.query.Schema.Table.Index; +import org.xbib.jdbc.query.Schema.Table.Unique; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Java representation of a database schema with the various things it can contain. + */ +public class Schema { + + private final List tables = new ArrayList<>(); + + private final List sequences = new ArrayList<>(); + + private boolean indexForeignKeys = true; + + private String userTableName = "user_principal"; + + public Sequence addSequence(String name) { + Sequence sequence = new Sequence(name); + sequences.add(sequence); + return sequence; + } + + public Schema withoutForeignKeyIndexing() { + indexForeignKeys = false; + return this; + } + + /** + * Set the table to which the foreign key will be created for + * user change tracking ({@link Table#trackCreateTimeAndUser(String)} + * and {@link Table#trackUpdateTimeAndUser(String)}). + * + * @param userTableName the default table name containing users + */ + public Schema userTableName(String userTableName) { + this.userTableName = userTableName; + return this; + } + + public void validate() { + for (Table t : tables) { + t.validate(); + } + for (Sequence s : sequences) { + s.validate(); + } + } + + public Table addTable(String name) { + Table table = new Table(name); + tables.add(table); + return table; + } + + public Table addTableFromRow(String tableName, Row r) { + Table table = addTable(tableName); + try { + ResultSetMetaData metadata = r.getMetadata(); + + int columnCount = metadata.getColumnCount(); + String[] names = new String[columnCount]; + for (int i = 0; i < columnCount; i++) { + names[i] = metadata.getColumnName(i + 1); + } + names = SqlArgs.tidyColumnNames(names); + + for (int i = 0; i < columnCount; i++) { + int type = metadata.getColumnType(i + 1); + + switch (type) { + case Types.SMALLINT: + case Types.INTEGER: + table.addColumn(names[i]).asInteger(); + break; + case Types.BIGINT: + table.addColumn(names[i]).asLong(); + break; + case Types.REAL: + case 100: // Oracle proprietary it seems + table.addColumn(names[i]).asFloat(); + break; + case Types.DOUBLE: + case 101: // Oracle proprietary it seems + table.addColumn(names[i]).asDouble(); + break; + case Types.NUMERIC: + int precision1 = metadata.getPrecision(i + 1); + int scale = metadata.getScale(i + 1); + if (precision1 == 10 && scale == 0) { + // Oracle reports integer as numeric + table.addColumn(names[i]).asInteger(); + } else if (precision1 == 19 && scale == 0) { + // Oracle reports long as numeric + table.addColumn(names[i]).asLong(); + } else if (precision1 == 126 && scale == -127) { + // this clause was added to support ETL from MSSQL Server + table.addColumn(names[i]).asFloat(); + } else if (precision1 == 0 && scale == -127) { + // this clause was also added to support ETL from MSSQL Server + table.addColumn(names[i]).asInteger(); + } else { + table.addColumn(names[i]).asBigDecimal(precision1, scale); + } + break; + case Types.BINARY: + case Types.VARBINARY: + case Types.BLOB: + table.addColumn(names[i]).asBlob(); + break; + case Types.CLOB: + case Types.NCLOB: + table.addColumn(names[i]).asClob(); + break; + + // The date type is used for a true date - no time info. + // It must be checked before TimeStamp because sql dates are also + // recognized as sql timestamp. + case Types.DATE: + table.addColumn(names[i]).asLocalDate(); + break; + + // This is the type dates and times with time and time zone associated. + // Note that Oracle dates are always really Timestamps. + case Types.TIMESTAMP: + // Check if we really have a LocalDate implemented by the DB as a timestamp + if (metadata.getScale(i + 1) == 0) { + // If the scale is 0, this is a LocalDate (no time/timezone). + // Anything with a time/timezone will have a non-zero scale + table.addColumn(names[i]).asLocalDate(); + } else { + table.addColumn(names[i]).asDate(); + } + break; + + case Types.NVARCHAR: + case Types.VARCHAR: + int precision = metadata.getPrecision(i + 1); + if (precision >= 2147483647) { + // Postgres seems to report clobs are varchar(2147483647) + table.addColumn(names[i]).asClob(); + } else { + table.addColumn(names[i]).asString(precision); + } + break; + case Types.CHAR: + case Types.NCHAR: + table.addColumn(names[i]).asStringFixed(metadata.getPrecision(i + 1)); + break; + default: + throw new DatabaseException("Don't know what type to use for: " + type); + } + } + } catch (SQLException e) { + throw new DatabaseException("Unable to retrieve metadata from ResultSet", e); + } + return table; + } + + public void execute(Supplier db) { + executeOrPrint(db.get(), null); + } + + public String print(Flavor flavor) { + return executeOrPrint(null, flavor); + } + + private String executeOrPrint(Database db, Flavor flavor) { + validate(); + + if (flavor == null) { + flavor = db.flavor(); + } + StringBuilder script = new StringBuilder(); + + for (Table table : tables) { + Sql sql = new Sql(); + sql.append("create table ").append(table.name).append(" (\n"); + boolean first = true; + for (Column column : table.columns) { + if (first) { + first = false; + sql.append(" "); + } else { + sql.append(",\n "); + } + sql.append(rpad(column.name, 30)).append(" "); + switch (column.type) { + case Boolean: + sql.append(flavor.typeBoolean()); + break; + case Integer: + sql.append(flavor.typeInteger()); + break; + case Long: + sql.append(flavor.typeLong()); + break; + case Float: + sql.append(flavor.typeFloat()); + break; + case Double: + sql.append(flavor.typeDouble()); + break; + case BigDecimal: + sql.append(flavor.typeBigDecimal(column.scale, column.precision)); + break; + case StringVar: + sql.append(flavor.typeStringVar(column.scale)); + break; + case StringFixed: + sql.append(flavor.typeStringFixed(column.scale)); + break; + case Date: + sql.append(flavor.typeDate()); // Append a date with time + break; + case LocalDate: + sql.append(flavor.typeLocalDate()); // Append a true date - no time + break; + case Clob: + sql.append(flavor.typeClob()); + break; + case Blob: + sql.append(flavor.typeBlob()); + break; + } + if (column.notNull) { + sql.append(" not null"); + } + } + + if (table.primaryKey != null) { + sql.append(",\n constraint "); + sql.append(rpad(table.primaryKey.name, 30)); + sql.listStart(" primary key ("); + for (String name : table.primaryKey.columnNames) { + sql.listSeparator(", "); + sql.append(name); + } + sql.listEnd(")"); + } + + for (Unique u : table.uniques) { + sql.append(",\n constraint "); + sql.append(rpad(u.name, 30)); + sql.listStart(" unique ("); + for (String name : u.columnNames) { + sql.listSeparator(", "); + sql.append(name); + } + sql.listEnd(")"); + } + + for (Check check : table.checks) { + sql.append(",\n constraint "); + sql.append(rpad(check.name, 30)); + sql.append(" check ("); + sql.append(check.expression); + sql.append(")"); + } + + sql.append("\n)"); + if (table.customClauses.containsKey(flavor)) { + sql.append(" ").append(table.customClauses.get(flavor)); + } + executeOrPrint(sql, db, script); + sql = new Sql(); + + if (flavor == Flavor.oracle || flavor == Flavor.postgresql) { + if (table.comment != null) { + sql.append("comment on table "); + sql.append(table.name); + sql.append(" is \n'"); + sql.append(table.comment.replace("'", "''")); + sql.append("'"); + executeOrPrint(sql, db, script); + sql = new Sql(); + } + + for (Column c : table.columns) { + if (c.comment != null) { + sql.append("comment on column "); + sql.append(table.name); + sql.append("."); + sql.append(c.name); + sql.append(" is \n'"); + sql.append(c.comment.replace("'", "''")); + sql.append("'"); + executeOrPrint(sql, db, script); + sql = new Sql(); + } + } + } + } + + for (Table table : tables) { + for (ForeignKey fk : table.foreignKeys) { + Sql sql = new Sql(); + sql.append("alter table "); + sql.append(table.name); + sql.append(" add constraint "); + sql.append(fk.name); + sql.listStart("\n foreign key ("); + for (String name : fk.columnNames) { + sql.listSeparator(", "); + sql.append(name); + } + sql.listEnd(") references "); + sql.append(fk.foreignTable); + if (fk.onDeleteCascade) { + sql.append(" on delete cascade"); + } + executeOrPrint(sql, db, script); + } + } + + for (Table table : tables) { + for (Index index : table.indexes) { + Sql sql = new Sql(); + sql.append("create "); + if (index.unique) { + sql.append("unique "); + } + sql.append("index "); + sql.append(index.name); + sql.append(" on "); + sql.append(table.name); + sql.listStart(" ("); + for (String name : index.columnNames) { + sql.listSeparator(", "); + sql.append(name); + } + sql.listEnd(")"); + executeOrPrint(sql, db, script); + } + } + + for (Sequence sequence : sequences) { + Sql sql = new Sql(); + sql.append("create sequence "); + sql.append(sequence.name); + sql.append(flavor.sequenceOptions()); + sql.append(" minvalue "); + sql.append(sequence.min); + sql.append(" maxvalue "); + sql.append(sequence.max); + sql.append(" start with "); + sql.append(sequence.start); + sql.append(" increment by "); + sql.append(sequence.increment); + sql.append(flavor.sequenceCacheClause(sequence.cache)); + sql.append(flavor.sequenceOrderClause(sequence.order)); + sql.append(flavor.sequenceCycleClause(sequence.cycle)); + executeOrPrint(sql, db, script); + } + + if (db == null) { + return script.toString(); + } + return null; + } + + private void executeOrPrint(Sql sql, Database db, StringBuilder script) { + if (db != null) { + db.ddl(sql.toString()).execute(); + } else { + script.append(sql.toString()); + script.append(";\n\n"); + } + } + + private String toName(String name) { + name = name.toLowerCase().trim(); + + if (!name.matches("[a-z][a-z0-9_]{0,28}[a-z0-9]?")) { + throw new IllegalArgumentException("Identifier name should match pattern [a-z][a-z0-9_]{0,28}[a-z0-9]?"); + } + + return name; + } + + private String rpad(String s, int size) { + if (s.length() < size) { + s += " ".substring(0, size - s.length()); + } + return s; + } + + public enum ColumnType { + Integer, Long, Float, Double, BigDecimal, StringVar, StringFixed, Clob, Blob, Date, LocalDate, Boolean + } + + public class Sequence { + private final String name; + private long min = 1; + private long max = 999999999999999999L; + private int increment = 1; + private long start = 1; + private int cache = 1; + private boolean order; + private boolean cycle; + + public Sequence(String name) { + this.name = toName(name); + } + + public Sequence min(long min) { + if (start == this.min) { + start = min; + } + this.min = min; + return this; + } + + public Sequence max(long max) { + this.max = max; + return this; + } + + public Sequence increment(int increment) { + this.increment = increment; + return this; + } + + public Sequence start(long start) { + this.start = start; + return this; + } + + public Sequence cache(int cache) { + this.cache = cache < 2 ? 1 : cache; + return this; + } + + /** + * On databases that support it, indicate you want to strictly order the values returned + * from the sequence. This is generally NOT what you want, because it can dramatically + * reduce performance (requires locking and synchronization). Also keep in mind it doesn't + * guarantee there will not be gaps in the numbers handed out (nothing you can do will + * ever prevent that). + */ + public Sequence order() { + order = true; + return this; + } + + public Sequence cycle() { + cycle = true; + return this; + } + + private void validate() { + + } + + public Schema schema() { + validate(); + return Schema.this; + } + } + + public class Table { + private final String name; + private String comment; + private final List columns = new ArrayList<>(); + private PrimaryKey primaryKey; + private final List foreignKeys = new ArrayList<>(); + private final List indexes = new ArrayList<>(); + private final List checks = new ArrayList<>(); + private final List uniques = new ArrayList<>(); + private final Map customClauses = new HashMap<>(); + private boolean createTracking; + private String createTrackingFkName; + private String createTrackingFkTable; + private boolean updateTracking; + private String updateTrackingFkName; + private String updateTrackingFkTable; + private boolean updateSequence; + private boolean historyTable; + + public Table(String name) { + this.name = toName(name); + if (this.name.length() > 27) { + throw new RuntimeException("Table name should be 27 characters or less"); + } + } + + public void validate() { + if (columns.size() < 1) { + throw new RuntimeException("Table " + name + " needs at least one column"); + } + for (Column c : columns) { + c.validate(); + } + + if (primaryKey != null) { + primaryKey.validate(); + } + + for (ForeignKey fk : foreignKeys) { + fk.validate(); + } + + for (Check c : checks) { + c.validate(); + } + + for (Index i : indexes) { + i.validate(); + } + } + + public Schema schema() { + if (createTracking) { + addColumn("create_time").asDate().table(); + } + if (createTrackingFkName != null) { + addColumn("create_user").foreignKey(createTrackingFkName).references(createTrackingFkTable).table(); + } + if (updateTracking || updateSequence) { + addColumn("update_time").asDate().table(); + } + if (updateTrackingFkName != null) { + addColumn("update_user").foreignKey(updateTrackingFkName).references(updateTrackingFkTable).table(); + } + if (updateSequence) { + addColumn("update_sequence").asLong().table(); + } + // Avoid auto-indexing foreign keys if an index already exists (the first columns of the pk or explicit index) + if (indexForeignKeys) { + for (ForeignKey fk : foreignKeys) { + if (primaryKey != null && 0 == Collections.indexOfSubList(primaryKey.columnNames, fk.columnNames)) { + continue; + } + boolean skip = false; + for (Index i : indexes) { + if (0 == Collections.indexOfSubList(i.columnNames, fk.columnNames)) { + skip = true; + break; + } + } + if (!skip) { + addIndex(fk.name + "_ix", fk.columnNames.toArray(new String[fk.columnNames.size()])); + } + } + } + validate(); + if (historyTable) { + String historyTableName = name + "_history"; + if (historyTableName.length() > 27 && historyTableName.length() <= 30) { + historyTableName = name + "_hist"; + } + Table hist = Schema.this.addTable(historyTableName); + // History table needs all the same columns as the original + hist.columns.addAll(columns); + // Add a synthetic column to indicate when the original row has been deleted + hist.addColumn("is_deleted").asBoolean().table(); + List pkColumns = new ArrayList<>(); + pkColumns.addAll(primaryKey.columnNames); + // Index the primary key from the regular table for retrieving history + hist.addIndex(historyTableName + "_ix", pkColumns.toArray(new String[pkColumns.size()])); + // The primary key for the history table will be that of the original table, plus the update sequence + pkColumns.add("update_sequence"); + hist.addPrimaryKey(historyTableName + "_pk", pkColumns.toArray(new String[pkColumns.size()])); + // To perform any validation + hist.schema(); + } + return Schema.this; + } + + public Table withComment(String comment) { + this.comment = comment; + return this; + } + + public Table withStandardPk() { + return addColumn(name + "_id").primaryKey().table(); + } + + public Table trackCreateTime() { + createTracking = true; + return this; + } + + public Table trackCreateTimeAndUser(String fkConstraintName) { + return trackCreateTimeAndUser(fkConstraintName, userTableName); + } + + public Table trackCreateTimeAndUser(String fkConstraintName, String fkReferencesTable) { + createTracking = true; + createTrackingFkName = fkConstraintName; + createTrackingFkTable = fkReferencesTable; + return this; + } + + public Table trackUpdateTime() { + updateTracking = true; + updateSequence = true; + return this; + } + + public Table trackUpdateTimeAndUser(String fkConstraintName) { + return trackUpdateTimeAndUser(fkConstraintName, userTableName); + } + + public Table trackUpdateTimeAndUser(String fkConstraintName, String fkReferencesTable) { + updateTracking = true; + updateSequence = true; + updateTrackingFkName = fkConstraintName; + updateTrackingFkTable = fkReferencesTable; + return this; + } + + public Table withHistoryTable() { + updateSequence = true; + historyTable = true; + return this; + } + + public Column addColumn(String name) { + Column column = new Column(name); + columns.add(column); + return column; + } + + public PrimaryKey addPrimaryKey(String name, String... columnNames) { + if (primaryKey != null) { + throw new RuntimeException("Only one primary key is allowed. For composite keys use" + + " addPrimaryKey(name, c1, c2, ...)."); + } + for (Column c : columns) { + if (c.name.equalsIgnoreCase(name)) { + throw new RuntimeException("For table: " + this.name + " primary key name should not be a column name: " + name); + } + } + primaryKey = new PrimaryKey(name, columnNames); + return primaryKey; + } + + public ForeignKey addForeignKey(String name, String... columnNames) { + ForeignKey foreignKey = new ForeignKey(name, columnNames); + foreignKeys.add(foreignKey); + return foreignKey; + } + + public Check addCheck(String name, String expression) { + Check check = new Check(name, expression); + checks.add(check); + return check; + } + + public Unique addUnique(String name, String... columnNames) { + Unique unique = new Unique(name, columnNames); + uniques.add(unique); + return unique; + } + + public Index addIndex(String name, String... columnNames) { + Index index = new Index(name, columnNames); + indexes.add(index); + return index; + } + + public Table customTableClause(Flavor flavor, String clause) { + customClauses.put(flavor, clause); + return this; + } + + public class PrimaryKey { + private final String name; + private final List columnNames = new ArrayList<>(); + + public PrimaryKey(String name, String[] columnNames) { + this.name = toName(name); + for (String s : columnNames) { + this.columnNames.add(toName(s)); + } + } + + public void validate() { + + } + + public Table table() { + validate(); + return Table.this; + } + } + + public class Unique { + private final String name; + private final List columnNames = new ArrayList<>(); + + public Unique(String name, String[] columnNames) { + this.name = toName(name); + for (String s : columnNames) { + this.columnNames.add(toName(s)); + } + } + + public void validate() { + + } + + public Table table() { + validate(); + return Table.this; + } + } + + public class ForeignKey { + private final String name; + private final List columnNames = new ArrayList<>(); + public String foreignTable; + private boolean onDeleteCascade = false; + + public ForeignKey(String name, String[] columnNames) { + this.name = toName(name); + for (String s : columnNames) { + this.columnNames.add(toName(s)); + } + } + + public ForeignKey references(String tableName) { + foreignTable = toName(tableName); + return this; + } + + public ForeignKey onDeleteCascade() { + onDeleteCascade = true; + return this; + } + + private void validate() { + if (foreignTable == null) { + throw new RuntimeException("Foreign key " + name + " must reference a table"); + } + } + + public Table table() { + validate(); + return Table.this; + } + } + + public class Check { + private final String name; + private final String expression; + + public Check(String name, String expression) { + this.name = toName(name); + this.expression = expression; + } + + private void validate() { + if (expression == null) { + throw new RuntimeException("Expression needed for check constraint " + name + " on table " + Table.this.name); + } + } + + public Table table() { + validate(); + return Table.this; + } + } + + public class Index { + private final String name; + private final List columnNames = new ArrayList<>(); + private boolean unique; + + public Index(String name, String[] columnNames) { + this.name = toName(name); + for (String s : columnNames) { + this.columnNames.add(toName(s)); + } + } + + public Index unique() { + unique = true; + return this; + } + + private void validate() { + if (columnNames.size() < 1) { + throw new RuntimeException("Index " + name + " needs at least one column"); + } + } + + public Table table() { + validate(); + return Table.this; + } + } + + public class Column { + private final String name; + private ColumnType type; + private int scale; + private int precision; + private boolean notNull; + private String comment; + + public Column(String name) { + this.name = toName(name); + } + + /** + * Create a boolean column, usually char(1) to hold values 'Y' or 'N'. This + * parameterless version does not create any check constraint at the database + * level. + */ + public Column asBoolean() { + return asType(ColumnType.Boolean); + } + + /** + * Create a boolean column, usually char(1) to hold values 'Y' or 'N'. This + * version creates a check constraint at the database level with the provided name. + */ + public Column asBoolean(String checkConstraintName) { + return asBoolean().check(checkConstraintName, name + " in ('Y', 'N')"); + } + + public Column asInteger() { + return asType(ColumnType.Integer); + } + + public Column asLong() { + return asType(ColumnType.Long); + } + + public Column asFloat() { + return asType(ColumnType.Float); + } + + public Column asDouble() { + return asType(ColumnType.Double); + } + + public Column asBigDecimal(int scale, int precision) { + this.scale = scale; + this.precision = precision; + return asType(ColumnType.BigDecimal); + } + + public Column asString(int scale) { + this.scale = scale; + return asType(ColumnType.StringVar); + } + + public Column asStringFixed(int scale) { + this.scale = scale; + return asType(ColumnType.StringFixed); + } + + // This type is for dates that have time associated + public Column asDate() { + return asType(ColumnType.Date); + } + + // This type is for true dates with no time associated + public Column asLocalDate() { + return asType(ColumnType.LocalDate); + } + + public Column asClob() { + return asType(ColumnType.Clob); + } + + public Column asBlob() { + return asType(ColumnType.Blob); + } + + private Column asType(ColumnType type) { + this.type = type; + return this; + } + + public Column notNull() { + this.notNull = true; + return this; + } + + private void validate() { + if (type == null) { + throw new RuntimeException("Call as*() on column " + name + " table " + Table.this.name); + } + } + + public Table table() { + validate(); + return Table.this; + } + + public ForeignKey foreignKey(String constraintName) { + if (type == null) { + asLong(); + } + return table().addForeignKey(constraintName, name); + } + + public Column check(String checkConstraintName, String expression) { + table().addCheck(checkConstraintName, expression).table(); + return this; + } + + public Column primaryKey() { + if (type == null) { + asLong(); + } + if (comment == null) { + comment = "Internally generated primary key"; + } + notNull(); + Table.this.addPrimaryKey(Table.this.name + "_pk", name); + return this; + } + + public Column unique(String constraintName) { + notNull(); + Table.this.addUnique(constraintName, name); + return this; + } + + public Column withComment(String comment) { + this.comment = comment; + return this; + } + + public Schema schema() { + return table().schema(); + } + } + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SecretArg.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SecretArg.java new file mode 100644 index 0000000..dcaa945 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SecretArg.java @@ -0,0 +1,19 @@ +package org.xbib.jdbc.query; + +public class SecretArg { + + private final Object arg; + + public SecretArg(Object arg) { + this.arg = arg; + } + + Object getArg() { + return arg; + } + + @Override + public String toString() { + return ""; + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Sql.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Sql.java new file mode 100644 index 0000000..091e159 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Sql.java @@ -0,0 +1,443 @@ +package org.xbib.jdbc.query; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Stack; + +/** + * This class is useful for dynamically generating SQL. It can "buffer" the + * various arg*() calls and replay them later via the apply(sqlArgs) methods. + */ +public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { + private final StringBuilder sql = new StringBuilder(); + private final Stack listFirstItem = new Stack<>(); + private List batched; + private SqlArgs sqlArgs = new SqlArgs(); + + public Sql() { + } + + public Sql(/*@Untainted*/ String sql) { + this.sql.append(sql); + } + + public static Sql insert(/*@Untainted*/ String table, SqlArgs args) { + return insert(table, Collections.singletonList(args)); + } + + public static Sql insert(/*@Untainted*/ String table, List args) { + Sql sql = null; + List expectedColumns = null; + + for (SqlArgs arg : args) { + if (arg.positionalCount() > 0) { + throw new DatabaseException("The SqlArgs must all be named to do this"); + } + List columns = arg.names(); + if (columns.size() < 1) { + throw new DatabaseException("You must add named arguments to SqlArgs"); + } + if (sql == null) { + expectedColumns = columns; + sql = new Sql("insert into ").append(table); + sql.listStart(" ("); + for (String column : columns) { + sql.listSeparator(","); + sql.append(column); + } + sql.listEnd(") values ("); + sql.appendQuestionMarks(columns.size()); + sql.append(")"); + } else { + if (!expectedColumns.equals(columns)) { + throw new DatabaseException("The columns for all rows in a batch must match. \nFirst: " + expectedColumns + + "\nCurrent: " + columns); + } + sql.batch(); + } + sql.setSqlArgs(arg.makePositional()); + } + return sql; + } + + public Sql setSqlArgs(SqlArgs args) { + sqlArgs = args; + return this; + } + + public Sql batch() { + if (sqlArgs.argCount() > 0) { + if (batched == null) { + batched = new ArrayList<>(); + } + batched.add(sqlArgs); + sqlArgs = new SqlArgs(); + } + return this; + } + + public int argCount() { + return sqlArgs.argCount(); + } + + public Sql appendQuestionMarks(int howMany) { + boolean first = true; + for (int i = 0; i < howMany; i++) { + if (first) { + first = false; + append("?"); + } else { + append(",?"); + } + } + return this; + } + + public Sql append(/*@Untainted*/ String sql) { + this.sql.append(sql); + return this; + } + + public Sql append(boolean value) { + this.sql.append(value); + return this; + } + + public Sql append(int value) { + this.sql.append(value); + return this; + } + + public Sql append(long value) { + this.sql.append(value); + return this; + } + + public Sql append(float value) { + this.sql.append(value); + return this; + } + + public Sql append(double value) { + this.sql.append(value); + return this; + } + + public Sql deleteCharAt(int index) { + this.sql.deleteCharAt(index); + return this; + } + + public Sql replace(int start, int end, /*@Untainted*/ String str) { + this.sql.replace(start, end, str); + return this; + } + + public Sql insert(int offset, /*@Untainted*/ String str) { + this.sql.insert(offset, str); + return this; + } + + public Sql insert(int offset, boolean value) { + this.sql.insert(offset, value); + return this; + } + + public Sql insert(int offset, int value) { + this.sql.insert(offset, value); + return this; + } + + public Sql insert(int offset, long value) { + this.sql.insert(offset, value); + return this; + } + + public Sql insert(int offset, double value) { + this.sql.insert(offset, value); + return this; + } + + public Sql insert(int offset, float value) { + this.sql.insert(offset, value); + return this; + } + + public int indexOf(String str) { + return this.sql.indexOf(str); + } + + public int indexOf(String str, int fromIndex) { + return this.sql.indexOf(str, fromIndex); + } + + public int lastIndexOf(String str) { + return this.sql.lastIndexOf(str); + } + + public int lastIndexOf(String str, int fromIndex) { + return this.sql.lastIndexOf(str, fromIndex); + } + + /** + * Appends the bit of sql and notes that a list, or a sublist, has started. + *

+ * Each list started must have be ended. "Lists" are only to support using listSeparator(sep) + */ + public Sql listStart(/*@Untainted*/ String sql) { + listFirstItem.push(true); + return append(sql); + } + + /** + * Appends the passed bit of sql only if a previous item has already been appended, + * and notes that the list is not empty. + */ + public Sql listSeparator(/*@Untainted*/ String sql) { + if (listFirstItem.peek()) { + listFirstItem.pop(); + listFirstItem.push(false); + return this; + } else { + return append(sql); + } + } + + + public Sql listEnd(/*@Untainted*/ String sql) { + listFirstItem.pop(); + return append(sql); + } + + /*@Untainted*/ + public String sql() { + return sql.toString(); + } + + /** + * Same as sql(), provided for drop-in compatibility with StringBuilder. + */ + /*@Untainted*/ + public String toString() { + return sql(); + } + + public Sql argBoolean(Boolean arg) { + sqlArgs.argBoolean(arg); + return this; + } + + + public Sql argBoolean( String argName, Boolean arg) { + sqlArgs.argBoolean(argName, arg); + return this; + } + + + public Sql argInteger(Integer arg) { + sqlArgs.argInteger(arg); + return this; + } + + + public Sql argInteger( String argName, Integer arg) { + sqlArgs.argInteger(argName, arg); + return this; + } + + + public Sql argLong(Long arg) { + sqlArgs.argLong(arg); + return this; + } + + + public Sql argLong(String argName, Long arg) { + sqlArgs.argLong(argName, arg); + return this; + } + + + public Sql argFloat(Float arg) { + sqlArgs.argFloat(arg); + return this; + } + + + public Sql argFloat( String argName, Float arg) { + sqlArgs.argFloat(argName, arg); + return this; + } + + + public Sql argDouble(Double arg) { + sqlArgs.argDouble(arg); + return this; + } + + + public Sql argDouble( String argName, Double arg) { + sqlArgs.argDouble(argName, arg); + return this; + } + + + public Sql argBigDecimal(BigDecimal arg) { + sqlArgs.argBigDecimal(arg); + return this; + } + + + public Sql argBigDecimal( String argName, BigDecimal arg) { + sqlArgs.argBigDecimal(argName, arg); + return this; + } + + + public Sql argString(String arg) { + sqlArgs.argString(arg); + return this; + } + + + public Sql argString( String argName, String arg) { + sqlArgs.argString(argName, arg); + return this; + } + + + public Sql argDate(Date arg) { + sqlArgs.argDate(arg); + return this; + } + + + public Sql argDate( String argName, Date arg) { + sqlArgs.argDate(argName, arg); + return this; + } + + + public Sql argDateNowPerApp() { + sqlArgs.argDateNowPerApp(); + return this; + } + + + public Sql argDateNowPerApp( String argName) { + sqlArgs.argDateNowPerApp(argName); + return this; + } + + + public Sql argDateNowPerDb() { + sqlArgs.argDateNowPerDb(); + return this; + } + + + public Sql argDateNowPerDb( String argName) { + sqlArgs.argDateNowPerDb(argName); + return this; + } + + + public Sql argBlobBytes(byte[] arg) { + sqlArgs.argBlobBytes(arg); + return this; + } + + + public Sql argBlobBytes( String argName, byte[] arg) { + sqlArgs.argBlobBytes(argName, arg); + return this; + } + + + public Sql argBlobInputStream(InputStream arg) { + sqlArgs.argBlobInputStream(arg); + return this; + } + + + public Sql argBlobInputStream( String argName, InputStream arg) { + sqlArgs.argBlobInputStream(argName, arg); + return this; + } + + + public Sql argClobString(String arg) { + sqlArgs.argClobString(arg); + return this; + } + + + public Sql argClobString( String argName, String arg) { + sqlArgs.argClobString(argName, arg); + return this; + } + + + public Sql argClobReader(Reader arg) { + sqlArgs.argClobReader(arg); + return this; + } + + + public Sql argClobReader( String argName, Reader arg) { + sqlArgs.argClobReader(argName, arg); + return this; + } + + @Override + public void apply(SqlSelect select) { + if (batched != null) { + throw new DatabaseException("Batch not supported for select"); + } + sqlArgs.apply(select); + } + + @Override + public void apply(SqlInsert insert) { + if (batched != null) { + batch(); + for (SqlArgs args : batched) { + args.apply(insert); + insert.batch(); + } + } else { + sqlArgs.apply(insert); + } + } + + @Override + public void apply(SqlUpdate update) { + if (batched != null) { + throw new DatabaseException("Batch not supported for update"); + } + + sqlArgs.apply(update); + } + + public enum ColumnType { + Integer, Long, Float, Double, BigDecimal, String, ClobString, ClobStream, + BlobBytes, BlobStream, Date, DateNowPerApp, DateNowPerDb, Boolean + } + + private static class Invocation { + ColumnType columnType; + String argName; + Object arg; + + Invocation(ColumnType columnType, String argName, Object arg) { + this.columnType = columnType; + this.argName = argName; + this.arg = arg; + } + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlArgs.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlArgs.java new file mode 100644 index 0000000..7ee86cf --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlArgs.java @@ -0,0 +1,773 @@ +package org.xbib.jdbc.query; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * This class is useful for dynamically generating SQL. It can "buffer" the + * various arg*() calls and replay them later via the apply(sqlArgs) methods. + */ +public class SqlArgs implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply { + private final List invocations = new ArrayList<>(); + + + public static Builder fromMetadata(Row r) { + return new Builder(r); + } + + /** + * Convenience method for reading a single row. If you are reading multiple + * rows, see {@link #fromMetadata(Row)} to avoid reading metadata multiple times. + * + * @return a SqlArgs with one invocation for each column in the Row, with name + * and type inferred from the metadata + */ + + public static SqlArgs readRow(Row r) { + return new Builder(r).read(r); + } + + public static String[] tidyColumnNames(String[] names) { + Set uniqueNames = new LinkedHashSet<>(); + for (String name : names) { + if (name == null || name.length() == 0) { + name = "column_" + (uniqueNames.size() + 1); + } + name = name.replaceAll("[^a-zA-Z0-9]", " "); + name = name.replaceAll("([a-z])([A-Z])", "$1_$2"); + name = name.trim().toLowerCase(); + name = name.replaceAll("\\s", "_"); + if (Character.isDigit(name.charAt(0))) { + name = "a" + name; + } + int i = 2; + String uniqueName = name; + while (uniqueNames.contains(uniqueName)) { + uniqueName = name + "_" + i++; + } + name = uniqueName; + uniqueNames.add(name); + } + return uniqueNames.toArray(new String[uniqueNames.size()]); + } + + + public SqlArgs argBoolean(Boolean arg) { + invocations.add(new Invocation(ColumnType.Boolean, null, arg)); + return this; + } + + + public SqlArgs argBoolean( String argName, Boolean arg) { + invocations.add(new Invocation(ColumnType.Boolean, argName, arg)); + return this; + } + + + public SqlArgs argInteger(Integer arg) { + invocations.add(new Invocation(ColumnType.Integer, null, arg)); + return this; + } + + + public SqlArgs argInteger( String argName, Integer arg) { + invocations.add(new Invocation(ColumnType.Integer, argName, arg)); + return this; + } + + + public SqlArgs argLong(Long arg) { + invocations.add(new Invocation(ColumnType.Long, null, arg)); + return this; + } + + + public SqlArgs argLong( String argName, Long arg) { + invocations.add(new Invocation(ColumnType.Long, argName, arg)); + return this; + } + + + public SqlArgs argFloat(Float arg) { + invocations.add(new Invocation(ColumnType.Float, null, arg)); + return this; + } + + + public SqlArgs argFloat( String argName, Float arg) { + invocations.add(new Invocation(ColumnType.Float, argName, arg)); + return this; + } + + + public SqlArgs argDouble(Double arg) { + invocations.add(new Invocation(ColumnType.Double, null, arg)); + return this; + } + + + public SqlArgs argDouble( String argName, Double arg) { + invocations.add(new Invocation(ColumnType.Double, argName, arg)); + return this; + } + + + public SqlArgs argBigDecimal(BigDecimal arg) { + invocations.add(new Invocation(ColumnType.BigDecimal, null, arg)); + return this; + } + + + public SqlArgs argBigDecimal( String argName, BigDecimal arg) { + invocations.add(new Invocation(ColumnType.BigDecimal, argName, arg)); + return this; + } + + + public SqlArgs argString(String arg) { + invocations.add(new Invocation(ColumnType.String, null, arg)); + return this; + } + + + public SqlArgs argString( String argName, String arg) { + invocations.add(new Invocation(ColumnType.String, argName, arg)); + return this; + } + + + public SqlArgs argDate(Date arg) { + // date argument with a time on it + invocations.add(new Invocation(ColumnType.Date, null, arg)); + return this; + } + + + public SqlArgs argDate( String argName, Date arg) { + // date argument with a time on it + invocations.add(new Invocation(ColumnType.Date, argName, arg)); + return this; + } + + + public SqlArgs argLocalDate(LocalDate arg) { + // date argument with no time on it + invocations.add(new Invocation(ColumnType.LocalDate, null, arg)); + return this; + } + + + public SqlArgs argLocalDate( String argName, LocalDate arg) { + // date argument with no time on it + invocations.add(new Invocation(ColumnType.LocalDate, argName, arg)); + return this; + } + + + public SqlArgs argDateNowPerApp() { + invocations.add(new Invocation(ColumnType.DateNowPerApp, null, null)); + return this; + } + + + public SqlArgs argDateNowPerApp( String argName) { + invocations.add(new Invocation(ColumnType.DateNowPerApp, argName, null)); + return this; + } + + + public SqlArgs argDateNowPerDb() { + invocations.add(new Invocation(ColumnType.DateNowPerDb, null, null)); + return this; + } + + + public SqlArgs argDateNowPerDb( String argName) { + invocations.add(new Invocation(ColumnType.DateNowPerDb, argName, null)); + return this; + } + + + public SqlArgs argBlobBytes(byte[] arg) { + invocations.add(new Invocation(ColumnType.BlobBytes, null, arg)); + return this; + } + + + public SqlArgs argBlobBytes( String argName, byte[] arg) { + invocations.add(new Invocation(ColumnType.BlobBytes, argName, arg)); + return this; + } + + + public SqlArgs argBlobInputStream(InputStream arg) { + invocations.add(new Invocation(ColumnType.BlobStream, null, arg)); + return this; + } + + + public SqlArgs argBlobInputStream( String argName, InputStream arg) { + invocations.add(new Invocation(ColumnType.BlobStream, argName, arg)); + return this; + } + + + public SqlArgs argClobString(String arg) { + invocations.add(new Invocation(ColumnType.ClobString, null, arg)); + return this; + } + + + public SqlArgs argClobString( String argName, String arg) { + invocations.add(new Invocation(ColumnType.ClobString, argName, arg)); + return this; + } + + + public SqlArgs argClobReader(Reader arg) { + invocations.add(new Invocation(ColumnType.ClobStream, null, arg)); + return this; + } + + + public SqlArgs argClobReader( String argName, Reader arg) { + invocations.add(new Invocation(ColumnType.ClobStream, argName, arg)); + return this; + } + + + public SqlArgs makePositional() { + for (Invocation invocation : invocations) { + invocation.argName = null; + } + return this; + } + + + public List names() { + List names = new ArrayList<>(); + for (Invocation invocation : invocations) { + if (invocation.argName != null) { + names.add(invocation.argName); + } + } + return names; + } + + public int argCount() { + return invocations.size(); + } + + public int positionalCount() { + int count = 0; + for (Invocation invocation : invocations) { + if (invocation.argName == null) { + count++; + } + } + return count; + } + + @SuppressWarnings("CheckReturnValue") + @Override + public void apply(SqlSelect select) { + for (Invocation i : invocations) { + switch (i.columnType) { + case Boolean: + if (i.argName == null) { + select.argBoolean((Boolean) i.arg); + } else { + select.argBoolean(i.argName, (Boolean) i.arg); + } + break; + case Integer: + if (i.argName == null) { + select.argInteger((Integer) i.arg); + } else { + select.argInteger(i.argName, (Integer) i.arg); + } + break; + case Long: + if (i.argName == null) { + select.argLong((Long) i.arg); + } else { + select.argLong(i.argName, (Long) i.arg); + } + break; + case Float: + if (i.argName == null) { + select.argFloat((Float) i.arg); + } else { + select.argFloat(i.argName, (Float) i.arg); + } + break; + case Double: + if (i.argName == null) { + select.argDouble((Double) i.arg); + } else { + select.argDouble(i.argName, (Double) i.arg); + } + break; + case BigDecimal: + if (i.argName == null) { + select.argBigDecimal((BigDecimal) i.arg); + } else { + select.argBigDecimal(i.argName, (BigDecimal) i.arg); + } + break; + case String: + if (i.argName == null) { + select.argString((String) i.arg); + } else { + select.argString(i.argName, (String) i.arg); + } + break; + case ClobString: + if (i.argName == null) { + select.argString((String) i.arg); + } else { + select.argString(i.argName, (String) i.arg); + } + break; + case ClobStream: + throw new DatabaseException("Don't use Clob stream parameters with select statements"); + case BlobBytes: + throw new DatabaseException("Don't use Blob parameters with select statements"); + case BlobStream: + throw new DatabaseException("Don't use Blob parameters with select statements"); + case LocalDate: + // date argument with no time on it + if (i.argName == null) { + select.argLocalDate((LocalDate) i.arg); + } else { + select.argLocalDate(i.argName, (LocalDate) i.arg); + } + break; + case Date: + // date argument with a time on it + if (i.argName == null) { + select.argDate((Date) i.arg); + } else { + select.argDate(i.argName, (Date) i.arg); + } + break; + case DateNowPerApp: + if (i.argName == null) { + select.argDateNowPerApp(); + } else { + select.argDateNowPerApp(i.argName); + } + break; + case DateNowPerDb: + if (i.argName == null) { + select.argDateNowPerDb(); + } else { + select.argDateNowPerDb(i.argName); + } + break; + } + } + } + + @SuppressWarnings("CheckReturnValue") + @Override + public void apply(SqlInsert insert) { + for (Invocation i : invocations) { + switch (i.columnType) { + case Boolean: + if (i.argName == null) { + insert.argBoolean((Boolean) i.arg); + } else { + insert.argBoolean(i.argName, (Boolean) i.arg); + } + break; + case Integer: + if (i.argName == null) { + insert.argInteger((Integer) i.arg); + } else { + insert.argInteger(i.argName, (Integer) i.arg); + } + break; + case Long: + if (i.argName == null) { + insert.argLong((Long) i.arg); + } else { + insert.argLong(i.argName, (Long) i.arg); + } + break; + case Float: + if (i.argName == null) { + insert.argFloat((Float) i.arg); + } else { + insert.argFloat(i.argName, (Float) i.arg); + } + break; + case Double: + if (i.argName == null) { + insert.argDouble((Double) i.arg); + } else { + insert.argDouble(i.argName, (Double) i.arg); + } + break; + case BigDecimal: + if (i.argName == null) { + insert.argBigDecimal((BigDecimal) i.arg); + } else { + insert.argBigDecimal(i.argName, (BigDecimal) i.arg); + } + break; + case String: + if (i.argName == null) { + insert.argString((String) i.arg); + } else { + insert.argString(i.argName, (String) i.arg); + } + break; + case ClobString: + if (i.argName == null) { + insert.argClobString((String) i.arg); + } else { + insert.argClobString(i.argName, (String) i.arg); + } + break; + case ClobStream: + if (i.argName == null) { + insert.argClobReader((Reader) i.arg); + } else { + insert.argClobReader(i.argName, (Reader) i.arg); + } + break; + case BlobBytes: + if (i.argName == null) { + insert.argBlobBytes((byte[]) i.arg); + } else { + insert.argBlobBytes(i.argName, (byte[]) i.arg); + } + break; + case BlobStream: + if (i.argName == null) { + insert.argBlobStream((InputStream) i.arg); + } else { + insert.argBlobStream(i.argName, (InputStream) i.arg); + } + break; + case LocalDate: + // date argument with no time on it + if (i.argName == null) { + insert.argLocalDate((LocalDate) i.arg); + } else { + insert.argLocalDate(i.argName, (LocalDate) i.arg); + } + break; + case Date: + // date argument with a time on it + if (i.argName == null) { + insert.argDate((Date) i.arg); + } else { + insert.argDate(i.argName, (Date) i.arg); + } + break; + case DateNowPerApp: + if (i.argName == null) { + insert.argDateNowPerApp(); + } else { + insert.argDateNowPerApp(i.argName); + } + break; + case DateNowPerDb: + if (i.argName == null) { + insert.argDateNowPerDb(); + } else { + insert.argDateNowPerDb(i.argName); + } + break; + } + } + } + + @SuppressWarnings("CheckReturnValue") + @Override + public void apply(SqlUpdate update) { + for (Invocation i : invocations) { + switch (i.columnType) { + case Boolean: + if (i.argName == null) { + update.argBoolean((Boolean) i.arg); + } else { + update.argBoolean(i.argName, (Boolean) i.arg); + } + break; + case Integer: + if (i.argName == null) { + update.argInteger((Integer) i.arg); + } else { + update.argInteger(i.argName, (Integer) i.arg); + } + break; + case Long: + if (i.argName == null) { + update.argLong((Long) i.arg); + } else { + update.argLong(i.argName, (Long) i.arg); + } + break; + case Float: + if (i.argName == null) { + update.argFloat((Float) i.arg); + } else { + update.argFloat(i.argName, (Float) i.arg); + } + break; + case Double: + if (i.argName == null) { + update.argDouble((Double) i.arg); + } else { + update.argDouble(i.argName, (Double) i.arg); + } + break; + case BigDecimal: + if (i.argName == null) { + update.argBigDecimal((BigDecimal) i.arg); + } else { + update.argBigDecimal(i.argName, (BigDecimal) i.arg); + } + break; + case String: + if (i.argName == null) { + update.argString((String) i.arg); + } else { + update.argString(i.argName, (String) i.arg); + } + break; + case ClobString: + if (i.argName == null) { + update.argClobString((String) i.arg); + } else { + update.argClobString(i.argName, (String) i.arg); + } + break; + case ClobStream: + if (i.argName == null) { + update.argClobReader((Reader) i.arg); + } else { + update.argClobReader(i.argName, (Reader) i.arg); + } + break; + case BlobBytes: + if (i.argName == null) { + update.argBlobBytes((byte[]) i.arg); + } else { + update.argBlobBytes(i.argName, (byte[]) i.arg); + } + break; + case BlobStream: + if (i.argName == null) { + update.argBlobStream((InputStream) i.arg); + } else { + update.argBlobStream(i.argName, (InputStream) i.arg); + } + break; + case LocalDate: + // date argument with no time on it + if (i.argName == null) { + update.argLocalDate((LocalDate) i.arg); + } else { + update.argLocalDate(i.argName, (LocalDate) i.arg); + } + break; + case Date: + // date argument with a time on it + if (i.argName == null) { + update.argDate((Date) i.arg); + } else { + update.argDate(i.argName, (Date) i.arg); + } + break; + case DateNowPerApp: + if (i.argName == null) { + update.argDateNowPerApp(); + } else { + update.argDateNowPerApp(i.argName); + } + break; + case DateNowPerDb: + if (i.argName == null) { + update.argDateNowPerDb(); + } else { + update.argDateNowPerDb(i.argName); + } + break; + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SqlArgs sqlArgs = (SqlArgs) o; + return Objects.equals(invocations, sqlArgs.invocations); + } + + @Override + public int hashCode() { + return Objects.hash(invocations); + } + + @Override + public String toString() { + return "SqlArgs" + invocations; + } + + public enum ColumnType { + Integer, Long, Float, Double, BigDecimal, String, ClobString, ClobStream, + BlobBytes, BlobStream, Date, LocalDate, DateNowPerApp, DateNowPerDb, Boolean + } + + private static class Invocation { + ColumnType columnType; + String argName; + Object arg; + + Invocation(ColumnType columnType, String argName, Object arg) { + this.columnType = columnType; + this.argName = argName; + this.arg = arg; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Invocation that = (Invocation) o; + return columnType == that.columnType && + Objects.equals(argName, that.argName) && + Objects.deepEquals(arg, that.arg); + } + + @Override + public int hashCode() { + return Objects.hash(columnType, argName, arg); + } + + @Override + public String toString() { + return "{name=" + argName + ", type=" + columnType + ", arg=" + arg + '}'; + } + } + + public static class Builder { + private final int[] types; + private final int[] precision; + private final int[] scale; + private String[] names; + + public Builder(Row r) { + try { + ResultSetMetaData metadata = r.getMetadata(); + int columnCount = metadata.getColumnCount(); + names = new String[columnCount]; + types = new int[columnCount]; + precision = new int[columnCount]; + scale = new int[columnCount]; + + for (int i = 0; i < columnCount; i++) { + names[i] = metadata.getColumnLabel(i + 1); + types[i] = metadata.getColumnType(i + 1); + precision[i] = metadata.getPrecision(i + 1); + scale[i] = metadata.getScale(i + 1); + } + + names = tidyColumnNames(names); + } catch (SQLException e) { + throw new DatabaseException("Unable to retrieve metadata from ResultSet", e); + } + } + + + public SqlArgs read(Row r) { + SqlArgs args = new SqlArgs(); + + for (int i = 0; i < names.length; i++) { + switch (types[i]) { + case Types.SMALLINT: + case Types.INTEGER: + args.argInteger(names[i], r.getIntegerOrNull()); + break; + case Types.BIGINT: + args.argLong(names[i], r.getLongOrNull()); + break; + case Types.REAL: + case 100: // Oracle proprietary it seems + args.argFloat(names[i], r.getFloatOrNull()); + break; + case Types.DOUBLE: + case 101: // Oracle proprietary it seems + args.argDouble(names[i], r.getDoubleOrNull()); + break; + case Types.NUMERIC: + if (precision[i] == 10 && scale[i] == 0) { + // Oracle reports integer as numeric + args.argInteger(names[i], r.getIntegerOrNull()); + } else if (precision[i] == 19 && scale[i] == 0) { + // Oracle reports long as numeric + args.argLong(names[i], r.getLongOrNull()); + } else { + args.argBigDecimal(names[i], r.getBigDecimalOrNull()); + } + break; + case Types.BINARY: + case Types.VARBINARY: + case Types.BLOB: + args.argBlobBytes(names[i], r.getBlobBytesOrNull()); + break; + case Types.CLOB: + case Types.NCLOB: + args.argClobString(names[i], r.getClobStringOrNull()); + break; + + // Check Date before TimeStamp because SQL dates are also timestamps + case Types.DATE: + args.argLocalDate(names[i], r.getLocalDateOrNull()); + break; + + case Types.TIMESTAMP: + if (this.scale[i] == 0) { + // If the scale is 0, this is a LocalDate (no time/timezone). + // Anything with a time will have a non-zero scale + args.argLocalDate(names[i], r.getLocalDateOrNull()); + } else { + args.argDate(names[i], r.getDateOrNull()); + } + break; + + case Types.NVARCHAR: + case Types.VARCHAR: + case Types.CHAR: + case Types.NCHAR: + if (precision[i] >= 2147483647) { + // Postgres seems to report clobs are varchar(2147483647) + args.argClobString(names[i], r.getClobStringOrNull()); + } else { + args.argString(names[i], r.getStringOrNull()); + } + break; + default: + throw new DatabaseException("Don't know how to deal with column type: " + types[i]); + } + } + return args; + } + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsert.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsert.java new file mode 100644 index 0000000..ff5119c --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsert.java @@ -0,0 +1,152 @@ +package org.xbib.jdbc.query; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Date; + +/** + * Interface for configuring (setting parameters) and executing a chunk of SQL. + */ +public interface SqlInsert { + + SqlInsert argBoolean(Boolean arg); + + SqlInsert argBoolean( String argName, Boolean arg); + + SqlInsert argInteger(Integer arg); + + SqlInsert argInteger( String argName, Integer arg); + + SqlInsert argLong(Long arg); + + SqlInsert argLong( String argName, Long arg); + + SqlInsert argFloat(Float arg); + + SqlInsert argFloat( String argName, Float arg); + + SqlInsert argDouble(Double arg); + + SqlInsert argDouble( String argName, Double arg); + + SqlInsert argBigDecimal(BigDecimal arg); + + SqlInsert argBigDecimal( String argName, BigDecimal arg); + + SqlInsert argString(String arg); + + SqlInsert argString( String argName, String arg); + + SqlInsert argDate(Date arg); // date with time + + SqlInsert argDate( String argName, Date arg); // date with time + + SqlInsert argLocalDate(LocalDate arg); // date only - no timestamp + + SqlInsert argLocalDate( String argName, LocalDate arg); // date only - no timestamp + + SqlInsert argDateNowPerApp(); + + SqlInsert argDateNowPerApp( String argName); + + SqlInsert argDateNowPerDb(); + + SqlInsert argDateNowPerDb( String argName); + + SqlInsert argBlobBytes(byte[] arg); + + SqlInsert argBlobBytes( String argName, byte[] arg); + + SqlInsert argBlobStream(InputStream arg); + + SqlInsert argBlobStream( String argName, InputStream arg); + + SqlInsert argClobString(String arg); + + SqlInsert argClobString( String argName, String arg); + + SqlInsert argClobReader(Reader arg); + + SqlInsert argClobReader( String argName, Reader arg); + + SqlInsert withArgs(SqlArgs args); + + SqlInsert apply(Apply apply); + + /** + * Call this between setting rows of parameters for a SQL statement. You may call it before + * setting any parameters, after setting all, or multiple times between rows. This feature + * only currently works with basic inserts (you can't do insertReturning type operations). + */ + SqlInsert batch(); + + /** + * Perform the insert into the database without any verification of how many rows + * were affected. + * + * @return the number of rows affected + */ + int insert(); + + /** + * Perform the insert into the database. This will automatically verify + * that the specified number of rows was affected, and throw a {@link WrongNumberOfRowsException} + * if it does not match. + */ + void insert(int expectedRowsUpdated); + + /** + * Insert multiple rows in one database call. This will automatically verify + * that exactly 1 row is affected for each row of parameters. + */ + void insertBatch(); + + /** + * Insert multiple rows in one database call. This returns the results for + * each row so you can check them yourself. + * + * @return an array with an element for each row in the batch; the value + * of each array indicates how many rows were affected; note that + * some database/driver combinations do now return this information + * (for example, older versions of Oracle return -2 rather than the + * number of rows) + */ + int[] insertBatchUnchecked(); + + /** + * Use this method in conjunction with argPkSeq() to optimize inserts where the + * primary key is being populated from a database sequence at insert time. If the + * database can't support this feature it will be simulated with a select and then + * the insert. + * + *

This version of insert expects exactly one row to be inserted, and will throw + * a DatabaseException if that isn't the case.

+ */ + + Long insertReturningPkSeq(String primaryKeyColumnName); + + T insertReturning(String tableName, String primaryKeyColumnName, RowsHandler rowsHandler, + String... otherColumnNames); + + SqlInsert argPkSeq( String sequenceName); + + /** + * Use this method to populate the primary key value (assumed to be type Long) + * from a sequence in the database. This can be used standalone, but is intended + * to be used in conjunction with insertReturningPkSeq() to both insert and obtain + * the inserted value in an optimized way (if possible). For databases that are + * unable to return the value from the insert (such as Derby) this will be simulated + * first issuing a select to read the sequence, then an insert. + */ + SqlInsert argPkSeq( String argName, String sequenceName); + + SqlInsert argPkLong(Long pkValue); + + SqlInsert argPkLong(String argName, Long pkValue); + + interface Apply { + void apply(SqlInsert insert); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsertImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsertImpl.java new file mode 100644 index 0000000..9fcdcea --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlInsertImpl.java @@ -0,0 +1,708 @@ +package org.xbib.jdbc.query; + +import org.xbib.jdbc.query.util.DebugSql; +import org.xbib.jdbc.query.util.InternalStringReader; +import org.xbib.jdbc.query.util.Metric; +import org.xbib.jdbc.query.util.RewriteArg; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This is the key class for configuring (query parameters) and executing a database query. + */ +public class SqlInsertImpl implements SqlInsert { + + private static final Logger log = Logger.getLogger(Database.class.getName()); + + private final Connection connection; + + private final StatementAdaptor adaptor; + + private final String sql; + + private final Options options; + + private List batched; + + private List parameterList; // !null ==> traditional ? args + + private Map parameterMap; // !null ==> named :abc args + + private String pkArgName; + + private int pkPos; + + private String pkSeqName; + + private Long pkLong; + + public SqlInsertImpl(Connection connection, String sql, Options options) { + this.connection = connection; + this.sql = sql; + this.options = options; + adaptor = new StatementAdaptor(options); + } + + + @Override + public SqlInsert argBoolean(Boolean arg) { + return positionalArg(adaptor.nullString(booleanToString(arg))); + } + + + @Override + public SqlInsert argBoolean( String argName, Boolean arg) { + return namedArg(argName, adaptor.nullString(booleanToString(arg))); + } + + @Override + + public SqlInsert argInteger(Integer arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + @Override + + public SqlInsert argInteger( String argName, Integer arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + @Override + + public SqlInsert argLong(Long arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + @Override + + public SqlInsert argLong( String argName, Long arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + @Override + + public SqlInsert argFloat(Float arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + @Override + + public SqlInsert argFloat( String argName, Float arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + @Override + + public SqlInsert argDouble(Double arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + @Override + + public SqlInsert argDouble( String argName, Double arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + @Override + + public SqlInsert argBigDecimal(BigDecimal arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + @Override + + public SqlInsert argBigDecimal( String argName, BigDecimal arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + @Override + + public SqlInsert argString(String arg) { + return positionalArg(adaptor.nullString(arg)); + } + + @Override + + public SqlInsert argString( String argName, String arg) { + return namedArg(argName, adaptor.nullString(arg)); + } + + @Override + + public SqlInsert argDate(Date arg) { + return positionalArg(adaptor.nullDate(arg)); + } + + @Override + + public SqlInsert argDate( String argName, Date arg) { + return namedArg(argName, adaptor.nullDate(arg)); + } + + @Override + + public SqlInsert argLocalDate( String argName, LocalDate arg) { + return namedArg(argName, adaptor.nullLocalDate(arg)); + } + + @Override + + public SqlInsert argLocalDate(LocalDate arg) { + return positionalArg(adaptor.nullLocalDate(arg)); + } + + + @Override + public SqlInsert argDateNowPerApp() { + return positionalArg(adaptor.nullDate(options.currentDate())); + } + + @Override + + public SqlInsert argDateNowPerApp( String argName) { + return namedArg(argName, adaptor.nullDate(options.currentDate())); + } + + + @Override + public SqlInsert argDateNowPerDb() { + if (options.useDatePerAppOnly()) { + return positionalArg(adaptor.nullDate(options.currentDate())); + } + return positionalArg(new RewriteArg(options.flavor().dbTimeMillis())); + } + + @Override + + public SqlInsert argDateNowPerDb( String argName) { + if (options.useDatePerAppOnly()) { + return namedArg(argName, adaptor.nullDate(options.currentDate())); + } + return namedArg(argName, new RewriteArg(options.flavor().dbTimeMillis())); + } + + @Override + + public SqlInsert argBlobBytes(byte[] arg) { + return positionalArg(adaptor.nullBytes(arg)); + } + + @Override + + public SqlInsert argBlobBytes( String argName, byte[] arg) { + return namedArg(argName, adaptor.nullBytes(arg)); + } + + @Override + + public SqlInsert argBlobStream(InputStream arg) { + return positionalArg(adaptor.nullInputStream(arg)); + } + + @Override + + public SqlInsert argBlobStream( String argName, InputStream arg) { + return namedArg(argName, adaptor.nullInputStream(arg)); + } + + @Override + + public SqlInsert argClobString(String arg) { + return positionalArg(adaptor.nullClobReader(arg == null ? null : new InternalStringReader(arg))); + } + + @Override + + public SqlInsert argClobString( String argName, String arg) { + return namedArg(argName, adaptor.nullClobReader(arg == null ? null : new InternalStringReader(arg))); + } + + @Override + + public SqlInsert argClobReader(Reader arg) { + return positionalArg(adaptor.nullClobReader(arg)); + } + + @Override + + public SqlInsert argClobReader( String argName, Reader arg) { + return namedArg(argName, adaptor.nullClobReader(arg)); + } + + + @Override + public SqlInsert withArgs(SqlArgs args) { + return apply(args); + } + + + @Override + public SqlInsert apply(Apply apply) { + apply.apply(this); + return this; + } + + @Override + public SqlInsert batch() { + if ((parameterList != null && !parameterList.isEmpty()) + || (parameterMap != null && !parameterMap.isEmpty())) { + if (batched == null) { + batched = new ArrayList<>(); + } + batched.add(new Batch(parameterList, parameterMap)); + parameterList = new ArrayList<>(); + parameterMap = new HashMap<>(); + } + return this; + } + + @Override + public int insert() { + return updateInternal(0); + } + + @Override + public void insert(int expectedRowsUpdated) { + updateInternal(expectedRowsUpdated); + } + + @Override + public void insertBatch() { + int[] result = updateBatch(); + for (int r : result) { + // Tolerate SUCCESS_NO_INFO for older versions of Oracle + if (r != 1 && r != Statement.SUCCESS_NO_INFO) { + throw new DatabaseException("Batch did not return the expected result: " + Arrays.toString(result)); + } + } + } + + @Override + public int[] insertBatchUnchecked() { + return updateBatch(); + } + + @Override + public Long insertReturningPkSeq(String primaryKeyColumnName) { + if (!hasPk()) { + throw new DatabaseException("Call argPkSeq() before insertReturningPkSeq()"); + } + + if (options.flavor().supportsInsertReturning()) { + return updateInternal(1, primaryKeyColumnName); + } else { + // Simulate by issuing a select for the next sequence value, inserting, and returning it + Long pk = new SqlSelectImpl(connection, options.flavor().sequenceSelectNextVal(pkSeqName), options).queryLongOrNull(); + if (pk == null) { + throw new DatabaseException("Unable to retrieve next sequence value from " + pkSeqName); + } + if (pkArgName != null) { + namedArg(pkArgName, adaptor.nullNumeric(pk)); + } else { + parameterList.set(pkPos, adaptor.nullNumeric(pk)); + } + updateInternal(1); + return pk; + } + } + + @Override + public T insertReturning(String tableName, String primaryKeyColumnName, RowsHandler handler, + String... otherColumnNames) { + if (!hasPk()) { + throw new DatabaseException("Identify a primary key with argPk*() before insertReturning()"); + } + + if (options.flavor().supportsInsertReturning()) { + return updateInternal(1, primaryKeyColumnName, handler, otherColumnNames); + } else if (pkSeqName != null) { + // Simulate by issuing a select for the next sequence value, inserting, and returning it + Long pk = new SqlSelectImpl(connection, options.flavor().sequenceSelectNextVal(pkSeqName), options) + .queryLongOrNull(); + if (pk == null) { + throw new DatabaseException("Unable to retrieve next sequence value from " + pkSeqName); + } + if (pkArgName != null) { + namedArg(pkArgName, adaptor.nullNumeric(pk)); + } else { + parameterList.set(pkPos, adaptor.nullNumeric(pk)); + } + updateInternal(1); + StringBuilder sql = new StringBuilder(); + sql.append("select ").append(primaryKeyColumnName); + for (String colName : otherColumnNames) { + sql.append(", ").append(colName); + } + sql.append(" from ").append(tableName).append(" where ").append(primaryKeyColumnName).append("=?"); + return new SqlSelectImpl(connection, sql.toString(), options).argLong(pk).query(handler); + } else if (pkLong != null) { + // Insert the value, then do a select based on the primary key + updateInternal(1); + StringBuilder sql = new StringBuilder(); + sql.append("select ").append(primaryKeyColumnName); + for (String colName : otherColumnNames) { + sql.append(", ").append(colName); + } + sql.append(" from ").append(tableName).append(" where ").append(primaryKeyColumnName).append("=?"); + return new SqlSelectImpl(connection, sql.toString(), options).argLong(pkLong).query(handler); + } else { + // Should never happen if our safety checks worked + throw new DatabaseException("Internal error"); + } + } + + + @Override + public SqlInsert argPkSeq( String sequenceName) { + if (hasPk() && batched == null) { + throw new DatabaseException("Only call one argPk*() method"); + } + if (hasPk() && (!pkSeqName.equals(sequenceName) || pkPos != parameterList.size())) { + throw new DatabaseException("The argPkSeq() calls must be in the same position across batch records"); + } + pkSeqName = sequenceName; + SqlInsert sqlInsert = positionalArg(new RewriteArg(options.flavor().sequenceNextVal(sequenceName))); + pkPos = parameterList.size() - 1; + return sqlInsert; + } + + @Override + + public SqlInsert argPkSeq( String argName, String sequenceName) { + if (hasPk() && batched == null) { + throw new DatabaseException("Only call one argPk*() method"); + } + if (hasPk() && !argName.equals(pkArgName)) { + throw new DatabaseException("The primary key argument name must match across batch rows"); + } + pkArgName = argName; + pkSeqName = sequenceName; + return namedArg(argName, new RewriteArg(options.flavor().sequenceNextVal(sequenceName))); + } + + @Override + + public SqlInsert argPkLong(String argName, Long arg) { + if (hasPk() && batched == null) { + throw new DatabaseException("Only call one argPk*() method"); + } + if (hasPk() && !argName.equals(pkArgName)) { + throw new DatabaseException("The primary key argument name must match across batch rows"); + } + pkArgName = argName; + pkLong = arg; + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + @Override + + public SqlInsert argPkLong(Long arg) { + if (hasPk() && batched == null) { + throw new DatabaseException("Only call one argPk*() method"); + } + if (hasPk() && pkPos != parameterList.size()) { + throw new DatabaseException("The argPkLong() calls must be in the same position across batch records"); + } + pkLong = arg; + SqlInsert sqlInsert = positionalArg(adaptor.nullNumeric(arg)); + pkPos = parameterList.size() - 1; + return sqlInsert; + } + + private boolean hasPk() { + return pkArgName != null || pkSeqName != null || pkLong != null; + } + + private int[] updateBatch() { + batch(); + + if (batched == null || batched.size() == 0) { + throw new DatabaseException("Batch insert requires parameters"); + } + + PreparedStatement ps = null; + Metric metric = new Metric(log.isLoggable(Level.FINE)); + + String executeSql = sql; + Object[] firstRowParameters = null; + List parameters = new ArrayList<>(); + + boolean isSuccess = false; + String errorCode = null; + Exception logEx = null; + try { + for (Batch batch : batched) { + MixedParameterSql mpSql = new MixedParameterSql(sql, batch.parameterList, batch.parameterMap); + if (firstRowParameters == null) { + executeSql = mpSql.getSqlToExecute(); + firstRowParameters = mpSql.getArgs(); + } else { + if (!executeSql.equals(mpSql.getSqlToExecute())) { + throw new DatabaseException("All rows in a batch must use parameters in the same way. \nSQL1: " + + executeSql + "\nSQL2: " + mpSql.getSqlToExecute()); + } + } + parameters.add(mpSql.getArgs()); + } + + if (connection != null) { + ps = connection.prepareStatement(executeSql); + + for (Object[] params : parameters) { + adaptor.addParameters(ps, params); + ps.addBatch(); + } + + metric.checkpoint("prep"); + int[] numAffectedRows = ps.executeBatch(); + metric.checkpoint("execBatch", parameters.size()); + isSuccess = true; + return numAffectedRows; + } else { + return new int[]{-1}; + } + } catch (WrongNumberOfRowsException e) { + throw e; + } catch (Exception e) { + errorCode = options.generateErrorCode(); + logEx = e; + throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, firstRowParameters, errorCode, options), e); + } finally { + adaptor.closeQuietly(ps, log); + metric.done("close"); + if (isSuccess) { + DebugSql.logSuccess("Insert", log, metric, executeSql, firstRowParameters, options); + } else { + DebugSql.logError("Insert", log, metric, errorCode, executeSql, firstRowParameters, options, logEx); + } + } + } + + private int updateInternal(int expectedNumAffectedRows) { + if (batched != null) { + throw new DatabaseException("Call insertBatch() if you are using the batch() feature"); + } + + PreparedStatement ps = null; + Metric metric = new Metric(log.isLoggable(Level.FINE)); + + String executeSql = sql; + Object[] parameters = null; + + boolean isSuccess = false; + String errorCode = null; + Exception logEx = null; + try { + MixedParameterSql mpSql = new MixedParameterSql(sql, parameterList, parameterMap); + executeSql = mpSql.getSqlToExecute(); + parameters = mpSql.getArgs(); + + if (connection != null) { + ps = connection.prepareStatement(executeSql); + + adaptor.addParameters(ps, parameters); + metric.checkpoint("prep"); + int numAffectedRows = ps.executeUpdate(); + metric.checkpoint("exec", numAffectedRows); + if (expectedNumAffectedRows > 0 && numAffectedRows != expectedNumAffectedRows) { + errorCode = options.generateErrorCode(); + throw new WrongNumberOfRowsException("The number of affected rows was " + numAffectedRows + ", but " + + expectedNumAffectedRows + " were expected." + "\n" + + DebugSql.exceptionMessage(executeSql, parameters, errorCode, options)); + } + isSuccess = true; + return numAffectedRows; + } else { + return -1; + } + } catch (WrongNumberOfRowsException e) { + throw e; + } catch (Exception e) { + errorCode = options.generateErrorCode(); + logEx = e; + throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, parameters, errorCode, options), e); + } finally { + adaptor.closeQuietly(ps, log); + metric.done("close"); + if (isSuccess) { + DebugSql.logSuccess("Insert", log, metric, executeSql, parameters, options); + } else { + DebugSql.logError("Insert", log, metric, errorCode, executeSql, parameters, options, logEx); + } + } + } + + private Long updateInternal(int expectedNumAffectedRows, String pkToReturn) { + if (batched != null) { + throw new DatabaseException("Call insertBatch() if you are using the batch() feature"); + } + + PreparedStatement ps = null; + ResultSet rs = null; + Metric metric = new Metric(log.isLoggable(Level.FINE)); + + String executeSql = sql; + Object[] parameters = null; + + boolean isSuccess = false; + String errorCode = null; + Exception logEx = null; + try { + MixedParameterSql mpSql = new MixedParameterSql(sql, parameterList, parameterMap); + executeSql = mpSql.getSqlToExecute(); + parameters = mpSql.getArgs(); + + if (connection != null) { + ps = connection.prepareStatement(executeSql, new String[]{pkToReturn}); + + adaptor.addParameters(ps, parameters); + metric.checkpoint("prep"); + int numAffectedRows = ps.executeUpdate(); + metric.checkpoint("exec", numAffectedRows); + if (expectedNumAffectedRows > 0 && numAffectedRows != expectedNumAffectedRows) { + errorCode = options.generateErrorCode(); + throw new WrongNumberOfRowsException("The number of affected rows was " + numAffectedRows + ", but " + + expectedNumAffectedRows + " were expected." + "\n" + + DebugSql.exceptionMessage(executeSql, parameters, errorCode, options)); + } + rs = ps.getGeneratedKeys(); + Long pk = null; + if (rs != null && rs.next()) { + pk = rs.getLong(1); + } + isSuccess = true; + return pk; + } else { + return null; + } + } catch (WrongNumberOfRowsException e) { + throw e; + } catch (Exception e) { + errorCode = options.generateErrorCode(); + logEx = e; + throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, parameters, errorCode, options), e); + } finally { + adaptor.closeQuietly(rs, log); + adaptor.closeQuietly(ps, log); + metric.done("close"); + if (isSuccess) { + DebugSql.logSuccess("Insert", log, metric, executeSql, parameters, options); + } else { + DebugSql.logError("Insert", log, metric, errorCode, executeSql, parameters, options, logEx); + } + } + } + + private T updateInternal(int expectedNumAffectedRows, String pkToReturn, RowsHandler handler, + String... otherCols) { + if (batched != null) { + throw new DatabaseException("Call insertBatch() if you are using the batch() feature"); + } + + PreparedStatement ps = null; + ResultSet rs = null; + Metric metric = new Metric(log.isLoggable(Level.FINE)); + + String executeSql = sql; + Object[] parameters = null; + + boolean isSuccess = false; + String errorCode = null; + Exception logEx = null; + try { + MixedParameterSql mpSql = new MixedParameterSql(sql, parameterList, parameterMap); + executeSql = mpSql.getSqlToExecute(); + parameters = mpSql.getArgs(); + + String[] returnCols = new String[otherCols.length + 1]; + returnCols[0] = pkToReturn; + System.arraycopy(otherCols, 0, returnCols, 1, otherCols.length); + + if (connection != null) { + ps = connection.prepareStatement(executeSql, returnCols); + + adaptor.addParameters(ps, parameters); + metric.checkpoint("prep"); + int numAffectedRows = ps.executeUpdate(); + metric.checkpoint("exec", numAffectedRows); + if (expectedNumAffectedRows > 0 && numAffectedRows != expectedNumAffectedRows) { + errorCode = options.generateErrorCode(); + throw new WrongNumberOfRowsException("The number of affected rows was " + numAffectedRows + ", but " + + expectedNumAffectedRows + " were expected." + "\n" + + DebugSql.exceptionMessage(executeSql, parameters, errorCode, options)); + } + rs = ps.getGeneratedKeys(); + final ResultSet finalRs = rs; + T result = handler.process(new RowsAdaptor(finalRs, options)); + metric.checkpoint("read"); + isSuccess = true; + return result; + } else { + return null; + } + } catch (WrongNumberOfRowsException e) { + throw e; + } catch (Exception e) { + errorCode = options.generateErrorCode(); + logEx = e; + throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, parameters, errorCode, options), e); + } finally { + adaptor.closeQuietly(rs, log); + adaptor.closeQuietly(ps, log); + metric.done("close"); + if (isSuccess) { + DebugSql.logSuccess("Insert", log, metric, executeSql, parameters, options); + } else { + DebugSql.logError("Insert", log, metric, errorCode, executeSql, parameters, options, logEx); + } + } + } + + private SqlInsert positionalArg(Object arg) { + if (parameterList == null) { + parameterList = new ArrayList<>(); + } + parameterList.add(arg); + return this; + } + + private SqlInsert namedArg(String argName, Object arg) { + if (parameterMap == null) { + parameterMap = new HashMap<>(); + } + if (argName.startsWith(":")) { + argName = argName.substring(1); + } + parameterMap.put(argName, arg); + return this; + } + + private String booleanToString(Boolean b) { + return b == null ? null : b ? "Y" : "N"; + } + + private class Batch { + private final List parameterList; // !null ==> traditional ? args + private final Map parameterMap; // !null ==> named :abc args + + public Batch(List parameterList, Map parameterMap) { + this.parameterList = parameterList; + this.parameterMap = parameterMap; + } + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlNull.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlNull.java new file mode 100644 index 0000000..230ff3e --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlNull.java @@ -0,0 +1,14 @@ +package org.xbib.jdbc.query; + +public class SqlNull { + + private final int type; + + public SqlNull(int type) { + this.type = type; + } + + int getType() { + return type; + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelect.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelect.java new file mode 100644 index 0000000..82985c8 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelect.java @@ -0,0 +1,289 @@ +package org.xbib.jdbc.query; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Date; +import java.util.List; + +/** + * Interface for configuring (setting parameters) and executing a chunk of SQL. + */ +public interface SqlSelect { + + + SqlSelect argBoolean(Boolean arg); + + + + SqlSelect argBoolean( String argName, Boolean arg); + + + + SqlSelect argInteger(Integer arg); + + + + SqlSelect argInteger( String argName, Integer arg); + + + + SqlSelect argLong(Long arg); + + + + SqlSelect argLong( String argName, Long arg); + + + + SqlSelect argFloat(Float arg); + + + + SqlSelect argFloat( String argName, Float arg); + + + + SqlSelect argDouble(Double arg); + + + + SqlSelect argDouble( String argName, Double arg); + + + + SqlSelect argBigDecimal(BigDecimal arg); + + + + SqlSelect argBigDecimal( String argName, BigDecimal arg); + + + + SqlSelect argString(String arg); + + + + SqlSelect argString( String argName, String arg); + + + + SqlSelect argDate(Date arg); // Date with time + + + + SqlSelect argDate( String argName, Date arg); // Date with time + + + + SqlSelect argLocalDate(LocalDate arg); // Date without time + + + + SqlSelect argLocalDate( String argName, LocalDate arg); // Date without time + + + + SqlSelect argDateNowPerApp(); + + + + SqlSelect argDateNowPerApp( String argName); + + + + SqlSelect argDateNowPerDb(); + + + + SqlSelect argDateNowPerDb( String argName); + + + + SqlSelect withTimeoutSeconds(int seconds); + + + + SqlSelect withMaxRows(int rows); + + + + SqlSelect withArgs(SqlArgs args); + + + + SqlSelect apply(Apply apply); + + + + SqlSelect fetchSize(int fetchSize); + + + + Boolean queryBooleanOrNull(); + + + boolean queryBooleanOrFalse(); + + + boolean queryBooleanOrTrue(); + + + + Long queryLongOrNull(); + + + long queryLongOrZero(); + + /** + * Shorthand for reading numbers from the first column of the result. + * + * @return the first column values, omitting any that were null + */ + + + List queryLongs(); + + + + Integer queryIntegerOrNull(); + + + int queryIntegerOrZero(); + + + + List queryIntegers(); + + + + Float queryFloatOrNull(); + + + float queryFloatOrZero(); + + + + List queryFloats(); + + + + Double queryDoubleOrNull(); + + + double queryDoubleOrZero(); + + + + List queryDoubles(); + + + + BigDecimal queryBigDecimalOrNull(); + + + + BigDecimal queryBigDecimalOrZero(); + + + + List queryBigDecimals(); + + + + String queryStringOrNull(); + + + + String queryStringOrEmpty(); + + /** + * Shorthand for reading strings from the first column of the result. + * + * @return the first column values, omitting any that were null + */ + + + List queryStrings(); + + + + Date queryDateOrNull(); // Date with time + + + + List queryDates(); // Date with time + + + + LocalDate queryLocalDateOrNull(); // Date without time + + + + List queryLocalDates(); // Date without time + + /** + * This is the most generic and low-level way to iterate the query results. + * Consider using one of the other methods that can handle the iteration for you. + * + * @param rowsHandler the process() method of this handler will be called once + * and it will be responsible for iterating the results + */ + T query(RowsHandler rowsHandler); + + /** + * Query zero or one row. If zero rows are available a null will be returned. + * If more than one row is available a {@link ConstraintViolationException} + * will be thrown. + * + * @param rowHandler the process() method of this handler will be called once + * if there are results, or will not be called if there are + * no results + */ + T queryOneOrNull(RowHandler rowHandler); + + /** + * Query exactly one row. If zero rows are available or more than one row is + * available a {@link ConstraintViolationException} will be thrown. + * + * @param rowHandler the process() method of this handler will be called once + * if there are results, or will not be called if there are + * no results + */ + T queryOneOrThrow(RowHandler rowHandler); + + /** + * Query zero or one row. If zero rows are available a null will be returned. + * If more than one row is available the first row will be returned. + * + * @param rowHandler the process() method of this handler will be called once + * if there are results (for the first row), or will not be + * called if there are no results + */ + T queryFirstOrNull(RowHandler rowHandler); + + /** + * Query zero or one row. If zero rows are available a {@link ConstraintViolationException} + * will be thrown. If more than one row is available the first row will be returned. + * + * @param rowHandler the process() method of this handler will be called once + * if there are results (for the first row), or will not be + * called if there are no results + */ + T queryFirstOrThrow(RowHandler rowHandler); + + /** + * Query zero or more rows. If zero rows are available an empty list will be returned. + * If one or more rows are available each row will be read and added to a list, which + * is returned. + * + * @param rowHandler the process() method of this handler will be called once + * for each row in the result, or will not be called if there are + * no results. Only non-null values returned will be added to the + * result list. + */ + List queryMany(RowHandler rowHandler); + + interface Apply { + void apply(SqlSelect select); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelectImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelectImpl.java new file mode 100644 index 0000000..e429ac4 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlSelectImpl.java @@ -0,0 +1,766 @@ +package org.xbib.jdbc.query; + +import org.xbib.jdbc.query.util.DebugSql; +import org.xbib.jdbc.query.util.Metric; +import org.xbib.jdbc.query.util.RewriteArg; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This is the key class for configuring (query parameters) and executing a database query. + */ +public class SqlSelectImpl implements SqlSelect { + + private static final Logger log = Logger.getLogger(Database.class.getName()); + + private final Connection connection; + + private final StatementAdaptor adaptor; + + private final Object cancelLock = new Object(); + + private final String sql; + + private final Options options; + + private PreparedStatement ps; // hold reference to support cancel from another thread + + private List parameterList; // !null ==> traditional ? args + + private Map parameterMap; // !null ==> named :abc args + + private int timeoutSeconds = -1; // -1 ==> no timeout + + private int maxRows = -1; // -1 ==> unlimited + + private int fetchSize = -1; // -1 ==> do not call setFetchSize() + + public SqlSelectImpl(Connection connection, String sql, Options options) { + this.connection = connection; + this.sql = sql; + this.options = options; + adaptor = new StatementAdaptor(options); + } + + + @Override + public SqlSelect argBoolean(Boolean arg) { + return positionalArg(adaptor.nullString(booleanToString(arg))); + } + + + @Override + public SqlSelect argBoolean( String argName, Boolean arg) { + return namedArg(argName, adaptor.nullString(booleanToString(arg))); + } + + + @Override + public SqlSelect argInteger(Integer arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + + @Override + public SqlSelect argInteger( String argName, Integer arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + + @Override + public SqlSelect argLong(Long arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + + @Override + public SqlSelect argLong( String argName, Long arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + + @Override + public SqlSelect argFloat(Float arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + + @Override + public SqlSelect argFloat( String argName, Float arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + + @Override + public SqlSelect argDouble(Double arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + + @Override + public SqlSelect argDouble( String argName, Double arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + + @Override + public SqlSelect argBigDecimal(BigDecimal arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + + @Override + public SqlSelect argBigDecimal( String argName, BigDecimal arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + + @Override + public SqlSelect argString(String arg) { + return positionalArg(adaptor.nullString(arg)); + } + + + @Override + public SqlSelect argString( String argName, String arg) { + return namedArg(argName, adaptor.nullString(arg)); + } + + + @Override + public SqlSelect argDate(Date arg) { + // Date with time + return positionalArg(adaptor.nullDate(arg)); + } + + + @Override + public SqlSelect argDate( String argName, Date arg) { + // Date with time + return namedArg(argName, adaptor.nullDate(arg)); + } + + + @Override + public SqlSelect argLocalDate(LocalDate arg) { + // Date with no time + return positionalArg(adaptor.nullLocalDate(arg)); + } + + + @Override + public SqlSelect argLocalDate( String argName, LocalDate arg) { + // Date with no time + return namedArg(argName, adaptor.nullLocalDate(arg)); + } + + + @Override + public SqlSelect argDateNowPerApp() { + return positionalArg(adaptor.nullDate(options.currentDate())); + } + + + @Override + public SqlSelect argDateNowPerApp( String argName) { + return namedArg(argName, adaptor.nullDate(options.currentDate())); + } + + + @Override + public SqlSelect argDateNowPerDb() { + if (options.useDatePerAppOnly()) { + return positionalArg(adaptor.nullDate(options.currentDate())); + } + return positionalArg(new RewriteArg(options.flavor().dbTimeMillis())); + } + + + @Override + public SqlSelect argDateNowPerDb( String argName) { + if (options.useDatePerAppOnly()) { + return namedArg(argName, adaptor.nullDate(options.currentDate())); + } + return namedArg(argName, new RewriteArg(options.flavor().dbTimeMillis())); + } + + + @Override + public SqlSelect withTimeoutSeconds(int seconds) { + timeoutSeconds = seconds; + return this; + } + + + @Override + public SqlSelect withMaxRows(int rows) { + maxRows = rows; + return this; + } + + + @Override + public SqlSelect withArgs(SqlArgs args) { + return apply(args); + } + + + @Override + public SqlSelect apply(Apply apply) { + apply.apply(this); + return this; + } + + + @Override + public SqlSelect fetchSize(int rows) { + fetchSize = rows; + return this; + } + + + @Override + public Boolean queryBooleanOrNull() { + return queryWithTimeout(new RowsHandler() { + @Override + public Boolean process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getBooleanOrNull(); + } + return null; + } + }); + } + + @Override + public boolean queryBooleanOrFalse() { + Boolean result = queryBooleanOrNull(); + return result != null && result; + } + + @Override + public boolean queryBooleanOrTrue() { + Boolean result = queryBooleanOrNull(); + return result == null || result; + } + + @Override + + public Long queryLongOrNull() { + return queryWithTimeout(new RowsHandler() { + @Override + public Long process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getLongOrNull(1); + } + return null; + } + }); + } + + @Override + public long queryLongOrZero() { + return queryWithTimeout(new RowsHandler() { + @Override + public Long process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getLongOrZero(1); + } + return 0L; + } + }); + } + + + @Override + public List queryLongs() { + return queryWithTimeout(new RowsHandler>() { + @Override + public List process(Rows rs) throws Exception { + List result = new ArrayList<>(); + while (rs.next()) { + Long value = rs.getLongOrNull(1); + if (value != null) { + result.add(value); + } + } + return result; + } + }); + } + + + @Override + public Integer queryIntegerOrNull() { + return queryWithTimeout(new RowsHandler() { + @Override + public Integer process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getIntegerOrNull(1); + } + return null; + } + }); + } + + @Override + public int queryIntegerOrZero() { + return queryWithTimeout(new RowsHandler() { + @Override + public Integer process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getIntegerOrZero(1); + } + return 0; + } + }); + } + + + @Override + public List queryIntegers() { + return queryWithTimeout(new RowsHandler>() { + @Override + public List process(Rows rs) throws Exception { + List result = new ArrayList<>(); + while (rs.next()) { + Integer value = rs.getIntegerOrNull(1); + if (value != null) { + result.add(value); + } + } + return result; + } + }); + } + + + @Override + public Float queryFloatOrNull() { + return queryWithTimeout(new RowsHandler() { + @Override + public Float process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getFloatOrNull(1); + } + return null; + } + }); + } + + @Override + public float queryFloatOrZero() { + return queryWithTimeout(new RowsHandler() { + @Override + public Float process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getFloatOrZero(1); + } + return 0f; + } + }); + } + + + @Override + public List queryFloats() { + return queryWithTimeout(new RowsHandler>() { + @Override + public List process(Rows rs) throws Exception { + List result = new ArrayList<>(); + while (rs.next()) { + Float value = rs.getFloatOrNull(1); + if (value != null) { + result.add(value); + } + } + return result; + } + }); + } + + + @Override + public Double queryDoubleOrNull() { + return queryWithTimeout(new RowsHandler() { + @Override + public Double process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getDoubleOrNull(1); + } + return null; + } + }); + } + + @Override + public double queryDoubleOrZero() { + return queryWithTimeout(new RowsHandler() { + @Override + public Double process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getDoubleOrZero(1); + } + return 0d; + } + }); + } + + + @Override + public List queryDoubles() { + return queryWithTimeout(new RowsHandler>() { + @Override + public List process(Rows rs) throws Exception { + List result = new ArrayList<>(); + while (rs.next()) { + Double value = rs.getDoubleOrNull(1); + if (value != null) { + result.add(value); + } + } + return result; + } + }); + } + + + @Override + public BigDecimal queryBigDecimalOrNull() { + return queryWithTimeout(new RowsHandler() { + @Override + public BigDecimal process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getBigDecimalOrNull(1); + } + return null; + } + }); + } + + + @Override + public BigDecimal queryBigDecimalOrZero() { + return queryWithTimeout(new RowsHandler() { + @Override + public BigDecimal process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getBigDecimalOrZero(1); + } + return new BigDecimal(0); + } + }); + } + + + @Override + public List queryBigDecimals() { + return queryWithTimeout(new RowsHandler>() { + @Override + public List process(Rows rs) throws Exception { + List result = new ArrayList<>(); + while (rs.next()) { + BigDecimal value = rs.getBigDecimalOrNull(1); + if (value != null) { + result.add(value); + } + } + return result; + } + }); + } + + @Override + public String queryStringOrNull() { + return queryWithTimeout(new RowsHandler() { + @Override + public String process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getStringOrNull(1); + } + return null; + } + }); + } + + + @Override + public String queryStringOrEmpty() { + return queryWithTimeout(new RowsHandler() { + @Override + public String process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getStringOrEmpty(1); + } + return ""; + } + }); + } + + + @Override + public List queryStrings() { + return queryWithTimeout(new RowsHandler>() { + @Override + public List process(Rows rs) throws Exception { + List result = new ArrayList<>(); + while (rs.next()) { + String value = rs.getStringOrNull(1); + if (value != null) { + result.add(value); + } + } + return result; + } + }); + } + + + @Override + public Date queryDateOrNull() { + return queryWithTimeout(new RowsHandler() { + @Override + public Date process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getDateOrNull(1); + } + return null; + } + }); + } + + + @Override + public List queryDates() { + return queryWithTimeout(new RowsHandler>() { + @Override + public List process(Rows rs) throws Exception { + List result = new ArrayList<>(); + while (rs.next()) { + Date value = rs.getDateOrNull(1); + if (value != null) { + result.add(value); + } + } + return result; + } + }); + } + + + @Override + public LocalDate queryLocalDateOrNull() { + // Date without time + return queryWithTimeout(new RowsHandler() { + @Override + public LocalDate process(Rows rs) throws Exception { + if (rs.next()) { + return rs.getLocalDateOrNull(1); + } + return null; + } + }); + } + + + @Override + public List queryLocalDates() { + // Date without time + return queryWithTimeout(new RowsHandler>() { + @Override + public List process(Rows rs) throws Exception { + List result = new ArrayList<>(); + while (rs.next()) { + LocalDate value = rs.getLocalDateOrNull(1); + if (value != null) { + result.add(value); + } + } + return result; + } + }); + } + + @Override + public T query(RowsHandler rowsHandler) { + return queryWithTimeout(rowsHandler); + } + + @Override + public T queryOneOrNull(final RowHandler rowHandler) { + return queryWithTimeout(new RowsHandler() { + @Override + public T process(Rows rs) throws Exception { + if (rs.next()) { + T result = rowHandler.process(rs); + if (rs.next()) { + throw new ConstraintViolationException("Expected exactly one row to be returned but found multiple"); + } + return result; + } + return null; + } + }); + } + + @Override + public T queryOneOrThrow(RowHandler rowHandler) { + T result = queryOneOrNull(rowHandler); + if (result == null) { + throw new ConstraintViolationException("Expected exactly one row to be returned but found none"); + } + return result; + } + + @Override + public T queryFirstOrNull(final RowHandler rowHandler) { + return queryWithTimeout(new RowsHandler() { + @Override + public T process(Rows rs) throws Exception { + if (rs.next()) { + return rowHandler.process(rs); + } + return null; + } + }); + } + + @Override + public T queryFirstOrThrow(RowHandler rowHandler) { + T result = queryFirstOrNull(rowHandler); + if (result == null) { + throw new ConstraintViolationException("Expected one or more rows to be returned but found none"); + } + return result; + } + + @Override + public List queryMany(final RowHandler rowHandler) { + return queryWithTimeout(new RowsHandler>() { + @Override + public List process(Rows rs) throws Exception { + List result = new ArrayList<>(); + + while (rs.next()) { + T row = rowHandler.process(rs); + if (row != null) { + result.add(row); + } + } + + return result; + } + }); + } + + private SqlSelect positionalArg(Object arg) { + if (parameterList == null) { + parameterList = new ArrayList<>(); + } + parameterList.add(arg); + return this; + } + + private SqlSelect namedArg(String argName, Object arg) { + if (parameterMap == null) { + parameterMap = new HashMap<>(); + } + if (argName.startsWith(":")) { + argName = argName.substring(1); + } + parameterMap.put(argName, arg); + return this; + } + + private String booleanToString(Boolean b) { + return b == null ? null : b ? "Y" : "N"; + } + + private T queryWithTimeout(RowsHandler handler) { + assert ps == null; + ResultSet rs = null; + Metric metric = new Metric(log.isLoggable(Level.FINE)); + + String executeSql = sql; + Object[] parameters = null; + + boolean isWarn = false; + boolean isSuccess = false; + String errorCode = null; + Exception logEx = null; + try { + MixedParameterSql mpSql = new MixedParameterSql(sql, parameterList, parameterMap); + executeSql = mpSql.getSqlToExecute(); + parameters = mpSql.getArgs(); + + if (connection != null) { + synchronized (cancelLock) { + ps = connection.prepareStatement(executeSql); + } + + if (timeoutSeconds >= 0) { + ps.setQueryTimeout(timeoutSeconds); + } + + if (maxRows > 0) { + ps.setMaxRows(maxRows); + } + + if (fetchSize >= 0) { + ps.setFetchSize(fetchSize); + } + + adaptor.addParameters(ps, parameters); + metric.checkpoint("prep"); + rs = ps.executeQuery(); + metric.checkpoint("exec"); + final ResultSet finalRs = rs; + T result = handler.process(new RowsAdaptor(finalRs, options)); + metric.checkpoint("read"); + isSuccess = true; + return result; + } else { + return null; + } + } catch (SQLException e) { + if (e.getErrorCode() == 1013) { + isWarn = true; + // It's ambiguous based on the Oracle error code whether it was a timeout or cancel + throw new QueryTimedOutException("Timeout of " + timeoutSeconds + " seconds exceeded or user cancelled", e); + } + errorCode = options.generateErrorCode(); + logEx = e; + throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, parameters, errorCode, options), e); + } catch (Exception e) { + errorCode = options.generateErrorCode(); + logEx = e; + throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, parameters, errorCode, options), e); + } finally { + adaptor.closeQuietly(rs, log); + adaptor.closeQuietly(ps, log); + synchronized (cancelLock) { + ps = null; + } + metric.done("close"); + if (isSuccess) { + DebugSql.logSuccess("Query", log, metric, executeSql, parameters, options); + } else if (isWarn) { + DebugSql.logWarning("Query", log, metric, "QueryTimedOutException", executeSql, parameters, options, logEx); + } else { + DebugSql.logError("Query", log, metric, errorCode, executeSql, parameters, options, logEx); + } + } + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdate.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdate.java new file mode 100644 index 0000000..e2c14cc --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdate.java @@ -0,0 +1,165 @@ +package org.xbib.jdbc.query; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Date; + +/** + * Interface for configuring (setting parameters) and executing a chunk of SQL. + */ +public interface SqlUpdate { + + + SqlUpdate argBoolean(Boolean arg); + + + + SqlUpdate argBoolean( String argName, Boolean arg); + + + + SqlUpdate argInteger(Integer arg); + + + + SqlUpdate argInteger( String argName, Integer arg); + + + + SqlUpdate argLong(Long arg); + + + + SqlUpdate argLong( String argName, Long arg); + + + + SqlUpdate argFloat(Float arg); + + + + SqlUpdate argFloat( String argName, Float arg); + + + + SqlUpdate argDouble(Double arg); + + + + SqlUpdate argDouble( String argName, Double arg); + + + + SqlUpdate argBigDecimal(BigDecimal arg); + + + + SqlUpdate argBigDecimal( String argName, BigDecimal arg); + + + + SqlUpdate argString(String arg); + + + + SqlUpdate argString( String argName, String arg); + + + + SqlUpdate argDate(Date arg); // Date with timestamp + + + + SqlUpdate argDate( String argName, Date arg); // Date with timestamp + + + + SqlUpdate argLocalDate(LocalDate arg); // Date only - no timestamp + + + + SqlUpdate argLocalDate( String argName, LocalDate arg); // Date only - no timestamp + + + + SqlUpdate argDateNowPerApp(); + + + + SqlUpdate argDateNowPerApp( String argName); + + + + SqlUpdate argDateNowPerDb(); + + + + SqlUpdate argDateNowPerDb( String argName); + + + + SqlUpdate argBlobBytes(byte[] arg); + + + + SqlUpdate argBlobBytes( String argName, byte[] arg); + + + + SqlUpdate argBlobStream(InputStream arg); + + + + SqlUpdate argBlobStream( String argName, InputStream arg); + + + + SqlUpdate argClobString(String arg); + + + + SqlUpdate argClobString( String argName, String arg); + + + + SqlUpdate argClobReader(Reader arg); + + + + SqlUpdate argClobReader( String argName, Reader arg); + + + + SqlUpdate withArgs(SqlArgs args); + + + + SqlUpdate apply(Apply apply); + + /** + * Execute the SQL update and return the number of rows was affected. + */ + int update(); + + /** + * Execute the SQL update and check that the expected number of rows was affected. + * + * @throws WrongNumberOfRowsException if the number of rows affected did not match + * the value provided + */ + void update(int expectedRowsUpdated); + + /** + * Call this between setting rows of parameters for a SQL statement. You may call it before + * setting any parameters, after setting all, or multiple times between rows. + */ +// SqlUpdate batch(); + +// SqlUpdate withTimeoutSeconds(int seconds); + + interface Apply { + void apply(SqlUpdate update); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdateImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdateImpl.java new file mode 100644 index 0000000..98d01ed --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/SqlUpdateImpl.java @@ -0,0 +1,334 @@ +package org.xbib.jdbc.query; + +import org.xbib.jdbc.query.util.DebugSql; +import org.xbib.jdbc.query.util.InternalStringReader; +import org.xbib.jdbc.query.util.Metric; +import org.xbib.jdbc.query.util.RewriteArg; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This is the key class for configuring (query parameters) and executing a database query. + */ +public class SqlUpdateImpl implements SqlUpdate { + + private static final Logger log = Logger.getLogger(Database.class.getName()); + + private final Connection connection; + + private final StatementAdaptor adaptor; + + private final String sql; + + private final Options options; + + private List parameterList; + + private Map parameterMap; + + public SqlUpdateImpl(Connection connection, String sql, Options options) { + this.connection = connection; + this.sql = sql; + this.options = options; + adaptor = new StatementAdaptor(options); + } + + + @Override + public SqlUpdate argBoolean(Boolean arg) { + return positionalArg(adaptor.nullString(booleanToString(arg))); + } + + + @Override + public SqlUpdate argBoolean(String argName, Boolean arg) { + return namedArg(argName, adaptor.nullString(booleanToString(arg))); + } + + @Override + + public SqlUpdate argInteger(Integer arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + @Override + + public SqlUpdate argInteger(String argName, Integer arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + @Override + + public SqlUpdate argLong(Long arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + @Override + + public SqlUpdate argLong(String argName, Long arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + @Override + + public SqlUpdate argFloat(Float arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + @Override + + public SqlUpdate argFloat(String argName, Float arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + @Override + + public SqlUpdate argDouble(Double arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + @Override + + public SqlUpdate argDouble(String argName, Double arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + @Override + + public SqlUpdate argBigDecimal(BigDecimal arg) { + return positionalArg(adaptor.nullNumeric(arg)); + } + + @Override + + public SqlUpdate argBigDecimal(String argName, BigDecimal arg) { + return namedArg(argName, adaptor.nullNumeric(arg)); + } + + @Override + + public SqlUpdate argString(String arg) { + return positionalArg(adaptor.nullString(arg)); + } + + @Override + + public SqlUpdate argString(String argName, String arg) { + return namedArg(argName, adaptor.nullString(arg)); + } + + @Override + + public SqlUpdate argDate(Date arg) { + // Date with time + return positionalArg(adaptor.nullDate(arg)); + } + + @Override + + public SqlUpdate argDate(String argName, Date arg) { + // Date with time + return namedArg(argName, adaptor.nullDate(arg)); + } + + @Override + + public SqlUpdate argLocalDate(LocalDate arg) { + // Date with no time + return positionalArg(adaptor.nullLocalDate(arg)); + } + + @Override + + public SqlUpdate argLocalDate(String argName, LocalDate arg) { + // Date with no time + return namedArg(argName, adaptor.nullLocalDate(arg)); + } + + + @Override + public SqlUpdate argDateNowPerApp() { + return positionalArg(adaptor.nullDate(options.currentDate())); + } + + @Override + + public SqlUpdate argDateNowPerApp(String argName) { + return namedArg(argName, adaptor.nullDate(options.currentDate())); + } + + + @Override + public SqlUpdate argDateNowPerDb() { + if (options.useDatePerAppOnly()) { + return positionalArg(adaptor.nullDate(options.currentDate())); + } + return positionalArg(new RewriteArg(options.flavor().dbTimeMillis())); + } + + @Override + + public SqlUpdate argDateNowPerDb(String argName) { + if (options.useDatePerAppOnly()) { + return namedArg(argName, adaptor.nullDate(options.currentDate())); + } + return namedArg(argName, new RewriteArg(options.flavor().dbTimeMillis())); + } + + @Override + + public SqlUpdate argBlobBytes(byte[] arg) { + return positionalArg(adaptor.nullBytes(arg)); + } + + @Override + + public SqlUpdate argBlobBytes(String argName, byte[] arg) { + return namedArg(argName, adaptor.nullBytes(arg)); + } + + @Override + + public SqlUpdate argBlobStream(InputStream arg) { + return positionalArg(adaptor.nullInputStream(arg)); + } + + @Override + + public SqlUpdate argBlobStream(String argName, InputStream arg) { + return namedArg(argName, adaptor.nullInputStream(arg)); + } + + @Override + + public SqlUpdate argClobString(String arg) { + return positionalArg(adaptor.nullClobReader(arg == null ? null : new InternalStringReader(arg))); + } + + @Override + + public SqlUpdate argClobString(String argName, String arg) { + return namedArg(argName, adaptor.nullClobReader(arg == null ? null : new InternalStringReader(arg))); + } + + @Override + + public SqlUpdate argClobReader(Reader arg) { + return positionalArg(adaptor.nullClobReader(arg)); + } + + @Override + + public SqlUpdate argClobReader(String argName, Reader arg) { + return namedArg(argName, adaptor.nullClobReader(arg)); + } + + + @Override + public SqlUpdate withArgs(SqlArgs args) { + return apply(args); + } + + + @Override + public SqlUpdate apply(Apply apply) { + apply.apply(this); + return this; + } + + @Override + public int update() { + return updateInternal(0); + } + + @Override + public void update(int expectedNumAffectedRows) { + updateInternal(expectedNumAffectedRows); + } + + private int updateInternal(int expectedNumAffectedRows) { + PreparedStatement ps = null; + Metric metric = new Metric(log.isLoggable(Level.FINE)); + + String executeSql = sql; + Object[] parameters = null; + + boolean isSuccess = false; + String errorCode = null; + Exception logEx = null; + try { + MixedParameterSql mpSql = new MixedParameterSql(sql, parameterList, parameterMap); + executeSql = mpSql.getSqlToExecute(); + parameters = mpSql.getArgs(); + + if (connection != null) { + ps = connection.prepareStatement(executeSql); + adaptor.addParameters(ps, parameters); + metric.checkpoint("prep"); + int numAffectedRows = ps.executeUpdate(); + metric.checkpoint("exec", numAffectedRows); + if (expectedNumAffectedRows > 0 && numAffectedRows != expectedNumAffectedRows) { + errorCode = options.generateErrorCode(); + throw new WrongNumberOfRowsException("The number of affected rows was " + numAffectedRows + ", but " + + expectedNumAffectedRows + " were expected." + "\n" + + DebugSql.exceptionMessage(executeSql, parameters, errorCode, options)); + } + isSuccess = true; + return numAffectedRows; + } else { + return -1; + } + } catch (WrongNumberOfRowsException e) { + throw e; + } catch (Exception e) { + errorCode = options.generateErrorCode(); + logEx = e; + throw DatabaseException.wrap(DebugSql.exceptionMessage(executeSql, parameters, errorCode, options), e); + } finally { + adaptor.closeQuietly(ps, log); + metric.done("close"); + if (isSuccess) { + DebugSql.logSuccess("Update", log, metric, executeSql, parameters, options); + } else { + DebugSql.logError("Update", log, metric, errorCode, executeSql, parameters, options, logEx); + } + } + } + + + private SqlUpdate positionalArg(Object arg) { + if (parameterList == null) { + parameterList = new ArrayList<>(); + } + parameterList.add(arg); + return this; + } + + + private SqlUpdate namedArg(String argName, Object arg) { + if (parameterMap == null) { + parameterMap = new HashMap<>(); + } + if (argName.startsWith(":")) { + argName = argName.substring(1); + } + parameterMap.put(argName, arg); + return this; + } + + private String booleanToString(Boolean b) { + return b == null ? null : b ? "Y" : "N"; + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/StatementAdaptor.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/StatementAdaptor.java new file mode 100644 index 0000000..47099da --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/StatementAdaptor.java @@ -0,0 +1,220 @@ +package org.xbib.jdbc.query; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.LocalDate; +import java.util.Date; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Deal with mapping parameters into prepared statements. + */ +public class StatementAdaptor { + private final Options options; + + public StatementAdaptor(Options options) { + this.options = options; + } + + private static String readerToString(Reader r) { + Scanner s = new Scanner(r).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } + + private static byte[] streamToBytes(InputStream is) throws SQLException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + + try { + while ((length = is.read(buffer)) != -1) { + out.write(buffer, 0, length); + } + } catch (IOException e) { + throw new SQLException("Unable to convert InputStream parameter to bytes", e); + } + + return out.toByteArray(); + } + + /** + * Converts the java.util.Date into a java.sql.Timestamp, following the nanos/millis canonicalization + * required by the spec. If a java.sql.Timestamp is passed in (since it extends java.util.Date), + * it will be checked and canonicalized only if not already correct. + */ + private static Timestamp toSqlTimestamp(Date date) { + long millis = date.getTime(); + int fractionalSecondMillis = (int) (millis % 1000); // guaranteed < 1000 + + if (fractionalSecondMillis == 0) { // this means it's already correct by the spec + if (date instanceof Timestamp) { + return (Timestamp) date; + } else { + return new Timestamp(millis); + } + } else { // the millis are invalid and need to be corrected + int tsNanos = fractionalSecondMillis * 1000000; + long tsMillis = millis - fractionalSecondMillis; + Timestamp timestamp = new Timestamp(tsMillis); + timestamp.setNanos(tsNanos); + return timestamp; + } + } + + public void addParameters(PreparedStatement ps, Object[] parameters) throws SQLException { + for (int i = 0; i < parameters.length; i++) { + Object parameter = parameters[i]; + + // Unwrap secret args here so we can use them + if (parameter instanceof SecretArg) { + parameter = ((SecretArg) parameter).getArg(); + } + + if (parameter == null) { + ParameterMetaData metaData; + int parameterType; + try { + metaData = ps.getParameterMetaData(); + parameterType = metaData.getParameterType(i + 1); + } catch (SQLException e) { + throw new DatabaseException("Parameter " + (i + 1) + + " was null and the JDBC driver could not report the type of this column." + + " Please update the JDBC driver to support PreparedStatement.getParameterMetaData()" + + " or use SqlNull in place of null values to this query.", e); + } + ps.setNull(i + 1, parameterType); + } else if (parameter instanceof SqlNull) { + SqlNull sqlNull = (SqlNull) parameter; + if (options.useBytesForBlob() && sqlNull.getType() == Types.BLOB) { + // The setNull() seems more correct, but PostgreSQL chokes on it + ps.setBytes(i + 1, null); + } else { + ps.setNull(i + 1, sqlNull.getType()); + } + } else if (parameter instanceof java.sql.Date) { + ps.setDate(i + 1, (java.sql.Date) parameter); + } else if (parameter instanceof Date) { + // this will correct the millis and nanos according to the JDBC spec + // if a correct Timestamp is passed in, this will detect that and leave it alone + ps.setTimestamp(i + 1, toSqlTimestamp((Date) parameter), options.calendarForTimestamps()); + } else if (parameter instanceof Reader) { + if (options.useStringForClob()) { + ps.setString(i + 1, readerToString((Reader) parameter)); + } else { + ps.setCharacterStream(i + 1, (Reader) parameter); + } + } else if (parameter instanceof InputStream) { + if (options.useBytesForBlob()) { + ps.setBytes(i + 1, streamToBytes((InputStream) parameter)); + } else { + ps.setBinaryStream(i + 1, (InputStream) parameter); + } + } else if (parameter instanceof Float) { + //if (options.flavor() == Flavor.oracle && ps.isWrapperFor(OraclePreparedStatement.class)) { + // The Oracle 11 driver setDouble() first converts the double to NUMBER, causing underflow + // for small values so we need to use the proprietary mechanism + //ps.unwrap(OraclePreparedStatement.class).setBinaryFloat(i + 1, (Float) parameter); + //} else { + ps.setFloat(i + 1, (Float) parameter); + //} + } else if (parameter instanceof Double) { + //if (options.flavor() == Flavor.oracle && ps.isWrapperFor(OraclePreparedStatement.class)) { + // The Oracle 11 driver setDouble() first converts the double to NUMBER, causing underflow + // for small values so we need to use the proprietary mechanism + //ps.unwrap(OraclePreparedStatement.class).setBinaryDouble(i + 1, (Double) parameter); + //} else { + ps.setDouble(i + 1, (Double) parameter); + //} + } else { + ps.setObject(i + 1, parameter); + } + } + } + + public Object nullDate(Date arg) { + if (arg == null) { + return new SqlNull(Types.TIMESTAMP); + } + return new Timestamp(arg.getTime()); + } + + // Processes a true date without time information. + public Object nullLocalDate(LocalDate arg) { + if (arg == null) { + return new SqlNull(Types.DATE); + } + + return java.sql.Date.valueOf(arg); + } + + public Object nullNumeric(Number arg) { + if (arg == null) { + return new SqlNull(Types.NUMERIC); + } + return arg; + } + + public Object nullString(String arg) { + if (arg == null) { + return new SqlNull(Types.VARCHAR); + } + return arg; + } + + public Object nullClobReader(Reader arg) { + if (arg == null) { + return new SqlNull(Types.VARCHAR); + } + return arg; + } + + public Object nullBytes(byte[] arg) { + if (arg == null) { + return new SqlNull(Types.BLOB); + } + return arg; + } + + public Object nullInputStream(InputStream arg) { + if (arg == null) { + return new SqlNull(Types.BLOB); + } + return arg; + } + + public void closeQuietly(ResultSet rs, Logger log) { + if (rs != null) { + try { + rs.close(); + } catch (Exception e) { + if (log != null) { + log.log(Level.SEVERE, "Caught exception closing the ResultSet", e); + } + } + } + } + + public void closeQuietly( Statement s, Logger log) { + if (s != null) { + try { + s.close(); + } catch (Exception e) { + if (log != null) { + log.log(Level.SEVERE, "Caught exception closing the Statement", e); + } + } + } + } + +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/Transaction.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/Transaction.java new file mode 100644 index 0000000..af78996 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/Transaction.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015 The Board of Trustees of The Leland Stanford Junior University. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.xbib.jdbc.query; + +/** + * Allow customization of the transaction behavior. + */ +public interface Transaction { + /** + * @return whether this code block has requested rollback upon a {@link Throwable} + * being thrown from the run method - this only reflects what was requested + * by calling {@link #setRollbackOnError(boolean)}, which is not necessarily + * what will actually happen + */ + boolean isRollbackOnError(); + + /** + * Use this to request either "commit always" or "commit unless error" behavior. + * This will have no effect if {@link #isRollbackOnly()} returns true. + * + * @param rollbackOnError true to rollback after errors; false to commit or rollback based on + * the other settings + * @see DatabaseProvider#transact(DbCodeTx) + */ + void setRollbackOnError(boolean rollbackOnError); + + /** + * @return whether this code block has requested unconditional rollback - this only + * reflects what was requested by calling {@link #setRollbackOnly(boolean)}, + * which is not necessarily what will actually happen + */ + boolean isRollbackOnly(); + + /** + *

If your code inside run() decides for some reason the transaction should rollback + * rather than commit, use this method.

+ * + * @param rollbackOnly true to request an unconditional rollback; false to commit or rollback based on + * the other settings + * @see DatabaseProvider#transact(DbCodeTx) + */ + void setRollbackOnly(boolean rollbackOnly); +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/TransactionImpl.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/TransactionImpl.java new file mode 100644 index 0000000..b518cef --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/TransactionImpl.java @@ -0,0 +1,30 @@ +package org.xbib.jdbc.query; + +/** + * Simple bean representing how the database transaction should behave + * in terms of commit/rollback. + */ +public class TransactionImpl implements Transaction { + private boolean rollbackOnError; + private boolean rollbackOnly; + + @Override + public boolean isRollbackOnError() { + return rollbackOnError; + } + + @Override + public void setRollbackOnError(boolean rollbackOnError) { + this.rollbackOnError = rollbackOnError; + } + + @Override + public boolean isRollbackOnly() { + return rollbackOnly; + } + + @Override + public void setRollbackOnly(boolean rollbackOnly) { + this.rollbackOnly = rollbackOnly; + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/When.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/When.java new file mode 100644 index 0000000..d66c152 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/When.java @@ -0,0 +1,75 @@ +package org.xbib.jdbc.query; + +import java.util.Objects; + +/** + * Convenience for conditional SQL generation. + */ +public class When { + + private final Flavor actualFlavor; + + private String chosen; + + public When(Flavor actualFlavor) { + this.actualFlavor = actualFlavor; + } + + public When oracle(String sql) { + if (actualFlavor == Flavor.oracle) { + chosen = sql; + } + return this; + } + + public When derby(String sql) { + if (actualFlavor == Flavor.derby) { + chosen = sql; + } + return this; + } + + public When postgres(String sql) { + if (actualFlavor == Flavor.postgresql) { + chosen = sql; + } + return this; + } + + public When sqlserver(String sql) { + if (actualFlavor == Flavor.sqlserver) { + chosen = sql; + } + return this; + } + + + public String other(String sql) { + if (chosen == null) { + chosen = sql; + } + return chosen; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + When when = (When) o; + return Objects.equals(chosen, when.chosen) && actualFlavor == when.actualFlavor; + } + + @Override + public int hashCode() { + return Objects.hash(chosen, actualFlavor); + } + + @Override + public String toString() { + return chosen == null ? "" : chosen; + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/WrongNumberOfRowsException.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/WrongNumberOfRowsException.java new file mode 100644 index 0000000..860d4c6 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/WrongNumberOfRowsException.java @@ -0,0 +1,13 @@ +package org.xbib.jdbc.query; + +/** + * Thrown when inserting/updating rows and the actual number of rows modified does + * not match the expected number of rows. + */ +@SuppressWarnings("serial") +public class WrongNumberOfRowsException extends DatabaseException { + + public WrongNumberOfRowsException(String message) { + super(message); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/util/DebugSql.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/DebugSql.java new file mode 100644 index 0000000..c3f469f --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/DebugSql.java @@ -0,0 +1,175 @@ +package org.xbib.jdbc.query.util; + +import org.xbib.jdbc.query.Options; +import org.xbib.jdbc.query.SecretArg; +import org.xbib.jdbc.query.SqlNull; + +import java.io.InputStream; +import java.io.Reader; +import java.util.Arrays; +import java.util.Date; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Convenience class to substitute real values into a database query for debugging, logging, etc. + *

+ * WARNING!!! Never execute this SQL without manual inspection because this class does NOTHING + * to prevent SQL injection or any other bad things. + */ +public class DebugSql { + public static final String PARAM_SQL_SEPARATOR = "\tParamSql:\t"; + + public static String printDebugOnlySqlString(String sql, Object[] args, Options options) { + StringBuilder buf = new StringBuilder(); + printSql(buf, sql, args, false, true, options); + return buf.toString(); + } + + public static void printSql(StringBuilder buf, String sql, Object[] args, Options options) { + printSql(buf, sql, args, true, options.isLogParameters(), options); + } + + public static void printSql(StringBuilder buf, String sql, Object[] args, boolean includeExecSql, + boolean includeParameters, Options options) { + Object[] argsToPrint = args; + if (argsToPrint == null) { + argsToPrint = new Object[0]; + } + int batchSize = -1; + if (argsToPrint.length > 0 && argsToPrint instanceof Object[][]) { + // The arguments provided were from a batch - just use the first set + batchSize = argsToPrint.length; + argsToPrint = (Object[]) argsToPrint[0]; + } + String[] sqlParts = sql.split("\\?"); + if (sqlParts.length != argsToPrint.length + (sql.endsWith("?") ? 0 : 1)) { + buf.append("(wrong # args) query: "); + buf.append(sql); + if (args != null) { + buf.append(" args: "); + if (includeParameters) { + buf.append(Arrays.toString(argsToPrint)); + } else { + buf.append(argsToPrint.length); + } + } + } else { + if (includeExecSql) { + buf.append(removeTabs(sql)); + } + if (includeParameters && argsToPrint.length > 0) { + if (includeExecSql) { + buf.append(PARAM_SQL_SEPARATOR); + } + for (int i = 0; i < argsToPrint.length; i++) { + buf.append(removeTabs(sqlParts[i])); + Object argToPrint = argsToPrint[i]; + if (argToPrint instanceof String) { + String argToPrintString = (String) argToPrint; + int maxLength = options.maxStringLengthParam(); + if (argToPrintString.length() > maxLength && maxLength > 0) { + buf.append("'").append(argToPrintString, 0, maxLength).append("...'"); + } else { + buf.append("'"); + buf.append(removeTabs(escapeSingleQuoted(argToPrintString))); + buf.append("'"); + } + } else if (argToPrint instanceof SqlNull || argToPrint == null) { + buf.append("null"); + } else if (argToPrint instanceof java.sql.Timestamp) { + buf.append(options.flavor().dateAsSqlFunction((Date) argToPrint, options.calendarForTimestamps())); + } else if (argToPrint instanceof java.sql.Date) { + buf.append(options.flavor().localDateAsSqlFunction((Date) argToPrint)); + } else if (argToPrint instanceof Number) { + buf.append(argToPrint); + } else if (argToPrint instanceof Boolean) { + buf.append(((Boolean) argToPrint) ? "'Y'" : "'N'"); + } else if (argToPrint instanceof SecretArg) { + buf.append(""); + } else if (argToPrint instanceof InternalStringReader) { + String argToPrintString = ((InternalStringReader) argToPrint).getString(); + int maxLength = options.maxStringLengthParam(); + if (argToPrintString.length() > maxLength && maxLength > 0) { + buf.append("'").append(argToPrintString, 0, maxLength).append("...'"); + } else { + buf.append("'"); + buf.append(removeTabs(escapeSingleQuoted(argToPrintString))); + buf.append("'"); + } + } else if (argToPrint instanceof Reader || argToPrint instanceof InputStream) { + buf.append("<").append(argToPrint.getClass().getName()).append(">"); + } else if (argToPrint instanceof byte[]) { + buf.append("<").append(((byte[]) argToPrint).length).append(" bytes>"); + } else { + buf.append(""); + } + } + if (sqlParts.length > argsToPrint.length) { + buf.append(sqlParts[sqlParts.length - 1]); + } + } + } + if (batchSize != -1) { + buf.append(" (first in batch of "); + buf.append(batchSize); + buf.append(')'); + } + } + + private static String removeTabs(String s) { + return s == null ? null : s.replace("\t", ""); + } + + private static String escapeSingleQuoted(String s) { + return s == null ? null : s.replace("'", "''"); + } + + public static String exceptionMessage(String sql, Object[] parameters, String errorCode, Options options) { + StringBuilder buf = new StringBuilder("Error executing SQL"); + if (errorCode != null) { + buf.append(" (errorCode=").append(errorCode).append(")"); + } + if (options.isDetailedExceptions()) { + buf.append(": "); + DebugSql.printSql(buf, sql, parameters, options); + } + return buf.toString(); + } + + public static void logSuccess(String sqlType, Logger log, Metric metric, String sql, Object[] args, Options options) { + if (log.isLoggable(Level.FINE)) { + String msg = logMiddle('\t', sqlType, metric, null, sql, args, options); + log.fine(msg); + } + } + + public static void logWarning(String sqlType, Logger log, Metric metric, String errorCode, String sql, Object[] args, + Options options, Throwable t) { + if (log.isLoggable(Level.WARNING)) { + String msg = logMiddle(' ', sqlType, metric, errorCode, sql, args, options); + log.log(Level.WARNING, msg, t); + } + } + + public static void logError(String sqlType, Logger log, Metric metric, String errorCode, String sql, Object[] args, + Options options, Throwable t) { + if (log.isLoggable(Level.SEVERE)) { + String msg = logMiddle(' ', sqlType, metric, errorCode, sql, args, options); + log.log(Level.SEVERE, msg, t); + } + } + + private static String logMiddle(char separator, String sqlType, Metric metric, + String errorCode, String sql, Object[] args, Options options) { + StringBuilder buf = new StringBuilder(); + if (errorCode != null) { + buf.append("errorCode=").append(errorCode).append(" "); + } + buf.append(sqlType).append(": "); + metric.printMessage(buf); + buf.append(separator); + printSql(buf, sql, args, options); + return buf.toString(); + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/util/InternalStringReader.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/InternalStringReader.java new file mode 100644 index 0000000..3e807c6 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/InternalStringReader.java @@ -0,0 +1,21 @@ +package org.xbib.jdbc.query.util; + +import java.io.StringReader; + +/** + * This class exists to distinguish cases where we are mapping String to Reader + * internally, but want to be able to know they really started as a String (and + * be able to get the String back for things like logging). + */ +public final class InternalStringReader extends StringReader { + private final String s; + + public InternalStringReader(String s) { + super(s); + this.s = s; + } + + public String getString() { + return s; + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/util/Metric.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/Metric.java new file mode 100644 index 0000000..dd3ec02 --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/Metric.java @@ -0,0 +1,243 @@ +package org.xbib.jdbc.query.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class provides explicit instrumentation functionality. + */ +public class Metric { + + private final boolean enabled; + + private boolean done; + + private long startNanos; + + private long lastCheckpointNanos; + + private List checkpoints; + + /** + *

Create a metric tracking object and start the nanosecond timer. Times + * are obtained using {@code System.nanoTime()}. The canonical way to use + * this looks something like this: + *

+ * + *
+     *   Metric metric = new Metric(log.isDebugEnabled);
+     *   ...
+     *   metric.checkpoint("received");
+     *   ...
+     *   metric.checkpoint("processed");
+     *   ...
+     *   metric.done("sent");
+     *   ...
+     *   if (log.isDebugEnabled()) {
+     *    log.debug("Processed: " + metric.getMessage());
+     *   }
+     * 
+ * + * @param enabled {@code true} if timings will be taken, {@code false} to + * optimize out the time tracking + */ + public Metric(boolean enabled) { + this.enabled = enabled; + if (enabled) { + checkpoints = new ArrayList<>(); + startNanos = System.nanoTime(); + lastCheckpointNanos = startNanos; + } + } + + /** + * Find out how many milliseconds have elapsed since this timer was started. + * + * @return the number of milliseconds elapsed, or -1 if {@code false} was + * passed in the constructor + */ + public long elapsedMillis() { + if (!enabled) { + return -1; + } + return (System.nanoTime() - startNanos) / 1000000; + } + + /** + * Find out how many nanoseconds have elapsed since this timer was started. + * + * @return the number of nanoseconds elapsed, or -1 if {@code false} was + * passed in the constructor + */ + public long elapsedNanos() { + if (!enabled) { + return -1; + } + return (System.nanoTime() - startNanos); + } + + /** + * Set a mark for timing. It is strongly recommended to use a short, + * simple alphanumeric description. For example, "sent" or "didThat". + * With this version you can provide additional arguments, such as + * byte counts, which will be appended comma-separated within brackets. + * For example, {@code checkpoint("sent", 25, 3)} will result in a + * description "sent[25,3]". The string evaluation and concatenation is + * lazy, and won't be done if this metric is disabled. + * + * @param description a label for this mark; may not be null; spaces + * and tabs will be converted to underscores + * @param args additional information to append to the description; will + * print "null" if null; evaluated with String.valueOf() lazily + * and sanitized of most non-alphanumeric characters + */ + public void checkpoint(String description, Object... args) { + if (enabled) { + long currentCheckpointNanos = System.nanoTime(); + checkpoints.add(new Checkpoint(noTabsOrSpaces(description), currentCheckpointNanos - lastCheckpointNanos, args)); + lastCheckpointNanos = currentCheckpointNanos; + } + } + + /** + * Set a final mark for timing and stop the timer. Once you call this + * method, subsequent calls have no effect. + * + * @param description a label for this mark; may not be null; spaces + * and tabs will be converted to underscores + * @return time in nanoseconds from the start of this metric, or -1 + * if {@code false} was passed in the constructor + */ + public long done(String description, Object... args) { + checkpoint(description, args); + return done(); + } + + /** + * Indicate we are done (stop the timer). Once you call this + * method, subsequent calls have no effect. + * + * @return time in nanoseconds from the start of this metric, or -1 + * if {@code false} was passed in the constructor + */ + public long done() { + if (enabled) { + if (!done) { + lastCheckpointNanos = System.nanoTime(); + done = true; + } + return lastCheckpointNanos - startNanos; + } + return -1; + } + + /** + * Construct and return a message based on the timing and checkpoints. This + * will look like "123.456ms(checkpoint1=100.228ms,checkpoint2=23.228ms)" + * without the quotes. There will be no spaces or tabs in the output. + * + *

This will automatically call the done() method to stop the timer if + * you haven't already done so.

+ * + * @return a string with timing information, or {@code "metricsDisabled"} + * if {@code false} was passed in the constructor. + * @see #printMessage(StringBuilder) + */ + public String getMessage() { + if (enabled) { + StringBuilder buf = new StringBuilder(); + printMessage(buf); + return buf.toString(); + } + return "metricsDisabled"; + } + + /** + * Construct and print a message based on the timing and checkpoints. This + * will look like "123.456ms(checkpoint1=100.228ms,checkpoint2=23.228ms)" + * without the quotes. There will be no spaces or tabs in the output. A + * value of {@code "metricsDisabled"} will be printed if {@code false} was + * passed in the constructor. + * + *

This will automatically call the done() method to stop the timer if + * you haven't already done so.

+ * + * @param buf the message will be printed to this builder + * @see #getMessage() + */ + public void printMessage(StringBuilder buf) { + if (enabled) { + done(); + writeNanos(buf, lastCheckpointNanos - startNanos); + if (!checkpoints.isEmpty()) { + buf.append("("); + boolean first = true; + for (Checkpoint checkpoint : checkpoints) { + if (first) { + first = false; + } else { + buf.append(','); + } + buf.append(checkpoint.description); + if (checkpoint.args != null && checkpoint.args.length > 0) { + buf.append('['); + boolean firstArg = true; + for (Object o : checkpoint.args) { + if (firstArg) { + firstArg = false; + } else { + buf.append(','); + } + buf.append(sanitizeArg(String.valueOf(o))); + } + buf.append(']'); + } + buf.append('='); + writeNanos(buf, checkpoint.durationNanos); + } + buf.append(')'); + } + } else { + buf.append("metricsDisabled"); + } + } + + private void writeNanos(StringBuilder buf, long nanos) { + if (nanos < 0) { + buf.append("-"); + nanos = -nanos; + } + String nanosStr = Long.toString(nanos); + if (nanosStr.length() > 6) { + buf.append(nanosStr, 0, nanosStr.length() - 6); + buf.append('.'); + buf.append(nanosStr, nanosStr.length() - 6, nanosStr.length() - 3); + } else { + buf.append("0.0000000", 0, 8 - Math.max(nanosStr.length(), 4)); + if (nanosStr.length() > 3) { + buf.append(nanosStr, 0, nanosStr.length() - 3); + } + } + buf.append("ms"); + } + + private String noTabsOrSpaces(String s) { + return s.replace(' ', '_').replace('\t', '_'); + } + + private String sanitizeArg(String s) { + return s.replaceAll("[^\\p{Alnum}_.\\-\\+]", "*"); + } + + private static class Checkpoint { + private final Object[] args; + String description; + long durationNanos; + + Checkpoint(String description, long durationNanos, Object... args) { + this.description = description; + this.durationNanos = durationNanos; + this.args = args; + } + } +} diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/util/RewriteArg.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/RewriteArg.java new file mode 100644 index 0000000..675bc7b --- /dev/null +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/util/RewriteArg.java @@ -0,0 +1,14 @@ +package org.xbib.jdbc.query.util; + +public class RewriteArg { + + private final String sql; + + public RewriteArg(String sql) { + this.sql = sql; + } + + public String getSql() { + return sql; + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/CommonTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/CommonTest.java new file mode 100644 index 0000000..eac47ee --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/CommonTest.java @@ -0,0 +1,1715 @@ +package org.xbib.jdbc.query.test; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.jdbc.query.ConstraintViolationException; +import org.xbib.jdbc.query.Database; +import org.xbib.jdbc.query.DatabaseException; +import org.xbib.jdbc.query.DatabaseProvider; +import org.xbib.jdbc.query.OptionsDefault; +import org.xbib.jdbc.query.OptionsOverride; +import org.xbib.jdbc.query.Row; +import org.xbib.jdbc.query.RowHandler; +import org.xbib.jdbc.query.RowsHandler; +import org.xbib.jdbc.query.Schema; +import org.xbib.jdbc.query.Sql; +import org.xbib.jdbc.query.SqlArgs; +import org.xbib.jdbc.query.StatementAdaptor; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.math.BigDecimal; +import java.sql.ResultSetMetaData; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.Month; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Exercise Database functionality with a real databases. + */ +public abstract class CommonTest { + + final static String TEST_TABLE_NAME = "dbtest"; + + /** + * Enable retrying failed tests if they have the @Retry annotation. + */ + + protected DatabaseProvider dbp; + + protected Database db; + + protected Date now = new Date(); + + protected LocalDate localDateNow = LocalDate.now(); + + @BeforeEach + public void setupJdbc() throws Exception { + dbp = createDatabaseProvider(new OptionsOverride() { + @Override + public Date currentDate() { + return now; + } + + @Override + public Calendar calendarForTimestamps() { + return Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles")); + } + }); + db = dbp.get(); + db.dropTableQuietly(TEST_TABLE_NAME); + } + + protected abstract DatabaseProvider createDatabaseProvider(OptionsOverride options) throws Exception; + + @AfterEach + public void closeJdbc() { + if (dbp != null) { + dbp.commitAndClose(); + } + } + + @Test + public void tableExists() { + // Verify dbtest table does not exist + String lowercaseTable = TEST_TABLE_NAME.toLowerCase(); + testTableLookup(lowercaseTable); + db.dropTableQuietly(lowercaseTable); + + // Let's try creating a table with an upper case name and verify it works + String uppercaseTable = TEST_TABLE_NAME.toUpperCase(); + testTableLookup(uppercaseTable); + db.dropTableQuietly(uppercaseTable); + + // Verify that null or empty name is handled gracefully + assertFalse(db.tableExists(null)); + assertFalse(db.tableExists("")); + } + + private void testTableLookup(String tableName) { + // Verify test table does not exist + assertFalse(db.tableExists(tableName)); + + // Create and verify it exists. + new Schema().addTable(tableName).addColumn("pk").primaryKey().schema().execute(db); + assertTrue(db.tableExists(tableName)); + } + + @Test + public void normalizeTableName() { + // Verify that null and empty cases are handled gracefully + assertNull(db.normalizeTableName(null)); + assertEquals("", db.normalizeTableName("")); + + // Verify a quoted table name is returned in exactly the same case, with quotes removed. + String camelCaseTableName = "\"DbTest\""; + assertEquals(camelCaseTableName.substring(1, camelCaseTableName.length() - 1), + db.normalizeTableName(camelCaseTableName)); + + // Verify that the database flavor gets the expected normalized case + boolean isUpperCase = db.flavor().isNormalizedUpperCase(); + if (isUpperCase) { + assertEquals(TEST_TABLE_NAME.toUpperCase(), db.normalizeTableName(TEST_TABLE_NAME)); + } else { + assertEquals(TEST_TABLE_NAME.toLowerCase(), db.normalizeTableName(TEST_TABLE_NAME)); + } + } + + @Test + public void selectNewTable() { + new Schema() + .addTable("dbtest") + .addColumn("nbr_integer").asInteger().primaryKey().table() + .addColumn("nbr_long").asLong().table() + .addColumn("nbr_float").asFloat().table() + .addColumn("nbr_double").asDouble().table() + .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table() + .addColumn("str_varchar").asString(80).table() + .addColumn("str_fixed").asStringFixed(1).table() + .addColumn("str_lob").asClob().table() + .addColumn("bin_blob").asBlob().table() + .addColumn("date_millis").asDate().table() + .addColumn("local_date").asLocalDate().table().schema().execute(db); + + BigDecimal bigDecimal = new BigDecimal("5.3"); + db.toInsert("insert into dbtest values (?,?,?,?,?,?,?,?,?,?,?)").argInteger(1).argLong(2L).argFloat(3.2f).argDouble(4.2) + .argBigDecimal(bigDecimal).argString("Hello").argString("T").argClobString("World") + .argBlobBytes("More".getBytes()).argDate(now).argLocalDate(localDateNow).insert(1); + + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, " + + "bin_blob, date_millis, local_date from dbtest") + .query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals(Integer.valueOf(1), rs.getIntegerOrNull(1)); + assertEquals(Integer.valueOf(1), rs.getIntegerOrNull("nbr_integer")); + assertEquals(1, rs.getIntegerOrZero(1)); + assertEquals(1, rs.getIntegerOrZero("nbr_integer")); + assertEquals(Long.valueOf(2), rs.getLongOrNull(2)); + assertEquals(Long.valueOf(2), rs.getLongOrNull("nbr_long")); + assertEquals(2, rs.getLongOrZero(2)); + assertEquals(2, rs.getLongOrZero("nbr_long")); + assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull(3)); + assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull("nbr_float")); + assertEquals(3.2, rs.getFloatOrZero(3), 0.01); + assertEquals(3.2, rs.getFloatOrZero("nbr_float"), 0.01); + assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull(4)); + assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull("nbr_double")); + assertEquals(4.2, rs.getDoubleOrZero(4), 0.01); + assertEquals(4.2, rs.getDoubleOrZero("nbr_double"), 0.01); + assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull(5)); + assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull("nbr_big_decimal")); + assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrZero(5)); + assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrZero("nbr_big_decimal")); + assertEquals("Hello", rs.getStringOrNull(6)); + assertEquals("Hello", rs.getStringOrNull("str_varchar")); + assertEquals("Hello", rs.getStringOrEmpty(6)); + assertEquals("Hello", rs.getStringOrEmpty("str_varchar")); + assertEquals("T", rs.getStringOrNull(7)); + assertEquals("T", rs.getStringOrNull("str_fixed")); + assertEquals("T", rs.getStringOrEmpty(7)); + assertEquals("T", rs.getStringOrEmpty("str_fixed")); + assertEquals("World", rs.getClobStringOrNull(8)); + assertEquals("World", rs.getClobStringOrNull("str_lob")); + assertEquals("World", rs.getClobStringOrEmpty(8)); + assertEquals("World", rs.getClobStringOrEmpty("str_lob")); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull(9)); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull("bin_blob")); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrZeroLen(9)); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrZeroLen("bin_blob")); + assertEquals(now, rs.getDateOrNull(10)); + assertEquals(now, rs.getDateOrNull("date_millis")); + assertEquals(localDateNow, rs.getLocalDateOrNull(11)); + assertEquals(localDateNow, rs.getLocalDateOrNull("local_date")); + return null; + }); + // Repeat the above query, using the various methods that automatically infer the column + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, " + + "bin_blob, date_millis, local_date from dbtest") + .query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals(Integer.valueOf(1), rs.getIntegerOrNull()); + assertEquals(Long.valueOf(2), rs.getLongOrNull()); + assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull()); + assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull()); + assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull()); + assertEquals("Hello", rs.getStringOrNull()); + assertEquals("T", rs.getStringOrNull()); + assertEquals("World", rs.getClobStringOrNull()); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull()); + assertEquals(now, rs.getDateOrNull()); + assertEquals(localDateNow, rs.getLocalDateOrNull()); + return null; + }); + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, " + + "bin_blob, date_millis, local_date from dbtest") + .query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals(1, rs.getIntegerOrZero()); + assertEquals(2, rs.getLongOrZero()); + assertEquals(3.2, rs.getFloatOrZero(), 0.01); + assertEquals(4.2, rs.getDoubleOrZero(), 0.01); + assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrZero()); + assertEquals("Hello", rs.getStringOrEmpty()); + assertEquals("T", rs.getStringOrEmpty()); + assertEquals("World", rs.getClobStringOrEmpty()); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrZeroLen()); + return null; + }); + db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals("World", readerToString(rs.getClobReaderOrNull(1))); + assertArrayEquals("More".getBytes(), inputStreamToString(rs.getBlobInputStreamOrNull(2))); + return null; + }); + db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals("World", readerToString(rs.getClobReaderOrEmpty(1))); + assertArrayEquals("More".getBytes(), inputStreamToString(rs.getBlobInputStreamOrEmpty(2))); + return null; + }); + db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals("World", readerToString(rs.getClobReaderOrNull())); + assertArrayEquals("More".getBytes(), inputStreamToString(rs.getBlobInputStreamOrNull())); + return null; + }); + db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals("World", readerToString(rs.getClobReaderOrEmpty())); + assertArrayEquals("More".getBytes(), inputStreamToString(rs.getBlobInputStreamOrEmpty())); + return null; + }); + db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals("World", readerToString(rs.getClobReaderOrNull("str_lob"))); + assertArrayEquals("More".getBytes(), inputStreamToString(rs.getBlobInputStreamOrNull("bin_blob"))); + return null; + }); + db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals("World", readerToString(rs.getClobReaderOrEmpty("str_lob"))); + assertArrayEquals("More".getBytes(), inputStreamToString(rs.getBlobInputStreamOrEmpty("bin_blob"))); + return null; + }); + assertEquals(Long.valueOf(1), db.toSelect("select count(*) from dbtest where nbr_integer=:i and nbr_long=:l and " + + "abs(nbr_float-:f)<0.01 and abs(nbr_double-:d)<0.01 and nbr_big_decimal=:bd and str_varchar=:s " + + "and str_fixed=:sf and date_millis=:date and local_date=:local_date") + .argInteger("i", 1) + .argLong("l", 2L) + .argFloat("f", 3.2f) + .argDouble("d", 4.2) + .argBigDecimal("bd", bigDecimal) + .argString("s", "Hello") + .argString("sf", "T") + .argDate("date", now) + .argLocalDate("local_date", localDateNow) + .queryLongOrNull()); + List result = db.toSelect("select count(*) from dbtest where nbr_integer=:i and nbr_long=:l and " + + "abs(nbr_float-:f)<0.01 and abs(nbr_double-:d)<0.01 and nbr_big_decimal=:bd and str_varchar=:s " + + "and str_fixed=:sf and date_millis=:date and local_date=:local_date").argInteger("i", 1).argLong("l", 2L).argFloat("f", 3.2f) + .argDouble("d", 4.2).argBigDecimal("bd", bigDecimal).argString("s", "Hello").argString("sf", "T") + .argDate("date", now).argLocalDate("local_date", localDateNow).queryLongs(); + assertEquals(1, result.size()); + assertEquals(Long.valueOf(1), result.get(0)); + } + + @Test + public void updatePositionalArgs() { + new Schema() + .addTable("dbtest") + .addColumn("pk").primaryKey().table() + .addColumn("nbr_integer").asInteger().table() + .addColumn("nbr_long").asLong().table() + .addColumn("nbr_float").asFloat().table() + .addColumn("nbr_double").asDouble().table() + .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table() + .addColumn("str_varchar").asString(80).table() + .addColumn("str_fixed").asStringFixed(1).table() + .addColumn("str_lob").asClob().table() + .addColumn("bin_blob").asBlob().table() + .addColumn("date_millis").asDate().table() + .addColumn("local_date").asLocalDate().table().schema().execute(db); + + BigDecimal bigDecimal = new BigDecimal("5.3"); + assertEquals(1, db.toInsert("insert into dbtest values (?,?,?,?,?,?,?,?,?,?,?,?)") + .argLong(1L) + .argInteger(1) + .argLong(2L) + .argFloat(3.2f) + .argDouble(4.2) + .argBigDecimal(bigDecimal) + .argString("Hello") + .argString("T") + .argClobString("World") + .argBlobBytes("More".getBytes()) + .argDate(now) + .argLocalDate(localDateNow).insert()); + db.toUpdate("update dbtest set nbr_integer=?, nbr_long=?, nbr_float=?, nbr_double=?, nbr_big_decimal=?, " + + "str_varchar=?, str_fixed=?, str_lob=?, bin_blob=?, date_millis=?, local_date=?").argInteger(null).argLong(null) + .argFloat(null).argDouble(null).argBigDecimal(null).argString(null).argString(null).argClobString(null) + .argBlobBytes(null).argDate(null).argLocalDate(null).update(1); + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, " + + "bin_blob, date_millis, local_date from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertNull(rs.getIntegerOrNull(1)); + assertNull(rs.getIntegerOrNull("nbr_integer")); + assertNull(rs.getLongOrNull(2)); + assertNull(rs.getLongOrNull("nbr_long")); + assertNull(rs.getFloatOrNull(3)); + assertNull(rs.getFloatOrNull("nbr_float")); + assertNull(rs.getDoubleOrNull(4)); + assertNull(rs.getDoubleOrNull("nbr_double")); + assertNull(rs.getBigDecimalOrNull(5)); + assertNull(rs.getBigDecimalOrNull("nbr_big_decimal")); + assertNull(rs.getStringOrNull(6)); + assertNull(rs.getStringOrNull("str_varchar")); + assertNull(rs.getStringOrNull(7)); + assertNull(rs.getStringOrNull("str_fixed")); + assertNull(rs.getClobStringOrNull(8)); + assertNull(rs.getClobStringOrNull("str_lob")); + assertNull(rs.getBlobBytesOrNull(9)); + assertNull(rs.getBlobBytesOrNull("bin_blob")); + assertNull(rs.getDateOrNull(10)); + assertNull(rs.getDateOrNull("date_millis")); + assertNull(rs.getLocalDateOrNull(11)); + assertNull(rs.getLocalDateOrNull("local_date")); + return null; + }); + assertEquals(1, db.toUpdate("update dbtest set nbr_integer=?, nbr_long=?, nbr_float=?, nbr_double=?, " + + "nbr_big_decimal=?, str_varchar=?, str_fixed=?, str_lob=?, bin_blob=?, date_millis=?, local_date=?").argInteger(1) + .argLong(2L).argFloat(3.2f).argDouble(4.2).argBigDecimal(bigDecimal).argString("Hello").argString("T") + .argClobString("World").argBlobBytes("More".getBytes()).argDate(now).argLocalDate(localDateNow).update()); + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, " + + "bin_blob, date_millis, local_date from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals(Integer.valueOf(1), rs.getIntegerOrNull(1)); + assertEquals(Integer.valueOf(1), rs.getIntegerOrNull("nbr_integer")); + assertEquals(Long.valueOf(2), rs.getLongOrNull(2)); + assertEquals(Long.valueOf(2), rs.getLongOrNull("nbr_long")); + assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull(3)); + assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull("nbr_float")); + assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull(4)); + assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull("nbr_double")); + assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull(5)); + assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull("nbr_big_decimal")); + assertEquals("Hello", rs.getStringOrNull(6)); + assertEquals("Hello", rs.getStringOrNull("str_varchar")); + assertEquals("T", rs.getStringOrNull(7)); + assertEquals("T", rs.getStringOrNull("str_fixed")); + assertEquals("World", rs.getClobStringOrNull(8)); + assertEquals("World", rs.getClobStringOrNull("str_lob")); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull(9)); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull("bin_blob")); + assertEquals(now, rs.getDateOrNull(10)); + assertEquals(now, rs.getDateOrNull("date_millis")); + assertEquals(localDateNow, rs.getLocalDateOrNull(11)); + assertEquals(localDateNow, rs.getLocalDateOrNull("local_date")); + return null; + }); + db.toUpdate("update dbtest set str_lob=?, bin_blob=?").argClobReader(null).argBlobStream(null).update(1); + db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertNull(rs.getClobStringOrNull(1)); + assertNull(rs.getClobStringOrNull("str_lob")); + assertNull(rs.getBlobBytesOrNull(2)); + assertNull(rs.getBlobBytesOrNull("bin_blob")); + return null; + }); + db.toUpdate("update dbtest set str_lob=?, bin_blob=?").argClobReader(new StringReader("World")) + .argBlobStream(new ByteArrayInputStream("More".getBytes())).update(1); + db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals("World", rs.getClobStringOrNull(1)); + assertEquals("World", rs.getClobStringOrNull("str_lob")); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull(2)); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull("bin_blob")); + return null; + }); + } + + @Test + public void updateNamedArgs() { + new Schema() + .addTable("dbtest") + .addColumn("pk").primaryKey().table() + .addColumn("nbr_integer").asInteger().table() + .addColumn("nbr_long").asLong().table() + .addColumn("nbr_float").asFloat().table() + .addColumn("nbr_double").asDouble().table() + .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table() + .addColumn("str_varchar").asString(80).table() + .addColumn("str_fixed").asStringFixed(1).table() + .addColumn("str_lob").asClob().table() + .addColumn("bin_blob").asBlob().table() + .addColumn("date_millis").asDate().table() + .addColumn("local_date").asLocalDate().table().schema().execute(db); + BigDecimal bigDecimal = new BigDecimal("5.3"); + db.toInsert("insert into dbtest values (:pk,:a,:b,:c,:d,:e,:f,:sf,:g,:h,:i,:j)").argLong(":pk", 1L).argInteger(":a", 1) + .argLong(":b", 2L).argFloat(":c", 3.2f).argDouble(":d", 4.2).argBigDecimal(":e", bigDecimal) + .argString(":f", "Hello").argString(":sf", "T") + .argClobString(":g", "World").argBlobBytes(":h", "More".getBytes()) + .argDate(":i", now).argLocalDate(":j", localDateNow).insert(1); + db.toUpdate("update dbtest set nbr_integer=:a, nbr_long=:b, nbr_float=:c, nbr_double=:d, nbr_big_decimal=:e, " + + "str_varchar=:f, str_fixed=:sf, str_lob=:g, bin_blob=:h, date_millis=:i, local_date=:j").argInteger(":a", null) + .argLong(":b", null).argFloat(":c", null).argDouble(":d", null).argBigDecimal(":e", null) + .argString(":f", null).argString(":sf", null) + .argClobString(":g", null).argBlobBytes(":h", null) + .argDate(":i", null).argLocalDate(":j", null).update(1); + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, " + + "bin_blob, date_millis, local_date from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertNull(rs.getIntegerOrNull(1)); + assertNull(rs.getIntegerOrNull("nbr_integer")); + assertNull(rs.getLongOrNull(2)); + assertNull(rs.getLongOrNull("nbr_long")); + assertNull(rs.getFloatOrNull(3)); + assertNull(rs.getFloatOrNull("nbr_float")); + assertNull(rs.getDoubleOrNull(4)); + assertNull(rs.getDoubleOrNull("nbr_double")); + assertNull(rs.getBigDecimalOrNull(5)); + assertNull(rs.getBigDecimalOrNull("nbr_big_decimal")); + assertNull(rs.getStringOrNull(6)); + assertNull(rs.getStringOrNull("str_varchar")); + assertNull(rs.getStringOrNull(7)); + assertNull(rs.getStringOrNull("str_fixed")); + assertNull(rs.getClobStringOrNull(8)); + assertNull(rs.getClobStringOrNull("str_lob")); + assertNull(rs.getBlobBytesOrNull(9)); + assertNull(rs.getBlobBytesOrNull("bin_blob")); + assertNull(rs.getDateOrNull(10)); + assertNull(rs.getDateOrNull("date_millis")); + assertNull(rs.getLocalDateOrNull(11)); + assertNull(rs.getLocalDateOrNull("local_date")); + return null; + }); + db.toUpdate("update dbtest set nbr_integer=:a, nbr_long=:b, nbr_float=:c, nbr_double=:d, nbr_big_decimal=:e, " + + "str_varchar=:f, str_fixed=:sf, str_lob=:g, bin_blob=:h, date_millis=:i, local_date=:j").argInteger(":a", 1) + .argLong(":b", 2L).argFloat(":c", 3.2f).argDouble(":d", 4.2).argBigDecimal(":e", bigDecimal) + .argString(":f", "Hello").argString(":sf", "T") + .argClobString(":g", "World").argBlobBytes(":h", "More".getBytes()) + .argDate(":i", now).argLocalDate(":j", localDateNow).update(1); + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, " + + "bin_blob, date_millis, local_date from dbtest") + .query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals(Integer.valueOf(1), rs.getIntegerOrNull(1)); + assertEquals(Integer.valueOf(1), rs.getIntegerOrNull("nbr_integer")); + assertEquals(Long.valueOf(2), rs.getLongOrNull(2)); + assertEquals(Long.valueOf(2), rs.getLongOrNull("nbr_long")); + assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull(3)); + assertEquals(Float.valueOf(3.2f), rs.getFloatOrNull("nbr_float")); + assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull(4)); + assertEquals(Double.valueOf(4.2), rs.getDoubleOrNull("nbr_double")); + assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull(5)); + assertEquals(new BigDecimal("5.3"), rs.getBigDecimalOrNull("nbr_big_decimal")); + assertEquals("Hello", rs.getStringOrNull(6)); + assertEquals("Hello", rs.getStringOrNull("str_varchar")); + assertEquals("T", rs.getStringOrNull(7)); + assertEquals("T", rs.getStringOrNull("str_fixed")); + assertEquals("World", rs.getClobStringOrNull(8)); + assertEquals("World", rs.getClobStringOrNull("str_lob")); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull(9)); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull("bin_blob")); + assertEquals(now, rs.getDateOrNull(10)); + assertEquals(now, rs.getDateOrNull("date_millis")); + assertEquals(localDateNow, rs.getLocalDateOrNull(11)); + assertEquals(localDateNow, rs.getLocalDateOrNull("local_date")); + return null; + }); + + db.toUpdate("update dbtest set str_lob=:a, bin_blob=:b").argClobReader(":a", null).argBlobStream(":b", null).update(1); + db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertNull(rs.getClobStringOrNull(1)); + assertNull(rs.getClobStringOrNull("str_lob")); + assertNull(rs.getBlobBytesOrNull(2)); + assertNull(rs.getBlobBytesOrNull("bin_blob")); + return null; + }); + db.toUpdate("update dbtest set str_lob=:a, bin_blob=:b").argClobReader(":a", new StringReader("World")) + .argBlobStream(":b", new ByteArrayInputStream("More".getBytes())).update(1); + db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals("World", rs.getClobStringOrNull(1)); + assertEquals("World", rs.getClobStringOrNull("str_lob")); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull(2)); + assertArrayEquals("More".getBytes(), rs.getBlobBytesOrNull("bin_blob")); + return null; + }); + } + + @Test + public void nullValues() { + new Schema() + .addTable("dbtest") + .addColumn("pk").primaryKey().table() + .addColumn("nbr_integer").asInteger().table() + .addColumn("nbr_long").asLong().table() + .addColumn("nbr_float").asFloat().table() + .addColumn("nbr_double").asDouble().table() + .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table() + .addColumn("str_varchar").asString(80).table() + .addColumn("str_fixed").asStringFixed(1).table() + .addColumn("str_lob").asClob().table() + .addColumn("bin_blob").asBlob().table() + .addColumn("date_millis").asDate().table() + .addColumn("local_date").asLocalDate().table().schema().execute(db); + db.toInsert("insert into dbtest values (?,?,?,?,?,?,?,?,?,?,?,?)").argLong(1L).argInteger(null).argLong(null) + .argFloat(null).argDouble(null).argBigDecimal(null).argString(null).argString(null).argClobString(null) + .argBlobBytes(null).argDate(null).argLocalDate(null).insert(1); + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, " + + "bin_blob, date_millis, local_date from dbtest") + .query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertNull(rs.getIntegerOrNull(1)); + assertNull(rs.getIntegerOrNull("nbr_integer")); + assertNull(rs.getLongOrNull(2)); + assertNull(rs.getLongOrNull("nbr_long")); + assertNull(rs.getFloatOrNull(3)); + assertNull(rs.getFloatOrNull("nbr_float")); + assertNull(rs.getDoubleOrNull(4)); + assertNull(rs.getDoubleOrNull("nbr_double")); + assertNull(rs.getBigDecimalOrNull(5)); + assertNull(rs.getBigDecimalOrNull("nbr_big_decimal")); + assertNull(rs.getStringOrNull(6)); + assertNull(rs.getStringOrNull("str_varchar")); + assertNull(rs.getStringOrNull(7)); + assertNull(rs.getStringOrNull("str_fixed")); + assertNull(rs.getClobStringOrNull(8)); + assertNull(rs.getClobStringOrNull("str_lob")); + assertNull(rs.getBlobBytesOrNull(9)); + assertNull(rs.getBlobBytesOrNull("bin_blob")); + assertNull(rs.getDateOrNull(10)); + assertNull(rs.getDateOrNull("date_millis")); + assertNull(rs.getLocalDateOrNull(11)); + assertNull(rs.getLocalDateOrNull("local_date")); + return null; + }); + db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertNull(rs.getClobReaderOrNull(1)); + assertNull(rs.getBlobInputStreamOrNull(2)); + return null; + }); + db.toSelect("select str_lob, bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertNull(rs.getClobReaderOrNull("str_lob")); + assertNull(rs.getBlobInputStreamOrNull("bin_blob")); + return null; + }); + } + + @Test + public void fromAny() { + assertEquals(db.toSelect("select 1" + db.flavor().fromAny()).queryIntegerOrZero(), 1); + } + + @Test + public void metadataColumnNames() { + new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db); + db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\" from dbtest").query(rs -> { + assertArrayEquals(new String[]{"PK", "FOO", "Foo"}, rs.getColumnLabels()); + return null; + }); + } + + @Test + public void metadataColumnTypes() { + String timestampColumnName = "data_millis"; + String dateColumnName = "local_date"; + new Schema() + .addTable("dbtest") + .addColumn(timestampColumnName).asDate().table() + .addColumn(dateColumnName).asLocalDate().table().schema().execute(db); + db.toSelect("select * from dbtest").query((RowsHandler) rs -> { + ResultSetMetaData metadata = rs.getMetadata(); + for (int i = 1; i <= metadata.getColumnCount(); i++) { + String columnName = metadata.getColumnName(i); + String columnType = metadata.getColumnTypeName(i); + if (columnName.equalsIgnoreCase(timestampColumnName)) { + if ("sqlserver".equals(db.flavor().toString())) { + assertEquals("DATETIME2", columnType.toUpperCase()); + } else { + assertEquals("TIMESTAMP", columnType.toUpperCase()); + } + } else if (columnName.equalsIgnoreCase(dateColumnName)) { + assertEquals("DATE", columnType.toUpperCase()); + } else { + fail("Unexpected column " + columnName + " of type " + columnType); + } + } + return null; + }); + } + + @Test + public void intervals() { + new Schema().addTable("dbtest").addColumn("d").asDate().schema().execute(db); + db.toInsert("insert into dbtest (d) values (?)").argDate(now).insert(1); + assertEquals(1, db.toSelect("select count(1) from dbtest where d - interval '1' hour * ? < ?") + .argInteger(2) + .argDate(now) + .queryIntegerOrZero()); + } + + @Test + public void saveResultAsTable() { + new Schema().addTable("dbtest") + .addColumn("nbr_integer").asInteger().primaryKey().table() + .addColumn("nbr_long").asLong().table() + .addColumn("nbr_float").asFloat().table() + .addColumn("nbr_double").asDouble().table() + .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table() + .addColumn("str_varchar").asString(80).table() + .addColumn("str_fixed").asStringFixed(1).table() + .addColumn("str_lob").asClob().table() + .addColumn("bin_blob").asBlob().table() + .addColumn("boolean_flag").asBoolean().table() + .addColumn("date_millis").asDate().table() + .addColumn("local_date").asLocalDate().schema().execute(db); + db.toInsert("insert into dbtest (nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar," + + " str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date) values (?,?,?,?,?,?,?,?,?,?,?,?)") + .argInteger(Integer.MAX_VALUE).argLong(Long.MAX_VALUE).argFloat(Float.MAX_VALUE) + .argDouble(Double.MAX_VALUE).argBigDecimal(new BigDecimal("123.456")) + .argString("hello").argString("Z").argClobString("hello again") + .argBlobBytes(new byte[]{'1', '2'}).argBoolean(true) + .argDateNowPerApp().argLocalDate(localDateNow).insert(1); + db.toInsert("insert into dbtest (nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar," + + " str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date) values (?,?,?,?,?,?,?,?,?,?,?,?)") + .argInteger(Integer.MIN_VALUE).argLong(Long.MIN_VALUE).argFloat(0.000001f) + .argDouble(Double.MIN_VALUE).argBigDecimal(new BigDecimal("-123.456")) + .argString("goodbye").argString("A").argClobString("bye again") + .argBlobBytes(new byte[]{'3', '4'}).argBoolean(false) + .argDateNowPerApp().argLocalDate(localDateNow).insert(1); + String expectedSchema = new Schema().addTable("dbtest2") + .addColumn("nbr_integer").asInteger().table() + .addColumn("nbr_long").asLong().table() + .addColumn("nbr_float").asFloat().table() + .addColumn("nbr_double").asDouble().table() + .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table() + .addColumn("str_varchar").asString(80).table() + .addColumn("str_fixed").asStringFixed(1).table() + .addColumn("str_lob").asClob().table() + .addColumn("bin_blob").asBlob().table() + .addColumn("boolean_flag").asBoolean().table() + .addColumn("date_millis").asDate().table() + .addColumn("local_date").asLocalDate().schema().print(db.flavor()); + List args = db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal," + + " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest") + .query(rs -> { + List result = new ArrayList<>(); + while (rs.next()) { + if (result.size() == 0) { + db.dropTableQuietly("dbtest2"); + Schema schema = new Schema().addTableFromRow("dbtest2", rs).schema(); + assertEquals(expectedSchema, schema.print(db.flavor())); + schema.execute(db); + } + result.add(SqlArgs.readRow(rs)); + } + return result; + }); + + db.toInsert(Sql.insert("dbtest2", args)).insertBatch(); + + assertEquals(2, db.toSelect("select count(*) from dbtest2").queryIntegerOrZero()); + + assertEquals( + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal," + + " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest order by 1") + .queryMany(SqlArgs::readRow), + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal," + + " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest2 order by 1") + .queryMany(SqlArgs::readRow)); + + assertEquals( + Arrays.asList( + new SqlArgs().argInteger("nbr_integer", Integer.MIN_VALUE) + .argLong("nbr_long", Long.MIN_VALUE) + .argFloat("nbr_float", 0.000001f) + .argDouble("nbr_double", Double.MIN_VALUE) + .argBigDecimal("nbr_big_decimal", new BigDecimal("-123.456")) + .argString("str_varchar", "goodbye") + .argString("str_fixed", "A") + .argClobString("str_lob", "bye again") + .argBlobBytes("bin_blob", new byte[]{'3', '4'}) + .argString("boolean_flag", "N")//.argBoolean("boolean_flag", false) + .argDate("date_millis", now) + .argLocalDate("local_date", localDateNow), + new SqlArgs().argInteger("nbr_integer", Integer.MAX_VALUE) + .argLong("nbr_long", Long.MAX_VALUE) + .argFloat("nbr_float", Float.MAX_VALUE) + .argDouble("nbr_double", Double.MAX_VALUE) + .argBigDecimal("nbr_big_decimal", new BigDecimal("123.456")) + .argString("str_varchar", "hello") + .argString("str_fixed", "Z") + .argClobString("str_lob", "hello again") + .argBlobBytes("bin_blob", new byte[]{'1', '2'}) + .argString("boolean_flag", "Y")//.argBoolean("boolean_flag", true) + .argDate("date_millis", now) + .argLocalDate("local_date", localDateNow)), + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal," + + " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest2 order by 1") + .queryMany(SqlArgs::readRow)); + } + + @Test + public void readSqlArgs() { + new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db); + db.toInsert("insert into dbtest (pk) values (?)").argInteger(1).insert(1); + SqlArgs args = db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\", pk as \"g arB#G!\"," + + " pk as \"TitleCase\" from dbtest") + .queryOneOrThrow(SqlArgs::readRow); + + assertEquals(Arrays.asList("pk", "foo", "foo_2", "g_ar_b_g", "title_case"), args.names()); + } + + @Test + public void clockSync() { + db.assertTimeSynchronized(); + } + + @Test + public void booleanColumn() { + new Schema().addTable("dbtest") + .addColumn("t").asBoolean().table() + .addColumn("f").asBoolean().table() + .addColumn("n").asBoolean().schema().execute(db); + db.toInsert("insert into dbtest (t,f,n) values (?,:f,?)") + .argBoolean(true).argBoolean("f", false).argBoolean(null).insert(1); + db.toSelect("select t,f,n from dbtest") + .query(rs -> { + assertTrue(rs.next()); + assertSame(rs.getBooleanOrNull(), Boolean.TRUE); + assertSame(rs.getBooleanOrNull(), Boolean.FALSE); + assertNull(rs.getBooleanOrNull()); + return null; + }); + // Verify use of getBooleanOrNull(int) followed by default getBooleanOrNull() tracks + // the current column index correctly (picks up where the explicit one left off) + db.toSelect("select t,f,n from dbtest") + .query(rs -> { + assertTrue(rs.next()); + assertSame(rs.getBooleanOrNull(2), Boolean.FALSE); + assertNull(rs.getBooleanOrNull()); + return null; + }); + // Verify use of getBooleanOrNull(String) followed by default getBooleanOrNull() tracks + // the current column index correctly (picks up where the explicit one left off) + db.toSelect("select t,f,n from dbtest") + .query(rs -> { + assertTrue(rs.next()); + assertSame(rs.getBooleanOrNull("f"), Boolean.FALSE); + assertNull(rs.getBooleanOrNull()); + return null; + }); + db.toSelect("select t,f,n from dbtest") + .query(rs -> { + assertTrue(rs.next()); + assertTrue(rs.getBooleanOrFalse()); + assertFalse(rs.getBooleanOrFalse()); + assertFalse(rs.getBooleanOrFalse()); + return null; + }); + // Verify use of getBooleanOrFalse(int) followed by default getBooleanOrFalse() tracks + // the current column index correctly (picks up where the explicit one left off) + db.toSelect("select t,f,n from dbtest") + .query(rs -> { + assertTrue(rs.next()); + assertFalse(rs.getBooleanOrFalse(2)); + assertFalse(rs.getBooleanOrFalse()); + return null; + }); + // Verify use of getBooleanOrFalse(String) followed by default getBooleanOrFalse() tracks + // the current column index correctly (picks up where the explicit one left off) + db.toSelect("select t,f,n from dbtest") + .query(rs -> { + assertTrue(rs.next()); + assertFalse(rs.getBooleanOrFalse("f")); + assertFalse(rs.getBooleanOrFalse()); + return null; + }); + db.toSelect("select t,f,n from dbtest") + .query(rs -> { + assertTrue(rs.next()); + assertTrue(rs.getBooleanOrTrue()); + assertFalse(rs.getBooleanOrTrue()); + assertTrue(rs.getBooleanOrTrue()); + return null; + }); + // Verify use of getBooleanOrTrue(int) followed by default getBooleanOrTrue() tracks + // the current column index correctly (picks up where the explicit one left off) + db.toSelect("select t,f,n from dbtest") + .query(rs -> { + assertTrue(rs.next()); + assertFalse(rs.getBooleanOrTrue(2)); + assertTrue(rs.getBooleanOrTrue()); + return null; + }); + // Verify use of getBooleanOrTrue(String) followed by default getBooleanOrTrue() tracks + // the current column index correctly (picks up where the explicit one left off) + db.toSelect("select t,f,n from dbtest") + .query(rs -> { + assertTrue(rs.next()); + assertFalse(rs.getBooleanOrTrue("f")); + assertTrue(rs.getBooleanOrTrue()); + return null; + }); + db.toDelete("delete from dbtest where t=? and f=?") + .argBoolean(true).argBoolean(false).update(1); + + db.toInsert("insert into dbtest (t,f,n) values (:t,:f,:n)") + .argBoolean("t", true).argBoolean("f", false).argBoolean("n", null).insert(1); + db.toSelect("select t,f,n from dbtest") + .query(rs -> { + assertTrue(rs.next()); + assertSame(rs.getBooleanOrNull(1), Boolean.TRUE); + assertSame(rs.getBooleanOrNull(2), Boolean.FALSE); + assertNull(rs.getBooleanOrNull(3)); + assertEquals(rs.getBooleanOrFalse(1), Boolean.TRUE); + assertEquals(rs.getBooleanOrFalse(2), Boolean.FALSE); + assertEquals(rs.getBooleanOrFalse(3), Boolean.FALSE); + assertEquals(rs.getBooleanOrTrue(1), Boolean.TRUE); + assertEquals(rs.getBooleanOrTrue(2), Boolean.FALSE); + assertEquals(rs.getBooleanOrTrue(3), Boolean.TRUE); + assertSame(rs.getBooleanOrNull("t"), Boolean.TRUE); + assertSame(rs.getBooleanOrNull("f"), Boolean.FALSE); + assertNull(rs.getBooleanOrNull("n")); + assertEquals(rs.getBooleanOrFalse("t"), Boolean.TRUE); + assertEquals(rs.getBooleanOrFalse("f"), Boolean.FALSE); + assertEquals(rs.getBooleanOrFalse("n"), Boolean.FALSE); + assertEquals(rs.getBooleanOrTrue("t"), Boolean.TRUE); + assertEquals(rs.getBooleanOrTrue("f"), Boolean.FALSE); + assertEquals(rs.getBooleanOrTrue("n"), Boolean.TRUE); + return null; + }); + assertSame(db.toSelect("select t from dbtest").queryBooleanOrNull(), Boolean.TRUE); + assertTrue(db.toSelect("select t from dbtest").queryBooleanOrFalse()); + assertTrue(db.toSelect("select t from dbtest").queryBooleanOrTrue()); + assertSame(db.toSelect("select f from dbtest").queryBooleanOrNull(), Boolean.FALSE); + assertFalse(db.toSelect("select f from dbtest").queryBooleanOrFalse()); + assertFalse(db.toSelect("select f from dbtest").queryBooleanOrTrue()); + assertNull(db.toSelect("select n from dbtest").queryBooleanOrNull()); + assertFalse(db.toSelect("select n from dbtest").queryBooleanOrFalse()); + assertTrue(db.toSelect("select n from dbtest").queryBooleanOrTrue()); + } + + @Test + public void batchInsert() { + new Schema().addTable("dbtest") + .addColumn("pk").primaryKey().schema().execute(db); + + db.toInsert("insert into dbtest (pk) values (?)") + .argInteger(1).batch() + .argInteger(2).batch() + .argInteger(3).batch().insertBatch(); + + assertEquals(3, db.toSelect("select count(*) from dbtest").queryIntegerOrZero()); + } + + @Test + public void batchInsertPkLong() { + new Schema().addTable("dbtest") + .addColumn("pk").primaryKey().table() + .addColumn("s").asString(10).schema().execute(db); + + db.toInsert("insert into dbtest (pk,s) values (?,?)") + .argPkLong(1L).argString("hi").batch() + .argPkLong(2L).argString("hello").batch() + .argPkLong(3L).argString("howdy").batch().insertBatch(); + + assertEquals(3, db.toSelect("select count(*) from dbtest").queryIntegerOrZero()); + + try { + db.toInsert("insert into dbtest (pk,s) values (?,?)") + .argPkLong(1L).argString("hi").batch() + // argPkLong in different position ==> error + .argString("hello").argPkLong(2L).batch().insertBatch(); + fail("Expecting an exception to be thrown"); + } catch (DatabaseException e) { + assertEquals("The argPkLong() calls must be in the same position across batch records", e.getMessage()); + } + + try { + db.toInsert("insert into dbtest (pk,s) values (?,?)") + // multiple pk calls ==> error + .argPkLong(1L).argPkLong(1L).batch() + .argPkLong(2L).argString("hello").batch().insertBatch(); + fail("Expecting an exception to be thrown"); + } catch (DatabaseException e) { + assertEquals("Only call one argPk*() method", e.getMessage()); + } + } + + @Test + public void batchInsertPkLongNamed() { + new Schema().addTable("dbtest") + .addColumn("pk").primaryKey().table() + .addColumn("s").asString(10).schema().execute(db); + + db.toInsert("insert into dbtest (pk,s) values (:pk,?)") + .argPkLong("pk", 1L).argString("hi").batch() + .argPkLong("pk", 2L).argString("hello").batch() + .argPkLong("pk", 3L).argString("howdy").batch().insertBatch(); + + assertEquals(3, db.toSelect("select count(*) from dbtest").queryIntegerOrZero()); + + db.toInsert("insert into dbtest (pk,s) values (:pk,?)") + .batch().argPkLong("pk", 4L).argString("hi").batch() + .argString("hello").argPkLong("pk", 5L).insertBatch(); + + assertEquals(5, db.toSelect("select count(*) from dbtest").queryIntegerOrZero()); + + try { + db.toInsert("insert into dbtest (pk,s) values (:pk,?)") + // multiple pk calls ==> error + .argPkLong("pk", 1L).argPkLong(1L).batch().insertBatch(); + fail("Expecting an exception to be thrown"); + } catch (DatabaseException e) { + assertEquals("Only call one argPk*() method", e.getMessage()); + } + + try { + db.toInsert("insert into dbtest (pk,s) values (?,?)") + .argPkLong("pk", 1L).argString("howdy").batch() + // different name for pk on second batch ==> error + .argPkLong("na", 2L).argString("hello").batch().insertBatch(); + fail("Expecting an exception to be thrown"); + } catch (DatabaseException e) { + assertEquals("The primary key argument name must match across batch rows", e.getMessage()); + } + } + + @Test + public void batchInsertPkSeq() { + db.dropSequenceQuietly("seq"); + new Schema().addTable("dbtest") + .addColumn("pk").primaryKey().table() + .addColumn("s").asString(10).schema() + .addSequence("seq").schema().execute(db); + + db.toInsert("insert into dbtest (pk,s) values (?,?)") + .argPkSeq("seq").argString("hi").batch() + .argPkSeq("seq").argString("hello").batch() + .argPkSeq("seq").argString("howdy").batch().insertBatch(); + + assertEquals(3, db.toSelect("select count(*) from dbtest").queryIntegerOrZero()); + + try { + db.toInsert("insert into dbtest (pk,s) values (?,?)") + .argPkSeq("seq").argString("hi").batch() + // argPkLong in different position ==> error + .argString("hello").argPkSeq("seq").batch().insertBatch(); + fail("Expecting an exception to be thrown"); + } catch (DatabaseException e) { + assertEquals("The argPkSeq() calls must be in the same position across batch records", e.getMessage()); + } + + try { + db.toInsert("insert into dbtest (pk,s) values (?,?)") + // multiple pk calls ==> error + .argPkSeq("seq").argPkSeq("seq").batch().insertBatch(); + fail("Expecting an exception to be thrown"); + } catch (DatabaseException e) { + assertEquals("Only call one argPk*() method", e.getMessage()); + } + } + + @Test + public void batchInsertPkSeqNamed() { + db.dropSequenceQuietly("seq"); + new Schema().addTable("dbtest") + .addColumn("pk").primaryKey().table() + .addColumn("s").asString(10).schema() + .addSequence("seq").schema().execute(db); + + db.toInsert("insert into dbtest (pk,s) values (:pk,?)") + .argPkSeq("pk", "seq").argString("hi").batch() + .argPkSeq("pk", "seq").argString("hello").batch() + .argPkSeq("pk", "seq").argString("howdy").batch().insertBatch(); + + assertEquals(3, db.toSelect("select count(*) from dbtest").queryIntegerOrZero()); + + db.toInsert("insert into dbtest (pk,s) values (:pk,?)") + .batch().argPkSeq("pk", "seq").argString("hi").batch() + .argString("hello").argPkSeq("pk", "seq").insertBatch(); + + assertEquals(5, db.toSelect("select count(*) from dbtest").queryIntegerOrZero()); + + try { + db.toInsert("insert into dbtest (pk,s) values (:pk,?)") + // multiple pk calls ==> error + .argPkSeq("pk", "seq").argPkSeq("pk", "seq").batch().insertBatch(); + fail("Expecting an exception to be thrown"); + } catch (DatabaseException e) { + assertEquals("Only call one argPk*() method", e.getMessage()); + } + + try { + db.toInsert("insert into dbtest (pk,s) values (?,?)") + .argPkSeq("pk", "seq").argString("howdy").batch() + // different name for pk on second batch ==> error + .argPkSeq("na", "seq").argString("hello").batch().insertBatch(); + fail("Expecting an exception to be thrown"); + } catch (DatabaseException e) { + assertEquals("The primary key argument name must match across batch rows", e.getMessage()); + } + } + + @Test + public void bigClob() { + new Schema().addTable("dbtest").addColumn("str_lob").asClob().schema().execute(db); + final String longString = "0123456789".repeat(40000); + db.toInsert("insert into dbtest values (?)").argClobString(longString).insert(1); + db.toSelect("select str_lob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals(longString, rs.getClobStringOrNull(1)); + assertEquals(longString, rs.getClobStringOrNull("str_lob")); + assertEquals(longString, readerToString(rs.getClobReaderOrNull(1))); + return null; + }); + // Intentional slight variation here to test get() + db.get().toSelect("select str_lob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals(longString, readerToString(rs.getClobReaderOrNull("str_lob"))); + return null; + }); + db.toDelete("delete from dbtest").update(1); + db.toInsert("insert into dbtest values (?)").argClobReader(new StringReader(longString)).insert(1); + db.toSelect("select str_lob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals(longString, rs.getClobStringOrNull(1)); + assertEquals(longString, rs.getClobStringOrNull("str_lob")); + assertEquals(longString, readerToString(rs.getClobReaderOrNull(1))); + return null; + }); + db.toSelect("select str_lob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertEquals(longString, readerToString(rs.getClobReaderOrNull("str_lob"))); + return null; + }); + } + + @Test + public void bigBlob() { + new Schema().addTable("dbtest").addColumn("bin_blob").asBlob().schema().execute(db); + final byte[] bigBytes = "0123456789".repeat(40000).getBytes(); + db.toInsert("insert into dbtest values (?)").argBlobBytes(bigBytes).insert(1); + db.toSelect("select bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertArrayEquals(bigBytes, rs.getBlobBytesOrNull(1)); + assertArrayEquals(bigBytes, rs.getBlobBytesOrNull("bin_blob")); + assertArrayEquals(bigBytes, inputStreamToString(rs.getBlobInputStreamOrNull(1))); + return null; + }); + db.toSelect("select bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertArrayEquals(bigBytes, inputStreamToString(rs.getBlobInputStreamOrNull("bin_blob"))); + return null; + }); + db.toDelete("delete from dbtest").update(1); + db.toInsert("insert into dbtest values (?)").argBlobStream(new ByteArrayInputStream(bigBytes)).insert(1); + db.toSelect("select bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertArrayEquals(bigBytes, rs.getBlobBytesOrNull(1)); + assertArrayEquals(bigBytes, rs.getBlobBytesOrNull("bin_blob")); + assertArrayEquals(bigBytes, inputStreamToString(rs.getBlobInputStreamOrNull(1))); + return null; + }); + db.toSelect("select bin_blob from dbtest").query((RowsHandler) rs -> { + assertTrue(rs.next()); + assertArrayEquals(bigBytes, inputStreamToString(rs.getBlobInputStreamOrNull("bin_blob"))); + return null; + }); + } + + @Test + public void argLocalDateTimeZones() { + LocalDate januaryOne2000 = LocalDate.of(2000, Month.JANUARY, 1); + // Verify we always get the same LocalDate regardless of time zone and DB across all drivers + new Schema().addTable("dbtest").addColumn("i").asLocalDate().schema().execute(db); + db.toInsert("insert into dbtest (i) values (?)").argLocalDate(januaryOne2000).insert(1); + // Query without specifying a zone + assertEquals(januaryOne2000, + db.toSelect("select i from dbtest where i=?").argLocalDate(januaryOne2000).queryLocalDateOrNull()); + TimeZone defaultTZ = TimeZone.getDefault(); + try { + String[] availableTZs = TimeZone.getAvailableIDs(); + for (String tz : availableTZs) { + TimeZone.setDefault(TimeZone.getTimeZone(tz)); + LocalDate result = + db.toSelect("select i from dbtest where i=?").argLocalDate(januaryOne2000).queryLocalDateOrNull(); + assertEquals(januaryOne2000, result); + } + } finally { + TimeZone.setDefault(defaultTZ); + } + } + + @Test + public void argLocalDateLeapYear() { + new Schema().addTable("dbtest").addColumn("testdate").asLocalDate().schema().execute(db); + + // Start by adding Febriary 28 and March 1 of 1900. This was not a leap year. + LocalDate feb1900 = LocalDate.of(1900, Month.FEBRUARY, 28); + db.toInsert("insert into dbtest (testdate) values (?)").argLocalDate(feb1900).insert(1); + assertEquals(feb1900, + db.toSelect("select testdate from dbtest where testdate=?").argLocalDate(feb1900).queryLocalDateOrNull()); + + LocalDate mar1900 = LocalDate.of(1900, Month.MARCH, 1); + db.toInsert("insert into dbtest (testdate) values (?)").argLocalDate(mar1900).insert(1); + assertEquals(mar1900, + db.toSelect("select testdate from dbtest where testdate=?").argLocalDate(mar1900).queryLocalDateOrNull()); + + // Now try Feb 28, 29, and March 1 of 2000. This was a leap year + LocalDate feb2000 = LocalDate.of(2000, Month.FEBRUARY, 28); + db.toInsert("insert into dbtest (testdate) values (?)").argLocalDate(feb2000).insert(1); + assertEquals(feb2000, + db.toSelect("select testdate from dbtest where testdate=?").argLocalDate(feb2000).queryLocalDateOrNull()); + + LocalDate febLeap2000 = LocalDate.of(2000, Month.FEBRUARY, 29); + db.toInsert("insert into dbtest (testdate) values (?)").argLocalDate(febLeap2000).insert(1); + assertEquals(febLeap2000, + db.toSelect("select testdate from dbtest where testdate=?").argLocalDate(febLeap2000).queryLocalDateOrNull()); + + LocalDate mar2000 = LocalDate.of(2000, Month.MARCH, 1); + db.toInsert("insert into dbtest (testdate) values (?)").argLocalDate(mar2000).insert(1); + assertEquals(mar2000, + db.toSelect("select testdate from dbtest where testdate=?").argLocalDate(mar2000).queryLocalDateOrNull()); + } + + @Test + public void argIntegerMinMax() { + new Schema().addTable("dbtest").addColumn("i").asInteger().schema().execute(db); + + db.toInsert("insert into dbtest (i) values (?)").argInteger(Integer.MIN_VALUE).insert(1); + assertEquals(Integer.valueOf(Integer.MIN_VALUE), + db.toSelect("select i from dbtest where i=?").argInteger(Integer.MIN_VALUE).queryIntegerOrNull()); + + db.toInsert("insert into dbtest (i) values (?)").argInteger(Integer.MAX_VALUE).insert(1); + assertEquals(Integer.valueOf(Integer.MAX_VALUE), + db.toSelect("select i from dbtest where i=?").argInteger(Integer.MAX_VALUE).queryIntegerOrNull()); + } + + @Test + public void argLongMinMax() { + new Schema().addTable("dbtest").addColumn("i").asLong().schema().execute(db); + + db.toInsert("insert into dbtest (i) values (?)").argLong(Long.MIN_VALUE).insert(1); + assertEquals(Long.valueOf(Long.MIN_VALUE), + db.toSelect("select i from dbtest where i=?").argLong(Long.MIN_VALUE).queryLongOrNull()); + + db.toInsert("insert into dbtest (i) values (?)").argLong(Long.MAX_VALUE).insert(1); + assertEquals(Long.valueOf(Long.MAX_VALUE), + db.toSelect("select i from dbtest where i=?").argLong(Long.MAX_VALUE).queryLongOrNull()); + } + + @Test + public void argFloatMinMax() { + new Schema().addTable("dbtest").addColumn("i").asFloat().schema().execute(db); + + db.toInsert("insert into dbtest (i) values (?)").argFloat(Float.MIN_VALUE).insert(1); + assertEquals(Float.valueOf(Float.MIN_VALUE), + db.toSelect("select i from dbtest where i=?").argFloat(Float.MIN_VALUE).queryFloatOrNull()); + + db.toInsert("insert into dbtest (i) values (?)").argFloat(Float.MAX_VALUE).insert(1); + assertEquals(Float.valueOf(Float.MAX_VALUE), + db.toSelect("select i from dbtest where i=?").argFloat(Float.MAX_VALUE).queryFloatOrNull()); + } + + @Test + public void argFloatNaN() { + new Schema().addTable("dbtest").addColumn("i").asFloat().schema().execute(db); + + db.toInsert("insert into dbtest (i) values (?)").argFloat(Float.NaN).insert(1); + assertEquals(Float.valueOf(Float.NaN), + db.toSelect("select i from dbtest where i=?").argFloat(Float.NaN).queryFloatOrNull()); + } + + @Test + public void argFloatInfinity() { + new Schema().addTable("dbtest").addColumn("i").asFloat().schema().execute(db); + + db.toInsert("insert into dbtest (i) values (?)").argFloat(Float.NEGATIVE_INFINITY).insert(1); + assertEquals(Float.valueOf(Float.NEGATIVE_INFINITY), + db.toSelect("select i from dbtest where i=?").argFloat(Float.NEGATIVE_INFINITY).queryFloatOrNull()); + + db.toInsert("insert into dbtest (i) values (?)").argFloat(Float.POSITIVE_INFINITY).insert(1); + assertEquals(Float.valueOf(Float.POSITIVE_INFINITY), + db.toSelect("select i from dbtest where i=?").argFloat(Float.POSITIVE_INFINITY).queryFloatOrNull()); + } + + @Test + public void argFloatZero() { + new Schema().addTable("dbtest").addColumn("i").asFloat().schema().execute(db); + + db.toInsert("insert into dbtest (i) values (?)").argFloat(0f).insert(1); + assertEquals(Float.valueOf(0f), + db.toSelect("select i from dbtest where i=?").argFloat(0f).queryFloatOrNull()); + } + + @Test + public void argFloatNegativeZero() { + new Schema().addTable("dbtest").addColumn("i").asFloat().schema().execute(db); + + db.toInsert("insert into dbtest (i) values (?)").argFloat(-0f).insert(1); + assertEquals(Float.valueOf(-0f), + db.toSelect("select i from dbtest where i=?").argFloat(-0f).queryFloatOrNull()); + } + + @Test + public void argDoubleMinMax() { + new Schema().addTable("dbtest").addColumn("i").asDouble().schema().execute(db); + + db.toInsert("insert into dbtest (i) values (?)").argDouble(Double.MIN_VALUE).insert(1); + assertEquals(Double.valueOf(Double.MIN_VALUE), + db.toSelect("select i from dbtest where i=?").argDouble(Double.MIN_VALUE).queryDoubleOrNull()); + + db.toInsert("insert into dbtest (i) values (?)").argDouble(Double.MAX_VALUE).insert(1); + assertEquals(Double.valueOf(Double.MAX_VALUE), + db.toSelect("select i from dbtest where i=?").argDouble(Double.MAX_VALUE).queryDoubleOrNull()); + } + + @Test + public void argDoubleNaN() { + new Schema().addTable("dbtest").addColumn("i").asDouble().schema().execute(db); + + db.toInsert("insert into dbtest (i) values (?)").argDouble(Double.NaN).insert(1); + assertEquals(Double.valueOf(Double.NaN), + db.toSelect("select i from dbtest where i=?").argDouble(Double.NaN).queryDoubleOrNull()); + } + + @Test + public void argDoubleInfinity() { + new Schema().addTable("dbtest").addColumn("i").asDouble().schema().execute(db); + + db.toInsert("insert into dbtest (i) values (?)").argDouble(Double.NEGATIVE_INFINITY).insert(1); + assertEquals(Double.valueOf(Double.NEGATIVE_INFINITY), + db.toSelect("select i from dbtest where i=?").argDouble(Double.NEGATIVE_INFINITY).queryDoubleOrNull()); + + db.toInsert("insert into dbtest (i) values (?)").argDouble(Double.POSITIVE_INFINITY).insert(1); + assertEquals(Double.valueOf(Double.POSITIVE_INFINITY), + db.toSelect("select i from dbtest where i=?").argDouble(Double.POSITIVE_INFINITY).queryDoubleOrNull()); + } + + @Test + public void argDoubleZero() { + new Schema().addTable("dbtest").addColumn("i").asDouble().schema().execute(db); + + db.toInsert("insert into dbtest (i) values (?)").argDouble(0d).insert(1); + assertEquals(Double.valueOf(0d), + db.toSelect("select i from dbtest where i=?").argDouble(0d).queryDoubleOrNull()); + } + + @Test + public void argDoubleNegativeZero() { + new Schema().addTable("dbtest").addColumn("i").asDouble().schema().execute(db); + + db.toInsert("insert into dbtest (i) values (?)").argDouble(-0d).insert(1); + assertEquals(Double.valueOf(-0d), + db.toSelect("select i from dbtest where i=?").argDouble(-0d).queryDoubleOrNull()); + } + + @Test + public void argBigDecimal38Precision0() { + new Schema().addTable("dbtest").addColumn("i").asBigDecimal(38, 0).schema().execute(db); + + BigDecimal value = new BigDecimal("99999999999999999999999999999999999999"); // 38 digits + db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); + assertEquals(value, + db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); + } + + @Test + public void argBigDecimal38Precision1() { + new Schema().addTable("dbtest").addColumn("i").asBigDecimal(38, 1).schema().execute(db); + + BigDecimal value = new BigDecimal("9999999999999999999999999999999999999.9"); // 38 digits + db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); + assertEquals(value, + db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); + } + + @Test + public void argBigDecimal38Precision37() { + new Schema().addTable("dbtest").addColumn("i").asBigDecimal(38, 37).schema().execute(db); + + BigDecimal value = new BigDecimal("9.9999999999999999999999999999999999999"); // 38 digits + db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); + assertEquals(value, + db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); + } + + @Test + public void argBigDecimal38Precision38() { + new Schema().addTable("dbtest").addColumn("i").asBigDecimal(38, 38).schema().execute(db); + + BigDecimal value = new BigDecimal("0.99999999999999999999999999999999999999"); // 38 digits + db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); + System.out.println(db.toSelect("select i from dbtest").queryBigDecimalOrNull()); + assertEquals(value, + db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); + } + + @Test + public void dropTableQuietly() { + db.dropTableQuietly("dbtest"); + new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db); + db.dropTableQuietly("dbtest"); + // Verify the quietly part really kicks in, since the table might have existed above + db.dropTableQuietly("dbtest"); + new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db); + } + + @Test + public void dropSequenceQuietly() { + db.dropSequenceQuietly("dbtest_seq"); + // Verify the quietly part really kicks in, since the sequence might have existed above + db.dropSequenceQuietly("dbtest_seq"); + } + + @Test + public void insertReturningPkSeq() { + db.dropSequenceQuietly("dbtest_seq"); + + db.ddl("create table dbtest (pk numeric)").execute(); + db.ddl("create sequence dbtest_seq start with 1").execute(); + + assertEquals(Long.valueOf(1L), db.toInsert("insert into dbtest (pk) values (:seq)") + .argPkSeq(":seq", "dbtest_seq").insertReturningPkSeq("pk")); + assertEquals(Long.valueOf(2L), db.toInsert("insert into dbtest (pk) values (:seq)") + .argPkSeq(":seq", "dbtest_seq").insertReturningPkSeq("pk")); + } + + @Test + public void insertReturningAppDate() { + db.dropSequenceQuietly("dbtest_seq"); + + new Schema() + .addTable("dbtest") + .addColumn("pk").primaryKey().table() + .addColumn("d").asDate().table().schema() + .addSequence("dbtest_seq").schema() + .execute(db); + + db.toInsert("insert into dbtest (pk, d) values (:seq, :d)") + .argPkSeq(":seq", "dbtest_seq") + .argDateNowPerApp(":d") + .insertReturning("dbtest", "pk", rs -> { + assertTrue(rs.next()); + assertEquals(Long.valueOf(1L), rs.getLongOrNull(1)); + assertThat(rs.getDateOrNull(2), equalTo(now)); + assertFalse(rs.next()); + return null; + }, "d"); + assertEquals(Long.valueOf(1L), db.toSelect("select count(*) from dbtest where d=?").argDate(now).queryLongOrNull()); + } + + @Test + public void quickQueries() { + new Schema() + .addTable("dbtest") + .addColumn("pk").primaryKey().table() + .addColumn("d").asDate().table() + .addColumn("d2").asDate().table() + .addColumn("d3").asLocalDate().table() + .addColumn("d4").asLocalDate().table() + .addColumn("s").asString(5).table() + .addColumn("s2").asString(5).table() + .addColumn("i").asInteger().table().schema() + .execute(db); + + db.toInsert("insert into dbtest (pk, d, d3, s) values (?,?,?,?)") + .argLong(1L).argDateNowPerApp().argLocalDate(localDateNow).argString("foo").insert(1); + + assertEquals(Long.valueOf(1L), db.toSelect("select pk from dbtest").queryLongOrNull()); + assertNull(db.toSelect("select pk from dbtest where 1=0").queryLongOrNull()); + assertNull(db.toSelect("select i from dbtest").queryLongOrNull()); + assertEquals(1L, db.toSelect("select pk from dbtest").queryLongOrZero()); + assertEquals(0L, db.toSelect("select pk from dbtest where 1=0").queryLongOrZero()); + assertEquals(0L, db.toSelect("select i from dbtest").queryLongOrZero()); + assertEquals(1L, (long) db.toSelect("select pk from dbtest").queryLongs().get(0)); + assertTrue(db.toSelect("select pk from dbtest where 1=0").queryLongs().isEmpty()); + assertTrue(db.toSelect("select i from dbtest").queryLongs().isEmpty()); + + assertEquals(Integer.valueOf(1), db.toSelect("select pk from dbtest").queryIntegerOrNull()); + assertNull(db.toSelect("select pk from dbtest where 1=0").queryIntegerOrNull()); + assertNull(db.toSelect("select i from dbtest").queryIntegerOrNull()); + assertEquals(1, db.toSelect("select pk from dbtest").queryIntegerOrZero()); + assertEquals(0, db.toSelect("select pk from dbtest where 1=0").queryIntegerOrZero()); + assertEquals(0, db.toSelect("select i from dbtest").queryIntegerOrZero()); + assertEquals(1L, (int) db.toSelect("select pk from dbtest").queryIntegers().get(0)); + assertTrue(db.toSelect("select pk from dbtest where 1=0").queryIntegers().isEmpty()); + assertTrue(db.toSelect("select i from dbtest").queryIntegers().isEmpty()); + + assertEquals("foo", db.toSelect("select s from dbtest").queryStringOrNull()); + assertNull(db.toSelect("select s from dbtest where 1=0").queryStringOrNull()); + assertNull(db.toSelect("select s2 from dbtest").queryStringOrNull()); + assertEquals("foo", db.toSelect("select s from dbtest").queryStringOrEmpty()); + assertEquals("", db.toSelect("select s from dbtest where 1=0").queryStringOrEmpty()); + assertEquals("", db.toSelect("select s2 from dbtest").queryStringOrEmpty()); + assertEquals("foo", db.toSelect("select s from dbtest").queryStrings().get(0)); + assertTrue(db.toSelect("select s from dbtest where 1=0").queryStrings().isEmpty()); + assertTrue(db.toSelect("select s2 from dbtest").queryStrings().isEmpty()); + + assertEquals(now, db.toSelect("select d from dbtest").queryDateOrNull()); + assertNull(db.toSelect("select d from dbtest where 1=0").queryDateOrNull()); + assertNull(db.toSelect("select d2 from dbtest").queryDateOrNull()); + assertEquals(db.toSelect("select d from dbtest").queryDates().get(0), now); + assertTrue(db.toSelect("select d from dbtest where 1=0").queryDates().isEmpty()); + assertTrue(db.toSelect("select d2 from dbtest").queryDates().isEmpty()); + + assertEquals(localDateNow, db.toSelect("select d3 from dbtest").queryLocalDateOrNull()); + assertNull(db.toSelect("select d3 from dbtest where 1=0").queryLocalDateOrNull()); + assertEquals(db.toSelect("select d3 from dbtest").queryLocalDates().get(0), localDateNow); + assertEquals(Long.valueOf(1L), + db.toSelect("select count(*) from dbtest where d3=?").argLocalDate(localDateNow).queryLongOrNull()); + + assertNull(db.toSelect("select d4 from dbtest").queryLocalDateOrNull()); + assertNull(db.toSelect("select d4 from dbtest where 1=0").queryLocalDateOrNull()); + assertTrue(db.toSelect("select d4 from dbtest").queryLocalDates().isEmpty()); + } + + @Test + public void rowHandlerQueries() { + new Schema() + .addTable("dbtest") + .addColumn("pk").primaryKey().schema() + .execute(db); + + db.toInsert("insert into dbtest (pk) values (?)").argLong(1L).insert(1); + db.toInsert("insert into dbtest (pk) values (?)").argLong(2L).insert(1); + + RowHandler rowHandler = Row::getLongOrNull; + + List many = db.toSelect("select pk from dbtest").queryMany(rowHandler); + assertEquals(2, many.size()); + + assertEquals(Long.valueOf(1), db.toSelect("select pk from dbtest where pk=1").queryOneOrNull(rowHandler)); + assertNull(db.toSelect("select pk from dbtest where pk=9").queryOneOrNull(rowHandler)); + try { + db.toSelect("select pk from dbtest").queryOneOrNull(rowHandler); + fail("Should have thrown an exception"); + } catch (ConstraintViolationException e) { + assertEquals("Expected exactly one row to be returned but found multiple", e.getCause().getMessage()); + } + try { + db.toSelect("select pk from dbtest where pk=9").queryOneOrThrow(rowHandler); + fail("Should have thrown an exception"); + } catch (ConstraintViolationException e) { + assertEquals("Expected exactly one row to be returned but found none", e.getMessage()); + } + + assertEquals(Long.valueOf(1), db.toSelect("select pk from dbtest where pk=1").queryFirstOrNull(rowHandler)); + assertEquals(Long.valueOf(1), db.toSelect("select pk from dbtest order by 1").queryFirstOrNull(rowHandler)); + assertNull(db.toSelect("select pk from dbtest where pk=9").queryFirstOrNull(rowHandler)); + try { + db.toSelect("select pk from dbtest where pk=9").queryFirstOrThrow(rowHandler); + fail("Should have thrown an exception"); + } catch (ConstraintViolationException e) { + assertEquals("Expected one or more rows to be returned but found none", e.getMessage()); + } + } + + @Test + public void nextSequenceValue() { + db.dropSequenceQuietly("dbtest_seq"); + new Schema() + .addSequence("dbtest_seq").schema() + .execute(db); + + assertEquals(Long.valueOf(1L), db.nextSequenceValue("dbtest_seq")); + } + + @Test + public void insertReturningDbDate() { + db.dropSequenceQuietly("dbtest_seq"); + new Schema() + .addTable("dbtest") + .addColumn("pk").primaryKey().table() + .addColumn("d").asDate().table().schema() + .addSequence("dbtest_seq").schema() + .execute(db); + Date dbNow = db.toInsert("insert into dbtest (pk, d) values (:seq, :d)") + .argPkSeq(":seq", "dbtest_seq") + .argDateNowPerDb(":d") + .insertReturning("dbtest", "pk", rs -> { + assertTrue(rs.next()); + assertEquals(Long.valueOf(1L), rs.getLongOrNull(1)); + Date dbDate = rs.getDateOrNull(2); + assertFalse(rs.next()); + return dbDate; + }, "d"); + assertEquals(Long.valueOf(1L), db.toSelect("select count(*) from dbtest where d=?").argDate(dbNow).queryLongOrNull()); + } + + @Test + public void daylightSavings() { + LocalDate lastStdDateSpring = LocalDate.of(2019, Month.MARCH, 9); + LocalDate firstDSTDateSpring = LocalDate.of(2019, Month.MARCH, 10); + // Verify that the original LocalDate matches the driver SQL LocalDate generated. + StatementAdaptor adaptor = new StatementAdaptor(new OptionsDefault(db.flavor())); + assertEquals(lastStdDateSpring.toString(), adaptor.nullLocalDate(lastStdDateSpring).toString()); + assertEquals(firstDSTDateSpring.toString(), adaptor.nullLocalDate(firstDSTDateSpring).toString()); + } + + @Test + public void insertLocalDate() { + // Date without time + new Schema() + .addTable("dbtest") + .addColumn("d").asLocalDate().table().schema() + .execute(db); + LocalDate dateOfBirth = LocalDate.of(1951, Month.AUGUST, 9); + db.toInsert("insert into dbtest (d) values (?)") + .argLocalDate(dateOfBirth) + .insert(1); + LocalDate testDate = db.toSelect("select d from dbtest").queryLocalDateOrNull(); + assertEquals(dateOfBirth, testDate); + } + + @Test + public void localDateRoundTrip() { + new Schema() + .addTable("dbtest") + .addColumn("d1").asLocalDate().table() + .addColumn("d2").asLocalDate().table().schema() + .execute(db); + // Store current time as per the database + db.toInsert("insert into dbtest (d1) values (?)") + .argLocalDate(localDateNow) + .insert(1); + // Now pull it out, put it back in, and verify it matches in the database + LocalDate queryRsDate = db.toSelect("select d1 from dbtest").queryLocalDateOrNull(); + db.toUpdate("update dbtest set d2=?") + .argLocalDate(queryRsDate) + .update(1); + assertEquals(Long.valueOf(1L), db.toSelect("select count(*) from dbtest where d1=d2").queryLongOrNull()); + } + + /** + * Make sure database times are inserted with at least millisecond precision. + * This test is non-deterministic since it is checking the timestamp provided + * by the database, so we use a retry to give it up to ten attempts. + */ + @Test + public void dateMillis() { + for (int attempts = 1; attempts <= 10; attempts++) { + new Schema() + .addTable("dbtest") + .addColumn("d").asDate().table().schema() + .execute(db); + db.toInsert("insert into dbtest (d) values (?)") + .argDateNowPerDb() + .insert(1); + Date dbNow = db.toSelect("select d from dbtest").queryDateOrNull(); + if (dbNow != null && dbNow.getTime() % 10 != 0) { + return; + } + System.out.println("Zero in least significant digit (attempt " + attempts + ")"); + db.dropTableQuietly(TEST_TABLE_NAME); + } + fail("Timestamp had zero in the least significant digit"); + } + + @Test + public void dateRoundTrip() { + new Schema() + .addTable("dbtest") + .addColumn("d1").asDate().table() + .addColumn("d2").asDate().table().schema() + .execute(db); + // Store current time as per the database + db.toInsert("insert into dbtest (d1) values (?)") + .argDateNowPerDb() + .insert(1); + // Now pull it out, put it back in, and verify it matches in the database + Date dbNow = db.toSelect("select d1 from dbtest").queryDateOrNull(); + db.toUpdate("update dbtest set d2=?") + .argDate(dbNow) + .update(1); + + assertEquals(Long.valueOf(1L), db.toSelect("select count(*) from dbtest where d1=d2").queryLongOrNull()); + } + + @Test + public void dateRoundTripTimezones() { + new Schema() + .addTable("dbtest") + .addColumn("d").asDate().table().schema() + .execute(db); + Date date = new Date(166656789L); + TimeZone defaultTZ = TimeZone.getDefault(); + try { + TimeZone.setDefault(TimeZone.getTimeZone("GMT-4:00")); + db.toInsert("insert into dbtest (d) values (?)").argDate(date).insert(1); + assertEquals(date, db.toSelect("select d from dbtest").queryDateOrNull()); + assertEquals("1970-01-02 18:17:36.789000-0400", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000Z").format( + db.toSelect("select d from dbtest").queryDateOrNull())); + db.toDelete("delete from dbtest where d=?").argDate(date).update(1); + TimeZone.setDefault(TimeZone.getTimeZone("GMT+4:00")); + db.toInsert("insert into dbtest (d) values (?)").argDate(date).insert(1); + assertEquals(date, db.toSelect("select d from dbtest").queryDateOrNull()); + assertEquals("1970-01-03 02:17:36.789000+0400", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000Z").format( + db.toSelect("select d from dbtest").queryDateOrNull())); + db.toDelete("delete from dbtest where d=?").argDate(date).update(1); + } finally { + TimeZone.setDefault(defaultTZ); + } + } + + /** + * Verify the appropriate database flavor can correctly convert a {@code Date} + * into a SQL function representing a conversion from string to timestamp. This + * function is used to write debug SQL to the log in a way that could be manually + * executed if desired. + */ + @Test + public void stringDateFunctions() { + Date date = new Date(166656789L); + System.out.println("Date: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000Z").format(date)); + TimeZone defaultTZ = TimeZone.getDefault(); + try { + TimeZone.setDefault(TimeZone.getTimeZone("GMT-4:00")); + new Schema() + .addTable("dbtest") + .addColumn("d").asDate().schema().execute(db); + db.toInsert("insert into dbtest (d) values (" + + db.flavor().dateAsSqlFunction(date, db.options().calendarForTimestamps()).replace(":", "::") + ")") + .insert(1); + assertEquals("1970-01-02 18:17:36.789000-0400", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000Z").format( + db.toSelect("select d from dbtest").queryDateOrNull())); + // Now do some client operations in a different time zone + TimeZone.setDefault(TimeZone.getTimeZone("GMT+4:00")); + // Verify regular arg maps date the same way even though our TimeZone is now different + db.toDelete("delete from dbtest where d=?").argDate(date).update(1); + db.toInsert("insert into dbtest (d) values (" + + db.flavor().dateAsSqlFunction(date, db.options().calendarForTimestamps()).replace(":", "::") + ")") + .insert(1); + assertEquals("1970-01-03 02:17:36.789000+0400", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000Z").format( + db.toSelect("select d from dbtest").queryDateOrNull())); + // Verify the function maps correctly for equals operations as well + db.toDelete("delete from dbtest where d=" + db.flavor().dateAsSqlFunction(date, + db.options().calendarForTimestamps()).replace(":", "::")).update(1); + } finally { + TimeZone.setDefault(defaultTZ); + } + } + + @Test + public void mixPositionalAndNamedParameters() { + new Schema() + .addTable("dbtest") + .addColumn("pk").primaryKey().table() + .addColumn("d").asDate().table() + .addColumn("a").asInteger().table().schema() + .execute(db); + + db.toSelect("select pk as \"time:: now??\" from dbtest where a=? and d=:now") + .argInteger(1).argDateNowPerDb("now").query(rs -> { + assertFalse(rs.next()); + return null; + }); + } + + public String readerToString(Reader reader) throws IOException { + char[] buffer = new char[1024]; + StringBuilder out = new StringBuilder(); + int byteCount; + while ((byteCount = reader.read(buffer, 0, buffer.length)) >= 0) { + out.append(buffer, 0, byteCount); + } + return out.toString(); + } + + public byte[] inputStreamToString(InputStream inputStream) throws IOException { + byte[] buffer = new byte[1024]; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int byteCount; + while ((byteCount = inputStream.read(buffer, 0, buffer.length)) >= 0) { + out.write(buffer, 0, byteCount); + } + return out.toByteArray(); + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/ConfigTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/ConfigTest.java new file mode 100644 index 0000000..f0f47d8 --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/ConfigTest.java @@ -0,0 +1,250 @@ +package org.xbib.jdbc.query.test; + +import org.junit.jupiter.api.Test; +import org.xbib.jdbc.query.Config; +import org.xbib.jdbc.query.ConfigFrom; + +import java.io.File; +import java.io.FileWriter; +import java.math.BigDecimal; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for the configuration classes. + */ +public class ConfigTest { + + @Test + public void testSystemProperties() { + System.setProperty("foo", "bar"); + Config config = ConfigFrom.firstOf().systemProperties().get(); + assertEquals("bar", config.getString("foo")); + assertNull(config.getString("unknown")); + assertEquals("default", config.getString("unknown", "default")); + } + + @Test + public void testProperties() { + Properties properties = new Properties(); + properties.setProperty("foo", "bar"); + + Config config = ConfigFrom.firstOf().properties(properties).get(); + + assertEquals("bar", config.getString("foo")); + assertNull(config.getString("unknown")); + assertEquals("default", config.getString("unknown", "default")); + } + + @Test + public void testPropertyFiles() throws Exception { + Properties properties = new Properties(); + + properties.setProperty("foo", "1"); + String filename1 = "build/ConfigTest-properties-1.properties"; + properties.store(new FileWriter(filename1), null); + + properties.setProperty("foo", "2"); + properties.setProperty("foo2", "-2"); + String filename2 = "build/ConfigTest-properties-2.properties"; + properties.store(new FileWriter(filename2), null); + + // Throw a null in here just to make sure it doesn't blow up + Config config = ConfigFrom.firstOf().propertyFile(filename1, null, filename2).get(); + + assertEquals(Integer.valueOf(1), config.getInteger("foo")); + assertEquals(Integer.valueOf(-2), config.getInteger("foo2")); + assertNull(config.getInteger("unknown")); + assertEquals(5, config.getInteger("unknown", 5)); + + // Now flip the order and verify precedence works + config = ConfigFrom.firstOf().propertyFile(filename2, null, filename1).get(); + assertEquals(Integer.valueOf(2), config.getInteger("foo")); + assertEquals(Integer.valueOf(-2), config.getInteger("foo2")); + + // Same as above tests, but using File version rather than filename String + config = ConfigFrom.firstOf().propertyFile(new File(filename1), new File("does not exist"), new File(filename2)).get(); + + assertEquals(Integer.valueOf(1), config.getInteger("foo")); + assertEquals(Integer.valueOf(-2), config.getInteger("foo2")); + assertNull(config.getInteger("unknown")); + assertEquals(5, config.getInteger("unknown", 5)); + + // Now flip the order and verify precedence works + config = ConfigFrom.firstOf().propertyFile(new File(filename2), null, new File(filename1)).get(); + assertEquals(Integer.valueOf(2), config.getInteger("foo")); + assertEquals(Integer.valueOf(-2), config.getInteger("foo2")); + } + + @Test + public void testNested() { + Config config = ConfigFrom.firstOf() + .config(ConfigFrom.firstOf().custom(key -> key.equals("foo") ? "a" : null)) + .config(ConfigFrom.firstOf().custom(key -> key.equals("foo") ? "b" : null)).get(); + + assertEquals("a", config.getString("foo")); + + // Re-mapping prefix in nested config + config = ConfigFrom.firstOf() + .config(ConfigFrom.firstOf().custom(key -> key.equals("a.foo") ? "a" : null).removePrefix("a.")) + .config(ConfigFrom.firstOf().custom(key -> key.equals("foo") ? "b" : null)).get(); + + assertEquals("a", config.getString("foo")); + + // Excluding nested config, should skip to next + config = ConfigFrom.firstOf() + .config(ConfigFrom.firstOf().custom(key -> key.equals("a.foo") ? "a" : null).removePrefix("a.").excludeRegex("fo{2}")) + .config(ConfigFrom.firstOf().custom(key -> key.equals("foo") ? "b" : null)).get(); + + assertEquals("b", config.getString("foo")); + assertNull(config.getString("foooo")); + + config = ConfigFrom.firstOf() + .config(ConfigFrom.firstOf().custom(key -> key.equals("a.foo") ? "a" : null).excludePrefix("a.", "other.")) + .config(ConfigFrom.firstOf().custom(key -> key.equals("foo") ? "b" : null).addPrefix("a.")).get(); + + assertEquals("b", config.getString("a.foo")); + assertNull(config.getString("foo")); + + config = ConfigFrom.firstOf() + .config(ConfigFrom.firstOf().custom(key -> key.equals("a.foo") ? "a" : null).includePrefix("other.")) + .config(ConfigFrom.firstOf().custom(key -> key.equals("foo") ? "b" : null).addPrefix("a.").includeRegex("a.*f.*")).get(); + + assertEquals("b", config.getString("a.foo")); + assertNull(config.getString("foo")); + assertNull(config.getString("other.foo")); + } + + @Test + public void testStripPrefixConflict() { + Config config = ConfigFrom.firstOf().value("a.foo", "a").value("foo", "bar").removePrefix("a.").get(); + + assertEquals("bar", config.getString("foo")); + } + + @Test + public void testException() { + Config config = ConfigFrom.firstOf().custom(key -> { + throw new SecurityException("Pretending security policy is in place"); + }).get(); + + // We do this call twice, but you should see only one warning in the log + assertEquals("default", config.getString("foo", "default")); + assertEquals("default", config.getString("foo", "default")); + } + + @Test + public void testTidyValues() { + Config config = ConfigFrom.firstOf().value("foo", " a ").get(); + + // Strip whitespace + assertEquals("a", config.getString("foo")); + + config = ConfigFrom.firstOf().value("foo", " ").value("foo", "").value("foo", null).value("foo", "a").get(); + + // Skip over the garbage ones + assertEquals("a", config.getString("foo")); + } + + @Test + public void testBoolean() { + // Case insensitive, allow either true/false or yes/no + Config config = ConfigFrom.firstOf().value("foo", "tRuE").get(); + + assertTrue(config.getBooleanOrFalse("foo")); + assertTrue(config.getBooleanOrTrue("foo")); + assertFalse(config.getBooleanOrFalse("unknown")); + assertTrue(config.getBooleanOrTrue("unknown")); + + config = ConfigFrom.firstOf().value("foo", "yEs").get(); + + assertTrue(config.getBooleanOrFalse("foo")); + assertTrue(config.getBooleanOrTrue("foo")); + assertFalse(config.getBooleanOrFalse("unknown")); + assertTrue(config.getBooleanOrTrue("unknown")); + + config = ConfigFrom.firstOf().value("foo", "fAlSe").get(); + + assertFalse(config.getBooleanOrFalse("foo")); + assertFalse(config.getBooleanOrTrue("foo")); + assertFalse(config.getBooleanOrFalse("unknown")); + assertTrue(config.getBooleanOrTrue("unknown")); + + config = ConfigFrom.firstOf().value("foo", "nO").get(); + + assertFalse(config.getBooleanOrFalse("foo")); + assertFalse(config.getBooleanOrTrue("foo")); + assertFalse(config.getBooleanOrFalse("unknown")); + assertTrue(config.getBooleanOrTrue("unknown")); + + config = ConfigFrom.firstOf().value("foo", "bad value").get(); + + assertFalse(config.getBooleanOrFalse("foo")); + assertTrue(config.getBooleanOrTrue("foo")); + assertFalse(config.getBooleanOrFalse("unknown")); + assertTrue(config.getBooleanOrTrue("unknown")); + } + + @Test + public void testInteger() { + Config config = ConfigFrom.firstOf().value("good", "123").value("bad", "hi").get(); + assertEquals(Integer.valueOf(123), config.getInteger("good")); + assertNull(config.getInteger("bad")); + assertNull(config.getInteger("missing")); + assertEquals(123, config.getInteger("good", 5)); + assertEquals(5, config.getInteger("bad", 5)); + assertEquals(5, config.getInteger("missing", 5)); + } + + @Test + public void testLong() { + Config config = ConfigFrom.firstOf().value("good", "123").value("bad", "hi").get(); + + assertEquals(Long.valueOf(123), config.getLong("good")); + assertNull(config.getLong("bad")); + assertNull(config.getLong("missing")); + assertEquals(123, config.getLong("good", 5)); + assertEquals(5, config.getLong("bad", 5)); + assertEquals(5, config.getLong("missing", 5)); + } + + @Test + public void testFloat() { + Config config = ConfigFrom.firstOf().value("good", "123.45").value("bad", "hi").get(); + + assertEquals(Float.valueOf(123.45f), config.getFloat("good")); + assertNull(config.getFloat("bad")); + assertNull(config.getFloat("missing")); + assertEquals(123.45, config.getFloat("good", 5.45f), 0.001); + assertEquals(5.45, config.getFloat("bad", 5.45f), 0.001); + assertEquals(5.45, config.getFloat("missing", 5.45f), 0.001); + } + + @Test + public void testDouble() { + Config config = ConfigFrom.firstOf().value("good", "123.45").value("bad", "hi").get(); + + assertEquals(Double.valueOf(123.45), config.getDouble("good")); + assertNull(config.getDouble("bad")); + assertNull(config.getDouble("missing")); + assertEquals(123.45, config.getDouble("good", 5.45), 0.001); + assertEquals(5.45, config.getDouble("bad", 5.45), 0.001); + assertEquals(5.45, config.getDouble("missing", 5.45), 0.001); + } + + @Test + public void testBigDecimal() { + Config config = ConfigFrom.firstOf().value("good", "123.45").value("bad", "hi").get(); + + assertEquals(new BigDecimal("123.45"), config.getBigDecimal("good")); + assertNull(config.getBigDecimal("bad")); + assertNull(config.getBigDecimal("missing")); + assertEquals(new BigDecimal("123.45"), config.getBigDecimal("good", new BigDecimal("5.45"))); + assertEquals(new BigDecimal("5.45"), config.getBigDecimal("bad", new BigDecimal("5.45"))); + assertEquals(new BigDecimal("5.45"), config.getBigDecimal("missing", new BigDecimal("5.45"))); + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DatabaseMock.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DatabaseMock.java new file mode 100644 index 0000000..ea73424 --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DatabaseMock.java @@ -0,0 +1,18 @@ +package org.xbib.jdbc.query.test; + +/** + * Convenience class to intercept calls to Connection and return stubbed results + * for testing purposes. + */ +public interface DatabaseMock { + + RowStub query(String executeSql, String debugSql); + + Integer insert(String executeSql, String debugSql); + + Long insertReturningPk(String executeSql, String debugSql); + + RowStub insertReturning(String executeSql, String debugSql); + + Integer update(String executeSql, String debugSql); +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DerbyTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DerbyTest.java new file mode 100644 index 0000000..d16d542 --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/DerbyTest.java @@ -0,0 +1,145 @@ +package org.xbib.jdbc.query.test; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.xbib.jdbc.query.DatabaseProvider; +import org.xbib.jdbc.query.OptionsOverride; +import org.xbib.jdbc.query.Schema; + +import java.math.BigDecimal; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/** + * Exercise Database functionality with a real database (Derby). + */ +public class DerbyTest extends CommonTest { + + private static final Logger logger = Logger.getLogger(DerbyTest.class.getName()); + + static { + System.setProperty("derby.stream.error.file", System.getProperty("user.dir") + "/build/derby-errors.log"); + } + + @Override + protected DatabaseProvider createDatabaseProvider(OptionsOverride options) { + return DatabaseProvider.fromDriverManager("jdbc:derby:build/testdb;create=true") + .withSqlParameterLogging().withSqlInExceptionMessages().withOptions(options).create(); + } + + // TODO fix this test + @Disabled("Not sure why this fails on the build servers right now...") + @Test + public void clockSync() { + super.clockSync(); + } + + @Disabled("Derby prohibits NaN and Infinity (https://issues.apache.org/jira/browse/DERBY-3290)") + @Test + public void argFloatNaN() { + super.argFloatNaN(); + } + + @Disabled("Derby prohibits NaN and Infinity (https://issues.apache.org/jira/browse/DERBY-3290)") + @Test + public void argFloatInfinity() { + super.argFloatInfinity(); + } + + @Disabled("Derby prohibits NaN and Infinity (https://issues.apache.org/jira/browse/DERBY-3290)") + @Test + public void argDoubleNaN() { + super.argDoubleNaN(); + } + + @Disabled("Derby prohibits NaN and Infinity (https://issues.apache.org/jira/browse/DERBY-3290)") + @Test + public void argDoubleInfinity() { + super.argDoubleInfinity(); + } + + @Disabled("Current Derby behavior is to convert -0f to 0f") + @Test + public void argFloatNegativeZero() { + super.argFloatNegativeZero(); + } + + @Disabled("Current Derby behavior is to convert -0d to 0d") + @Test + public void argDoubleNegativeZero() { + super.argDoubleNegativeZero(); + } + + @Disabled("Derby does not support timestamp intervals") + @Test + public void intervals() { + super.intervals(); + } + + @Test + public void argBigDecimal31Precision0() { + db.dropTableQuietly("dbtest"); + new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 0).schema().execute(db); + BigDecimal value = new BigDecimal("9999999999999999999999999999999"); // 31 digits + db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); + assertEquals(value, + db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); + } + + @Test + public void argBigDecimal31Precision1() { + db.dropTableQuietly("dbtest"); + new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 1).schema().execute(db); + BigDecimal value = new BigDecimal("999999999999999999999999999999.9"); // 31 digits + db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); + assertEquals(value, + db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); + } + + @Test + public void argBigDecimal31Precision30() { + db.dropTableQuietly("dbtest"); + new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 30).schema().execute(db); + BigDecimal value = new BigDecimal("9.999999999999999999999999999999"); // 31 digits + db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); + assertEquals(value, + db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); + } + + @Test + public void argBigDecimal31Precision31() { + db.dropTableQuietly("dbtest"); + new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 31).schema().execute(db); + BigDecimal value = new BigDecimal("0.9999999999999999999999999999999"); // 31 digits + db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); + System.out.println(db.toSelect("select i from dbtest").queryBigDecimalOrNull()); + assertEquals(value, + db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); + } + + @Disabled("Derby limits out at precision 31") + @Test + public void argBigDecimal38Precision0() { + super.argBigDecimal38Precision0(); + } + + @Disabled("Derby limits out at precision 31") + @Test + public void argBigDecimal38Precision1() { + super.argBigDecimal38Precision1(); + } + + @Disabled("Derby limits out at precision 31") + @Test + public void argBigDecimal38Precision37() { + super.argBigDecimal38Precision37(); + } + + @Disabled("Derby limits out at precision 31") + @Test + public void argBigDecimal38Precision38() { + super.argBigDecimal38Precision38(); + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/HsqldbTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/HsqldbTest.java new file mode 100644 index 0000000..a11a53e --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/HsqldbTest.java @@ -0,0 +1,177 @@ +package org.xbib.jdbc.query.test; + +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.xbib.jdbc.query.Config; +import org.xbib.jdbc.query.ConfigFrom; +import org.xbib.jdbc.query.DatabaseProvider; +import org.xbib.jdbc.query.OptionsOverride; +import org.xbib.jdbc.query.Schema; +import org.xbib.jdbc.query.Sql; +import org.xbib.jdbc.query.SqlArgs; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Exercise database functionality with a real HyperSQL database. + */ +public class HsqldbTest extends CommonTest { + + private static final Logger logger = Logger.getLogger(HsqldbTest.class.getName()); + + @Override + protected DatabaseProvider createDatabaseProvider(OptionsOverride options) throws Exception { + String propertiesFile = System.getProperty("local.properties", "local.properties"); + Config config = ConfigFrom.firstOf() + .systemProperties() + .propertyFile(propertiesFile) + .excludePrefix("database.") + .removePrefix("hsqldb.").get(); + logger.log(Level.INFO, "config = " + config); + return DatabaseProvider.fromDriverManager(config) + .withSqlParameterLogging() + .withSqlInExceptionMessages() + .withOptions(options).create(); + } + + @Test + public void noDatabaseAccess() throws Exception { + DatabaseProvider provider = createDatabaseProvider(new OptionsOverride()); + provider.transact(dbp -> { + // Do nothing, just making sure no exception is thrown + }); + provider.transact((dbp, tx) -> { + // Do nothing, just making sure no exception is thrown + }); + provider.transact((dbp, tx) -> { + tx.setRollbackOnError(true); + // Do nothing, just making sure no exception is thrown + }); + provider.transact((dbp, tx) -> { + tx.setRollbackOnly(true); + // Do nothing, just making sure no exception is thrown + }); + } + + @Disabled("LocalDate implementations should be TimeZone agnostic, but HSQLDB implementation has a bug.") + @Test + public void argLocalDateTimeZones() { + // See bug: https://bugs.documentfoundation.org/show_bug.cgi?id=63566 + super.argLocalDateTimeZones(); + } + + /** + * This one is adjusted in that the float values are passed as double, because + * the database stores them both as double and there doesn't appear to be a way + * to tell that one was actually declared as a float. + */ + @Test + public void saveResultAsTable() { + new Schema().addTable("dbtest") + .addColumn("nbr_integer").asInteger().primaryKey().table() + .addColumn("nbr_long").asLong().table() + .addColumn("nbr_float").asFloat().table() + .addColumn("nbr_double").asDouble().table() + .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table() + .addColumn("str_varchar").asString(80).table() + .addColumn("str_fixed").asStringFixed(1).table() + .addColumn("str_lob").asClob().table() + .addColumn("bin_blob").asBlob().table() + .addColumn("boolean_flag").asBoolean().table() + .addColumn("date_millis").asDate().table() + .addColumn("local_date").asLocalDate().schema().execute(db); + + db.toInsert("insert into dbtest (nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar," + + " str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date) values (?,?,?,?,?,?,?,?,?,?,?,?)") + .argInteger(Integer.MAX_VALUE).argLong(Long.MAX_VALUE).argDouble((double) Float.MAX_VALUE) + .argDouble(Double.MAX_VALUE).argBigDecimal(new BigDecimal("123.456")) + .argString("hello").argString("Z").argClobString("hello again") + .argBlobBytes(new byte[]{'1', '2'}).argBoolean(true) + .argDateNowPerApp().argLocalDate(localDateNow).insert(1); + + db.toInsert("insert into dbtest (nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar," + + " str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date) values (?,?,?,?,?,?,?,?,?,?,?,?)") + .argInteger(Integer.MIN_VALUE).argLong(Long.MIN_VALUE).argDouble(0.000001d) + .argDouble(Double.MIN_VALUE).argBigDecimal(new BigDecimal("-123.456")) + .argString("goodbye").argString("A").argClobString("bye again") + .argBlobBytes(new byte[]{'3', '4'}).argBoolean(false) + .argDateNowPerApp().argLocalDate(localDateNow).insert(1); + + String expectedSchema = new Schema().addTable("dbtest2") + .addColumn("nbr_integer").asInteger().table() + .addColumn("nbr_long").asLong().table() + .addColumn("nbr_float").asFloat().table() + .addColumn("nbr_double").asDouble().table() + .addColumn("nbr_big_decimal").asBigDecimal(19, 9).table() + .addColumn("str_varchar").asString(80).table() + .addColumn("str_fixed").asStringFixed(1).table() + .addColumn("str_lob").asClob().table() + .addColumn("bin_blob").asBlob().table() + .addColumn("boolean_flag").asBoolean().table() + .addColumn("date_millis").asDate().table() + .addColumn("local_date").asLocalDate().schema().print(db.flavor()); + + List args = db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal," + + " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest") + .query(rs -> { + List result = new ArrayList<>(); + while (rs.next()) { + if (result.size() == 0) { + db.dropTableQuietly("dbtest2"); + Schema schema = new Schema().addTableFromRow("dbtest2", rs).schema(); + assertEquals(expectedSchema, schema.print(db.flavor())); + schema.execute(db); + } + result.add(SqlArgs.readRow(rs)); + } + return result; + }); + + db.toInsert(Sql.insert("dbtest2", args)).insertBatch(); + + assertEquals(2, db.toSelect("select count(*) from dbtest2").queryIntegerOrZero()); + assertEquals(db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal," + + " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest order by 1") + .queryMany(SqlArgs::readRow), + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal," + + " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest2 order by 1") + .queryMany(SqlArgs::readRow)); + assertEquals(Arrays.asList( + new SqlArgs() + .argInteger("nbr_integer", Integer.MIN_VALUE) + .argLong("nbr_long", Long.MIN_VALUE) + .argDouble("nbr_float", 0.000001d) + .argDouble("nbr_double", Double.MIN_VALUE) + .argBigDecimal("nbr_big_decimal", new BigDecimal("-123.456")) + .argString("str_varchar", "goodbye") + .argString("str_fixed", "A") + .argClobString("str_lob", "bye again") + .argBlobBytes("bin_blob", new byte[]{'3', '4'}) + .argString("boolean_flag", "N")//.argBoolean("boolean_flag", false) + .argDate("date_millis", now) + .argLocalDate("local_date", localDateNow), + new SqlArgs() + .argInteger("nbr_integer", Integer.MAX_VALUE) + .argLong("nbr_long", Long.MAX_VALUE) + .argDouble("nbr_float", (double) Float.MAX_VALUE) + .argDouble("nbr_double", Double.MAX_VALUE) + .argBigDecimal("nbr_big_decimal", new BigDecimal("123.456")) + .argString("str_varchar", "hello") + .argString("str_fixed", "Z") + .argClobString("str_lob", "hello again") + .argBlobBytes("bin_blob", new byte[]{'1', '2'}) + .argString("boolean_flag", "Y")//.argBoolean("boolean_flag", true) + .argDate("date_millis", now) + .argLocalDate("local_date", localDateNow)), + db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal," + + " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest2 order by 1") + .queryMany(SqlArgs::readRow)); + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/OracleTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/OracleTest.java new file mode 100644 index 0000000..76cbaeb --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/OracleTest.java @@ -0,0 +1,43 @@ +package org.xbib.jdbc.query.test; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.OracleContainer; +import org.testcontainers.junit.jupiter.Container; +import org.xbib.jdbc.query.DatabaseProvider; +import org.xbib.jdbc.query.OptionsOverride; + +import java.io.FileReader; +import java.util.Properties; + +/** + * Exercise Database functionality with a real Oracle database. + */ +public class OracleTest extends CommonTest { + + @Container + public OracleContainer oracleContainer = new OracleContainer(""); + + @Override + protected DatabaseProvider createDatabaseProvider(OptionsOverride options) throws Exception { + Properties properties = new Properties(); + properties.load(new FileReader(System.getProperty("local.properties", "local.properties"))); + return DatabaseProvider.fromDriverManager( + properties.getProperty("database.url"), + properties.getProperty("database.user"), + properties.getProperty("database.password") + ).withSqlParameterLogging().withSqlInExceptionMessages().withOptions(options).create(); + } + + @Disabled("Current Oracle behavior is to convert -0f to 0f") + @Test + public void argFloatNegativeZero() { + super.argFloatNegativeZero(); + } + + @Disabled("Current Oracle behavior is to convert -0d to 0d") + @Test + public void argDoubleNegativeZero() { + super.argDoubleNegativeZero(); + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/PostgreSqlTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/PostgreSqlTest.java new file mode 100644 index 0000000..818588c --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/PostgreSqlTest.java @@ -0,0 +1,50 @@ +package org.xbib.jdbc.query.test; + +import java.io.IOException; +import org.junit.jupiter.api.Test; +import org.xbib.jdbc.query.DatabaseProvider; +import org.xbib.jdbc.query.OptionsOverride; +import org.xbib.jdbc.query.Schema; + +import java.io.FileReader; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +/** + * Exercise Database functionality with a real PostgreSQL database. + */ +public class PostgreSqlTest extends CommonTest { + + @Override + protected DatabaseProvider createDatabaseProvider(OptionsOverride options) throws IOException { + Properties properties = new Properties(); + properties.load(new FileReader(System.getProperty("local.properties", "local.properties"))); + return DatabaseProvider.fromDriverManager( + properties.getProperty("postgres.database.url"), + properties.getProperty("postgres.database.user"), + properties.getProperty("postgres.database.password") + ).withOptions(options).withSqlParameterLogging().withSqlInExceptionMessages().create(); + } + + /** + * PostgreSQL seems to have different behavior in that is does not convert + * column names to uppercase (it actually converts them to lowercase). + * I haven't figured out how to smooth over this difference, since all databases + * seem to respect the provided case when it is inside quotes, but don't provide + * a way to tell whether a particular parameter was quoted. + */ + @Override + @Test + public void metadataColumnNames() { + db.dropTableQuietly("dbtest"); + + new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db); + + db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\" from dbtest") + .query(rs -> { + assertArrayEquals(new String[]{"pk", "foo", "Foo"}, rs.getColumnLabels()); + return null; + }); + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/RowStub.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/RowStub.java new file mode 100644 index 0000000..9723db8 --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/RowStub.java @@ -0,0 +1,799 @@ +package org.xbib.jdbc.query.test; + +import org.xbib.jdbc.query.DatabaseException; +import org.xbib.jdbc.query.Rows; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.math.BigDecimal; +import java.sql.ResultSetMetaData; +import java.sql.Types; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Convenience for specifying hard-coded values for the Rows object. Useful for testing, + * especially with Mock libraries. + */ +public class RowStub { + + private final List rows = new ArrayList<>(); + + private String[] columnNames; + + public RowStub withColumnNames(String... names) { + columnNames = names; + return this; + } + + public RowStub addRow(Object... columns) { + rows.add(columns); + return this; + } + + public Rows toRows() { + return new Rows() { + private int row = -1; + private int col = -1; + + @Override + public boolean next() { + col = -1; + return !rows.isEmpty() && ++row < rows.size(); + } + + + @Override + public String[] getColumnLabels() { + requireColumnNames(); + return columnNames; + } + + + @Override + public ResultSetMetaData getMetadata() { + requireColumnNames(); + return new ResultSetMetaData() { + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public boolean isAutoIncrement(int column) { + return false; + } + + @Override + public boolean isCaseSensitive(int column) { + return false; + } + + @Override + public boolean isSearchable(int column) { + return false; + } + + @Override + public boolean isCurrency(int column) { + return false; + } + + @Override + public int isNullable(int column) { + return columnNullable; + } + + @Override + public boolean isSigned(int column) { + return false; + } + + @Override + public int getColumnDisplaySize(int column) { + return 4000; + } + + @Override + public String getColumnLabel(int column) { + return columnNames[column - 1]; + } + + @Override + public String getColumnName(int column) { + return columnNames[column - 1]; + } + + @Override + public String getSchemaName(int column) { + return ""; + } + + @Override + public int getPrecision(int column) { + return 4000; + } + + @Override + public int getScale(int column) { + return 0; + } + + @Override + public String getTableName(int column) { + return ""; + } + + @Override + public String getCatalogName(int column) { + return ""; + } + + @Override + public int getColumnType(int column) { + return Types.VARCHAR; + } + + @Override + public String getColumnTypeName(int column) { + return "VARCHAR"; + } + + @Override + public boolean isReadOnly(int column) { + return false; + } + + @Override + public boolean isWritable(int column) { + return false; + } + + @Override + public boolean isDefinitelyWritable(int column) { + return false; + } + + @Override + public String getColumnClassName(int column) { + return String.class.getName(); + } + + @Override + public T unwrap(Class iface) { + return null; + } + + @Override + public boolean isWrapperFor(Class iface) { + return false; + } + }; + } + + + @Override + public Boolean getBooleanOrNull() { + return toBoolean(rows.get(row)[++col]); + } + + + @Override + public Boolean getBooleanOrNull(int columnOneBased) { + col = columnOneBased; + return toBoolean(rows.get(row)[columnOneBased - 1]); + } + + + @Override + public Boolean getBooleanOrNull(String columnName) { + col = columnIndexByName(columnName) + 1; + return toBoolean(rows.get(row)[columnIndexByName(columnName)]); + } + + @Override + public boolean getBooleanOrFalse() { + Boolean i = getBooleanOrNull(); + return i != null && i; + } + + @Override + public boolean getBooleanOrFalse(int columnOneBased) { + Boolean i = getBooleanOrNull(columnOneBased); + return i != null && i; + } + + @Override + public boolean getBooleanOrFalse(String columnName) { + Boolean i = getBooleanOrNull(columnName); + return i != null && i; + } + + @Override + public boolean getBooleanOrTrue() { + Boolean i = getBooleanOrNull(); + return i == null || i; + } + + @Override + public boolean getBooleanOrTrue(int columnOneBased) { + col = columnOneBased; + Boolean i = getBooleanOrNull(columnOneBased); + return i == null || i; + } + + @Override + public boolean getBooleanOrTrue(String columnName) { + Boolean i = getBooleanOrNull(columnName); + return i == null || i; + } + + + @Override + public Integer getIntegerOrNull() { + return toInteger(rows.get(row)[++col]); + } + + + @Override + public Integer getIntegerOrNull(int columnOneBased) { + col = columnOneBased; + return toInteger(rows.get(row)[columnOneBased - 1]); + } + + + @Override + public Integer getIntegerOrNull(String columnName) { + col = columnIndexByName(columnName) + 1; + return toInteger(rows.get(row)[columnIndexByName(columnName)]); + } + + @Override + public int getIntegerOrZero() { + Integer i = getIntegerOrNull(); + return i == null ? 0 : i; + } + + @Override + public int getIntegerOrZero(int columnOneBased) { + Integer i = getIntegerOrNull(columnOneBased); + return i == null ? 0 : i; + } + + @Override + public int getIntegerOrZero(String columnName) { + Integer i = getIntegerOrNull(columnName); + return i == null ? 0 : i; + } + + + @Override + public Long getLongOrNull() { + return toLong(rows.get(row)[++col]); + } + + + @Override + public Long getLongOrNull(int columnOneBased) { + col = columnOneBased; + return toLong(rows.get(row)[columnOneBased - 1]); + } + + + @Override + public Long getLongOrNull(String columnName) { + col = columnIndexByName(columnName) + 1; + return toLong(rows.get(row)[columnIndexByName(columnName)]); + } + + @Override + public long getLongOrZero() { + Long i = getLongOrNull(); + return i == null ? 0 : i; + } + + @Override + public long getLongOrZero(int columnOneBased) { + Long i = getLongOrNull(columnOneBased); + return i == null ? 0 : i; + } + + @Override + public long getLongOrZero(String columnName) { + Long i = getLongOrNull(columnName); + return i == null ? 0 : i; + } + + + @Override + public Float getFloatOrNull() { + return toFloat(rows.get(row)[++col]); + } + + + @Override + public Float getFloatOrNull(int columnOneBased) { + col = columnOneBased; + return toFloat(rows.get(row)[columnOneBased - 1]); + } + + + @Override + public Float getFloatOrNull(String columnName) { + col = columnIndexByName(columnName) + 1; + return toFloat(rows.get(row)[columnIndexByName(columnName)]); + } + + @Override + public float getFloatOrZero() { + Float i = getFloatOrNull(); + return i == null ? 0 : i; + } + + @Override + public float getFloatOrZero(int columnOneBased) { + Float i = getFloatOrNull(columnOneBased); + return i == null ? 0 : i; + } + + @Override + public float getFloatOrZero(String columnName) { + Float i = getFloatOrNull(columnName); + return i == null ? 0 : i; + } + + + @Override + public Double getDoubleOrNull() { + return toDouble(rows.get(row)[++col]); + } + + + @Override + public Double getDoubleOrNull(int columnOneBased) { + col = columnOneBased; + return toDouble(rows.get(row)[columnOneBased - 1]); + } + + + @Override + public Double getDoubleOrNull(String columnName) { + col = columnIndexByName(columnName) + 1; + return toDouble(rows.get(row)[columnIndexByName(columnName)]); + } + + @Override + public double getDoubleOrZero() { + Double i = getDoubleOrNull(); + return i == null ? 0 : i; + } + + @Override + public double getDoubleOrZero(int columnOneBased) { + Double i = getDoubleOrNull(columnOneBased); + return i == null ? 0 : i; + } + + @Override + public double getDoubleOrZero(String columnName) { + Double i = getDoubleOrNull(columnName); + return i == null ? 0 : i; + } + + + @Override + public BigDecimal getBigDecimalOrNull() { + return toBigDecimal(rows.get(row)[++col]); + } + + + @Override + public BigDecimal getBigDecimalOrNull(int columnOneBased) { + col = columnOneBased; + return toBigDecimal(rows.get(row)[columnOneBased - 1]); + } + + + @Override + public BigDecimal getBigDecimalOrNull(String columnName) { + col = columnIndexByName(columnName) + 1; + return toBigDecimal(rows.get(row)[columnIndexByName(columnName)]); + } + + + @Override + public BigDecimal getBigDecimalOrZero() { + BigDecimal i = getBigDecimalOrNull(); + return i == null ? new BigDecimal(0) : i; + } + + + @Override + public BigDecimal getBigDecimalOrZero(int columnOneBased) { + BigDecimal i = getBigDecimalOrNull(columnOneBased); + return i == null ? new BigDecimal(0) : i; + } + + + @Override + public BigDecimal getBigDecimalOrZero(String columnName) { + BigDecimal i = getBigDecimalOrNull(columnName); + return i == null ? new BigDecimal(0) : i; + } + + + @Override + public String getStringOrNull() { + return toString(rows.get(row)[++col]); + } + + + @Override + public String getStringOrNull(int columnOneBased) { + col = columnOneBased; + return toString(rows.get(row)[columnOneBased - 1]); + } + + + @Override + public String getStringOrNull(String columnName) { + col = columnIndexByName(columnName) + 1; + return toString(rows.get(row)[columnIndexByName(columnName)]); + } + + + @Override + public String getStringOrEmpty() { + String i = getStringOrNull(); + return i == null ? "" : i; + } + + + @Override + public String getStringOrEmpty(int columnOneBased) { + String i = getStringOrNull(columnOneBased); + return i == null ? "" : i; + } + + + @Override + public String getStringOrEmpty(String columnName) { + String i = getStringOrNull(columnName); + return i == null ? "" : i; + } + + + @Override + public String getClobStringOrNull() { + return getStringOrNull(); + } + + + @Override + public String getClobStringOrNull(int columnOneBased) { + return getStringOrNull(columnOneBased); + } + + + @Override + public String getClobStringOrNull(String columnName) { + return getStringOrNull(columnName); + } + + + @Override + public String getClobStringOrEmpty() { + return getStringOrEmpty(); + } + + + @Override + public String getClobStringOrEmpty(int columnOneBased) { + return getStringOrEmpty(columnOneBased); + } + + + @Override + public String getClobStringOrEmpty(String columnName) { + return getStringOrEmpty(columnName); + } + + + @Override + public Reader getClobReaderOrNull() { + String s = getStringOrNull(); + return s == null ? null : new StringReader(s); + } + + + @Override + public Reader getClobReaderOrNull(int columnOneBased) { + String s = getStringOrNull(columnOneBased); + return s == null ? null : new StringReader(s); + } + + + @Override + public Reader getClobReaderOrNull(String columnName) { + String s = getStringOrNull(columnName); + return s == null ? null : new StringReader(s); + } + + + @Override + public Reader getClobReaderOrEmpty() { + return new StringReader(getStringOrEmpty()); + } + + + @Override + public Reader getClobReaderOrEmpty(int columnOneBased) { + return new StringReader(getStringOrEmpty(columnOneBased)); + } + + + @Override + public Reader getClobReaderOrEmpty(String columnName) { + return new StringReader(getStringOrEmpty(columnName)); + } + + + @Override + public byte[] getBlobBytesOrNull() { + return toBytes(rows.get(row)[++col]); + } + + + @Override + public byte[] getBlobBytesOrNull(int columnOneBased) { + col = columnOneBased; + return toBytes(rows.get(row)[columnOneBased - 1]); + } + + + @Override + public byte[] getBlobBytesOrNull(String columnName) { + col = columnIndexByName(columnName) + 1; + return toBytes(rows.get(row)[columnIndexByName(columnName)]); + } + + + @Override + public byte[] getBlobBytesOrZeroLen() { + byte[] a = getBlobBytesOrNull(); + return a == null ? new byte[0] : a; + } + + + @Override + public byte[] getBlobBytesOrZeroLen(int columnOneBased) { + byte[] a = getBlobBytesOrNull(columnOneBased); + return a == null ? new byte[0] : a; + } + + + @Override + public byte[] getBlobBytesOrZeroLen(String columnName) { + byte[] a = getBlobBytesOrNull(columnName); + return a == null ? new byte[0] : a; + } + + + @Override + public InputStream getBlobInputStreamOrNull() { + byte[] a = getBlobBytesOrNull(); + return a == null ? null : new ByteArrayInputStream(a); + } + + + @Override + public InputStream getBlobInputStreamOrNull(int columnOneBased) { + byte[] a = getBlobBytesOrNull(columnOneBased); + return a == null ? null : new ByteArrayInputStream(a); + } + + + @Override + public InputStream getBlobInputStreamOrNull(String columnName) { + byte[] a = getBlobBytesOrNull(columnName); + return a == null ? null : new ByteArrayInputStream(a); + } + + + @Override + public InputStream getBlobInputStreamOrEmpty() { + return new ByteArrayInputStream(getBlobBytesOrZeroLen()); + } + + + @Override + public InputStream getBlobInputStreamOrEmpty(int columnOneBased) { + return new ByteArrayInputStream(getBlobBytesOrZeroLen(columnOneBased)); + } + + + @Override + public InputStream getBlobInputStreamOrEmpty(String columnName) { + return new ByteArrayInputStream(getBlobBytesOrZeroLen(columnName)); + } + + + @Override + public Date getDateOrNull() { + return toDate(rows.get(row)[++col]); + } + + + @Override + public Date getDateOrNull(int columnOneBased) { + col = columnOneBased; + return toDate(rows.get(row)[columnOneBased - 1]); + } + + + @Override + public Date getDateOrNull(String columnName) { + col = columnIndexByName(columnName) + 1; + return toDate(rows.get(row)[columnIndexByName(columnName)]); + } + + /** + * Returns a java.time.LocalDate. It will have no timezone or other time data. + * If you require time, use the Date APIs instead. + */ + + @Override + public LocalDate getLocalDateOrNull() { + return toLocalDate(rows.get(row)[++col]); + } + + /** + * Returns a java.time.LocalDate. It will have no timezone or other time data. + * If you require time, use the Date APIs instead. + */ + + @Override + public LocalDate getLocalDateOrNull(int columnOneBased) { + col = columnOneBased; + return toLocalDate(rows.get(row)[columnOneBased - 1]); + } + + /** + * Returns a java.time.LocalDate. It will have no timezone or other time data. + * If you require time, use the Date APIs instead. + */ + + @Override + public LocalDate getLocalDateOrNull(String columnName) { + col = columnIndexByName(columnName) + 1; + return toLocalDate(rows.get(row)[columnIndexByName(columnName)]); + } + + private void requireColumnNames() { + if (columnNames == null) { + throw new DatabaseException("Column names were not provided for this stub"); + } + } + + private int columnIndexByName(String columnName) { + requireColumnNames(); + for (int i = 0; i < columnNames.length; i++) { + if (columnName.equals(columnNames[i])) { + return i; + } + } + throw new DatabaseException("Column name '" + columnName + "' not found"); + } + + private Boolean toBoolean(Object o) { + if (o instanceof String) { + if ("Y".equals(o)) { + return Boolean.TRUE; + } else if ("N".equals(o)) { + return Boolean.FALSE; + } else { + throw new DatabaseException("Value returned for boolean was not 'Y' or 'N'"); + } + } + return (Boolean) o; + } + + private Integer toInteger(Object o) { + return (Integer) o; + } + + private Long toLong(Object o) { + if (o instanceof Integer) { + return ((Integer) o).longValue(); + } + return (Long) o; + } + + private Float toFloat(Object o) { + if (o instanceof Integer) { + return ((Integer) o).floatValue(); + } + return (Float) o; + } + + private Double toDouble(Object o) { + if (o instanceof Integer) { + return ((Integer) o).doubleValue(); + } + if (o instanceof Float) { + return ((Float) o).doubleValue(); + } + return (Double) o; + } + + private BigDecimal toBigDecimal(Object o) { + if (o instanceof Integer) { + return BigDecimal.valueOf(((Integer) o).longValue()); + } + if (o instanceof Long) { + return BigDecimal.valueOf((Long) o); + } + if (o instanceof Float) { + return BigDecimal.valueOf(((Float) o).doubleValue()); + } + if (o instanceof Double) { + return BigDecimal.valueOf((Double) o); + } + return (BigDecimal) o; + } + + private byte[] toBytes(Object o) { + return (byte[]) o; + } + + private String toString(Object o) { + return (String) o; + } + + /** + * Returns a java.util.Date. It may be used for dates or times. + */ + private Date toDate(Object o) { + if (o instanceof String) { + String s = (String) o; + if (s.length() == "yyyy-MM-dd".length()) { + try { + return new SimpleDateFormat("yyyy-MM-dd").parse(s); + } catch (ParseException e) { + throw new DatabaseException("Could not parse date as yyyy-MM-dd for " + s); + } + } + if (s.length() == "yyyy-MM-ddThh:mm:ss".length()) { + try { + return new SimpleDateFormat("yyyy-MM-ddThh:mm:ss").parse(s); + } catch (ParseException e) { + throw new DatabaseException("Could not parse date as yyyy-MM-ddThh:mm:ss for " + s); + } + } + throw new DatabaseException("Didn't understand date string: " + s); + } + return (Date) o; + } + + /** + * Returns a LocalDate (no time). + * If the object is a String, it should be in ISO 8601 format. + * + * @return a LocalDate representation of the object + */ + private LocalDate toLocalDate(Object o) { + if (o instanceof String) { + return LocalDate.parse((String) o); + } + + return (LocalDate) o; + } + }; + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlArgsTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlArgsTest.java new file mode 100644 index 0000000..6f2e31a --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlArgsTest.java @@ -0,0 +1,24 @@ +package org.xbib.jdbc.query.test; + +import org.junit.jupiter.api.Test; +import org.xbib.jdbc.query.SqlArgs; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +/** + * Unit tests for the SqlArgs class. + */ +public class SqlArgsTest { + + @Test + public void testTidyColumnNames() { + assertArrayEquals(new String[]{"column_1", "column_2", "a", "a_2", "a_3", "a1"}, + SqlArgs.tidyColumnNames(new String[]{null, "", " a ", "a ", "#!@#$_a", "#!@#$_1"})); + + check("TheBest", "the_best"); + } + + private void check(String input, String output) { + assertArrayEquals(new String[]{output}, SqlArgs.tidyColumnNames(new String[]{input})); + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlServerTest.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlServerTest.java new file mode 100644 index 0000000..bb77373 --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/SqlServerTest.java @@ -0,0 +1,86 @@ +package org.xbib.jdbc.query.test; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.xbib.jdbc.query.Config; +import org.xbib.jdbc.query.ConfigFrom; +import org.xbib.jdbc.query.DatabaseProvider; +import org.xbib.jdbc.query.OptionsOverride; +import org.xbib.jdbc.query.Schema; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +/** + * Exercise Database functionality with a real Oracle database. + */ +public class SqlServerTest extends CommonTest { + @Override + protected DatabaseProvider createDatabaseProvider(OptionsOverride options) throws Exception { + String propertiesFile = System.getProperty("local.properties", "local.properties"); + Config config = ConfigFrom.firstOf() + .systemProperties() + .propertyFile(propertiesFile) + .excludePrefix("database.") + .removePrefix("sqlserver.").get(); + return DatabaseProvider.fromDriverManager(config) + .withSqlParameterLogging() + .withSqlInExceptionMessages() + .withOptions(options).create(); + } + + @Disabled("SQL Server prohibits NaN and Infinity") + @Test + public void argFloatNaN() { + super.argFloatNaN(); + } + + @Disabled("SQL Server prohibits NaN and Infinity") + @Test + public void argFloatInfinity() { + super.argFloatInfinity(); + } + + @Disabled("SQL Server prohibits NaN and Infinity") + @Test + public void argDoubleNaN() { + super.argDoubleNaN(); + } + + @Disabled("SQL Server prohibits NaN and Infinity") + @Test + public void argDoubleInfinity() { + super.argDoubleInfinity(); + } + + @Disabled("SQL Server seems to have incorrect min value for float (rounds to zero)") + @Test + public void argFloatMinMax() { + super.argFloatMinMax(); + } + + @Disabled("SQL Server doesn't support the interval syntax for date arithmetic") + @Test + public void intervals() { + super.intervals(); + } + + /** + * SQL Server seems to have different behavior in that is does not convert + * column names to uppercase (it preserves the case). + * I haven't figured out how to smooth over this difference, since all databases + * seem to respect the provided case when it is inside quotes, but don't provide + * a way to tell whether a particular parameter was quoted. + */ + @Override + @Test + public void metadataColumnNames() { + db.dropTableQuietly("dbtest"); + + new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db); + + db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\" from dbtest").query(rs -> { + assertArrayEquals(new String[]{"Pk", "Foo", "Foo"}, rs.getColumnLabels()); + return null; + }); + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DerbyExample.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DerbyExample.java new file mode 100644 index 0000000..cfc3eb6 --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DerbyExample.java @@ -0,0 +1,35 @@ +package org.xbib.jdbc.query.test.example; + +import org.xbib.jdbc.query.Database; +import org.xbib.jdbc.query.DatabaseProvider; + +/** + * Demo of using some com.github.susom.database classes with Derby. + */ +public abstract class DerbyExample { + + void example(Database db, String[] args) { + // For subclasses to override + } + + void example(DatabaseProvider.Builder dbb, final String[] args) { + dbb.transact(db -> { + example(db.get(), args); + }); + } + + public void println(String s) { + System.out.println(s); + } + + public final void launch(final String[] args) { + try { + System.setProperty("derby.stream.error.file", "java.lang.System.err"); + String url = "jdbc:derby:target/testdb;create=true"; + example(DatabaseProvider.fromDriverManager(url), args); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DynamicSql.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DynamicSql.java new file mode 100644 index 0000000..c1b3125 --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/DynamicSql.java @@ -0,0 +1,52 @@ +package org.xbib.jdbc.query.test.example; + +import org.xbib.jdbc.query.Database; +import org.xbib.jdbc.query.Schema; +import org.xbib.jdbc.query.Sql; + +/** + * Demo of how to use the Sql helper class to dynamically build queries. + */ +public class DynamicSql extends DerbyExample { + public static void main(String[] args) { + new DynamicSql().launch(args); + } + + void example(Database db, String[] args) { + // Drops in case we are running this multiple times + db.dropTableQuietly("t"); + + // Create and populate a simple table + new Schema() + .addTable("t") + .addColumn("pk").primaryKey().table() + .addColumn("s").asString(80).schema().execute(db); + db.toInsert("insert into t (pk,s) values (?,?)") + .argLong(1L).argString("Hi").insert(1); + db.toInsert("insert into t (pk,s) values (?,?)") + .argLong(2L).argString("Hi").insert(1); + + // Construct various dynamic queries and execute them + println("Rows with none: " + countByPkOrS(db, null, null)); + println("Rows with pk=1: " + countByPkOrS(db, 1L, null)); + println("Rows with s=Hi: " + countByPkOrS(db, null, "Hi")); + } + + Long countByPkOrS(Database db, Long pk, String s) { + Sql sql = new Sql("select count(*) from t"); + boolean where = true; + if (pk != null) { + where = false; + sql.append(" where pk=?").argLong(pk); + } + if (s != null) { + if (where) { + sql.append(" where "); + } else { + sql.append(" and "); + } + sql.append("s=?").argString(s); + } + return db.toSelect(sql).queryLongOrNull(); + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/FakeBuilder.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/FakeBuilder.java new file mode 100644 index 0000000..0940134 --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/FakeBuilder.java @@ -0,0 +1,65 @@ +package org.xbib.jdbc.query.test.example; + +import org.xbib.jdbc.query.DatabaseException; +import org.xbib.jdbc.query.DatabaseProvider; +import org.xbib.jdbc.query.Schema; + +/** + * Demo of how to use the {@code DatabaseProvider.fakeBuilder()} to control + * transactions for testing purposes. + */ +public class FakeBuilder extends DerbyExample { + public static void main(String[] args) { + new FakeBuilder().launch(args); + } + + void example(DatabaseProvider.Builder dbb, String[] args) { + DatabaseProvider realDbp = null; + + try { + realDbp = dbb.create(); + + dbb.transact(db -> { + // Drops in case we are running this multiple times + db.get().dropTableQuietly("t"); + + // Create and populate a simple table + new Schema().addTable("t").addColumn("pk").primaryKey().schema().execute(db.get()); + }); + + DatabaseProvider.Builder fakeBuilder = realDbp.fakeBuilder(); + + // Trying all three transact methods, just for completeness + fakeBuilder.transact(db -> { + db.get().toInsert("insert into t (pk) values (?)").argLong(1L).insert(1); + }); + fakeBuilder.transact((db, tx) -> { + db.get().toInsert("insert into t (pk) values (?)").argLong(2L).insert(1); + }); + + fakeBuilder.transact(db -> { + println("Rows before rollback: " + db.get().toSelect("select count(*) from t").queryLongOrZero()); + }); + + realDbp.rollbackAndClose(); + + // Can't use fakeBuilder after close + try { + fakeBuilder.transact(db -> { + db.get().tableExists("foo"); + println("Eeek...shouldn't get here!"); + }); + } catch (DatabaseException e) { + println("Correctly threw exception: " + e.getMessage()); + } + + dbb.transact(db -> { + println("Rows after rollback: " + db.get().toSelect("select count(*) from t").queryLongOrZero()); + }); + } finally { + if (realDbp != null) { + realDbp.rollbackAndClose(); + } + } + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloAny.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloAny.java new file mode 100644 index 0000000..b9144df --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloAny.java @@ -0,0 +1,47 @@ +package org.xbib.jdbc.query.test.example; + +import org.xbib.jdbc.query.Database; +import org.xbib.jdbc.query.DatabaseProvider; + +/** + * Example with database info provided from command line. To use this, set properties like this: + *
+ *
+ *   -Ddatabase.url=...      Database connect string (required)
+ *   -Ddatabase.user=...     Authenticate as this user (optional if provided in url)
+ *   -Ddatabase.password=... User password (optional if user and password provided in
+ *                           url; prompted on standard input if user is provided and
+ *                           password is not)
+ *   -Ddatabase.flavor=...   What kind of database it is (optional, will guess based
+ *                           on the url if this is not provided)
+ *   -Ddatabase.driver=...   The Java class of the JDBC driver to load (optional, will
+ *                           guess based on the flavor if this is not provided)
+ * 
+ */ +public class HelloAny { + public static void main(final String[] args) { + try { + new HelloAny().run(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } + + public void run() { + DatabaseProvider.fromSystemProperties().transact(dbp -> { + Database db = dbp.get(); + db.dropTableQuietly("t"); + db.ddl("create table t (a numeric)").execute(); + db.toInsert("insert into t (a) values (?)") + .argInteger(32) + .insert(1); + db.toUpdate("update t set a=:val") + .argInteger("val", 23) + .update(1); + + Long rows = db.toSelect("select count(1) from t ").queryLongOrNull(); + System.out.println("Rows: " + rows); + }); + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloDerby.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloDerby.java new file mode 100644 index 0000000..13678de --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/HelloDerby.java @@ -0,0 +1,46 @@ +package org.xbib.jdbc.query.test.example; + +import org.xbib.jdbc.query.Database; +import org.xbib.jdbc.query.DatabaseProvider; + +import java.io.File; + +/** + * Demo of using some com.github.susom.database classes with Derby. + */ +public class HelloDerby { + public static void main(final String[] args) { + try { + new HelloDerby().run(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } + + public void run() { + // Put all Derby related files inside ./build to keep our working copy clean + File directory = new File("target").getAbsoluteFile(); + if (directory.exists() || directory.mkdirs()) { + System.setProperty("derby.stream.error.file", new File(directory, "derby.log").getAbsolutePath()); + } + + String url = "jdbc:derby:target/testdb;create=true"; + DatabaseProvider.fromDriverManager(url).transact(dbp -> { + Database db = dbp.get(); + db.ddl("drop table t").executeQuietly(); + db.ddl("create table t (a numeric)").execute(); + db.toInsert("insert into t (a) values (?)").argInteger(32).insert(1); + db.toUpdate("update t set a=:val") + .argInteger("val", 23) + .update(1); + + Long rows = db.toSelect("select count(1) from t ").queryLongOrNull(); + println("Rows: " + rows); + }); + } + + public void println(String s) { + System.out.println(s); + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/InsertReturning.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/InsertReturning.java new file mode 100644 index 0000000..7d174be --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/InsertReturning.java @@ -0,0 +1,42 @@ +package org.xbib.jdbc.query.test.example; + +import org.xbib.jdbc.query.Database; +import org.xbib.jdbc.query.Schema; + +/** + * Demo of using some com.github.susom.database classes with Derby. + */ +public class InsertReturning extends DerbyExample { + + public static void main(String[] args) { + new InsertReturning().launch(args); + } + + void example(Database db, String[] args) { + // Drops in case we are running this multiple times + db.dropTableQuietly("t"); + db.dropSequenceQuietly("pk_seq"); + + // Create a table and a sequence + new Schema() + .addTable("t") + .addColumn("pk").primaryKey().table() + .addColumn("d").asDate().table() + .addColumn("s").asString(80).schema() + .addSequence("pk_seq").schema().execute(db); + + // Insert a row into the table, populating the primary key from a sequence, + // and the date based on current database time. Observe that this will work + // on Derby, where it results in a query for the sequence value, followed by + // the insert. On databases like Oracle, this will be optimized into a single + // statement that does the insert and also returns the primary key. + Long pk = db.toInsert( + "insert into t (pk,d,s) values (?,?,?)") + .argPkSeq("pk_seq") + .argDateNowPerDb() + .argString("Hi") + .insertReturningPkSeq("pk"); + + println("Inserted row with pk=" + pk); + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/Sample.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/Sample.java new file mode 100644 index 0000000..4c2d887 --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/Sample.java @@ -0,0 +1,49 @@ +package org.xbib.jdbc.query.test.example; + +import java.util.Date; + +/** + * Simple bean for use with SampleDao. + */ +public class Sample { + + private Long sampleId; + + private String name; + + private Integer updateSequence; + + private Date updateTime; + + public Long getSampleId() { + return sampleId; + } + + public void setSampleId(Long sampleId) { + this.sampleId = sampleId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getUpdateSequence() { + return updateSequence; + } + + public void setUpdateSequence(Integer updateSequence) { + this.updateSequence = updateSequence; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } +} diff --git a/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/SampleDao.java b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/SampleDao.java new file mode 100644 index 0000000..eb8b573 --- /dev/null +++ b/jdbc-query/src/test/java/org/xbib/jdbc/query/test/example/SampleDao.java @@ -0,0 +1,109 @@ +package org.xbib.jdbc.query.test.example; + +import org.xbib.jdbc.query.Database; + +import java.util.Date; +import java.util.function.Supplier; + +/** + * Create, read, update, and delete sample database objects. + */ +public class SampleDao { + + private final Supplier dbp; + + public SampleDao(Supplier dbp) { + this.dbp = dbp; + } + + public void createSample(final Sample sample, Long userIdMakingChange) { + Database db = dbp.get(); + + Date updateTime = db.nowPerApp(); + Long sampleId = db.toInsert( + "insert into sample (sample_id, sample_name, update_sequence, update_time) values (?,?,0,?)") + .argPkSeq("id_seq") + .argString(sample.getName()) + .argDate(updateTime) + .insertReturningPkSeq("sample_id"); + + db.toInsert("insert into sample_history (sample_id, sample_name, update_sequence, update_time, update_user_id," + + " is_deleted) values (?,?,0,?,?,'N')") + .argLong(sampleId) + .argString(sample.getName()) + .argDate(updateTime) + .argLong(userIdMakingChange) + .insert(1); + + // Update the object in memory + sample.setSampleId(sampleId); + sample.setUpdateSequence(0); + sample.setUpdateTime(updateTime); + } + + public Sample findSampleById(final Long sampleId, boolean lockRow) { + return dbp.get().toSelect("select sample_name, update_sequence, update_time from sample where sample_id=?" + + (lockRow ? " for update" : "")) + .argLong(sampleId).queryOneOrNull(r -> { + Sample result = new Sample(); + result.setSampleId(sampleId); + result.setName(r.getStringOrNull()); + result.setUpdateSequence(r.getIntegerOrNull()); + result.setUpdateTime(r.getDateOrNull()); + return result; + }); + } + + public void updateSample(Sample sample, Long userIdMakingChange) { + Database db = dbp.get(); + + // Insert the history row first, so it will fail (non-unique sample_id + update_sequence) + // if someone else modified the row. This is an optimistic locking strategy. + int newUpdateSequence = sample.getUpdateSequence() + 1; + Date newUpdateTime = db.nowPerApp(); + db.toInsert("insert into sample_history (sample_id, sample_name, update_sequence, update_time, update_user_id," + + " is_deleted) values (?,?,?,?,?,'N')") + .argLong(sample.getSampleId()) + .argString(sample.getName()) + .argInteger(newUpdateSequence) + .argDate(newUpdateTime) + .argLong(userIdMakingChange) + .insert(1); + + db.toUpdate("update sample set sample_name=?, update_sequence=?, update_time=? where sample_id=?") + .argString(sample.getName()) + .argInteger(newUpdateSequence) + .argDate(newUpdateTime) + .argLong(sample.getSampleId()) + .update(1); + + // Make sure the object in memory matches the database. + sample.setUpdateSequence(newUpdateSequence); + sample.setUpdateTime(newUpdateTime); + } + + public void deleteSample(Sample sample, Long userIdMakingChange) { + Database db = dbp.get(); + + // Insert the history row first, so it will fail (non-unique sample_id + update_sequence) + // if someone else modified the row. This is an optimistic locking strategy. + int newUpdateSequence = sample.getUpdateSequence() + 1; + Date newUpdateTime = db.nowPerApp(); + db.toInsert("insert into sample_history (sample_id, sample_name, update_sequence, update_time, update_user_id," + + " is_deleted) values (?,?,?,?,?,'Y')") + .argLong(sample.getSampleId()) + .argString(sample.getName()) + .argInteger(newUpdateSequence) + .argDate(newUpdateTime) + .argLong(userIdMakingChange) + .insert(1); + + db.toDelete("delete from sample where sample_id=?") + .argLong(sample.getSampleId()) + .update(1); + + // Make sure the object in memory matches the database. + sample.setUpdateSequence(newUpdateSequence); + sample.setUpdateTime(newUpdateTime); + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..c48c38f --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +include 'jdbc-connection-pool' +include 'jdbc-query'