From e5e95ddfd6ceed7bce757a5b2521c61208e59700 Mon Sep 17 00:00:00 2001 From: rusher Date: Wed, 9 Dec 2020 15:10:31 +0100 Subject: [PATCH 001/113] [misc] initial release for 3.0 version --- .travis.yml | 43 +- .travis/build/Dockerfile | 99 - .travis/build/build.sh | 33 - .travis/build/docker-entrypoint.sh | 196 -- .travis/galera-compose.yml | 50 - .travis/gen-ssl.sh | 148 +- .travis/maxscale-compose.yml | 34 +- .travis/maxscale/Dockerfile | 6 +- .travis/maxscale/maxscale.cnf | 118 +- .travis/script.sh | 261 +- .travis/sql/dbinit.sql | 4 +- CHANGELOG.md | 12 +- README.md | 5 + appveyor-download.bat | 5 +- appveyor.yml | 18 +- codecov.yml | 4 +- .../about-mariadb-connector-j.creole | 96 - documentation/developers-guide.creole | 89 - ...ailability-with-mariadb-connector-j.creole | 265 -- documentation/failover_loop.creole | 45 - .../misc/images/aurora_fail_extract.png | Bin 41235 -> 0 bytes documentation/misc/images/bulk_batch.png | Bin 20010 -> 0 bytes documentation/misc/images/standard.png | Bin 4986 -> 0 bytes documentation/misc/images/standard_batch.png | Bin 21624 -> 0 bytes documentation/misc/images/telemetry.png | Bin 770670 -> 0 bytes .../misc/images/use_batch_multi_send.png | Bin 8592 -> 0 bytes documentation/plugin/GSSAPI.creole | 131 - .../use-batch-multi-send-description.creole | 51 - .../use-mariadb-connector-j-driver.creole | 472 --- maven-build.xml | 86 +- pom.xml | 265 +- src/benchmark/README.md | 32 + .../java/org/mariadb/jdbc/Common.java | 98 + .../java/org/mariadb/jdbc/Select_1.java | 40 + .../org/mariadb/jdbc/Select_10000_Rows.java | 58 + .../org/mariadb/jdbc/Select_1000_params.java | 74 + .../java/org/mariadb/jdbc/Select_1_user.java | 55 + src/benchmark/resources/logback-test.xml | 34 + .../mariadb/jdbc/BaseCallableStatement.java | 2796 +++++++++++++++++ ...tement.java => BasePreparedStatement.java} | 1930 ++++++------ .../org/mariadb/jdbc/BlobOutputStream.java | 107 - .../java/org/mariadb/jdbc/CallParameter.java | 164 - .../jdbc/CallableFunctionStatement.java | 881 ------ .../jdbc/CallableParameterMetaData.java | 559 ++-- .../jdbc/CallableProcedureStatement.java | 904 ------ .../mariadb/jdbc/ClientPreparedStatement.java | 395 +++ .../jdbc/ClientSidePreparedStatement.java | 567 ---- .../java/org/mariadb/jdbc/Configuration.java | 1467 +++++++++ .../java/org/mariadb/jdbc/Connection.java | 840 +++++ ...aseMetaData.java => DatabaseMetaData.java} | 521 ++- src/main/java/org/mariadb/jdbc/Driver.java | 135 +- .../org/mariadb/jdbc/FunctionStatement.java | 77 + .../java/org/mariadb/jdbc/HostAddress.java | 229 +- .../mariadb/jdbc/LocalInfileInterceptor.java | 58 - .../java/org/mariadb/jdbc/MariaDbBlob.java | 144 +- .../java/org/mariadb/jdbc/MariaDbClob.java | 66 +- .../org/mariadb/jdbc/MariaDbConnection.java | 1755 ----------- .../org/mariadb/jdbc/MariaDbDataSource.java | 508 +-- .../jdbc/MariaDbFunctionStatement.java | 184 -- .../jdbc/MariaDbParameterMetaData.java | 168 - .../mariadb/jdbc/MariaDbPoolDataSource.java | 675 ---- .../mariadb/jdbc/MariaDbPooledConnection.java | 245 -- .../jdbc/MariaDbProcedureStatement.java | 198 -- .../org/mariadb/jdbc/MariaDbSavepoint.java | 90 - .../org/mariadb/jdbc/MariaDbStatement.java | 1547 --------- .../java/org/mariadb/jdbc/MariaDbXid.java | 104 - .../org/mariadb/jdbc/MariaXaConnection.java | 68 - .../org/mariadb/jdbc/MariaXaResource.java | 336 -- .../org/mariadb/jdbc/MySQLDataSource.java | 81 - .../org/mariadb/jdbc/ParameterMetaData.java | 202 ++ .../org/mariadb/jdbc/ProcedureStatement.java | 87 + .../mariadb/jdbc/ServerPreparedStatement.java | 659 ++++ .../jdbc/ServerSidePreparedStatement.java | 491 --- .../mariadb/jdbc/SimpleParameterMetaData.java | 187 -- src/main/java/org/mariadb/jdbc/SslMode.java | 56 + src/main/java/org/mariadb/jdbc/Statement.java | 1417 +++++++++ src/main/java/org/mariadb/jdbc/UrlParser.java | 530 ---- .../java/org/mariadb/jdbc/client/Client.java | 95 + .../org/mariadb/jdbc/client/ClientImpl.java | 775 +++++ .../mariadb/jdbc/client/ConnectionHelper.java | 397 +++ .../jdbc/client/MultiPrimaryClient.java | 443 +++ .../client/MultiPrimaryReplicaClient.java | 335 ++ .../org/mariadb/jdbc/client/PacketReader.java | 270 ++ .../PacketWriter.java} | 833 +++-- .../org/mariadb/jdbc/client/PrepareCache.java | 71 + .../ReadAheadBufferedStream.java | 44 +- .../mariadb/jdbc/client/ReadableByteBuf.java | 288 ++ .../mariadb/jdbc/client/ServerVersion.java | 164 + .../mariadb/jdbc/client/TransactionSaver.java | 32 + .../jdbc/client/context/BaseContext.java | 156 + .../mariadb/jdbc/client/context/Context.java | 76 + .../jdbc/client/context/RedoContext.java | 57 + .../jdbc/client/result/CompleteResult.java | 299 ++ .../mariadb/jdbc/client/result/Result.java | 1254 ++++++++ .../result/ResultSetMetaData.java} | 148 +- .../jdbc/client/result/StreamingResult.java | 373 +++ .../jdbc/client/result/UpdatableResult.java | 1136 +++++++ .../io => client}/socket/NamedPipeSocket.java | 33 +- .../client/socket/SocketHandlerFunction.java | 32 + .../jdbc/client/socket/SocketUtility.java | 60 + .../socket/UnixDomainSocket.java | 33 +- .../tls/DefaultTlsSocketPlugin.java | 98 +- .../tls/HostnameVerifierImpl.java | 71 +- .../tls/MariaDbX509KeyManager.java | 33 +- .../client/tls/MariaDbX509TrustManager.java | 175 ++ .../mariadb/jdbc/codec/BinaryRowDecoder.java | 213 ++ .../java/org/mariadb/jdbc/codec/Codec.java | 77 + .../java/org/mariadb/jdbc/codec/Codecs.java | 56 + .../java/org/mariadb/jdbc/codec/DataType.java | 76 + .../org/mariadb/jdbc/codec/Parameter.java | 114 + .../org/mariadb/jdbc/codec/RowDecoder.java | 196 ++ .../mariadb/jdbc/codec/TextRowDecoder.java | 128 + .../jdbc/codec/list/BigDecimalCodec.java | 218 ++ .../jdbc/codec/list/BigIntegerCodec.java | 213 ++ .../mariadb/jdbc/codec/list/BitSetCodec.java | 109 + .../mariadb/jdbc/codec/list/BlobCodec.java | 252 ++ .../mariadb/jdbc/codec/list/BooleanCodec.java | 164 + .../jdbc/codec/list/ByteArrayCodec.java | 164 + .../mariadb/jdbc/codec/list/ByteCodec.java | 259 ++ .../mariadb/jdbc/codec/list/ClobCodec.java | 178 ++ .../mariadb/jdbc/codec/list/DateCodec.java | 221 ++ .../mariadb/jdbc/codec/list/DoubleCodec.java | 188 ++ .../jdbc/codec/list/DurationCodec.java | 201 ++ .../mariadb/jdbc/codec/list/FloatCodec.java | 187 ++ .../org/mariadb/jdbc/codec/list/IntCodec.java | 242 ++ .../jdbc/codec/list/LocalDateCodec.java | 234 ++ .../jdbc/codec/list/LocalDateTimeCodec.java | 268 ++ .../jdbc/codec/list/LocalTimeCodec.java | 286 ++ .../mariadb/jdbc/codec/list/LongCodec.java | 257 ++ .../mariadb/jdbc/codec/list/ReaderCodec.java | 198 ++ .../mariadb/jdbc/codec/list/ShortCodec.java | 220 ++ .../mariadb/jdbc/codec/list/StreamCodec.java | 203 ++ .../mariadb/jdbc/codec/list/StringCodec.java | 291 ++ .../mariadb/jdbc/codec/list/TimeCodec.java | 245 ++ .../jdbc/codec/list/TimestampCodec.java | 323 ++ .../mariadb/jdbc/codec/list/TinyIntCodec.java | 207 ++ .../jdbc/codec/list/ZonedDateTimeCodec.java | 128 + .../org/mariadb/jdbc/internal/ColumnType.java | 369 --- .../org/mariadb/jdbc/internal/com/Packet.java | 86 - .../jdbc/internal/com/read/Buffer.java | 458 --- .../jdbc/internal/com/read/ErrorPacket.java | 96 - .../com/read/ReadInitialHandShakePacket.java | 211 -- .../internal/com/read/dao/CmdInformation.java | 91 - .../com/read/dao/CmdInformationBatch.java | 280 -- .../com/read/dao/CmdInformationMultiple.java | 259 -- .../com/read/dao/CmdInformationSingle.java | 170 - .../com/read/dao/ColumnLabelIndexer.java | 137 - .../jdbc/internal/com/read/dao/Results.java | 474 --- .../com/read/resultset/ColumnDefinition.java | 344 -- .../com/read/resultset/SelectResultSet.java | 2033 ------------ .../resultset/UpdatableColumnDefinition.java | 107 - .../read/resultset/UpdatableResultSet.java | 1431 --------- .../rowprotocol/BinaryRowProtocol.java | 1794 ----------- .../resultset/rowprotocol/RowProtocol.java | 327 -- .../rowprotocol/TextRowProtocol.java | 1159 ------- .../jdbc/internal/com/send/ComQuery.java | 365 --- .../internal/com/send/ComStmtExecute.java | 170 - .../internal/com/send/ComStmtPrepare.java | 183 -- .../internal/com/send/SendChangeDbPacket.java | 75 - .../internal/com/send/SendClosePacket.java | 75 - .../com/send/SendHandshakeResponsePacket.java | 241 -- .../send/SendSslConnectionRequestPacket.java | 80 - .../authentication/ClearPasswordPlugin.java | 120 - .../authentication/NativePasswordPlugin.java | 133 - .../authentication/OldPasswordPlugin.java | 169 - .../authentication/SendGssApiAuthPacket.java | 139 - .../authentication/SendPamAuthPacket.java | 147 - .../authentication/gssapi/GssUtility.java | 82 - .../authentication/gssapi/GssapiAuth.java | 70 - .../WindowsNativeSspiAuthentication.java | 111 - .../send/parameters/BigDecimalParameter.java | 104 - .../com/send/parameters/BooleanParameter.java | 71 - .../send/parameters/ByteArrayParameter.java | 117 - .../com/send/parameters/ByteParameter.java | 108 - .../com/send/parameters/DateParameter.java | 144 - .../com/send/parameters/DefaultParameter.java | 103 - .../com/send/parameters/DoubleParameter.java | 101 - .../com/send/parameters/FloatParameter.java | 101 - .../com/send/parameters/IntParameter.java | 101 - .../send/parameters/LocalTimeParameter.java | 122 - .../com/send/parameters/LongParameter.java | 100 - .../com/send/parameters/NullParameter.java | 105 - .../send/parameters/OffsetTimeParameter.java | 153 - .../com/send/parameters/ParameterHolder.java | 83 - .../com/send/parameters/ReaderParameter.java | 138 - .../parameters/SerializableParameter.java | 136 - .../com/send/parameters/ShortParameter.java | 101 - .../com/send/parameters/StreamParameter.java | 138 - .../com/send/parameters/StringParameter.java | 115 - .../com/send/parameters/TimeParameter.java | 156 - .../send/parameters/TimestampParameter.java | 151 - .../parameters/ZonedDateTimeParameter.java | 143 - .../failover/AbstractMastersListener.java | 633 ---- .../AbstractMastersSlavesListener.java | 208 -- .../jdbc/internal/failover/FailoverProxy.java | 431 --- .../internal/failover/HandleErrorResult.java | 84 - .../jdbc/internal/failover/Listener.java | 174 - .../failover/impl/AuroraListener.java | 499 --- .../impl/MastersFailoverListener.java | 357 --- .../failover/impl/MastersSlavesListener.java | 1128 ------- .../failover/thread/ConnectionValidator.java | 158 - .../failover/thread/FailoverLoop.java | 98 - .../failover/thread/TerminableRunnable.java | 120 - .../internal/failover/tools/SearchFilter.java | 119 - .../jdbc/internal/io/LruTraceCache.java | 148 - .../mariadb/jdbc/internal/io/TraceObject.java | 104 - .../io/input/DecompressPacketInputStream.java | 327 -- .../internal/io/input/PacketInputStream.java | 74 - .../io/input/StandardPacketInputStream.java | 402 --- .../io/output/CompressPacketOutputStream.java | 477 --- .../io/output/PacketOutputStream.java | 136 - .../io/output/StandardPacketOutputStream.java | 167 - .../io/socket/SharedMemorySocket.java | 432 --- .../io/socket/SocketHandlerFunction.java | 63 - .../internal/io/socket/SocketUtility.java | 97 - .../mariadb/jdbc/internal/logging/Logger.java | 116 - .../jdbc/internal/logging/LoggerFactory.java | 95 - .../jdbc/internal/logging/NoLogger.java | 176 -- .../logging/ProtocolLoggingProxy.java | 264 -- .../jdbc/internal/logging/Slf4JLogger.java | 182 -- .../jdbc/internal/osgi/MariaDbActivator.java | 97 - .../osgi/MariaDbDataSourceFactory.java | 198 -- .../protocol/AbstractConnectProtocol.java | 1632 ---------- .../internal/protocol/AbstractMultiSend.java | 386 --- .../protocol/AbstractQueryProtocol.java | 2075 ------------ .../internal/protocol/AsyncMultiRead.java | 187 -- .../protocol/AsyncMultiReadResult.java | 83 - .../internal/protocol/AuroraProtocol.java | 436 --- .../internal/protocol/MasterProtocol.java | 197 -- .../protocol/MastersSlavesProtocol.java | 279 -- .../jdbc/internal/protocol/Protocol.java | 313 -- .../protocol/tls/MariaDbX509TrustManager.java | 245 -- .../jdbc/internal/util/BulkStatus.java | 60 - .../internal/util/CallableStatementCache.java | 79 - .../jdbc/internal/util/LogQueryTool.java | 94 - .../jdbc/internal/util/OptionUtils.java | 45 - .../util/ServerPrepareStatementCache.java | 135 - .../mariadb/jdbc/internal/util/SqlStates.java | 84 - .../org/mariadb/jdbc/internal/util/Utils.java | 1058 ------- .../internal/util/constant/ColumnFlags.java | 69 - .../jdbc/internal/util/constant/HaMode.java | 61 - .../util/constant/ParameterConstant.java | 59 - .../internal/util/constant/ServerStatus.java | 70 - .../internal/util/constant/StateChange.java | 63 - .../util/dao/CallableStatementCacheKey.java | 83 - .../util/dao/CloneableCallableStatement.java | 61 - .../jdbc/internal/util/dao/Identifier.java | 74 - .../jdbc/internal/util/dao/PrepareResult.java | 60 - .../ReconnectDuringTransactionException.java | 62 - .../util/dao/ServerPrepareResult.java | 190 -- .../util/exceptions/ExceptionFactory.java | 208 -- .../exceptions/MaxAllowedPacketException.java | 69 - .../jdbc/internal/util/pid/JnaPidFactory.java | 106 - .../jdbc/internal/util/pid/PidFactory.java | 88 - .../internal/util/pool/GlobalStateInfo.java | 105 - .../mariadb/jdbc/internal/util/pool/Pool.java | 680 ---- .../jdbc/internal/util/pool/Pools.java | 129 - .../scheduler/DynamicSizedSchedulerImpl.java | 78 - .../DynamicSizedSchedulerInterface.java | 65 - .../scheduler/FixedSizedSchedulerImpl.java | 68 - .../util/scheduler/MariaDbThreadFactory.java | 78 - .../SchedulerServiceProviderHolder.java | 261 -- .../message/client/AuthMoreRawPacket.java | 45 + .../message/client/BulkExecutePacket.java | 173 + .../jdbc/message/client/ChangeDbPacket.java | 47 + .../message/client/ClearPasswordPacket.java | 49 + .../jdbc/message/client/ClientMessage.java | 159 + .../message/client/ClosePreparePacket.java | 51 + .../message/client/Ed25519PasswordPacket.java | 109 + .../jdbc/message/client/ExecutePacket.java | 147 + .../message/client/HandshakeResponse.java | 206 ++ .../jdbc/message/client/LongDataPacket.java | 124 + .../message/client/NativePasswordPacket.java | 80 + .../jdbc/message/client/PingPacket.java | 42 + .../jdbc/message/client/PreparePacket.java | 104 + .../jdbc/message/client/QueryPacket.java | 51 + .../client/QueryWithParametersPacket.java | 69 + .../jdbc/message/client/QuitPacket.java | 42 + .../message/client/RedoableClientMessage.java | 34 + .../RedoableWithPrepareClientMessage.java | 56 + .../client/RsaPublicKeyRequestPacket.java | 40 + .../message/client/Sha256PasswordPacket.java | 92 + .../client/Sha2PublicKeyRequestPacket.java | 42 + .../jdbc/message/client/SslRequestPacket.java | 62 + .../message/server/AuthMoreDataPacket.java | 50 + .../jdbc/message/server/AuthSwitchPacket.java | 65 + .../server/CachedPrepareResultPacket.java | 101 + .../server/ColumnDefinitionPacket.java | 396 +++ .../jdbc/message/server/Completion.java | 24 + .../jdbc/message/server/ErrorPacket.java | 83 + .../server/InitialHandshakePacket.java | 249 ++ .../mariadb/jdbc/message/server/OkPacket.java | 98 + .../message/server/PrepareResultPacket.java | 92 + .../jdbc/message/server/ServerMessage.java | 24 + .../authentication/AuthenticationPlugin.java | 24 +- .../AuthenticationPluginLoader.java | 8 +- .../standard}/CachingSha2PasswordPlugin.java | 161 +- .../standard/ClearPasswordPlugin.java | 80 + .../standard}/Ed25519PasswordPlugin.java | 61 +- .../standard/NativePasswordPlugin.java | 140 + .../standard/SendGssApiAuthPacket.java | 105 + .../standard/SendPamAuthPacket.java | 108 + .../standard}/Sha256PasswordPlugin.java | 104 +- .../authentication/standard}/ed25519/README | 0 .../standard}/ed25519/Utils.java | 2 +- .../standard}/ed25519/math/Constants.java | 4 +- .../standard}/ed25519/math/Curve.java | 8 +- .../standard}/ed25519/math/Encoding.java | 2 +- .../standard}/ed25519/math/Field.java | 8 +- .../standard}/ed25519/math/FieldElement.java | 2 +- .../standard}/ed25519/math/GroupElement.java | 98 +- .../math/ed25519/Ed25519FieldElement.java | 47 +- .../ed25519/Ed25519LittleEndianEncoding.java | 6 +- .../ed25519/math/ed25519/ScalarOps.java | 6 +- .../ed25519/spec/EdDSANamedCurveSpec.java | 9 +- .../ed25519/spec/EdDSANamedCurveTable.java | 12 +- .../ed25519/spec/EdDSAParameterSpec.java | 14 +- .../standard/gssapi/GssUtility.java | 54 + .../standard/gssapi/GssapiAuth.java | 34 + .../gssapi/StandardGssapiAuthentication.java | 55 +- .../WindowsNativeSspiAuthentication.java | 75 + .../{ => plugin}/credential/Credential.java | 4 +- .../credential/CredentialPlugin.java | 6 +- .../credential/CredentialPluginLoader.java | 4 +- .../aws/AwsCredentialGenerator.java | 10 +- .../aws/AwsIamCredentialPlugin.java | 34 +- .../credential/env/EnvCredentialPlugin.java | 18 +- .../system/PropertiesCredentialPlugin.java | 18 +- .../{ => plugin}/tls/TlsSocketPlugin.java | 19 +- .../tls/TlsSocketPluginLoader.java | 4 +- .../jdbc/util/CharsetEncodingLength.java | 362 +++ .../ClientParser.java} | 16 +- .../jdbc/util/ConfigurableSocketFactory.java | 24 +- .../org/mariadb/jdbc/util/DefaultOptions.java | 1061 ------- .../org/mariadb/jdbc/util/MutableInt.java | 21 + .../java/org/mariadb/jdbc/util/NativeSql.java | 339 ++ .../java/org/mariadb/jdbc/util/Options.java | 559 ---- .../org/mariadb/jdbc/util/ParameterList.java | 53 + .../PrepareResult.java} | 15 +- .../java/org/mariadb/jdbc/util/Security.java | 141 + .../util/constant => util}/Version.java | 33 +- .../constants/Capabilities.java} | 43 +- .../jdbc/util/constants/ColumnFlags.java | 38 + .../constants}/ConnectionState.java | 2 +- .../mariadb/jdbc/util/constants/HaMode.java | 109 + .../jdbc/util/constants/ServerStatus.java | 39 + .../jdbc/util/constants/StateChange.java | 32 + .../util/exceptions/ExceptionFactory.java | 294 ++ .../util/exceptions/MariaDbSqlException.java | 5 +- .../exceptions/MaxAllowedPacketException.java | 39 + .../org/mariadb/jdbc/util/log/Logger.java | 45 + .../mariadb/jdbc/util/log/LoggerHelper.java | 104 + .../org/mariadb/jdbc/util/log/Loggers.java | 479 +++ .../options/OptionAliases.java} | 16 +- ...lugin.authentication.AuthenticationPlugin} | 15 +- ...b.jdbc.plugin.credential.CredentialPlugin} | 6 +- ...g.mariadb.jdbc.plugin.tls.TlsSocketPlugin} | 0 src/main/resources/driver.properties | 58 + .../mariadb/jdbc/AllowMultiQueriesTest.java | 196 -- .../java/org/mariadb/jdbc/AttributeTest.java | 100 - .../org/mariadb/jdbc/AuroraListenerTest.java | 36 - .../org/mariadb/jdbc/AutoReconnectTest.java | 117 - src/test/java/org/mariadb/jdbc/BaseTest.java | 1168 ------- .../java/org/mariadb/jdbc/BasicBatchTest.java | 373 --- .../java/org/mariadb/jdbc/BasicFailover.java | 39 - .../java/org/mariadb/jdbc/BigQueryTest.java | 353 --- src/test/java/org/mariadb/jdbc/BlobTest.java | 592 ---- .../java/org/mariadb/jdbc/BooleanTest.java | 188 -- .../java/org/mariadb/jdbc/BufferTest.java | 292 -- src/test/java/org/mariadb/jdbc/ByteTest.java | 106 - .../org/mariadb/jdbc/CallStatementTest.java | 320 -- .../java/org/mariadb/jdbc/CancelTest.java | 209 -- .../java/org/mariadb/jdbc/CatalogTest.java | 105 - .../java/org/mariadb/jdbc/CheckDataTest.java | 215 -- .../ClientPreparedStatementParsingTest.java | 433 --- .../jdbc/ClientPreparedStatementTest.java | 288 -- .../java/org/mariadb/jdbc/CollationTest.java | 297 -- .../jdbc/ComMultiPrepareStatementTest.java | 70 - src/test/java/org/mariadb/jdbc/Common.java | 157 + .../java/org/mariadb/jdbc/ConnectionTest.java | 1009 ------ .../mariadb/jdbc/CredentialPluginTest.java | 116 - .../java/org/mariadb/jdbc/DataNTypeTest.java | 239 -- .../org/mariadb/jdbc/DataSourcePoolTest.java | 236 -- .../java/org/mariadb/jdbc/DataSourceTest.java | 254 -- .../org/mariadb/jdbc/DataTypeSignedTest.java | 466 --- .../mariadb/jdbc/DataTypeUnsignedTest.java | 727 ----- .../jdbc/DatatypeCompatibilityTest.java | 376 --- .../java/org/mariadb/jdbc/DatatypeTest.java | 1165 ------- src/test/java/org/mariadb/jdbc/DateTest.java | 758 ----- .../jdbc/DistributedTransactionTest.java | 291 -- .../java/org/mariadb/jdbc/DriverTest.java | 1762 ----------- .../org/mariadb/jdbc/ErrorMessageTest.java | 577 ---- .../org/mariadb/jdbc/ExecuteBatchTest.java | 463 --- .../java/org/mariadb/jdbc/FetchSizeTest.java | 272 -- .../org/mariadb/jdbc/GeneratedKeysTest.java | 147 - .../java/org/mariadb/jdbc/GeneratedTest.java | 112 - .../java/org/mariadb/jdbc/GeometryTest.java | 149 - .../jdbc/GiganticLoadDataInfileTest.java | 135 - .../java/org/mariadb/jdbc/JdbcParserTest.java | 654 ---- .../mariadb/jdbc/LocalInfileDisableTest.java | 94 - .../jdbc/LocalInfileInputStreamTest.java | 312 -- .../jdbc/LocalInfileInterceptorImpl.java | 61 - .../java/org/mariadb/jdbc/LocalTimeTest.java | 198 -- .../jdbc/MariaDbCompatibilityTest.java | 117 - .../jdbc/MariaDbDatabaseMetaDataTest.java | 165 - .../jdbc/MariaDbPoolDataSourceTest.java | 527 ---- .../org/mariadb/jdbc/MariaXaResourceTest.java | 22 - src/test/java/org/mariadb/jdbc/MultiTest.java | 1653 ---------- .../org/mariadb/jdbc/MyEventListener.java | 94 - .../java/org/mariadb/jdbc/ParserTest.java | 189 -- .../mariadb/jdbc/PasswordEncodingTest.java | 199 -- .../mariadb/jdbc/PooledConnectionTest.java | 148 - .../mariadb/jdbc/PreparedStatementTest.java | 618 ---- .../java/org/mariadb/jdbc/RePrepareTest.java | 149 - .../ReconnectionStateMaxAllowedStatement.java | 114 - .../mariadb/jdbc/ResultSetMetaDataTest.java | 260 -- .../java/org/mariadb/jdbc/ResultSetTest.java | 1236 -------- .../jdbc/ResultSetUnsupportedMethodsTest.java | 599 ---- .../org/mariadb/jdbc/ScalarFunctionsTest.java | 188 -- .../java/org/mariadb/jdbc/ScrollTypeTest.java | 156 - .../org/mariadb/jdbc/SerializableClass.java | 75 - .../jdbc/ServerPrepareStatementTest.java | 1186 ------- .../jdbc/Sha256AuthenticationTest.java | 227 -- src/test/java/org/mariadb/jdbc/SslTest.java | 1172 ------- .../org/mariadb/jdbc/StateChangeTest.java | 157 - .../java/org/mariadb/jdbc/StatementTest.java | 718 ----- .../org/mariadb/jdbc/StoredProcedureTest.java | 1665 ---------- .../java/org/mariadb/jdbc/TimeoutTest.java | 198 -- .../jdbc/TimezoneDaylightSavingTimeTest.java | 1181 ------- .../jdbc/TimezoneExplicitCalendarTest.java | 132 - .../org/mariadb/jdbc/TransactionTest.java | 128 - .../mariadb/jdbc/TruncateExceptionTest.java | 191 -- .../java/org/mariadb/jdbc/UnicodeTest.java | 158 - .../jdbc/UpdateResultSetMethodsTest.java | 1033 ------ src/test/java/org/mariadb/jdbc/UtilTest.java | 112 - .../jdbc/failover/AllowMasterDownTest.java | 217 -- .../failover/AuroraAutoDiscoveryTest.java | 104 - .../jdbc/failover/AuroraFailoverTest.java | 272 -- .../mariadb/jdbc/failover/BaseMonoServer.java | 228 -- .../jdbc/failover/BaseMultiHostTest.java | 466 --- .../jdbc/failover/BaseReplication.java | 395 --- .../org/mariadb/jdbc/failover/CancelTest.java | 113 - .../jdbc/failover/GaleraFailoverTest.java | 167 - .../failover/LoadBalanceFailoverTest.java | 143 - .../jdbc/failover/MonoServerFailoverTest.java | 206 -- .../jdbc/failover/OldFailoverTest.java | 104 - .../failover/ReplicationFailoverTest.java | 257 -- .../jdbc/failover/SequentialFailoverTest.java | 241 -- .../mariadb/jdbc/integration/BatchTest.java | 285 ++ .../BlobTest.java} | 103 +- .../ClobTest.java} | 93 +- .../jdbc/integration/ConnectionTest.java | 642 ++++ .../DatabaseMetadataTest.java | 764 +++-- .../mariadb/jdbc/integration/DriverTest.java | 86 + .../jdbc/integration/FailoverTest.java | 187 ++ .../jdbc/integration/FunctionTest.java | 56 + .../jdbc/integration/MultiQueriesTest.java | 148 + .../integration/ParameterMetaDataTest.java | 163 + .../PreparedStatementMetadataTest.java | 53 + .../PreparedStatementParametersTest.java | 403 +++ .../integration/PreparedStatementTest.java | 571 ++++ .../integration/ProcedureParameterTest.java | 250 ++ .../jdbc/integration/ProcedureTest.java | 919 ++++++ .../jdbc/integration/ResultSetTest.java | 235 ++ .../jdbc/integration/StatementTest.java | 746 +++++ .../UpdateResultSetTest.java | 715 ++--- .../integration/codec/BinaryCodecTest.java | 691 ++++ .../jdbc/integration/codec/BitCodecTest.java | 607 ++++ .../jdbc/integration/codec/BlobCodecTest.java | 552 ++++ .../jdbc/integration/codec/CharCodecTest.java | 674 ++++ .../integration/codec/CommonCodecTest.java | 95 + .../jdbc/integration/codec/DateCodecTest.java | 581 ++++ .../integration/codec/DateTimeCodecTest.java | 602 ++++ .../integration/codec/DecimalCodecTest.java | 684 ++++ .../integration/codec/DoubleCodecTest.java | 627 ++++ .../jdbc/integration/codec/EnumCodecTest.java | 686 ++++ .../integration/codec/FloatCodecTest.java | 644 ++++ .../jdbc/integration/codec/IntCodecTest.java | 797 +++++ .../jdbc/integration/codec/LongCodecTest.java | 809 +++++ .../integration/codec/MediumIntCodecTest.java | 803 +++++ .../integration/codec/SmallIntCodecTest.java | 846 +++++ .../jdbc/integration/codec/TimeCodecTest.java | 590 ++++ .../integration/codec/TinyIntCodecTest.java | 840 +++++ .../integration/codec/VarbinaryCodecTest.java | 690 ++++ .../integration/codec/VarcharCodecTest.java | 683 ++++ .../resultset/ReadResultSetTest.java | 261 ++ .../resultset/ResultSetMetadataTest.java | 245 ++ .../integration/resultset/RowChangeTest.java | 275 ++ .../resultset/StreamingRowChangeTest.java | 416 +++ .../tools}/TcpProxy.java | 9 +- .../tools}/TcpProxySocket.java | 9 +- .../read/resultset/ColumnDefinitionTest.java | 23 - .../tls/HostnameVerifierImplTest.java | 464 --- .../internal/util/DefaultOptionsTest.java | 230 -- .../SchedulerServiceProviderHolderTest.java | 161 - .../mariadb/jdbc/internal/util/UtilsTest.java | 159 - .../jdbc/internal/util/buffer/BufferTest.java | 127 - .../util}/ClientPrepareResultTest.java | 10 +- .../jdbc/unit/util/ConfigurationTest.java | 519 +++ .../jdbc/unit/util/log/LoggerHelperTest.java | 91 + .../org.mariadb.jdbc.LocalInfileInterceptor | 105 - src/test/resources/conf.properties | 1 + src/test/resources/localInfile.txt | 2 - src/test/resources/logback-test-travis.xml | 93 - src/test/resources/logback-test.xml | 69 +- src/test/resources/ssl/README | 16 - src/test/resources/ssl/wrong-server.crt | 21 - src/test/resources/timezoneTest.sql | 447 --- 508 files changed, 50331 insertions(+), 85315 deletions(-) delete mode 100644 .travis/build/Dockerfile delete mode 100644 .travis/build/build.sh delete mode 100644 .travis/build/docker-entrypoint.sh delete mode 100644 .travis/galera-compose.yml delete mode 100644 documentation/about-mariadb-connector-j.creole delete mode 100644 documentation/developers-guide.creole delete mode 100644 documentation/failover-and-high-availability-with-mariadb-connector-j.creole delete mode 100644 documentation/failover_loop.creole delete mode 100644 documentation/misc/images/aurora_fail_extract.png delete mode 100644 documentation/misc/images/bulk_batch.png delete mode 100644 documentation/misc/images/standard.png delete mode 100644 documentation/misc/images/standard_batch.png delete mode 100644 documentation/misc/images/telemetry.png delete mode 100644 documentation/misc/images/use_batch_multi_send.png delete mode 100644 documentation/plugin/GSSAPI.creole delete mode 100644 documentation/use-batch-multi-send-description.creole delete mode 100644 documentation/use-mariadb-connector-j-driver.creole create mode 100644 src/benchmark/README.md create mode 100644 src/benchmark/java/org/mariadb/jdbc/Common.java create mode 100644 src/benchmark/java/org/mariadb/jdbc/Select_1.java create mode 100644 src/benchmark/java/org/mariadb/jdbc/Select_10000_Rows.java create mode 100644 src/benchmark/java/org/mariadb/jdbc/Select_1000_params.java create mode 100644 src/benchmark/java/org/mariadb/jdbc/Select_1_user.java create mode 100644 src/benchmark/resources/logback-test.xml create mode 100644 src/main/java/org/mariadb/jdbc/BaseCallableStatement.java rename src/main/java/org/mariadb/jdbc/{BasePrepareStatement.java => BasePreparedStatement.java} (54%) delete mode 100644 src/main/java/org/mariadb/jdbc/BlobOutputStream.java delete mode 100644 src/main/java/org/mariadb/jdbc/CallParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/CallableFunctionStatement.java delete mode 100644 src/main/java/org/mariadb/jdbc/CallableProcedureStatement.java create mode 100644 src/main/java/org/mariadb/jdbc/ClientPreparedStatement.java delete mode 100644 src/main/java/org/mariadb/jdbc/ClientSidePreparedStatement.java create mode 100644 src/main/java/org/mariadb/jdbc/Configuration.java create mode 100644 src/main/java/org/mariadb/jdbc/Connection.java rename src/main/java/org/mariadb/jdbc/{MariaDbDatabaseMetaData.java => DatabaseMetaData.java} (92%) create mode 100644 src/main/java/org/mariadb/jdbc/FunctionStatement.java delete mode 100644 src/main/java/org/mariadb/jdbc/LocalInfileInterceptor.java delete mode 100644 src/main/java/org/mariadb/jdbc/MariaDbConnection.java delete mode 100644 src/main/java/org/mariadb/jdbc/MariaDbFunctionStatement.java delete mode 100644 src/main/java/org/mariadb/jdbc/MariaDbParameterMetaData.java delete mode 100644 src/main/java/org/mariadb/jdbc/MariaDbPoolDataSource.java delete mode 100644 src/main/java/org/mariadb/jdbc/MariaDbPooledConnection.java delete mode 100644 src/main/java/org/mariadb/jdbc/MariaDbProcedureStatement.java delete mode 100644 src/main/java/org/mariadb/jdbc/MariaDbSavepoint.java delete mode 100755 src/main/java/org/mariadb/jdbc/MariaDbStatement.java delete mode 100644 src/main/java/org/mariadb/jdbc/MariaDbXid.java delete mode 100644 src/main/java/org/mariadb/jdbc/MariaXaConnection.java delete mode 100644 src/main/java/org/mariadb/jdbc/MariaXaResource.java delete mode 100644 src/main/java/org/mariadb/jdbc/MySQLDataSource.java create mode 100644 src/main/java/org/mariadb/jdbc/ParameterMetaData.java create mode 100644 src/main/java/org/mariadb/jdbc/ProcedureStatement.java create mode 100644 src/main/java/org/mariadb/jdbc/ServerPreparedStatement.java delete mode 100644 src/main/java/org/mariadb/jdbc/ServerSidePreparedStatement.java delete mode 100644 src/main/java/org/mariadb/jdbc/SimpleParameterMetaData.java create mode 100644 src/main/java/org/mariadb/jdbc/SslMode.java create mode 100644 src/main/java/org/mariadb/jdbc/Statement.java delete mode 100644 src/main/java/org/mariadb/jdbc/UrlParser.java create mode 100644 src/main/java/org/mariadb/jdbc/client/Client.java create mode 100644 src/main/java/org/mariadb/jdbc/client/ClientImpl.java create mode 100644 src/main/java/org/mariadb/jdbc/client/ConnectionHelper.java create mode 100644 src/main/java/org/mariadb/jdbc/client/MultiPrimaryClient.java create mode 100644 src/main/java/org/mariadb/jdbc/client/MultiPrimaryReplicaClient.java create mode 100644 src/main/java/org/mariadb/jdbc/client/PacketReader.java rename src/main/java/org/mariadb/jdbc/{internal/io/output/AbstractPacketOutputStream.java => client/PacketWriter.java} (57%) create mode 100644 src/main/java/org/mariadb/jdbc/client/PrepareCache.java rename src/main/java/org/mariadb/jdbc/{internal/io/input => client}/ReadAheadBufferedStream.java (75%) create mode 100644 src/main/java/org/mariadb/jdbc/client/ReadableByteBuf.java create mode 100644 src/main/java/org/mariadb/jdbc/client/ServerVersion.java create mode 100644 src/main/java/org/mariadb/jdbc/client/TransactionSaver.java create mode 100644 src/main/java/org/mariadb/jdbc/client/context/BaseContext.java create mode 100644 src/main/java/org/mariadb/jdbc/client/context/Context.java create mode 100644 src/main/java/org/mariadb/jdbc/client/context/RedoContext.java create mode 100644 src/main/java/org/mariadb/jdbc/client/result/CompleteResult.java create mode 100644 src/main/java/org/mariadb/jdbc/client/result/Result.java rename src/main/java/org/mariadb/jdbc/{MariaDbResultSetMetaData.java => client/result/ResultSetMetaData.java} (74%) create mode 100644 src/main/java/org/mariadb/jdbc/client/result/StreamingResult.java create mode 100644 src/main/java/org/mariadb/jdbc/client/result/UpdatableResult.java rename src/main/java/org/mariadb/jdbc/{internal/io => client}/socket/NamedPipeSocket.java (73%) create mode 100644 src/main/java/org/mariadb/jdbc/client/socket/SocketHandlerFunction.java create mode 100644 src/main/java/org/mariadb/jdbc/client/socket/SocketUtility.java rename src/main/java/org/mariadb/jdbc/{internal/io => client}/socket/UnixDomainSocket.java (81%) rename src/main/java/org/mariadb/jdbc/{internal/protocol => client}/tls/DefaultTlsSocketPlugin.java (62%) rename src/main/java/org/mariadb/jdbc/{internal/protocol => client}/tls/HostnameVerifierImpl.java (85%) rename src/main/java/org/mariadb/jdbc/{internal/protocol => client}/tls/MariaDbX509KeyManager.java (75%) create mode 100644 src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509TrustManager.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/BinaryRowDecoder.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/Codec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/Codecs.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/DataType.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/Parameter.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/RowDecoder.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/TextRowDecoder.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/BigDecimalCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/BigIntegerCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/BitSetCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/BlobCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/BooleanCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/ByteArrayCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/ByteCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/ClobCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/DateCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/DoubleCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/DurationCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/FloatCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/IntCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/LocalDateCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/LocalDateTimeCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/LocalTimeCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/LongCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/ReaderCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/ShortCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/StreamCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/StringCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/TimeCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/TimestampCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/TinyIntCodec.java create mode 100644 src/main/java/org/mariadb/jdbc/codec/list/ZonedDateTimeCodec.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/ColumnType.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/Packet.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/Buffer.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/ErrorPacket.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/ReadInitialHandShakePacket.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/dao/CmdInformation.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/dao/CmdInformationBatch.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/dao/CmdInformationMultiple.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/dao/CmdInformationSingle.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/dao/ColumnLabelIndexer.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/dao/Results.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/resultset/ColumnDefinition.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/resultset/SelectResultSet.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/resultset/UpdatableColumnDefinition.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/resultset/UpdatableResultSet.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/resultset/rowprotocol/BinaryRowProtocol.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/resultset/rowprotocol/RowProtocol.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/read/resultset/rowprotocol/TextRowProtocol.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/ComQuery.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/ComStmtExecute.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/ComStmtPrepare.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/SendChangeDbPacket.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/SendClosePacket.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/SendHandshakeResponsePacket.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/SendSslConnectionRequestPacket.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/authentication/ClearPasswordPlugin.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/authentication/NativePasswordPlugin.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/authentication/OldPasswordPlugin.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/authentication/SendGssApiAuthPacket.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/authentication/SendPamAuthPacket.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/authentication/gssapi/GssUtility.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/authentication/gssapi/GssapiAuth.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/authentication/gssapi/WindowsNativeSspiAuthentication.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/BigDecimalParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/BooleanParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/ByteArrayParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/ByteParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/DateParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/DefaultParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/DoubleParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/FloatParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/IntParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/LocalTimeParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/LongParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/NullParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/OffsetTimeParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/ParameterHolder.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/ReaderParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/SerializableParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/ShortParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/StreamParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/StringParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/TimeParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/TimestampParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/com/send/parameters/ZonedDateTimeParameter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/failover/AbstractMastersListener.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/failover/AbstractMastersSlavesListener.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/failover/FailoverProxy.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/failover/HandleErrorResult.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/failover/Listener.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/failover/impl/AuroraListener.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/failover/impl/MastersFailoverListener.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/failover/impl/MastersSlavesListener.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/failover/thread/ConnectionValidator.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/failover/thread/FailoverLoop.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/failover/thread/TerminableRunnable.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/failover/tools/SearchFilter.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/io/LruTraceCache.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/io/TraceObject.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/io/input/DecompressPacketInputStream.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/io/input/PacketInputStream.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/io/input/StandardPacketInputStream.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/io/output/CompressPacketOutputStream.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/io/output/PacketOutputStream.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/io/output/StandardPacketOutputStream.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/io/socket/SharedMemorySocket.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/io/socket/SocketHandlerFunction.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/io/socket/SocketUtility.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/logging/Logger.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/logging/LoggerFactory.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/logging/NoLogger.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/logging/ProtocolLoggingProxy.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/logging/Slf4JLogger.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/osgi/MariaDbActivator.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/osgi/MariaDbDataSourceFactory.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/protocol/AbstractConnectProtocol.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/protocol/AbstractMultiSend.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/protocol/AbstractQueryProtocol.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/protocol/AsyncMultiRead.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/protocol/AsyncMultiReadResult.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/protocol/AuroraProtocol.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/protocol/MasterProtocol.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/protocol/MastersSlavesProtocol.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/protocol/Protocol.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/protocol/tls/MariaDbX509TrustManager.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/BulkStatus.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/CallableStatementCache.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/LogQueryTool.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/OptionUtils.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/ServerPrepareStatementCache.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/SqlStates.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/Utils.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/constant/ColumnFlags.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/constant/HaMode.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/constant/ParameterConstant.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/constant/ServerStatus.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/constant/StateChange.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/dao/CallableStatementCacheKey.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/dao/CloneableCallableStatement.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/dao/Identifier.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/dao/PrepareResult.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/dao/ReconnectDuringTransactionException.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/dao/ServerPrepareResult.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/exceptions/ExceptionFactory.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/exceptions/MaxAllowedPacketException.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/pid/JnaPidFactory.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/pid/PidFactory.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/pool/GlobalStateInfo.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/pool/Pool.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/pool/Pools.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/scheduler/DynamicSizedSchedulerImpl.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/scheduler/DynamicSizedSchedulerInterface.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/scheduler/FixedSizedSchedulerImpl.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/scheduler/MariaDbThreadFactory.java delete mode 100644 src/main/java/org/mariadb/jdbc/internal/util/scheduler/SchedulerServiceProviderHolder.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/AuthMoreRawPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/BulkExecutePacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/ChangeDbPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/ClearPasswordPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/ClientMessage.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/ClosePreparePacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/Ed25519PasswordPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/ExecutePacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/HandshakeResponse.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/LongDataPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/NativePasswordPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/PingPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/PreparePacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/QueryPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/QueryWithParametersPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/QuitPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/RedoableClientMessage.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/RedoableWithPrepareClientMessage.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/RsaPublicKeyRequestPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/Sha256PasswordPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/Sha2PublicKeyRequestPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/client/SslRequestPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/server/AuthMoreDataPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/server/AuthSwitchPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/server/CachedPrepareResultPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/server/ColumnDefinitionPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/server/Completion.java create mode 100644 src/main/java/org/mariadb/jdbc/message/server/ErrorPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/server/InitialHandshakePacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/server/OkPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/server/PrepareResultPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/message/server/ServerMessage.java rename src/main/java/org/mariadb/jdbc/{ => plugin}/authentication/AuthenticationPlugin.java (74%) rename src/main/java/org/mariadb/jdbc/{ => plugin}/authentication/AuthenticationPluginLoader.java (90%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/CachingSha2PasswordPlugin.java (54%) create mode 100644 src/main/java/org/mariadb/jdbc/plugin/authentication/standard/ClearPasswordPlugin.java rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/Ed25519PasswordPlugin.java (62%) create mode 100644 src/main/java/org/mariadb/jdbc/plugin/authentication/standard/NativePasswordPlugin.java create mode 100644 src/main/java/org/mariadb/jdbc/plugin/authentication/standard/SendGssApiAuthPacket.java create mode 100644 src/main/java/org/mariadb/jdbc/plugin/authentication/standard/SendPamAuthPacket.java rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/Sha256PasswordPlugin.java (67%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/README (100%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/Utils.java (97%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/math/Constants.java (88%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/math/Curve.java (86%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/math/Encoding.java (95%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/math/Field.java (86%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/math/FieldElement.java (96%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/math/GroupElement.java (87%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/math/ed25519/Ed25519FieldElement.java (94%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/math/ed25519/Ed25519LittleEndianEncoding.java (97%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/math/ed25519/ScalarOps.java (98%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/spec/EdDSANamedCurveSpec.java (66%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/spec/EdDSANamedCurveTable.java (81%) rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/ed25519/spec/EdDSAParameterSpec.java (79%) create mode 100644 src/main/java/org/mariadb/jdbc/plugin/authentication/standard/gssapi/GssUtility.java create mode 100644 src/main/java/org/mariadb/jdbc/plugin/authentication/standard/gssapi/GssapiAuth.java rename src/main/java/org/mariadb/jdbc/{internal/com/send/authentication => plugin/authentication/standard}/gssapi/StandardGssapiAuthentication.java (65%) create mode 100644 src/main/java/org/mariadb/jdbc/plugin/authentication/standard/gssapi/WindowsNativeSspiAuthentication.java rename src/main/java/org/mariadb/jdbc/{ => plugin}/credential/Credential.java (93%) rename src/main/java/org/mariadb/jdbc/{ => plugin}/credential/CredentialPlugin.java (86%) rename src/main/java/org/mariadb/jdbc/{ => plugin}/credential/CredentialPluginLoader.java (94%) rename src/main/java/org/mariadb/jdbc/{ => plugin}/credential/aws/AwsCredentialGenerator.java (92%) rename src/main/java/org/mariadb/jdbc/{ => plugin}/credential/aws/AwsIamCredentialPlugin.java (79%) rename src/main/java/org/mariadb/jdbc/{ => plugin}/credential/env/EnvCredentialPlugin.java (77%) rename src/main/java/org/mariadb/jdbc/{ => plugin}/credential/system/PropertiesCredentialPlugin.java (80%) rename src/main/java/org/mariadb/jdbc/{ => plugin}/tls/TlsSocketPlugin.java (77%) rename src/main/java/org/mariadb/jdbc/{ => plugin}/tls/TlsSocketPluginLoader.java (94%) create mode 100644 src/main/java/org/mariadb/jdbc/util/CharsetEncodingLength.java rename src/main/java/org/mariadb/jdbc/{internal/util/dao/ClientPrepareResult.java => util/ClientParser.java} (97%) delete mode 100644 src/main/java/org/mariadb/jdbc/util/DefaultOptions.java create mode 100644 src/main/java/org/mariadb/jdbc/util/MutableInt.java create mode 100644 src/main/java/org/mariadb/jdbc/util/NativeSql.java delete mode 100644 src/main/java/org/mariadb/jdbc/util/Options.java create mode 100644 src/main/java/org/mariadb/jdbc/util/ParameterList.java rename src/main/java/org/mariadb/jdbc/{internal/util/pool/PoolMBean.java => util/PrepareResult.java} (78%) create mode 100644 src/main/java/org/mariadb/jdbc/util/Security.java rename src/main/java/org/mariadb/jdbc/{internal/util/constant => util}/Version.java (60%) rename src/main/java/org/mariadb/jdbc/{internal/MariaDbServerCapabilities.java => util/constants/Capabilities.java} (58%) create mode 100644 src/main/java/org/mariadb/jdbc/util/constants/ColumnFlags.java rename src/main/java/org/mariadb/jdbc/{internal/util => util/constants}/ConnectionState.java (96%) create mode 100644 src/main/java/org/mariadb/jdbc/util/constants/HaMode.java create mode 100644 src/main/java/org/mariadb/jdbc/util/constants/ServerStatus.java create mode 100644 src/main/java/org/mariadb/jdbc/util/constants/StateChange.java create mode 100644 src/main/java/org/mariadb/jdbc/util/exceptions/ExceptionFactory.java rename src/main/java/org/mariadb/jdbc/{internal => }/util/exceptions/MariaDbSqlException.java (91%) create mode 100644 src/main/java/org/mariadb/jdbc/util/exceptions/MaxAllowedPacketException.java create mode 100644 src/main/java/org/mariadb/jdbc/util/log/Logger.java create mode 100644 src/main/java/org/mariadb/jdbc/util/log/LoggerHelper.java create mode 100644 src/main/java/org/mariadb/jdbc/util/log/Loggers.java rename src/main/java/org/mariadb/jdbc/{internal/util/DeRegister.java => util/options/OptionAliases.java} (72%) rename src/main/resources/META-INF/services/{org.mariadb.jdbc.authentication.AuthenticationPlugin => org.mariadb.jdbc.plugin.authentication.AuthenticationPlugin} (59%) rename src/main/resources/META-INF/services/{org.mariadb.jdbc.credential.CredentialPlugin => org.mariadb.jdbc.plugin.credential.CredentialPlugin} (80%) rename src/main/resources/META-INF/services/{org.mariadb.jdbc.tls.TlsSocketPlugin => org.mariadb.jdbc.plugin.tls.TlsSocketPlugin} (100%) create mode 100644 src/main/resources/driver.properties delete mode 100644 src/test/java/org/mariadb/jdbc/AllowMultiQueriesTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/AttributeTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/AuroraListenerTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/AutoReconnectTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/BaseTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/BasicBatchTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/BasicFailover.java delete mode 100644 src/test/java/org/mariadb/jdbc/BigQueryTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/BlobTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/BooleanTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/BufferTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/ByteTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/CallStatementTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/CancelTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/CatalogTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/CheckDataTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/ClientPreparedStatementParsingTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/ClientPreparedStatementTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/CollationTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/ComMultiPrepareStatementTest.java create mode 100644 src/test/java/org/mariadb/jdbc/Common.java delete mode 100644 src/test/java/org/mariadb/jdbc/ConnectionTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/CredentialPluginTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/DataNTypeTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/DataSourcePoolTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/DataSourceTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/DataTypeSignedTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/DataTypeUnsignedTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/DatatypeCompatibilityTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/DatatypeTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/DateTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/DistributedTransactionTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/DriverTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/ErrorMessageTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/ExecuteBatchTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/FetchSizeTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/GeneratedKeysTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/GeneratedTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/GeometryTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/GiganticLoadDataInfileTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/JdbcParserTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/LocalInfileDisableTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/LocalInfileInputStreamTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/LocalInfileInterceptorImpl.java delete mode 100644 src/test/java/org/mariadb/jdbc/LocalTimeTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/MariaDbCompatibilityTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/MariaDbDatabaseMetaDataTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/MariaDbPoolDataSourceTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/MariaXaResourceTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/MultiTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/MyEventListener.java delete mode 100644 src/test/java/org/mariadb/jdbc/ParserTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/PasswordEncodingTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/PooledConnectionTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/PreparedStatementTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/RePrepareTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/ReconnectionStateMaxAllowedStatement.java delete mode 100644 src/test/java/org/mariadb/jdbc/ResultSetMetaDataTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/ResultSetTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/ResultSetUnsupportedMethodsTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/ScalarFunctionsTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/ScrollTypeTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/SerializableClass.java delete mode 100644 src/test/java/org/mariadb/jdbc/ServerPrepareStatementTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/Sha256AuthenticationTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/SslTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/StateChangeTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/StatementTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/StoredProcedureTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/TimeoutTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/TimezoneDaylightSavingTimeTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/TimezoneExplicitCalendarTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/TransactionTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/TruncateExceptionTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/UnicodeTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/UpdateResultSetMethodsTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/UtilTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/AllowMasterDownTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/AuroraAutoDiscoveryTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/AuroraFailoverTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/BaseMonoServer.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/BaseMultiHostTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/BaseReplication.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/CancelTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/GaleraFailoverTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/LoadBalanceFailoverTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/MonoServerFailoverTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/OldFailoverTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/ReplicationFailoverTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/failover/SequentialFailoverTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/BatchTest.java rename src/test/java/org/mariadb/jdbc/{MariaDbBlobTest.java => integration/BlobTest.java} (72%) rename src/test/java/org/mariadb/jdbc/{MariaDbClobTest.java => integration/ClobTest.java} (76%) create mode 100644 src/test/java/org/mariadb/jdbc/integration/ConnectionTest.java rename src/test/java/org/mariadb/jdbc/{ => integration}/DatabaseMetadataTest.java (63%) create mode 100644 src/test/java/org/mariadb/jdbc/integration/DriverTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/FailoverTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/FunctionTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/MultiQueriesTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/ParameterMetaDataTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/PreparedStatementMetadataTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/PreparedStatementParametersTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/PreparedStatementTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/ProcedureParameterTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/ProcedureTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/ResultSetTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/StatementTest.java rename src/test/java/org/mariadb/jdbc/{ => integration}/UpdateResultSetTest.java (56%) create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/BinaryCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/BitCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/BlobCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/CharCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/CommonCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/DateCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/DateTimeCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/DecimalCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/DoubleCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/EnumCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/FloatCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/IntCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/LongCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/MediumIntCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/SmallIntCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/TimeCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/TinyIntCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/VarbinaryCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/codec/VarcharCodecTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/resultset/ReadResultSetTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/resultset/ResultSetMetadataTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/resultset/RowChangeTest.java create mode 100644 src/test/java/org/mariadb/jdbc/integration/resultset/StreamingRowChangeTest.java rename src/test/java/org/mariadb/jdbc/{failover => integration/tools}/TcpProxy.java (94%) rename src/test/java/org/mariadb/jdbc/{failover => integration/tools}/TcpProxySocket.java (97%) delete mode 100644 src/test/java/org/mariadb/jdbc/internal/com/read/resultset/ColumnDefinitionTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/internal/protocol/tls/HostnameVerifierImplTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/internal/util/DefaultOptionsTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/internal/util/SchedulerServiceProviderHolderTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/internal/util/UtilsTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/internal/util/buffer/BufferTest.java rename src/test/java/org/mariadb/jdbc/{internal/util/dao => unit/util}/ClientPrepareResultTest.java (94%) create mode 100644 src/test/java/org/mariadb/jdbc/unit/util/ConfigurationTest.java create mode 100644 src/test/java/org/mariadb/jdbc/unit/util/log/LoggerHelperTest.java delete mode 100644 src/test/resources/META-INF/services/org.mariadb.jdbc.LocalInfileInterceptor delete mode 100644 src/test/resources/localInfile.txt delete mode 100644 src/test/resources/logback-test-travis.xml delete mode 100644 src/test/resources/ssl/README delete mode 100644 src/test/resources/ssl/wrong-server.crt delete mode 100644 src/test/resources/timezoneTest.sql diff --git a/.travis.yml b/.travis.yml index 0cc134f74..9a251bad7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,6 @@ addons: before_install: - chmod +x .travis/script.sh - - chmod +x .travis/build/build.sh - - chmod +x .travis/build/docker-entrypoint.sh - - chmod 777 .travis/build/ - echo "MAVEN_OPTS='-Xmx384m'" > ~/.mavenrc install: @@ -27,56 +24,36 @@ cache: matrix: allow_failures: - - env: DB=build PACKET=8M - jdk: openjdk11 - env: DB=mysql:8.0 PACKET=8M ADDITIONAL_CONF=--default-authentication-plugin=mysql_native_password --caching_sha2_password_private_key_path=/etc/sslcert/server.key --caching_sha2_password_public_key_path=/etc/sslcert/public.key jdk: openjdk11 include: - - env: DB=build PACKET=8M - jdk: openjdk11 - env: SKYSQL=true PACKET=8M jdk: openjdk11 - - env: DB=mysql:5.6 PACKET=8M + - env: SKYSQL_HA=true PACKET=8M jdk: openjdk11 - env: DB=mysql:5.7 PACKET=8M jdk: openjdk11 - env: DB=mysql:8.0 PACKET=8M ADDITIONAL_CONF=--default-authentication-plugin=mysql_native_password --caching_sha2_password_private_key_path=/etc/sslcert/server.key --caching_sha2_password_public_key_path=/etc/sslcert/public.key jdk: openjdk11 - - env: DB=mariadb:10.1 PACKET=8M - jdk: openjdk11 - env: DB=mariadb:10.2 PACKET=8M jdk: openjdk11 - env: DB=mariadb:10.3 PACKET=8M jdk: openjdk11 - env: DB=mariadb:10.4 PACKET=8M - jdk: oraclejdk11 - - env: DB=mariadb:10.4 PACKET=8M - jdk: openjdk11 - - env: DB=mariadb:10.4 PACKET=8M GALERA=true - jdk: openjdk11 - - env: DB=mariadb:10.4 PACKET=8M PROFILE=true - jdk: openjdk11 - - env: DB=mariadb:10.4 PACKET=8M TYPE=PREPARE jdk: openjdk11 - - env: DB=mariadb:10.4 PACKET=8M TYPE=REWRITE + - env: DB=mariadb:10.5 PACKET=8M jdk: openjdk11 - - env: DB=mariadb:10.4 PACKET=8M TYPE=MULTI - jdk: openjdk11 - - env: DB=mariadb:10.4 PACKET=20M - jdk: openjdk11 - - env: DB=mariadb:10.4 PACKET=40M - jdk: openjdk11 - - env: DB=mariadb:10.4 PACKET=40M TYPE=BULK_SERVER - jdk: openjdk11 - - env: DB=mariadb:10.4 PACKET=40M TYPE=NO_BULK_CLIENT - jdk: openjdk11 - - env: DB=mariadb:10.4 PACKET=40M TYPE=NO_BULK_SERVER + - env: DB=mariadb:10.5 PACKET=8M + jdk: oraclejdk11 + - env: DB=mariadb:10.5 PACKET=8M BENCH=true + jdk: oraclejdk11 + - env: DB=mariadb:10.5 PACKET=20M jdk: openjdk11 - - env: DB=mariadb:10.4 PACKET=40M COMPRESSION=true + - env: DB=mariadb:10.5 PACKET=40M jdk: openjdk11 - - env: DB=mariadb:10.4 PACKET=8M + - env: DB=mariadb:10.5 PACKET=8M jdk: openjdk12 - - env: DB=mariadb:10.4 PACKET=8M MAXSCALE_VERSION=2.2.9 + - env: DB=mariadb:10.5 PACKET=8M MAXSCALE_VERSION=2.5.3 MAXSCALE_TEST_DISABLE=true SSLPORT=4009 jdk: openjdk11 script: diff --git a/.travis/build/Dockerfile b/.travis/build/Dockerfile deleted file mode 100644 index f152504be..000000000 --- a/.travis/build/Dockerfile +++ /dev/null @@ -1,99 +0,0 @@ -# vim:set ft=dockerfile: -FROM ubuntu:bionic - -# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added -RUN groupadd -r mysql && useradd -r -g mysql mysql - -# https://bugs.debian.org/830696 (apt uses gpgv by default in newer releases, rather than gpg) -RUN set -ex; \ - apt-get update; \ - if ! which gpg; then \ - apt-get install -y --no-install-recommends gnupg; \ - fi; \ -# Ubuntu includes "gnupg" (not "gnupg2", but still 2.x), but not dirmngr, and gnupg 2.x requires dirmngr -# so, if we're not running gnupg 1.x, explicitly install dirmngr too - if ! gpg --version | grep -q '^gpg (GnuPG) 1\.'; then \ - apt-get install -y --no-install-recommends dirmngr; \ - fi; \ - rm -rf /var/lib/apt/lists/* - -# add gosu for easy step-down from root -ENV GOSU_VERSION 1.10 -RUN set -ex; \ - \ - fetchDeps=' \ - ca-certificates \ - wget \ - '; \ - apt-get update; \ - apt-get install -y --no-install-recommends $fetchDeps; \ - rm -rf /var/lib/apt/lists/*; \ - \ - dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \ - wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \ - wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \ - \ -# verify the signature - export GNUPGHOME="$(mktemp -d)"; \ - gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \ - gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \ - command -v gpgconf > /dev/null && gpgconf --kill all || :; \ - rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \ - \ - chmod +x /usr/local/bin/gosu; \ -# verify that the binary works - gosu nobody true; \ - \ - apt-get purge -y --auto-remove $fetchDeps - -RUN mkdir /docker-entrypoint-initdb.d - -# install "pwgen" for randomizing passwords -# install "apt-transport-https" for Percona's repo (switched to https-only) -RUN apt-get update && apt-get install -y --no-install-recommends \ - apt-transport-https ca-certificates \ - tzdata \ - pwgen \ - && rm -rf /var/lib/apt/lists/* - -RUN { \ - echo "mariadb-server-10.5" mysql-server/root_password password 'unused'; \ - echo "mariadb-server-10.5" mysql-server/root_password_again password 'unused'; \ - } | debconf-set-selections - -RUN apt-get update -y -RUN apt-get install -y software-properties-common wget -RUN apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xcbcb082a1bb943db -RUN apt-key adv --recv-keys --keyserver ha.pool.sks-keyservers.net F1656F24C74CD1D8 -RUN echo 'deb http://yum.mariadb.org/galera/repo/deb bionic main' > /etc/apt/sources.list.d/galera-test-repo.list -RUN apt-get update -y - -RUN apt-get install -y curl libdbi-perl rsync socat galera3 libnuma1 libaio1 zlib1g-dev libreadline5 libjemalloc1 libsnappy1v5 libcrack2 - -COPY *.deb /root/ -RUN chmod 777 /root/* - -RUN dpkg --install /root/mysql-common* -RUN dpkg --install /root/mariadb-common* -RUN dpkg -R --unpack /root/ -RUN apt-get install -f -y - -RUN rm -rf /var/lib/apt/lists/* \ - && sed -ri 's/^user\s/#&/' /etc/mysql/my.cnf /etc/mysql/conf.d/* \ - && rm -rf /var/lib/mysql && mkdir -p /var/lib/mysql /var/run/mysqld \ - && chown -R mysql:mysql /var/lib/mysql /var/run/mysqld \ - && chmod 777 /var/run/mysqld \ - && find /etc/mysql/ -name '*.cnf' -print0 \ - | xargs -0 grep -lZE '^(bind-address|log)' \ - | xargs -rt -0 sed -Ei 's/^(bind-address|log)/#&/' \ - && echo '[mysqld]\nskip-host-cache\nskip-name-resolve' > /etc/mysql/conf.d/docker.cnf - -VOLUME /var/lib/mysql - -COPY docker-entrypoint.sh /usr/local/bin/ -RUN ln -s usr/local/bin/docker-entrypoint.sh / # backwards compat -ENTRYPOINT ["docker-entrypoint.sh"] - -EXPOSE 3306 -CMD ["mysqld"] - diff --git a/.travis/build/build.sh b/.travis/build/build.sh deleted file mode 100644 index f4d4c85da..000000000 --- a/.travis/build/build.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -echo "**************************************************************************" -echo "* searching for last complete build" -echo "**************************************************************************" - -wget -q -o /dev/null index.html http://hasky.askmonty.org/archive/10.5/ -grep -o ">build-[0-9]*" index.html | grep -o "[0-9]*" | tac | while read -r line ; do - - curl -s --head http://hasky.askmonty.org/archive/10.5/build-$line/kvm-deb-bionic-amd64/md5sums.txt | head -n 1 | grep "HTTP/1.[01] [23].." > /dev/null - if [ $? = "0" ]; then - echo "**************************************************************************" - echo "* Processing $line" - echo "**************************************************************************" - wget -q -o /dev/null -O $line.html http://hasky.askmonty.org/archive/10.5/build-$line/kvm-deb-bionic-amd64/debs/binary/ - grep -o ">[^\"]*\.deb" $line.html | grep -o "[^>]*\.deb" | while read -r file ; do - if [[ "$file" =~ ^mariadb-plugin.* ]] ; - then - echo "skipped file: $file" - else - echo "download file: $file" - wget -q -o /dev/null -O .travis/build/$file http://hasky.askmonty.org/archive/10.5/build-$line/kvm-deb-bionic-amd64/debs/binary/$file - fi - done - - exit - else - echo "skip build $line" - fi -done - - - diff --git a/.travis/build/docker-entrypoint.sh b/.travis/build/docker-entrypoint.sh deleted file mode 100644 index a3ee049c9..000000000 --- a/.travis/build/docker-entrypoint.sh +++ /dev/null @@ -1,196 +0,0 @@ -#!/bin/bash -set -eo pipefail -shopt -s nullglob - -# if command starts with an option, prepend mysqld -if [ "${1:0:1}" = '-' ]; then - set -- mysqld "$@" -fi - -# skip setup if they want an option that stops mysqld -wantHelp= -for arg; do - case "$arg" in - -'?'|--help|--print-defaults|-V|--version) - wantHelp=1 - break - ;; - esac -done - -# usage: file_env VAR [DEFAULT] -# ie: file_env 'XYZ_DB_PASSWORD' 'example' -# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of -# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) -file_env() { - local var="$1" - local fileVar="${var}_FILE" - local def="${2:-}" - if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then - echo >&2 "error: both $var and $fileVar are set (but are exclusive)" - exit 1 - fi - local val="$def" - if [ "${!var:-}" ]; then - val="${!var}" - elif [ "${!fileVar:-}" ]; then - val="$(< "${!fileVar}")" - fi - export "$var"="$val" - unset "$fileVar" -} - -_check_config() { - toRun=( "$@" --verbose --help --log-bin-index="$(mktemp -u)" ) - if ! errors="$("${toRun[@]}" 2>&1 >/dev/null)"; then - cat >&2 <<-EOM - ERROR: mysqld failed while attempting to check config - command was: "${toRun[*]}" - $errors - EOM - exit 1 - fi -} - -# Fetch value from server config -# We use mysqld --verbose --help instead of my_print_defaults because the -# latter only show values present in config files, and not server defaults -_get_config() { - local conf="$1"; shift - "$@" --verbose --help --log-bin-index="$(mktemp -u)" 2>/dev/null \ - | awk '$1 == "'"$conf"'" && /^[^ \t]/ { sub(/^[^ \t]+[ \t]+/, ""); print; exit }' - # match "datadir /some/path with/spaces in/it here" but not "--xyz=abc\n datadir (xyz)" -} - -# allow the container to be started with `--user` -if [ "$1" = 'mysqld' -a -z "$wantHelp" -a "$(id -u)" = '0' ]; then - _check_config "$@" - DATADIR="$(_get_config 'datadir' "$@")" - mkdir -p "$DATADIR" - find "$DATADIR" \! -user mysql -exec chown mysql '{}' + - exec gosu mysql "$BASH_SOURCE" "$@" -fi - -if [ "$1" = 'mysqld' -a -z "$wantHelp" ]; then - # still need to check config, container may have started with --user - _check_config "$@" - # Get config - DATADIR="$(_get_config 'datadir' "$@")" - - if [ ! -d "$DATADIR/mysql" ]; then - file_env 'MYSQL_ROOT_PASSWORD' - if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then - echo >&2 'error: database is uninitialized and password option is not specified ' - echo >&2 ' You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD' - exit 1 - fi - - mkdir -p "$DATADIR" - - echo 'Initializing database' - installArgs=( --datadir="$DATADIR" --rpm ) - if { mysql_install_db --help || :; } | grep -q -- '--auth-root-authentication-method'; then - # beginning in 10.4.3, install_db uses "socket" which only allows system user root to connect, switch back to "normal" to allow mysql root without a password - # see https://github.com/mariadb-corporation/server/commit/b9f3f06857ac6f9105dc65caae19782f09b47fb3 - # (this flag doesn't exist in 10.0 and below) - installArgs+=( --auth-root-authentication-method=normal ) - fi - # "Other options are passed to mysqld." (so we pass all "mysqld" arguments directly here) - mysql_install_db "${installArgs[@]}" "${@:2}" - echo 'Database initialized' - - SOCKET="$(_get_config 'socket' "$@")" - "$@" --skip-networking --socket="${SOCKET}" & - pid="$!" - - mysql=( mysql --protocol=socket -uroot -hlocalhost --socket="${SOCKET}" ) - - for i in {30..0}; do - if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then - break - fi - echo 'MySQL init process in progress...' - sleep 1 - done - if [ "$i" = 0 ]; then - echo >&2 'MySQL init process failed.' - exit 1 - fi - - if [ -z "$MYSQL_INITDB_SKIP_TZINFO" ]; then - # sed is for https://bugs.mysql.com/bug.php?id=20545 - mysql_tzinfo_to_sql /usr/share/zoneinfo | sed 's/Local time zone must be set--see zic manual page/FCTY/' | "${mysql[@]}" mysql - fi - - if [ ! -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then - export MYSQL_ROOT_PASSWORD="$(pwgen -1 32)" - echo "GENERATED ROOT PASSWORD: $MYSQL_ROOT_PASSWORD" - fi - - rootCreate= - # default root to listen for connections from anywhere - file_env 'MYSQL_ROOT_HOST' '%' - if [ ! -z "$MYSQL_ROOT_HOST" -a "$MYSQL_ROOT_HOST" != 'localhost' ]; then - # no, we don't care if read finds a terminating character in this heredoc - # https://unix.stackexchange.com/questions/265149/why-is-set-o-errexit-breaking-this-read-heredoc-expression/265151#265151 - read -r -d '' rootCreate <<-EOSQL || true - CREATE USER 'root'@'${MYSQL_ROOT_HOST}' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; - GRANT ALL ON *.* TO 'root'@'${MYSQL_ROOT_HOST}' WITH GRANT OPTION ; - EOSQL - fi - - "${mysql[@]}" <<-EOSQL - -- What's done in this file shouldn't be replicated - -- or products like mysql-fabric won't work - SET @@SESSION.SQL_LOG_BIN=0; - DELETE FROM mysql.user WHERE user NOT IN ('mysql.sys', 'mysqlxsys', 'root') OR host NOT IN ('localhost') ; - SET PASSWORD FOR 'root'@'localhost'=PASSWORD('${MYSQL_ROOT_PASSWORD}') ; - GRANT ALL ON *.* TO 'root'@'localhost' WITH GRANT OPTION ; - ${rootCreate} - DROP DATABASE IF EXISTS test ; - FLUSH PRIVILEGES ; - EOSQL - - if [ ! -z "$MYSQL_ROOT_PASSWORD" ]; then - mysql+=( -p"${MYSQL_ROOT_PASSWORD}" ) - fi - - file_env 'MYSQL_DATABASE' - if [ "$MYSQL_DATABASE" ]; then - echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" | "${mysql[@]}" - mysql+=( "$MYSQL_DATABASE" ) - fi - - file_env 'MYSQL_USER' - file_env 'MYSQL_PASSWORD' - if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then - echo "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" | "${mysql[@]}" - - if [ "$MYSQL_DATABASE" ]; then - echo "GRANT ALL ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_USER'@'%' ;" | "${mysql[@]}" - fi - fi - - echo - for f in /docker-entrypoint-initdb.d/*; do - case "$f" in - *.sh) echo "$0: running $f"; . "$f" ;; - *.sql) echo "$0: running $f"; "${mysql[@]}" < "$f"; echo ;; - *.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${mysql[@]}"; echo ;; - *) echo "$0: ignoring $f" ;; - esac - echo - done - - if ! kill -s TERM "$pid" || ! wait "$pid"; then - echo >&2 'MySQL init process failed.' - exit 1 - fi - - echo - echo 'MySQL init process done. Ready for start up.' - echo - fi -fi - -exec "$@" \ No newline at end of file diff --git a/.travis/galera-compose.yml b/.travis/galera-compose.yml deleted file mode 100644 index 7862b6712..000000000 --- a/.travis/galera-compose.yml +++ /dev/null @@ -1,50 +0,0 @@ -version: '2.1' -services: - node1: - image: mariadb:10.2 - environment: - MYSQL_DATABASE: testj - MYSQL_ALLOW_EMPTY_PASSWORD: 1 - MYSQL_INITDB_SKIP_TZINFO: "true" - ports: - - 3106:3306 - - 4167:4567 - - 4168:4568 - command: --wsrep-on=ON --max-connections=500 --wsrep-new-cluster --wsrep-cluster-address='gcomm://node1,node2,node3' --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --bind-address=0.0.0.0 --wsrep-new-cluster --binlog-format=ROW --wsrep-provider=/usr/lib/galera/libgalera_smm.so --wsrep-cluster-name=my_super_cluster --wsrep-on=ON --ssl-ca=/etc/sslcert/ca.crt --ssl-cert=/etc/sslcert/server.crt --ssl-key=/etc/sslcert/server.key - volumes: - - $SSLCERT:/etc/sslcert - - $ENTRYPOINT:/docker-entrypoint-initdb.d - - node2: - image: mariadb:10.2 - depends_on: - - node1 - environment: - MYSQL_DATABASE: testj - MYSQL_ALLOW_EMPTY_PASSWORD: 1 - MYSQL_INITDB_SKIP_TZINFO: "true" - ports: - - 3107:3306 - - 4267:4567 - - 4268:4568 - command: --wsrep-on=ON --max-connections=500 --wsrep-cluster-address='gcomm://node1,node2,node3' --wsrep-node-name=node2 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --bind-address=0.0.0.0 --binlog-format=ROW --wsrep-provider=/usr/lib/galera/libgalera_smm.so --wsrep-cluster-name=my_super_cluster --wsrep-on=ON --ssl-ca=/etc/sslcert/ca.crt --ssl-cert=/etc/sslcert/server.crt --ssl-key=/etc/sslcert/server.key - volumes: - - $SSLCERT:/etc/sslcert - - $ENTRYPOINT:/docker-entrypoint-initdb.d - - node3: - image: mariadb:10.2 - depends_on: - - node1 - environment: - MYSQL_DATABASE: testj - MYSQL_ALLOW_EMPTY_PASSWORD: 1 - MYSQL_INITDB_SKIP_TZINFO: "true" - ports: - - 3108:3306 - - 4367:4567 - - 4368:4568 - command: --wsrep-on=ON --max-connections=500 --wsrep-cluster-address='gcomm://node1,node2,node3' --wsrep-node-name=node3 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --bind-address=0.0.0.0 --binlog-format=ROW --wsrep-provider=/usr/lib/galera/libgalera_smm.so --wsrep-cluster-name=my_super_cluster --wsrep-on=ON --ssl-ca=/etc/sslcert/ca.crt --ssl-cert=/etc/sslcert/server.crt --ssl-key=/etc/sslcert/server.key - volumes: - - $SSLCERT:/etc/sslcert - - $ENTRYPOINT:/docker-entrypoint-initdb.d \ No newline at end of file diff --git a/.travis/gen-ssl.sh b/.travis/gen-ssl.sh index 687fbdb35..8e5875b67 100755 --- a/.travis/gen-ssl.sh +++ b/.travis/gen-ssl.sh @@ -1,26 +1,26 @@ #!/bin/bash set -e -log () { +log() { echo "$@" 1>&2 } -print_error () { +print_error() { echo "$@" 1>&2 exit 1 } -print_usage () { +print_usage() { print_error "Usage: gen-ssl-cert-key " } -gen_cert_subject () { +gen_cert_subject() { local fqdn="$1" [[ "${fqdn}" != "" ]] || print_error "FQDN cannot be blank" echo "/C=XX/ST=X/O=X/localityName=X/CN=${fqdn}/organizationalUnitName=X/emailAddress=X/" } -main () { +main() { local fqdn="$1" local sslDir="$2" [[ "${fqdn}" != "" ]] || print_usage @@ -45,14 +45,14 @@ main () { log "Generating CA certificate" openssl req \ - -sha1 \ - -new \ - -x509 \ - -nodes \ - -days 3650 \ - -subj "$(gen_cert_subject ca.example.com)" \ - -key "${caKeyFile}" \ - -out "${caCertFile}" + -sha1 \ + -new \ + -x509 \ + -nodes \ + -days 3650 \ + -subj "$(gen_cert_subject ca.example.com)" \ + -key "${caKeyFile}" \ + -out "${caCertFile}" log "Generating private key" openssl genrsa -out "${keyFile}" 2048 @@ -62,89 +62,88 @@ main () { log "Generating certificate signing request" openssl req \ - -new \ - -batch \ - -sha1 \ - -subj "$(gen_cert_subject "$fqdn")" \ - -set_serial 01 \ - -key "${keyFile}" \ - -out "${csrFile}" \ - -nodes + -new \ + -batch \ + -sha1 \ + -subj "$(gen_cert_subject "$fqdn")" \ + -set_serial 01 \ + -key "${keyFile}" \ + -out "${csrFile}" \ + -nodes log "Generating X509 certificate" openssl x509 \ - -req \ - -sha1 \ - -set_serial 01 \ - -CA "${caCertFile}" \ - -CAkey "${caKeyFile}" \ - -days 3650 \ - -in "${csrFile}" \ - -signkey "${keyFile}" \ - -out "${certFile}" + -req \ + -sha1 \ + -set_serial 01 \ + -CA "${caCertFile}" \ + -CAkey "${caKeyFile}" \ + -days 3650 \ + -in "${csrFile}" \ + -signkey "${keyFile}" \ + -out "${certFile}" log "Generating client certificate" openssl req \ - -batch \ - -newkey rsa:2048 \ - -days 3600 \ - -subj "$(gen_cert_subject "$fqdn")" \ - -nodes \ - -keyout "${clientKeyFile}" \ - -out "${clientReqFile}" + -batch \ + -newkey rsa:2048 \ + -days 3600 \ + -subj "$(gen_cert_subject "$fqdn")" \ + -nodes \ + -keyout "${clientKeyFile}" \ + -out "${clientReqFile}" openssl x509 \ - -req \ - -in "${clientReqFile}" \ - -days 3600 \ - -CA "${caCertFile}" \ - -CAkey "${caKeyFile}" \ - -set_serial 01 \ - -out "${clientCertFile}" + -req \ + -in "${clientReqFile}" \ + -days 3600 \ + -CA "${caCertFile}" \ + -CAkey "${caKeyFile}" \ + -set_serial 01 \ + -out "${clientCertFile}" # Now generate a keystore with the client cert & key log "Generating client keystore" openssl pkcs12 \ - -export \ - -in "${clientCertFile}" \ - -inkey "${clientKeyFile}" \ - -out "${tmpKeystoreFile}" \ - -name "mysqlAlias" \ - -passout pass:kspass + -export \ + -in "${clientCertFile}" \ + -inkey "${clientKeyFile}" \ + -out "${tmpKeystoreFile}" \ + -name "mysqlAlias" \ + -passout pass:kspass # convert PKSC12 to JKS keytool \ - -importkeystore \ - -deststorepass kspass \ - -destkeypass kspass \ - -destkeystore "${clientKeystoreFile}" \ - -srckeystore ${tmpKeystoreFile} \ - -srcstoretype PKCS12 \ - -srcstorepass kspass \ - -alias "mysqlAlias" + -importkeystore \ + -deststorepass kspass \ + -destkeypass kspass \ + -destkeystore "${clientKeystoreFile}" \ + -srckeystore ${tmpKeystoreFile} \ + -srcstoretype PKCS12 \ + -srcstorepass kspass \ + -alias "mysqlAlias" # Now generate a full keystore with the client cert & key + trust certificates log "Generating full client keystore" openssl pkcs12 \ - -export \ - -in "${clientCertFile}" \ - -inkey "${clientKeyFile}" \ - -out "${pcks12FullKeystoreFile}" \ - -name "mysqlAlias" \ - -passout pass:kspass - + -export \ + -in "${clientCertFile}" \ + -inkey "${clientKeyFile}" \ + -out "${pcks12FullKeystoreFile}" \ + -name "mysqlAlias" \ + -passout pass:kspass # convert PKSC12 to JKS keytool \ - -importkeystore \ - -deststorepass kspass \ - -destkeypass kspasskey \ - -deststoretype JKS \ - -destkeystore "${fullClientKeystoreFile}" \ - -srckeystore ${pcks12FullKeystoreFile} \ - -srcstoretype PKCS12 \ - -srcstorepass kspass \ - -alias "mysqlAlias" + -importkeystore \ + -deststorepass kspass \ + -destkeypass kspasskey \ + -deststoretype JKS \ + -destkeystore "${fullClientKeystoreFile}" \ + -srckeystore ${pcks12FullKeystoreFile} \ + -srcstoretype PKCS12 \ + -srcstorepass kspass \ + -alias "mysqlAlias" log "Generating trustStore" keytool -import -file "${certFile}" -alias CA -keystore "${fullClientKeystoreFile}" -storepass kspass -keypass kspasskey -noprompt @@ -159,4 +158,3 @@ main () { } main "$@" - diff --git a/.travis/maxscale-compose.yml b/.travis/maxscale-compose.yml index ae8ea7d03..9fb200649 100644 --- a/.travis/maxscale-compose.yml +++ b/.travis/maxscale-compose.yml @@ -1,17 +1,5 @@ version: '2.1' services: - maxscale: - depends_on: - - db - ports: - - 4006:4006 - - 4007:4007 - - 4008:4008 - build: - context: . - dockerfile: maxscale/Dockerfile - args: - MAXSCALE_VERSION: $MAXSCALE_VERSION db: image: $DB command: --max-connections=500 --max-allowed-packet=$PACKET --innodb-log-file-size=$INNODB_LOG_FILE_SIZE --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --ssl-ca=/etc/sslcert/ca.crt --ssl-cert=/etc/sslcert/server.crt --ssl-key=/etc/sslcert/server.key --bind-address=0.0.0.0 @@ -23,3 +11,25 @@ services: environment: MYSQL_DATABASE: testj MYSQL_ALLOW_EMPTY_PASSWORD: 1 + healthcheck: + test: ["CMD", "mysql", "--protocol=tcp", "-ubob", "-h127.0.0.1", "-ubob"] + timeout: 20s + retries: 10 + + maxscale: + depends_on: + db: + condition: service_healthy + links: + - "db:database" + ports: + - 4006:4006 + - 4008:4008 + - 4009:4009 + volumes: + - $SSLCERT:/etc/sslcert + build: + context: . + dockerfile: maxscale/Dockerfile + args: + MAXSCALE_VERSION: $MAXSCALE_VERSION diff --git a/.travis/maxscale/Dockerfile b/.travis/maxscale/Dockerfile index 0ae214d93..0a60b4e79 100644 --- a/.travis/maxscale/Dockerfile +++ b/.travis/maxscale/Dockerfile @@ -1,12 +1,12 @@ FROM centos:7 ARG MAXSCALE_VERSION -ENV MAXSCALE_VERSION ${MAXSCALE_VERSION:-2.1.4} +ENV MAXSCALE_VERSION ${MAXSCALE_VERSION:-2.5.3} COPY maxscale/mariadb.repo /etc/yum.repos.d/ RUN rpm --import https://yum.mariadb.org/RPM-GPG-KEY-MariaDB \ - && yum -y install https://downloads.mariadb.com/MaxScale/${MAXSCALE_VERSION}/centos/7/x86_64/maxscale-${MAXSCALE_VERSION}-1.centos.7.x86_64.rpm \ + && yum -y install https://downloads.mariadb.com/MaxScale/${MAXSCALE_VERSION}/centos/7/x86_64/maxscale-${MAXSCALE_VERSION}-2.rhel.7.x86_64.rpm \ && yum -y update RUN yum -y install maxscale-${MAXSCALE_VERSION} MariaDB-client \ @@ -14,8 +14,8 @@ RUN yum -y install maxscale-${MAXSCALE_VERSION} MariaDB-client \ && rm -rf /tmp/* COPY maxscale/docker-entrypoint.sh / -RUN chmod 777 /etc/maxscale.cnf COPY maxscale/maxscale.cnf /etc/ +RUN chmod 777 /etc/maxscale.cnf RUN chmod 777 /docker-entrypoint.sh diff --git a/.travis/maxscale/maxscale.cnf b/.travis/maxscale/maxscale.cnf index 01f5dbd0d..d65c0f624 100644 --- a/.travis/maxscale/maxscale.cnf +++ b/.travis/maxscale/maxscale.cnf @@ -1,45 +1,59 @@ -# MaxScale documentation on GitHub: -# https://github.com/mariadb-corporation/MaxScale/blob/2.1/Documentation/Documentation-Contents.md +# MaxScale documentation: +# https://mariadb.com/kb/en/mariadb-maxscale-24/ # Global parameters # # Complete list of configuration options: -# https://github.com/mariadb-corporation/MaxScale/blob/2.1/Documentation/Getting-Started/Configuration-Guide.md - +# https://mariadb.com/kb/en/mariadb-maxscale-24-mariadb-maxscale-configuration-guide/ [maxscale] -threads=2 -log_messages=1 -log_trace=1 -log_debug=1 +threads=auto # Server definitions # # Set the address of the server to the network -# address of a MySQL server. +# address of a MariaDB server. # +[server2] +type=server +address=database +port=3306 +protocol=MariaDBBackend +ssl=true +ssl_ca_cert=/etc/sslcert/server.crt +ssl_cert=/etc/sslcert/client.crt +ssl_key=/etc/sslcert/client.key + + [server1] type=server address=db port=3306 protocol=MariaDBBackend -authenticator_options=skip_authentication=true -router_options=master + # Monitor for the servers # # This will keep MaxScale aware of the state of the servers. -# MySQL Monitor documentation: -# https://github.com/mariadb-corporation/MaxScale/blob/2.1/Documentation/Monitors/MySQL-Monitor.md +# MariaDB Monitor documentation: +# https://mariadb.com/kb/en/mariadb-maxscale-24-mariadb-monitor/ -[MySQLMonitor] +[MariaDB-Monitor] type=monitor module=mariadbmon servers=server1 user=boby -passwd=hey -monitor_interval=10000 +password=heyPassw0@rd +monitor_interval=2000 + +[MariaDB-Monitor2] +type=monitor +module=mariadbmon +servers=server2 +user=boby +password=heyPassw0@rd +monitor_interval=2000 # Service definitions # @@ -48,78 +62,60 @@ monitor_interval=10000 # # ReadConnRoute documentation: -# https://github.com/mariadb-corporation/MaxScale/blob/2.1/Documentation/Routers/ReadConnRoute.md +# https://mariadb.com/kb/en/mariadb-maxscale-24-readconnroute/ -[Read-OnlyService] -enable_root_user=1 -version_string=10.4.99-MariaDB-maxScale +[Read-Only-Service] type=service router=readconnroute servers=server1 user=boby -passwd=hey +password=heyPassw0@rd router_options=slave -localhost_match_wildcard_host=1 -[Read-WriteService] -enable_root_user=1 -version_string=10.4.99-MariaDB-maxScale +# ReadWriteSplit documentation: +# https://mariadb.com/kb/en/mariadb-maxscale-24-readwritesplit/ + +[Read-Write-Service] type=service router=readwritesplit servers=server1 +version_string=10.5.99-MariaDB-maxScale user=boby -passwd=hey -localhost_match_wildcard_host=1 +password=heyPassw0@rd -[WriteService] +[Read-Write-Service2] type=service -router=readconnroute -servers=server1 +router=readwritesplit +version_string=10.5.99-MariaDB-maxScale +servers=server2 user=boby -passwd=hey -router_options=master -localhost_match_wildcard_host=1 -version_string=10.4.99-MariaDB-maxscale - - -# This service enables the use of the MaxAdmin interface -# MaxScale administration guide: -# https://github.com/mariadb-corporation/MaxScale/blob/2.1/Documentation/Reference/MaxAdmin.mda - -[MaxAdminService] -enable_root_user=1 -version_string=10.4.99-MariaDB-maxScale -type=service -router=cli +password=heyPassw0@rd # Listener definitions for the services # # These listeners represent the ports the # services will listen on. # -[WriteListener] -type=listener -service=WriteService -protocol=MariaDBClient -port=4007 -#socket=/var/lib/maxscale/writeconn.sock -[Read-OnlyListener] +[Read-Only-Listener] type=listener -service=Read-OnlyService +service=Read-Only-Service protocol=MariaDBClient port=4008 -#socket=/var/lib/maxscale/readconn.sock -[Read-WriteListener] +[Read-Write-Listener] type=listener -service=Read-WriteService +service=Read-Write-Service protocol=MariaDBClient port=4006 -#socket=/var/lib/maxscale/rwsplit.sock -[MaxAdminListener] + +[Read-Write-Listener2] type=listener -service=MaxAdminService -protocol=maxscaled -socket=/tmp/maxadmin.sock +service=Read-Write-Service2 +protocol=MariaDBClient +port=4009 +ssl=true +ssl_ca_cert=/etc/sslcert/ca.crt +ssl_cert=/etc/sslcert/server.crt +ssl_key=/etc/sslcert/server.key diff --git a/.travis/script.sh b/.travis/script.sh index f120707b6..eb230fdf0 100644 --- a/.travis/script.sh +++ b/.travis/script.sh @@ -4,182 +4,105 @@ set -x set -e ################################################################################################################### -# test different type of configuration +# launch docker server ################################################################################################################### -if [ -n "$SKYSQL" ] ; then - - if [ -z "$SKYSQL_TEST_HOST" ] ; then - echo "No SkySQL configuration found !" - exit 0 - fi - testSingleHost=true - urlString="jdbc:mariadb://$SKYSQL_TEST_HOST:$SKYSQL_TEST_PORT/testj?user=$SKYSQL_TEST_USER&password=$SKYSQL_TEST_PASSWORD&enablePacketDebug=true&useSsl&serverSslCert=$SKYSQL_TEST_SSL_CA" - - cmd=( mvn clean test $ADDITIONNAL_VARIABLES -DjobId=${TRAVIS_JOB_ID} ) - -else - case "$TYPE" in - "REWRITE" ) - urlString='jdbc:mariadb://mariadb.example.com:3305/testj?user=bob&rewriteBatchedStatements=true&enablePacketDebug=true' - ;; - "PREPARE" ) - urlString='jdbc:mariadb://mariadb.example.com:3305/testj?user=bob&useServerPrepStmts=true&enablePacketDebug=true' - ;; - "MULTI" ) - urlString='jdbc:mariadb://mariadb.example.com:3305/testj?user=bob&allowMultiQueries=true&enablePacketDebug=true' - ;; - "BULK_SERVER" ) - urlString='jdbc:mariadb://mariadb.example.com:3305/testj?user=bob&useBatchMultiSend=true&useServerPrepStmts=true&enablePacketDebug=true&useBulkStmts=true' - ;; - "NO_BULK_CLIENT" ) - urlString='jdbc:mariadb://mariadb.example.com:3305/testj?user=bob&useBatchMultiSend=true&enablePacketDebug=true' - ;; - "NO_BULK_SERVER" ) - urlString='jdbc:mariadb://mariadb.example.com:3305/testj?user=bob&useBatchMultiSend=false&useServerPrepStmts=true&enablePacketDebug=true' - ;; - "COMPRESSION" ) - urlString='jdbc:mariadb://mariadb.example.com:3305/testj?user=bob&useCompression=true&enablePacketDebug=true' - ;; - *) - urlString='jdbc:mariadb://mariadb.example.com:3305/testj?user=bob&enablePacketDebug=true' - ;; - esac; - - - - if [ -n "$PROFILE" ] ; then - export urlString="$urlString&profileSql=true" - pwd - rm src/test/resources/logback-test.xml - mv src/test/resources/logback-test-travis.xml src/test/resources/logback-test.xml - fi - - cmd=( mvn clean test $ADDITIONNAL_VARIABLES -DjobId=${TRAVIS_JOB_ID} \ - -DkeystorePath="$SSLCERT/client-keystore.jks" \ - -DkeystorePassword="kspass" \ - -DserverCertificatePath="$SSLCERT/server.crt" \ - -Dkeystore2Path="$SSLCERT/fullclient-keystore.jks" \ - -Dkeystore2Password="kspass" -DkeyPassword="kspasskey" \ - -Dkeystore2PathP12="$SSLCERT/fullclient-keystore.p12" \ - -DrunLongTest=true \ - -DserverPublicKey="$SSLCERT/public.key" ) - - if [ -n "$AURORA" ] ; then - if [ -n "$AURORA_STRING_URL" ] ; then - urlString=${AURORA_STRING_URL} - testSingleHost=true - else - testSingleHost=false - fi +if [ -z "$SKYSQL" ] && [ -z "$SKYSQL_HA" ]; then + export INNODB_LOG_FILE_SIZE=$(echo ${PACKET} | cut -d'M' -f 1)0M + if [ -n "$MAXSCALE_VERSION" ] ; then + ################################################################################################################### + # launch Maxscale with one server + ################################################################################################################### + mysql=( mysql --protocol=TCP -ubob -h127.0.0.1 --port=4006 test2) + export COMPOSE_FILE=.travis/maxscale-compose.yml + docker-compose -f ${COMPOSE_FILE} build + docker-compose -f ${COMPOSE_FILE} up -d else + mysql=(mysql --protocol=tcp -ubob -h127.0.0.1 --port=3305) + export COMPOSE_FILE=.travis/docker-compose.yml + docker-compose -f ${COMPOSE_FILE} up -d + fi - testSingleHost=true - - export INNODB_LOG_FILE_SIZE=$(echo ${PACKET}| cut -d'M' -f 1)0M - + ################################################################################################################### + # wait for docker initialisation + ################################################################################################################### + for i in {60..0}; do + if echo 'SELECT 1' | "${mysql[@]}" &>/dev/null; then + break + fi + echo 'server still not up' + sleep 1 + done + + if [ "$i" = 0 ]; then + if [ -n "COMPOSE_FILE" ]; then + docker-compose -f ${COMPOSE_FILE} logs if [ -n "$MAXSCALE_VERSION" ] ; then - ################################################################################################################### - # launch Maxscale with one server - ################################################################################################################### - mysql=( mysql --protocol=tcp -ubob -h127.0.0.1 --port=4007 ) - export COMPOSE_FILE=.travis/maxscale-compose.yml - urlString='jdbc:mariadb://mariadb.example.com:4007/testj?user=bob&killFetchStmtOnClose=false&enablePacketDebug=true' - docker-compose -f ${COMPOSE_FILE} build - docker-compose -f ${COMPOSE_FILE} up -d - else - if [ -n "$GALERA" ] || [ -n "$GALERA3" ] ; then - if [ -n "$GALERA3" ] ; then - ################################################################################################################### - # launch 3 galera servers - ################################################################################################################### - mysql=( mysql --protocol=tcp -ubob -hmariadb.example.com --port=3106 ) - export COMPOSE_FILE=.travis/galera-compose.yml - - urlString='jdbc:mariadb://mariadb.example.com:3106/testj?user=bob&enablePacketDebug=true' - cmd+=( -DdefaultGaleraUrl="jdbc:mariadb:sequential://mariadb.example.com:3106,mariadb.example.com:3107,mariadb.example.com:3108/testj?user=bob&enablePacketDebug=true" -DdefaultSequentialUrl="jdbc:mariadb:sequential://mariadb.example.com:3106,mariadb.example.com:3107,mariadb.example.com:3108/testj?user=bob&enablePacketDebug=true" -DdefaultLoadbalanceUrl="jdbc:mariadb:loadbalance://mariadb.example.com:3106,mariadb.example.com:3107,mariadb.example.com:3108/testj?user=bob&enablePacketDebug=true" ) - docker-compose -f ${COMPOSE_FILE} up -d - SLEEP 10 - else - mysql=( mysql --protocol=tcp -ubob -hmariadb.example.com --port=3106 ) - - urlString='jdbc:mariadb://mariadb.example.com:3106/testj?user=bob&enablePacketDebug=true' - docker run \ - -v $SSLCERT:/etc/sslcert \ - -v $ENTRYPOINT:/docker-entrypoint-initdb.d \ - -e MYSQL_INITDB_SKIP_TZINFO=yes \ - -e MYSQL_ALLOW_EMPTY_PASSWORD=1 \ - -e MYSQL_DATABASE=testj \ - -d \ - -p 3106:3306 \ - -p 4067:4567 \ - -p 4068:4568 \ - --name=node1 \ - mariadb:10.2 --wsrep-new-cluster --wsrep-cluster-address='gcomm://node1' \ - --wsrep-on=ON \ - --max-connections=500 \ - --wsrep-node-address=node1:4567 \ - --wsrep-node-name=node1 \ - --character-set-server=utf8mb4 \ - --collation-server=utf8mb4_unicode_ci \ - --bind-address=0.0.0.0 \ - --binlog-format=ROW \ - --wsrep-provider=/usr/lib/galera/libgalera_smm.so \ - --wsrep-cluster-name=my_super_cluster \ - --ssl-ca=/etc/sslcert/ca.crt \ - --ssl-cert=/etc/sslcert/server.crt --ssl-key=/etc/sslcert/server.key - - fi - else - - ################################################################################################################### - # launch docker server - ################################################################################################################### - mysql=( mysql --protocol=tcp -ubob -h127.0.0.1 --port=3305 ) - export COMPOSE_FILE=.travis/docker-compose.yml - docker-compose -f ${COMPOSE_FILE} up -d - - fi + docker-compose -f $COMPOSE_FILE exec maxscale tail -n 500 /var/log/maxscale/maxscale.log fi + fi - - ################################################################################################################### - # wait for docker initialisation - ################################################################################################################### - - for i in {60..0}; do - if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then - break - fi - echo 'data server still not active' - sleep 1 - done - - - if [ "$i" = 0 ]; then - if [ -n "COMPOSE_FILE" ] ; then - docker-compose -f ${COMPOSE_FILE} logs - fi - - echo 'SELECT 1' | "${mysql[@]}" - echo >&2 'data server init process failed.' - exit 1 - fi + echo 'SELECT 1' | "${mysql[@]}" + echo >&2 'data server init process failed.' + exit 1 fi + if [ -n "$BENCH" ]; then + ################################################################################################################### + # run bench + ################################################################################################################### + mvn clean package -P bench -Dmaven.test.skip + java -Duser.country=US -Duser.language=en -DTEST_PORT=3305 -DTEST_HOST=mariadb.example.com -DTEST_USERNAME=bob -jar target/benchmarks.jar + else + ################################################################################################################### + # run test suite + ################################################################################################################### + export TEST_DB_HOST=mariadb.example.com + export TEST_DB_HOST=mariadb.example.com + export TEST_DB_PORT=3305 + export TEST_DB_DATABASE=testj + export TEST_DB_USER=bob + export TEST_DB_OTHER= + + if [ -n "$MAXSCALE_VERSION" ] ; then + export TEST_DB_PORT=4006 + fi + + echo "Running tests for JDK version: $TRAVIS_JDK_VERSION" + mvn clean test $ADDITIONNAL_VARIABLES -DjobId=${TRAVIS_JOB_ID} + fi +else + if [ -n "$SKYSQL" ]; then + if [ -z "$SKYSQL_HOST" ] ; then + echo "No SkySQL configuration found !" + exit 0 + else + export TEST_DB_USER=$SKYSQL_USER + export TEST_DB_HOST=$SKYSQL_HOST + export TEST_DB_PASSWORD=$SKYSQL_PASSWORD + export TEST_DB_DATABASE=testj + export TEST_DB_PORT=$SKYSQL_PORT + export TEST_DB_OTHER=$'sslMode=verify-full&serverSslCert='$SKYSQL_SSL_CA + fi + else + if [ -z "$SKYSQL_HA_HOST" ] ; then + echo "No SkySQL HA configuration found !" + exit 0 + else + export TEST_DB_USER=$SKYSQL_HA_USER + export TEST_DB_HOST=$SKYSQL_HA_HOST + export TEST_DB_PASSWORD=$SKYSQL_HA_PASSWORD + export TEST_DB_DATABASE=testj + export TEST_DB_PORT=$SKYSQL_HA_PORT + export TEST_DB_OTHER=$'sslMode=verify-full&serverSslCert='$SKYSQL_HA_SSL_CA + fi + fi + if [ -z "$BENCH" ] ; then + echo "Running test for JDK version: $TRAVIS_JDK_VERSION" + mvn clean test $ADDITIONNAL_VARIABLES -DjobId=${TRAVIS_JOB_ID} + else + echo "Running benchmarks" + mvn clean package -P bench -Dmaven.test.skip + java -Duser.country=US -Duser.language=en -DTEST_PORT=$TEST_DB_PORT -DTEST_HOST=$TEST_DB_HOST -DTEST_USERNAME=bob -jar target/benchmarks.jar + fi fi - - -################################################################################################################### -# run test suite -################################################################################################################### -echo "Running coveralls for JDK version: $TRAVIS_JDK_VERSION" -cmd+=( -DdbUrl="$urlString" ) -cmd+=( -DtestSingleHost="$testSingleHost" ) -echo ${cmd} - -if [ -n "$MAXSCALE_VERSION" ] ; then - docker-compose -f $COMPOSE_FILE exec maxscale tail -n 500 /var/log/maxscale/maxscale.log -fi - -"${cmd[@]}" \ No newline at end of file diff --git a/.travis/sql/dbinit.sql b/.travis/sql/dbinit.sql index e9dc8b690..ebfaaedbb 100644 --- a/.travis/sql/dbinit.sql +++ b/.travis/sql/dbinit.sql @@ -1,8 +1,8 @@ CREATE USER 'bob'@'%'; GRANT ALL ON *.* TO 'bob'@'%' with grant option; -CREATE USER 'boby'@'%' identified by 'hey'; -GRANT ALL ON *.* TO 'boby'@'%' with grant option; +/*M!100501 CREATE USER 'boby'@'%' identified by 'heyPassw0@rd'*/; +/*M!100501 GRANT ALL ON *.* TO 'boby'@'%' with grant option*/; FLUSH PRIVILEGES; diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe722669..075b9b644 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,7 +110,7 @@ Evolutions * CONJ-678 - permit indication of truststore/keystore type (JKS/PKCS12), then not relying on java default type * CONJ-378 - GSSAPI: client can provide SPN * CONJ-667 - Support MYSQL_TYPE_JSON datatype -* CONJ-652 - faster results buffering socket available +* CONJ-652 - faster results bufing socket available * CONJ-659 - improve text performance reading date/time/timestamp resultset * CONJ-670 - ability to Refresh SSL certificate @@ -118,7 +118,7 @@ New options |Option|Description| |---|---| -|useReadAheadInput|use a buffered inputSteam that read socket available data.
Default: true| +|useReadAheadInput|use a bufed inputSteam that read socket available data.
Default: true| |keyStoreType|indicate key store type (JKS/PKCS12). default is null, then using java default type.| |trustStoreType|indicate trust store type (JKS/PKCS12). default is null, then using java default type| |servicePrincipalName|when using GSSAPI authentication, SPN (Service Principal Name) use the server SPN information. When set, connector will use this value, ignoring server information| @@ -400,7 +400,7 @@ Bug Bug * CONJ-490 - DataSource connectTimeout is in second, but was set on socket timeout that is in milliseconds -* CONJ-481 - Buffer overrun reading ResultSet when using option "useServerPrepStmts" +* CONJ-481 - buf overrun reading ResultSet when using option "useServerPrepStmts" * CONJ-470 - Error when executing SQL contains "values" and rewriteBatchedStatements=true * CONJ-471 - PK_NAME returned by DatabaseMetadata.getPrimaryKeys() should not be null * CONJ-477 - Aurora not compatible with option usePipelineAuth. Now automatically disabled when aurora is detected @@ -461,7 +461,7 @@ will be executed on close, to avoid having to parse all remaining results. ##= [CONJ-442] Memory optimization : streaming query. -Very big command now doesn't use any intermediate buffer. Commands are send directly to socket avoiding using memory, This permit to send very large object (1G) without using any additional memory. +Very big command now doesn't use any intermediate buf. Commands are send directly to socket avoiding using memory, This permit to send very large object (1G) without using any additional memory. ##= [CONJ-366] Faster connection : bundle first commands in authentication packet @@ -650,7 +650,7 @@ Different performance improvement have been done : * Using PreparedStatement on client side use a simple query parser to identify query parameters. This parsing was taking up to 7% of query time, reduced to 3%. * Better UTF-8 decoding avoiding memory consumption and gain 1-2% query time for big String. * client parsing optimization : rewriteBatchedStatements (insert into ab (i) values (1) and insert into ab (i) values (2) rewritten as insert into ab (i) values (1), (2)) - is now 19% faster (Depending on queries 40-50% of CPU time was spend testing that buffer size is big enough to contain query). + is now 19% faster (Depending on queries 40-50% of CPU time was spend testing that buf size is big enough to contain query). * there was some memory wastage when query return big resultset (> 10kb), slowing query. * ... @@ -794,7 +794,7 @@ Loading all results for large result sets is using a lot of memory. This functio ### Memory footprint improvement CONJ-125 -Buffers have been optimized to reduced memory footprint +bufs have been optimized to reduced memory footprint ### CallableStatement performance improvement. CONJ-209 diff --git a/README.md b/README.md index 9e3ab2a5e..603cf3056 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Tracker link https://ji [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.mariadb.jdbc/mariadb-java-client/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.mariadb.jdbc/mariadb-java-client) [![License (LGPL version 2.1)](https://img.shields.io/badge/license-GNU%20LGPL%20version%202.1-green.svg?style=flat-square)](http://opensource.org/licenses/LGPL-2.1) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/be7f4c89d63e496d824e8f365478e8c8)](https://www.codacy.com/app/diego-dupin/mariadb-connector-j?utm_source=github.com&utm_medium=referral&utm_content=MariaDB/mariadb-connector-j&utm_campaign=Badge_Grade) +[![codecov][codecov-image]][codecov-url] ## Obtaining the driver @@ -63,3 +64,7 @@ For a Getting started guide, API docs, recipes, etc. see the ## Contributing To get started with a development installation and learn more about contributing, please follow the instructions at our [Developers Guide.](/documentation/developers-guide.creole) + + +[codecov-image]:https://codecov.io/gh/rusher/mariadb-connector-j/branch/master/graph/badge.svg +[codecov-url]:https://codecov.io/gh/rusher/mariadb-connector-j diff --git a/appveyor-download.bat b/appveyor-download.bat index fc12a609e..5cc2dbbb3 100644 --- a/appveyor-download.bat +++ b/appveyor-download.bat @@ -2,12 +2,11 @@ set archive=http://ftp.hosteurope.de/mirror/archive.mariadb.org//mariadb-%DB%/winx64-packages/mariadb-%DB%-winx64.msi set last=http://mirror.i3d.net/pub/mariadb//mariadb-%DB%/winx64-packages/mariadb-%DB%-winx64.msi -curl -fsS -o server.msi %archive% +curl -fLsS -o server.msi %archive% if %ERRORLEVEL% == 0 goto end - -curl -fsS -o server.msi %last% +curl -fLsS -o server.msi %last% if %ERRORLEVEL% == 0 goto end echo Failure Reason Given is %errorlevel% diff --git a/appveyor.yml b/appveyor.yml index 7ed1e1388..3be7a6f09 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,32 +1,22 @@ version: '{build}' environment: matrix: - - DB: '10.2.31' + - DB: '10.2.34' APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 CMAKE_PARAM: 'Visual Studio 15 2017 Win64' JAVA_HOME: C:\Program Files\Java\jdk1.8.0 - - DB: '10.3.22' + - DB: '10.3.25' APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 CMAKE_PARAM: 'Visual Studio 15 2017 Win64' JAVA_HOME: C:\Program Files\Java\jdk1.8.0 - - DB: '10.4.12' + - DB: '10.4.15' APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 CMAKE_PARAM: 'Visual Studio 15 2017 Win64' JAVA_HOME: C:\Program Files\Java\jdk1.8.0 - - DB: '10.5.1' - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - CMAKE_PARAM: 'Visual Studio 15 2017 Win64' - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 - - - DB: '10.1.44' - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - CMAKE_PARAM: 'Visual Studio 15 2017 Win64' - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 - - - DB: '5.5.67' + - DB: '10.5.6' APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 CMAKE_PARAM: 'Visual Studio 15 2017 Win64' JAVA_HOME: C:\Program Files\Java\jdk1.8.0 diff --git a/codecov.yml b/codecov.yml index ce33bf10e..caa957e39 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1 +1,3 @@ -comment: off \ No newline at end of file +comment: off +codecov: + token: 02383a2a-0543-449d-8e35-3ec42dbc8b36 \ No newline at end of file diff --git a/documentation/about-mariadb-connector-j.creole b/documentation/about-mariadb-connector-j.creole deleted file mode 100644 index 729b13116..000000000 --- a/documentation/about-mariadb-connector-j.creole +++ /dev/null @@ -1,96 +0,0 @@ -= About MariaDB java connector - -MariaDB Connector/J is used to connect applications developed in Java to -MariaDB and MySQL databases using the standard JDBC API. The library is LGPL -licensed. - - -== Introduction -MariaDB Connector/J is a Type 4 JDBC driver. It was developed specifically as -a lightweight JDBC connector for use with MySQL and MariaDB database servers. -It's originally based on the Drizzle JDBC code, and with a lot of additions and -bug fixes. - -== Obtaining the driver -The driver source code can be downloaded from: -https://downloads.mariadb.org/connector-java/ - -Pre-built .jar files can be downloaded from: -https://code.mariadb.com/connectors/java/ - - -== Installing the driver -Installation of the client library is very simple, the jar file should be saved -in an appropriate place for your application and the classpath of your -application altered to include MariaDB Connector/J rather than your current -connector. - -Using maven : -{{{ - - org.mariadb.jdbc - mariadb-java-client - xxx - -}}} - -== Requirements - -* Java 7 or 8 (Last compatible version with java 6 is [[https://downloads.mariadb.org/connector-java/1.1.9/|1.1.9]]) - -Dependencies (not mandatory): -* [[https://maven-badges.herokuapp.com/maven-central/com.github.dblock.waffle/waffle-jna|waffle-jna 1.8.1]] -* [[https://maven-badges.herokuapp.com/maven-central/net.java.dev.jna/jna|jna 4.2.1]] -* [[https://maven-badges.herokuapp.com/maven-central/net.java.dev.jna/jna-platform|jna-platform 4.2.1]] -* [[https://maven-badges.herokuapp.com/maven-central/org.slf4j/jcl-over-slf4j|jcl-over-slf4j 1.7.14]] -* [[https://maven-badges.herokuapp.com/maven-central/org.slf4j/slf4j-api|slf4j-api 1.7.14]] -* [[https://maven-badges.herokuapp.com/com.google.guava/guava|guava 19.0]] - -jna is needed when when connecting to the server with unix sockets or windows shared memory -All of them are needed for native windows kerberos implementation. (see [[plugin/GSSAPI.creole|Gssapi documentation]]) - -== Source code - -The source code is available on GitHub: -https://github.com/mariadb-corporation/mariadb-connector-j and the most recent development -version can be obtained using the following command: - -{{{ -git clone https://github.com/mariadb-corporation/mariadb-connector-j.git -}}} - -== License - -GNU Lesser General Public License as published by the Free Software Foundation; -either version 2.1 of the License, or (at your option) any later version. - - -== Building and testing the driver - -The section deals with building the connector from source and testing it. If -you have downloaded a ready built connector, in a jar file, then this section -may be skipped. - -MariaDB Client Library for Java Applications uses maven for build. You first -need to ensure you have both java and maven installed on your server before you -can build the driver. - -To run the unit test, you'll need a MariaDB or MySQL server running on -localhost (on default TCP port 3306) and a database called 'testj', and user -'root' with empty password - -{{{ -git clone https://github.com/mariadb-corporation/mariadb-connector-j.git = Or, unpack the source distribution tarball -cd mariadb-connector-j - -# For the unit test run, start local mysqld mysqld, -# ensure that user root with empty password can login -mvn package - -# If you want to build without running unit tests, use -# mvn -Dmaven.test.skip=true package -}}} - -After that, you should have JDBC jar mariadb-java-client-x.y.z.jar in the -'target' subdirectory - diff --git a/documentation/developers-guide.creole b/documentation/developers-guide.creole deleted file mode 100644 index 44465b644..000000000 --- a/documentation/developers-guide.creole +++ /dev/null @@ -1,89 +0,0 @@ - -//This guide will teach you:// - - * How to install a local version of connector/J - * How to run tests locally and on travis CI - * How to submit a request - -= Contributing - -Each pull request should address a single issue, and contain both the fix as well as a description of how the pull request and tests that validate that the PR fixes the issue in question. - -For significant feature additions, we like to have an open issue in [[https://mariadb.atlassian.net/secure/RapidBoard.jspa?projectKey=CONJ|MariaDB JIRA]]. It is expected that discussion will have taken place in the attached issue. - -= Install Prerequisites - -These are the set of tools which are required in order to complete any build. Follow the links to download and install them on your own before continuing. - -* [[http://www.oracle.com/technetwork/java/javase/downloads/index.html|Oracle JDK 8]] ( with [[http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html|JCE policies]] if using TLS/SSL) -* IDE (eclipse / netbean / intelliJ) with maven and GIT plugins - -= Fork source - -Before downloading source, fork the project to your own repository, and use your repository as source. - - -= Run local test - -Before any submission : -Run the test locally : by default, you need to have a MySQL/MariaDB server on localhost:3306 with a database named "testj" and a user root without password. -so you can run - -{{{ - mvn test -}}} - -You can change those parameter by adding -DdbUrl parameter. like : - -{{{ - mvn test -DdbUrl=jdbc:mariadb://127.0.0.1:3306/testj?user=root&password=***** -}}} - -You can launch a specific test by adding -Dtest - -{{{ - mvn test -Dtest=org.mariadb.jdbc.JdbcParserTest -}}} - -When all test are passing, you can package project. -Additional tests , like javadoc formatting, code style validation will be done : - -{{{ - mvn package -Dmaven.test.skip=true -}}} - -If operation succeed, a new mariadb-java-client jar will be on the target folder. - -= Run travis test - -You can activate travis to validate your repository. -The advantage of travis compare to running test locally is that it will launch tests for a combination of those parameters : - -jdk: -* oraclejdk8 -* oraclejdk7 - -server : -* MariaDB 5.5 -* MariaDB 10.0 -* MariaDB 10.1 -* MariaDB 10.2 -* MySQL 5.6 -* MySQL 5.7 - -max_allowed_packet : -* 1M -* 16M -* 32M - -For that, you have to go on [[https://travis-ci.org|travis website]], connect with your github account, and activate your mariadb-connector-j repository. -After this step, every push to your repository will launch a travis test. - -== Submitting a request - -When your repository has the correction/change done, you can submit a pull request by clicking the "Pull request" button on github. -Please detail the operation done in your request. - -== License - -Distributed under the terms of the GNU Library or "Lesser" General Public License (LGPL). diff --git a/documentation/failover-and-high-availability-with-mariadb-connector-j.creole b/documentation/failover-and-high-availability-with-mariadb-connector-j.creole deleted file mode 100644 index 60a46bf1b..000000000 --- a/documentation/failover-and-high-availability-with-mariadb-connector-j.creole +++ /dev/null @@ -1,265 +0,0 @@ - -//**This guide will teach you:**// - - * The load balancing and high availability concepts in Mariadb java connector - * the different options - -Failover and high availability were introduced in 1.2.0. - - -= Load balancing and failover distinction -Failover occurs when a connection to a primary database server fails and the connector will open up a connection to another database server.\\ -For example, server A has the current connection. After a failure (server crash, network down …) the connection will switch to another server (B). - -Load balancing allows load (read and write) to be distributed over multiple servers. -\\ -= Replication cluster type -In MariaDB (and MySQL) replication, there are 2 different replication roles: -* Master role: Database server that permits read and write operations -* Slave role: Database server that permits only read operations - -This document describes configuration and implementation for 3 types of clusters: -* Multi-Master replication cluster. All hosts have a master replication role. (example : Galera) -* Master/slaves cluster: one host has the master replication role with multiple hosts in slave replication role. -* Hybrid cluster: multiple hosts in master replication role with multiple hosts in slave replication role. - -= Load balancing implementation -== Random picking -When initializing a connection or after a failed connection, the connector will attempt to connect to a host with a certain role (slave/master). -The connection is selected randomly among the valid hosts. Thereafter, all statements will run on that database server until the connection will be closed (or fails). - -The load-balancing will includes a pooling mechanism. -Example: when creating a pool of 60 connections, each one will use a random host. With 3 master hosts, the pool will have about 20 connections to each host. - -== Master/slave distributed load - -For a cluster composed of masters and slaves on connection initialization, there will be 2 underlying connections: one with a master host, another with a slave host. Only one connection is used at a time. \\ -For a cluster composed of master hosts only, each connection has only one underlying connection. \\ -The load will be distributed due to the random distribution of connections..\\ - -== Master/slave connection selection - -It’s the application that has to decide to use master or slave connection (the master connection is set by default).\\ -Switching the type of connection is done by using JDBC [[http://docs.oracle.com/javase/7/docs/api/java/sql/Connection.html#setReadOnly(boolean)|connection.setReadOnly(boolean readOnly)]] method. Setting read-only to true will use the slave connection, false, the master connection.\\ - -Example in standard java: -{{{ -connection = DriverManager.getConnection("jdbc:mariadb:replication://master1,slave1/test"); -stmt = connection.createStatement(); -stmt.execute("SELECT 1"); // will execute query on the underlying master1 connection -connection.setReadOnly(true); -stmt.execute("SELECT 1"); // will execute query on the underlying slave1 connection -}}} - -Some frameworks render this kind of operation easier, as for example Spring [[http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html#readOnly--|@transactionnal]] readOnly parameter (since spring 3.0.1). -In this example, setting readOnly to false will call the connection.setReadOnly(false) and therefore use the master connection. -{{{ -@Autowired -private EntityManager em; - -@Transactional(readOnly = false, propagation = Propagation.REQUIRED) -public void createContacts() { - Contact contact1 = new Contact(); - contact1.setGender("M"); - contact1.setName("JIM"); - em.persist(contact1); -} -}}} - -Generated Spring Data repository objects use the same logic: the find* method will use the slave connection, other use master connection without having to explicitly set that for each method. - -On a cluster with master hosts only, the use of connection.setReadOnly(true) does not change the connection, but if the database version is 10.0.0 or higher, the session is set to readOnly if option assureReadOnly is set to true, which means that any write query will throw an exception. - -=Failover behaviour -==Basic failover -When no failover/high availability parameter is set, the failover support is basic. Before executing a query, if the connection with the host is discarded, the connection will be reinitialized if parameter “autoReconnect” is set to true. - -==Standard failover -When a failover /high availability parameter is set.Check the [[configuration]] section for an overview on how to set the parameters. - -There can be multiple fail causes. When a failure occurs many things will be done: -* The fail host address will be put on a blacklist (shared by JVM). This host will not be used for the amount of time defined by the “loadBalanceBlacklistTimeout” parameter (default to 50 seconds). The only time a blacklisted address can be used is if all host of the same type (master/slave) are blacklisted. -* The connector will check the connection (with the mysql [[https://dev.mysql.com/doc/internals/en/com-ping.html|ping protocol]]). If the connection is back, is not read-only, and is in a transaction, the transaction will be rollbacked (there is no way to know if the last query has been received by the server and executed). -* If the failure relates to a slave connection - * If the master connection is still active, the master connection will be used immediately. - The query that was read-only will be relaunched and the connector will not throw any exception. - A "failover" thread will be launched to attempt to reconnect a slave host. - (if the query was a prepared query, this query will be re-prepared before execution) - * If the master connection is not active, the driver will attempt to create a new master or slave connection with a [[#connection-loop|connection loop]]. - if any connection is found, the query will be relaunched, if not, an SQLException with sqlState like “08XXX” will be thrown. -* If the failure relates to a master connection, the driver will attempt to create a new master connection with a [[#connection-loop|connection loop]], so the connection object will be immediately reusable.\\ - * on failure, an SQLException with be thrown with SQLState "08XXX". If using a pool, this connection will be discarded. - * on success, - * if possible query will be relaunched without throwing error (if was using a slave connection, or was a SELECT query not in a transaction for example). - * if not possible, an SQLException with be thrown with SQLState "25S03". -* When throwing an SQLException with SQLState "08XXX", the connection will be marked as closed. -* A “failover” thread will be launched to attempt to reconnect failing connection if connection is not closed. - -It’s up to the application to take measures to handle SQLException. See details in [[#application-concerns|application concerns]]. - -#Connection loop -When initializing a connection or after a failure, the driver will launch a connection loop the only case when this connection loop will not be executed is when the failure occurred on a slave with an active master. -This connection loop will try to connect to a valid host until finding a new connection or until the number of connections exceed the parameter “retriesAllDown” value (default to 120). - -This loop will attempt to connect sequentially to hosts in the following order: - -For a master connection : -* random connect to master host not blacklisted -* random connect to master blacklisted - -For a slave connection : -* random connect to slave host not blacklisted -* random connect to master host not blacklisted (if no active master connection) -* random connect to slave blacklisted -* random connect to master host blacklisted (if no active master connection) -The sequence stops as soon as all the underlying needed connections are found. Every time an attempt fails, the host will be blacklisted. -If after an entire loop a master connection is missing, the connection will be marked as closed. - -=Additional threads - -==Failover reconnection threads -A thread pool is created in case of a master/slave cluster, the size is defined according to the number of connection. -After a failure on a slave connection, readonly operations are temporary executed on the master connection. Some “failover threads” will try to reconnect the failed underlying connections. -When a new slave connection is retrieved, this one will be immediately used if connection was still in read-only mode.\\ -More details in [[failover_loop.creole|Failover loop threads]]. - - -==Connection validation thread -An additional thread is created when setting the option "validConnectionTimeout". -This thread will very that connections are all active. -This is normally done by pool that call [[https://docs.oracle.com/javase/7/docs/api/java/sql/Connection.html#isValid(int)|Connection.isValid()]]. - -=Application concerns -When a failover happen a SQLException with sqlState like "08XXX" or "25S03" may be thrown. - -Here are the different connection error codes: - -|=Code |=Condition | -|08000 | connection exception| -|08001 |SQL client unable to establish SQL connection| -|08002 |connection name in use| -|08003 |connection does not exist| -|08004 |SQL server rejected SQL connection| -|08006 |connection failure| -|08007 |transaction resolution unknown| -|25S03 |invalid transaction state-transaction is rolled back| - -A connection pool will detect connection error in SQLException (SQLState begin with "08"), and this connection will be discarded from pool.\\ - -When a failover occur the connector cannot know if the last request has been received by the database server and executed. Applications may have failover design to handle these particular cases: -If the application was in autoCommit mode (not recommended), the last query may have been executed and committed. The application will have no possibility to know that but the application will be functional. -If not in autoCommit mode, the query has been launched in a transaction that will not be committed. Depending of what caused the exception, the host may have the connection open on his side during a certain amount of time. Take care of [[https://mariadb.com/kb/en/mariadb/set-transaction/|transaction isolation]] level that may lock too much rows. - - - -=Configuration - -(See [[about-mariadb-connector-j.creole|About MariaDB java connector]] for all connection parameters) -JDBC connection string format is : -{{{ -jdbc:(mysql|mariadb):[replication:|failover:|loadbalance:|aurora:]//[,...]/[database][?=[&=]...] -}}} - -The standard option "connectTimeout" defined the socket connection timeout. by default, these option is set to 0 (no timeout).\\ -Since there are many servers, setting this option to a small amount of time make sense.\\ -During the [[#connection-loop|connection loop phase]], the driver will try to connect to server sequentially until the creation of an active connection. -Set this option to a small value (like 2000ms - to be set according to your environment) will permit to reject faulty server quickly. - - -==Failover / high availability parameters - -Each parameter corresponds to a specific use case: - -|=Failover option|=Description| -| **failover** | High availability (random picking connection initialisation) with failover support for master replication cluster (exemple Galera). \\* Since 1.2.0*| -| **sequential** |Failover support for master replication cluster (for example Galera) **without** High availability. \\the host will be connected in the order in which they were declared.\\\\Example when using the jdbc url string "jdbc:mariadb:replication:host1,host2,host3/test" : \\When connecting, the driver will always try first host1, and if not available host2 and following. After a host fail, the driver will reconnect according to this order.\\*Since 1.3.0*| -| **replication** | High availability (random picking connection initialisation) with failover support for master/slaves replication cluster (one or multiple master).\\* Since 1.2.0*| -| **aurora** | High availability (random picking connection initialisation) with failover support for Amazon Aurora replication cluster.\\* Since 1.2.0*| - - -==Failover / high availability options - -|=Option|=Description| -|autoReconnect|With basic failover only, if true, will attempt to recreate connection after a failover.\\*Default is false. Since 1.1.7*| -|retriesAllDown|When searching a valid host, maximum number of connection attempts before throwing an exception.\\*Default: 120. Since 1.2.0| -|failoverLoopRetries|When searching silently for a valid host, maximum number of connection attempts.\\This differ from "retriesAllDown" parameter, because this silent search is for example used after a disconnection of a slave connection when using the master connection.\\*Default: 120. Since 1.2.0*| -|validConnectionTimeout|With multiple hosts, after this time in seconds has elapsed it’s verified that the connections haven’t been lost.\\When 0, no verification will be done.\\*Default:120 seconds. Since 1.2.0*| -|loadBalanceBlacklistTimeout|When a connection fails, this host will be blacklisted during the "loadBalanceBlacklistTimeout" amount of time.\\When connecting to a host, the driver will try to connect to a host in the list of not blacklisted hosts and after that only on blacklisted ones if none has been found before that.\\This blacklist is shared inside the classloader.\\*Default: 50 seconds. Since 1.2.0*| -|assureReadOnly|If true, in high availability, and switching to a read-only host, assure that this host is in read-only mode by setting session read-only.\\alias "readOnlyPropagatesToServer" worked to for compatibility\\*Default to false.\\ Since 1.3.0*| - - -=Specifics for Amazon Aurora - -Amazon Aurora is a Master/Slaves cluster composed of one master instance with a maximum of 15 slave instances. Amazon Aurora includes automatic promotion of a slave instance in case of the master instance failing. The MariaDB connector/J implementation for Aurora is specific to handle this automatic failover.\\ - -To permit development/integration on a single-node cluster, only one host can be defined. -In this case, the driver behaves as for the configuration **failover**. - - -==Aurora failover implementation -Aurora failover management steps : -* Instance A is in write replication mode, instance B and C are in read replication mode. -* Instance A fails. -* Aurora detects A failure, and promote instance B in write mode. Instance C will change his master to use B. -* Cluster end-point will change to instance B end-point. -* Instance A will recover and be in read replication mode. - -==Aurora configuration - -===Aurora endpoints and discovery - -Every aurora instance has a specific endpoint, i.e. an URL that identify the host. Those endpoints look like `xxx.yyy.zzz.rds.amazonaws.com`. - -There is another endpoint named "cluster endpoint" (format `xxx.cluster-yyy.zzz.rds.amazonaws.com`) which is assigned to the current master instance and will change when a new master is promoted. - -In version before 1.5.1, cluster endpoint use was discouraged, since when a failover occur, this cluster endpoint can point for a limited time to a host that isn't the current master anymore. Old recommandation was to list all specific end-points. -This kind of url string will still work, but now, recommended url string has to use only cluster endpoint. - -Driver will automatically discover master and slaves of this cluster from current cluster end-point during connection time. This permit to add new replicas to the cluster instance will be discovered without changing driver configuration. - -This discovery append at connection time, so if you are using pool framework, check if this framework as a property that controls the maximum lifetime of a connection in the pool, and set a value to avoid infinite lifetime. When this lifetime is reached, pool will discarded the current connection, and create a new one (if needed). New connections will use the new replicas. -(If connections are never discarded, new replicas will begin be used only when a failover occur) - -===JDBC connection string - -The implementation is activated by specifying the “aurora” failover parameter. -Recommended connection string is using cluster end-point: -{{{ -jdbc:(mysql|mariadb):aurora://[clusterInstanceEndPoint[:port]]/[database][?=[&=]...] -}}} - -Before driver version 1.5.1, connection string has to list all end-point: -{{{ -jdbc:(mysql|mariadb):aurora://[instanceEndPoint[:port]][,instanceEndPoint[:port]...]/[database][?=[&=]...] -}}} - -If setting many endpoint, the replication role of each instance must not be defined for Aurora, because the role of each instance changes over time. The driver will check the instance role after connection initialisation. - -Example of connection string -{{{ - jdbc:mariadb:aurora://cluster.cluster-xxxx.us-east-1.rds.amazonaws.com/db -}}} - -Another difference is the option "socketTimeout" that defaults to 10 seconds, meaning that - if not changed - queries exceeding 10 seconds will throw exceptions. -Note that the option "createDatabaseIfNotExist=true" causes reader instances to be blacklisted during the creation of a new connection, because this option sends a "CREATE DATABASE" statement to the reader which is not read-only and therefore is rejected. - -==Aurora connection loop - -When searching for the master instance and connect to a slave instance, the connection order will be: -* Every Aurora instance knows the hostname of the current master. If the host has been described using their instance endpoint, that will permit to know the master instance and connect directly to it. -* If this isn’t the current master (because using IP, or possible after a failover between step 2 and 3), the loop will connect randomly the other not blacklisted instance (minus the current slave instance) -* Connect randomly to a blacklisted instance. - -When searching for a slave instance, the loop will connection order will be: -* random not blacklisted instances (excluding the current host if connected) -* random blacklisted instances -The loop will retry until the connections are found or parameter “retriesAllDown” is exceeded. - -==Aurora master verification -Without any query during the time defined by the parameter validConnectionTimeout (default to 120s) and if not set to 0, a verification will be done that the replication role of the underlying connections haven’t changed. - -==Aurora connection validation thread -Aurora as a specific [[#connection-validation-thread|connection validation thread]] implementation. -Since role of each instance can change over time, this will validate that connection are active AND role have not changed. - - \ No newline at end of file diff --git a/documentation/failover_loop.creole b/documentation/failover_loop.creole deleted file mode 100644 index 26460793a..000000000 --- a/documentation/failover_loop.creole +++ /dev/null @@ -1,45 +0,0 @@ - -//**This guide will teach you:**// - - * The goal of the failover threads - -= Failover reconnection - -//** This concern only master/slave cluster **// - -On a master/slave cluster, driver will use underlying 2 connections: one to a master instance, one to a slave instance. -When one of the connection fail, if driver does need it at once, it will create a new connection immediately before re-executing query if possible.\\ -If the failed connection is not needed immediately, this driver will subscribe to the "failover reconnection" that will be handle in other threads. -Failover threads will attempt to create new connection to replace failing ones, so the interruption is minimal for the queries in progress. -When client asked to use a failed connection, the new connection created by failover thread will replace the failed one. - -Example: after a failure on a slave connection, readonly operations are temporary executed on the master connection to avoid interruption client side. -Failover thread will then create a new slave connection that will replace the failed one. Next query will use the new slave connection. - -A pool of threads is initialized when using a master/slave configuration. The pool size evolves according to the number of connection. - -== Illustration - -Here is an example of a failover on a aurora cluster of 3 instances (one master and 2 slaves).\\ -(Source code https://github.com/rusher/connector-aurora-fail-test/tree/master) - -We can see 2 kinds of threads : -* Threads named "test-thread-XXX" do 130 queries "SELECT 1". 1/3 use master connection, 2/3 slave connection. -* Threads "mariaDb-reconnection-XXX" are created by the driver to handle failover. - -==== Colour signification: -"test-thread-XXX" threads: -* blue: querying -* red: blocked waiting to connect - -"mariaDb-reconnection-XXX" threads: -* yellow: waiting (idle) -* blue: working (recreating connection) -* red: blocked waiting to connect - -When the failover occur, most of the wasted time to reconnect is supported by the reconnection thread. -Most of query will be executed normally, only a few query executions will have a small additional delay (red block on "test-thread-XXX" threads). - -[[misc/images/telemetry.png|complete results]]\\ -{{misc/images/aurora_fail_extract.png}} - \ No newline at end of file diff --git a/documentation/misc/images/aurora_fail_extract.png b/documentation/misc/images/aurora_fail_extract.png deleted file mode 100644 index 3ee469b28bd72cb2fd1d5651d55591be09661f75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41235 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 diff --git a/documentation/misc/images/bulk_batch.png b/documentation/misc/images/bulk_batch.png deleted file mode 100644 index a1667de61f5c298a16692dda8e2d6388b9c24ebc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20010 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 fiGgHIKWMPq+X5Aoy30uE)&-K diff --git a/documentation/misc/images/standard_batch.png b/documentation/misc/images/standard_batch.png deleted file mode 100644 index 217cda6c108fd4720fc23c67e6c52a07c4e109ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21624 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

a)cRe0Y;=S*=GR#Gx)@uUhk*)j^F7oh8Iy_ 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 diff --git a/documentation/misc/images/telemetry.png b/documentation/misc/images/telemetry.png deleted file mode 100644 index 65af107a92f9c1dd95c41c994917289be96db38e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 770670 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

#nlSND}?>j5F#wY@~?*bVYO}F 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 z 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

BC?e`Pw-=wX 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 JzdL*Fe*t1CJ}&?O diff --git a/documentation/misc/images/use_batch_multi_send.png b/documentation/misc/images/use_batch_multi_send.png deleted file mode 100644 index 1198bf16322a3246b261a0e08a3c18ea71f8139c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8592 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[,...]/[database][?=[&=]] -}}} - - - HostDescription: -{{{ -[:] or address=(host=)[(port=)][(type=(master|slave))] -}}} - -Host must be a DNS name or IP address. In case of ipv6 and simple host -description, the IP address must be written inside brackets. The default port -is {{{3306}}}. The default type is {{{master}}}. If {{{replication}}} failover is -set, by default the first host is master, and the others are slaves. - -Examples : -* {{{localhost:3306}}} -* {{{[2001:0660:7401:0200:0000:0000:0edf:bdd7]:3306}}} -* {{{somehost.com:3306}}} -* {{{address=(host=localhost)(port=3306)(type=master)}}} - -=== Having MariaDB and MySQL driver in the same classpath -Since MariaDB want ambition to be a droppin replacement for mysql, driver permit connection string beginning with "jdbc:mariadb" or "jdbc:mysql". -To permit having MySQL and MariaDB driver are on the same classpath, since version 1.5.9, MariaDB driver doesn't accept connection string beginning with "jdbc:mysql" if option "disableMariaDbDriver" is set. - -{{{jdbc:mysql://localhost:3306/db?user=someUser&disableMariaDbDriver}}} won't be accepted by MariaDB driver. - - -== Failover parameters - -Failover was introduced in Connector/J 1.2.0. - -|=sequential |Failover support for master replication cluster (for example Galera) without High availability. The hosts will be connected in the order in which they were declared.\\\\Example when using the jdbc url string "jdbc:mariadb:replication:host1,host2,host3/test" : \\When connecting, the driver will always first try host1, and if not available host2 and so on. After a host fail, the driver will reconnect according to this order. \\//since 1.3.0//| -|=failover | High availability (random picking connection initialisation) with failover support for master replication cluster (for example Galera). \\//since 1.2.0//| -|=replication | High availability (random picking connection initialisation) with failover support for master/slave replication cluster (one or multiple masters) \\//since 1.2.0//| -|=aurora | High availability (random picking connection initialisation) with failover support for Amazon Aurora replication cluster \\//since 1.2.0//| - -See [[failover-and-high-availability-with-mariadb-connector-j|failover description]] for more information. -\\\\ -== Optional URL parameters - -General remark: Unknown options are accepted and silently ignored. - -The following options are currently supported. -=== Essential options -|=user|Database user name. \\//since 1.0.0//| -|=password|Password of database user.\\//since 1.0.0//| -|=rewriteBatchedStatements| For insert queries, rewrite batchedStatement to execute in a single executeQuery.\\example:\\{{{insert into ab (i) values (?)}}} with first batch values = 1, second = 2 will be rewritten\\{{{insert into ab (i) values (1), (2)}}}. \\\\If query cannot be rewriten in "multi-values", rewrite will use multi-queries : {{{INSERT INTO TABLE(col1) VALUES (?) ON DUPLICATE KEY UPDATE col2=?}}} with values [1,2] and [2,3]" will be rewritten\\{{{INSERT INTO TABLE(col1) VALUES (1) ON DUPLICATE KEY UPDATE col2=2;INSERT INTO TABLE(col1) VALUES (3) ON DUPLICATE KEY UPDATE col2=4}}}\\\\when active, the useServerPrepStmts option is set to false\\//Default: false. Since 1.1.8//| -|=connectTimeout| The connect timeout value, in milliseconds.\\//Default: DriverManager.getLoginTimeout() value if set or 30s. Since 1.1.8//| -|=useServerPrepStmts|Queries are prepared on the server side before executing, permitting faster execution next time. \\if rewriteBatchedStatements is set to true, this option will be set to false.\\//Default: true. Since 1.3.0//| -|=useBatchMultiSend|*Not compatible with aurora* Driver will can send queries by batch. \\If disable, queries are send one by one, waiting for result before sending next one. \\If enable, queries will be send by batch corresponding to option useBatchMultiSendNumber value (default 100) or according to server variable @@max_allowed_packet if packet size cannot permit to send as many queries. Results will be read afterwhile, avoiding a lot of network latency when client and server aren't on same host. \\\\ There is 2 differents use case : JDBC executeBatch() and when option useServerPrepStmts is enable and MariaDB server >= 10.2.1, PREPARE commands will be delayed, to send PREPARE + EXECUTE in the same packet. This option if mainly effective when client is distant from server. [[./use-batch-multi-send-description.creole|more information]]\\//Default: true (false if using aurora failover). Since 1.5.0//| -\\ - -=== Pooling - -See [[use-mariadb-connector-j-driver.creole#using-pooling|using pooling]] for more information. - -|=pool|Use pool. This option is useful only if not using a DataSource object, but only connection. \\//Default: false. since 2.2.0//| -|=poolName|Pool name that will permit to identify thread.\\default: auto-generated as MariaDb-pool-//since 2.2.0//| -|=maxPoolSize| The maximum number of physical connections that the pool should contain. \\//Default: 8. since 2.2.0//| -|=minPoolSize| When connection are removed since not used since more than "maxIdleTime", connections are closed and removed from pool. "minPoolSize" indicate the number of physical connections the pool should keep available at all times. Should be less or equal to maxPoolSize.\\//Default: maxPoolSize value. Since 2.2.0//| -|=poolValidMinDelay| When asking a connection to pool, Pool will validate connection state. "poolValidMinDelay" permit to disable this validation if connection has been borrowed recently avoiding useless verification in case of frequent reuse of connection. 0 meaning validation is done each time connection is asked.\\//Default: 1000 (in milliseconds). Since 2.2.0//| -|=maxIdleTime|The maximum amount of time in seconds that a connection can stay in pool when not used. This value must always be below @wait_timeout value - 45s \\//Default: 600 in seconds (=10 minutes), minimum value is 60 seconds. Since 2.2.0//| -|=staticGlobal|Indicate the following global variable (@@max_allowed_packet,@@wait_timeout,@@autocommit,@@auto_increment_increment,@@time_zone,@@system_time_zone,@@tx_isolation) values won't changed, permitting to pool to create new connection faster.\\//Default: false. Since 2.2.0//| -|=useResetConnection|When a connection is closed() (give back to pool), pool reset connection state. Setting this option, session variables change will be reset, and user variables will be destroyed when server permit it (MariaDB >= 10.2.4, MySQL >= 5.7.3), permitting to save memory on server if application make extensive use of variables\\//Default: false. Since 2.2.0//| - -\\ - - -=== TLS (SSL) - -|=useSSL|Force [[https://mariadb.com/kb/en/mariadb/secure-connections-overview/secure-connections-overview|SSL/TLS on connection]].\\//Default: false. Since 1.1.0//| -|=trustServerCertificate|When using SSL/TLS, do not check server's certificate.\\//Default: false. Since 1.1.1//| -|=serverSslCert|Server's certificate in DER form, or server's CA certificate. \\Can be used in one of 3 forms : \\* sslServerCert=/path/to/cert.pem (full path to certificate)\\* sslServerCert=classpath:relative/cert.pem (relative to current classpath)\\* or as verbatim DER-encoded certificate string "------BEGING CERTIFICATE-----" .\\//since 1.1.3//| -|=keyStore|File path of the keyStore file that contain client private key store and associate certificates (similar to java System property "javax.net.ssl.keyStore", but ensure that only the private key's entries are used).(legacy alias clientCertificateKeyStoreUrl).\\//Since 1.3.4//| -|=keyStorePassword|Password for the client certificate keyStore (similar to java System property "javax.net.ssl.keyStorePassword").(legacy alias clientCertificateKeyStorePassword)\\//Since 1.3.4//| -|=keyPassword|Password for the private key in client certificate keyStore. (only needed if private key password differ from keyStore password).\\//Since 1.5.3//| -|=trustStore|File path of the trustStore file (similar to java System property "javax.net.ssl.trustStore"). (legacy alias trustCertificateKeyStoreUrl)\\Use the specified file for trusted root certificates.\\When set, overrides serverSslCert.\\//Since 1.3.4//| -|=trustStorePassword|Password for the trusted root certificate file (similar to java System property "javax.net.ssl.trustStorePassword").\\(legacy alias trustCertificateKeyStorePassword).\\//Since 1.3.4//| -|=enabledSslProtocolSuites|Force TLS/SSL protocol to a specific set of TLS versions (comma separated list). \\Example : "TLSv1, TLSv1.1, TLSv1.2"\\//Default: TLSv1, TLSv1.1" before 2.3.0, "TLSv1, TLSv1.1, TLSv1.2" since v2.3.0". Since 1.5.0//| -|=enabledSslCipherSuites|Force TLS/SSL cipher (comma separated list).\\ Example : "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, TLS_DHE_DSS_WITH_AES_256_GCM_SHA384"\\//Default: use JRE ciphers. Since 1.5.0//| - -\\ - -=== Log - |=log|Enable log information. \\//require Slf4j version > 1.4 dependency.//\\Log level correspond to Slf4j logging implementation\\//Default: false. Since 1.5.0//| - |=maxQuerySizeToLog|Only the first characters corresponding to this options size will be displayed in logs\\//Default: 1024. Since 1.5.0//| - |=slowQueryThresholdNanos|Will log query with execution time superior to this value (if defined )\\//Default: 1024. Since 1.5.0//| - |=profileSql|log query execution time.\\//Default: false. Since 1.5.0//| - -\\ -=== Infrequently used - -|=useFractionalSeconds| Correctly handle subsecond precision in timestamps (feature available with MariaDB 5.3 and later).\\May confuse 3rd party components (Hibernated).\\//Default: true. Since 1.0.0//| -|=allowMultiQueries| permit multi-queries like {{{insert into ab (i) values (1); insert into ab (i) values (2)}}}. //Default: false. Since 1.0.0//| -|=dumpQueriesOnException|If set to 'true', an exception is thrown during query execution containing a query string.\\//Default: false. Since 1.1.0//| -|=useCompression|allow compression in the MySQL Protocol.\\//Default: false. Since 1.0.0//| -|=socketFactory| to use a custom socket factory, set it to the full name of the class that implements javax.net.SocketFactory.\\//since 1.0.0//| -|=tcpNoDelay|Sets corresponding option on the connection socket.\\//since 1.0.0//| -|=tcpKeepAlive|Sets corresponding option on the connection socket.\\//since 1.0.0//| -|=tcpAbortiveClose|Sets corresponding option on the connection socket.\\//since 1.1.1//| -|=tcpRcvBuf| set buffer size for TCP buffer (SO_RCVBUF).\\//since 1.0.0//| -|=tcpSndBuf| set buffer size for TCP buffer (SO_SNDBUF).\\//since 1.0.0//| -|=pipe| On Windows, specify named pipe name to connect to mysqld.exe. When using pipe, named pipe can throw an exception ERROR_PIPE_BUSY if all pipe are busy. Please consider setting option connectTimeout too, to permit retry connection in those particular cases. \\//since 1.1.3//| -|=tinyInt1isBit| Datatype mapping flag, handle MySQL Tiny as BIT(boolean).\\//Default: true. Since 1.0.0//| -|=yearIsDateType|Year is date type, rather than numerical.\\//Default: true. Since 1.0.0//| -|=sessionVariables|= pairs separated by comma, mysql session variables, set upon establishing successful connection.\\//since 1.1.0//| -|=localSocket|Permits connecting to the database via Unix domain socket, if the server allows it. \\The value is the path of Unix domain socket (i.e "socket" database parameter : select @@socket) .\\//since 1.1.4//| -|=sharedMemory|Permits connecting to the database via shared memory, if the server allows it. \\The value is the base name of the shared memory.\\//since 1.1.4//| -|=localSocketAddress|Hostname or IP address to bind the connection socket to a local (UNIX domain) socket.\\//since 1.1.7//| -|=socketTimeout|Defined the network socket timeout (SO_TIMEOUT) in milliseconds. Value of 0 disable this timeout.. \\Default: 0 (standard configuration) or 10 000ms (using "aurora" failover configuration).\\//since 1.1.7//| -|=interactiveClient|Session timeout is defined by the [[https://mariadb.com/kb/en/server-system-variables/#wait_timeout|wait_timeout]] server variable. Setting interactiveClient to true will tell the server to use the [[https://mariadb.com/kb/en/mariadb/server-system-variables/#interactive_timeout|interactive_timeout]] server variable.\\//Default: false. Since 1.1.7//| -|=useOldAliasMetadataBehavior|Metadata ResultSetMetaData.getTableName() returns the physical table name. "useOldAliasMetadataBehavior" permits activating the legacy code that sends the table alias if set. \\//Default: false. Since 1.1.9//| -|=createDatabaseIfNotExist|the specified database in the url will be created if nonexistent.\\//Default: false. Since 1.1.7//| -|=serverTimezone|Defines the server time zone.\\to use only if the jre server has a different time implementation of the server.\\(best to have the same server time zone when possible).\\//since 1.1.7//| -|=prepStmtCacheSize| if useServerPrepStmts = true, defines the prepared statement cache size. \\//Default: 250. Since 1.3.0//| -|=prepStmtCacheSqlLimit| if useServerPrepStmts = true, defined queries larger than this size will not be cached. \\//Default: 2048. Since 1.3.0//| -|=jdbcCompliantTruncation| Truncation error ("Data truncated for column '%' at row %", "Out of range value for column '%' at row %") will be thrown as an error, and not as a warning.\\//Default: true. Since 1.4.0//| -|=cacheCallableStmts| enable/disable callable Statement cache\\//Default: true. Since 1.4.0//| -|=callableStmtCacheSize| This sets the number of callable statements that the driver will cache per VM if "cacheCallableStmts" is enabled.\\//Default: true. Since 1.4.0//| -|=useBatchMultiSendNumber| When option useBatchMultiSend is active, indicate the maximum query send in a row before reading results.\\//Default: 100. Since 1.5.0//| -|=connectionAttributes| When performance_schema is active, permit to send server some client information in a key;value pair format (example: connectionAttributes=key1:value1,key2,value2).\\Those informations can be retrieved on server within tables performance_schema.session_connect_attrs and performance_schema.session_account_connect_attrs.\\This can permit from server an identification of client/application\\//Since 1.4.0//| -|=continueBatchOnError| When executing batch queries, must batch continue on error and throw exception when ended, or stop immediately \\//Default: true. Since 1.4.0// -|=disableSslHostnameVerification| When using ssl, driver check hostname against the server's identity as presented in the server's Certificate (checking alternative names or certificate CN) to prevent man-in-the-middle attack. This option permit to deactivate this validation.\\//Default: false. Since 2.1.0// -|=autocommit|Set default autocommit value.\\//Default: true. Since 2.2.0// -|=galeraAllowedState|Usually, Connection.isValid just send an empty packet to server, and server send a small response to ensure connectivity. When this option is set, connector will ensure Galera server state "wsrep_local_state" correspond to allowed values (separated by comma). example "4,5", recommended is "4". see [galera state](http://galeracluster.com/documentation-webpages/nodestates.html#node-state-changes) to know more..\\//Default: empty. Since 2.2.5// -|=useAffectedRows|default correspond to the JDBC standard, reporting real affected rows. if enable, will report "affected" rows. example : if enable, an update command that doesn't change a row value will still be "affected", then report.\\//Default: false. Since 2.2.6// -|=includeInnodbStatusInDeadlockExceptions|add "SHOW ENGINE INNODB STATUS" result to exception trace when having a deadlock exception\\//Default: false. Since 2.3.0// -|=includeThreadDumpInDeadlockExceptions|add thread dump to exception trace when having a deadlock exception\\//Default: false. Since 2.3.0// -|=blankTableNameMeta|Result-set metadata getTableName always return blank. This option is mainly for ORACLE db compatibility\\//Default: false. Since 2.4.3// - -\\\\ -== Failover/High availability URL parameters - -|=autoReconnect|With basic failover: if true, will attempt to recreate connection after a failover. \\With standard failover: if true, will attempt to recreate connection even if there is a temporary solution (like using a master connection temporary until reconnect to a slave connection) \\//Default is false. Since 1.1.7//| -|=retriesAllDown|When searching a valid host, maximum number of connection attempts before throwing an exception.\\//Default: 120 seconds. Since 1.2.0//| -|=failoverLoopRetries|When searching silently for a valid host, maximum number of connection attempts.\\This differs from the "retriesAllDown" parameter because this silent search is for example used after a disconnection of a slave connection when using the master connection\\//Default: 120. Since 1.2.0//| -|=validConnectionTimeout|With multiple hosts, after this time in seconds has elapsed, verifies that the connections haven’t been lost.\\When 0, no verification will be done. \\//Default:120 seconds. Since 1.2.0//| -|=loadBalanceBlacklistTimeout|When a connection fails, this host will be blacklisted for the "loadBalanceBlacklistTimeout" amount of time.\\When connecting to a host, the driver will try to connect to a host in the list of non-blacklisted hosts and, only if none are found, attempt blacklisted ones.\\This blacklist is shared inside the classloader.\\//Default: 50 seconds. Since 1.2.0//| -|=assureReadOnly|If true, in high availability, and switching to a read-only host, assure that this host is in read-only mode by setting the session to read-only.\\//Default to false. Since 1.3.0//| -|=allowMasterDownConnection|When using master/slave configuration, permit to create connection when master is down. If all masters are down, default connection is then a slave and Connection.isReadOnly() will then return true. \\//Default: false. Since 2.2.0//| -|=galeraAllowedState|Usually, Connection.isValid just send an empty packet to server, and server send a small response to ensure connectivity. When this option is set, connector will ensure server that "wsrep_local_state" correspond to allowed values (separated by comma). example "4,5".\\//Default: empty. Since 2.2.5//| -\\\\ - -= JDBC API implementation notes - -== "LOAD DATA INFILE" -The fastest way to load lots of data is using [[https://mariadb.com/kb/en/mariadb/load-data-infile/|LOAD DATA INFILE]]. \\However, using "LOAD DATA LOCAL INFILE" (ie : loading a file from client) may be a security problem : -* A "man in the middle" proxy server can change the actual file requested from the server so the client will send a local file to this proxy. -* if someone can execute a query from the client, he can have access to any file on the client (according to the rights of the user running the client process). - -A specific option "allowLocalInfile" (default to false) permit to enable functionality on the client side. -The global variable [[server-system-variables#local_infile|local_infile]] can disable LOAD DATA LOCAL INFILE on the server side. - -A non-JDBC method can permit using this kind of query without this security issue: The application has to create an InputStream with the file to load. If MariaDbStatement.setLocalInfileInputStream(InputStream inputStream) is set, the inputStream will be sent to the server, replacing the file content. - -Code example: -{{{ - Statement statement = ... - InputStream in = new FileInputStream("/file.sql"); - - if (statement.isWrapperFor(MariaDbStatement.class)) { - MariaDbStatement mariaDbStatement = statement.unwrap(MariaDbStatement.class); - mariaDbStatement.setLocalInfileInputStream(in); - String sql = "LOAD DATA LOCAL INFILE 'dummyFileName'" - + " INTO TABLE gigantic_load_data_infile " - + " FIELDS TERMINATED BY '\\t' ENCLOSED BY ''" - + " ESCAPED BY '\\\\' LINES TERMINATED BY '\\n'"; - statement.execute(sql); - } else { - in.close(); - throw new RuntimeException("Mariadb JDBC adaptor must be used"); - } - -}}} - -Since 1.5.0, Interceptors can now filter LOAD DATA LOCAL INFILE queries according to filename. - -These interceptors must implement the {{{org.mariadb.jdbc.LocalInfileInterceptor}}} interface. -Interceptors use the [[http://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html|ServiceLoader]] pattern, so interceptors must be defined in the META-INF/services/org.mariadb.jdbc.LocalInfileInterceptor file. - -Example : -create the META-INF/services/org.mariadb.jdbc.LocalInfileInterceptor file with content org.project.LocalInfileInterceptorImpl. -{{{ - public class LocalInfileInterceptorImpl implements LocalInfileInterceptor { - @Override - public boolean validate(String fileName) { - File file = new File(fileName); - String absolutePath = file.getAbsolutePath(); - String filePath = absolutePath.substring(0,absolutePath.lastIndexOf(File.separator)); - return filePath.equals("/var/tmp/exchanges"); - } - } -}}} - -You can avoid defining the META-INF/services file using [[https://github.com/google/auto/tree/master/service|google auto-service]] framework -Using the previous example, just add {{{@AutoService(LocalInfileInterceptor.class)}}}, and your interceptor will be automatically defined. -{{{ - @AutoService(LocalInfileInterceptor.class) - public class LocalInfileInterceptorImpl implements LocalInfileInterceptor { - @Override - public boolean validate(String fileName) { - File file = new File(fileName); - String absolutePath = file.getAbsolutePath(); - String filePath = absolutePath.substring(0,absolutePath.lastIndexOf(File.separator)); - return filePath.equals("/var/tmp/exchanges"); - } - } -}}} - - -== Using pooling - -MariaDB has 2 different Datasource implementation : -* MariaDbDataSource : Basic implementation. A new connection each time method getConnection() is called. -* MariaDbPoolDataSource : Connection pooling implementation (since 2.2.0 version). MariaDB Driver will keep a pool of connection and borrow Connections when asked for it. - -When using MariaDbPoolDataSource, different options permit to indicate how pooling will handle pool. See [[use-mariadb-connector-j-driver.creole#pooling|pooling options]] for detail information of options. - -Example of use : -{{{ - MariaDbPoolDataSource pool = new MariaDbPoolDataSource("jdbc:mariadb://server/db?user=myUser&maxPoolSize=10"); - - try (Connection connection = pool.getConnection()) { - Statement statement = connection.createStatement(); - statement.execute("SELECT * FROM mysql.user"); - } - - try (Connection connection = pool.getConnection()) { - Statement statement = connection.createStatement(); - statement.execute("SELECT * FROM mysql.user"); - } - - pool.close(); -}}} - -Pooling can be configured at connection level using the "pool" option: (The main difference is that there is no accessible object to close pool if needed.) -{{{ - //indicating &pool(=true) indicate that pool will be initialized - String connectionString = "jdbc:mariadb://server/db?user=myUser&maxPoolSize=10&pool"; - try (Connection connection = DriverManager.getConnection(connectionString)) { - try (Statement statement = connection.createStatement()) { - statement.execute("SELECT * FROM mysql.user"); - } - } - - //since reusing the same connection string, connection will be get from pool - try (Connection connection = DriverManager.getConnection(connectionString)) { - try (Statement statement = connection.createStatement()) { - statement.execute("SELECT 'another query'"); - } - } -}}} - - - -==== Pool connection handling : -When a new connection is asked to pool, if there is an existing connection not used, Connection validation is done then borrowed directly. -If no connection is available, the request for a connection will be put in a queue until connection timeout. -When a connection is available (new creation or released to the pool), it will be use to satisfy queued request in FIFO order. - -A dedicated thread will handle new connection creation (one by one) to avoid connection burst. -This thread will create connections until "maxPoolSize" if needed with a minimum connection of "minPoolSize". - -99.99% of the time, connection is created, a few queries are executed, then connection is released. -Creating connection one after another permit to handle sudden peak of connection, to avoid creating lot of connections immediately and drop them after idle timeout: - -EXAMPLE - -Configuration -* minPoolSize=2 -* maxPoolSize=20 - -Pool with no activity (only 2 connection in pool). -* Average query executing : 0.1 milliseconds -* Connection creation : 5 milliseconds - - -If a sudden peak of 50 connections are needed to execute one query, that mean that there will be 2 connection borrowed and released 25 times each. -So all queries will be handled in 2.5 milliseconds, less than one Connection creation. 2.5 milliseconds after that pike of demand, there will be 3 connections in pool. - -If peak continue, then connections in pool will quickly increase. - - -==== Connection close: -On connection closing, borrowed connection state will be reset, then give back to pool. -This reset goal is that next Connection get from pool has the same state as a newly "fresh" created connection. - -Reset operations : -* rollback remaining active transaction -* reuse the configured database if changed -* default connection read-only state to false (master in a masters/slaves configuration) if changed -* re-initialize socket timeout if changed -* autocommit reset to default -* Transaction Isolation if changed - -If server version is >= 10.2.4 (5.7.3 for MySQL server), then option "useResetConnection" can be used. This option will delete all user variables, and reset session variables to their initial state. -\\ - -==== Idle timeout Thread -An additional thread will periodically close idle connections not used for a time corresponding to option "maxIdleTime". -Pool will ensure to recreate connection to satisfy the option "minPoolSize" value. - -This avoids keeping unused connection in pool, overloading server uselessly. -If option "staticGlobal" is set, driver will ensure that option "maxIdleTime" is less than server @@wait_timeout. - -\\ - -==== Connection performance boost. -Driver has the advantage to know current server state, permitting fast pooling : -When creating a connection, java driver need to execute 1 or 2 additional query after socket initialization / ssl initialization. -If the following variables don't change (rarely changed) : -* @@max_allowed_packet -* @@wait_timeout -* @@autocommit -* @@auto_increment_increment -* @@time_zone -* @@system_time_zone -* @@tx_isolation -, using the option "staticGlobal", those value will be kept in memory, avoiding any additional queries when establishing a new connection (connection creation can be 30% faster, depending on network) - -Statement.cancel, Connection.abort() methods using pool are super fast, because of reusing a connection from pool. - -Each time a connection is asked, pool validate the connection exchanging an empty MySQL packet with the server to ensure connection state. But pool reuse connection intensively, so this validation is done only if Connection has not been use since some time (option "poolValidMinDelay" with the default value of 1000ms). -\\ - -==== JMX -JMX give some information. MBeans name are like "org.mariadb.jdbc.pool:type=*". - -Some statistics of current pool : -* long getActiveConnections(); -> indicate current used connection -* long getTotalConnections(); -> indicate current number of connections in pool -* long getIdleConnections(); -> indicate the number of connection currently not used -* long getConnectionRequests(); -> indicate threads number that wait for a connection. - -One method to reset "staticGlobal" value : - void resetStaticGlobal(); -> method to reset staticGlobal values in memory. - -Example accessing JMX through java : -{{{ -try (MariaDbPoolDataSource pool = new MariaDbPoolDataSource(connUri + "jdbc:mariadb://localhost/testj?user=root&maxPoolSize=5&minPoolSize=3&poolName=PoolTestJmx")) { - - try (Connection connection = pool.getConnection()) { - - MBeanServer server = ManagementFactory.getPlatformMBeanServer(); - ObjectName filter = new ObjectName("org.mariadb.jdbc.pool:type=PoolTest*"); - Set objectNames = server.queryNames(filter, null); - ObjectName name = objectNames.iterator().next(); - - System.out.println(server.getAttribute(name, "ActiveConnections")); //1 - System.out.println(server.getAttribute(name, "TotalConnections")); //3 - System.out.println(server.getAttribute(name, "IdleConnections")); //2 - System.out.println(server.getAttribute(name, "ConnectionRequests")); //0 - } -} -}}} - -\\ - - -== Streaming result sets -By default, {{{Statement.executeQuery()}}} will read the full result set from the server. -With large result sets, this will require large amounts of memory. \\ - -To avoid using too much memory, a better behaviour is using Statement.setFetchSize(int numberOfRowInMemory) to indicate the number of row that will be store on memory\\ -\\ -Example :\\ -Using {{{Statement.setFetchSize(1000) }}} indicate that 1000 rows will be stored in memory.\\ -So, when query execute, 1000 rows will be in memory. After 1000 {{{ResultSet.next()}}}, next 1000 rows will be stored in memory, and so on. - -Note that server usually expects client to read off the result set relatively fast. Server variable "net_write_timeout" controls this behavior (default to 60s). -If you doesn't expect results to be handled in this amount of time there is different possibility : -* if your server version > 10.1.2, you can use the query "SET STATEMENT net_write_timeout=10000 FOR XXX" with XXX your "normal" query. This will indicate that specifically for this query, net_write_timeout will be set to a longer time (10000 in this example). -* for older servers, a specific query will have to set net_write_timeout temporary ("SET STATEMENT net_write_timeout=..."), and set it back afterwards. -* if your application usually use a lot of long queries with fetch size, Connection can be set using option "sessionVariables=net_write_timeout=xxx" - -Even using setFetchSize, Server will send all results to client. Sending another query on the same connection will throw an exception until all results aren't read - -== Prepared statements -The driver uses server prepared statements as a standard to communicate with the database (since 1.3.0). If the "rewriteBatchedStatements" options are set to true, the driver will only use text protocol. Prepared statements (parameter substitution) is handled by the driver, on the client side. - -== CallableStatement -Callable statement implementation won't need to access stored procedure -metadata ([[https://mariadb.com/kb/en/mariadb/mysqlproc-table/|mysql.proc]]) table if both of following are true - -* CallableStatement.getMetadata() is not used -* Parameters are accessed by index, not by name - -When possible, following the two rules above provides both better speed and -eliminates concerns about SELECT privileges on the -[[https://mariadb.com/kb/en/mariadb/mysqlproc-table/|mysql.proc]] table. - -== Optional JDBC classes -The following optional interfaces are implemented by the -org.mariadb.jdbc.MariaDbDataSource class : javax.sql.DataSource, -javax.sql.ConnectionPoolDataSource, javax.sql.XADataSource - -careful : org.mariadb.jdbc.MySQLDataSource doesn't exist anymore and should be replaced with org.mariadb.jdbc.MariaDbDataSource since v1.3.0 - -== Usage examples - -The following code provides a basic example of how to connect to a MariaDB or -MySQL server and create a table. - -=== Creating a table on a MariaDB or MySQL server -{{{ -try (Connection connection = DriverManager.getConnection("jdbc:mariadb://localhost:3306/test", "username", "password")) { - try (Statement stmt = connection.createStatement()) { - stmt.executeUpdate("CREATE TABLE a (id int not null primary key, value varchar(20))"); - } -} -}}} diff --git a/maven-build.xml b/maven-build.xml index 907a00b9a..30c2d9f25 100644 --- a/maven-build.xml +++ b/maven-build.xml @@ -100,17 +100,17 @@ + location="${maven.repo.local}/commons-pool/commons-pool/1.5.4/commons-pool-1.5.4.jar"/> + location="${maven.repo.local}/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar"/> + location="${maven.repo.local}/commons-pool/commons-pool/1.5.4/commons-pool-1.5.4.jar"/> @@ -306,68 +306,68 @@ + src="https://oss.sonatype.org/content/repositories/snapshots/junit/junit/4.12/junit-4.12.jar" + dest="${maven.repo.local}/junit/junit/4.12/junit-4.12.jar" + usetimestamp="false" + ignoreerrors="true"/> + src="https://oss.sonatype.org/content/repositories/snapshots/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar" + dest="${maven.repo.local}/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar" + usetimestamp="false" + ignoreerrors="true"/> + src="https://repo.maven.apache.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar" + dest="${maven.repo.local}/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar" + usetimestamp="false" + ignoreerrors="true"/> + src="https://oss.sonatype.org/content/repositories/snapshots/net/java/dev/jna/jna/3.3.0/jna-3.3.0.jar" + dest="${maven.repo.local}/net/java/dev/jna/jna/3.3.0/jna-3.3.0.jar" + usetimestamp="false" + ignoreerrors="true"/> + src="https://oss.sonatype.org/content/repositories/snapshots/net/java/dev/jna/jna/3.3.0/jna-3.3.0-platform.jar" + dest="${maven.repo.local}/net/java/dev/jna/jna/3.3.0/jna-3.3.0-platform.jar" + usetimestamp="false" + ignoreerrors="true"/> + src="https://repo.maven.apache.org/maven2/net/java/dev/jna/jna/3.3.0/jna-3.3.0-platform.jar" + dest="${maven.repo.local}/net/java/dev/jna/jna/3.3.0/jna-3.3.0-platform.jar" + usetimestamp="false" + ignoreerrors="true"/> + src="https://oss.sonatype.org/content/repositories/snapshots/commons-dbcp/commons-dbcp/1.4/commons-dbcp-1.4.jar" + dest="${maven.repo.local}/commons-dbcp/commons-dbcp/1.4/commons-dbcp-1.4.jar" + usetimestamp="false" + ignoreerrors="true"/> + src="https://repo.maven.apache.org/maven2/commons-dbcp/commons-dbcp/1.4/commons-dbcp-1.4.jar" + dest="${maven.repo.local}/commons-dbcp/commons-dbcp/1.4/commons-dbcp-1.4.jar" + usetimestamp="false" + ignoreerrors="true"/> + src="https://oss.sonatype.org/content/repositories/snapshots/commons-pool/commons-pool/1.5.4/commons-pool-1.5.4.jar" + dest="${maven.repo.local}/commons-pool/commons-pool/1.5.4/commons-pool-1.5.4.jar" + usetimestamp="false" + ignoreerrors="true"/> + src="https://repo.maven.apache.org/maven2/commons-pool/commons-pool/1.5.4/commons-pool-1.5.4.jar" + dest="${maven.repo.local}/commons-pool/commons-pool/1.5.4/commons-pool-1.5.4.jar" + usetimestamp="false" + ignoreerrors="true"/> diff --git a/pom.xml b/pom.xml index 4861e0e57..55b0be9e6 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ @@ -60,15 +35,19 @@ mariadb-java-client jar mariadb-java-client - 2.6.1-SNAPSHOT + 3.0.0-SNAPSHOT JDBC driver for MariaDB and MySQL https://mariadb.com/kb/en/mariadb/about-mariadb-connector-j/ UTF-8 - 5.5.0 + 1.23 + 5.6.2 + 3.15.0 6.0.0 5.0.0 + UTF-8 + 1.2.3 @@ -109,6 +88,29 @@ + + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + + com.amazonaws + aws-java-sdk-bom + 1.11.788 + pom + import + + + + + + @@ -117,6 +119,11 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + org.jacoco jacoco-maven-plugin @@ -138,62 +145,29 @@ maven-compiler-plugin - 3.6.0 + 3.8.1 1.8 1.8 + true - -Xlint:all,-options,-path + -Xlint:all,-options,-path,-processing - org.apache.maven.plugins maven-javadoc-plugin 3.1.1 8 + + org.mariadb.jdbc.client,org.mariadb.jdbc.codec,org.mariadb.jdbc.message,org.mariadb.jdbc.util + attach-javadocs - prepare-package - - jar - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.2.0 - - - src/main/resources/META-INF/MANIFEST.MF - - ${project.version} - 2 - mariadb-java-client - org.mariadb.jdbc - org.mariadb.jdbc - org.mariadb.jdbc - - org.osgi.framework,javax.naming,javax.management,javax.sql,javax.net;resolution:=optional,javax.net.ssl;resolution:=optional,javax.transaction.xa;resolution:=optional,waffle.windows.auth;resolution:=optional,waffle.windows.auth.impl;resolution:=optional - - org.mariadb.jdbc.internal.osgi.MariaDbActivator - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - jar @@ -216,8 +190,6 @@ false - - org.apache.maven.plugins maven-gpg-plugin @@ -250,50 +222,39 @@ com.github.waffle waffle-jna - 2.2.1 + 1.9.1 true - - junit - junit - 4.13 + org.assertj + assertj-core + ${assertj.version} test - net.java.dev.jna - jna - ${jna.version} - true - - - net.java.dev.jna - jna-platform - ${jna.version} - true - - - org.slf4j - slf4j-api - [1.4.0,1.7.25] - true + org.junit.jupiter + junit-jupiter-engine + test ch.qos.logback logback-classic - 1.2.3 + ${logback.version} test - true - - - - com.amazonaws - aws-java-sdk-rds - 1.11.734 - true - + + + + + + + + + + + + org.osgi org.osgi.core @@ -306,5 +267,109 @@ ${osgi.compendium.version} provided + + + + + + + com.amazonaws + aws-java-sdk-rds + true + + + + + + default + + true + + + + bench + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + + mysql + mysql-connector-java + [8.0.20,) + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + add-source + generate-sources + + add-source + + + + src/benchmark/java + + + + + add-resource + generate-resources + + add-resource + + + + + src/benchmark/resources + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + benchmarks + + + org.openjdk.jmh.Main + + + + + + + + + + + diff --git a/src/benchmark/README.md b/src/benchmark/README.md new file mode 100644 index 000000000..667c3f2e3 --- /dev/null +++ b/src/benchmark/README.md @@ -0,0 +1,32 @@ +

+ +# Benchmark + +How to run : +```script +mvn clean package -P bench -Dmaven.test.skip + +# run all benchmarks +java -Duser.country=US -Duser.language=en -jar target/benchmarks.jar + +# run a specific benchmark +java -Duser.country=US -Duser.language=en -jar target/benchmarks.jar "Select_1_user" +``` + +Configuration by system properties : +* TEST_HOST: localhost +* TEST_PORT: 3306 +* TEST_USERNAME: root +* TEST_PASSWORD: "" +* TEST_DATABASE: "testj" + +example: +```script +mvn clean package -P bench -Dmaven.test.skip +java -DTEST_PORT=3307 -Duser.country=US -Duser.language=en -jar target/benchmarks.jar "Select_1_user" +``` + diff --git a/src/benchmark/java/org/mariadb/jdbc/Common.java b/src/benchmark/java/org/mariadb/jdbc/Common.java new file mode 100644 index 000000000..d40b12af6 --- /dev/null +++ b/src/benchmark/java/org/mariadb/jdbc/Common.java @@ -0,0 +1,98 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +import org.openjdk.jmh.annotations.*; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@Warmup(iterations = 10, timeUnit = TimeUnit.SECONDS, time = 1) +@Measurement(iterations = 10, timeUnit = TimeUnit.SECONDS, time = 1) +@Fork(value = 5) +@Threads(value = -1) // detecting CPU count +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +public class Common { + + @State(Scope.Thread) + public static class MyState { + + // conf + public final String host = System.getProperty("TEST_HOST", "localhost"); + public final int port = Integer.parseInt(System.getProperty("TEST_PORT", "3306")); + public final String username = System.getProperty("TEST_USERNAME", "root"); + public final String password = System.getProperty("TEST_PASSWORD", ""); + public final String database = System.getProperty("TEST_DATABASE", "testj"); + // connections + protected Connection connectionText; + protected Connection connectionBinary; + + @Param({"mysql", "mariadb"}) + String driver; + + @Setup(Level.Trial) + public void doSetup() throws Exception { + + String className; + switch (driver) { + case "mysql": + className = "com.mysql.cj.jdbc.Driver"; + break; + case "mariadb": + className = "org.mariadb.jdbc.Driver"; + break; + default: + throw new RuntimeException("wrong param"); + } + try { + String jdbcBase = "%s:%s/%s?user=%s&password=%s&sslMode=DISABLED&useServerPrepStmts=%s&cachePrepStmts=%s&serverTimezone=UTC"; + String jdbcUrlText = + String.format( + jdbcBase, + host, port, database, username, password, false, false); + String jdbcUrlBinary = + String.format( + jdbcBase, + host, port, database, username, password, true, true); + connectionText = + ((java.sql.Driver) Class.forName(className).getDeclaredConstructor().newInstance()) + .connect("jdbc:" + driver + "://" + jdbcUrlText, new Properties()); + connectionBinary = + ((java.sql.Driver) Class.forName(className).getDeclaredConstructor().newInstance()) + .connect("jdbc:" + driver + "://" + jdbcUrlBinary, new Properties()); + } catch (SQLException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @TearDown(Level.Trial) + public void doTearDown() throws SQLException { + connectionText.close(); + connectionBinary.close(); + } + } +} diff --git a/src/benchmark/java/org/mariadb/jdbc/Select_1.java b/src/benchmark/java/org/mariadb/jdbc/Select_1.java new file mode 100644 index 000000000..7e911f3fd --- /dev/null +++ b/src/benchmark/java/org/mariadb/jdbc/Select_1.java @@ -0,0 +1,40 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +import org.openjdk.jmh.annotations.Benchmark; + +import java.sql.ResultSet; +import java.sql.Statement; + +public class Select_1 extends Common { + + @Benchmark + public int run(MyState state) throws Throwable { + try (Statement st = state.connectionText.createStatement()) { + int rnd = (int) (Math.random() * 1000); + ResultSet rs = st.executeQuery("select " + rnd); + rs.next(); + return rs.getInt(1); + } + } +} diff --git a/src/benchmark/java/org/mariadb/jdbc/Select_10000_Rows.java b/src/benchmark/java/org/mariadb/jdbc/Select_10000_Rows.java new file mode 100644 index 000000000..83a16a2c9 --- /dev/null +++ b/src/benchmark/java/org/mariadb/jdbc/Select_10000_Rows.java @@ -0,0 +1,58 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +import org.openjdk.jmh.annotations.Benchmark; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +public class Select_10000_Rows extends Common { + private static final String sql = + "SELECT lpad(conv(floor(rand()*pow(36,8)), 10, 36), 8, 0) as rnd_str_8 FROM seq_1_to_10000"; + + + @Benchmark + public String[] text(MyState state) throws Throwable { + return run(state.connectionText); + } + + + @Benchmark + public String[] binary(MyState state) throws Throwable { + return run(state.connectionBinary); + } + + private String[] run(Connection con) throws Throwable { + try (PreparedStatement st = con.prepareStatement(sql)) { + + ResultSet rs = st.executeQuery(); + String[] res = new String[10000]; + int i = 0; + while (rs.next()) { + res[i++] = rs.getString(1); + } + return res; + } + } +} diff --git a/src/benchmark/java/org/mariadb/jdbc/Select_1000_params.java b/src/benchmark/java/org/mariadb/jdbc/Select_1000_params.java new file mode 100644 index 000000000..5fc0b246d --- /dev/null +++ b/src/benchmark/java/org/mariadb/jdbc/Select_1000_params.java @@ -0,0 +1,74 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +import org.openjdk.jmh.annotations.Benchmark; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +public class Select_1000_params extends Common { + + private static final String sql; + + static { + StringBuilder sb = new StringBuilder("select ?"); + for (int i = 1; i < 1000; i++) { + sb.append(",?"); + } + sql = sb.toString(); + } + + private static int[] randParams() { + int[] rnds = new int[1000]; + for (int i = 0; i < 1000; i++) { + rnds[i] = (int) (Math.random() * 1000); + } + return rnds; + } + + @Benchmark + public Integer text(MyState state) throws Throwable { + return run(state.connectionText); + } + + @Benchmark + public Integer binary(MyState state) throws Throwable { + return run(state.connectionBinary); + } + + private Integer run(Connection con) throws Throwable { + int[] rnds = randParams(); + try (PreparedStatement st = con.prepareStatement(sql)) { + for (int i = 1; i <= 1000; i++) { + st.setInt(i, rnds[i - 1]); + } + ResultSet rs = st.executeQuery(); + rs.next(); + for (int i = 1; i <= 1000; i++) { + if (rnds[i - 1] != rs.getInt(i)) throw new IllegalStateException("ERROR"); + } + return rs.getInt(1); + } + } +} diff --git a/src/benchmark/java/org/mariadb/jdbc/Select_1_user.java b/src/benchmark/java/org/mariadb/jdbc/Select_1_user.java new file mode 100644 index 000000000..2df955064 --- /dev/null +++ b/src/benchmark/java/org/mariadb/jdbc/Select_1_user.java @@ -0,0 +1,55 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +import org.openjdk.jmh.annotations.Benchmark; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + +public class Select_1_user extends Common { + + @Benchmark + public Object[] text(MyState state) throws Throwable { + return run(state.connectionText); + } + + + @Benchmark + public Object[] binary(MyState state) throws Throwable { + return run(state.connectionBinary); + } + + private Object[] run(Connection con) throws Throwable { + final int numberOfUserCol = 46; + try (Statement st = con.createStatement()) { + ResultSet rs = st.executeQuery("select * FROM mysql.user LIMIT 1"); + rs.next(); + Object[] objs = new Object[numberOfUserCol]; + for (int i = 0; i < numberOfUserCol; i++) { + objs[i] = rs.getObject(i + 1); + } + return objs; + } + } +} diff --git a/src/benchmark/resources/logback-test.xml b/src/benchmark/resources/logback-test.xml new file mode 100644 index 000000000..4b5b5c558 --- /dev/null +++ b/src/benchmark/resources/logback-test.xml @@ -0,0 +1,34 @@ + + + + + + + + %d{yyyy-MM-dd} | %d{HH:mm:ss.SSS} | %-20.20thread | %5p | %logger{25} | %m%n + + + + + + + + + + + + diff --git a/src/main/java/org/mariadb/jdbc/BaseCallableStatement.java b/src/main/java/org/mariadb/jdbc/BaseCallableStatement.java new file mode 100644 index 000000000..d3e1ded28 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/BaseCallableStatement.java @@ -0,0 +1,2796 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.*; +import java.sql.Date; +import java.sql.Statement; +import java.util.*; +import java.util.concurrent.locks.ReentrantLock; +import org.mariadb.jdbc.client.result.Result; +import org.mariadb.jdbc.codec.Parameter; +import org.mariadb.jdbc.util.NativeSql; + +public abstract class BaseCallableStatement extends ServerPreparedStatement + implements CallableStatement { + protected final String sql; + protected final String databaseName; + protected final String procedureName; + protected CallableParameterMetaData parameterMetaData = null; + protected Set outputParameters = new HashSet<>(); + protected Result outputResult = null; + + public BaseCallableStatement( + String sql, + Connection con, + ReentrantLock lock, + String databaseName, + String procedureName, + boolean canUseServerTimeout, + boolean canUseServerMaxRows, + int resultSetType, + int resultSetConcurrency, + int defaultFetchSize) + throws SQLException { + super( + sql, + con, + lock, + canUseServerTimeout, + canUseServerMaxRows, + Statement.RETURN_GENERATED_KEYS, + resultSetType, + resultSetConcurrency, + defaultFetchSize); + this.sql = sql; + this.databaseName = databaseName; + this.procedureName = procedureName; + } + + public abstract boolean isFunction(); + + /** + * Registers the OUT parameter in ordinal position parameterIndex to the JDBC type + * sqlType. All OUT parameters must be registered before a stored procedure is + * executed. + * + *

The JDBC type specified by sqlType for an OUT parameter determines the Java + * type that must be used in the get method to read the value of that parameter. + * + *

If the JDBC type expected to be returned to this output parameter is specific to this + * particular database, sqlType should be java.sql.Types.OTHER. The + * method {@link #getObject} retrieves the value. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @param sqlType the JDBC type code defined by java.sql.Types. If the parameter is + * of JDBC type NUMERIC or DECIMAL, the version of + * registerOutParameter that accepts a scale value should be used. + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if sqlType is a ARRAY, + * BLOB, CLOB, DATALINK, JAVA_OBJECT, + * NCHAR, NCLOB, NVARCHAR, LONGNVARCHAR, + * REF, ROWID, SQLXML or STRUCT data type and + * the JDBC driver does not support this data type + * @see Types + */ + @Override + public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException { + checkIndex(parameterIndex); + outputParameters.add(parameterIndex); + parameters.set(parameterIndex - 1, Parameter.NULL_PARAMETER); + } + + private void checkIndex(int index) throws SQLException { + if (index <= 0 + || (prepareResult != null + && index > (prepareResult.getParameters().length + (isFunction() ? 1 : 0))) + || (prepareResult == null + && parameterMetaData != null + && index > parameterMetaData.getParameterCount())) { + throw exceptionFactory().create(String.format("wrong parameter index %s", index)); + } + } + + /** + * Registers the parameter in ordinal position parameterIndex to be of JDBC type + * sqlType. All OUT parameters must be registered before a stored procedure is + * executed. + * + *

The JDBC type specified by sqlType for an OUT parameter determines the Java + * type that must be used in the get method to read the value of that parameter. + * + *

This version of registerOutParameter should be used when the parameter is of + * JDBC type NUMERIC or DECIMAL. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @param sqlType the SQL type code defined by java.sql.Types. + * @param scale the desired number of digits to the right of the decimal point. It must be greater + * than or equal to zero. + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if sqlType is a ARRAY, + * BLOB, CLOB, DATALINK, JAVA_OBJECT, + * NCHAR, NCLOB, NVARCHAR, LONGNVARCHAR, + * REF, ROWID, SQLXML or STRUCT data type and + * the JDBC driver does not support this data type + * @see Types + */ + @Override + public void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException { + registerOutParameter(parameterIndex, sqlType); + } + + /** + * Retrieves whether the last OUT parameter read had the value of SQL NULL. Note that + * this method should be called only after calling a getter method; otherwise, there is no value + * to use in determining whether it is null or not. + * + * @return true if the last parameter read was SQL NULL; false + * otherwise + * @throws SQLException if a database access error occurs or this method is called on a closed + * CallableStatement + */ + @Override + public boolean wasNull() throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.wasNull(); + } + + private int idxToOutIdx(int idx) throws SQLException { + int outputIndex = 1; + if (idx < 1) throw exceptionFactory().create(String.format("wrong index %s", idx)); + if (!outputParameters.contains(idx)) + throw exceptionFactory().create(String.format("index %s not declared as output", idx)); + for (int i = 1; i < idx; i++) { + if (outputParameters.contains(i)) outputIndex++; + } + + return outputIndex; + } + + /** + * Check if statement is closed, and throw exception if so. + * + * @throws SQLException if statement close + */ + protected void checkOutputResult() throws SQLException { + if (outputResult == null) { + throw exceptionFactory().create("No output result"); + } + } + + /** + * Retrieves the value of the designated JDBC CHAR, VARCHAR, or + * LONGVARCHAR parameter as a String in the Java programming language. + * + *

For the fixed-length type JDBC CHAR, the String object returned + * has exactly the same value the SQL CHAR value had in the database, including any + * padding added by the database. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setString + */ + @Override + public String getString(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getString(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC BIT or BOOLEAN parameter + * as a boolean in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value. If the value is SQL NULL, the result is false + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setBoolean + */ + @Override + public boolean getBoolean(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getBoolean(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC TINYINT parameter as a byte + * in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value. If the value is SQL NULL, the result is 0 + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setByte + */ + @Override + public byte getByte(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getByte(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC SMALLINT parameter as a short + * in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value. If the value is SQL NULL, the result is 0 + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setShort + */ + @Override + public short getShort(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getShort(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC INTEGER parameter as an int + * in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value. If the value is SQL NULL, the result is 0 + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setInt + */ + @Override + public int getInt(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getInt(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC BIGINT parameter as a long + * in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value. If the value is SQL NULL, the result is 0 + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setLong + */ + @Override + public long getLong(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getLong(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC FLOAT parameter as a float + * in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value. If the value is SQL NULL, the result is 0 + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setFloat + */ + @Override + public float getFloat(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getFloat(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC DOUBLE parameter as a double + * in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value. If the value is SQL NULL, the result is 0 + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setDouble + */ + @Override + public double getDouble(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getDouble(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC NUMERIC parameter as a + * java.math.BigDecimal object with scale digits to the right of the decimal point. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @param scale the number of digits to the right of the decimal point + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setBigDecimal + * @deprecated use getBigDecimal(int parameterIndex) or + * getBigDecimal(String parameterName) + */ + @Override + @Deprecated + public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getBigDecimal(idxToOutIdx(parameterIndex), scale); + } + + /** + * Retrieves the value of the designated JDBC BINARY or VARBINARY + * parameter as an array of byte values in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setBytes + */ + @Override + public byte[] getBytes(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getBytes(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC DATE parameter as a java.sql.Date + * object. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setDate + */ + @Override + public Date getDate(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getDate(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC TIME parameter as a java.sql.Time + * object. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setTime + */ + @Override + public Time getTime(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getTime(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC TIMESTAMP parameter as a + * java.sql.Timestamp object. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setTimestamp + */ + @Override + public Timestamp getTimestamp(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getTimestamp(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated parameter as an Object in the Java + * programming language. If the value is an SQL NULL, the driver returns a Java + * null. + * + *

This method returns a Java object whose type corresponds to the JDBC type that was + * registered for this parameter using the method registerOutParameter. By + * registering the target JDBC type as java.sql.Types.OTHER, this method can be used + * to read database-specific abstract data types. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return A java.lang.Object holding the OUT parameter value + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see Types + * @see #setObject + */ + @Override + public Object getObject(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getObject(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC NUMERIC parameter as a + * java.math.BigDecimal object with as many digits to the right of the decimal point as the + * value contains. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value in full precision. If the value is SQL NULL, the + * result is null. + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setBigDecimal + * @since 1.2 + */ + @Override + public BigDecimal getBigDecimal(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getBigDecimal(idxToOutIdx(parameterIndex)); + } + + /** + * Returns an object representing the value of OUT parameter parameterIndex and uses + * map for the custom mapping of the parameter value. + * + *

This method returns a Java object whose type corresponds to the JDBC type that was + * registered for this parameter using the method registerOutParameter. By + * registering the target JDBC type as java.sql.Types.OTHER, this method can be used + * to read database-specific abstract data types. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @param map the mapping from SQL type names to Java classes + * @return a java.lang.Object holding the OUT parameter value + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setObject + * @since 1.2 + */ + @Override + public Object getObject(int parameterIndex, Map> map) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getObject(idxToOutIdx(parameterIndex), map); + } + + /** + * Retrieves the value of the designated JDBC REF(<structured-type>) parameter + * as a {@link Ref} object in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value as a Ref object in the Java programming language. If + * the value was SQL NULL, the value null is returned. + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.2 + */ + @Override + public Ref getRef(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getRef(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC BLOB parameter as a {@link Blob} object + * in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value as a Blob object in the Java programming language. If + * the value was SQL NULL, the value null is returned. + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.2 + */ + @Override + public Blob getBlob(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getBlob(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC CLOB parameter as a java.sql.Clob + * object in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value as a Clob object in the Java programming language. If + * the value was SQL NULL, the value null is returned. + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.2 + */ + @Override + public Clob getClob(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getClob(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC ARRAY parameter as an {@link Array} + * object in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value as an Array object in the Java programming language. + * If the value was SQL NULL, the value null is returned. + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.2 + */ + @Override + public Array getArray(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getArray(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC DATE parameter as a java.sql.Date + * object, using the given Calendar object to construct the date. With a + * Calendar object, the driver can calculate the date taking into account a custom + * timezone and locale. If no Calendar object is specified, the driver uses the + * default timezone and locale. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @param cal the Calendar object the driver will use to construct the date + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setDate + * @since 1.2 + */ + @Override + public Date getDate(int parameterIndex, Calendar cal) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getDate(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC TIME parameter as a java.sql.Time + * object, using the given Calendar object to construct the time. With a + * Calendar object, the driver can calculate the time taking into account a custom + * timezone and locale. If no Calendar object is specified, the driver uses the + * default timezone and locale. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @param cal the Calendar object the driver will use to construct the time + * @return the parameter value; if the value is SQL NULL, the result is null + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setTime + * @since 1.2 + */ + @Override + public Time getTime(int parameterIndex, Calendar cal) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getTime(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated JDBC TIMESTAMP parameter as a + * java.sql.Timestamp object, using the given Calendar object to construct the + * Timestamp object. With a Calendar object, the driver can calculate + * the timestamp taking into account a custom timezone and locale. If no Calendar + * object is specified, the driver uses the default timezone and locale. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @param cal the Calendar object the driver will use to construct the timestamp + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @see #setTimestamp + * @since 1.2 + */ + @Override + public Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getTimestamp(idxToOutIdx(parameterIndex)); + } + + /** + * Registers the designated output parameter. This version of the method + * registerOutParameter should be used for a user-defined or REF output + * parameter. Examples of user-defined types include: STRUCT, DISTINCT, + * JAVA_OBJECT, and named array types. + * + *

All OUT parameters must be registered before a stored procedure is executed. + * + *

For a user-defined parameter, the fully-qualified SQL type name of the parameter should also + * be given, while a REF parameter requires that the fully-qualified type name of the + * referenced type be given. A JDBC driver that does not need the type code and type name + * information may ignore it. To be portable, however, applications should always provide these + * values for user-defined and REF parameters. + * + *

Although it is intended for user-defined and REF parameters, this method may be + * used to register a parameter of any JDBC type. If the parameter does not have a user-defined or + * REF type, the typeName parameter is ignored. + * + *

Note: When reading the value of an out parameter, you must use the getter method + * whose Java type corresponds to the parameter's registered SQL type. + * + * @param parameterIndex the first parameter is 1, the second is 2,... + * @param sqlType a value from {@link Types} + * @param typeName the fully-qualified name of an SQL structured type + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if sqlType is a ARRAY, + * BLOB, CLOB, DATALINK, JAVA_OBJECT, + * NCHAR, NCLOB, NVARCHAR, LONGNVARCHAR, + * REF, ROWID, SQLXML or STRUCT data type and + * the JDBC driver does not support this data type + * @see Types + * @since 1.2 + */ + @Override + public void registerOutParameter(int parameterIndex, int sqlType, String typeName) + throws SQLException { + registerOutParameter(parameterIndex, sqlType); + } + + /** + * Registers the OUT parameter named parameterName to the JDBC type sqlType + * . All OUT parameters must be registered before a stored procedure is executed. + * + *

The JDBC type specified by sqlType for an OUT parameter determines the Java + * type that must be used in the get method to read the value of that parameter. + * + *

If the JDBC type expected to be returned to this output parameter is specific to this + * particular database, sqlType should be java.sql.Types.OTHER. The + * method {@link #getObject} retrieves the value. + * + * @param parameterName the name of the parameter + * @param sqlType the JDBC type code defined by java.sql.Types. If the parameter is + * of JDBC type NUMERIC or DECIMAL, the version of + * registerOutParameter that accepts a scale value should be used. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if sqlType is a ARRAY, + * BLOB, CLOB, DATALINK, JAVA_OBJECT, + * NCHAR, NCLOB, NVARCHAR, LONGNVARCHAR, + * REF, ROWID, SQLXML or STRUCT data type and + * the JDBC driver does not support this data type or if the JDBC driver does not support this + * method + * @see Types + * @since 1.4 + */ + @Override + public void registerOutParameter(String parameterName, int sqlType) throws SQLException { + checkNotClosed(); + registerOutParameter(nameToIndex(parameterName), sqlType); + } + + private int nameToIndex(String parameterName) throws SQLException { + if (parameterName == null) throw exceptionFactory().create("parameterName cannot be null"); + if (parameterMetaData == null) parameterMetaData = getParameterMetaData(); + + int count = parameterMetaData.getParameterCount(); + for (int i = 1; i <= count; i++) { + String name = parameterMetaData.getParameterName(i); + if (name != null && name.equalsIgnoreCase(parameterName)) { + return i; + } + } + throw exceptionFactory().create(String.format("parameterName %s not found", parameterName)); + } + + /** + * Registers the parameter named parameterName to be of JDBC type sqlType + * . All OUT parameters must be registered before a stored procedure is executed. + * + *

The JDBC type specified by sqlType for an OUT parameter determines the Java + * type that must be used in the get method to read the value of that parameter. + * + *

This version of registerOutParameter should be used when the parameter is of + * JDBC type NUMERIC or DECIMAL. + * + * @param parameterName the name of the parameter + * @param sqlType SQL type code defined by java.sql.Types. + * @param scale the desired number of digits to the right of the decimal point. It must be greater + * than or equal to zero. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if sqlType is a ARRAY, + * BLOB, CLOB, DATALINK, JAVA_OBJECT, + * NCHAR, NCLOB, NVARCHAR, LONGNVARCHAR, + * REF, ROWID, SQLXML or STRUCT data type and + * the JDBC driver does not support this data type or if the JDBC driver does not support this + * method + * @see Types + * @since 1.4 + */ + @Override + public void registerOutParameter(String parameterName, int sqlType, int scale) + throws SQLException { + registerOutParameter(parameterName, sqlType); + } + + /** + * Registers the designated output parameter. This version of the method + * registerOutParameter should be used for a user-named or REF output parameter. Examples + * of user-named types include: STRUCT, DISTINCT, JAVA_OBJECT, and named array types. + * + *

All OUT parameters must be registered before a stored procedure is executed. + * + *

For a user-named parameter the fully-qualified SQL type name of the parameter should also be + * given, while a REF parameter requires that the fully-qualified type name of the referenced type + * be given. A JDBC driver that does not need the type code and type name information may ignore + * it. To be portable, however, applications should always provide these values for user-named and + * REF parameters. + * + *

Although it is intended for user-named and REF parameters, this method may be used to + * register a parameter of any JDBC type. If the parameter does not have a user-named or REF type, + * the typeName parameter is ignored. + * + *

Note: When reading the value of an out parameter, you must use the getXXX + * method whose Java type XXX corresponds to the parameter's registered SQL type. + * + * @param parameterName the name of the parameter + * @param sqlType a value from {@link Types} + * @param typeName the fully-qualified name of an SQL structured type + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if sqlType is a ARRAY, + * BLOB, CLOB, DATALINK, JAVA_OBJECT, + * NCHAR, NCLOB, NVARCHAR, LONGNVARCHAR, + * REF, ROWID, SQLXML or STRUCT data type and + * the JDBC driver does not support this data type or if the JDBC driver does not support this + * method + * @see Types + * @since 1.4 + */ + @Override + public void registerOutParameter(String parameterName, int sqlType, String typeName) + throws SQLException { + registerOutParameter(parameterName, sqlType); + } + + /** + * Retrieves the value of the designated JDBC DATALINK parameter as a + * java.net.URL object. + * + * @param parameterIndex the first parameter is 1, the second is 2,... + * @return a java.net.URL object that represents the JDBC DATALINK value + * used as the designated parameter + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs, + * this method is called on a closed CallableStatement, or if the URL being + * returned is not a valid URL on the Java platform + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setURL + * @since 1.4 + */ + @Override + public URL getURL(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getURL(idxToOutIdx(parameterIndex)); + } + + /** + * Sets the designated parameter to the given java.net.URL object. The driver + * converts this to an SQL DATALINK value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param val the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs; this method is called on a closed CallableStatement or if + * a URL is malformed + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getURL + * @since 1.4 + */ + @Override + public void setURL(String parameterName, URL val) throws SQLException { + setURL(nameToIndex(parameterName), val); + } + + /** + * Sets the designated parameter to SQL NULL. + * + *

Note: You must specify the parameter's SQL type. + * + * @param parameterName the name of the parameter + * @param sqlType the SQL type code defined in java.sql.Types + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.4 + */ + @Override + public void setNull(String parameterName, int sqlType) throws SQLException { + setNull(nameToIndex(parameterName), sqlType); + } + + /** + * Sets the designated parameter to the given Java boolean value. The driver converts + * this to an SQL BIT or BOOLEAN value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getBoolean + * @since 1.4 + */ + @Override + public void setBoolean(String parameterName, boolean x) throws SQLException { + setBoolean(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given Java byte value. The driver converts + * this to an SQL TINYINT value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getByte + * @since 1.4 + */ + @Override + public void setByte(String parameterName, byte x) throws SQLException { + setByte(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given Java short value. The driver converts + * this to an SQL SMALLINT value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getShort + * @since 1.4 + */ + @Override + public void setShort(String parameterName, short x) throws SQLException { + setShort(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given Java int value. The driver converts + * this to an SQL INTEGER value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getInt + * @since 1.4 + */ + @Override + public void setInt(String parameterName, int x) throws SQLException { + setInt(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given Java long value. The driver converts + * this to an SQL BIGINT value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getLong + * @since 1.4 + */ + @Override + public void setLong(String parameterName, long x) throws SQLException { + setLong(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given Java float value. The driver converts + * this to an SQL FLOAT value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getFloat + * @since 1.4 + */ + @Override + public void setFloat(String parameterName, float x) throws SQLException { + setFloat(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given Java double value. The driver converts + * this to an SQL DOUBLE value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getDouble + * @since 1.4 + */ + @Override + public void setDouble(String parameterName, double x) throws SQLException { + setDouble(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given java.math.BigDecimal value. The driver + * converts this to an SQL NUMERIC value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getBigDecimal + * @since 1.4 + */ + @Override + public void setBigDecimal(String parameterName, BigDecimal x) throws SQLException { + setBigDecimal(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given Java String value. The driver converts + * this to an SQL VARCHAR or LONGVARCHAR value (depending on the + * argument's size relative to the driver's limits on VARCHAR values) when it sends + * it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getString + * @since 1.4 + */ + @Override + public void setString(String parameterName, String x) throws SQLException { + setString(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given Java array of bytes. The driver converts this to an + * SQL VARBINARY or LONGVARBINARY (depending on the argument's size + * relative to the driver's limits on VARBINARY values) when it sends it to the + * database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getBytes + * @since 1.4 + */ + @Override + public void setBytes(String parameterName, byte[] x) throws SQLException { + setBytes(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given java.sql.Date value using the default + * time zone of the virtual machine that is running the application. The driver converts this to + * an SQL DATE value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getDate + * @since 1.4 + */ + @Override + public void setDate(String parameterName, Date x) throws SQLException { + setDate(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given java.sql.Time value. The driver + * converts this to an SQL TIME value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getTime + * @since 1.4 + */ + @Override + public void setTime(String parameterName, Time x) throws SQLException { + setTime(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given java.sql.Timestamp value. The driver + * converts this to an SQL TIMESTAMP value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getTimestamp + * @since 1.4 + */ + @Override + public void setTimestamp(String parameterName, Timestamp x) throws SQLException { + setTimestamp(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given input stream, which will have the specified number + * of bytes. When a very large ASCII value is input to a LONGVARCHAR parameter, it + * may be more practical to send it via a java.io.InputStream. Data will be read from + * the stream as needed until end-of-file is reached. The JDBC driver will do any necessary + * conversion from ASCII to the database char format. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + * @param parameterName the name of the parameter + * @param x the Java input stream that contains the ASCII parameter value + * @param length the number of bytes in the stream + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.4 + */ + @Override + public void setAsciiStream(String parameterName, InputStream x, int length) throws SQLException { + setAsciiStream(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given input stream, which will have the specified number + * of bytes. When a very large binary value is input to a LONGVARBINARY parameter, it + * may be more practical to send it via a java.io.InputStream object. The data will + * be read from the stream as needed until end-of-file is reached. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + * @param parameterName the name of the parameter + * @param x the java input stream which contains the binary parameter value + * @param length the number of bytes in the stream + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.4 + */ + @Override + public void setBinaryStream(String parameterName, InputStream x, int length) throws SQLException { + setBinaryStream(nameToIndex(parameterName), x, length); + } + + /** + * Sets the value of the designated parameter with the given object. + * + *

The given Java object will be converted to the given targetSqlType before being sent to the + * database. + * + *

If the object has a custom mapping (is of a class implementing the interface SQLData + * ), the JDBC driver should call the method SQLData.writeSQL to write it to + * the SQL data stream. If, on the other hand, the object is of a class implementing Ref + * , Blob, Clob, NClob, Struct, + * java.net.URL, or Array, the driver should pass it to the database as a + * value of the corresponding SQL type. + * + *

Note that this method may be used to pass datatabase- specific abstract data types. + * + * @param parameterName the name of the parameter + * @param x the object containing the input parameter value + * @param targetSqlType the SQL type (as defined in java.sql.Types) to be sent to the database. + * The scale argument may further qualify this type. + * @param scale for java.sql.Types.DECIMAL or java.sql.Types.NUMERIC types, this is the number of + * digits after the decimal point. For all other types, this value will be ignored. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * targetSqlType + * @see Types + * @see #getObject + * @since 1.4 + */ + @Override + public void setObject(String parameterName, Object x, int targetSqlType, int scale) + throws SQLException { + setObject(nameToIndex(parameterName), x); + } + + /** + * Sets the value of the designated parameter with the given object. + * + *

This method is similar to {@link #setObject(String parameterName, Object x, int + * targetSqlType, int scaleOrLength)}, except that it assumes a scale of zero. + * + * @param parameterName the name of the parameter + * @param x the object containing the input parameter value + * @param targetSqlType the SQL type (as defined in java.sql.Types) to be sent to the database + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * targetSqlType + * @see #getObject + * @since 1.4 + */ + @Override + public void setObject(String parameterName, Object x, int targetSqlType) throws SQLException { + setObject(nameToIndex(parameterName), x); + } + + /** + * Sets the value of the designated parameter with the given object. + * + *

The JDBC specification specifies a standard mapping from Java Object types to + * SQL types. The given argument will be converted to the corresponding SQL type before being sent + * to the database. + * + *

Note that this method may be used to pass database- specific abstract data types, by using a + * driver-specific Java type. + * + *

If the object is of a class implementing the interface SQLData, the JDBC driver + * should call the method SQLData.writeSQL to write it to the SQL data stream. If, on + * the other hand, the object is of a class implementing Ref, Blob, + * Clob, NClob, Struct, java.net.URL, or + * Array, the driver should pass it to the database as a value of the corresponding + * SQL type. + * + *

This method throws an exception if there is an ambiguity, for example, if the object is of a + * class implementing more than one of the interfaces named above. + * + *

Note: Not all databases allow for a non-typed Null to be sent to the backend. For + * maximum portability, the setNull or the + * setObject(String parameterName, Object x, int sqlType) method should be used instead of + * setObject(String parameterName, Object x). + * + * @param parameterName the name of the parameter + * @param x the object containing the input parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs, this method is called on a closed CallableStatement or if + * the given Object parameter is ambiguous + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getObject + * @since 1.4 + */ + @Override + public void setObject(String parameterName, Object x) throws SQLException { + setObject(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given Reader object, which is the given + * number of characters long. When a very large UNICODE value is input to a LONGVARCHAR + * parameter, it may be more practical to send it via a java.io.Reader + * object. The data will be read from the stream as needed until end-of-file is reached. The JDBC + * driver will do any necessary conversion from UNICODE to the database char format. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + * @param parameterName the name of the parameter + * @param reader the java.io.Reader object that contains the UNICODE data used as the + * designated parameter + * @param length the number of characters in the stream + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.4 + */ + @Override + public void setCharacterStream(String parameterName, Reader reader, int length) + throws SQLException { + setCharacterStream(nameToIndex(parameterName), reader, length); + } + + /** + * Sets the designated parameter to the given java.sql.Date value, using the given + * Calendar object. The driver uses the Calendar object to construct an + * SQL DATE value, which the driver then sends to the database. With a a + * Calendar object, the driver can calculate the date taking into account a custom + * timezone. If no Calendar object is specified, the driver uses the default + * timezone, which is that of the virtual machine running the application. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @param cal the Calendar object the driver will use to construct the date + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getDate + * @since 1.4 + */ + @Override + public void setDate(String parameterName, Date x, Calendar cal) throws SQLException { + setDate(nameToIndex(parameterName), x, cal); + } + + /** + * Sets the designated parameter to the given java.sql.Time value, using the given + * Calendar object. The driver uses the Calendar object to construct an + * SQL TIME value, which the driver then sends to the database. With a a + * Calendar object, the driver can calculate the time taking into account a custom + * timezone. If no Calendar object is specified, the driver uses the default + * timezone, which is that of the virtual machine running the application. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @param cal the Calendar object the driver will use to construct the time + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getTime + * @since 1.4 + */ + @Override + public void setTime(String parameterName, Time x, Calendar cal) throws SQLException { + setTime(nameToIndex(parameterName), x, cal); + } + + /** + * Sets the designated parameter to the given java.sql.Timestamp value, using the + * given Calendar object. The driver uses the Calendar object to + * construct an SQL TIMESTAMP value, which the driver then sends to the database. + * With a a Calendar object, the driver can calculate the timestamp taking into + * account a custom timezone. If no Calendar object is specified, the driver uses the + * default timezone, which is that of the virtual machine running the application. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @param cal the Calendar object the driver will use to construct the timestamp + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #getTimestamp + * @since 1.4 + */ + @Override + public void setTimestamp(String parameterName, Timestamp x, Calendar cal) throws SQLException { + setTimestamp(nameToIndex(parameterName), x, cal); + } + + /** + * Sets the designated parameter to SQL NULL. This version of the method + * setNull should be used for user-defined types and REF type parameters. Examples of + * user-defined types include: STRUCT, DISTINCT, JAVA_OBJECT, and named array types. + * + *

Note: To be portable, applications must give the SQL type code and the + * fully-qualified SQL type name when specifying a NULL user-defined or REF parameter. In the case + * of a user-defined type the name is the type name of the parameter itself. For a REF parameter, + * the name is the type name of the referenced type. + * + *

Although it is intended for user-defined and Ref parameters, this method may be used to set + * a null parameter of any JDBC type. If the parameter does not have a user-defined or REF type, + * the given typeName is ignored. + * + * @param parameterName the name of the parameter + * @param sqlType a value from java.sql.Types + * @param typeName the fully-qualified name of an SQL user-defined type; ignored if the parameter + * is not a user-defined type or SQL REF value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.4 + */ + @Override + public void setNull(String parameterName, int sqlType, String typeName) throws SQLException { + setNull(nameToIndex(parameterName), sqlType, typeName); + } + + /** + * Retrieves the value of a JDBC CHAR, VARCHAR, or LONGVARCHAR + * parameter as a String in the Java programming language. + * + *

For the fixed-length type JDBC CHAR, the String object returned + * has exactly the same value the SQL CHAR value had in the database, including any + * padding added by the database. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setString + * @since 1.4 + */ + @Override + public String getString(String parameterName) throws SQLException { + return outputResult.getString(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC BIT or BOOLEAN parameter as a + * boolean in the Java programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is false + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setBoolean + * @since 1.4 + */ + @Override + public boolean getBoolean(String parameterName) throws SQLException { + return outputResult.getBoolean(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC TINYINT parameter as a byte in the Java + * programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is 0 + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setByte + * @since 1.4 + */ + @Override + public byte getByte(String parameterName) throws SQLException { + return outputResult.getByte(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC SMALLINT parameter as a short in the + * Java programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is 0 + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setShort + * @since 1.4 + */ + @Override + public short getShort(String parameterName) throws SQLException { + return outputResult.getShort(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC INTEGER parameter as an int in the Java + * programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is 0 + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setInt + * @since 1.4 + */ + @Override + public int getInt(String parameterName) throws SQLException { + return outputResult.getInt(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC BIGINT parameter as a long in the Java + * programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is 0 + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setLong + * @since 1.4 + */ + @Override + public long getLong(String parameterName) throws SQLException { + return outputResult.getLong(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC FLOAT parameter as a float in the Java + * programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is 0 + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setFloat + * @since 1.4 + */ + @Override + public float getFloat(String parameterName) throws SQLException { + return outputResult.getFloat(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC DOUBLE parameter as a double in the + * Java programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is 0 + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setDouble + * @since 1.4 + */ + @Override + public double getDouble(String parameterName) throws SQLException { + return outputResult.getDouble(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC BINARY or VARBINARY parameter as an + * array of byte values in the Java programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setBytes + * @since 1.4 + */ + @Override + public byte[] getBytes(String parameterName) throws SQLException { + return outputResult.getBytes(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC DATE parameter as a java.sql.Date + * object. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setDate + * @since 1.4 + */ + @Override + public Date getDate(String parameterName) throws SQLException { + return outputResult.getDate(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC TIME parameter as a java.sql.Time + * object. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setTime + * @since 1.4 + */ + @Override + public Time getTime(String parameterName) throws SQLException { + return outputResult.getTime(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC TIMESTAMP parameter as a java.sql.Timestamp + * object. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setTimestamp + * @since 1.4 + */ + @Override + public Timestamp getTimestamp(String parameterName) throws SQLException { + return outputResult.getTimestamp(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a parameter as an Object in the Java programming language. + * If the value is an SQL NULL, the driver returns a Java null. + * + *

This method returns a Java object whose type corresponds to the JDBC type that was + * registered for this parameter using the method registerOutParameter. By + * registering the target JDBC type as java.sql.Types.OTHER, this method can be used + * to read database-specific abstract data types. + * + * @param parameterName the name of the parameter + * @return A java.lang.Object holding the OUT parameter value. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see Types + * @see #setObject + * @since 1.4 + */ + @Override + public Object getObject(String parameterName) throws SQLException { + return outputResult.getObject(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC NUMERIC parameter as a java.math.BigDecimal + * object with as many digits to the right of the decimal point as the value contains. + * + * @param parameterName the name of the parameter + * @return the parameter value in full precision. If the value is SQL NULL, the + * result is null. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setBigDecimal + * @since 1.4 + */ + @Override + public BigDecimal getBigDecimal(String parameterName) throws SQLException { + return outputResult.getBigDecimal(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Returns an object representing the value of OUT parameter parameterName and uses + * map for the custom mapping of the parameter value. + * + *

This method returns a Java object whose type corresponds to the JDBC type that was + * registered for this parameter using the method registerOutParameter. By + * registering the target JDBC type as java.sql.Types.OTHER, this method can be used + * to read database-specific abstract data types. + * + * @param parameterName the name of the parameter + * @param map the mapping from SQL type names to Java classes + * @return a java.lang.Object holding the OUT parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setObject + * @since 1.4 + */ + @Override + public Object getObject(String parameterName, Map> map) throws SQLException { + return outputResult.getObject(idxToOutIdx(nameToIndex(parameterName)), map); + } + + /** + * Retrieves the value of a JDBC REF(<structured-type>) parameter as a {@link + * Ref} object in the Java programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value as a Ref object in the Java programming language. If + * the value was SQL NULL, the value null is returned. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.4 + */ + @Override + public Ref getRef(String parameterName) throws SQLException { + return outputResult.getRef(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC BLOB parameter as a {@link Blob} object in the Java + * programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value as a Blob object in the Java programming language. If + * the value was SQL NULL, the value null is returned. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.4 + */ + @Override + public Blob getBlob(String parameterName) throws SQLException { + return outputResult.getBlob(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC CLOB parameter as a java.sql.Clob + * object in the Java programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value as a Clob object in the Java programming language. If + * the value was SQL NULL, the value null is returned. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.4 + */ + @Override + public Clob getClob(String parameterName) throws SQLException { + return outputResult.getClob(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC ARRAY parameter as an {@link Array} object in the + * Java programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value as an Array object in Java programming language. If + * the value was SQL NULL, the value null is returned. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.4 + */ + @Override + public Array getArray(String parameterName) throws SQLException { + return outputResult.getArray(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of a JDBC DATE parameter as a java.sql.Date + * object, using the given Calendar object to construct the date. With a + * Calendar object, the driver can calculate the date taking into account a custom timezone + * and locale. If no Calendar object is specified, the driver uses the default + * timezone and locale. + * + * @param parameterName the name of the parameter + * @param cal the Calendar object the driver will use to construct the date + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setDate + * @since 1.4 + */ + @Override + public Date getDate(String parameterName, Calendar cal) throws SQLException { + return outputResult.getDate(idxToOutIdx(nameToIndex(parameterName)), cal); + } + + /** + * Retrieves the value of a JDBC TIME parameter as a java.sql.Time + * object, using the given Calendar object to construct the time. With a + * Calendar object, the driver can calculate the time taking into account a custom timezone + * and locale. If no Calendar object is specified, the driver uses the default + * timezone and locale. + * + * @param parameterName the name of the parameter + * @param cal the Calendar object the driver will use to construct the time + * @return the parameter value; if the value is SQL NULL, the result is null + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setTime + * @since 1.4 + */ + @Override + public Time getTime(String parameterName, Calendar cal) throws SQLException { + return outputResult.getTime(idxToOutIdx(nameToIndex(parameterName)), cal); + } + + /** + * Retrieves the value of a JDBC TIMESTAMP parameter as a java.sql.Timestamp + * object, using the given Calendar object to construct the Timestamp + * object. With a Calendar object, the driver can calculate the timestamp + * taking into account a custom timezone and locale. If no Calendar object is + * specified, the driver uses the default timezone and locale. + * + * @param parameterName the name of the parameter + * @param cal the Calendar object the driver will use to construct the timestamp + * @return the parameter value. If the value is SQL NULL, the result is null + * . + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setTimestamp + * @since 1.4 + */ + @Override + public Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException { + return outputResult.getTimestamp(idxToOutIdx(nameToIndex(parameterName)), cal); + } + + /** + * Retrieves the value of a JDBC DATALINK parameter as a java.net.URL + * object. + * + * @param parameterName the name of the parameter + * @return the parameter value as a java.net.URL object in the Java programming + * language. If the value was SQL NULL, the value null is returned. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs, this method is called on a closed CallableStatement, or + * if there is a problem with the URL + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setURL + * @since 1.4 + */ + @Override + public URL getURL(String parameterName) throws SQLException { + return outputResult.getURL(idxToOutIdx(nameToIndex(parameterName))); + } + + /** + * Retrieves the value of the designated JDBC ROWID parameter as a + * java.sql.RowId object. + * + * @param parameterIndex the first parameter is 1, the second is 2,... + * @return a RowId object that represents the JDBC ROWID value is used + * as the designated parameter. If the parameter contains a SQL NULL, then a + * null value is returned. + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public RowId getRowId(int parameterIndex) throws SQLException { + throw exceptionFactory().notSupported("RowId are not supported"); + } + + /** + * Retrieves the value of the designated JDBC ROWID parameter as a + * java.sql.RowId object. + * + * @param parameterName the name of the parameter + * @return a RowId object that represents the JDBC ROWID value is used + * as the designated parameter. If the parameter contains a SQL NULL, then a + * null value is returned. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public RowId getRowId(String parameterName) throws SQLException { + throw exceptionFactory().notSupported("RowId are not supported"); + } + + /** + * Sets the designated parameter to the given java.sql.RowId object. The driver + * converts this to a SQL ROWID when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setRowId(String parameterName, RowId x) throws SQLException { + throw exceptionFactory().notSupported("RowId parameter are not supported"); + } + + /** + * Sets the designated parameter to the given String object. The driver converts this + * to a SQL NCHAR or NVARCHAR or LONGNVARCHAR + * + * @param parameterName the name of the parameter to be set + * @param value the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if the driver + * does not support national character sets; if the driver can detect that a data conversion + * error could occur; if a database access error occurs or this method is called on a closed + * CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setNString(String parameterName, String value) throws SQLException { + setNString(nameToIndex(parameterName), value); + } + + /** + * Sets the designated parameter to a Reader object. The Reader reads + * the data till end-of-file is reached. The driver does the necessary conversion from Java + * character format to the national character set in the database. + * + * @param parameterName the name of the parameter to be set + * @param value the parameter value + * @param length the number of characters in the parameter data. + * @throws SQLException if parameterName does not correspond to a named parameter; if the driver + * does not support national character sets; if the driver can detect that a data conversion + * error could occur; if a database access error occurs or this method is called on a closed + * CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setNCharacterStream(String parameterName, Reader value, long length) + throws SQLException { + setNCharacterStream(nameToIndex(parameterName), value, length); + } + + /** + * Sets the designated parameter to a java.sql.NClob object. The object implements + * the java.sql.NClob interface. This NClob object maps to a SQL + * NCLOB. + * + * @param parameterName the name of the parameter to be set + * @param value the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if the driver + * does not support national character sets; if the driver can detect that a data conversion + * error could occur; if a database access error occurs or this method is called on a closed + * CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setNClob(String parameterName, NClob value) throws SQLException { + setNClob(nameToIndex(parameterName), value); + } + + /** + * Sets the designated parameter to a Reader object. The reader must + * contain the number of characters specified by length otherwise a SQLException will + * be generated when the CallableStatement is executed. This method differs from the + * setCharacterStream (int, Reader, int) method because it informs the driver that + * the parameter value should be sent to the server as a CLOB. When the + * setCharacterStream method is used, the driver may have to do extra work to determine + * whether the parameter data should be send to the server as a LONGVARCHAR or a + * CLOB + * + * @param parameterName the name of the parameter to be set + * @param reader An object that contains the data to set the parameter value to. + * @param length the number of characters in the parameter data. + * @throws SQLException if parameterName does not correspond to a named parameter; if the length + * specified is less than zero; a database access error occurs or this method is called on a + * closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setClob(String parameterName, Reader reader, long length) throws SQLException { + setClob(nameToIndex(parameterName), reader, length); + } + + /** + * Sets the designated parameter to an {@code InputStream} object. The Inputstream + * must contain the number of characters specified by length, otherwise a SQLException + * will be generated when the CallableStatement is executed. This method + * differs from the setBinaryStream (int, InputStream, int) method because it informs + * the driver that the parameter value should be sent to the server as a BLOB. When + * the setBinaryStream method is used, the driver may have to do extra work to + * determine whether the parameter data should be sent to the server as a LONGVARBINARY + * or a BLOB + * + * @param parameterName the name of the parameter to be set the second is 2, ... + * @param inputStream An object that contains the data to set the parameter value to. + * @param length the number of bytes in the parameter data. + * @throws SQLException if parameterName does not correspond to a named parameter; if the length + * specified is less than zero; if the number of bytes in the {@code InputStream} does not + * match the specified length; if a database access error occurs or this method is called on a + * closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setBlob(String parameterName, InputStream inputStream, long length) + throws SQLException { + setBlob(nameToIndex(parameterName), inputStream, length); + } + + /** + * Sets the designated parameter to a Reader object. The reader must + * contain the number of characters specified by length otherwise a SQLException will + * be generated when the CallableStatement is executed. This method differs from the + * setCharacterStream (int, Reader, int) method because it informs the driver that + * the parameter value should be sent to the server as a NCLOB. When the + * setCharacterStream method is used, the driver may have to do extra work to determine + * whether the parameter data should be send to the server as a LONGNVARCHAR or a + * NCLOB + * + * @param parameterName the name of the parameter to be set + * @param reader An object that contains the data to set the parameter value to. + * @param length the number of characters in the parameter data. + * @throws SQLException if parameterName does not correspond to a named parameter; if the length + * specified is less than zero; if the driver does not support national character sets; if the + * driver can detect that a data conversion error could occur; if a database access error + * occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setNClob(String parameterName, Reader reader, long length) throws SQLException { + setNClob(nameToIndex(parameterName), reader, length); + } + + /** + * Retrieves the value of the designated JDBC NCLOB parameter as a + * java.sql.NClob object in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @return the parameter value as a NClob object in the Java programming language. If + * the value was SQL NULL, the value null is returned. + * @throws SQLException if the parameterIndex is not valid; if the driver does not support + * national character sets; if the driver can detect that a data conversion error could occur; + * if a database access error occurs or this method is called on a closed + * CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public NClob getNClob(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getNClob(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of a JDBC NCLOB parameter as a java.sql.NClob + * object in the Java programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value as a NClob object in the Java programming language. If + * the value was SQL NULL, the value null is returned. + * @throws SQLException if parameterName does not correspond to a named parameter; if the driver + * does not support national character sets; if the driver can detect that a data conversion + * error could occur; if a database access error occurs or this method is called on a closed + * CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public NClob getNClob(String parameterName) throws SQLException { + return getNClob(nameToIndex(parameterName)); + } + + /** + * Sets the designated parameter to the given java.sql.SQLXML object. The driver + * converts this to an SQL XML value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param xmlObject a SQLXML object that maps an SQL XML value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs; this method is called on a closed CallableStatement or + * the java.xml.transform.Result, Writer or OutputStream + * has not been closed for the SQLXML object + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException { + throw exceptionFactory().notSupported("SQLXML parameter are not supported"); + } + + /** + * Retrieves the value of the designated SQL XML parameter as a java.sql.SQLXML + * object in the Java programming language. + * + * @param parameterIndex index of the first parameter is 1, the second is 2, ... + * @return a SQLXML object that maps an SQL XML value + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public SQLXML getSQLXML(int parameterIndex) throws SQLException { + throw exceptionFactory().notSupported("SQLXML are not supported"); + } + + /** + * Retrieves the value of the designated SQL XML parameter as a java.sql.SQLXML + * object in the Java programming language. + * + * @param parameterName the name of the parameter + * @return a SQLXML object that maps an SQL XML value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public SQLXML getSQLXML(String parameterName) throws SQLException { + return getSQLXML(nameToIndex(parameterName)); + } + + /** + * Retrieves the value of the designated NCHAR, NVARCHAR or + * LONGNVARCHAR parameter as a String in the Java programming language. + * + *

For the fixed-length type JDBC NCHAR, the String object returned + * has exactly the same value the SQL NCHAR value had in the database, including any + * padding added by the database. + * + * @param parameterIndex index of the first parameter is 1, the second is 2, ... + * @return a String object that maps an NCHAR, NVARCHAR or + * LONGNVARCHAR value + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setNString + * @since 1.6 + */ + @Override + public String getNString(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getNString(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated NCHAR, NVARCHAR or + * LONGNVARCHAR parameter as a String in the Java programming language. + * + *

For the fixed-length type JDBC NCHAR, the String object returned + * has exactly the same value the SQL NCHAR value had in the database, including any + * padding added by the database. + * + * @param parameterName the name of the parameter + * @return a String object that maps an NCHAR, NVARCHAR or + * LONGNVARCHAR value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @see #setNString + * @since 1.6 + */ + @Override + public String getNString(String parameterName) throws SQLException { + return getNString(nameToIndex(parameterName)); + } + + /** + * Retrieves the value of the designated parameter as a java.io.Reader object in the + * Java programming language. It is intended for use when accessing NCHAR, + * NVARCHAR and LONGNVARCHAR parameters. + * + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @return a java.io.Reader object that contains the parameter value; if the value is + * SQL NULL, the value returned is null in the Java programming + * language. + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public Reader getNCharacterStream(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getNCharacterStream(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated parameter as a java.io.Reader object in the + * Java programming language. It is intended for use when accessing NCHAR, + * NVARCHAR and LONGNVARCHAR parameters. + * + * @param parameterName the name of the parameter + * @return a java.io.Reader object that contains the parameter value; if the value is + * SQL NULL, the value returned is null in the Java programming + * language + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public Reader getNCharacterStream(String parameterName) throws SQLException { + return getNCharacterStream(nameToIndex(parameterName)); + } + + /** + * Retrieves the value of the designated parameter as a java.io.Reader object in the + * Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @return a java.io.Reader object that contains the parameter value; if the value is + * SQL NULL, the value returned is null in the Java programming + * language. + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed CallableStatement + * @since 1.6 + */ + @Override + public Reader getCharacterStream(int parameterIndex) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getCharacterStream(idxToOutIdx(parameterIndex)); + } + + /** + * Retrieves the value of the designated parameter as a java.io.Reader object in the + * Java programming language. + * + * @param parameterName the name of the parameter + * @return a java.io.Reader object that contains the parameter value; if the value is + * SQL NULL, the value returned is null in the Java programming + * language + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public Reader getCharacterStream(String parameterName) throws SQLException { + return getNCharacterStream(nameToIndex(parameterName)); + } + + /** + * Sets the designated parameter to the given java.sql.Blob object. The driver + * converts this to an SQL BLOB value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x a Blob object that maps an SQL BLOB value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setBlob(String parameterName, Blob x) throws SQLException { + setBlob(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given java.sql.Clob object. The driver + * converts this to an SQL CLOB value when it sends it to the database. + * + * @param parameterName the name of the parameter + * @param x a Clob object that maps an SQL CLOB value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setClob(String parameterName, Clob x) throws SQLException { + setClob(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given input stream, which will have the specified number + * of bytes. When a very large ASCII value is input to a LONGVARCHAR parameter, it + * may be more practical to send it via a java.io.InputStream. Data will be read from + * the stream as needed until end-of-file is reached. The JDBC driver will do any necessary + * conversion from ASCII to the database char format. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + * @param parameterName the name of the parameter + * @param x the Java input stream that contains the ASCII parameter value + * @param length the number of bytes in the stream + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setAsciiStream(String parameterName, InputStream x, long length) throws SQLException { + setAsciiStream(nameToIndex(parameterName), x, length); + } + + /** + * Sets the designated parameter to the given input stream, which will have the specified number + * of bytes. When a very large binary value is input to a LONGVARBINARY parameter, it + * may be more practical to send it via a java.io.InputStream object. The data will + * be read from the stream as needed until end-of-file is reached. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + * @param parameterName the name of the parameter + * @param x the java input stream which contains the binary parameter value + * @param length the number of bytes in the stream + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setBinaryStream(String parameterName, InputStream x, long length) + throws SQLException { + setBinaryStream(nameToIndex(parameterName), x, length); + } + + /** + * Sets the designated parameter to the given Reader object, which is the given + * number of characters long. When a very large UNICODE value is input to a LONGVARCHAR + * parameter, it may be more practical to send it via a java.io.Reader + * object. The data will be read from the stream as needed until end-of-file is reached. The JDBC + * driver will do any necessary conversion from UNICODE to the database char format. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + * @param parameterName the name of the parameter + * @param reader the java.io.Reader object that contains the UNICODE data used as the + * designated parameter + * @param length the number of characters in the stream + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setCharacterStream(String parameterName, Reader reader, long length) + throws SQLException { + setCharacterStream(nameToIndex(parameterName), reader, length); + } + + /** + * Sets the designated parameter to the given input stream. When a very large ASCII value is input + * to a LONGVARCHAR parameter, it may be more practical to send it via a + * java.io.InputStream. Data will be read from the stream as needed until end-of-file is + * reached. The JDBC driver will do any necessary conversion from ASCII to the database char + * format. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setAsciiStream which takes a length parameter. + * + * @param parameterName the name of the parameter + * @param x the Java input stream that contains the ASCII parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setAsciiStream(String parameterName, InputStream x) throws SQLException { + setAsciiStream(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given input stream. When a very large binary value is + * input to a LONGVARBINARY parameter, it may be more practical to send it via a + * java.io.InputStream object. The data will be read from the stream as needed until + * end-of-file is reached. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setBinaryStream which takes a length parameter. + * + * @param parameterName the name of the parameter + * @param x the java input stream which contains the binary parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setBinaryStream(String parameterName, InputStream x) throws SQLException { + setBinaryStream(nameToIndex(parameterName), x); + } + + /** + * Sets the designated parameter to the given Reader object. When a very large + * UNICODE value is input to a LONGVARCHAR parameter, it may be more practical to + * send it via a java.io.Reader object. The data will be read from the stream as + * needed until end-of-file is reached. The JDBC driver will do any necessary conversion from + * UNICODE to the database char format. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setCharacterStream which takes a length parameter. + * + * @param parameterName the name of the parameter + * @param reader the java.io.Reader object that contains the Unicode data + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setCharacterStream(String parameterName, Reader reader) throws SQLException { + setCharacterStream(nameToIndex(parameterName), reader); + } + + /** + * Sets the designated parameter to a Reader object. The Reader reads + * the data till end-of-file is reached. The driver does the necessary conversion from Java + * character format to the national character set in the database. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setNCharacterStream which takes a length parameter. + * + * @param parameterName the name of the parameter + * @param value the parameter value + * @throws SQLException if parameterName does not correspond to a named parameter; if the driver + * does not support national character sets; if the driver can detect that a data conversion + * error could occur; if a database access error occurs; or this method is called on a closed + * CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setNCharacterStream(String parameterName, Reader value) throws SQLException { + setNCharacterStream(nameToIndex(parameterName), value); + } + + /** + * Sets the designated parameter to a Reader object. This method differs from the + * setCharacterStream (int, Reader) method because it informs the driver that the + * parameter value should be sent to the server as a CLOB. When the + * setCharacterStream method is used, the driver may have to do extra work to determine + * whether the parameter data should be send to the server as a LONGVARCHAR or a + * CLOB + * + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setClob which takes a length parameter. + * + * @param parameterName the name of the parameter + * @param reader An object that contains the data to set the parameter value to. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setClob(String parameterName, Reader reader) throws SQLException { + setClob(nameToIndex(parameterName), reader); + } + + /** + * Sets the designated parameter to an {@code InputStream} object. This method differs from the + * setBinaryStream (int, InputStream) method because it informs the driver that the + * parameter value should be sent to the server as a BLOB. When the + * setBinaryStream method is used, the driver may have to do extra work to determine + * whether the parameter data should be send to the server as a LONGVARBINARY or a + * BLOB + * + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setBlob which takes a length parameter. + * + * @param parameterName the name of the parameter + * @param inputStream An object that contains the data to set the parameter value to. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setBlob(String parameterName, InputStream inputStream) throws SQLException { + setBlob(nameToIndex(parameterName), inputStream); + } + + /** + * Sets the designated parameter to a Reader object. This method differs from the + * setCharacterStream (int, Reader) method because it informs the driver that the + * parameter value should be sent to the server as a NCLOB. When the + * setCharacterStream method is used, the driver may have to do extra work to determine + * whether the parameter data should be send to the server as a LONGNVARCHAR or a + * NCLOB + * + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setNClob which takes a length parameter. + * + * @param parameterName the name of the parameter + * @param reader An object that contains the data to set the parameter value to. + * @throws SQLException if parameterName does not correspond to a named parameter; if the driver + * does not support national character sets; if the driver can detect that a data conversion + * error could occur; if a database access error occurs or this method is called on a closed + * CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setNClob(String parameterName, Reader reader) throws SQLException { + setNClob(nameToIndex(parameterName), reader); + } + + /** + * Returns an object representing the value of OUT parameter {@code parameterIndex} and will + * convert from the SQL type of the parameter to the requested Java data type, if the conversion + * is supported. If the conversion is not supported or null is specified for the type, a + * SQLException is thrown. + * + *

At a minimum, an implementation must support the conversions defined in Appendix B, Table + * B-3 and conversion of appropriate user defined SQL types to a Java type which implements {@code + * SQLData}, or {@code Struct}. Additional conversions may be supported and are vendor defined. + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @param type Class representing the Java data type to convert the designated parameter to. + * @return an instance of {@code type} holding the OUT parameter value + * @throws SQLException if conversion is not supported, type is null or another error occurs. The + * getCause() method of the exception may provide a more detailed exception, for example, if a + * conversion error occurs + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.7 + */ + @Override + public T getObject(int parameterIndex, Class type) throws SQLException { + checkNotClosed(); + checkOutputResult(); + return outputResult.getObject(idxToOutIdx(parameterIndex), type); + } + + /** + * Returns an object representing the value of OUT parameter {@code parameterName} and will + * convert from the SQL type of the parameter to the requested Java data type, if the conversion + * is supported. If the conversion is not supported or null is specified for the type, a + * SQLException is thrown. + * + *

At a minimum, an implementation must support the conversions defined in Appendix B, Table + * B-3 and conversion of appropriate user defined SQL types to a Java type which implements {@code + * SQLData}, or {@code Struct}. Additional conversions may be supported and are vendor defined. + * + * @param parameterName the name of the parameter + * @param type Class representing the Java data type to convert the designated parameter to. + * @return an instance of {@code type} holding the OUT parameter value + * @throws SQLException if conversion is not supported, type is null or another error occurs. The + * getCause() method of the exception may provide a more detailed exception, for example, if a + * conversion error occurs + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.7 + */ + @Override + public T getObject(String parameterName, Class type) throws SQLException { + return getObject(nameToIndex(parameterName), type); + } + + /** + * Sets the value of the designated parameter with the given object. + * + *

If the second argument is an {@code InputStream} then the stream must contain the number of + * bytes specified by scaleOrLength. If the second argument is a {@code Reader} then the reader + * must contain the number of characters specified by scaleOrLength. If these conditions are not + * true the driver will generate a {@code SQLException} when the prepared statement is executed. + * + *

The given Java object will be converted to the given targetSqlType before being sent to the + * database. + * + *

If the object has a custom mapping (is of a class implementing the interface {@code + * SQLData}), the JDBC driver should call the method {@code SQLData.writeSQL} to write it to the + * SQL data stream. If, on the other hand, the object is of a class implementing {@code Ref}, + * {@code Blob}, {@code Clob}, {@code NClob}, {@code Struct}, {@code java.net.URL}, or {@code + * Array}, the driver should pass it to the database as a value of the corresponding SQL type. + * + *

Note that this method may be used to pass database-specific abstract data types. + * + *

The default implementation will throw {@code SQLFeatureNotSupportedException} + * + * @param parameterName the name of the parameter + * @param x the object containing the input parameter value + * @param targetSqlType the SQL type to be sent to the database. The scale argument may further + * qualify this type. + * @param scaleOrLength for {@code java.sql.JDBCType.DECIMAL} or {@code java.sql.JDBCType.NUMERIC + * types}, this is the number of digits after the decimal point. For Java Object types {@code + * InputStream} and {@code Reader}, this is the length of the data in the stream or reader. + * For all other types, this value will be ignored. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed {@code CallableStatement} or if + * the Java Object specified by x is an InputStream or Reader object and the value of the + * scale parameter is less than zero + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * targetSqlType + * @see JDBCType + * @see SQLType + * @since 1.8 + */ + @Override + public void setObject(String parameterName, Object x, SQLType targetSqlType, int scaleOrLength) + throws SQLException { + setObject(nameToIndex(parameterName), x, targetSqlType, scaleOrLength); + } + + /** + * Sets the value of the designated parameter with the given object. + * + *

This method is similar to {@link #setObject(String parameterName, Object x, SQLType + * targetSqlType, int scaleOrLength)}, except that it assumes a scale of zero. + * + *

The default implementation will throw {@code SQLFeatureNotSupportedException} + * + * @param parameterName the name of the parameter + * @param x the object containing the input parameter value + * @param targetSqlType the SQL type to be sent to the database + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed {@code CallableStatement} + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * targetSqlType + * @see JDBCType + * @see SQLType + * @since 1.8 + */ + @Override + public void setObject(String parameterName, Object x, SQLType targetSqlType) throws SQLException { + setObject(nameToIndex(parameterName), x, targetSqlType); + } + + /** + * Registers the OUT parameter in ordinal position {@code parameterIndex} to the JDBC type {@code + * sqlType}. All OUT parameters must be registered before a stored procedure is executed. + * + *

The JDBC type specified by {@code sqlType} for an OUT parameter determines the Java type + * that must be used in the {@code get} method to read the value of that parameter. + * + *

If the JDBC type expected to be returned to this output parameter is specific to this + * particular database, {@code sqlType} may be {@code JDBCType.OTHER} or a {@code SQLType} that is + * supported by the JDBC driver. The method {@link #getObject} retrieves the value. + * + *

The default implementation will throw {@code SQLFeatureNotSupportedException} + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @param sqlType the JDBC type code defined by {@code SQLType} to use to register the OUT + * Parameter. If the parameter is of JDBC type {@code JDBCType.NUMERIC} or {@code + * JDBCType.DECIMAL}, the version of {@code registerOutParameter} that accepts a scale value + * should be used. + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed {@code CallableStatement} + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * sqlType + * @see JDBCType + * @see SQLType + * @since 1.8 + */ + @Override + public void registerOutParameter(int parameterIndex, SQLType sqlType) throws SQLException { + checkIndex(parameterIndex); + outputParameters.add(parameterIndex); + parameters.set(parameterIndex - 1, Parameter.NULL_PARAMETER); + } + + /** + * Registers the parameter in ordinal position {@code parameterIndex} to be of JDBC type {@code + * sqlType}. All OUT parameters must be registered before a stored procedure is executed. + * + *

The JDBC type specified by {@code sqlType} for an OUT parameter determines the Java type + * that must be used in the {@code get} method to read the value of that parameter. + * + *

This version of {@code registerOutParameter} should be used when the parameter is of JDBC + * type {@code JDBCType.NUMERIC} or {@code JDBCType.DECIMAL}. + * + *

The default implementation will throw {@code SQLFeatureNotSupportedException} + * + * @param parameterIndex the first parameter is 1, the second is 2, and so on + * @param sqlType the JDBC type code defined by {@code SQLType} to use to register the OUT + * Parameter. + * @param scale the desired number of digits to the right of the decimal point. It must be greater + * than or equal to zero. + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed {@code CallableStatement} + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * sqlType + * @see JDBCType + * @see SQLType + * @since 1.8 + */ + @Override + public void registerOutParameter(int parameterIndex, SQLType sqlType, int scale) + throws SQLException { + registerOutParameter(parameterIndex, sqlType); + } + + /** + * Registers the designated output parameter. This version of the method {@code + * registerOutParameter} should be used for a user-defined or {@code REF} output parameter. + * Examples of user-defined types include: {@code STRUCT}, {@code DISTINCT}, {@code JAVA_OBJECT}, + * and named array types. + * + *

All OUT parameters must be registered before a stored procedure is executed. + * + *

For a user-defined parameter, the fully-qualified SQL type name of the parameter should also + * be given, while a {@code REF} parameter requires that the fully-qualified type name of the + * referenced type be given. A JDBC driver that does not need the type code and type name + * information may ignore it. To be portable, however, applications should always provide these + * values for user-defined and {@code REF} parameters. + * + *

Although it is intended for user-defined and {@code REF} parameters, this method may be used + * to register a parameter of any JDBC type. If the parameter does not have a user-defined or + * {@code REF} type, the typeName parameter is ignored. + * + *

Note: When reading the value of an out parameter, you must use the getter method + * whose Java type corresponds to the parameter's registered SQL type. + * + *

The default implementation will throw {@code SQLFeatureNotSupportedException} + * + * @param parameterIndex the first parameter is 1, the second is 2,... + * @param sqlType the JDBC type code defined by {@code SQLType} to use to register the OUT + * Parameter. + * @param typeName the fully-qualified name of an SQL structured type + * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or + * this method is called on a closed {@code CallableStatement} + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * sqlType + * @see JDBCType + * @see SQLType + * @since 1.8 + */ + @Override + public void registerOutParameter(int parameterIndex, SQLType sqlType, String typeName) + throws SQLException { + registerOutParameter(parameterIndex, sqlType); + } + + /** + * Registers the OUT parameter named parameterName to the JDBC type {@code sqlType}. + * All OUT parameters must be registered before a stored procedure is executed. + * + *

The JDBC type specified by {@code sqlType} for an OUT parameter determines the Java type + * that must be used in the {@code get} method to read the value of that parameter. + * + *

If the JDBC type expected to be returned to this output parameter is specific to this + * particular database, {@code sqlType} should be {@code JDBCType.OTHER} or a {@code SQLType} that + * is supported by the JDBC driver.. The method {@link #getObject} retrieves the value. + * + *

The default implementation will throw {@code SQLFeatureNotSupportedException} + * + * @param parameterName the name of the parameter + * @param sqlType the JDBC type code defined by {@code SQLType} to use to register the OUT + * Parameter. If the parameter is of JDBC type {@code JDBCType.NUMERIC} or {@code + * JDBCType.DECIMAL}, the version of {@code registerOutParameter} that accepts a scale value + * should be used. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed {@code CallableStatement} + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * sqlType or if the JDBC driver does not support this method + * @see JDBCType + * @see SQLType + * @since 1.8 + */ + @Override + public void registerOutParameter(String parameterName, SQLType sqlType) throws SQLException { + registerOutParameter(nameToIndex(parameterName), sqlType); + } + + /** + * Registers the parameter named parameterName to be of JDBC type {@code sqlType}. + * All OUT parameters must be registered before a stored procedure is executed. + * + *

The JDBC type specified by {@code sqlType} for an OUT parameter determines the Java type + * that must be used in the {@code get} method to read the value of that parameter. + * + *

This version of {@code registerOutParameter} should be used when the parameter is of JDBC + * type {@code JDBCType.NUMERIC} or {@code JDBCType.DECIMAL}. + * + *

The default implementation will throw {@code SQLFeatureNotSupportedException} + * + * @param parameterName the name of the parameter + * @param sqlType the JDBC type code defined by {@code SQLType} to use to register the OUT + * Parameter. + * @param scale the desired number of digits to the right of the decimal point. It must be greater + * than or equal to zero. + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed {@code CallableStatement} + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * sqlType or if the JDBC driver does not support this method + * @see JDBCType + * @see SQLType + * @since 1.8 + */ + @Override + public void registerOutParameter(String parameterName, SQLType sqlType, int scale) + throws SQLException { + registerOutParameter(nameToIndex(parameterName), sqlType); + } + + /** + * Registers the designated output parameter. This version of the method {@code + * registerOutParameter} should be used for a user-named or REF output parameter. Examples of + * user-named types include: STRUCT, DISTINCT, JAVA_OBJECT, and named array types. + * + *

All OUT parameters must be registered before a stored procedure is executed. For a + * user-named parameter the fully-qualified SQL type name of the parameter should also be given, + * while a REF parameter requires that the fully-qualified type name of the referenced type be + * given. A JDBC driver that does not need the type code and type name information may ignore it. + * To be portable, however, applications should always provide these values for user-named and REF + * parameters. + * + *

Although it is intended for user-named and REF parameters, this method may be used to + * register a parameter of any JDBC type. If the parameter does not have a user-named or REF type, + * the typeName parameter is ignored. + * + *

Note: When reading the value of an out parameter, you must use the {@code getXXX} + * method whose Java type XXX corresponds to the parameter's registered SQL type. + * + *

The default implementation will throw {@code SQLFeatureNotSupportedException} + * + * @param parameterName the name of the parameter + * @param sqlType the JDBC type code defined by {@code SQLType} to use to register the OUT + * Parameter. + * @param typeName the fully-qualified name of an SQL structured type + * @throws SQLException if parameterName does not correspond to a named parameter; if a database + * access error occurs or this method is called on a closed {@code CallableStatement} + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * sqlType or if the JDBC driver does not support this method + * @see JDBCType + * @see SQLType + * @since 1.8 + */ + @Override + public void registerOutParameter(String parameterName, SQLType sqlType, String typeName) + throws SQLException { + registerOutParameter(nameToIndex(parameterName), sqlType); + } + + @Override + public CallableParameterMetaData getParameterMetaData() throws SQLException { + String sql = + "SELECT * from information_schema.parameters WHERE SPECIFIC_NAME = ? and SPECIFIC_SCHEMA = ?"; + PreparedStatement prep = + new ClientPreparedStatement( + NativeSql.parse(sql, con.getContext()), + con, + lock, + false, + false, + Statement.NO_GENERATED_KEYS, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + 0); + prep.setString(1, procedureName); + prep.setString(2, databaseName); + ResultSet rs = prep.executeQuery(); + parameterMetaData = new CallableParameterMetaData(rs); + return parameterMetaData; + } +} diff --git a/src/main/java/org/mariadb/jdbc/BasePrepareStatement.java b/src/main/java/org/mariadb/jdbc/BasePreparedStatement.java similarity index 54% rename from src/main/java/org/mariadb/jdbc/BasePrepareStatement.java rename to src/main/java/org/mariadb/jdbc/BasePreparedStatement.java index 525133ccc..bd240e832 100644 --- a/src/main/java/org/mariadb/jdbc/BasePrepareStatement.java +++ b/src/main/java/org/mariadb/jdbc/BasePreparedStatement.java @@ -1,5 +1,4 @@ /* - * * MariaDB Client for Java * * Copyright (c) 2012-2014 Monty Program Ab. @@ -18,36 +17,6 @@ * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to Monty Program Ab info@montyprogram.com. * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * */ package org.mariadb.jdbc; @@ -55,379 +24,371 @@ import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; -import java.math.BigInteger; import java.net.URL; import java.sql.*; -import java.time.*; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.util.Calendar; -import java.util.TimeZone; -import org.mariadb.jdbc.internal.ColumnType; -import org.mariadb.jdbc.internal.com.send.parameters.*; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; +import java.sql.Date; +import java.sql.ParameterMetaData; +import java.util.*; +import java.util.concurrent.locks.ReentrantLock; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.Codecs; +import org.mariadb.jdbc.codec.Parameter; +import org.mariadb.jdbc.codec.list.*; +import org.mariadb.jdbc.util.ParameterList; + +public abstract class BasePreparedStatement extends Statement implements PreparedStatement { + + protected ParameterList parameters; + protected List batchParameters; + protected String sql; + + public BasePreparedStatement( + String sql, + Connection con, + ReentrantLock lock, + boolean canUseServerTimeout, + boolean canUseServerMaxRows, + int autoGeneratedKeys, + int resultSetType, + int resultSetConcurrency, + int defaultFetchSize) { + super( + con, + lock, + canUseServerTimeout, + canUseServerMaxRows, + autoGeneratedKeys, + resultSetType, + resultSetConcurrency, + defaultFetchSize); + this.sql = sql; + } -public abstract class BasePrepareStatement extends MariaDbStatement implements PreparedStatement { + public abstract boolean execute() throws SQLException; - /** - * The ISO-like date-time formatter that formats or parses a date-time with offset and zone, such - * as '2011-12-03T10:15:30+01:00[Europe/Paris]'. and without the 'T' time delimiter - * - *

This returns an immutable formatter capable of formatting and parsing a format that extends - * the ISO-8601 extended offset date-time format to add the time-zone. - */ - public static final DateTimeFormatter SPEC_ISO_ZONED_DATE_TIME = - new DateTimeFormatterBuilder() - .parseCaseInsensitive() - .append(DateTimeFormatter.ISO_LOCAL_DATE) - .optionalStart() - .appendLiteral('T') - .optionalEnd() - .optionalStart() - .appendLiteral(' ') - .optionalEnd() - .append(DateTimeFormatter.ISO_LOCAL_TIME) - .appendOffsetId() - .optionalStart() - .appendLiteral('[') - .parseCaseSensitive() - .appendZoneRegionId() - .appendLiteral(']') - .toFormatter(); - - protected int autoGeneratedKeys; - protected boolean hasLongData = false; - private boolean useFractionalSeconds; - private boolean noBackslashEscapes; + public abstract ResultSet executeQuery() throws SQLException; - /** - * Constructor. Base class that permit setting parameters for client and server PrepareStatement. - * - * @param connection current connection - * @param resultSetScrollType one of the following ResultSet constants: - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, or - * ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency one of the following ResultSet constants: - * ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE - * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be returned; one - * of Statement.RETURN_GENERATED_KEYS or Statement.NO_GENERATED_KEYS - * @param exceptionFactory exception factory - */ - public BasePrepareStatement( - MariaDbConnection connection, - int resultSetScrollType, - int resultSetConcurrency, - int autoGeneratedKeys, - ExceptionFactory exceptionFactory) { - super(connection, resultSetScrollType, resultSetConcurrency, exceptionFactory); - this.noBackslashEscapes = protocol.noBackslashEscapes(); - this.useFractionalSeconds = options.useFractionalSeconds; - this.autoGeneratedKeys = autoGeneratedKeys; + public abstract int executeUpdate() throws SQLException; + + public abstract long executeLargeUpdate() throws SQLException; + + public abstract void addBatch() throws SQLException; + + public abstract ResultSetMetaData getMetaData() throws SQLException; + + public abstract ParameterMetaData getParameterMetaData() throws SQLException; + + public void setParameters(ParameterList parameters) { + this.parameters = parameters; } - /** - * Clone cached object. - * - * @param connection connection - * @return BasePrepareStatement - * @throws CloneNotSupportedException if cloning exception - */ - public BasePrepareStatement clone(MariaDbConnection connection) - throws CloneNotSupportedException { - BasePrepareStatement base = (BasePrepareStatement) super.clone(connection); - base.useFractionalSeconds = options.useFractionalSeconds; - return base; + public void setParameter(int index, Parameter param) { + parameters.set(index, param); } @Override - public long executeLargeUpdate() throws SQLException { - if (executeInternal(getFetchSize())) { - return 0; - } - return getLargeUpdateCount(); + public abstract int[] executeBatch() throws SQLException; + + @Override + public abstract long[] executeLargeBatch() throws SQLException; + + // *************************************************************************************************** + // methods inherited from Statement that are disabled + // *************************************************************************************************** + + @Override + public void addBatch(String sql) throws SQLException { + throw exceptionFactory().create("addBatch(String sql) cannot be called on preparedStatement"); } - protected abstract boolean executeInternal(int fetchSize) throws SQLException; + @Override + public boolean execute(String sql) throws SQLException { + throw exceptionFactory().create("execute(String sql) cannot be called on preparedStatement"); + } - /** - * Sets the designated parameter to the given Reader object, which is the given - * number of characters long. When a very large UNICODE value is input to a LONGVARCHAR - * parameter, it may be more practical to send it via a java.io.Reader - * object. The data will be read from the stream as needed until end-of-file is reached. The JDBC - * driver will do any necessary conversion from UNICODE to the database char format. - * - *

Note: This stream object can either be a standard Java stream object or your own - * subclass that implements the standard interface. - * - * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param reader the java.io.Reader object that contains the Unicode data - * @param length the number of characters in the stream - * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs or this method is called on a closed - * PreparedStatement - */ - public void setCharacterStream(final int parameterIndex, final Reader reader, final int length) - throws SQLException { - if (reader == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + throw exceptionFactory() + .create("execute(String sql, int autoGeneratedKeys) cannot be called on preparedStatement"); + } + + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + throw exceptionFactory() + .create("execute(String sql, int[] columnIndexes) cannot be called on preparedStatement"); + } + + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + throw exceptionFactory() + .create("execute(String sql, String[] columnNames) cannot be called on preparedStatement"); + } + + @Override + public ResultSet executeQuery(String sql) throws SQLException { + throw exceptionFactory() + .create("executeQuery(String sql) cannot be called on preparedStatement"); + } + + @Override + public int executeUpdate(String sql) throws SQLException { + throw exceptionFactory() + .create("executeUpdate(String sql) cannot be called on preparedStatement"); + } + + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + throw exceptionFactory() + .create( + "executeUpdate(String sql, int autoGeneratedKeys) cannot be called on preparedStatement"); + } + + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + throw exceptionFactory() + .create( + "executeUpdate(String sql, int[] columnIndexes) cannot be called on preparedStatement"); + } + + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + throw exceptionFactory() + .create( + "executeUpdate(String sql, String[] columnNames) cannot be called on preparedStatement"); + } + + @Override + public long executeLargeUpdate(String sql) throws SQLException { + throw exceptionFactory() + .create("executeLargeUpdate(String sql) cannot be called on preparedStatement"); + } + + @Override + public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + throw exceptionFactory() + .create( + "executeLargeUpdate(String sql, int autoGeneratedKeys) cannot be called on preparedStatement"); + } + + @Override + public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException { + throw exceptionFactory() + .create( + "executeLargeUpdate(String sql, int[] columnIndexes) cannot be called on preparedStatement"); + } + + @Override + public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException { + throw exceptionFactory() + .create( + "executeLargeUpdate(String sql, String[] columnNames) cannot be called on preparedStatement"); + } + + // *************************************************************************************************** + // Setters + // *************************************************************************************************** + + private void checkIndex(int index) throws SQLException { + if (index <= 0) { + throw exceptionFactory().create(String.format("wrong parameter index %s", index)); } - setParameter(parameterIndex, new ReaderParameter(reader, length, noBackslashEscapes)); - hasLongData = true; } /** - * Sets the designated parameter to the given Reader object, which is the given - * number of characters long. When a very large UNICODE value is input to a LONGVARCHAR - * parameter, it may be more practical to send it via a java.io.Reader - * object. The data will be read from the stream as needed until end-of-file is reached. The JDBC - * driver will do any necessary conversion from UNICODE to the database char format. + * Sets the designated parameter to SQL NULL. * - *

Note: This stream object can either be a standard Java stream object or your own - * subclass that implements the standard interface. + *

Note: You must specify the parameter's SQL type. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param reader the java.io.Reader object that contains the Unicode data - * @param length the number of characters in the stream + * @param sqlType the SQL type code defined in java.sql.Types * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement + * @throws SQLFeatureNotSupportedException if sqlType is a ARRAY, + * BLOB, CLOB, DATALINK, JAVA_OBJECT, + * NCHAR, NCLOB, NVARCHAR, LONGNVARCHAR, + * REF, ROWID, SQLXML or STRUCT data type and + * the JDBC driver does not support this data type */ - public void setCharacterStream(final int parameterIndex, final Reader reader, final long length) - throws SQLException { - if (reader == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; - } - setParameter(parameterIndex, new ReaderParameter(reader, length, noBackslashEscapes)); - hasLongData = true; + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, Parameter.NULL_PARAMETER); } /** - * Sets the designated parameter to the given Reader object. When a very large - * UNICODE value is input to a LONGVARCHAR parameter, it may be more practical to - * send it via a java.io.Reader object. The data will be read from the stream as - * needed until end-of-file is reached. The JDBC driver will do any necessary conversion from - * UNICODE to the database char format. - * - *

Note: This stream object can either be a standard Java stream object or your own - * subclass that implements the standard interface. - * - *

Note: Consult your JDBC driver documentation to determine if it might be more - * efficient to use a version of setCharacterStream which takes a length parameter. + * Sets the designated parameter to the given Java boolean value. The driver converts + * this to an SQL BIT or BOOLEAN value when it sends it to the database. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param reader the java.io.Reader object that contains the Unicode data + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement */ - public void setCharacterStream(final int parameterIndex, final Reader reader) - throws SQLException { - if (reader == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; - } - setParameter(parameterIndex, new ReaderParameter(reader, noBackslashEscapes)); - hasLongData = true; + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(BooleanCodec.INSTANCE, x)); } /** - * Sets the designated parameter to the given REF(<structured-type>) value. The - * driver converts this to an SQL REF value when it sends it to the database. + * Sets the designated parameter to the given Java byte value. The driver converts + * this to an SQL TINYINT value when it sends it to the database. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param ref an SQL REF value + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement */ - public void setRef(final int parameterIndex, final Ref ref) throws SQLException { - throw exceptionFactory.notSupported("REF parameter are not supported"); + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ByteCodec.INSTANCE, x)); } /** - * Sets the designated parameter to the given java.sql.Blob object. The driver - * converts this to an SQL BLOB value when it sends it to the database. + * Sets the designated parameter to the given Java short value. The driver converts + * this to an SQL SMALLINT value when it sends it to the database. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param blob a Blob object that maps an SQL BLOB value + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement */ - public void setBlob(final int parameterIndex, final Blob blob) throws SQLException { - if (blob == null) { - setNull(parameterIndex, Types.BLOB); - return; - } - setParameter( - parameterIndex, - new StreamParameter(blob.getBinaryStream(), blob.length(), noBackslashEscapes)); - hasLongData = true; + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ShortCodec.INSTANCE, x)); } /** - * Sets the designated parameter to a InputStream object. The inputstream must - * contain the number of characters specified by length otherwise a SQLException will - * be generated when the PreparedStatement is executed. This method differs from the - * setBinaryStream - * (int, InputStream, int) method because it informs the driver that the parameter value - * should be sent to the server as a BLOB. When the setBinaryStream - * method is used, the driver may have to do extra work to determine whether the parameter data - * should be sent to the server as a LONGVARBINARY or a BLOB + * Sets the designated parameter to the given Java int value. The driver converts + * this to an SQL INTEGER value when it sends it to the database. * - * @param parameterIndex index of the first parameter is 1, the second is 2, ... - * @param inputStream An object that contains the data to set the parameter value to. - * @param length the number of bytes in the parameter data. + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs; this method is called on a closed - * PreparedStatement; if the length specified is less than zero or if the number of - * bytes in the inputstream does not match the specfied length. + * statement; if a database access error occurs or this method is called on a closed + * PreparedStatement */ - public void setBlob(final int parameterIndex, final InputStream inputStream, final long length) - throws SQLException { - if (inputStream == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; - } - setParameter(parameterIndex, new StreamParameter(inputStream, length, noBackslashEscapes)); - hasLongData = true; + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(IntCodec.INSTANCE, x)); } /** - * Sets the designated parameter to a InputStream object. This method differs from - * the setBinaryStream (int, InputStream) method because it informs the driver that - * the parameter value should be sent to the server as a BLOB. When the - * setBinaryStream method is used, the driver may have to do extra work to determine - * whether the parameter data should be sent to the server as a LONGVARBINARY or a - * BLOB - * - *

Note: Consult your JDBC driver documentation to determine if it might be more - * efficient to use a version of setBlob which takes a length parameter. + * Sets the designated parameter to the given Java long value. The driver converts + * this to an SQL BIGINT value when it sends it to the database. * - * @param parameterIndex index of the first parameter is 1, the second is 2, ... - * @param inputStream An object that contains the data to set the parameter value to. + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs; this method is called on a closed - * PreparedStatement or if parameterIndex does not correspond to a parameter marker in - * the SQL statement, + * statement; if a database access error occurs or this method is called on a closed + * PreparedStatement */ - public void setBlob(final int parameterIndex, final InputStream inputStream) throws SQLException { - if (inputStream == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; - } - - setParameter(parameterIndex, new StreamParameter(inputStream, noBackslashEscapes)); - hasLongData = true; + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(LongCodec.INSTANCE, x)); } /** - * Sets the designated parameter to the given java.sql.Clob object. The driver - * converts this to an SQL CLOB value when it sends it to the database. + * Sets the designated parameter to the given Java float value. The driver converts + * this to an SQL REAL value when it sends it to the database. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param clob a Clob object that maps an SQL CLOB value + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement */ - public void setClob(final int parameterIndex, final Clob clob) throws SQLException { - if (clob == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; - } - - setParameter( - parameterIndex, - new ReaderParameter(clob.getCharacterStream(), clob.length(), noBackslashEscapes)); - hasLongData = true; + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(FloatCodec.INSTANCE, x)); } /** - * Sets the designated parameter to a Reader object. The reader must contain the - * number of characters specified by length otherwise a SQLException will be - * generated when the PreparedStatement is executed. This method differs from the - * setCharacterStream (int, Reader, - * int) method because it informs the driver that the parameter value should be sent to the - * server as a CLOB. When the setCharacterStream method is used, the - * driver may have to do extra work to determine whether the parameter data should be sent to the - * server as a LONGVARCHAR or a CLOB + * Sets the designated parameter to the given Java double value. The driver converts + * this to an SQL DOUBLE value when it sends it to the database. * - * @param parameterIndex index of the first parameter is 1, the second is 2, ... - * @param reader An object that contains the data to set the parameter value to. - * @param length the number of characters in the parameter data. + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs; this method is called on a closed - * PreparedStatement or if the length specified is less than zero. + * statement; if a database access error occurs or this method is called on a closed + * PreparedStatement */ - public void setClob(final int parameterIndex, final Reader reader, final long length) - throws SQLException { - setCharacterStream(parameterIndex, reader, length); + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(DoubleCodec.INSTANCE, x)); } /** - * Sets the designated parameter to a Reader object. This method differs from the - * setCharacterStream (int, Reader) method because it informs the driver that the - * parameter value should be sent to the server as a CLOB. When the - * setCharacterStream method is used, the driver may have to do extra work to determine - * whether the parameter data should be sent to the server as a LONGVARCHAR or a - * CLOB - * - *

Note: Consult your JDBC driver documentation to determine if it might be more - * efficient to use a version of setClob which takes a length parameter. + * Sets the designated parameter to the given java.math.BigDecimal value. The driver + * converts this to an SQL NUMERIC value when it sends it to the database. * - * @param parameterIndex index of the first parameter is 1, the second is 2, ... - * @param reader An object that contains the data to set the parameter value to. + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs; this method is called on a closed - * PreparedStatementor if parameterIndex does not correspond to a parameter marker in - * the SQL statement + * statement; if a database access error occurs or this method is called on a closed + * PreparedStatement */ - public void setClob(final int parameterIndex, final Reader reader) throws SQLException { - setCharacterStream(parameterIndex, reader); + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(BigDecimalCodec.INSTANCE, x)); } /** - * Sets the designated parameter to the given java.sql.Array object. The driver - * converts this to an SQL ARRAY value when it sends it to the database. + * Sets the designated parameter to the given Java String value. The driver converts + * this to an SQL VARCHAR or LONGVARCHAR value (depending on the + * argument's size relative to the driver's limits on VARCHAR values) when it sends + * it to the database. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param array an Array object that maps an SQL ARRAY value + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement */ - public void setArray(final int parameterIndex, final Array array) throws SQLException { - throw exceptionFactory.notSupported("Arrays not supported"); + @Override + public void setString(int parameterIndex, String x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(StringCodec.INSTANCE, x)); } /** - * Sets the designated parameter to the given java.sql.Date value, using the given - * Calendar object. The driver uses the Calendar object to construct an - * SQL DATE value, which the driver then sends to the database. With a Calendar - * object, the driver can calculate the date taking into account a custom timezone. If no - * Calendar object is specified, the driver uses the default timezone, which is that - * of the virtual machine running the application. + * Sets the designated parameter to the given Java array of bytes. The driver converts this to an + * SQL VARBINARY or LONGVARBINARY (depending on the argument's size + * relative to the driver's limits on VARBINARY values) when it sends it to the + * database. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param date the parameter value - * @param cal the Calendar object the driver will use to construct the date + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement */ - public void setDate(final int parameterIndex, final Date date, final Calendar cal) - throws SQLException { - if (date == null) { - setNull(parameterIndex, Types.DATE); - return; - } - setParameter( - parameterIndex, - new DateParameter( - date, cal != null ? cal.getTimeZone() : TimeZone.getDefault(), protocol.getOptions())); + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ByteArrayCodec.INSTANCE, x)); } /** @@ -436,140 +397,371 @@ public void setDate(final int parameterIndex, final Date date, final Calendar ca * an SQL DATE value when it sends it to the database. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param date the parameter value + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement */ - public void setDate(int parameterIndex, Date date) throws SQLException { - if (date == null) { - setNull(parameterIndex, Types.DATE); - return; - } - setParameter( - parameterIndex, new DateParameter(date, TimeZone.getDefault(), protocol.getOptions())); + @Override + public void setDate(int parameterIndex, Date x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(DateCodec.INSTANCE, x)); } /** - * Sets the designated parameter to the given java.sql.Time value, using the given - * Calendar object. The driver uses the Calendar object to construct an - * SQL TIME value, which the driver then sends to the database. With a Calendar - * object, the driver can calculate the time taking into account a custom timezone. If no - * Calendar object is specified, the driver uses the default timezone, which is that - * of the virtual machine running the application. + * Sets the designated parameter to the given java.sql.Time value. The driver + * converts this to an SQL TIME value when it sends it to the database. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param time the parameter value - * @param cal the Calendar object the driver will use to construct the time + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement */ - public void setTime(final int parameterIndex, final Time time, final Calendar cal) - throws SQLException { - if (time == null) { - setNull(parameterIndex, ColumnType.TIME); - return; - } - setParameter( - parameterIndex, - new TimeParameter( - time, cal != null ? cal.getTimeZone() : TimeZone.getDefault(), useFractionalSeconds)); + @Override + public void setTime(int parameterIndex, Time x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(TimeCodec.INSTANCE, x)); } /** - * Sets the designated parameter to the given java.sql.Time value. the driver uses - * the default timezone, which is that of the virtual machine running the application. + * Sets the designated parameter to the given java.sql.Timestamp value. The driver + * converts this to an SQL TIMESTAMP value when it sends it to the database. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param time the parameter value + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement */ - public void setTime(final int parameterIndex, final Time time) throws SQLException { - if (time == null) { - setNull(parameterIndex, ColumnType.TIME); - return; - } - setParameter( - parameterIndex, new TimeParameter(time, TimeZone.getDefault(), useFractionalSeconds)); + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(TimestampCodec.INSTANCE, x)); } /** - * Sets the designated parameter to the given java.sql.Timestamp value, using the - * given Calendar object. The driver uses the Calendar object to - * construct an SQL TIMESTAMP value, which the driver then sends to the database. - * With a Calendar object, the driver can calculate the timestamp taking into account - * a custom timezone. If no Calendar object is specified, the driver uses the default - * timezone, which is that of the virtual machine running the application. + * Sets the designated parameter to the given input stream, which will have the specified number + * of bytes. When a very large ASCII value is input to a LONGVARCHAR parameter, it + * may be more practical to send it via a java.io.InputStream. Data will be read from + * the stream as needed until end-of-file is reached. The JDBC driver will do any necessary + * conversion from ASCII to the database char format. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param timestamp the parameter value - * @param cal the Calendar object the driver will use to construct the timestamp + * @param x the Java input stream that contains the ASCII parameter value + * @param length the number of bytes in the stream * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement */ - public void setTimestamp(final int parameterIndex, final Timestamp timestamp, final Calendar cal) - throws SQLException { - if (timestamp == null) { - setNull(parameterIndex, ColumnType.DATETIME); - return; - } - TimeZone tz = cal != null ? cal.getTimeZone() : protocol.getTimeZone(); - setParameter(parameterIndex, new TimestampParameter(timestamp, tz, useFractionalSeconds)); + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, (long) length)); } /** - * Sets the designated parameter to the given java.sql.Timestamp value. The driver - * converts this to an SQL TIMESTAMP value when it sends it to the database. + * Sets the designated parameter to the given input stream, which will have the specified number + * of bytes. + * + *

When a very large Unicode value is input to a LONGVARCHAR parameter, it may be + * more practical to send it via a java.io.InputStream object. The data will be read + * from the stream as needed until end-of-file is reached. The JDBC driver will do any necessary + * conversion from Unicode to the database char format. + * + *

The byte format of the Unicode stream must be a Java UTF-8, as defined in the Java Virtual + * Machine Specification. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param x a java.io.InputStream object that contains the Unicode parameter value + * @param length the number of bytes in the stream + * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL + * statement; if a database access error occurs or this method is called on a closed + * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @deprecated Use {@code setCharacterStream} + */ + @Override + @Deprecated + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, (long) length)); + } + + /** + * Sets the designated parameter to the given input stream, which will have the specified number + * of bytes. When a very large binary value is input to a LONGVARBINARY parameter, it + * may be more practical to send it via a java.io.InputStream object. The data will + * be read from the stream as needed until end-of-file is reached. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param timestamp the parameter value + * @param x the java input stream which contains the binary parameter value + * @param length the number of bytes in the stream * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement */ - public void setTimestamp(final int parameterIndex, final Timestamp timestamp) + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, (long) length)); + } + + /** + * Clears the current parameter values immediately. + * + *

In general, parameter values remain in force for repeated use of a statement. Setting a + * parameter value automatically clears its previous value. However, in some cases it is useful to + * immediately release the resources used by the current parameter values; this can be done by + * calling the method clearParameters. + * + * @throws SQLException if a database access error occurs or this method is called on a closed + * PreparedStatement + */ + @Override + public void clearParameters() throws SQLException { + checkNotClosed(); + parameters = new ParameterList(); + } + + /** + * Sets the value of the designated parameter with the given object. + * + *

This method is similar to {@link #setObject(int parameterIndex, Object x, int targetSqlType, + * int scaleOrLength)}, except that it assumes a scale of zero. + * + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param x the object containing the input parameter value + * @param targetSqlType the SQL type (as defined in java.sql.Types) to be sent to the database + * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL + * statement; if a database access error occurs or this method is called on a closed + * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * targetSqlType + * @see Types + */ + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + setInternalObject(parameterIndex, x, null); + } + + /** + * Sets the value of the designated parameter using the given object. + * + *

The JDBC specification specifies a standard mapping from Java Object types to + * SQL types. The given argument will be converted to the corresponding SQL type before being sent + * to the database. + * + *

Note that this method may be used to pass datatabase- specific abstract data types, by using + * a driver-specific Java type. + * + *

If the object is of a class implementing the interface SQLData, the JDBC driver + * should call the method SQLData.writeSQL to write it to the SQL data stream. If, on + * the other hand, the object is of a class implementing Ref, Blob, + * Clob, NClob, Struct, java.net.URL, + * RowId, SQLXML or Array, the driver should pass it to the + * database as a value of the corresponding SQL type. + * + *

Note: Not all databases allow for a non-typed Null to be sent to the backend. For + * maximum portability, the setNull or the + * setObject(int parameterIndex, Object x, int sqlType) method should be used instead of + * setObject(int parameterIndex, Object x). + * + *

Note: This method throws an exception if there is an ambiguity, for example, if the + * object is of a class implementing more than one of the interfaces named above. + * + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param x the object containing the input parameter value + * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL + * statement; if a database access error occurs; this method is called on a closed + * PreparedStatement or the type of the given object is ambiguous + */ + @Override + public void setObject(int parameterIndex, Object x) throws SQLException { + setInternalObject(parameterIndex, x, null); + } + + /** + * Sets the designated parameter to the given Reader object, which is the given + * number of characters long. When a very large UNICODE value is input to a LONGVARCHAR + * parameter, it may be more practical to send it via a java.io.Reader + * object. The data will be read from the stream as needed until end-of-file is reached. The JDBC + * driver will do any necessary conversion from UNICODE to the database char format. + * + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param reader the java.io.Reader object that contains the Unicode data + * @param length the number of characters in the stream + * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL + * statement; if a database access error occurs or this method is called on a closed + * PreparedStatement + * @since 1.2 + */ + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { - if (timestamp == null) { - setNull(parameterIndex, ColumnType.DATETIME); - return; - } - setParameter( - parameterIndex, - new TimestampParameter(timestamp, protocol.getTimeZone(), useFractionalSeconds)); + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set( + parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader, (long) length)); } /** - * Sets the designated parameter to SQL NULL. + * Sets the designated parameter to the given REF(<structured-type>) value. The + * driver converts this to an SQL REF value when it sends it to the database. * - *

Note: You must specify the parameter's SQL type. + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param x an SQL REF value + * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL + * statement; if a database access error occurs or this method is called on a closed + * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.2 + */ + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException { + throw exceptionFactory().notSupported("REF parameter are not supported"); + } + + /** + * Sets the designated parameter to the given java.sql.Blob object. The driver + * converts this to an SQL BLOB value when it sends it to the database. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param sqlType the SQL type code defined in java.sql.Types + * @param x a Blob object that maps an SQL BLOB value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.2 */ - public void setNull(final int parameterIndex, final int sqlType) throws SQLException { - setParameter(parameterIndex, new NullParameter()); + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(BlobCodec.INSTANCE, x)); } /** - * Sets the designated parameter to SQL NULL. + * Sets the designated parameter to the given java.sql.Clob object. The driver + * converts this to an SQL CLOB value when it sends it to the database. * - *

Note: You must specify the parameter's SQL type. + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param x a Clob object that maps an SQL CLOB value + * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL + * statement; if a database access error occurs or this method is called on a closed + * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.2 + */ + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ClobCodec.INSTANCE, x)); + } + + /** + * Sets the designated parameter to the given java.sql.Array object. The driver + * converts this to an SQL ARRAY value when it sends it to the database. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param mariadbType the type code defined in ColumnType + * @param x an Array object that maps an SQL ARRAY value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.2 */ - public void setNull(final int parameterIndex, final ColumnType mariadbType) throws SQLException { - setParameter(parameterIndex, new NullParameter(mariadbType)); + @Override + public void setArray(int parameterIndex, Array x) throws SQLException { + throw exceptionFactory().notSupported("Array parameter are not supported"); + } + + /** + * Sets the designated parameter to the given java.sql.Date value, using the given + * Calendar object. The driver uses the Calendar object to construct an + * SQL DATE value, which the driver then sends to the database. With a Calendar + * object, the driver can calculate the date taking into account a custom timezone. If no + * Calendar object is specified, the driver uses the default timezone, which is that + * of the virtual machine running the application. + * + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param x the parameter value + * @param cal the Calendar object the driver will use to construct the date + * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL + * statement; if a database access error occurs or this method is called on a closed + * PreparedStatement + * @since 1.2 + */ + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(DateCodec.INSTANCE, x)); + } + + /** + * Sets the designated parameter to the given java.sql.Time value, using the given + * Calendar object. The driver uses the Calendar object to construct an + * SQL TIME value, which the driver then sends to the database. With a Calendar + * object, the driver can calculate the time taking into account a custom timezone. If no + * Calendar object is specified, the driver uses the default timezone, which is that + * of the virtual machine running the application. + * + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param x the parameter value + * @param cal the Calendar object the driver will use to construct the time + * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL + * statement; if a database access error occurs or this method is called on a closed + * PreparedStatement + * @since 1.2 + */ + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(TimeCodec.INSTANCE, x, cal)); + } + + /** + * Sets the designated parameter to the given java.sql.Timestamp value, using the + * given Calendar object. The driver uses the Calendar object to + * construct an SQL TIMESTAMP value, which the driver then sends to the database. + * With a Calendar object, the driver can calculate the timestamp taking into account + * a custom timezone. If no Calendar object is specified, the driver uses the default + * timezone, which is that of the virtual machine running the application. + * + * @param parameterIndex the first parameter is 1, the second is 2, ... + * @param x the parameter value + * @param cal the Calendar object the driver will use to construct the timestamp + * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL + * statement; if a database access error occurs or this method is called on a closed + * PreparedStatement + * @since 1.2 + */ + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(TimestampCodec.INSTANCE, x, cal)); } /** @@ -594,63 +786,59 @@ public void setNull(final int parameterIndex, final ColumnType mariadbType) thro * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement + * @throws SQLFeatureNotSupportedException if sqlType is a ARRAY, + * BLOB, CLOB, DATALINK, JAVA_OBJECT, + * NCHAR, NCLOB, NVARCHAR, LONGNVARCHAR, + * REF, ROWID, SQLXML or STRUCT data type and + * the JDBC driver does not support this data type or if the JDBC driver does not support this + * method + * @since 1.2 */ - public void setNull(final int parameterIndex, final int sqlType, final String typeName) - throws SQLException { - setParameter(parameterIndex, new NullParameter()); + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, Parameter.NULL_PARAMETER); } - public abstract void setParameter(final int parameterIndex, final ParameterHolder holder) - throws SQLException; - /** * Sets the designated parameter to the given java.net.URL value. The driver converts * this to an SQL DATALINK value when it sends it to the database. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param url the java.net.URL object to be set + * @param x the java.net.URL object to be set * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.4 */ @Override - public void setURL(final int parameterIndex, final URL url) throws SQLException { - if (url == null) { - setNull(parameterIndex, ColumnType.STRING); - return; - } - setParameter(parameterIndex, new StringParameter(url.toString(), noBackslashEscapes)); + public void setURL(int parameterIndex, URL x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(StringCodec.INSTANCE, x.toString())); } - /** - * Retrieves the number, types and properties of this PreparedStatement object's - * parameters. - * - * @return a ParameterMetaData object that contains information about the number, - * types and properties for each parameter marker of this PreparedStatement - * object - * @throws SQLException if a database access error occurs or this method is called on a closed - * PreparedStatement - * @see ParameterMetaData - */ - public abstract ParameterMetaData getParameterMetaData() throws SQLException; - /** * Sets the designated parameter to the given java.sql.RowId object. The driver * converts this to a SQL ROWID value when it sends it to the database * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param rowid the parameter value + * @param x the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setRowId(final int parameterIndex, final RowId rowid) throws SQLException { - throw exceptionFactory.notSupported("RowIDs not supported"); + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException { + throw exceptionFactory().notSupported("RowId parameter are not supported"); } /** - * Sets the designated paramter to the given String object. The driver converts this + * Sets the designated parameter to the given String object. The driver converts this * to a SQL NCHAR or NVARCHAR or LONGNVARCHAR value * (depending on the argument's size relative to the driver's limits on NVARCHAR * values) when it sends it to the database. @@ -661,9 +849,14 @@ public void setRowId(final int parameterIndex, final RowId rowid) throws SQLExce * statement; if the driver does not support national character sets; if the driver can detect * that a data conversion error could occur; if a database access error occurs; or this method * is called on a closed PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setNString(final int parameterIndex, final String value) throws SQLException { - setString(parameterIndex, value); + @Override + public void setNString(int parameterIndex, String value) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(StringCodec.INSTANCE, value)); } /** @@ -678,33 +871,15 @@ public void setNString(final int parameterIndex, final String value) throws SQLE * statement; if the driver does not support national character sets; if the driver can detect * that a data conversion error could occur; if a database access error occurs; or this method * is called on a closed PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setNCharacterStream(final int parameterIndex, final Reader value, final long length) - throws SQLException { - setCharacterStream(parameterIndex, value, length); - } - - /** - * Sets the designated parameter to a Reader object. The Reader reads - * the data till end-of-file is reached. The driver does the necessary conversion from Java - * character format to the national character set in the database. - * - *

Note: This stream object can either be a standard Java stream object or your own - * subclass that implements the standard interface. - * - *

Note: Consult your JDBC driver documentation to determine if it might be more - * efficient to use a version of setNCharacterStream which takes a length parameter. - * - * @param parameterIndex of the first parameter is 1, the second is 2, ... - * @param value the parameter value - * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if the driver does not support national character sets; if the driver can detect - * that a data conversion error could occur; if a database access error occurs; or this method - * is called on a closed PreparedStatement - */ - public void setNCharacterStream(final int parameterIndex, final Reader value) + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { - setCharacterStream(parameterIndex, value); + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, value, length)); } /** @@ -717,60 +892,103 @@ public void setNCharacterStream(final int parameterIndex, final Reader value) * statement; if the driver does not support national character sets; if the driver can detect * that a data conversion error could occur; if a database access error occurs; or this method * is called on a closed PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setNClob(final int parameterIndex, final NClob value) throws SQLException { - setClob(parameterIndex, value); + @Override + public void setNClob(int parameterIndex, NClob value) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ClobCodec.INSTANCE, value)); } /** * Sets the designated parameter to a Reader object. The reader must contain the * number of characters specified by length otherwise a SQLException will be * generated when the PreparedStatement is executed. This method differs from the - * setCharacterStream (int, Reader, - * int) method because it informs the driver that the parameter value should be sent to the - * server as a NCLOB. When the setCharacterStream method is used, the - * driver may have to do extra work to determine whether the parameter data should be sent to the - * server as a LONGNVARCHAR or a NCLOB + * setCharacterStream (int, Reader, int) method because it informs the driver that + * the parameter value should be sent to the server as a CLOB. When the + * setCharacterStream method is used, the driver may have to do extra work to determine + * whether the parameter data should be sent to the server as a LONGVARCHAR or a + * CLOB * * @param parameterIndex index of the first parameter is 1, the second is 2, ... * @param reader An object that contains the data to set the parameter value to. * @param length the number of characters in the parameter data. * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if the length specified is less than zero; if the driver does not support - * national character sets; if the driver can detect that a data conversion error could occur; - * if a database access error occurs or this method is called on a closed - * PreparedStatement + * statement; if a database access error occurs; this method is called on a closed + * PreparedStatement or if the length specified is less than zero. + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setNClob(final int parameterIndex, final Reader reader, final long length) + @Override + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader, length)); + } + + /** + * Sets the designated parameter to a InputStream object. The inputstream must + * contain the number of characters specified by length otherwise a SQLException will + * be generated when the PreparedStatement is executed. This method differs from the + * setBinaryStream (int, InputStream, int) method because it informs the driver that + * the parameter value should be sent to the server as a BLOB. When the + * setBinaryStream method is used, the driver may have to do extra work to determine + * whether the parameter data should be sent to the server as a LONGVARBINARY or a + * BLOB + * + * @param parameterIndex index of the first parameter is 1, the second is 2, ... + * @param inputStream An object that contains the data to set the parameter value to. + * @param length the number of bytes in the parameter data. + * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL + * statement; if a database access error occurs; this method is called on a closed + * PreparedStatement; if the length specified is less than zero or if the number of + * bytes in the inputstream does not match the specified length. + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 + */ + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { - setClob(parameterIndex, reader, length); + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, inputStream, length)); } /** - * Sets the designated parameter to a Reader object. This method differs from the - * setCharacterStream (int, Reader) method because it informs the driver that the - * parameter value should be sent to the server as a NCLOB. When the + * Sets the designated parameter to a Reader object. The reader must contain the + * number of characters specified by length otherwise a SQLException will be + * generated when the PreparedStatement is executed. This method differs from the + * setCharacterStream (int, Reader, int) method because it informs the driver that + * the parameter value should be sent to the server as a NCLOB. When the * setCharacterStream method is used, the driver may have to do extra work to determine * whether the parameter data should be sent to the server as a LONGNVARCHAR or a * NCLOB * - *

Note: Consult your JDBC driver documentation to determine if it might be more - * efficient to use a version of setNClob which takes a length parameter. - * * @param parameterIndex index of the first parameter is 1, the second is 2, ... * @param reader An object that contains the data to set the parameter value to. + * @param length the number of characters in the parameter data. * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if the driver does not support national character sets; if the driver can detect - * that a data conversion error could occur; if a database access error occurs or this method - * is called on a closed PreparedStatement + * statement; if the length specified is less than zero; if the driver does not support + * national character sets; if the driver can detect that a data conversion error could occur; + * if a database access error occurs or this method is called on a closed + * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setNClob(final int parameterIndex, final Reader reader) throws SQLException { - setClob(parameterIndex, reader); + @Override + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader, length)); } /** * Sets the designated parameter to the given java.sql.SQLXML object. The driver - * converts this to an SQL XML value when it sends it to the database.
+ * converts this to an SQL XML value when it sends it to the database. + * + *

* * @param parameterIndex index of the first parameter is 1, the second is 2, ... * @param xmlObject a SQLXML object that maps an SQL XML value @@ -778,16 +996,16 @@ public void setNClob(final int parameterIndex, final Reader reader) throws SQLEx * statement; if a database access error occurs; this method is called on a closed * PreparedStatement or the java.xml.transform.Result, Writer * or OutputStream has not been closed for the SQLXML object + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ @Override - public void setSQLXML(final int parameterIndex, final SQLXML xmlObject) throws SQLException { - throw exceptionFactory.notSupported("SQlXML not supported"); + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + throw exceptionFactory().notSupported("SQLXML parameter are not supported"); } /** - * Sets the value of the designated parameter with the given object. The second argument must be - * an object type; for integral values, the java.lang equivalent objects should be - * used. + * Sets the value of the designated parameter with the given object. * *

If the second argument is an InputStream then the stream must contain the * number of bytes specified by scaleOrLength. If the second argument is a Reader @@ -805,359 +1023,49 @@ public void setSQLXML(final int parameterIndex, final SQLXML xmlObject) throws S * java.net.URL, or Array, the driver should pass it to the database as a * value of the corresponding SQL type. * - *

Note that this method may be used to pass database-specific abstract data types. - * - * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param obj the object containing the input parameter value - * @param targetSqlType the SQL type (as defined in java.sql.Types) to be sent to the database. - * The scale argument may further qualify this type. - * @param scaleOrLength for java.sql.Types.DECIMAL or java.sql.Types.NUMERIC - * types, this is the number of digits after the decimal point. For - * Java Object types InputStream and Reader, this is the length of - * the data in the stream or reader. For all other types, this value will be ignored. - * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs; this method is called on a closed - * PreparedStatement or if the Java Object specified by x is an InputStream or Reader - * object and the value of the scale parameter is less than zero - * @see Types - */ - public void setObject( - final int parameterIndex, final Object obj, final int targetSqlType, final int scaleOrLength) - throws SQLException { - setInternalObject(parameterIndex, obj, targetSqlType, scaleOrLength); - } - - /** - * Sets the value of the designated parameter with the given object. This method is like the - * method setObject above, except that it assumes a scale of zero. - * - * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param obj the object containing the input parameter value - * @param targetSqlType the SQL type (as defined in java.sql.Types) to be sent to the database - * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs or this method is called on a closed - * PreparedStatements - * @see Types - */ - public void setObject(final int parameterIndex, final Object obj, final int targetSqlType) - throws SQLException { - setInternalObject(parameterIndex, obj, targetSqlType, Long.MAX_VALUE); - } - - /** - * Sets the value of the designated parameter using the given object. The second parameter must be - * of type Object; therefore, the java.lang equivalent objects should be - * used for built-in types. - * - *

The JDBC specification specifies a standard mapping from Java Object types to - * SQL types. The given argument will be converted to the corresponding SQL type before being sent - * to the database. + *

Note that this method may be used to pass database-specific abstract data types. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param obj the object containing the input parameter value + * @param x the object containing the input parameter value + * @param targetSqlType the SQL type (as defined in java.sql.Types) to be sent to the database. + * The scale argument may further qualify this type. + * @param scaleOrLength for java.sql.Types.DECIMAL or + * java.sql.Types.NUMERIC types, this is the number of digits after the decimal point. + * For Java Object types InputStream and Reader, this is the length + * of the data in the stream or reader. For all other types, this value will be ignored. * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs; this method is called on a closed - * PreparedStatement or the type of the given object is ambiguous + * PreparedStatement or if the Java Object specified by x is an InputStream or Reader + * object and the value of the scale parameter is less than zero + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * targetSqlType + * @see Types */ - public void setObject(final int parameterIndex, final Object obj) throws SQLException { - if (obj == null) { - setNull(parameterIndex, Types.INTEGER); - } else if (obj instanceof String) { - setString(parameterIndex, (String) obj); - } else if (obj instanceof Integer) { - setInt(parameterIndex, (Integer) obj); - } else if (obj instanceof Long) { - setLong(parameterIndex, (Long) obj); - } else if (obj instanceof Short) { - setShort(parameterIndex, (Short) obj); - } else if (obj instanceof Double) { - setDouble(parameterIndex, (Double) obj); - } else if (obj instanceof Float) { - setFloat(parameterIndex, (Float) obj); - } else if (obj instanceof Byte) { - setByte(parameterIndex, (Byte) obj); - } else if (obj instanceof byte[]) { - setBytes(parameterIndex, (byte[]) obj); - } else if (obj instanceof Date) { - setDate(parameterIndex, (Date) obj); - } else if (obj instanceof Time) { - setTime(parameterIndex, (Time) obj); - } else if (obj instanceof Timestamp) { - setTimestamp(parameterIndex, (Timestamp) obj); - } else if (obj instanceof java.util.Date) { - setTimestamp(parameterIndex, new Timestamp(((java.util.Date) obj).getTime())); - } else if (obj instanceof Boolean) { - setBoolean(parameterIndex, (Boolean) obj); - } else if (obj instanceof Blob) { - setBlob(parameterIndex, (Blob) obj); - } else if (obj instanceof InputStream) { - setBinaryStream(parameterIndex, (InputStream) obj); - } else if (obj instanceof Reader) { - setCharacterStream(parameterIndex, (Reader) obj); - } else if (obj instanceof BigDecimal) { - setBigDecimal(parameterIndex, (BigDecimal) obj); - } else if (obj instanceof BigInteger) { - setString(parameterIndex, obj.toString()); - } else if (obj instanceof Clob) { - setClob(parameterIndex, (Clob) obj); - } else if (obj instanceof LocalDateTime) { - setTimestamp(parameterIndex, Timestamp.valueOf((LocalDateTime) obj)); - } else if (obj instanceof Instant) { - setTimestamp(parameterIndex, Timestamp.from((Instant) obj)); - } else if (obj instanceof LocalDate) { - setDate(parameterIndex, Date.valueOf((LocalDate) obj)); - } else if (obj instanceof OffsetDateTime) { - setParameter( - parameterIndex, - new ZonedDateTimeParameter( - ((OffsetDateTime) obj).toZonedDateTime(), - protocol.getTimeZone().toZoneId(), - useFractionalSeconds, - options)); - } else if (obj instanceof OffsetTime) { - setParameter( - parameterIndex, - new OffsetTimeParameter( - (OffsetTime) obj, protocol.getTimeZone().toZoneId(), useFractionalSeconds, options)); - } else if (obj instanceof ZonedDateTime) { - setParameter( - parameterIndex, - new ZonedDateTimeParameter( - (ZonedDateTime) obj, - protocol.getTimeZone().toZoneId(), - useFractionalSeconds, - options)); - } else if (obj instanceof LocalTime) { - setParameter(parameterIndex, new LocalTimeParameter((LocalTime) obj, useFractionalSeconds)); - } else { - // fallback to sending serialized object - setParameter(parameterIndex, new SerializableParameter(obj, noBackslashEscapes)); - hasLongData = true; - } - } - @Override - public void setObject(int parameterIndex, Object obj, SQLType targetSqlType, int scaleOrLength) + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { - setObject(parameterIndex, obj, targetSqlType.getVendorTypeNumber(), scaleOrLength); - } - - @Override - public void setObject(int parameterIndex, Object obj, SQLType targetSqlType) throws SQLException { - setObject(parameterIndex, obj, targetSqlType.getVendorTypeNumber()); + setInternalObject(parameterIndex, x, (long) scaleOrLength); } - private void setInternalObject( - final int parameterIndex, final Object obj, final int targetSqlType, final long scaleOrLength) + @SuppressWarnings({"unchecked", "rawtypes"}) + private void setInternalObject(int parameterIndex, Object x, Long scaleOrLength) throws SQLException { - switch (targetSqlType) { - case Types.ARRAY: - case Types.DATALINK: - case Types.JAVA_OBJECT: - case Types.REF: - case Types.ROWID: - case Types.SQLXML: - case Types.STRUCT: - throw exceptionFactory.notSupported("Type not supported"); - default: - break; + checkNotClosed(); + checkIndex(parameterIndex); + if (x == null) { + parameters.set(parameterIndex - 1, Parameter.NULL_PARAMETER); + return; } - if (obj == null) { - setNull(parameterIndex, Types.INTEGER); - } else if (obj instanceof String) { - if (targetSqlType == Types.BLOB) { - throw exceptionFactory.create("Cannot convert a String to a Blob"); - } - String str = (String) obj; - try { - switch (targetSqlType) { - case Types.BIT: - case Types.BOOLEAN: - setBoolean(parameterIndex, !("false".equalsIgnoreCase(str) || "0".equals(str))); - break; - case Types.TINYINT: - setByte(parameterIndex, Byte.parseByte(str)); - break; - case Types.SMALLINT: - setShort(parameterIndex, Short.parseShort(str)); - break; - case Types.INTEGER: - setInt(parameterIndex, Integer.parseInt(str)); - break; - case Types.DOUBLE: - case Types.FLOAT: - setDouble(parameterIndex, Double.valueOf(str)); - break; - case Types.REAL: - setFloat(parameterIndex, Float.valueOf(str)); - break; - case Types.BIGINT: - setLong(parameterIndex, Long.valueOf(str)); - break; - case Types.DECIMAL: - case Types.NUMERIC: - setBigDecimal(parameterIndex, new BigDecimal(str)); - break; - case Types.CLOB: - case Types.NCLOB: - case Types.CHAR: - case Types.VARCHAR: - case Types.LONGVARCHAR: - case Types.NCHAR: - case Types.NVARCHAR: - case Types.LONGNVARCHAR: - setString(parameterIndex, str); - break; - case Types.TIMESTAMP: - if (str.startsWith("0000-00-00")) { - setTimestamp(parameterIndex, null); - } else { - setTimestamp(parameterIndex, Timestamp.valueOf(str)); - } - break; - case Types.TIME: - setTime(parameterIndex, Time.valueOf((String) obj)); - break; - case Types.TIME_WITH_TIMEZONE: - setParameter( - parameterIndex, - new OffsetTimeParameter( - OffsetTime.parse(str), - protocol.getTimeZone().toZoneId(), - useFractionalSeconds, - options)); - break; - case Types.TIMESTAMP_WITH_TIMEZONE: - setParameter( - parameterIndex, - new ZonedDateTimeParameter( - ZonedDateTime.parse(str, SPEC_ISO_ZONED_DATE_TIME), - protocol.getTimeZone().toZoneId(), - useFractionalSeconds, - options)); - break; - default: - throw exceptionFactory.create( - String.format("Could not convert [%s] to %s", str, targetSqlType)); - } - } catch (IllegalArgumentException e) { - throw exceptionFactory.create( - String.format("Could not convert [%s] to %s", str, targetSqlType), e); - } - } else if (obj instanceof Number) { - Number bd = (Number) obj; - switch (targetSqlType) { - case Types.TINYINT: - setByte(parameterIndex, bd.byteValue()); - break; - case Types.SMALLINT: - setShort(parameterIndex, bd.shortValue()); - break; - case Types.INTEGER: - setInt(parameterIndex, bd.intValue()); - break; - case Types.BIGINT: - setLong(parameterIndex, bd.longValue()); - break; - case Types.FLOAT: - case Types.DOUBLE: - setDouble(parameterIndex, bd.doubleValue()); - break; - case Types.REAL: - setFloat(parameterIndex, bd.floatValue()); - break; - case Types.DECIMAL: - case Types.NUMERIC: - if (obj instanceof BigDecimal) { - setBigDecimal(parameterIndex, (BigDecimal) obj); - } else if (obj instanceof Double || obj instanceof Float) { - setDouble(parameterIndex, bd.doubleValue()); - } else { - setLong(parameterIndex, bd.longValue()); - } - break; - case Types.BIT: - setBoolean(parameterIndex, bd.shortValue() != 0); - break; - case Types.CHAR: - case Types.VARCHAR: - setString(parameterIndex, bd.toString()); - break; - default: - throw exceptionFactory.create( - String.format("Could not convert [%s] to %s", bd, targetSqlType)); - } - } else if (obj instanceof byte[]) { - if (targetSqlType == Types.BINARY - || targetSqlType == Types.VARBINARY - || targetSqlType == Types.LONGVARBINARY) { - setBytes(parameterIndex, (byte[]) obj); - } else { - throw exceptionFactory.create( - "Can only convert a byte[] to BINARY, VARBINARY or LONGVARBINARY"); - } - - } else if (obj instanceof Time) { - setTime(parameterIndex, (Time) obj); // it is just a string anyway - } else if (obj instanceof Timestamp) { - setTimestamp(parameterIndex, (Timestamp) obj); - } else if (obj instanceof Date) { - setDate(parameterIndex, (Date) obj); - } else if (obj instanceof java.util.Date) { - long timemillis = ((java.util.Date) obj).getTime(); - if (targetSqlType == Types.DATE) { - setDate(parameterIndex, new Date(timemillis)); - } else if (targetSqlType == Types.TIME) { - setTime(parameterIndex, new Time(timemillis)); - } else if (targetSqlType == Types.TIMESTAMP) { - setTimestamp(parameterIndex, new Timestamp(timemillis)); + for (Codec codec : Codecs.LIST) { + if (codec.canEncode(x)) { + Parameter p = new Parameter(codec, x, scaleOrLength); + parameters.set(parameterIndex - 1, p); + return; } - } else if (obj instanceof Boolean) { - setBoolean(parameterIndex, (Boolean) obj); - } else if (obj instanceof Blob) { - setBlob(parameterIndex, (Blob) obj); - } else if (obj instanceof Clob) { - setClob(parameterIndex, (Clob) obj); - } else if (obj instanceof InputStream) { - setBinaryStream(parameterIndex, (InputStream) obj, scaleOrLength); - } else if (obj instanceof Reader) { - setCharacterStream(parameterIndex, (Reader) obj, scaleOrLength); - } else if (obj instanceof LocalDateTime) { - setTimestamp(parameterIndex, Timestamp.valueOf((LocalDateTime) obj)); - } else if (obj instanceof Instant) { - setTimestamp(parameterIndex, Timestamp.from((Instant) obj)); - } else if (obj instanceof LocalDate) { - setDate(parameterIndex, Date.valueOf((LocalDate) obj)); - } else if (obj instanceof OffsetDateTime) { - setParameter( - parameterIndex, - new ZonedDateTimeParameter( - ((OffsetDateTime) obj).toZonedDateTime(), - protocol.getTimeZone().toZoneId(), - useFractionalSeconds, - options)); - } else if (obj instanceof OffsetTime) { - setParameter( - parameterIndex, - new OffsetTimeParameter( - (OffsetTime) obj, protocol.getTimeZone().toZoneId(), useFractionalSeconds, options)); - } else if (obj instanceof ZonedDateTime) { - setParameter( - parameterIndex, - new ZonedDateTimeParameter( - (ZonedDateTime) obj, - protocol.getTimeZone().toZoneId(), - useFractionalSeconds, - options)); - } else if (obj instanceof LocalTime) { - setParameter(parameterIndex, new LocalTimeParameter((LocalTime) obj, useFractionalSeconds)); - } else { - throw exceptionFactory.create( - String.format( - "Could not set parameter in setObject, could not convert: %s to %s", - obj.getClass(), targetSqlType)); } + + throw new SQLException(String.format("Type %s not supported type", x.getClass().getName())); } /** @@ -1171,110 +1079,99 @@ private void setInternalObject( * subclass that implements the standard interface. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param stream the Java input stream that contains the ASCII parameter value + * @param x the Java input stream that contains the ASCII parameter value * @param length the number of bytes in the stream * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement + * @since 1.6 */ - public void setAsciiStream(final int parameterIndex, final InputStream stream, final long length) - throws SQLException { - if (stream == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; - } - setParameter(parameterIndex, new StreamParameter(stream, length, noBackslashEscapes)); - hasLongData = true; + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, length)); } /** - * This function reads up the entire stream and stores it in memory since we need to know the - * length when sending it to the server use the corresponding method with a length parameter if - * memory is an issue
- * Sets the designated parameter to the given input stream. When a very large ASCII value is input - * to a LONGVARCHAR parameter, it may be more practical to send it via a - * java.io.InputStream. Data will be read from the stream as needed until end-of-file is - * reached. The JDBC driver will do any necessary conversion from ASCII to the database char - * format. + * Sets the designated parameter to the given input stream, which will have the specified number + * of bytes. When a very large binary value is input to a LONGVARBINARY parameter, it + * may be more practical to send it via a java.io.InputStream object. The data will + * be read from the stream as needed until end-of-file is reached. * *

Note: This stream object can either be a standard Java stream object or your own * subclass that implements the standard interface. * - *

Note: Consult your JDBC driver documentation to determine if it might be more - * efficient to use a version of setAsciiStream which takes a length parameter. - * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param stream the Java input stream that contains the ASCII parameter value + * @param x the java input stream which contains the binary parameter value + * @param length the number of bytes in the stream * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement + * @since 1.6 */ - public void setAsciiStream(final int parameterIndex, final InputStream stream) - throws SQLException { - if (stream == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; - } - setParameter(parameterIndex, new StreamParameter(stream, noBackslashEscapes)); - hasLongData = true; + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, length)); } /** - * Sets the designated parameter to the given input stream, which will have the specified number - * of bytes. When a very large ASCII value is input to a LONGVARCHAR parameter, it - * may be more practical to send it via a java.io.InputStream. Data will be read from - * the stream as needed until end-of-file is reached. The JDBC driver will do any necessary - * conversion from ASCII to the database char format. + * Sets the designated parameter to the given Reader object, which is the given + * number of characters long. When a very large UNICODE value is input to a LONGVARCHAR + * parameter, it may be more practical to send it via a java.io.Reader + * object. The data will be read from the stream as needed until end-of-file is reached. The JDBC + * driver will do any necessary conversion from UNICODE to the database char format. * *

Note: This stream object can either be a standard Java stream object or your own * subclass that implements the standard interface. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param stream the Java input stream that contains the ASCII parameter value - * @param length the number of bytes in the stream + * @param reader the java.io.Reader object that contains the Unicode data + * @param length the number of characters in the stream * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement + * @since 1.6 */ - public void setAsciiStream(final int parameterIndex, final InputStream stream, final int length) + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { - if (stream == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; - } - setParameter(parameterIndex, new StreamParameter(stream, length, noBackslashEscapes)); - hasLongData = true; + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader, length)); } /** - * Sets the designated parameter to the given input stream, which will have the specified number - * of bytes. When a very large binary value is input to a LONGVARBINARY parameter, it - * may be more practical to send it via a java.io.InputStream object. The data will - * be read from the stream as needed until end-of-file is reached. + * Sets the designated parameter to the given input stream. When a very large ASCII value is input + * to a LONGVARCHAR parameter, it may be more practical to send it via a + * java.io.InputStream. Data will be read from the stream as needed until end-of-file is + * reached. The JDBC driver will do any necessary conversion from ASCII to the database char + * format. * *

Note: This stream object can either be a standard Java stream object or your own * subclass that implements the standard interface. * + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setAsciiStream which takes a length parameter. + * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param stream the java input stream which contains the binary parameter value - * @param length the number of bytes in the stream + * @param x the Java input stream that contains the ASCII parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setBinaryStream(final int parameterIndex, final InputStream stream, final long length) - throws SQLException { - if (stream == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; - } - setParameter(parameterIndex, new StreamParameter(stream, length, noBackslashEscapes)); - hasLongData = true; + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x)); } /** - * This function reads up the entire stream and stores it in memory since we need to know the - * length when sending it to the server
* Sets the designated parameter to the given input stream. When a very large binary value is * input to a LONGVARBINARY parameter, it may be more practical to send it via a * java.io.InputStream object. The data will be read from the stream as needed until @@ -1287,222 +1184,223 @@ public void setBinaryStream(final int parameterIndex, final InputStream stream, * efficient to use a version of setBinaryStream which takes a length parameter. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param stream the java input stream which contains the binary parameter value + * @param x the java input stream which contains the binary parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setBinaryStream(final int parameterIndex, final InputStream stream) - throws SQLException { - if (stream == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; - } - setParameter(parameterIndex, new StreamParameter(stream, noBackslashEscapes)); - hasLongData = true; + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x)); } /** - * Sets the designated parameter to the given input stream, which will have the specified number - * of bytes. When a very large binary value is input to a LONGVARBINARY parameter, it - * may be more practical to send it via a java.io.InputStream object. The data will - * be read from the stream as needed until end-of-file is reached. + * Sets the designated parameter to the given Reader object. When a very large + * UNICODE value is input to a LONGVARCHAR parameter, it may be more practical to + * send it via a java.io.Reader object. The data will be read from the stream as + * needed until end-of-file is reached. The JDBC driver will do any necessary conversion from + * UNICODE to the database char format. * *

Note: This stream object can either be a standard Java stream object or your own * subclass that implements the standard interface. * + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setCharacterStream which takes a length parameter. + * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param stream the java input stream which contains the binary parameter value - * @param length the number of bytes in the stream + * @param reader the java.io.Reader object that contains the Unicode data * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL * statement; if a database access error occurs or this method is called on a closed * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setBinaryStream(final int parameterIndex, final InputStream stream, final int length) - throws SQLException { - if (stream == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; - } - setParameter(parameterIndex, new StreamParameter(stream, length, noBackslashEscapes)); - hasLongData = true; + @Override + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader)); } /** - * Sets the designated parameter to the given Java boolean value. The driver converts - * this to an SQL BIT or BOOLEAN value when it sends it to the database. + * Sets the designated parameter to a Reader object. The Reader reads + * the data till end-of-file is reached. The driver does the necessary conversion from Java + * character format to the national character set in the database. * - * @param parameterIndex the first parameter is 1, the second is 2, ... + *

Note: This stream object can either be a standard Java stream object or your own + * subclass that implements the standard interface. + * + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setNCharacterStream which takes a length parameter. + * + * @param parameterIndex of the first parameter is 1, the second is 2, ... * @param value the parameter value * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs or this method is called on a closed - * PreparedStatement + * statement; if the driver does not support national character sets; if the driver can detect + * that a data conversion error could occur; if a database access error occurs; or this method + * is called on a closed PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setBoolean(final int parameterIndex, final boolean value) throws SQLException { - setParameter(parameterIndex, new BooleanParameter(value)); + @Override + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, value)); } /** - * Sets the designated parameter to the given Java byte value. The driver converts - * this to an SQL TINYINT value when it sends it to the database. + * Sets the designated parameter to a Reader object. This method differs from the + * setCharacterStream (int, Reader) method because it informs the driver that the + * parameter value should be sent to the server as a CLOB. When the + * setCharacterStream method is used, the driver may have to do extra work to determine + * whether the parameter data should be sent to the server as a LONGVARCHAR or a + * CLOB * - * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param bit the parameter value + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setClob which takes a length parameter. + * + * @param parameterIndex index of the first parameter is 1, the second is 2, ... + * @param reader An object that contains the data to set the parameter value to. * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs or this method is called on a closed - * PreparedStatement + * statement; if a database access error occurs; this method is called on a closed + * PreparedStatementor if parameterIndex does not correspond to a parameter marker in + * the SQL statement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setByte(final int parameterIndex, final byte bit) throws SQLException { - setParameter(parameterIndex, new ByteParameter(bit)); + @Override + public void setClob(int parameterIndex, Reader reader) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader)); } /** - * Sets the designated parameter to the given Java short value. The driver converts - * this to an SQL SMALLINT value when it sends it to the database. + * Sets the designated parameter to a InputStream object. This method differs from + * the setBinaryStream (int, InputStream) method because it informs the driver that + * the parameter value should be sent to the server as a BLOB. When the + * setBinaryStream method is used, the driver may have to do extra work to determine + * whether the parameter data should be sent to the server as a LONGVARBINARY or a + * BLOB * - * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param value the parameter value + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setBlob which takes a length parameter. + * + * @param parameterIndex index of the first parameter is 1, the second is 2, ... + * @param inputStream An object that contains the data to set the parameter value to. * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs or this method is called on a closed - * PreparedStatement + * statement; if a database access error occurs; this method is called on a closed + * PreparedStatement or if parameterIndex does not correspond to a parameter marker in + * the SQL statement, + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setShort(final int parameterIndex, final short value) throws SQLException { - setParameter(parameterIndex, new ShortParameter(value)); + @Override + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, inputStream)); } /** - * Set string parameter. + * Sets the designated parameter to a Reader object. This method differs from the + * setCharacterStream (int, Reader) method because it informs the driver that the + * parameter value should be sent to the server as a NCLOB. When the + * setCharacterStream method is used, the driver may have to do extra work to determine + * whether the parameter data should be sent to the server as a LONGNVARCHAR or a + * NCLOB * - * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param str String + *

Note: Consult your JDBC driver documentation to determine if it might be more + * efficient to use a version of setNClob which takes a length parameter. + * + * @param parameterIndex index of the first parameter is 1, the second is 2, ... + * @param reader An object that contains the data to set the parameter value to. * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs or this method is called on a closed - * PreparedStatement + * statement; if the driver does not support national character sets; if the driver can detect + * that a data conversion error could occur; if a database access error occurs or this method + * is called on a closed PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.6 */ - public void setString(final int parameterIndex, final String str) throws SQLException { - if (str == null) { - setNull(parameterIndex, ColumnType.VARCHAR); - return; - } - - setParameter(parameterIndex, new StringParameter(str, noBackslashEscapes)); + @Override + public void setNClob(int parameterIndex, Reader reader) throws SQLException { + checkNotClosed(); + checkIndex(parameterIndex); + parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader)); } /** - * Sets the designated parameter to the given Java array of bytes. The driver converts this to an - * SQL VARBINARY or LONGVARBINARY (depending on the argument's size - * relative to the driver's limits on VARBINARY values) when it sends it to the + * Sets the value of the designated parameter with the given object. + * + *

If the second argument is an {@code InputStream} then the stream must contain the number of + * bytes specified by scaleOrLength. If the second argument is a {@code Reader} then the reader + * must contain the number of characters specified by scaleOrLength. If these conditions are not + * true the driver will generate a {@code SQLException} when the prepared statement is executed. + * + *

The given Java object will be converted to the given targetSqlType before being sent to the * database. * - * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param bytes the parameter value - * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs or this method is called on a closed - * PreparedStatement - */ - public void setBytes(final int parameterIndex, final byte[] bytes) throws SQLException { - if (bytes == null) { - setNull(parameterIndex, ColumnType.BLOB); - return; - } - - setParameter(parameterIndex, new ByteArrayParameter(bytes, noBackslashEscapes)); - } - - /** - * Sets the designated parameter to the given input stream, which will have the specified number - * of bytes.
- * When a very large Unicode value is input to a LONGVARCHAR parameter, it may be - * more practical to send it via a java.io.InputStream object. The data will be read - * from the stream as needed until end-of-file is reached. The JDBC driver will do any necessary - * conversion from Unicode to the database char format.
- * The byte format of the Unicode stream must be a Java UTF-8, as defined in the Java Virtual - * Machine Specification. + *

If the object has a custom mapping (is of a class implementing the interface {@code + * SQLData}), the JDBC driver should call the method {@code SQLData.writeSQL} to write it to the + * SQL data stream. If, on the other hand, the object is of a class implementing {@code Ref}, + * {@code Blob}, {@code Clob}, {@code NClob}, {@code Struct}, {@code java.net.URL}, or {@code + * Array}, the driver should pass it to the database as a value of the corresponding SQL type. * - *

Note: This stream object can either be a standard Java stream object or your own - * subclass that implements the standard interface. + *

Note that this method may be used to pass database-specific abstract data types. * - * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param x a java.io.InputStream object that contains the Unicode parameter value - * @param length the number of bytes in the stream - * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs or this method is called on a closed - * PreparedStatement - * @deprecated deprecated - */ - public void setUnicodeStream(final int parameterIndex, final InputStream x, final int length) - throws SQLException { - if (x == null) { - setNull(parameterIndex, Types.BLOB); - return; - } - setParameter(parameterIndex, new StreamParameter(x, length, noBackslashEscapes)); - hasLongData = true; - } - - public void setInt(final int column, final int value) throws SQLException { - setParameter(column, new IntParameter(value)); - } - - /** - * Sets the designated parameter to the given Java long value. The driver converts - * this to an SQL BIGINT value when it sends it to the database. + *

The default implementation will throw {@code SQLFeatureNotSupportedException} * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param value the parameter value + * @param x the object containing the input parameter value + * @param targetSqlType the SQL type to be sent to the database. The scale argument may further + * qualify this type. + * @param scaleOrLength for {@code java.sql.JDBCType.DECIMAL} or {@code java.sql.JDBCType.NUMERIC + * types}, this is the number of digits after the decimal point. For Java Object types {@code + * InputStream} and {@code Reader}, this is the length of the data in the stream or reader. + * For all other types, this value will be ignored. * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs or this method is called on a closed - * PreparedStatement + * statement; if a database access error occurs or this method is called on a closed {@code + * PreparedStatement} or if the Java Object specified by x is an InputStream or Reader object + * and the value of the scale parameter is less than zero + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * targetSqlType + * @see JDBCType + * @see SQLType + * @since 1.8 */ - public void setLong(final int parameterIndex, final long value) throws SQLException { - setParameter(parameterIndex, new LongParameter(value)); + @Override + public void setObject(int parameterIndex, Object x, SQLType targetSqlType, int scaleOrLength) + throws SQLException { + setInternalObject(parameterIndex, x, (long) scaleOrLength); } /** - * Sets the designated parameter to the given Java float value. The driver converts - * this to an SQL REAL value when it sends it to the database. + * Sets the value of the designated parameter with the given object. * - * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param value the parameter value - * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs or this method is called on a closed - * PreparedStatement - */ - public void setFloat(final int parameterIndex, final float value) throws SQLException { - setParameter(parameterIndex, new FloatParameter(value)); - } - - /** - * Sets the designated parameter to the given Java double value. The driver converts - * this to an SQL DOUBLE value when it sends it to the database. + *

This method is similar to {@link #setObject(int parameterIndex, Object x, SQLType + * targetSqlType, int scaleOrLength)}, except that it assumes a scale of zero. * - * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param value the parameter value - * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs or this method is called on a closed - * PreparedStatement - */ - public void setDouble(final int parameterIndex, final double value) throws SQLException { - setParameter(parameterIndex, new DoubleParameter(value)); - } - - /** - * Sets the designated parameter to the given java.math.BigDecimal value. The driver - * converts this to an SQL NUMERIC value when it sends it to the database. + *

The default implementation will throw {@code SQLFeatureNotSupportedException} * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param bigDecimal the parameter value + * @param x the object containing the input parameter value + * @param targetSqlType the SQL type to be sent to the database * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL - * statement; if a database access error occurs or this method is called on a closed - * PreparedStatement + * statement; if a database access error occurs or this method is called on a closed {@code + * PreparedStatement} + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified + * targetSqlType + * @see JDBCType + * @see SQLType + * @since 1.8 */ - public void setBigDecimal(final int parameterIndex, final BigDecimal bigDecimal) - throws SQLException { - if (bigDecimal == null) { - setNull(parameterIndex, ColumnType.DECIMAL); - return; - } - - setParameter(parameterIndex, new BigDecimalParameter(bigDecimal)); + @Override + public void setObject(int parameterIndex, Object x, SQLType targetSqlType) throws SQLException { + setInternalObject(parameterIndex, x, null); } } diff --git a/src/main/java/org/mariadb/jdbc/BlobOutputStream.java b/src/main/java/org/mariadb/jdbc/BlobOutputStream.java deleted file mode 100644 index 6a1c0eac7..000000000 --- a/src/main/java/org/mariadb/jdbc/BlobOutputStream.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.io.IOException; -import java.io.OutputStream; - -/** Output stream for the blob. */ -class BlobOutputStream extends OutputStream { - - private final MariaDbBlob blob; - private int pos; - - public BlobOutputStream(MariaDbBlob blob, int pos) { - this.blob = blob; - this.pos = pos; - } - - @Override - public void write(int bit) throws IOException { - - if (this.pos >= blob.length) { - byte[] tmp = new byte[2 * blob.length + 1]; - System.arraycopy(blob.data, blob.offset, tmp, 0, blob.length); - blob.data = tmp; - pos -= blob.offset; - blob.offset = 0; - blob.length++; - } - blob.data[pos] = (byte) bit; - pos++; - } - - @Override - public void write(byte[] buf, int off, int len) throws IOException { - if (off < 0) { - throw new IOException("Invalid offset " + off); - } - int realLen = Math.min(buf.length - off, len); - if (pos + realLen >= blob.length) { - int newLen = 2 * blob.length + realLen; - byte[] tmp = new byte[newLen]; - System.arraycopy(blob.data, blob.offset, tmp, 0, blob.length); - blob.data = tmp; - pos -= blob.offset; - blob.offset = 0; - blob.length = pos + realLen; - } - System.arraycopy(buf, off, blob.data, pos, realLen); - pos += realLen; - } - - @Override - public void write(byte[] buf) throws IOException { - write(buf, 0, buf.length); - } -} diff --git a/src/main/java/org/mariadb/jdbc/CallParameter.java b/src/main/java/org/mariadb/jdbc/CallParameter.java deleted file mode 100644 index 6daa331d8..000000000 --- a/src/main/java/org/mariadb/jdbc/CallParameter.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.sql.Types; - -/** Info about in/out parameters. */ -class CallParameter { - - private boolean isInput; - private boolean isOutput; - private int sqlType; - private int outputSqlType; - private int scale; - private String typeName; - private boolean isSigned; - private int canBeNull; - private int precision; - private String className; - private String name; - - public CallParameter() { - sqlType = Types.OTHER; - outputSqlType = Types.OTHER; - } - - public boolean isInput() { - return isInput; - } - - public void setInput(boolean input) { - isInput = input; - } - - public boolean isOutput() { - return isOutput; - } - - public void setOutput(boolean output) { - isOutput = output; - } - - public int getSqlType() { - return sqlType; - } - - public void setSqlType(int sqlType) { - this.sqlType = sqlType; - } - - public int getOutputSqlType() { - return outputSqlType; - } - - public void setOutputSqlType(int outputSqlType) { - this.outputSqlType = outputSqlType; - } - - public int getScale() { - return scale; - } - - public void setScale(int scale) { - this.scale = scale; - } - - public String getTypeName() { - return typeName; - } - - public void setTypeName(String typeName) { - this.typeName = typeName; - } - - public boolean isSigned() { - return isSigned; - } - - public void setSigned(boolean signed) { - isSigned = signed; - } - - public int getCanBeNull() { - return canBeNull; - } - - public void setCanBeNull(int canBeNull) { - this.canBeNull = canBeNull; - } - - public int getPrecision() { - return precision; - } - - public void setPrecision(int precision) { - this.precision = precision; - } - - public String getClassName() { - return className; - } - - public void setClassName(String className) { - this.className = className; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/org/mariadb/jdbc/CallableFunctionStatement.java b/src/main/java/org/mariadb/jdbc/CallableFunctionStatement.java deleted file mode 100644 index c833a179a..000000000 --- a/src/main/java/org/mariadb/jdbc/CallableFunctionStatement.java +++ /dev/null @@ -1,881 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.io.InputStream; -import java.io.Reader; -import java.math.BigDecimal; -import java.net.URL; -import java.sql.*; -import java.util.Calendar; -import java.util.Map; -import org.mariadb.jdbc.internal.ColumnType; -import org.mariadb.jdbc.internal.com.read.resultset.SelectResultSet; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; - -public abstract class CallableFunctionStatement extends ClientSidePreparedStatement - implements CallableStatement { - - protected CallableParameterMetaData parameterMetadata; - /** Information about parameters, merely from registerOutputParameter() and setXXX() calls. */ - private CallParameter[] params; - - /** - * Constructor for getter/setter of callableStatement. - * - * @param connection current connection - * @param sql query - * @param resultSetType a result set type; one of ResultSet.TYPE_FORWARD_ONLY, - * ResultSet.TYPE_SCROLL_INSENSITIVE, or ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of ResultSet.CONCUR_READ_ONLY - * or ResultSet.CONCUR_UPDATABLE - * @param exceptionFactory Exception factory - * @throws SQLException if clientPrepareStatement creation throw an exception - */ - public CallableFunctionStatement( - MariaDbConnection connection, - String sql, - int resultSetType, - final int resultSetConcurrency, - ExceptionFactory exceptionFactory) - throws SQLException { - super( - connection, - sql, - resultSetType, - resultSetConcurrency, - Statement.NO_GENERATED_KEYS, - exceptionFactory); - } - - /** - * Clone data. - * - * @param connection connection - * @return Cloned . - * @throws CloneNotSupportedException if any error occur. - */ - public CallableFunctionStatement clone(MariaDbConnection connection) - throws CloneNotSupportedException { - CallableFunctionStatement clone = (CallableFunctionStatement) super.clone(connection); - clone.params = params; - clone.parameterMetadata = parameterMetadata; - clone.connection = connection; - - return clone; - } - - /** - * Data initialisation when parameterCount is defined. - * - * @param parametersCount number of parameters - */ - public void initFunctionData(int parametersCount) { - params = new CallParameter[parametersCount]; - for (int i = 0; i < parametersCount; i++) { - params[i] = new CallParameter(); - if (i > 0) { - params[i].setInput(true); - } - } - // the query was in the form {?=call function()}, so the first parameter is always output - params[0].setOutput(true); - } - - protected abstract SelectResultSet getResult() throws SQLException; - - public ParameterMetaData getParameterMetaData() throws SQLException { - parameterMetadata.readMetadataFromDbIfRequired(); - return parameterMetadata; - } - - /** - * Convert parameter name to parameter index in the query. - * - * @param parameterName name - * @return index - * @throws SQLException exception - */ - private int nameToIndex(String parameterName) throws SQLException { - parameterMetadata.readMetadataFromDbIfRequired(); - for (int i = 1; i <= parameterMetadata.getParameterCount(); i++) { - String name = parameterMetadata.getName(i); - if (name != null && name.equalsIgnoreCase(parameterName)) { - return i; - } - } - throw exceptionFactory.create("there is no parameter with the name " + parameterName); - } - - /** - * Convert parameter name to output parameter index in the query. - * - * @param parameterName name - * @return index - * @throws SQLException exception - */ - private int nameToOutputIndex(String parameterName) throws SQLException { - for (int i = 0; i < parameterMetadata.getParameterCount(); i++) { - String name = parameterMetadata.getName(i); - if (name != null && name.equalsIgnoreCase(parameterName)) { - return i; - } - } - throw exceptionFactory.create("there is no parameter with the name " + parameterName); - } - - /** - * Convert parameter index to corresponding outputIndex. - * - * @param parameterIndex index - * @return index - */ - private int indexToOutputIndex(int parameterIndex) { - return parameterIndex; - } - - @Override - public boolean wasNull() throws SQLException { - return getResult().wasNull(); - } - - @Override - public String getString(int parameterIndex) throws SQLException { - return getResult().getString(indexToOutputIndex(parameterIndex)); - } - - @Override - public String getString(String parameterName) throws SQLException { - return getResult().getString(nameToOutputIndex(parameterName)); - } - - @Override - public boolean getBoolean(int parameterIndex) throws SQLException { - return getResult().getBoolean(indexToOutputIndex(parameterIndex)); - } - - @Override - public boolean getBoolean(String parameterName) throws SQLException { - return getResult().getBoolean(nameToOutputIndex(parameterName)); - } - - @Override - public byte getByte(int parameterIndex) throws SQLException { - return getResult().getByte(indexToOutputIndex(parameterIndex)); - } - - @Override - public byte getByte(String parameterName) throws SQLException { - return getResult().getByte(nameToOutputIndex(parameterName)); - } - - @Override - public short getShort(int parameterIndex) throws SQLException { - return getResult().getShort(indexToOutputIndex(parameterIndex)); - } - - @Override - public short getShort(String parameterName) throws SQLException { - return getResult().getShort(nameToOutputIndex(parameterName)); - } - - @Override - public int getInt(String parameterName) throws SQLException { - return getResult().getInt(nameToOutputIndex(parameterName)); - } - - @Override - public int getInt(int parameterIndex) throws SQLException { - return getResult().getInt(indexToOutputIndex(parameterIndex)); - } - - @Override - public long getLong(String parameterName) throws SQLException { - return getResult().getLong(nameToOutputIndex(parameterName)); - } - - @Override - public long getLong(int parameterIndex) throws SQLException { - return getResult().getLong(indexToOutputIndex(parameterIndex)); - } - - @Override - public float getFloat(String parameterName) throws SQLException { - return getResult().getFloat(nameToOutputIndex(parameterName)); - } - - @Override - public float getFloat(int parameterIndex) throws SQLException { - return getResult().getFloat(indexToOutputIndex(parameterIndex)); - } - - @Override - public double getDouble(int parameterIndex) throws SQLException { - return getResult().getDouble(indexToOutputIndex(parameterIndex)); - } - - @Override - public double getDouble(String parameterName) throws SQLException { - return getResult().getDouble(nameToOutputIndex(parameterName)); - } - - @Override - @Deprecated - @SuppressWarnings("deprecation") - public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException { - return getResult().getBigDecimal(indexToOutputIndex(parameterIndex)); - } - - @Override - public BigDecimal getBigDecimal(int parameterIndex) throws SQLException { - return getResult().getBigDecimal(indexToOutputIndex(parameterIndex)); - } - - @Override - public BigDecimal getBigDecimal(String parameterName) throws SQLException { - return getResult().getBigDecimal(nameToOutputIndex(parameterName)); - } - - @Override - public byte[] getBytes(String parameterName) throws SQLException { - return getResult().getBytes(nameToOutputIndex(parameterName)); - } - - @Override - public byte[] getBytes(int parameterIndex) throws SQLException { - return getResult().getBytes(indexToOutputIndex(parameterIndex)); - } - - @Override - public Date getDate(int parameterIndex) throws SQLException { - return getResult().getDate(indexToOutputIndex(parameterIndex)); - } - - @Override - public Date getDate(String parameterName) throws SQLException { - return getResult().getDate(nameToOutputIndex(parameterName)); - } - - @Override - public Date getDate(String parameterName, Calendar cal) throws SQLException { - return getResult().getDate(nameToOutputIndex(parameterName), cal); - } - - @Override - public Date getDate(int parameterIndex, Calendar cal) throws SQLException { - return getResult().getDate(parameterIndex, cal); - } - - @Override - public Time getTime(int parameterIndex, Calendar cal) throws SQLException { - return getResult().getTime(indexToOutputIndex(parameterIndex), cal); - } - - @Override - public Time getTime(String parameterName) throws SQLException { - return getResult().getTime(nameToOutputIndex(parameterName)); - } - - @Override - public Time getTime(String parameterName, Calendar cal) throws SQLException { - return getResult().getTime(nameToOutputIndex(parameterName), cal); - } - - @Override - public Time getTime(int parameterIndex) throws SQLException { - return getResult().getTime(indexToOutputIndex(parameterIndex)); - } - - @Override - public Timestamp getTimestamp(int parameterIndex) throws SQLException { - return getResult().getTimestamp(parameterIndex); - } - - @Override - public Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException { - return getResult().getTimestamp(indexToOutputIndex(parameterIndex), cal); - } - - @Override - public Timestamp getTimestamp(String parameterName) throws SQLException { - return getResult().getTimestamp(nameToOutputIndex(parameterName)); - } - - @Override - public Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException { - return getResult().getTimestamp(nameToOutputIndex(parameterName), cal); - } - - @Override - public Object getObject(int parameterIndex) throws SQLException { - Class classType = - ColumnType.classFromJavaType(getParameter(parameterIndex).getOutputSqlType()); - if (classType != null) { - return getResult().getObject(indexToOutputIndex(parameterIndex), classType); - } - return getResult().getObject(indexToOutputIndex(parameterIndex)); - } - - @Override - public Object getObject(String parameterName) throws SQLException { - int index = nameToIndex(parameterName); - Class classType = ColumnType.classFromJavaType(getParameter(index).getOutputSqlType()); - if (classType != null) { - return getResult().getObject(indexToOutputIndex(index), classType); - } - return getResult().getObject(indexToOutputIndex(index)); - } - - @Override - public Object getObject(int parameterIndex, Map> map) throws SQLException { - return getResult().getObject(indexToOutputIndex(parameterIndex), map); - } - - @Override - public Object getObject(String parameterName, Map> map) throws SQLException { - return getResult().getObject(nameToOutputIndex(parameterName), map); - } - - @Override - public T getObject(int parameterIndex, Class type) throws SQLException { - return getResult().getObject(indexToOutputIndex(parameterIndex), type); - } - - @Override - public T getObject(String parameterName, Class type) throws SQLException { - return getResult().getObject(nameToOutputIndex(parameterName), type); - } - - @Override - public Ref getRef(int parameterIndex) throws SQLException { - return getResult().getRef(indexToOutputIndex(parameterIndex)); - } - - @Override - public Ref getRef(String parameterName) throws SQLException { - return getResult().getRef(nameToOutputIndex(parameterName)); - } - - @Override - public Blob getBlob(int parameterIndex) throws SQLException { - return getResult().getBlob(parameterIndex); - } - - @Override - public Blob getBlob(String parameterName) throws SQLException { - return getResult().getBlob(nameToOutputIndex(parameterName)); - } - - @Override - public Clob getClob(String parameterName) throws SQLException { - return getResult().getClob(nameToOutputIndex(parameterName)); - } - - @Override - public Clob getClob(int parameterIndex) throws SQLException { - return getResult().getClob(indexToOutputIndex(parameterIndex)); - } - - @Override - public Array getArray(String parameterName) throws SQLException { - return getResult().getArray(nameToOutputIndex(parameterName)); - } - - @Override - public Array getArray(int parameterIndex) throws SQLException { - return getResult().getArray(indexToOutputIndex(parameterIndex)); - } - - @Override - public URL getURL(int parameterIndex) throws SQLException { - return getResult().getURL(indexToOutputIndex(parameterIndex)); - } - - @Override - public URL getURL(String parameterName) throws SQLException { - return getResult().getURL(nameToOutputIndex(parameterName)); - } - - @Override - public RowId getRowId(int parameterIndex) throws SQLException { - throw exceptionFactory.notSupported("RowIDs not supported"); - } - - @Override - public RowId getRowId(String parameterName) throws SQLException { - throw exceptionFactory.notSupported("RowIDs not supported"); - } - - @Override - public NClob getNClob(int parameterIndex) throws SQLException { - return getResult().getNClob(indexToOutputIndex(parameterIndex)); - } - - @Override - public NClob getNClob(String parameterName) throws SQLException { - return getResult().getNClob(nameToOutputIndex(parameterName)); - } - - @Override - public SQLXML getSQLXML(int parameterIndex) throws SQLException { - throw exceptionFactory.notSupported("SQLXML not supported"); - } - - @Override - public SQLXML getSQLXML(String parameterName) throws SQLException { - throw exceptionFactory.notSupported("SQLXML not supported"); - } - - @Override - public String getNString(int parameterIndex) throws SQLException { - return getResult().getNString(indexToOutputIndex(parameterIndex)); - } - - @Override - public String getNString(String parameterName) throws SQLException { - return getResult().getNString(nameToOutputIndex(parameterName)); - } - - @Override - public Reader getNCharacterStream(int parameterIndex) throws SQLException { - return getResult().getNCharacterStream(indexToOutputIndex(parameterIndex)); - } - - @Override - public Reader getNCharacterStream(String parameterName) throws SQLException { - return getResult().getNCharacterStream(nameToOutputIndex(parameterName)); - } - - @Override - public Reader getCharacterStream(int parameterIndex) throws SQLException { - return getResult().getCharacterStream(indexToOutputIndex(parameterIndex)); - } - - @Override - public Reader getCharacterStream(String parameterName) throws SQLException { - return getResult().getCharacterStream(nameToOutputIndex(parameterName)); - } - - /** - * Registers the designated output parameter. This version of the method - * registerOutParameter should be used for a user-defined or REF output - * parameter. Examples of user-defined types include: STRUCT, DISTINCT, - * JAVA_OBJECT, and named array types. - * - *

All OUT parameters must be registered before a stored procedure is executed. - * - *

For a user-defined parameter, the fully-qualified SQL type name of the parameter should also - * be given, while a REF parameter requires that the fully-qualified type name of the - * referenced type be given. A JDBC driver that does not need the type code and type name - * information may ignore it. To be portable, however, applications should always provide these - * values for user-defined and REF parameters. - * - *

Although it is intended for user-defined and REF parameters, this method may be - * used to register a parameter of any JDBC type. If the parameter does not have a user-defined or - * REF type, the typeName parameter is ignored. - * - *

Note: When reading the value of an out parameter, you must use the getter method - * whose Java type corresponds to the parameter's registered SQL type. - * - * @param parameterIndex the first parameter is 1, the second is 2,... - * @param sqlType a value from {@link Types} - * @param typeName the fully-qualified name of an SQL structured type - * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or - * this method is called on a closed CallableStatement - * @see Types - */ - public void registerOutParameter(int parameterIndex, int sqlType, String typeName) - throws SQLException { - CallParameter callParameter = getParameter(parameterIndex); - callParameter.setOutputSqlType(sqlType); - callParameter.setTypeName(typeName); - callParameter.setOutput(true); - } - - @Override - public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException { - registerOutParameter(parameterIndex, sqlType, -1); - } - - /** - * Registers the parameter in ordinal position parameterIndex to be of JDBC type - * sqlType. All OUT parameters must be registered before a stored procedure is - * executed. - * - *

The JDBC type specified by sqlType for an OUT parameter determines the Java - * type that must be used in the get method to read the value of that parameter. - * - *

This version of registerOutParameter should be used when the parameter is of - * JDBC type NUMERIC or DECIMAL. - * - * @param parameterIndex the first parameter is 1, the second is 2, and so on - * @param sqlType the SQL type code defined by java.sql.Types. - * @param scale the desired number of digits to the right of the decimal point. It must be greater - * than or equal to zero. - * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or - * this method is called on a closed CallableStatement - * @see Types - */ - @Override - public void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException { - CallParameter callParameter = getParameter(parameterIndex); - callParameter.setOutput(true); - callParameter.setOutputSqlType(sqlType); - callParameter.setScale(scale); - } - - @Override - public void registerOutParameter(String parameterName, int sqlType) throws SQLException { - registerOutParameter(nameToIndex(parameterName), sqlType); - } - - @Override - public void registerOutParameter(String parameterName, int sqlType, int scale) - throws SQLException { - registerOutParameter(nameToIndex(parameterName), sqlType, scale); - } - - @Override - public void registerOutParameter(String parameterName, int sqlType, String typeName) - throws SQLException { - registerOutParameter(nameToIndex(parameterName), sqlType, typeName); - } - - @Override - public void registerOutParameter(int parameterIndex, SQLType sqlType) throws SQLException { - registerOutParameter(parameterIndex, sqlType.getVendorTypeNumber()); - } - - @Override - public void registerOutParameter(int parameterIndex, SQLType sqlType, int scale) - throws SQLException { - registerOutParameter(parameterIndex, sqlType.getVendorTypeNumber(), scale); - } - - @Override - public void registerOutParameter(int parameterIndex, SQLType sqlType, String typeName) - throws SQLException { - registerOutParameter(parameterIndex, sqlType.getVendorTypeNumber(), typeName); - } - - @Override - public void registerOutParameter(String parameterName, SQLType sqlType) throws SQLException { - registerOutParameter(parameterName, sqlType.getVendorTypeNumber()); - } - - @Override - public void registerOutParameter(String parameterName, SQLType sqlType, int scale) - throws SQLException { - registerOutParameter(parameterName, sqlType.getVendorTypeNumber(), scale); - } - - @Override - public void registerOutParameter(String parameterName, SQLType sqlType, String typeName) - throws SQLException { - registerOutParameter(parameterName, sqlType.getVendorTypeNumber(), typeName); - } - - private CallParameter getParameter(int index) throws SQLException { - if (index > params.length || index <= 0) { - throw exceptionFactory.create("No parameter with index " + (index)); - } - return params[index - 1]; - } - - @Override - public void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException { - throw exceptionFactory.notSupported("SQLXML not supported"); - } - - @Override - public void setRowId(String parameterName, RowId rowid) throws SQLException { - throw exceptionFactory.notSupported("RowIDs not supported"); - } - - @Override - public void setNString(String parameterName, String value) throws SQLException { - setNString(nameToIndex(parameterName), value); - } - - @Override - public void setNCharacterStream(String parameterName, Reader reader, long length) - throws SQLException { - setCharacterStream(nameToIndex(parameterName), reader, length); - } - - @Override - public void setNCharacterStream(String parameterName, Reader reader) throws SQLException { - setCharacterStream(nameToIndex(parameterName), reader); - } - - @Override - public void setNClob(String parameterName, NClob value) throws SQLException { - setClob(nameToIndex(parameterName), value); - } - - @Override - public void setNClob(String parameterName, Reader reader, long length) throws SQLException { - setClob(nameToIndex(parameterName), reader, length); - } - - @Override - public void setNClob(String parameterName, Reader reader) throws SQLException { - setClob(nameToIndex(parameterName), reader); - } - - @Override - public void setClob(String parameterName, Reader reader, long length) throws SQLException { - setClob(nameToIndex(parameterName), reader, length); - } - - @Override - public void setClob(String parameterName, Clob clob) throws SQLException { - setClob(nameToIndex(parameterName), clob); - } - - @Override - public void setClob(String parameterName, Reader reader) throws SQLException { - setClob(nameToIndex(parameterName), reader); - } - - @Override - public void setBlob(String parameterName, InputStream inputStream, long length) - throws SQLException { - setBlob(nameToIndex(parameterName), inputStream, length); - } - - @Override - public void setBlob(String parameterName, Blob blob) throws SQLException { - setBlob(nameToIndex(parameterName), blob); - } - - @Override - public void setBlob(String parameterName, InputStream inputStream) throws SQLException { - setBlob(nameToIndex(parameterName), inputStream); - } - - @Override - public void setAsciiStream(String parameterName, InputStream inputStream, long length) - throws SQLException { - setAsciiStream(nameToIndex(parameterName), inputStream, length); - } - - @Override - public void setAsciiStream(String parameterName, InputStream inputStream, int length) - throws SQLException { - setAsciiStream(nameToIndex(parameterName), inputStream, length); - } - - @Override - public void setAsciiStream(String parameterName, InputStream inputStream) throws SQLException { - setAsciiStream(nameToIndex(parameterName), inputStream); - } - - @Override - public void setBinaryStream(String parameterName, InputStream inputStream, long length) - throws SQLException { - setBinaryStream(nameToIndex(parameterName), inputStream, length); - } - - @Override - public void setBinaryStream(String parameterName, InputStream inputStream) throws SQLException { - setBinaryStream(nameToIndex(parameterName), inputStream); - } - - @Override - public void setBinaryStream(String parameterName, InputStream inputStream, int length) - throws SQLException { - setBinaryStream(nameToIndex(parameterName), inputStream, length); - } - - @Override - public void setCharacterStream(String parameterName, Reader reader, long length) - throws SQLException { - setCharacterStream(nameToIndex(parameterName), reader, length); - } - - @Override - public void setCharacterStream(String parameterName, Reader reader) throws SQLException { - setCharacterStream(nameToIndex(parameterName), reader); - } - - @Override - public void setCharacterStream(String parameterName, Reader reader, int length) - throws SQLException { - setCharacterStream(nameToIndex(parameterName), reader, length); - } - - @Override - public void setURL(String parameterName, URL url) throws SQLException { - setURL(nameToIndex(parameterName), url); - } - - @Override - public void setNull(String parameterName, int sqlType) throws SQLException { - setNull(nameToIndex(parameterName), sqlType); - } - - @Override - public void setNull(String parameterName, int sqlType, String typeName) throws SQLException { - setNull(nameToIndex(parameterName), sqlType, typeName); - } - - @Override - public void setBoolean(String parameterName, boolean booleanValue) throws SQLException { - setBoolean(nameToIndex(parameterName), booleanValue); - } - - @Override - public void setByte(String parameterName, byte byteValue) throws SQLException { - setByte(nameToIndex(parameterName), byteValue); - } - - @Override - public void setShort(String parameterName, short shortValue) throws SQLException { - setShort(nameToIndex(parameterName), shortValue); - } - - @Override - public void setInt(String parameterName, int intValue) throws SQLException { - setInt(nameToIndex(parameterName), intValue); - } - - @Override - public void setLong(String parameterName, long longValue) throws SQLException { - setLong(nameToIndex(parameterName), longValue); - } - - @Override - public void setFloat(String parameterName, float floatValue) throws SQLException { - setFloat(nameToIndex(parameterName), floatValue); - } - - @Override - public void setDouble(String parameterName, double doubleValue) throws SQLException { - setDouble(nameToIndex(parameterName), doubleValue); - } - - @Override - public void setBigDecimal(String parameterName, BigDecimal bigDecimal) throws SQLException { - setBigDecimal(nameToIndex(parameterName), bigDecimal); - } - - @Override - public void setString(String parameterName, String stringValue) throws SQLException { - setString(nameToIndex(parameterName), stringValue); - } - - @Override - public void setBytes(String parameterName, byte[] bytes) throws SQLException { - setBytes(nameToIndex(parameterName), bytes); - } - - @Override - public void setDate(String parameterName, Date date) throws SQLException { - setDate(nameToIndex(parameterName), date); - } - - @Override - public void setDate(String parameterName, Date date, Calendar cal) throws SQLException { - setDate(nameToIndex(parameterName), date, cal); - } - - @Override - public void setTime(String parameterName, Time time) throws SQLException { - setTime(nameToIndex(parameterName), time); - } - - @Override - public void setTime(String parameterName, Time time, Calendar cal) throws SQLException { - setTime(nameToIndex(parameterName), time, cal); - } - - @Override - public void setTimestamp(String parameterName, Timestamp timestamp) throws SQLException { - setTimestamp(nameToIndex(parameterName), timestamp); - } - - @Override - public void setTimestamp(String parameterName, Timestamp timestamp, Calendar cal) - throws SQLException { - setTimestamp(nameToIndex(parameterName), timestamp, cal); - } - - @Override - public void setObject(String parameterName, Object obj, int targetSqlType, int scale) - throws SQLException { - setObject(nameToIndex(parameterName), obj, targetSqlType, scale); - } - - @Override - public void setObject(String parameterName, Object obj, int targetSqlType) throws SQLException { - setObject(nameToIndex(parameterName), obj, targetSqlType); - } - - @Override - public void setObject(String parameterName, Object obj) throws SQLException { - setObject(nameToIndex(parameterName), obj); - } - - @Override - public void setObject(String parameterName, Object obj, SQLType targetSqlType, int scaleOrLength) - throws SQLException { - setObject(nameToIndex(parameterName), obj, targetSqlType.getVendorTypeNumber(), scaleOrLength); - } - - @Override - public void setObject(String parameterName, Object obj, SQLType targetSqlType) - throws SQLException { - setObject(nameToIndex(parameterName), obj, targetSqlType.getVendorTypeNumber()); - } -} diff --git a/src/main/java/org/mariadb/jdbc/CallableParameterMetaData.java b/src/main/java/org/mariadb/jdbc/CallableParameterMetaData.java index cd1b1a8b4..5553c8e3f 100644 --- a/src/main/java/org/mariadb/jdbc/CallableParameterMetaData.java +++ b/src/main/java/org/mariadb/jdbc/CallableParameterMetaData.java @@ -1,385 +1,342 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - package org.mariadb.jdbc; +import java.math.BigDecimal; import java.sql.*; -import java.util.ArrayList; -import java.util.List; +import java.util.BitSet; import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -public class CallableParameterMetaData implements ParameterMetaData { +public class CallableParameterMetaData implements java.sql.ParameterMetaData { + private ResultSet rs; + private int parameterCount; - private static final Pattern PARAMETER_PATTERN = - Pattern.compile( - "\\s*(IN\\s+|OUT\\s+|INOUT\\s+)?([\\w\\d]+)\\s+(UNSIGNED\\s+)?(\\w+)\\s*(\\([\\d,]+\\))?\\s*", - Pattern.CASE_INSENSITIVE); - private static final Pattern RETURN_PATTERN = - Pattern.compile( - "\\s*(UNSIGNED\\s+)?(\\w+)\\s*(\\([\\d,]+\\))?\\s*(CHARSET\\s+)?(\\w+)?\\s*", - Pattern.CASE_INSENSITIVE); - private final MariaDbConnection con; - private final String name; - private List params; - private String database; - private boolean valid; - private boolean isFunction; + public CallableParameterMetaData(ResultSet rs) throws SQLException { + this.rs = rs; + int count = 0; + while (rs.next()) count++; + this.parameterCount = count; + } /** - * Retrieve Callable metaData. + * Retrieves the number of parameters in the PreparedStatement object for which this + * ParameterMetaData object contains information. * - * @param con connection - * @param database database name - * @param name procedure/function name - * @param isFunction is it a function + * @return the number of parameters + * @throws SQLException if a database access error occurs + * @since 1.4 */ - public CallableParameterMetaData( - MariaDbConnection con, String database, String name, boolean isFunction) { - this.params = null; - this.con = con; - if (database != null) { - this.database = database.replace("`", ""); - } else { - this.database = null; - } - this.name = name.replace("`", ""); - this.isFunction = isFunction; + @Override + public int getParameterCount() throws SQLException { + return parameterCount; } /** - * Search metaData if not already loaded. + * Retrieves whether null values are allowed in the designated parameter. * - * @throws SQLException if error append during loading metaData + * @param index the first parameter is 1, the second is 2, ... + * @return the nullability status of the given parameter; one of + * ParameterMetaData.parameterNoNulls, ParameterMetaData.parameterNullable + * , or ParameterMetaData.parameterNullableUnknown + * @throws SQLException if a database access error occurs + * @since 1.4 */ - public void readMetadataFromDbIfRequired() throws SQLException { - if (valid) { - return; + @Override + public int isNullable(int index) throws SQLException { + setIndex(index); + return ParameterMetaData.parameterNullableUnknown; + } + + private void setIndex(int index) throws SQLException { + if (index < 1 || index > parameterCount) { + throw new SQLException("invalid parameter index " + index); } - readMetadata(); - valid = true; + rs.absolute(index); + } + + /** + * Retrieves whether values for the designated parameter can be signed numbers. + * + * @param index the first parameter is 1, the second is 2, ... + * @return true if so; false otherwise + * @throws SQLException if a database access error occurs + * @since 1.4 + */ + @Override + public boolean isSigned(int index) throws SQLException { + setIndex(index); + String paramDetail = rs.getString("DTD_IDENTIFIER"); + return !paramDetail.contains(" unsigned"); + } + + /** + * Retrieves the designated parameter's specified column size. + * + *

The returned value represents the maximum column size for the given parameter. For numeric + * data, this is the maximum precision. For character data, this is the length in characters. For + * datetime datatypes, this is the length in characters of the String representation (assuming the + * maximum allowed precision of the fractional seconds component). For binary data, this is the + * length in bytes. For the ROWID datatype, this is the length in bytes. 0 is returned for data + * types where the column size is not applicable. + * + * @param index the first parameter is 1, the second is 2, ... + * @return precision + * @throws SQLException if a database access error occurs + * @since 1.4 + */ + @Override + public int getPrecision(int index) throws SQLException { + setIndex(index); + int characterMaxLength = rs.getInt("CHARACTER_MAXIMUM_LENGTH"); + int numericPrecision = rs.getInt("NUMERIC_PRECISION"); + return (numericPrecision > 0) ? numericPrecision : characterMaxLength; + } + + /** + * Retrieves the designated parameter's number of digits to right of the decimal point. 0 is + * returned for data types where the scale is not applicable. + * + * @param index the first parameter is 1, the second is 2, ... + * @return scale + * @throws SQLException if a database access error occurs + * @since 1.4 + */ + @Override + public int getScale(int index) throws SQLException { + setIndex(index); + return rs.getInt("NUMERIC_SCALE"); } - private int mapMariaDbTypeToJdbc(String str) { + public String getParameterName(int index) throws SQLException { + setIndex(index); + return rs.getString("PARAMETER_NAME"); + } + + /** + * Retrieves the designated parameter's SQL type. + * + * @param index the first parameter is 1, the second is 2, ... + * @return SQL type from java.sql.Types + * @throws SQLException if a database access error occurs + * @see Types + * @since 1.4 + */ + @Override + public int getParameterType(int index) throws SQLException { + setIndex(index); + String str = rs.getString("DATA_TYPE"); switch (str.toUpperCase(Locale.ROOT)) { case "BIT": return Types.BIT; case "TINYINT": return Types.TINYINT; case "SMALLINT": + case "YEAR": return Types.SMALLINT; case "MEDIUMINT": - return Types.INTEGER; case "INT": - return Types.INTEGER; + case "INT24": case "INTEGER": return Types.INTEGER; case "LONG": - return Types.INTEGER; case "BIGINT": return Types.BIGINT; - case "INT24": - return Types.INTEGER; case "REAL": + case "DOUBLE": return Types.DOUBLE; case "FLOAT": return Types.FLOAT; case "DECIMAL": return Types.DECIMAL; - case "NUMERIC": - return Types.NUMERIC; - case "DOUBLE": - return Types.DOUBLE; case "CHAR": return Types.CHAR; case "VARCHAR": + case "ENUM": + case "TINYTEXT": + case "SET": return Types.VARCHAR; case "DATE": return Types.DATE; case "TIME": return Types.TIME; - case "YEAR": - return Types.SMALLINT; case "TIMESTAMP": - return Types.TIMESTAMP; case "DATETIME": return Types.TIMESTAMP; - case "TINYBLOB": + case "BINARY": return Types.BINARY; + case "VARBINARY": + return Types.VARBINARY; + case "TINYBLOB": case "BLOB": - return Types.LONGVARBINARY; case "MEDIUMBLOB": - return Types.LONGVARBINARY; case "LONGBLOB": - return Types.LONGVARBINARY; - case "TINYTEXT": - return Types.VARCHAR; + case "GEOMETRY": + return Types.BLOB; case "TEXT": - return Types.LONGVARCHAR; case "MEDIUMTEXT": - return Types.LONGVARCHAR; case "LONGTEXT": - return Types.LONGVARCHAR; - case "ENUM": - return Types.VARCHAR; - case "SET": - return Types.VARCHAR; - case "GEOMETRY": - return Types.LONGVARBINARY; - case "VARBINARY": - return Types.VARBINARY; + return Types.CLOB; default: return Types.OTHER; } } - private String[] queryMetaInfos(boolean isFunction) throws SQLException { - String paramList; - String functionReturn; - try (PreparedStatement preparedStatement = - con.prepareStatement( - "select param_list, returns, db, type from mysql.proc where name=? and db=" - + (database != null ? "?" : "DATABASE()"))) { - - preparedStatement.setString(1, name); - if (database != null) { - preparedStatement.setString(2, database); - } - - try (ResultSet rs = preparedStatement.executeQuery()) { - if (!rs.next()) { - throw new SQLException( - (isFunction ? "function" : "procedure") + " `" + name + "` does not exist"); - } - paramList = rs.getString(1); - functionReturn = rs.getString(2); - database = rs.getString(3); - this.isFunction = "FUNCTION".equals(rs.getString(4)); - return new String[] {paramList, functionReturn}; - } - - } catch (SQLSyntaxErrorException sqlSyntaxErrorException) { - throw new SQLException( - "Access to metaData informations not granted for current user. Consider grant select access to mysql.proc " - + " or avoid using parameter by name", - sqlSyntaxErrorException); - } - } - - private void parseFunctionReturnParam(String functionReturn) throws SQLException { - if (functionReturn == null || functionReturn.length() == 0) { - throw new SQLException(name + "is not a function returning value"); - } - Matcher matcher = RETURN_PATTERN.matcher(functionReturn); - if (!matcher.matches()) { - throw new SQLException("can not parse return value definition :" + functionReturn); - } - CallParameter callParameter = params.get(0); - callParameter.setOutput(true); - callParameter.setSigned(matcher.group(1) == null); - callParameter.setTypeName(matcher.group(2).trim()); - callParameter.setSqlType(mapMariaDbTypeToJdbc(callParameter.getTypeName())); - String scale = matcher.group(3); - if (scale != null) { - scale = scale.replace("(", "").replace(")", "").replace(" ", ""); - callParameter.setScale(Integer.valueOf(scale)); - } + /** + * Retrieves the designated parameter's database-specific type name. + * + * @param index the first parameter is 1, the second is 2, ... + * @return type the name used by the database. If the parameter type is a user-defined type, then + * a fully-qualified type name is returned. + * @throws SQLException if a database access error occurs + * @since 1.4 + */ + @Override + public String getParameterTypeName(int index) throws SQLException { + setIndex(index); + return rs.getString("DATA_TYPE").toUpperCase(Locale.ROOT); } - private void parseParamList(boolean isFunction, String paramList) throws SQLException { - params = new ArrayList<>(); - if (isFunction) { - // output parameter - params.add(new CallParameter()); - } - - Matcher matcher2 = PARAMETER_PATTERN.matcher(paramList); - while (matcher2.find()) { - CallParameter callParameter = new CallParameter(); - String direction = matcher2.group(1); - if (direction != null) { - direction = direction.trim(); - } - - callParameter.setName(matcher2.group(2).trim()); - callParameter.setSigned(matcher2.group(3) == null); - callParameter.setTypeName(matcher2.group(4).trim().toUpperCase(Locale.ROOT)); - - if (direction == null || direction.equalsIgnoreCase("IN")) { - callParameter.setInput(true); - } else if (direction.equalsIgnoreCase("OUT")) { - callParameter.setOutput(true); - } else if (direction.equalsIgnoreCase("INOUT")) { - callParameter.setInput(true); - callParameter.setOutput(true); - } else { - throw new SQLException( - "unknown parameter direction " + direction + "for " + callParameter.getName()); - } + /** + * Retrieves the fully-qualified name of the Java class whose instances should be passed to the + * method PreparedStatement.setObject. + * + * @param index the first parameter is 1, the second is 2, ... + * @return the fully-qualified name of the class in the Java programming language that would be + * used by the method PreparedStatement.setObject to set the value in the + * specified parameter. This is the class name used for custom mapping. + * @throws SQLException if a database access error occurs + * @since 1.4 + */ + @Override + public String getParameterClassName(int index) throws SQLException { + setIndex(index); + String str = rs.getString("DATA_TYPE"); + switch (str.toUpperCase(Locale.ROOT)) { + case "BIT": + return BitSet.class.getName(); + case "TINYINT": + return byte.class.getName(); + case "SMALLINT": + case "YEAR": + return short.class.getName(); + case "MEDIUMINT": + case "INT": + case "INTEGER": + return int.class.getName(); + case "BINARY": + return byte[].class.getName(); + case "BIGINT": + return long.class.getName(); + case "FLOAT": + return float.class.getName(); + case "DECIMAL": + return BigDecimal.class.getName(); + case "REAL": + case "DOUBLE": + return double.class.getName(); + case "CHAR": + case "VARCHAR": + case "ENUM": + case "TINYTEXT": + return String.class.getName(); + case "TEXT": + case "MEDIUMTEXT": + case "LONGTEXT": + return Clob.class.getName(); - callParameter.setSqlType(mapMariaDbTypeToJdbc(callParameter.getTypeName())); + case "DATE": + return Date.class.getName(); + case "TIME": + return Time.class.getName(); + case "TIMESTAMP": + case "DATETIME": + return Timestamp.class.getName(); + case "BLOB": + case "MEDIUMBLOB": + case "LONGBLOB": + return Blob.class.getName(); - String scale = matcher2.group(5); - if (scale != null) { - scale = scale.trim().replace("(", "").replace(")", "").replace(" ", ""); - if (scale.contains(",")) { - scale = scale.substring(0, scale.indexOf(",")); - } - callParameter.setScale(Integer.valueOf(scale)); - } - params.add(callParameter); + case "SET": + case "GEOMETRY": + case "VARBINARY": + case "TINYBLOB": + return byte[].class.getName(); + default: + return Object.class.getName(); } } /** - * Read procedure metadata from mysql.proc table(column param_list). + * Retrieves the designated parameter's mode. * - * @throws SQLException if data doesn't correspond. + * @param index the first parameter is 1, the second is 2, ... + * @return mode of the parameter; one of ParameterMetaData.parameterModeIn, + * ParameterMetaData.parameterModeOut, or ParameterMetaData.parameterModeInOut + * ParameterMetaData.parameterModeUnknown. + * @throws SQLException if a database access error occurs + * @since 1.4 */ - private void readMetadata() throws SQLException { - if (valid) { - return; + @Override + public int getParameterMode(int index) throws SQLException { + setIndex(index); + String str = rs.getString("PARAMETER_MODE"); + if (str == null) { + return ParameterMetaData.parameterModeUnknown; } - - String[] metaInfos = queryMetaInfos(isFunction); - String paramList = metaInfos[0]; - String functionReturn = metaInfos[1]; - - parseParamList(isFunction, paramList); - - // parse type of the return value (for functions) - if (isFunction) { - parseFunctionReturnParam(functionReturn); - } - } - - public int getParameterCount() { - return params.size(); - } - - private CallParameter getParam(int index) throws SQLException { - if (index < 1 || index > params.size()) { - throw new SQLException("invalid parameter index " + index); + switch (str) { + case "IN": + return ParameterMetaData.parameterModeIn; + case "OUT": + return ParameterMetaData.parameterModeOut; + case "INOUT": + return ParameterMetaData.parameterModeInOut; + default: + return ParameterMetaData.parameterModeUnknown; } - readMetadataFromDbIfRequired(); - return params.get(index - 1); - } - - public int isNullable(int param) throws SQLException { - return getParam(param).getCanBeNull(); - } - - public boolean isSigned(int param) throws SQLException { - return getParam(param).isSigned(); - } - - public int getPrecision(int param) throws SQLException { - return getParam(param).getPrecision(); - } - - public int getScale(int param) throws SQLException { - return getParam(param).getScale(); - } - - public int getParameterType(int param) throws SQLException { - return getParam(param).getSqlType(); - } - - public String getParameterTypeName(int param) throws SQLException { - return getParam(param).getTypeName(); - } - - public String getParameterClassName(int param) throws SQLException { - return getParam(param).getClassName(); } /** - * Get mode info. + * Returns an object that implements the given interface to allow access to non-standard methods, + * or standard methods not exposed by the proxy. * - *

    - *
  • 0 : unknown - *
  • 1 : IN - *
  • 2 : INOUT - *
  • 4 : OUT - *
+ *

If the receiver implements the interface then the result is the receiver or a proxy for the + * receiver. If the receiver is a wrapper and the wrapped object implements the interface then the + * result is the wrapped object or a proxy for the wrapped object. Otherwise return the the result + * of calling unwrap recursively on the wrapped object or a proxy for that result. If + * the receiver is not a wrapper and does not implement the interface, then an SQLException + * is thrown. * - * @param param parameter index - * @return mode information - * @throws SQLException if index is wrong + * @param iface A Class defining an interface that the result must implement. + * @return an object that implements the interface. May be a proxy for the actual implementing + * object. + * @throws SQLException If no object found that implements the interface + * @since 1.6 */ - public int getParameterMode(int param) throws SQLException { - CallParameter callParameter = getParam(param); - if (callParameter.isInput() && callParameter.isOutput()) { - return parameterModeInOut; - } - if (callParameter.isInput()) { - return parameterModeIn; + @Override + public T unwrap(Class iface) throws SQLException { + if (isWrapperFor(iface)) { + return iface.cast(this); } - if (callParameter.isOutput()) { - return parameterModeOut; - } - return parameterModeUnknown; - } - - public String getName(int param) throws SQLException { - return getParam(param).getName(); - } - - public T unwrap(Class iface) { - return null; + throw new SQLException("The receiver is not a wrapper for " + iface.getName()); } - public boolean isWrapperFor(Class iface) { - return false; + /** + * Returns true if this either implements the interface argument or is directly or indirectly a + * wrapper for an object that does. Returns false otherwise. If this implements the interface then + * return true, else if this is a wrapper then return the result of recursively calling + * isWrapperFor on the wrapped object. If this does not implement the interface and is not + * a wrapper, return false. This method should be implemented as a low-cost operation compared to + * unwrap so that callers can use this method to avoid expensive unwrap + * calls that may fail. If this method returns true then calling unwrap with the same + * argument should succeed. + * + * @param iface a Class defining an interface. + * @return true if this implements the interface or directly or indirectly wraps an object that + * does. + * @throws SQLException if an error occurs while determining whether this is a wrapper for an + * object with the given interface. + * @since 1.6 + */ + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return iface.isInstance(this); } } diff --git a/src/main/java/org/mariadb/jdbc/CallableProcedureStatement.java b/src/main/java/org/mariadb/jdbc/CallableProcedureStatement.java deleted file mode 100644 index c2d4f9e20..000000000 --- a/src/main/java/org/mariadb/jdbc/CallableProcedureStatement.java +++ /dev/null @@ -1,904 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.io.InputStream; -import java.io.Reader; -import java.math.BigDecimal; -import java.net.URL; -import java.sql.*; -import java.util.Calendar; -import java.util.List; -import java.util.Map; -import org.mariadb.jdbc.internal.ColumnType; -import org.mariadb.jdbc.internal.com.read.resultset.SelectResultSet; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; - -public abstract class CallableProcedureStatement extends ServerSidePreparedStatement - implements CallableStatement, Cloneable { - - /** Information about parameters, merely from registerOutputParameter() and setXXX() calls. */ - protected List params; - - protected int[] outputParameterMapper = null; - protected CallableParameterMetaData parameterMetadata; - protected boolean hasInOutParameters; - - /** - * Constructor for getter/setter of callableStatement. - * - * @param connection current connection - * @param sql query - * @param resultSetScrollType one of the following ResultSet constants: - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, or - * ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of ResultSet.CONCUR_READ_ONLY - * or ResultSet.CONCUR_UPDATABLE - * @param exceptionFactory Exception factory - * @throws SQLException is prepareStatement connection throw any error - */ - public CallableProcedureStatement( - MariaDbConnection connection, - String sql, - int resultSetScrollType, - int resultSetConcurrency, - ExceptionFactory exceptionFactory) - throws SQLException { - super( - connection, - sql, - resultSetScrollType, - resultSetConcurrency, - Statement.NO_GENERATED_KEYS, - exceptionFactory); - } - - /** - * Clone data. - * - * @param connection connection - * @return Cloned . - * @throws CloneNotSupportedException if any error occur. - */ - public CallableProcedureStatement clone(MariaDbConnection connection) - throws CloneNotSupportedException { - CallableProcedureStatement clone = (CallableProcedureStatement) super.clone(connection); - clone.params = params; - clone.parameterMetadata = parameterMetadata; - clone.hasInOutParameters = hasInOutParameters; - clone.outputParameterMapper = outputParameterMapper; - return clone; - } - - /** Set in/out parameters value. */ - public void setParametersVariables() { - hasInOutParameters = false; - for (CallParameter param : params) { - if (param != null && param.isOutput() && param.isInput()) { - hasInOutParameters = true; - break; - } - } - } - - protected abstract SelectResultSet getOutputResult() throws SQLException; - - public ParameterMetaData getParameterMetaData() throws SQLException { - parameterMetadata.readMetadataFromDbIfRequired(); - return parameterMetadata; - } - - /** - * Convert parameter name to parameter index in the query. - * - * @param parameterName name - * @return index - * @throws SQLException exception - */ - private int nameToIndex(String parameterName) throws SQLException { - parameterMetadata.readMetadataFromDbIfRequired(); - for (int i = 1; i <= parameterMetadata.getParameterCount(); i++) { - String name = parameterMetadata.getName(i); - if (name != null && name.equalsIgnoreCase(parameterName)) { - return i; - } - } - throw new SQLException("there is no parameter with the name " + parameterName); - } - - /** - * Convert parameter name to output parameter index in the query. - * - * @param parameterName name - * @return index - * @throws SQLException exception - */ - private int nameToOutputIndex(String parameterName) throws SQLException { - parameterMetadata.readMetadataFromDbIfRequired(); - for (int i = 0; i < parameterMetadata.getParameterCount(); i++) { - String name = parameterMetadata.getName(i + 1); - if (name != null && name.equalsIgnoreCase(parameterName)) { - if (outputParameterMapper[i] == -1) { - // this is not an outputParameter - throw new SQLException( - "Parameter '" - + parameterName - + "' is not declared as output parameter with method registerOutParameter"); - } - return outputParameterMapper[i]; - } - } - throw new SQLException("there is no parameter with the name " + parameterName); - } - - /** - * Convert parameter index to corresponding outputIndex. - * - * @param parameterIndex index - * @return index - * @throws SQLException exception - */ - private int indexToOutputIndex(int parameterIndex) throws SQLException { - try { - if (outputParameterMapper[parameterIndex - 1] == -1) { - // this is not an outputParameter - throw new SQLException( - "Parameter in index '" - + parameterIndex - + "' is not declared as output parameter with method registerOutParameter"); - } - return outputParameterMapper[parameterIndex - 1]; - } catch (ArrayIndexOutOfBoundsException arrayIndexOutOfBoundsException) { - if (parameterIndex < 1) { - throw new SQLException("Index " + parameterIndex + " must at minimum be 1"); - } - throw new SQLException( - "Index value '" + parameterIndex + "' is incorrect. Maximum value is " + params.size()); - } - } - - @Override - public boolean wasNull() throws SQLException { - return getOutputResult().wasNull(); - } - - @Override - public String getString(int parameterIndex) throws SQLException { - return getOutputResult().getString(indexToOutputIndex(parameterIndex)); - } - - @Override - public String getString(String parameterName) throws SQLException { - return getOutputResult().getString(nameToOutputIndex(parameterName)); - } - - @Override - public boolean getBoolean(int parameterIndex) throws SQLException { - return getOutputResult().getBoolean(indexToOutputIndex(parameterIndex)); - } - - @Override - public boolean getBoolean(String parameterName) throws SQLException { - return getOutputResult().getBoolean(nameToOutputIndex(parameterName)); - } - - @Override - public byte getByte(int parameterIndex) throws SQLException { - return getOutputResult().getByte(indexToOutputIndex(parameterIndex)); - } - - @Override - public byte getByte(String parameterName) throws SQLException { - return getOutputResult().getByte(nameToOutputIndex(parameterName)); - } - - @Override - public short getShort(int parameterIndex) throws SQLException { - return getOutputResult().getShort(indexToOutputIndex(parameterIndex)); - } - - @Override - public short getShort(String parameterName) throws SQLException { - return getOutputResult().getShort(nameToOutputIndex(parameterName)); - } - - @Override - public int getInt(String parameterName) throws SQLException { - return getOutputResult().getInt(nameToOutputIndex(parameterName)); - } - - @Override - public int getInt(int parameterIndex) throws SQLException { - return getOutputResult().getInt(indexToOutputIndex(parameterIndex)); - } - - @Override - public long getLong(String parameterName) throws SQLException { - return getOutputResult().getLong(nameToOutputIndex(parameterName)); - } - - @Override - public long getLong(int parameterIndex) throws SQLException { - return getOutputResult().getLong(indexToOutputIndex(parameterIndex)); - } - - @Override - public float getFloat(String parameterName) throws SQLException { - return getOutputResult().getFloat(nameToOutputIndex(parameterName)); - } - - @Override - public float getFloat(int parameterIndex) throws SQLException { - return getOutputResult().getFloat(indexToOutputIndex(parameterIndex)); - } - - @Override - public double getDouble(int parameterIndex) throws SQLException { - return getOutputResult().getDouble(indexToOutputIndex(parameterIndex)); - } - - @Override - public double getDouble(String parameterName) throws SQLException { - return getOutputResult().getDouble(nameToOutputIndex(parameterName)); - } - - @Override - @Deprecated - @SuppressWarnings("deprecation") - public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException { - return getOutputResult().getBigDecimal(indexToOutputIndex(parameterIndex)); - } - - @Override - public BigDecimal getBigDecimal(int parameterIndex) throws SQLException { - return getOutputResult().getBigDecimal(indexToOutputIndex(parameterIndex)); - } - - @Override - public BigDecimal getBigDecimal(String parameterName) throws SQLException { - return getOutputResult().getBigDecimal(nameToOutputIndex(parameterName)); - } - - @Override - public byte[] getBytes(String parameterName) throws SQLException { - return getOutputResult().getBytes(nameToOutputIndex(parameterName)); - } - - @Override - public byte[] getBytes(int parameterIndex) throws SQLException { - return getOutputResult().getBytes(indexToOutputIndex(parameterIndex)); - } - - @Override - public Date getDate(int parameterIndex) throws SQLException { - return getOutputResult().getDate(indexToOutputIndex(parameterIndex)); - } - - @Override - public Date getDate(String parameterName) throws SQLException { - return getOutputResult().getDate(nameToOutputIndex(parameterName)); - } - - @Override - public Date getDate(String parameterName, Calendar cal) throws SQLException { - return getOutputResult().getDate(nameToOutputIndex(parameterName), cal); - } - - @Override - public Date getDate(int parameterIndex, Calendar cal) throws SQLException { - return getOutputResult().getDate(parameterIndex, cal); - } - - @Override - public Time getTime(int parameterIndex, Calendar cal) throws SQLException { - return getOutputResult().getTime(indexToOutputIndex(parameterIndex), cal); - } - - @Override - public Time getTime(String parameterName) throws SQLException { - return getOutputResult().getTime(nameToOutputIndex(parameterName)); - } - - @Override - public Time getTime(String parameterName, Calendar cal) throws SQLException { - return getOutputResult().getTime(nameToOutputIndex(parameterName), cal); - } - - @Override - public Time getTime(int parameterIndex) throws SQLException { - return getOutputResult().getTime(indexToOutputIndex(parameterIndex)); - } - - @Override - public Timestamp getTimestamp(int parameterIndex) throws SQLException { - return getOutputResult().getTimestamp(parameterIndex); - } - - @Override - public Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException { - return getOutputResult().getTimestamp(indexToOutputIndex(parameterIndex), cal); - } - - @Override - public Timestamp getTimestamp(String parameterName) throws SQLException { - return getOutputResult().getTimestamp(nameToOutputIndex(parameterName)); - } - - @Override - public Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException { - return getOutputResult().getTimestamp(nameToOutputIndex(parameterName), cal); - } - - @Override - public Object getObject(int parameterIndex, Map> map) throws SQLException { - return getOutputResult().getObject(indexToOutputIndex(parameterIndex), map); - } - - @Override - public Object getObject(int parameterIndex) throws SQLException { - Class classType = - ColumnType.classFromJavaType(getParameter(parameterIndex).getOutputSqlType()); - if (classType != null) { - return getOutputResult().getObject(indexToOutputIndex(parameterIndex), classType); - } - return getOutputResult().getObject(indexToOutputIndex(parameterIndex)); - } - - @Override - public Object getObject(String parameterName) throws SQLException { - int index = nameToIndex(parameterName); - Class classType = ColumnType.classFromJavaType(getParameter(index).getOutputSqlType()); - if (classType != null) { - return getOutputResult().getObject(indexToOutputIndex(index), classType); - } - return getOutputResult().getObject(indexToOutputIndex(index)); - } - - @Override - public Object getObject(String parameterName, Map> map) throws SQLException { - return getOutputResult().getObject(nameToOutputIndex(parameterName), map); - } - - @Override - public T getObject(int parameterIndex, Class type) throws SQLException { - return getOutputResult().getObject(indexToOutputIndex(parameterIndex), type); - } - - @Override - public T getObject(String parameterName, Class type) throws SQLException { - return getOutputResult().getObject(nameToOutputIndex(parameterName), type); - } - - @Override - public Ref getRef(int parameterIndex) throws SQLException { - return getOutputResult().getRef(indexToOutputIndex(parameterIndex)); - } - - @Override - public Ref getRef(String parameterName) throws SQLException { - return getOutputResult().getRef(nameToOutputIndex(parameterName)); - } - - @Override - public Blob getBlob(int parameterIndex) throws SQLException { - return getOutputResult().getBlob(parameterIndex); - } - - @Override - public Blob getBlob(String parameterName) throws SQLException { - return getOutputResult().getBlob(nameToOutputIndex(parameterName)); - } - - @Override - public Clob getClob(String parameterName) throws SQLException { - return getOutputResult().getClob(nameToOutputIndex(parameterName)); - } - - @Override - public Clob getClob(int parameterIndex) throws SQLException { - return getOutputResult().getClob(indexToOutputIndex(parameterIndex)); - } - - @Override - public Array getArray(String parameterName) throws SQLException { - return getOutputResult().getArray(nameToOutputIndex(parameterName)); - } - - @Override - public Array getArray(int parameterIndex) throws SQLException { - return getOutputResult().getArray(indexToOutputIndex(parameterIndex)); - } - - @Override - public URL getURL(int parameterIndex) throws SQLException { - return getOutputResult().getURL(indexToOutputIndex(parameterIndex)); - } - - @Override - public URL getURL(String parameterName) throws SQLException { - return getOutputResult().getURL(nameToOutputIndex(parameterName)); - } - - @Override - public RowId getRowId(int parameterIndex) throws SQLException { - throw exceptionFactory.notSupported("RowIDs not supported"); - } - - @Override - public RowId getRowId(String parameterName) throws SQLException { - throw exceptionFactory.notSupported("RowIDs not supported"); - } - - @Override - public NClob getNClob(int parameterIndex) throws SQLException { - return getOutputResult().getNClob(indexToOutputIndex(parameterIndex)); - } - - @Override - public NClob getNClob(String parameterName) throws SQLException { - return getOutputResult().getNClob(nameToOutputIndex(parameterName)); - } - - @Override - public SQLXML getSQLXML(int parameterIndex) throws SQLException { - throw exceptionFactory.notSupported("SQLXML not supported"); - } - - @Override - public SQLXML getSQLXML(String parameterName) throws SQLException { - throw exceptionFactory.notSupported("SQLXML not supported"); - } - - @Override - public String getNString(int parameterIndex) throws SQLException { - return getOutputResult().getString(indexToOutputIndex(parameterIndex)); - } - - @Override - public String getNString(String parameterName) throws SQLException { - return getOutputResult().getString(nameToOutputIndex(parameterName)); - } - - @Override - public Reader getNCharacterStream(int parameterIndex) throws SQLException { - return getOutputResult().getCharacterStream(indexToOutputIndex(parameterIndex)); - } - - @Override - public Reader getNCharacterStream(String parameterName) throws SQLException { - return getOutputResult().getCharacterStream(nameToOutputIndex(parameterName)); - } - - @Override - public Reader getCharacterStream(int parameterIndex) throws SQLException { - return getOutputResult().getCharacterStream(indexToOutputIndex(parameterIndex)); - } - - @Override - public Reader getCharacterStream(String parameterName) throws SQLException { - return getOutputResult().getCharacterStream(nameToOutputIndex(parameterName)); - } - - /** - * Registers the designated output parameter. This version of the method - * registerOutParameter should be used for a user-defined or REF output - * parameter. Examples of user-defined types include: STRUCT, DISTINCT, - * JAVA_OBJECT, and named array types. - * - *

All OUT parameters must be registered before a stored procedure is executed. - * - *

For a user-defined parameter, the fully-qualified SQL type name of the parameter should also - * be given, while a REF parameter requires that the fully-qualified type name of the - * referenced type be given. A JDBC driver that does not need the type code and type name - * information may ignore it. To be portable, however, applications should always provide these - * values for user-defined and REF parameters. - * - *

Although it is intended for user-defined and REF parameters, this method may be - * used to register a parameter of any JDBC type. If the parameter does not have a user-defined or - * REF type, the typeName parameter is ignored. - * - *

Note: When reading the value of an out parameter, you must use the getter method - * whose Java type corresponds to the parameter's registered SQL type. - * - * @param parameterIndex the first parameter is 1, the second is 2,... - * @param sqlType a value from {@link Types} - * @param typeName the fully-qualified name of an SQL structured type - * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or - * this method is called on a closed CallableStatement - * @see Types - */ - public void registerOutParameter(int parameterIndex, int sqlType, String typeName) - throws SQLException { - CallParameter callParameter = getParameter(parameterIndex); - callParameter.setOutputSqlType(sqlType); - callParameter.setTypeName(typeName); - callParameter.setOutput(true); - } - - @Override - public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException { - registerOutParameter(parameterIndex, sqlType, -1); - } - - /** - * Registers the parameter in ordinal position parameterIndex to be of JDBC type - * sqlType. All OUT parameters must be registered before a stored procedure is - * executed. - * - *

The JDBC type specified by sqlType for an OUT parameter determines the Java - * type that must be used in the get method to read the value of that parameter. - * - *

This version of registerOutParameter should be used when the parameter is of - * JDBC type NUMERIC or DECIMAL. - * - * @param parameterIndex the first parameter is 1, the second is 2, and so on - * @param sqlType the SQL type code defined by java.sql.Types. - * @param scale the desired number of digits to the right of the decimal point. It must be greater - * than or equal to zero. - * @throws SQLException if the parameterIndex is not valid; if a database access error occurs or - * this method is called on a closed CallableStatement - * @see Types - */ - @Override - public void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException { - CallParameter callParameter = getParameter(parameterIndex); - callParameter.setOutput(true); - callParameter.setOutputSqlType(sqlType); - callParameter.setScale(scale); - } - - @Override - public void registerOutParameter(String parameterName, int sqlType) throws SQLException { - registerOutParameter(nameToIndex(parameterName), sqlType); - } - - @Override - public void registerOutParameter(String parameterName, int sqlType, int scale) - throws SQLException { - registerOutParameter(nameToIndex(parameterName), sqlType, scale); - } - - @Override - public void registerOutParameter(String parameterName, int sqlType, String typeName) - throws SQLException { - registerOutParameter(nameToIndex(parameterName), sqlType, typeName); - } - - @Override - public void registerOutParameter(int parameterIndex, SQLType sqlType) throws SQLException { - registerOutParameter(parameterIndex, sqlType.getVendorTypeNumber()); - } - - @Override - public void registerOutParameter(int parameterIndex, SQLType sqlType, int scale) - throws SQLException { - registerOutParameter(parameterIndex, sqlType.getVendorTypeNumber(), scale); - } - - @Override - public void registerOutParameter(int parameterIndex, SQLType sqlType, String typeName) - throws SQLException { - registerOutParameter(parameterIndex, sqlType.getVendorTypeNumber(), typeName); - } - - @Override - public void registerOutParameter(String parameterName, SQLType sqlType) throws SQLException { - registerOutParameter(parameterName, sqlType.getVendorTypeNumber()); - } - - @Override - public void registerOutParameter(String parameterName, SQLType sqlType, int scale) - throws SQLException { - registerOutParameter(parameterName, sqlType.getVendorTypeNumber(), scale); - } - - @Override - public void registerOutParameter(String parameterName, SQLType sqlType, String typeName) - throws SQLException { - registerOutParameter(parameterName, sqlType.getVendorTypeNumber(), typeName); - } - - private CallParameter getParameter(int index) throws SQLException { - if (index > params.size() || index <= 0) { - throw new SQLException("No parameter with index " + index); - } - return params.get(index - 1); - } - - @Override - public void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException { - throw exceptionFactory.notSupported("SQLXML not supported"); - } - - @Override - public void setRowId(String parameterName, RowId rowid) throws SQLException { - throw exceptionFactory.notSupported("RowIDs not supported"); - } - - @Override - public void setNString(String parameterName, String value) throws SQLException { - setString(nameToIndex(parameterName), value); - } - - @Override - public void setNCharacterStream(String parameterName, Reader value, long length) - throws SQLException { - setCharacterStream(nameToIndex(parameterName), value, length); - } - - @Override - public void setNCharacterStream(String parameterName, Reader value) throws SQLException { - setCharacterStream(nameToIndex(parameterName), value); - } - - @Override - public void setNClob(String parameterName, NClob value) throws SQLException { - setClob(nameToIndex(parameterName), value); - } - - @Override - public void setNClob(String parameterName, Reader reader, long length) throws SQLException { - setClob(nameToIndex(parameterName), reader, length); - } - - @Override - public void setNClob(String parameterName, Reader reader) throws SQLException { - setClob(nameToIndex(parameterName), reader); - } - - @Override - public void setClob(String parameterName, Reader reader, long length) throws SQLException { - setClob(nameToIndex(parameterName), reader, length); - } - - @Override - public void setClob(String parameterName, Clob clob) throws SQLException { - setClob(nameToIndex(parameterName), clob); - } - - @Override - public void setClob(String parameterName, Reader reader) throws SQLException { - setClob(nameToIndex(parameterName), reader); - } - - @Override - public void setBlob(String parameterName, InputStream inputStream, long length) - throws SQLException { - setBlob(nameToIndex(parameterName), inputStream, length); - } - - @Override - public void setBlob(String parameterName, Blob blob) throws SQLException { - setBlob(nameToIndex(parameterName), blob); - } - - @Override - public void setBlob(String parameterName, InputStream inputStream) throws SQLException { - setBlob(nameToIndex(parameterName), inputStream); - } - - @Override - public void setAsciiStream(String parameterName, InputStream inputStream, long length) - throws SQLException { - setAsciiStream(nameToIndex(parameterName), inputStream, length); - } - - @Override - public void setAsciiStream(String parameterName, InputStream inputStream, int length) - throws SQLException { - setAsciiStream(nameToIndex(parameterName), inputStream, length); - } - - @Override - public void setAsciiStream(String parameterName, InputStream inputStream) throws SQLException { - setAsciiStream(nameToIndex(parameterName), inputStream); - } - - @Override - public void setBinaryStream(String parameterName, InputStream inputStream, long length) - throws SQLException { - setBinaryStream(nameToIndex(parameterName), inputStream, length); - } - - @Override - public void setBinaryStream(String parameterName, InputStream inputStream) throws SQLException { - setBinaryStream(nameToIndex(parameterName), inputStream); - } - - @Override - public void setBinaryStream(String parameterName, InputStream inputStream, int length) - throws SQLException { - setBinaryStream(nameToIndex(parameterName), inputStream, length); - } - - @Override - public void setCharacterStream(String parameterName, Reader reader, long length) - throws SQLException { - setCharacterStream(nameToIndex(parameterName), reader, length); - } - - @Override - public void setCharacterStream(String parameterName, Reader reader) throws SQLException { - setCharacterStream(nameToIndex(parameterName), reader); - } - - @Override - public void setCharacterStream(String parameterName, Reader reader, int length) - throws SQLException { - setCharacterStream(nameToIndex(parameterName), reader, length); - } - - @Override - public void setURL(String parameterName, URL url) throws SQLException { - setURL(nameToIndex(parameterName), url); - } - - @Override - public void setNull(String parameterName, int sqlType) throws SQLException { - setNull(nameToIndex(parameterName), sqlType); - } - - @Override - public void setNull(String parameterName, int sqlType, String typeName) throws SQLException { - setNull(nameToIndex(parameterName), sqlType, typeName); - } - - @Override - public void setBoolean(String parameterName, boolean booleanValue) throws SQLException { - setBoolean(nameToIndex(parameterName), booleanValue); - } - - @Override - public void setByte(String parameterName, byte byteValue) throws SQLException { - setByte(nameToIndex(parameterName), byteValue); - } - - @Override - public void setShort(String parameterName, short shortValue) throws SQLException { - setShort(nameToIndex(parameterName), shortValue); - } - - @Override - public void setInt(String parameterName, int intValue) throws SQLException { - setInt(nameToIndex(parameterName), intValue); - } - - @Override - public void setLong(String parameterName, long longValue) throws SQLException { - setLong(nameToIndex(parameterName), longValue); - } - - @Override - public void setFloat(String parameterName, float floatValue) throws SQLException { - setFloat(nameToIndex(parameterName), floatValue); - } - - @Override - public void setDouble(String parameterName, double doubleValue) throws SQLException { - setDouble(nameToIndex(parameterName), doubleValue); - } - - @Override - public void setBigDecimal(String parameterName, BigDecimal bigDecimal) throws SQLException { - setBigDecimal(nameToIndex(parameterName), bigDecimal); - } - - @Override - public void setString(String parameterName, String stringValue) throws SQLException { - setString(nameToIndex(parameterName), stringValue); - } - - @Override - public void setBytes(String parameterName, byte[] bytes) throws SQLException { - setBytes(nameToIndex(parameterName), bytes); - } - - @Override - public void setDate(String parameterName, Date date) throws SQLException { - setDate(nameToIndex(parameterName), date); - } - - @Override - public void setDate(String parameterName, Date date, Calendar cal) throws SQLException { - setDate(nameToIndex(parameterName), date, cal); - } - - @Override - public void setTime(String parameterName, Time time) throws SQLException { - setTime(nameToIndex(parameterName), time); - } - - @Override - public void setTime(String parameterName, Time time, Calendar cal) throws SQLException { - setTime(nameToIndex(parameterName), time, cal); - } - - @Override - public void setTimestamp(String parameterName, Timestamp timestamp) throws SQLException { - setTimestamp(nameToIndex(parameterName), timestamp); - } - - @Override - public void setTimestamp(String parameterName, Timestamp timestamp, Calendar cal) - throws SQLException { - setTimestamp(nameToIndex(parameterName), timestamp, cal); - } - - @Override - public void setObject(String parameterName, Object obj, int targetSqlType, int scale) - throws SQLException { - setObject(nameToIndex(parameterName), obj, targetSqlType, scale); - } - - @Override - public void setObject(String parameterName, Object obj, int targetSqlType) throws SQLException { - setObject(nameToIndex(parameterName), obj, targetSqlType); - } - - @Override - public void setObject(String parameterName, Object obj) throws SQLException { - setObject(nameToIndex(parameterName), obj); - } - - @Override - public void setObject(String parameterName, Object obj, SQLType targetSqlType, int scaleOrLength) - throws SQLException { - setObject(nameToIndex(parameterName), obj, targetSqlType.getVendorTypeNumber(), scaleOrLength); - } - - @Override - public void setObject(String parameterName, Object obj, SQLType targetSqlType) - throws SQLException { - setObject(nameToIndex(parameterName), obj, targetSqlType.getVendorTypeNumber()); - } -} diff --git a/src/main/java/org/mariadb/jdbc/ClientPreparedStatement.java b/src/main/java/org/mariadb/jdbc/ClientPreparedStatement.java new file mode 100644 index 000000000..279fe68c0 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/ClientPreparedStatement.java @@ -0,0 +1,395 @@ +package org.mariadb.jdbc; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; +import javax.sql.StatementEvent; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.result.CompleteResult; +import org.mariadb.jdbc.client.result.Result; +import org.mariadb.jdbc.message.client.*; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.message.server.Completion; +import org.mariadb.jdbc.message.server.OkPacket; +import org.mariadb.jdbc.message.server.PrepareResultPacket; +import org.mariadb.jdbc.util.ClientParser; +import org.mariadb.jdbc.util.NativeSql; +import org.mariadb.jdbc.util.ParameterList; +import org.mariadb.jdbc.util.constants.Capabilities; +import org.mariadb.jdbc.util.constants.ServerStatus; + +public class ClientPreparedStatement extends BasePreparedStatement { + private ClientParser parser; + + public ClientPreparedStatement( + String sql, + Connection con, + ReentrantLock lock, + boolean canUseServerTimeout, + boolean canUseServerMaxRows, + int autoGeneratedKeys, + int resultSetType, + int resultSetConcurrency, + int defaultFetchSize) + throws SQLException { + super( + sql, + con, + lock, + canUseServerTimeout, + canUseServerMaxRows, + autoGeneratedKeys, + resultSetType, + resultSetConcurrency, + defaultFetchSize); + + boolean noBackslashEscapes = + (con.getContext().getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) > 0; + if (con.getContext().getConf().rewriteBatchedStatements()) { + parser = + ClientParser.rewritableParts(NativeSql.parse(sql, con.getContext()), noBackslashEscapes); + } else { + parser = + ClientParser.parameterParts(NativeSql.parse(sql, con.getContext()), noBackslashEscapes); + } + parameters = new ParameterList(parser.getParamCount()); + } + + protected String preSqlCmd() { + if (queryTimeout != 0 && canUseServerTimeout) { + return "SET STATEMENT max_statement_time=" + queryTimeout + " FOR "; + } + return ""; + } + + private void executeInternal() throws SQLException { + checkNotClosed(); + validParameters(); + lock.lock(); + try { + QueryWithParametersPacket query = + new QueryWithParametersPacket(preSqlCmd(), parser, parameters); + results = + con.getClient() + .execute( + query, + this, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion); + } finally { + parameters = new ParameterList(parser.getParamCount()); + lock.unlock(); + } + } + + private List executeInternalPreparedBatch() throws SQLException { + checkNotClosed(); + long serverCapabilities = con.getContext().getServerCapabilities(); + if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS + && (serverCapabilities & Capabilities.MARIADB_CLIENT_STMT_BULK_OPERATIONS) > 0 + && con.getContext().getConf().useBulkStmts()) { + return executeBatchBulk(); + } else { + return executeBatchPipeline(); + } + } + + /** + * Send COM_STMT_PREPARE + X * COM_STMT_BULK_EXECUTE, then read for the all answers + * + * @throws SQLException if IOException / Command error + */ + private List executeBatchBulk() throws SQLException { + String cmd = escapeTimeout(sql); + ClientMessage[] packets = + new ClientMessage[] { + new PreparePacket(cmd), new BulkExecutePacket(null, batchParameters, cmd, null) + }; + try { + List res = + con.getClient() + .executePipeline( + packets, + this, + 0, + maxRows, + ResultSet.CONCUR_READ_ONLY, + ResultSet.TYPE_FORWARD_ONLY, + closeOnCompletion); + ((PrepareResultPacket) res.get(0)).close(con.getClient()); + results = res.subList(1, res.size()); + return results; + } catch (SQLException bue) { + results = null; + throw bue; + } + } + + /** + * Send n * COM_QUERY + n * read answer + * + * @throws SQLException if IOException / Command error + */ + private List executeBatchPipeline() throws SQLException { + ClientMessage[] packets = new ClientMessage[batchParameters.size()]; + for (int i = 0; i < batchParameters.size(); i++) { + packets[i] = new QueryWithParametersPacket(preSqlCmd(), parser, batchParameters.get(i)); + } + try { + results = + con.getClient() + .executePipeline( + packets, + this, + 0, + maxRows, + ResultSet.CONCUR_READ_ONLY, + ResultSet.TYPE_FORWARD_ONLY, + closeOnCompletion); + return results; + } catch (SQLException bue) { + results = null; + throw bue; + } + } + + /** + * Executes the SQL statement in this PreparedStatement object, which may be any kind + * of SQL statement. Some prepared statements return multiple results; the execute + * method handles these complex statements as well as the simpler form of statements handled by + * the methods executeQuery and executeUpdate. + * + *

The execute method returns a boolean to indicate the form of the + * first result. You must call either the method getResultSet or getUpdateCount + * to retrieve the result; you must call getMoreResults to move to any + * subsequent result(s). + * + * @return true if the first result is a ResultSet object; false + * if the first result is an update count or there is no result + * @throws SQLException if a database access error occurs; this method is called on a closed + * PreparedStatement or an argument is supplied to this method + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + * @see Statement#execute + * @see Statement#getResultSet + * @see Statement#getUpdateCount + * @see Statement#getMoreResults + */ + @Override + public boolean execute() throws SQLException { + executeInternal(); + currResult = results.remove(0); + return currResult instanceof Result; + } + + /** + * Executes the SQL query in this PreparedStatement object and returns the + * ResultSet object generated by the query. + * + * @return a ResultSet object that contains the data produced by the query; never + * null + * @throws SQLException if a database access error occurs; this method is called on a closed + * PreparedStatement or the SQL statement does not return a ResultSet + * object + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + */ + @Override + public ResultSet executeQuery() throws SQLException { + executeInternal(); + if (!results.isEmpty()) { + currResult = results.remove(0); + if (currResult instanceof Result) return (Result) currResult; + } + return new CompleteResult( + new ColumnDefinitionPacket[0], new ReadableByteBuf[0], con.getContext()); + } + + /** + * Executes the SQL statement in this PreparedStatement object, which must be an SQL + * Data Manipulation Language (DML) statement, such as INSERT, UPDATE or + * DELETE; or an SQL statement that returns nothing, such as a DDL statement. + * + * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0 + * for SQL statements that return nothing + * @throws SQLException if a database access error occurs; this method is called on a closed + * PreparedStatement or the SQL statement returns a ResultSet object + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + */ + @Override + public int executeUpdate() throws SQLException { + return (int) executeLargeUpdate(); + } + + /** + * Executes the SQL statement in this PreparedStatement object, which must be an SQL + * Data Manipulation Language (DML) statement, such as INSERT, UPDATE or + * DELETE; or an SQL statement that returns nothing, such as a DDL statement. + * + *

This method should be used when the returned row count may exceed {@link Integer#MAX_VALUE}. + * + *

The default implementation will throw {@code UnsupportedOperationException} + * + * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0 + * for SQL statements that return nothing + * @throws SQLException if a database access error occurs; this method is called on a closed + * PreparedStatement or the SQL statement returns a ResultSet object + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + * @since 1.8 + */ + @Override + public long executeLargeUpdate() throws SQLException { + executeInternal(); + currResult = results.remove(0); + if (currResult instanceof Result) { + throw exceptionFactory() + .create("the given SQL statement produces an unexpected ResultSet object", "HY000"); + } + return ((OkPacket) currResult).getAffectedRows(); + } + + /** + * Adds a set of parameters to this PreparedStatement object's batch of commands. + * + * @throws SQLException if a database access error occurs or this method is called on a closed + * PreparedStatement + * @see Statement#addBatch + * @since 1.2 + */ + @Override + public void addBatch() throws SQLException { + validParameters(); + if (batchParameters == null) batchParameters = new ArrayList<>(); + batchParameters.add(parameters); + parameters = new ParameterList(); + } + + protected void validParameters() throws SQLException { + for (int i = 0; i < parser.getParamCount(); i++) { + if (!parameters.containsKey(i)) { + throw exceptionFactory() + .create("Parameter at position " + (i + 1) + " is not set", "07004"); + } + } + } + + /** + * Retrieves a ResultSetMetaData object that contains information about the columns + * of the ResultSet object that will be returned when this PreparedStatement + * object is executed. + * + *

Because a PreparedStatement object is precompiled, it is possible to know about + * the ResultSet object that it will return without having to execute it. + * Consequently, it is possible to invoke the method getMetaData on a + * PreparedStatement object rather than waiting to execute it and then invoking the + * ResultSet.getMetaData method on the ResultSet object that is returned. + * + *

NOTE: Using this method may be expensive for some drivers due to the lack of + * underlying DBMS support. + * + * @return the description of a ResultSet object's columns or null if + * the driver cannot return a ResultSetMetaData object + * @throws SQLException if a database access error occurs or this method is called on a closed + * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.2 + */ + @Override + public ResultSetMetaData getMetaData() throws SQLException { + + // send COM_STMT_PREPARE + PreparePacket prepare = new PreparePacket(escapeTimeout(sql)); + PrepareResultPacket prepareResult = + (PrepareResultPacket) con.getClient().execute(prepare).get(0); + + try { + return new org.mariadb.jdbc.client.result.ResultSetMetaData( + exceptionFactory(), prepareResult.getColumns(), con.getContext().getConf(), false, false); + } finally { + prepareResult.close(con.getClient()); + } + } + + /** + * Retrieves the number, types and properties of this PreparedStatement object's + * parameters. + * + * @return a ParameterMetaData object that contains information about the number, + * types and properties for each parameter marker of this PreparedStatement + * object + * @throws SQLException if a database access error occurs or this method is called on a closed + * PreparedStatement + * @see ParameterMetaData + * @since 1.4 + */ + @Override + public ParameterMetaData getParameterMetaData() throws SQLException { + // send COM_STMT_PREPARE + PreparePacket prepare = new PreparePacket(escapeTimeout(sql)); + PrepareResultPacket prepareResult = + (PrepareResultPacket) con.getClient().execute(prepare).get(0); + + try { + return new ParameterMetaData(exceptionFactory(), prepareResult.getParameters()); + } finally { + prepareResult.close(con.getClient()); + } + } + + @Override + public int[] executeBatch() throws SQLException { + checkNotClosed(); + if (batchParameters == null || batchParameters.isEmpty()) return new int[0]; + lock.lock(); + try { + List res = executeInternalPreparedBatch(); + results = res; + int[] updates = new int[res.size()]; + for (int i = 0; i < res.size(); i++) { + updates[i] = (int) ((OkPacket) res.get(i)).getAffectedRows(); + } + currResult = results.remove(0); + return updates; + } finally { + batchParameters = new ArrayList<>(); + lock.unlock(); + } + } + + @Override + public long[] executeLargeBatch() throws SQLException { + checkNotClosed(); + if (batchParameters == null || batchParameters.isEmpty()) return new long[0]; + lock.lock(); + try { + List res = executeInternalPreparedBatch(); + results = res; + long[] updates = new long[res.size()]; + for (int i = 0; i < res.size(); i++) { + updates[i] = ((OkPacket) res.get(i)).getAffectedRows(); + } + currResult = results.remove(0); + return updates; + + } finally { + batchParameters = new ArrayList<>(); + lock.unlock(); + } + } + + @Override + public void close() throws SQLException { + con.fireStatementClosed(new StatementEvent(con, this)); + super.close(); + } +} diff --git a/src/main/java/org/mariadb/jdbc/ClientSidePreparedStatement.java b/src/main/java/org/mariadb/jdbc/ClientSidePreparedStatement.java deleted file mode 100644 index d4863ca64..000000000 --- a/src/main/java/org/mariadb/jdbc/ClientSidePreparedStatement.java +++ /dev/null @@ -1,567 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.sql.*; -import java.util.ArrayList; -import java.util.List; -import org.mariadb.jdbc.internal.com.read.dao.Results; -import org.mariadb.jdbc.internal.com.read.resultset.SelectResultSet; -import org.mariadb.jdbc.internal.com.send.parameters.ParameterHolder; -import org.mariadb.jdbc.internal.logging.Logger; -import org.mariadb.jdbc.internal.logging.LoggerFactory; -import org.mariadb.jdbc.internal.util.dao.ClientPrepareResult; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; - -public class ClientSidePreparedStatement extends BasePrepareStatement { - - private static final Logger logger = LoggerFactory.getLogger(ClientSidePreparedStatement.class); - private final List parameterList = new ArrayList<>(); - private ClientPrepareResult prepareResult; - private String sqlQuery; - private ParameterHolder[] parameters; - private ResultSetMetaData resultSetMetaData = null; - private ParameterMetaData parameterMetaData = null; - - /** - * Constructor. - * - * @param connection connection - * @param sql sql query - * @param resultSetScrollType one of the following ResultSet constants: - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, or - * ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of ResultSet.CONCUR_READ_ONLY - * or ResultSet.CONCUR_UPDATABLE - * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be returned; one - * of Statement.RETURN_GENERATED_KEYS or Statement.NO_GENERATED_KEYS - * @param exceptionFactory exception factory - * @throws SQLException exception - */ - public ClientSidePreparedStatement( - MariaDbConnection connection, - String sql, - int resultSetScrollType, - int resultSetConcurrency, - int autoGeneratedKeys, - ExceptionFactory exceptionFactory) - throws SQLException { - super( - connection, resultSetScrollType, resultSetConcurrency, autoGeneratedKeys, exceptionFactory); - sqlQuery = sql; - - if (options.rewriteBatchedStatements) { - prepareResult = ClientPrepareResult.rewritableParts(sqlQuery, protocol.noBackslashEscapes()); - } else { - prepareResult = ClientPrepareResult.parameterParts(sqlQuery, protocol.noBackslashEscapes()); - } - parameters = new ParameterHolder[prepareResult.getParamCount()]; - } - - /** - * Clone statement. - * - * @param connection connection - * @return Clone statement. - * @throws CloneNotSupportedException if any error occur. - */ - public ClientSidePreparedStatement clone(MariaDbConnection connection) - throws CloneNotSupportedException { - ClientSidePreparedStatement clone = (ClientSidePreparedStatement) super.clone(connection); - clone.sqlQuery = sqlQuery; - clone.prepareResult = prepareResult; - clone.parameters = new ParameterHolder[prepareResult.getParamCount()]; - clone.resultSetMetaData = resultSetMetaData; - clone.parameterMetaData = parameterMetaData; - return clone; - } - - /** - * Executes the SQL statement in this PreparedStatement object, which may be any kind - * of SQL statement. Some prepared statements return multiple results; the execute - * method handles these complex statements as well as the simpler form of statements handled by - * the methods executeQuery and executeUpdate.
- * The execute method returns a boolean to indicate the form of the - * first result. You must call either the method getResultSet or getUpdateCount - * to retrieve the result; you must call getInternalMoreResults to move to - * any subsequent result(s). - * - * @return true if the first result is a ResultSet object; false - * if the first result is an update count or there is no result - * @throws SQLException if a database access error occurs; this method is called on a closed - * PreparedStatement or an argument is supplied to this method - * @see Statement#execute - * @see Statement#getResultSet - * @see Statement#getUpdateCount - * @see Statement#getMoreResults - */ - public boolean execute() throws SQLException { - return executeInternal(getFetchSize()); - } - - /** - * Executes the SQL query in this PreparedStatement object and returns the - * ResultSet object generated by the query. - * - * @return a ResultSet object that contains the data produced by the query; never - * null - * @throws SQLException if a database access error occurs; this method is called on a closed - * PreparedStatement or the SQL statement does not return a ResultSet - * object - */ - public ResultSet executeQuery() throws SQLException { - if (execute()) { - return results.getResultSet(); - } - return SelectResultSet.createEmptyResultSet(); - } - - /** - * Executes the SQL statement in this PreparedStatement object, which must be an SQL - * Data Manipulation Language (DML) statement, such as INSERT, UPDATE or - * DELETE; or an SQL statement that returns nothing, such as a DDL statement. - * Result-set are permitted for historical reason, even if spec indicate to throw exception. - * - * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0 - * for SQL statements that return nothing - * @throws SQLException if a database access error occurs; this method is called on a closed - * PreparedStatement - */ - public int executeUpdate() throws SQLException { - if (execute()) { - return 0; - } - return getUpdateCount(); - } - - protected boolean executeInternal(int fetchSize) throws SQLException { - - // valid parameters - for (int i = 0; i < prepareResult.getParamCount(); i++) { - if (parameters[i] == null) { - logger.error("Parameter at position {} is not set", (i + 1)); - throw exceptionFactory - .raiseStatementError(connection, this) - .create("Parameter at position " + (i + 1) + " is " + "not set", "07004"); - } - } - - lock.lock(); - try { - executeQueryPrologue(false); - results = - new Results( - this, - fetchSize, - false, - 1, - false, - resultSetScrollType, - resultSetConcurrency, - autoGeneratedKeys, - protocol.getAutoIncrementIncrement(), - sqlQuery, - parameters); - if (queryTimeout != 0 && canUseServerTimeout) { - // timer will not be used for timeout to avoid having threads - protocol.executeQuery( - protocol.isMasterConnection(), results, prepareResult, parameters, queryTimeout); - } else { - protocol.executeQuery(protocol.isMasterConnection(), results, prepareResult, parameters); - } - results.commandEnd(); - return results.getResultSet() != null; - - } catch (SQLException exception) { - if (results != null) { - results.commandEnd(); - } - throw executeExceptionEpilogue(exception); - } finally { - executeEpilogue(); - lock.unlock(); - } - } - - /** - * Adds a set of parameters to this PreparedStatement object's batch of send.
- *
- * - * @throws SQLException if a database access error occurs or this method is called on a closed - * PreparedStatement - * @see Statement#addBatch - * @since 1.2 - */ - public void addBatch() throws SQLException { - ParameterHolder[] holder = new ParameterHolder[prepareResult.getParamCount()]; - for (int i = 0; i < holder.length; i++) { - holder[i] = parameters[i]; - if (holder[i] == null) { - logger.error( - "You need to set exactly " - + prepareResult.getParamCount() - + " parameters on the prepared statement"); - throw exceptionFactory - .raiseStatementError(connection, this) - .create( - "You need to set exactly " - + prepareResult.getParamCount() - + " parameters on the prepared statement"); - } - } - parameterList.add(holder); - } - - /** - * Add batch. - * - * @param sql typically this is a SQL INSERT or UPDATE statement - * @throws SQLException every time since that method is forbidden on prepareStatement - */ - @Override - public void addBatch(final String sql) throws SQLException { - throw exceptionFactory - .raiseStatementError(connection, this) - .create("Cannot do addBatch(String) on preparedStatement"); - } - - /** Clear batch. */ - @Override - public void clearBatch() { - parameterList.clear(); - hasLongData = false; - this.parameters = new ParameterHolder[prepareResult.getParamCount()]; - } - - /** {inheritdoc}. */ - public int[] executeBatch() throws SQLException { - checkClose(); - int size = parameterList.size(); - if (size == 0) { - return new int[0]; - } - - lock.lock(); - try { - executeInternalBatch(size); - results.commandEnd(); - return results.getCmdInformation().getUpdateCounts(); - - } catch (SQLException sqle) { - throw executeBatchExceptionEpilogue(sqle, size); - } finally { - executeBatchEpilogue(); - lock.unlock(); - } - } - - /** - * Non JDBC : Permit to retrieve server update counts when using option rewriteBatchedStatements. - * - * @return an array of update counts containing one element for each command in the batch. The - * elements of the array are ordered according to the order in which commands were added to - * the batch. - */ - public int[] getServerUpdateCounts() { - if (results != null && results.getCmdInformation() != null) { - return results.getCmdInformation().getServerUpdateCounts(); - } - return new int[0]; - } - - /** - * Execute batch, like executeBatch(), with returning results with long[]. For when row count may - * exceed Integer.MAX_VALUE. - * - * @return an array of update counts (one element for each command in the batch) - * @throws SQLException if a database error occur. - */ - public long[] executeLargeBatch() throws SQLException { - checkClose(); - int size = parameterList.size(); - if (size == 0) { - return new long[0]; - } - - lock.lock(); - try { - executeInternalBatch(size); - results.commandEnd(); - return results.getCmdInformation().getLargeUpdateCounts(); - } catch (SQLException sqle) { - throw executeBatchExceptionEpilogue(sqle, size); - } finally { - executeBatchEpilogue(); - lock.unlock(); - } - } - - /** - * Choose better way to execute queries according to query and options. - * - * @param size parameters number - * @throws SQLException if any error occur - */ - private void executeInternalBatch(int size) throws SQLException { - executeQueryPrologue(true); - results = - new Results( - this, - 0, - true, - size, - false, - resultSetScrollType, - resultSetConcurrency, - autoGeneratedKeys, - protocol.getAutoIncrementIncrement(), - null, - null); - if (protocol.executeBatchClient( - protocol.isMasterConnection(), results, prepareResult, parameterList, hasLongData)) { - return; - } - - // send query one by one, reading results for each query before sending another one - SQLException exception = null; - - if (queryTimeout > 0) { - for (int batchQueriesCount = 0; batchQueriesCount < size; batchQueriesCount++) { - protocol.stopIfInterrupted(); - try { - protocol.executeQuery( - protocol.isMasterConnection(), - results, - prepareResult, - parameterList.get(batchQueriesCount)); - } catch (SQLException e) { - if (options.continueBatchOnError) { - exception = e; - } else { - throw e; - } - } - } - - } else { - for (int batchQueriesCount = 0; batchQueriesCount < size; batchQueriesCount++) { - try { - protocol.executeQuery( - protocol.isMasterConnection(), - results, - prepareResult, - parameterList.get(batchQueriesCount)); - } catch (SQLException e) { - if (options.continueBatchOnError) { - exception = e; - } else { - throw e; - } - } - } - } - if (exception != null) { - throw exception; - } - } - - /** - * Retrieves a ResultSetMetaData object that contains information about the columns - * of the ResultSet object that will be returned when this PreparedStatement - * object is executed.
- * Because a PreparedStatement object is precompiled, it is possible to know about - * the ResultSet object that it will return without having to execute it. - * Consequently, it is possible to invoke the method getMetaData on a - * PreparedStatement object rather than waiting to execute it and then invoking the - * ResultSet.getMetaData method on the ResultSet object that is returned. - * - * @return the description of a ResultSet object's columns or null if - * the driver cannot return a ResultSetMetaData object - * @throws SQLException if a database access error occurs or this method is called on a closed - * PreparedStatement - */ - public ResultSetMetaData getMetaData() throws SQLException { - checkClose(); - ResultSet rs = getResultSet(); - if (rs != null) { - return rs.getMetaData(); - } - if (resultSetMetaData == null) { - loadParametersData(); - } - return resultSetMetaData; - } - - /** - * Set parameter. - * - * @param parameterIndex index - * @param holder parameter holder - * @throws SQLException if index position doesn't correspond to query parameters - */ - public void setParameter(final int parameterIndex, final ParameterHolder holder) - throws SQLException { - if (parameterIndex >= 1 && parameterIndex < prepareResult.getParamCount() + 1) { - parameters[parameterIndex - 1] = holder; - } else { - String error = - "Could not set parameter at position " - + parameterIndex - + " (values was " - + holder.toString() - + ")\n" - + "Query - conn:" - + protocol.getServerThreadId() - + "(" - + (protocol.isMasterConnection() ? "M" : "S") - + ") "; - - if (options.maxQuerySizeToLog > 0) { - error += " - \""; - if (sqlQuery.length() < options.maxQuerySizeToLog) { - error += sqlQuery; - } else { - error += sqlQuery.substring(0, options.maxQuerySizeToLog) + "..."; - } - error += "\""; - } else { - error += " - \"" + sqlQuery + "\""; - } - - logger.error(error); - throw exceptionFactory.raiseStatementError(connection, this).create(error); - } - } - - /** - * Retrieves the number, types and properties of this PreparedStatement object's - * parameters. - * - * @return a ParameterMetaData object that contains information about the number, - * types and properties for each parameter marker of this PreparedStatement - * object - * @throws SQLException if a database access error occurs or this method is called on a closed - * PreparedStatement - * @see ParameterMetaData - * @since 1.4 - */ - public ParameterMetaData getParameterMetaData() throws SQLException { - checkClose(); - if (parameterMetaData == null) { - loadParametersData(); - } - return parameterMetaData; - } - - private void loadParametersData() throws SQLSyntaxErrorException { - try (ServerSidePreparedStatement ssps = - new ServerSidePreparedStatement( - connection, - sqlQuery, - ResultSet.TYPE_SCROLL_INSENSITIVE, - ResultSet.CONCUR_READ_ONLY, - Statement.NO_GENERATED_KEYS, - exceptionFactory)) { - resultSetMetaData = ssps.getMetaData(); - parameterMetaData = ssps.getParameterMetaData(); - } catch (Exception exception) { - parameterMetaData = new SimpleParameterMetaData(prepareResult.getParamCount()); - } - } - - /** - * Clears the current parameter values immediately. - * - *

In general, parameter values remain in force for repeated use of a statement. Setting a - * parameter value automatically clears its previous value. However, in some cases it is useful to - * immediately release the resources used by the current parameter values; this can be done by - * calling the method clearParameters. - */ - public void clearParameters() { - parameters = new ParameterHolder[prepareResult.getParamCount()]; - } - - // Close prepared statement, maybe fire closed-statement events - @Override - public void close() throws SQLException { - super.close(); - } - - protected int getParameterCount() { - return prepareResult.getParamCount(); - } - - /** {inherit}. */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder("sql : '" + sqlQuery + "'"); - sb.append(", parameters : ["); - for (int i = 0; i < parameters.length; i++) { - ParameterHolder holder = parameters[i]; - if (holder == null) { - sb.append("null"); - } else { - sb.append(holder.toString()); - } - if (i != parameters.length - 1) { - sb.append(","); - } - } - sb.append("]"); - return sb.toString(); - } - - protected ClientPrepareResult getPrepareResult() { - return prepareResult; - } -} diff --git a/src/main/java/org/mariadb/jdbc/Configuration.java b/src/main/java/org/mariadb/jdbc/Configuration.java new file mode 100644 index 000000000..47f41e44e --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/Configuration.java @@ -0,0 +1,1467 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +import java.lang.reflect.Field; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.mariadb.jdbc.plugin.credential.CredentialPlugin; +import org.mariadb.jdbc.plugin.credential.CredentialPluginLoader; +import org.mariadb.jdbc.util.constants.HaMode; +import org.mariadb.jdbc.util.options.OptionAliases; + +/** + * parse and verification of URL. + * + *

basic syntax :
+ * {@code + * jdbc:mariadb:[replication:|failover|loadbalance:|aurora:]//[,]/[database>] + * [?=[&=]] } + * + *

hostDescription:
+ * - simple :
+ * {@code :}
+ * (for example localhost:3306)
+ *
+ * - complex :
+ * {@code address=[(type=(master|slave))][(port=)](host=)}
+ *
+ *
+ * type is by default master
+ * port is by default 3306
+ * + *

host can be dns name, ipv4 or ipv6.
+ * in case of ipv6 and simple host description, the ip must be written inside bracket.
+ * exemple : {@code jdbc:mariadb://[2001:0660:7401:0200:0000:0000:0edf:bdd7]:3306}
+ * + *

Some examples :
+ * {@code jdbc:mariadb://localhost:3306/database?user=greg&password=pass}
+ * {@code + * jdbc:mariadb://address=(type=master)(host=master1),address=(port=3307)(type=slave)(host=slave1)/database?user=greg&password=pass} + *
+ */ +public class Configuration implements Cloneable { + + private static final Pattern URL_PARAMETER = + Pattern.compile("(\\/([^\\?]*))?(\\?(.+))*", Pattern.DOTALL); + private String initialUrl; + private String database; + private List addresses; + private HaMode haMode; + private CredentialPlugin credentialType; + private String user; + private String password; + private String enabledSslProtocolSuites; + private boolean pinGlobalTxToPhysicalConnection; + private String socketFactory; + private int connectTimeout; + private String pipe; + private String localSocket; + private boolean tcpKeepAlive; + private boolean tcpAbortiveClose; + private String localSocketAddress; + private int socketTimeout; + private boolean allowMultiQueries; + private boolean rewriteBatchedStatements; + private boolean useCompression; + private boolean blankTableNameMeta; + private SslMode sslMode; + private String enabledSslCipherSuites; + private String sessionVariables; + private boolean tinyInt1isBit; + private boolean yearIsDateType; + private String timezone; + private boolean dumpQueriesOnException; + private int prepStmtCacheSize; + private boolean useAffectedRows; + private boolean useServerPrepStmts; + private String connectionAttributes; + private boolean useBulkStmts; + private boolean autocommit; + private boolean includeInnodbStatusInDeadlockExceptions; + private boolean includeThreadDumpInDeadlockExceptions; + private String servicePrincipalName; + private int defaultFetchSize; + private Properties nonMappedOptions; + private String tlsSocketType; + private String serverSslCert; + private int maxQuerySizeToLog; + private Integer maxAllowedPacket; + private boolean assureReadOnly; + private int retriesAllDown; + private int validConnectionTimeout; + private int loadBalanceBlacklistTimeout; + private int failoverLoopRetries; + private String galeraAllowedState; + private boolean pool; + private String poolName; + private int maxPoolSize; + private Integer minPoolSize; + private int maxIdleTime; + private boolean staticGlobal; + private boolean registerJmxPool; + private int poolValidMinDelay; + private boolean useResetConnection; + private String serverRsaPublicKeyFile; + private boolean allowPublicKeyRetrieval; + private boolean useReadAheadInput; + private boolean cachePrepStmts; + + private Configuration( + String database, + List addresses, + HaMode haMode, + String user, + String password, + String enabledSslProtocolSuites, + Boolean pinGlobalTxToPhysicalConnection, + String socketFactory, + Integer connectTimeout, + String pipe, + String localSocket, + Boolean tcpKeepAlive, + Boolean tcpAbortiveClose, + String localSocketAddress, + Integer socketTimeout, + Boolean allowMultiQueries, + Boolean rewriteBatchedStatements, + Boolean useCompression, + Boolean blankTableNameMeta, + String credentialType, + SslMode sslMode, + String enabledSslCipherSuites, + String sessionVariables, + Boolean tinyInt1isBit, + Boolean yearIsDateType, + String timezone, + Boolean dumpQueriesOnException, + Integer prepStmtCacheSize, + Boolean useAffectedRows, + Boolean useServerPrepStmts, + String connectionAttributes, + Boolean useBulkStmts, + Boolean autocommit, + Boolean includeInnodbStatusInDeadlockExceptions, + Boolean includeThreadDumpInDeadlockExceptions, + String servicePrincipalName, + Integer defaultFetchSize, + String tlsSocketType, + Integer maxQuerySizeToLog, + Integer maxAllowedPacket, + Boolean assureReadOnly, + Integer retriesAllDown, + Integer validConnectionTimeout, + Integer loadBalanceBlacklistTimeout, + Integer failoverLoopRetries, + String galeraAllowedState, + Boolean pool, + String poolName, + Integer maxPoolSize, + Integer minPoolSize, + Integer maxIdleTime, + Boolean staticGlobal, + Boolean registerJmxPool, + Integer poolValidMinDelay, + Boolean useResetConnection, + String serverRsaPublicKeyFile, + Boolean allowPublicKeyRetrieval, + String serverSslCert, + Boolean useReadAheadInput, + Boolean cachePrepStmts, + Properties nonMappedOptions) + throws SQLException { + this.database = database; + this.addresses = addresses; + this.nonMappedOptions = nonMappedOptions; + this.haMode = haMode != null ? haMode : HaMode.NONE; + this.credentialType = CredentialPluginLoader.get(credentialType); + this.user = user; + this.password = password; + this.enabledSslProtocolSuites = enabledSslProtocolSuites; + this.pinGlobalTxToPhysicalConnection = + pinGlobalTxToPhysicalConnection != null ? pinGlobalTxToPhysicalConnection : false; + this.socketFactory = socketFactory; + this.connectTimeout = + connectTimeout != null + ? connectTimeout + : (DriverManager.getLoginTimeout() > 0 + ? DriverManager.getLoginTimeout() * 1000 + : 30_000); + this.pipe = pipe; + this.localSocket = localSocket; + this.tcpKeepAlive = tcpKeepAlive != null ? tcpKeepAlive : false; + this.tcpAbortiveClose = tcpAbortiveClose != null ? tcpAbortiveClose : false; + this.localSocketAddress = localSocketAddress; + this.socketTimeout = socketTimeout != null ? socketTimeout : 0; + this.allowMultiQueries = allowMultiQueries != null ? allowMultiQueries : false; + this.rewriteBatchedStatements = + rewriteBatchedStatements != null ? rewriteBatchedStatements : false; + this.useCompression = useCompression != null ? useCompression : false; + this.blankTableNameMeta = blankTableNameMeta != null ? blankTableNameMeta : false; + this.sslMode = sslMode != null ? sslMode : SslMode.DISABLE; + this.enabledSslCipherSuites = enabledSslCipherSuites; + this.sessionVariables = sessionVariables; + this.tinyInt1isBit = tinyInt1isBit != null ? tinyInt1isBit : true; + this.yearIsDateType = yearIsDateType != null ? yearIsDateType : true; + this.timezone = timezone; + this.dumpQueriesOnException = dumpQueriesOnException != null ? dumpQueriesOnException : false; + this.prepStmtCacheSize = prepStmtCacheSize != null ? prepStmtCacheSize : 250; + this.useAffectedRows = useAffectedRows != null ? useAffectedRows : false; + this.useServerPrepStmts = useServerPrepStmts != null ? useServerPrepStmts : false; + this.connectionAttributes = connectionAttributes; + this.useBulkStmts = useBulkStmts != null ? useBulkStmts : true; + this.autocommit = autocommit != null ? autocommit : true; + this.includeInnodbStatusInDeadlockExceptions = + includeInnodbStatusInDeadlockExceptions != null + ? includeInnodbStatusInDeadlockExceptions + : false; + this.includeThreadDumpInDeadlockExceptions = + includeThreadDumpInDeadlockExceptions != null + ? includeThreadDumpInDeadlockExceptions + : false; + this.servicePrincipalName = servicePrincipalName; + this.defaultFetchSize = defaultFetchSize != null ? defaultFetchSize : 0; + this.tlsSocketType = tlsSocketType; + this.maxQuerySizeToLog = maxQuerySizeToLog != null ? maxQuerySizeToLog : 1024; + this.maxAllowedPacket = maxAllowedPacket; + this.assureReadOnly = assureReadOnly != null ? assureReadOnly : false; + this.retriesAllDown = retriesAllDown != null ? retriesAllDown : 120; + this.validConnectionTimeout = validConnectionTimeout != null ? validConnectionTimeout : 0; + this.loadBalanceBlacklistTimeout = + loadBalanceBlacklistTimeout != null ? loadBalanceBlacklistTimeout : 50; + this.failoverLoopRetries = failoverLoopRetries != null ? failoverLoopRetries : 120; + this.galeraAllowedState = galeraAllowedState; + this.pool = pool != null ? pool : false; + this.poolName = poolName; + this.maxPoolSize = maxPoolSize != null ? maxPoolSize : 8; + this.minPoolSize = minPoolSize; + this.maxIdleTime = maxIdleTime != null ? maxIdleTime : 600; + this.staticGlobal = staticGlobal != null ? staticGlobal : false; + this.registerJmxPool = registerJmxPool != null ? registerJmxPool : true; + this.poolValidMinDelay = poolValidMinDelay != null ? poolValidMinDelay : 1000; + this.useResetConnection = useResetConnection != null ? useResetConnection : false; + this.serverRsaPublicKeyFile = serverRsaPublicKeyFile; + this.allowPublicKeyRetrieval = + allowPublicKeyRetrieval != null ? allowPublicKeyRetrieval : false; + this.useReadAheadInput = useReadAheadInput != null ? useReadAheadInput : true; + this.cachePrepStmts = cachePrepStmts != null ? cachePrepStmts : true; + this.serverSslCert = serverSslCert; + + // ************************************************************* + // option value verification + // ************************************************************* + + // int fields must all be positive + Field[] fields = Configuration.class.getDeclaredFields(); + try { + for (Field field : fields) { + if (field.getType().equals(Integer.class) || field.getType().equals(int.class)) { + Integer val = (Integer) field.get(this); + if (val != null && val < 0) { + throw new SQLException( + String.format("Value for %s must be >= 1 (value is %s)", field.getName(), val)); + } + } + } + } catch (IllegalArgumentException | IllegalAccessException ie) { + // eat + } + + // ************************************************************* + // option coherence check + // ************************************************************* + // disable use server prepare id using client rewrite + if (this.rewriteBatchedStatements) { + this.useServerPrepStmts = false; + } + + // if min pool size default to maximum pool size if not set + if (this.pool) { + this.minPoolSize = + this.minPoolSize == null + ? this.maxPoolSize + : Math.min(this.minPoolSize, this.maxPoolSize); + } + + // if fetchSize is set to less than 0, default it to 0 + if (this.defaultFetchSize < 0) { + this.defaultFetchSize = 0; + } + + if (this.credentialType != null + && this.credentialType.mustUseSsl() + && this.sslMode == SslMode.DISABLE) { + this.sslMode = SslMode.VERIFY_FULL; + } + } + + /** + * Tell if mariadb driver accept url string. (Correspond to interface + * java.jdbc.Driver.acceptsURL() method) + * + * @param url url String + * @return true if url string correspond. + */ + public static boolean acceptsUrl(String url) { + return url != null && url.startsWith("jdbc:mariadb:"); + } + + public static Configuration parse(final String url) throws SQLException { + return parse(url, new Properties()); + } + + /** + * Parse url connection string with additional properties. + * + * @param url connection string + * @param prop properties + * @return UrlParser instance + * @throws SQLException if parsing exception occur + */ + public static Configuration parse(final String url, Properties prop) throws SQLException { + if (url != null && url.startsWith("jdbc:mariadb:")) { + return parseInternal(url, (prop == null) ? new Properties() : prop); + } + return null; + } + + /** + * Parses the connection URL in order to set the UrlParser instance with all the information + * provided through the URL. + * + * @param url connection URL + * @param properties properties + * @throws SQLException if format is incorrect + */ + private static Configuration parseInternal(String url, Properties properties) + throws SQLException { + try { + Builder builder = new Builder(); + int separator = url.indexOf("//"); + if (separator == -1) { + throw new IllegalArgumentException( + "url parsing error : '//' is not present in the url " + url); + } + builder.haMode(parseHaMode(url, separator)); + + String urlSecondPart = url.substring(separator + 2); + int dbIndex = urlSecondPart.indexOf("/"); + int paramIndex = urlSecondPart.indexOf("?"); + + String hostAddressesString; + String additionalParameters; + if ((dbIndex < paramIndex && dbIndex < 0) || (dbIndex > paramIndex && paramIndex > -1)) { + hostAddressesString = urlSecondPart.substring(0, paramIndex); + additionalParameters = urlSecondPart.substring(paramIndex); + } else if ((dbIndex < paramIndex && dbIndex > -1) + || (dbIndex > paramIndex && paramIndex < 0)) { + hostAddressesString = urlSecondPart.substring(0, dbIndex); + additionalParameters = urlSecondPart.substring(dbIndex); + } else { + hostAddressesString = urlSecondPart; + additionalParameters = null; + } + + if (additionalParameters != null) { + //noinspection Annotator + Matcher matcher = URL_PARAMETER.matcher(additionalParameters); + matcher.find(); + String database = matcher.group(2); + if (database != null && !database.isEmpty()) { + builder.database(database); + } + String urlParameters = matcher.group(4); + if (urlParameters != null && !urlParameters.isEmpty()) { + String[] parameters = urlParameters.split("&"); + for (String parameter : parameters) { + int pos = parameter.indexOf('='); + if (pos == -1) { + properties.setProperty(parameter, ""); + } else { + properties.setProperty(parameter.substring(0, pos), parameter.substring(pos + 1)); + } + } + } + } else { + builder.database(null); + } + + mapPropertiesToOption(builder, properties); + builder._addresses = HostAddress.parse(hostAddressesString, builder._haMode); + return builder.build(); + + } catch (IllegalArgumentException i) { + throw new SQLException("error parsing url : " + i.getMessage(), i); + } + } + + private static void mapPropertiesToOption(Builder builder, Properties properties) + throws SQLException { + Properties nonMappedOptions = new Properties(); + + try { + // Option object is already initialized to default values. + // loop on properties, + // - check DefaultOption to check that property value correspond to type (and range) + // - set values + for (String key : properties.stringPropertyNames()) { + String realKey = OptionAliases.OPTIONS_ALIASES.get(key); + if (realKey == null) realKey = key; + final String propertyValue = properties.getProperty(realKey); + + if (propertyValue != null) { + try { + final Field field = Builder.class.getDeclaredField(realKey); + field.setAccessible(true); + if (field.getGenericType().equals(String.class)) { + field.set(builder, propertyValue); + } else if (field.getGenericType().equals(Boolean.class)) { + switch (propertyValue.toLowerCase()) { + case "": + case "1": + case "true": + field.set(builder, Boolean.TRUE); + break; + + case "0": + case "false": + field.set(builder, Boolean.FALSE); + break; + + default: + throw new IllegalArgumentException( + String.format( + "Optional parameter %s must be boolean (true/false or 0/1) was '%s'", + key, propertyValue)); + } + } else if (field.getGenericType().equals(Integer.class)) { + try { + final Integer value = Integer.parseInt(propertyValue); + field.set(builder, value); + } catch (NumberFormatException n) { + throw new IllegalArgumentException( + String.format( + "Optional parameter %s must be Integer, was '%s'", key, propertyValue)); + } + } else if (field.getGenericType().equals(Long.class)) { + try { + final Long value = Long.parseLong(propertyValue); + field.set(builder, value); + } catch (NumberFormatException n) { + throw new IllegalArgumentException( + String.format( + "Optional parameter %s must be Long, was '%s'", key, propertyValue)); + } + } else if (field.getGenericType().equals(SslMode.class)) { + if (propertyValue.isEmpty()) { + field.set(builder, SslMode.VERIFY_FULL); + } else { + try { + field.set(builder, SslMode.from(propertyValue)); + } catch (IllegalArgumentException i) { + throw new SQLException(i.getMessage()); + } + } + } + } catch (NoSuchFieldException nfe) { + // keep unknown option: + // those might be used in authentication or identity plugin + nonMappedOptions.put(key, properties.getProperty(key)); + } + } + } + + } catch (IllegalAccessException n) { + n.printStackTrace(); + } catch (SecurityException s) { + // only for jws, so never thrown + throw new IllegalArgumentException("Security too restrictive : " + s.getMessage()); + } + builder._nonMappedOptions = nonMappedOptions; + } + + private static HaMode parseHaMode(String url, int separator) { + // parser is sure to have at least 2 colon, since jdbc:[mysql|mariadb]: is tested. + int firstColonPos = url.indexOf(':'); + int secondColonPos = url.indexOf(':', firstColonPos + 1); + int thirdColonPos = url.indexOf(':', secondColonPos + 1); + + if (thirdColonPos > separator || thirdColonPos == -1) { + if (secondColonPos == separator - 1) { + return HaMode.NONE; + } + thirdColonPos = separator; + } + + try { + String haModeString = url.substring(secondColonPos + 1, thirdColonPos); + if ("FAILOVER".equalsIgnoreCase(haModeString)) { + haModeString = "LOADBALANCE"; + } + return HaMode.from(haModeString); + } catch (IllegalArgumentException i) { + throw new IllegalArgumentException( + "wrong failover parameter format in connection String " + url); + } + } + + /** + * Parse url connection string. + * + * @param url connection string + * @return Configuration class + * @throws SQLException if url format is incorrect + */ + public static Configuration parseUrl(String url) throws SQLException { + if (acceptsUrl(url)) { + return parseInternal(url, new Properties()); + } + return null; + } + + protected Configuration clone(String username, String password) + throws CloneNotSupportedException { + Configuration conf = (Configuration) super.clone(); + conf.user = username; + conf.password = password; + return conf; + } + + public String database() { + return database; + } + + public List addresses() { + return addresses; + } + + public HaMode haMode() { + return haMode; + } + + public CredentialPlugin credentialPlugin() { + return credentialType; + } + + public String user() { + return user; + } + + public String password() { + return password; + } + + public String initialUrl() { + return initialUrl; + } + + public String serverSslCert() { + return serverSslCert; + } + + public String enabledSslProtocolSuites() { + return enabledSslProtocolSuites; + } + + public boolean pinGlobalTxToPhysicalConnection() { + return pinGlobalTxToPhysicalConnection; + } + + public String socketFactory() { + return socketFactory; + } + + public int connectTimeout() { + return connectTimeout; + } + + public int connectTimeout(int connectTimeout) { + return connectTimeout; + } + + public String pipe() { + return pipe; + } + + public String localSocket() { + return localSocket; + } + + public boolean tcpKeepAlive() { + return tcpKeepAlive; + } + + public boolean tcpAbortiveClose() { + return tcpAbortiveClose; + } + + public String localSocketAddress() { + return localSocketAddress; + } + + public int socketTimeout() { + return socketTimeout; + } + + public boolean allowMultiQueries() { + return allowMultiQueries; + } + + public boolean rewriteBatchedStatements() { + return rewriteBatchedStatements; + } + + public boolean useCompression() { + return useCompression; + } + + public boolean blankTableNameMeta() { + return blankTableNameMeta; + } + + public SslMode sslMode() { + return sslMode; + } + + public String enabledSslCipherSuites() { + return enabledSslCipherSuites; + } + + public String sessionVariables() { + return sessionVariables; + } + + public boolean tinyInt1isBit() { + return tinyInt1isBit; + } + + public boolean yearIsDateType() { + return yearIsDateType; + } + + public String timezone() { + return timezone; + } + + public boolean dumpQueriesOnException() { + return dumpQueriesOnException; + } + + public int prepStmtCacheSize() { + return prepStmtCacheSize; + } + + public boolean useAffectedRows() { + return useAffectedRows; + } + + public boolean useServerPrepStmts() { + return useServerPrepStmts; + } + + public String connectionAttributes() { + return connectionAttributes; + } + + public boolean useBulkStmts() { + return useBulkStmts; + } + + public boolean autocommit() { + return autocommit; + } + + public boolean includeInnodbStatusInDeadlockExceptions() { + return includeInnodbStatusInDeadlockExceptions; + } + + public boolean includeThreadDumpInDeadlockExceptions() { + return includeThreadDumpInDeadlockExceptions; + } + + public String servicePrincipalName() { + return servicePrincipalName; + } + + public int defaultFetchSize() { + return defaultFetchSize; + } + + public Properties nonMappedOptions() { + return nonMappedOptions; + } + + public String tlsSocketType() { + return tlsSocketType; + } + + public int maxQuerySizeToLog() { + return maxQuerySizeToLog; + } + + public Integer maxAllowedPacket() { + return maxAllowedPacket; + } + + public boolean assureReadOnly() { + return assureReadOnly; + } + + public int retriesAllDown() { + return retriesAllDown; + } + + public int validConnectionTimeout() { + return validConnectionTimeout; + } + + public int loadBalanceBlacklistTimeout() { + return loadBalanceBlacklistTimeout; + } + + public int failoverLoopRetries() { + return failoverLoopRetries; + } + + public String galeraAllowedState() { + return galeraAllowedState; + } + + public boolean pool() { + return pool; + } + + public String poolName() { + return poolName; + } + + public int maxPoolSize() { + return maxPoolSize; + } + + public Integer minPoolSize() { + return minPoolSize; + } + + public int maxIdleTime() { + return maxIdleTime; + } + + public boolean staticGlobal() { + return staticGlobal; + } + + public boolean registerJmxPool() { + return registerJmxPool; + } + + public int poolValidMinDelay() { + return poolValidMinDelay; + } + + public boolean useResetConnection() { + return useResetConnection; + } + + public String serverRsaPublicKeyFile() { + return serverRsaPublicKeyFile; + } + + public boolean allowPublicKeyRetrieval() { + return allowPublicKeyRetrieval; + } + + public boolean useReadAheadInput() { + return useReadAheadInput; + } + + public boolean cachePrepStmts() { + return cachePrepStmts; + } + + /** + * ToString implementation. + * + * @return String value + */ + public String toString() { + return initialUrl; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Configuration that = (Configuration) o; + + return initialUrl.equals(that.initialUrl); + } + + @Override + public int hashCode() { + return initialUrl.hashCode(); + } + + /** A builder for {@link Configuration} instances. */ + public static final class Builder implements Cloneable { + + private Properties _nonMappedOptions; + private HaMode _haMode; + private List _addresses = new ArrayList<>(); + + // standard options + private String user; + private String password; + private String database; + // divers + private String socketFactory; + private Integer connectTimeout; + private String pipe; + private String localSocket; + private Boolean tcpKeepAlive; + private Boolean tcpAbortiveClose; + private String localSocketAddress; + private Integer socketTimeout; + private Boolean allowMultiQueries; + private Boolean rewriteBatchedStatements; + private Boolean useCompression; + private Boolean blankTableNameMeta; + private String credentialType; + private SslMode sslMode; + private String enabledSslCipherSuites; + private String sessionVariables; + private Boolean tinyInt1isBit; + private Boolean yearIsDateType; + private String timezone; + private Boolean dumpQueriesOnException; + private Integer prepStmtCacheSize; + private Boolean useAffectedRows; + private Boolean useServerPrepStmts; + private String connectionAttributes; + private Boolean useBulkStmts; + private Boolean autocommit; + private Boolean includeInnodbStatusInDeadlockExceptions; + private Boolean includeThreadDumpInDeadlockExceptions; + private String servicePrincipalName; + private Integer defaultFetchSize; + private String tlsSocketType; + private Integer maxQuerySizeToLog; + private Integer maxAllowedPacket; + + // HA options + private Integer retriesAllDown; + private Boolean assureReadOnly; + private Integer validConnectionTimeout; + private Integer loadBalanceBlacklistTimeout; + private Integer failoverLoopRetries; + private String galeraAllowedState; + private String enabledSslProtocolSuites; + private Boolean pinGlobalTxToPhysicalConnection; + + // Pool options + private Boolean pool; + private String poolName; + private Integer maxPoolSize; + private Integer minPoolSize; + private Integer maxIdleTime; + private Boolean staticGlobal; + private Boolean registerJmxPool; + private Integer poolValidMinDelay; + private Boolean useResetConnection; + private Boolean useReadAheadInput; + private Boolean cachePrepStmts; + private String serverSslCert; + + // MySQL sha authentication + private String serverRsaPublicKeyFile; + private Boolean allowPublicKeyRetrieval; + + public Builder user(String user) { + this.user = user; + return this; + } + + public Builder serverSslCert(String serverSslCert) { + this.serverSslCert = serverSslCert; + return this; + } + + public Builder password(String password) { + this.password = password; + return this; + } + + public Builder enabledSslProtocolSuites(String enabledSslProtocolSuites) { + this.enabledSslProtocolSuites = enabledSslProtocolSuites; + return this; + } + + public Builder pinGlobalTxToPhysicalConnection(Boolean pinGlobalTxToPhysicalConnection) { + this.pinGlobalTxToPhysicalConnection = pinGlobalTxToPhysicalConnection; + return this; + } + + public Builder database(String database) { + this.database = database; + return this; + } + + public Builder haMode(HaMode haMode) { + this._haMode = haMode; + return this; + } + + public Builder addresses(String host, int port) { + this._addresses = new ArrayList<>(); + this._addresses.add(HostAddress.from(host, port)); + return this; + } + + public Builder addresses(String host, int port, boolean master) { + this._addresses = new ArrayList<>(); + this._addresses.add(HostAddress.from(host, port, master)); + return this; + } + + public Builder addresses(HostAddress... hostAddress) { + this._addresses = new ArrayList<>(); + for (HostAddress address : hostAddress) { + this._addresses.add(address); + } + return this; + } + + public Builder socketFactory(String socketFactory) { + this.socketFactory = socketFactory; + return this; + } + + /** + * Indicate connect timeout value, in milliseconds, or zero for no timeout. Default: 30000 + * + * @param connectTimeout connect Timeout + * @return this {@link Builder} + */ + public Builder connectTimeout(Integer connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + /** + * Indicate to use windows named pipe, specify named pipe name to connect + * + * @param pipe windows named pipe + * @return this {@link Builder} + */ + public Builder pipe(String pipe) { + this.pipe = pipe; + return this; + } + + /** + * Indicate to use Unix domain socket, if the server allows it, specifying named pipe name to + * connect The value is the path of Unix domain socket (available with "select @@socket" + * command). + * + * @param localSocket local socket path + * @return this {@link Builder} + */ + public Builder localSocket(String localSocket) { + this.localSocket = localSocket; + return this; + } + + /** + * Indicate if TCP keep-alive must be enable. + * + * @param tcpKeepAlive value + * @return this {@link Builder} + */ + public Builder tcpKeepAlive(Boolean tcpKeepAlive) { + this.tcpKeepAlive = tcpKeepAlive; + return this; + } + + /** + * Indicate that when connection fails, to send an RST TCP packet. + * + * @param tcpAbortiveClose value + * @return this {@link Builder} + */ + public Builder tcpAbortiveClose(Boolean tcpAbortiveClose) { + this.tcpAbortiveClose = tcpAbortiveClose; + return this; + } + + /** + * Indicate Hostname or IP address to bind the connection socket to a local (UNIX domain) + * socket. + * + * @param localSocketAddress Hostname or IP address + * @return this {@link Builder} + */ + public Builder localSocketAddress(String localSocketAddress) { + this.localSocketAddress = localSocketAddress; + return this; + } + + /** + * Indicate the network socket timeout (SO_TIMEOUT) in milliseconds. Value of 0 disables this + * timeout. + * + *

If the goal is to set a timeout for all queries, the server has permitted a solution to + * limit the query time by setting a system variable, max_statement_time. Default: 0 + * + * @param socketTimeout socket timeout value + * @return this {@link Builder} + */ + public Builder socketTimeout(Integer socketTimeout) { + this.socketTimeout = socketTimeout; + return this; + } + + /** + * Indicate that multi-queries are allowed. example: "insert into ab (i) values (1); insert into + * ab (i) values (2)". + * + *

If application build sql command string, this is probably a bad idea to enable this + * option, opening the door to sql injection. default: false. + * + * @param allowMultiQueries indicate if active + * @return this {@link Builder} + */ + public Builder allowMultiQueries(Boolean allowMultiQueries) { + this.allowMultiQueries = allowMultiQueries; + return this; + } + + /** + * For insert queries, rewrite batchedStatement to execute in a single executeQuery. + * + *

example: "insert into ab (i) values (?)" with first batch values = 1, second = 2 will be + * rewritten "insert into ab (i) values (1), (2)". + * + *

If query cannot be rewriten in "multi-values", rewrite will use multi-queries : "INSERT + * INTO TABLE(col1) VALUES (?) ON DUPLICATE KEY UPDATE col2=?" with values [1,2] and [2,3] will + * be rewritten "INSERT INTO TABLE(col1) VALUES (1) ON DUPLICATE KEY UPDATE col2=2;INSERT INTO + * TABLE(col1) VALUES (3) ON DUPLICATE KEY UPDATE col2=4" + * + *

when active, the useServerPrepStmts option is set to false + * + * @param rewriteBatchedStatements to enable/disable rewrite + * @return this {@link Builder} + */ + public Builder rewriteBatchedStatements(Boolean rewriteBatchedStatements) { + this.rewriteBatchedStatements = rewriteBatchedStatements; + return this; + } + + /** + * Indicate to compresses exchanges with the database through gzip. This permits better + * performance when the database is not in the same location. + * + * @param useCompression to enable/disable compression + * @return this {@link Builder} + */ + public Builder useCompression(Boolean useCompression) { + this.useCompression = useCompression; + return this; + } + + public Builder blankTableNameMeta(Boolean blankTableNameMeta) { + this.blankTableNameMeta = blankTableNameMeta; + return this; + } + + public Builder credentialType(String credentialType) { + this.credentialType = credentialType; + return this; + } + + public Builder sslMode(SslMode sslMode) { + this.sslMode = sslMode; + return this; + } + + public Builder enabledSslCipherSuites(String enabledSslCipherSuites) { + this.enabledSslCipherSuites = enabledSslCipherSuites; + return this; + } + + public Builder sessionVariables(String sessionVariables) { + this.sessionVariables = sessionVariables; + return this; + } + + public Builder tinyInt1isBit(Boolean tinyInt1isBit) { + this.tinyInt1isBit = tinyInt1isBit; + return this; + } + + public Builder yearIsDateType(Boolean yearIsDateType) { + this.yearIsDateType = yearIsDateType; + return this; + } + + public Builder timezone(String timezone) { + this.timezone = timezone; + return this; + } + + public Builder dumpQueriesOnException(Boolean dumpQueriesOnException) { + this.dumpQueriesOnException = dumpQueriesOnException; + return this; + } + + public Builder prepStmtCacheSize(Integer prepStmtCacheSize) { + this.prepStmtCacheSize = prepStmtCacheSize; + return this; + } + + public Builder useAffectedRows(Boolean useAffectedRows) { + this.useAffectedRows = useAffectedRows; + return this; + } + + public Builder useServerPrepStmts(Boolean useServerPrepStmts) { + this.useServerPrepStmts = useServerPrepStmts; + return this; + } + + public Builder connectionAttributes(String connectionAttributes) { + this.connectionAttributes = connectionAttributes; + return this; + } + + public Builder useBulkStmts(Boolean useBulkStmts) { + this.useBulkStmts = useBulkStmts; + return this; + } + + public Builder autocommit(Boolean autocommit) { + this.autocommit = autocommit; + return this; + } + + public Builder includeInnodbStatusInDeadlockExceptions( + Boolean includeInnodbStatusInDeadlockExceptions) { + this.includeInnodbStatusInDeadlockExceptions = includeInnodbStatusInDeadlockExceptions; + return this; + } + + public Builder includeThreadDumpInDeadlockExceptions( + Boolean includeThreadDumpInDeadlockExceptions) { + this.includeThreadDumpInDeadlockExceptions = includeThreadDumpInDeadlockExceptions; + return this; + } + + public Builder servicePrincipalName(String servicePrincipalName) { + this.servicePrincipalName = servicePrincipalName; + return this; + } + + public Builder defaultFetchSize(Integer defaultFetchSize) { + this.defaultFetchSize = defaultFetchSize; + return this; + } + + public Builder tlsSocketType(String tlsSocketType) { + this.tlsSocketType = tlsSocketType; + return this; + } + + public Builder maxQuerySizeToLog(Integer maxQuerySizeToLog) { + this.maxQuerySizeToLog = maxQuerySizeToLog; + return this; + } + + public Builder maxAllowedPacket(Integer maxAllowedPacket) { + this.maxAllowedPacket = maxAllowedPacket; + return this; + } + + public Builder retriesAllDown(Integer retriesAllDown) { + this.retriesAllDown = retriesAllDown; + return this; + } + + public Builder validConnectionTimeout(Integer validConnectionTimeout) { + this.validConnectionTimeout = validConnectionTimeout; + return this; + } + + public Builder loadBalanceBlacklistTimeout(Integer loadBalanceBlacklistTimeout) { + this.loadBalanceBlacklistTimeout = loadBalanceBlacklistTimeout; + return this; + } + + public Builder failoverLoopRetries(Integer failoverLoopRetries) { + this.failoverLoopRetries = failoverLoopRetries; + return this; + } + + public Builder galeraAllowedState(String galeraAllowedState) { + this.galeraAllowedState = galeraAllowedState; + return this; + } + + public Builder pool(Boolean pool) { + this.pool = pool; + return this; + } + + public Builder poolName(String poolName) { + this.poolName = poolName; + return this; + } + + public Builder maxPoolSize(Integer maxPoolSize) { + this.maxPoolSize = maxPoolSize; + return this; + } + + public Builder minPoolSize(Integer minPoolSize) { + this.minPoolSize = minPoolSize; + return this; + } + + public Builder maxIdleTime(Integer maxIdleTime) { + this.maxIdleTime = maxIdleTime; + return this; + } + + public Builder staticGlobal(Boolean staticGlobal) { + this.staticGlobal = staticGlobal; + return this; + } + + public Builder registerJmxPool(Boolean registerJmxPool) { + this.registerJmxPool = registerJmxPool; + return this; + } + + public Builder poolValidMinDelay(Integer poolValidMinDelay) { + this.poolValidMinDelay = poolValidMinDelay; + return this; + } + + public Builder useResetConnection(Boolean useResetConnection) { + this.useResetConnection = useResetConnection; + return this; + } + + public Builder serverRsaPublicKeyFile(String serverRsaPublicKeyFile) { + this.serverRsaPublicKeyFile = serverRsaPublicKeyFile; + return this; + } + + public Builder allowPublicKeyRetrieval(Boolean allowPublicKeyRetrieval) { + this.allowPublicKeyRetrieval = allowPublicKeyRetrieval; + return this; + } + + public Builder useReadAheadInput(Boolean useReadAheadInput) { + this.useReadAheadInput = useReadAheadInput; + return this; + } + + public Builder cachePrepStmts(Boolean cachePrepStmts) { + this.cachePrepStmts = cachePrepStmts; + return this; + } + + public Builder assureReadOnly(Boolean assureReadOnly) { + this.assureReadOnly = assureReadOnly; + return this; + } + + public Configuration build() throws SQLException { + Configuration conf = + new Configuration( + this.database, + this._addresses, + this._haMode, + this.user, + this.password, + this.enabledSslProtocolSuites, + this.pinGlobalTxToPhysicalConnection, + this.socketFactory, + this.connectTimeout, + this.pipe, + this.localSocket, + this.tcpKeepAlive, + this.tcpAbortiveClose, + this.localSocketAddress, + this.socketTimeout, + this.allowMultiQueries, + this.rewriteBatchedStatements, + this.useCompression, + this.blankTableNameMeta, + this.credentialType, + this.sslMode, + this.enabledSslCipherSuites, + this.sessionVariables, + this.tinyInt1isBit, + this.yearIsDateType, + this.timezone, + this.dumpQueriesOnException, + this.prepStmtCacheSize, + this.useAffectedRows, + this.useServerPrepStmts, + this.connectionAttributes, + this.useBulkStmts, + this.autocommit, + this.includeInnodbStatusInDeadlockExceptions, + this.includeThreadDumpInDeadlockExceptions, + this.servicePrincipalName, + this.defaultFetchSize, + this.tlsSocketType, + this.maxQuerySizeToLog, + this.maxAllowedPacket, + this.assureReadOnly, + this.retriesAllDown, + this.validConnectionTimeout, + this.loadBalanceBlacklistTimeout, + this.failoverLoopRetries, + this.galeraAllowedState, + this.pool, + this.poolName, + this.maxPoolSize, + this.minPoolSize, + this.maxIdleTime, + this.staticGlobal, + this.registerJmxPool, + this.poolValidMinDelay, + this.useResetConnection, + this.serverRsaPublicKeyFile, + this.allowPublicKeyRetrieval, + this.serverSslCert, + this.useReadAheadInput, + this.cachePrepStmts, + this._nonMappedOptions); + conf.initialUrl = this.buildUrl(conf); + return conf; + } + + private String buildUrl(Configuration conf) { + StringBuilder sb = new StringBuilder(); + sb.append("jdbc:mariadb:"); + if (_haMode != HaMode.NONE) { + sb.append(_haMode.toString().toLowerCase(Locale.ROOT)).append(":"); + } + sb.append("//"); + + for (int i = 0; i < _addresses.size(); i++) { + HostAddress hostAddress = _addresses.get(i); + if (i > 0) { + sb.append(","); + } + sb.append("address=(host=") + .append(hostAddress.host) + .append(")") + .append("(port=") + .append(hostAddress.port) + .append(")"); + sb.append("(type=").append(hostAddress.primary ? "primary" : "replica").append(")"); + } + + sb.append("/"); + if (database != null) { + sb.append(database); + } + + try { + // Option object is already initialized to default values. + // loop on properties, + // - check DefaultOption to check that property value correspond to type (and range) + // - set values + boolean first = true; + + Field[] fields = Builder.class.getDeclaredFields(); + for (Field field : fields) { + if ("database".equals(field.getName()) + || "_haMode".equals(field.getName()) + || "$jacocoData".equals(field.getName()) + || "_addresses".equals(field.getName())) { + continue; + } + Object obj = field.get(this); + if (obj != null + && (!(obj instanceof Properties) + || (obj instanceof Properties && ((Properties) obj).size() > 0))) { + + if (first) { + first = false; + sb.append('?'); + } else { + sb.append('&'); + } + + if (field.getType().equals(String.class)) { + sb.append(field.getName()).append('='); + sb.append((String) field.get(this)); + } else if (field.getType().equals(Boolean.class)) { + if (!((Boolean) obj).booleanValue()) { + sb.append(field.getName()).append('='); + sb.append(((Boolean) obj).toString()); + } + } else if (field.getType().equals(Integer.class) + || field.getType().equals(Long.class)) { + try { + Field confField = Configuration.class.getField(field.getName()); + if (!confField.get(conf).equals(obj)) { + sb.append(field.getName()).append('=').append(obj.toString()); + } + } catch (NoSuchFieldException | IllegalAccessException n) { + // eat + } + } else if (field.getType().equals(Properties.class)) { + boolean firstProp = true; + Properties properties = (Properties) obj; + for (Object key : properties.keySet()) { + if (firstProp) { + firstProp = false; + } else { + sb.append('&'); + } + sb.append(key).append('='); + sb.append(properties.get(key)); + } + } + } + } + + } catch (IllegalAccessException n) { + n.printStackTrace(); + } catch (SecurityException s) { + // only for jws, so never thrown + throw new IllegalArgumentException("Security too restrictive : " + s.getMessage()); + } + + return sb.toString(); + } + } +} diff --git a/src/main/java/org/mariadb/jdbc/Connection.java b/src/main/java/org/mariadb/jdbc/Connection.java new file mode 100644 index 000000000..cfc0abe92 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/Connection.java @@ -0,0 +1,840 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +import java.sql.*; +import java.util.*; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.sql.*; +import org.mariadb.jdbc.client.Client; +import org.mariadb.jdbc.client.ClientImpl; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.message.client.ChangeDbPacket; +import org.mariadb.jdbc.message.client.PingPacket; +import org.mariadb.jdbc.message.client.QueryPacket; +import org.mariadb.jdbc.util.NativeSql; +import org.mariadb.jdbc.util.constants.Capabilities; +import org.mariadb.jdbc.util.constants.ConnectionState; +import org.mariadb.jdbc.util.constants.ServerStatus; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; +import org.mariadb.jdbc.util.log.Logger; +import org.mariadb.jdbc.util.log.Loggers; + +public class Connection implements java.sql.Connection, PooledConnection { + + private static final Logger logger = Loggers.getLogger(Connection.class); + private static final Pattern CALLABLE_STATEMENT_PATTERN = + Pattern.compile( + "^(\\s*\\{)?\\s*((\\?\\s*=)?(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*" + + "call(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*((((`[^`]+`)|([^`\\}]+))\\.)?" + + "((`[^`]+`)|([^`\\}\\(]+)))\\s*(\\(.*\\))?(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*" + + "\\s*(#.*)?)\\s*(\\}\\s*)?$", + Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + private final ReentrantLock lock; + private final List connectionEventListeners = new ArrayList<>(); + private final List statementEventListeners = new ArrayList<>(); + private final Configuration conf; + private final ExceptionFactory exceptionFactory; + private final Client client; + private final Properties clientInfo = new Properties(); + private int lowercaseTableNames = -1; + private AtomicInteger savepointId = new AtomicInteger(); + private boolean readOnly; + private boolean canUseServerTimeout; + private boolean canUseServerMaxRows; + private final int defaultFetchSize; + + public Connection(Configuration conf, ReentrantLock lock, Client client) throws SQLException { + this.conf = conf; + this.lock = lock; + this.exceptionFactory = client.getExceptionFactory().setConnection(this); + this.client = client; + Context context = this.client.getContext(); + this.canUseServerTimeout = context.getVersion().versionGreaterOrEqual(10, 1, 2); + this.canUseServerMaxRows = context.getVersion().versionGreaterOrEqual(10, 3, 0); + this.defaultFetchSize = context.getConf().defaultFetchSize(); + } + + /** + * Cancels the current query - clones the current protocol and executes a query using the new + * connection. + * + * @throws SQLException never thrown + */ + public void cancelCurrentQuery() throws SQLException { + try (Client cli = + new ClientImpl(conf, client.getHostAddress(), false, new ReentrantLock(), true)) { + cli.execute(new QueryPacket("KILL QUERY " + client.getContext().getThreadId())); + } + } + + @Override + public Statement createStatement() { + return new Statement( + this, + lock, + canUseServerTimeout, + canUseServerMaxRows, + Statement.NO_GENERATED_KEYS, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + defaultFetchSize); + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + return prepareInternal( + sql, + Statement.NO_GENERATED_KEYS, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + conf.useServerPrepStmts()); + } + + public PreparedStatement prepareInternal( + String sql, + int autoGeneratedKeys, + int resultSetType, + int resultSetConcurrency, + boolean useBinary) + throws SQLException { + checkNotClosed(); + if (useBinary) { + return new ServerPreparedStatement( + NativeSql.parse(sql, client.getContext()), + this, + lock, + canUseServerTimeout, + canUseServerMaxRows, + autoGeneratedKeys, + resultSetType, + resultSetConcurrency, + defaultFetchSize); + } + return new ClientPreparedStatement( + NativeSql.parse(sql, client.getContext()), + this, + lock, + canUseServerTimeout, + canUseServerMaxRows, + autoGeneratedKeys, + resultSetType, + resultSetConcurrency, + defaultFetchSize); + } + + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + return prepareCall(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + } + + @Override + public String nativeSQL(String sql) throws SQLException { + return NativeSql.parse(sql, client.getContext()); + } + + @Override + public boolean getAutoCommit() throws SQLException { + return (client.getContext().getServerStatus() & ServerStatus.AUTOCOMMIT) > 0; + } + + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + if (autoCommit == getAutoCommit()) { + return; + } + lock.lock(); + try { + getContext().addStateFlag(ConnectionState.STATE_AUTOCOMMIT); + client.execute(new QueryPacket("set autocommit=" + ((autoCommit) ? "1" : "0"))); + } finally { + lock.unlock(); + } + } + + @Override + public void commit() throws SQLException { + lock.lock(); + try { + if ((client.getContext().getServerStatus() & ServerStatus.IN_TRANSACTION) > 0) { + client.execute(new QueryPacket("COMMIT")); + } + } finally { + lock.unlock(); + } + } + + @Override + public void rollback() throws SQLException { + lock.lock(); + try { + if ((client.getContext().getServerStatus() & ServerStatus.IN_TRANSACTION) > 0) { + client.execute(new QueryPacket("ROLLBACK")); + } + } finally { + lock.unlock(); + } + } + + @Override + public void close() throws SQLException { + fireConnectionClosed(new ConnectionEvent(this)); + client.close(); + } + + @Override + public boolean isClosed() { + return client.isClosed(); + } + + public Context getContext() { + return client.getContext(); + } + + /** + * Are table case sensitive or not . Default Value: 0 (Unix), 1 (Windows), 2 (Mac OS X). If set to + * 0 (the default on Unix-based systems), table names and aliases and database names are compared + * in a case-sensitive manner. If set to 1 (the default on Windows), names are stored in lowercase + * and not compared in a case-sensitive manner. If set to 2 (the default on Mac OS X), names are + * stored as declared, but compared in lowercase. + * + * @return int value. + * @throws SQLException if a connection error occur + */ + public int getLowercaseTableNames() throws SQLException { + if (lowercaseTableNames == -1) { + try (java.sql.Statement st = createStatement()) { + try (ResultSet rs = st.executeQuery("select @@lower_case_table_names")) { + rs.next(); + lowercaseTableNames = rs.getInt(1); + } + } + } + return lowercaseTableNames; + } + + @Override + public DatabaseMetaData getMetaData() { + return new DatabaseMetaData(this, this.conf); + } + + @Override + public boolean isReadOnly() { + return this.readOnly; + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + + lock.lock(); + try { + if (conf.assureReadOnly() + && this.readOnly != readOnly + && client.getContext().getVersion().versionGreaterOrEqual(5, 6, 5)) { + client.setReadOnly(readOnly); + } + this.readOnly = readOnly; + getContext().addStateFlag(ConnectionState.STATE_READ_ONLY); + } finally { + lock.unlock(); + } + } + + @Override + public String getCatalog() throws SQLException { + + if ((client.getContext().getServerCapabilities() & Capabilities.CLIENT_SESSION_TRACK) != 0) { + // client session track return empty value, not null value. Java require sending null if empty + String db = client.getContext().getDatabase(); + return (db != null && db.isEmpty()) ? null : db; + } + + Statement stmt = createStatement(); + ResultSet rs = stmt.executeQuery("select database()"); + if (rs.next()) { + client.getContext().setDatabase(rs.getString(1)); + return client.getContext().getDatabase(); + } + return null; + } + + @Override + public void setCatalog(String catalog) throws SQLException { + if ((client.getContext().getServerCapabilities() & Capabilities.CLIENT_SESSION_TRACK) != 0 + && catalog == client.getContext().getDatabase()) { + return; + } + lock.lock(); + try { + getContext().addStateFlag(ConnectionState.STATE_DATABASE); + client.execute(new ChangeDbPacket(catalog)); + } finally { + lock.unlock(); + } + } + + @Override + public int getTransactionIsolation() throws SQLException { + + String sql = "SELECT @@tx_isolation"; + + if (!client.getContext().getVersion().isMariaDBServer()) { + if ((client.getContext().getVersion().getMajorVersion() >= 8 + && client.getContext().getVersion().versionGreaterOrEqual(8, 0, 3)) + || (client.getContext().getVersion().getMajorVersion() < 8 + && client.getContext().getVersion().versionGreaterOrEqual(5, 7, 20))) { + sql = "SELECT @@transaction_isolation"; + } + } + + ResultSet rs = createStatement().executeQuery(sql); + if (rs.next()) { + final String response = rs.getString(1); + switch (response) { + case "REPEATABLE-READ": + return java.sql.Connection.TRANSACTION_REPEATABLE_READ; + + case "READ-UNCOMMITTED": + return java.sql.Connection.TRANSACTION_READ_UNCOMMITTED; + + case "READ-COMMITTED": + return java.sql.Connection.TRANSACTION_READ_COMMITTED; + + case "SERIALIZABLE": + return java.sql.Connection.TRANSACTION_SERIALIZABLE; + + default: + throw exceptionFactory.create( + String.format( + "Could not get transaction isolation level: Invalid value \"%s\"", response)); + } + } + throw exceptionFactory.create("Failed to retrieve transaction isolation"); + } + + @Override + public void setTransactionIsolation(int level) throws SQLException { + String query = "SET SESSION TRANSACTION ISOLATION LEVEL"; + switch (level) { + case java.sql.Connection.TRANSACTION_READ_UNCOMMITTED: + query += " READ UNCOMMITTED"; + break; + case java.sql.Connection.TRANSACTION_READ_COMMITTED: + query += " READ COMMITTED"; + break; + case java.sql.Connection.TRANSACTION_REPEATABLE_READ: + query += " REPEATABLE READ"; + break; + case java.sql.Connection.TRANSACTION_SERIALIZABLE: + query += " SERIALIZABLE"; + break; + default: + throw new SQLException("Unsupported transaction isolation level"); + } + lock.lock(); + try { + getContext().addStateFlag(ConnectionState.STATE_TRANSACTION_ISOLATION); + client.getContext().setTransactionIsolationLevel(level); + client.execute(new QueryPacket(query)); + } finally { + lock.unlock(); + } + } + + @Override + public SQLWarning getWarnings() throws SQLException { + checkNotClosed(); + if (client.getContext().getWarning() == 0) { + return null; + } + + SQLWarning last = null; + SQLWarning first = null; + + try (Statement st = this.createStatement()) { + try (ResultSet rs = st.executeQuery("show warnings")) { + // returned result set has 'level', 'code' and 'message' columns, in this order. + while (rs.next()) { + int code = rs.getInt(2); + String message = rs.getString(3); + SQLWarning warning = new SQLWarning(message, null, code); + if (first == null) { + first = warning; + last = warning; + } else { + last.setNextWarning(warning); + last = warning; + } + } + } + } + return first; + } + + @Override + public void clearWarnings() throws SQLException { + client.getContext().setWarning(0); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) + throws SQLException { + checkNotClosed(); + return new Statement( + this, + lock, + canUseServerTimeout, + canUseServerMaxRows, + Statement.NO_GENERATED_KEYS, + resultSetType, + resultSetConcurrency, + defaultFetchSize); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException { + return prepareInternal( + sql, + Statement.NO_GENERATED_KEYS, + resultSetType, + resultSetConcurrency, + conf.useServerPrepStmts()); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException { + checkNotClosed(); + Matcher matcher = CALLABLE_STATEMENT_PATTERN.matcher(sql); + if (!matcher.matches()) { + throw new SQLSyntaxErrorException( + "invalid callable syntax. must be like {[?=]call [(?,?, ...)]}\n but was : " + + sql); + } + + String query = NativeSql.parse(matcher.group(2), client.getContext()); + + boolean isFunction = (matcher.group(3) != null); + String databaseAndProcedure = matcher.group(8); + String database = matcher.group(10); + String procedureName = matcher.group(13); + String arguments = matcher.group(16); + if (database == null) { + database = getCatalog(); + } + + if (isFunction) { + return new FunctionStatement( + this, + database, + databaseAndProcedure, + (arguments == null) ? "()" : arguments, + lock, + canUseServerTimeout, + canUseServerMaxRows, + resultSetType, + resultSetConcurrency); + } else { + return new ProcedureStatement( + this, + query, + database, + procedureName, + lock, + canUseServerTimeout, + canUseServerMaxRows, + resultSetType, + resultSetConcurrency); + } + } + + @Override + public Map> getTypeMap() throws SQLException { + return new HashMap<>(); + } + + @Override + public void setTypeMap(Map> map) throws SQLException { + throw exceptionFactory.notSupported("TypeMap are not supported"); + } + + @Override + public int getHoldability() throws SQLException { + return ResultSet.HOLD_CURSORS_OVER_COMMIT; + } + + @Override + public void setHoldability(int holdability) throws SQLException { + // not supported + } + + @Override + public Savepoint setSavepoint() throws SQLException { + Savepoint savepoint = new Savepoint(savepointId.incrementAndGet()); + client.execute(new QueryPacket("SAVEPOINT `" + savepoint.rawValue() + "`")); + return savepoint; + } + + @Override + public Savepoint setSavepoint(String name) throws SQLException { + Savepoint savepoint = new Savepoint(name.replace("`", "``")); + client.execute(new QueryPacket("SAVEPOINT `" + savepoint.rawValue() + "`")); + return savepoint; + } + + @Override + public void rollback(java.sql.Savepoint savepoint) throws SQLException { + checkNotClosed(); + lock.lock(); + try { + if ((client.getContext().getServerStatus() & ServerStatus.IN_TRANSACTION) > 0) { + if (savepoint instanceof Connection.Savepoint) { + client.execute( + new QueryPacket( + "ROLLBACK TO SAVEPOINT `" + ((Connection.Savepoint) savepoint).rawValue() + "`")); + } else { + throw exceptionFactory.create("Unknown savepoint type"); + } + } + } finally { + lock.unlock(); + } + } + + @Override + public void releaseSavepoint(java.sql.Savepoint savepoint) throws SQLException { + checkNotClosed(); + lock.lock(); + try { + if ((client.getContext().getServerStatus() & ServerStatus.IN_TRANSACTION) > 0) { + if (savepoint instanceof Connection.Savepoint) { + client.execute( + new QueryPacket( + "RELEASE SAVEPOINT `" + ((Connection.Savepoint) savepoint).rawValue() + "`")); + } else { + throw exceptionFactory.create("Unknown savepoint type"); + } + } + } finally { + lock.unlock(); + } + } + + @Override + public Statement createStatement( + int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + checkNotClosed(); + return new Statement( + this, + lock, + canUseServerTimeout, + canUseServerMaxRows, + Statement.NO_GENERATED_KEYS, + resultSetType, + resultSetConcurrency, + defaultFetchSize); + } + + @Override + public PreparedStatement prepareStatement( + String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + return prepareStatement(sql, resultSetType, resultSetConcurrency); + } + + @Override + public CallableStatement prepareCall( + String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + // TODO + return null; + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + return prepareInternal( + sql, + autoGeneratedKeys, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + conf.useServerPrepStmts()); + } + + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + return prepareStatement(sql); + } + + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + return prepareStatement(sql); + } + + @Override + public Clob createClob() { + return new MariaDbClob(); + } + + @Override + public Blob createBlob() { + return new MariaDbBlob(); + } + + @Override + public NClob createNClob() { + return new MariaDbClob(); + } + + @Override + public SQLXML createSQLXML() throws SQLException { + throw exceptionFactory.notSupported("SQLXML type is not supported"); + } + + private void checkNotClosed() throws SQLException { + if (client.isClosed()) { + throw exceptionFactory.create("Connection is closed", "08000", 1220); + } + } + + @Override + public boolean isValid(int timeout) throws SQLException { + if (timeout < 0) { + throw exceptionFactory.create("the value supplied for timeout is negative"); + } + lock.lock(); + try { + client.execute(PingPacket.INSTANCE); + return true; + } catch (SQLException sqle) { + return false; + } finally { + lock.unlock(); + } + } + + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException { + clientInfo.put(name, value); + } + + @Override + public String getClientInfo(String name) throws SQLException { + return (String) clientInfo.get(name); + } + + @Override + public Properties getClientInfo() throws SQLException { + return clientInfo; + } + + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException { + for (Map.Entry entry : properties.entrySet()) { + clientInfo.put(entry.getKey(), entry.getValue()); + } + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + throw exceptionFactory.notSupported("Array type is not supported"); + } + + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + throw exceptionFactory.notSupported("Struct type is not supported"); + } + + @Override + public String getSchema() throws SQLException { + // We support only catalog + return null; + } + + @Override + public void setSchema(String schema) throws SQLException { + // We support only catalog, and JDBC indicate "If the driver does not support schemas, it will + // silently ignore this request." + } + + @Override + public void abort(Executor executor) throws SQLException { + client.abort(executor); + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + if (this.isClosed()) { + throw exceptionFactory.create( + "Connection.setNetworkTimeout cannot be called on a closed connection"); + } + if (milliseconds < 0) { + throw exceptionFactory.create( + "Connection.setNetworkTimeout cannot be called with a negative timeout"); + } + SQLPermission sqlPermission = new SQLPermission("setNetworkTimeout"); + SecurityManager securityManager = System.getSecurityManager(); + if (securityManager != null) { + securityManager.checkPermission(sqlPermission); + } + getContext().addStateFlag(ConnectionState.STATE_NETWORK_TIMEOUT); + + lock.lock(); + try { + client.setSocketTimeout(milliseconds); + } finally { + lock.unlock(); + } + } + + @Override + public int getNetworkTimeout() { + return client.getSocketTimeout(); + } + + @Override + public T unwrap(Class iface) throws SQLException { + if (isWrapperFor(iface)) { + return iface.cast(this); + } + throw new SQLException("The receiver is not a wrapper for " + iface.getName()); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return iface.isInstance(this); + } + + @Override + public java.sql.Connection getConnection() throws SQLException { + return this; + } + + @Override + public void addConnectionEventListener(ConnectionEventListener listener) { + connectionEventListeners.add(listener); + } + + @Override + public void removeConnectionEventListener(ConnectionEventListener listener) { + connectionEventListeners.remove(listener); + } + + @Override + public void addStatementEventListener(StatementEventListener listener) { + statementEventListeners.add(listener); + } + + @Override + public void removeStatementEventListener(StatementEventListener listener) { + statementEventListeners.remove(listener); + } + + public void fireStatementClosed(StatementEvent event) { + for (StatementEventListener listener : statementEventListeners) { + listener.statementClosed(event); + } + } + + public void fireStatementErrorOccurred(StatementEvent event) { + for (StatementEventListener listener : statementEventListeners) { + listener.statementErrorOccurred(event); + } + } + + public void fireConnectionClosed(ConnectionEvent event) { + for (ConnectionEventListener listener : connectionEventListeners) { + listener.connectionClosed(event); + } + } + + public void fireConnectionErrorOccurred(ConnectionEvent event) { + for (ConnectionEventListener listener : connectionEventListeners) { + listener.connectionErrorOccurred(event); + } + } + + public boolean isMariaDbServer() { + return client.getContext().getVersion().isMariaDBServer(); + } + + public HostAddress getHostAddress() { + return client.getHostAddress(); + } + + public Client getClient() { + return client; + } + + /** Internal Savepoint implementation */ + class Savepoint implements java.sql.Savepoint { + + private final String name; + private final Integer id; + + public Savepoint(final String name) { + this.name = name; + this.id = null; + } + + public Savepoint(final int savepointId) { + this.id = savepointId; + this.name = null; + } + + /** + * Retrieves the generated ID for the savepoint that this Savepoint object + * represents. + * + * @return the numeric ID of this savepoint + */ + public int getSavepointId() throws SQLException { + if (name != null) { + throw exceptionFactory.create("Cannot retrieve savepoint id of a named savepoint"); + } + return id; + } + + /** + * Retrieves the name of the savepoint that this Savepoint object represents. + * + * @return the name of this savepoint + */ + public String getSavepointName() throws SQLException { + if (id != null) { + throw exceptionFactory.create("Cannot retrieve savepoint name of an unnamed savepoint"); + } + return name; + } + + public String rawValue() { + if (id != null) { + return "_jid_" + id; + } + return name; + } + } +} diff --git a/src/main/java/org/mariadb/jdbc/MariaDbDatabaseMetaData.java b/src/main/java/org/mariadb/jdbc/DatabaseMetaData.java similarity index 92% rename from src/main/java/org/mariadb/jdbc/MariaDbDatabaseMetaData.java rename to src/main/java/org/mariadb/jdbc/DatabaseMetaData.java index 9e1cee0cf..81dcb72a0 100644 --- a/src/main/java/org/mariadb/jdbc/MariaDbDatabaseMetaData.java +++ b/src/main/java/org/mariadb/jdbc/DatabaseMetaData.java @@ -1,5 +1,4 @@ /* - * * MariaDB Client for Java * * Copyright (c) 2012-2014 Monty Program Ab. @@ -18,85 +17,54 @@ * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to Monty Program Ab info@montyprogram.com. * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * */ package org.mariadb.jdbc; import java.sql.*; +import java.sql.Statement; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Locale; -import org.mariadb.jdbc.internal.ColumnType; -import org.mariadb.jdbc.internal.com.read.resultset.ColumnDefinition; -import org.mariadb.jdbc.internal.com.read.resultset.SelectResultSet; -import org.mariadb.jdbc.internal.io.input.StandardPacketInputStream; -import org.mariadb.jdbc.internal.util.Utils; -import org.mariadb.jdbc.internal.util.constant.Version; -import org.mariadb.jdbc.internal.util.dao.Identifier; -import org.mariadb.jdbc.util.Options; - -public class MariaDbDatabaseMetaData implements DatabaseMetaData { +import org.mariadb.jdbc.client.result.CompleteResult; +import org.mariadb.jdbc.client.result.Result; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.util.Version; +import org.mariadb.jdbc.util.constants.ServerStatus; + +public class DatabaseMetaData implements java.sql.DatabaseMetaData { public static final String DRIVER_NAME = "MariaDB Connector/J"; - private final MariaDbConnection connection; - private final UrlParser urlParser; + + private final org.mariadb.jdbc.Connection connection; + private final Configuration conf; + private boolean datePrecisionColumnExist = true; /** * Constructor. * * @param connection connection - * @param urlParser Url parser + * @param conf configuration */ - public MariaDbDatabaseMetaData(Connection connection, UrlParser urlParser) { - this.connection = (MariaDbConnection) connection; - this.urlParser = urlParser; + public DatabaseMetaData(org.mariadb.jdbc.Connection connection, Configuration conf) { + this.connection = connection; + this.conf = conf; } - private static String columnTypeClause(Options options) { + private static String DataTypeClause(Configuration conf) { String upperCaseWithoutSize = " UCASE(IF( COLUMN_TYPE LIKE '%(%)%', CONCAT(SUBSTRING( COLUMN_TYPE,1, LOCATE('('," + "COLUMN_TYPE) - 1 ), SUBSTRING(COLUMN_TYPE ,1+locate(')', COLUMN_TYPE))), " + "COLUMN_TYPE))"; - if (options.tinyInt1isBit) { + if (conf.tinyInt1isBit()) { upperCaseWithoutSize = " IF(COLUMN_TYPE like 'tinyint(1)%', 'BIT', " + upperCaseWithoutSize + ")"; } - if (!options.yearIsDateType) { + if (!conf.yearIsDateType()) { return " IF(COLUMN_TYPE IN ('year(2)', 'year(4)'), 'SMALLINT', " + upperCaseWithoutSize + ")"; } @@ -105,7 +73,7 @@ private static String columnTypeClause(Options options) { // Extract identifier quoted string from input String. // Return new position, or -1 on error - private static int skipWhite(char[] part, int startPos) { + private static int skipWhiteSpace(char[] part, int startPos) { for (int i = startPos; i < part.length; i++) { if (!Character.isWhitespace(part[i])) { return i; @@ -116,7 +84,7 @@ private static int skipWhite(char[] part, int startPos) { private static int parseIdentifier(char[] part, int startPos, Identifier identifier) throws ParseException { - int pos = skipWhite(part, startPos); + int pos = skipWhiteSpace(part, startPos); if (part[pos] != '`') { throw new ParseException(new String(part), pos); } @@ -149,35 +117,8 @@ private static int parseIdentifier(char[] part, int startPos, Identifier identif throw new ParseException(new String(part), startPos); } - private static int parseIdentifierList(char[] part, int startPos, List list) - throws ParseException { - int pos = skipWhite(part, startPos); - if (part[pos] != '(') { - throw new ParseException(new String(part), pos); - } - pos++; - for (; ; ) { - pos = skipWhite(part, pos); - char ch = part[pos]; - switch (ch) { - case ')': - return pos + 1; - case '`': - Identifier id = new Identifier(); - pos = parseIdentifier(part, pos, id); - list.add(id); - break; - case ',': - pos++; - break; - default: - throw new ParseException(new String(part, startPos, part.length - startPos), startPos); - } - } - } - private static int skipKeyword(char[] part, int startPos, String keyword) throws ParseException { - int pos = skipWhite(part, startPos); + int pos = skipWhiteSpace(part, startPos); for (int i = 0; i < keyword.length(); i++, pos++) { if (part[pos] != keyword.charAt(i)) { throw new ParseException(new String(part), pos); @@ -188,29 +129,83 @@ private static int skipKeyword(char[] part, int startPos, String keyword) throws private static int getImportedKeyAction(String actionKey) { if (actionKey == null) { - return DatabaseMetaData.importedKeyRestrict; + return java.sql.DatabaseMetaData.importedKeyRestrict; } switch (actionKey) { case "NO ACTION": - return DatabaseMetaData.importedKeyNoAction; + return java.sql.DatabaseMetaData.importedKeyNoAction; case "CASCADE": - return DatabaseMetaData.importedKeyCascade; + return java.sql.DatabaseMetaData.importedKeyCascade; case "SET NULL": - return DatabaseMetaData.importedKeySetNull; + return java.sql.DatabaseMetaData.importedKeySetNull; case "SET DEFAULT": - return DatabaseMetaData.importedKeySetDefault; + return java.sql.DatabaseMetaData.importedKeySetDefault; case "RESTRICT": - return DatabaseMetaData.importedKeyRestrict; + return java.sql.DatabaseMetaData.importedKeyRestrict; default: throw new IllegalArgumentException("Illegal key action '" + actionKey + "' specified."); } } + private static String quoteIdentifier(String string) { + return "`" + string.replaceAll("`", "``") + "`"; + } + + /** + * Escape String. + * + * @param value value to escape + * @param noBackslashEscapes must backslash be escaped + * @return escaped string. + */ + public static String escapeString(String value, boolean noBackslashEscapes) { + if (!value.contains("'")) { + if (noBackslashEscapes) { + return value; + } + if (!value.contains("\\")) { + return value; + } + } + String escaped = value.replace("'", "''"); + if (noBackslashEscapes) { + return escaped; + } + return escaped.replace("\\", "\\\\"); + } + + private int parseIdentifierList(char[] part, int startPos, List list) + throws ParseException { + int pos = skipWhiteSpace(part, startPos); + if (part[pos] != '(') { + throw new ParseException(new String(part), pos); + } + pos++; + for (; ; ) { + pos = skipWhiteSpace(part, pos); + char ch = part[pos]; + switch (ch) { + case ')': + return pos + 1; + case '`': + Identifier id = new Identifier(); + pos = parseIdentifier(part, pos, id); + list.add(id); + break; + case ',': + pos++; + break; + default: + throw new ParseException(new String(part, startPos, part.length - startPos), startPos); + } + } + } + /** * Get imported keys. * @@ -221,8 +216,8 @@ private static int getImportedKeyAction(String actionKey) { * @return resultset resultset * @throws ParseException exception */ - private static ResultSet getImportedKeys( - String tableDef, String tableName, String catalog, MariaDbConnection connection) + private ResultSet getImportedKeys( + String tableDef, String tableName, String catalog, org.mariadb.jdbc.Connection connection) throws ParseException { String[] columnNames = { "PKTABLE_CAT", "PKTABLE_SCHEM", "PKTABLE_NAME", @@ -231,12 +226,12 @@ private static ResultSet getImportedKeys( "UPDATE_RULE", "DELETE_RULE", "FK_NAME", "PK_NAME", "DEFERRABILITY" }; - ColumnType[] columnTypes = { - ColumnType.VARCHAR, ColumnType.NULL, ColumnType.VARCHAR, - ColumnType.VARCHAR, ColumnType.VARCHAR, ColumnType.NULL, - ColumnType.VARCHAR, ColumnType.VARCHAR, ColumnType.SMALLINT, - ColumnType.SMALLINT, ColumnType.SMALLINT, ColumnType.VARCHAR, - ColumnType.NULL, ColumnType.SMALLINT + DataType[] dataTypes = { + DataType.VARCHAR, DataType.NULL, DataType.VARCHAR, + DataType.VARCHAR, DataType.VARCHAR, DataType.NULL, + DataType.VARCHAR, DataType.VARCHAR, DataType.SMALLINT, + DataType.SMALLINT, DataType.SMALLINT, DataType.VARCHAR, + DataType.VARCHAR, DataType.SMALLINT }; String[] parts = tableDef.split("\n"); @@ -265,8 +260,8 @@ private static ResultSet getImportedKeys( if (primaryKeyCols.size() != foreignKeyCols.size()) { throw new ParseException(tableDef, 0); } - int onUpdateReferenceAction = DatabaseMetaData.importedKeyRestrict; - int onDeleteReferenceAction = DatabaseMetaData.importedKeyRestrict; + int onUpdateReferenceAction = java.sql.DatabaseMetaData.importedKeyRestrict; + int onDeleteReferenceAction = java.sql.DatabaseMetaData.importedKeyRestrict; for (String referenceAction : new String[] {"RESTRICT", "CASCADE", "SET NULL", "NO ACTION"}) { if (part.contains("ON UPDATE " + referenceAction)) { @@ -280,23 +275,23 @@ private static ResultSet getImportedKeys( for (int i = 0; i < primaryKeyCols.size(); i++) { String[] row = new String[columnNames.length]; - row[0] = pkTable.schema; + row[0] = pkTable.schema; // PKTABLE_CAT if (row[0] == null) { row[0] = catalog; } - row[1] = null; - row[2] = pkTable.name; - row[3] = primaryKeyCols.get(i).name; - row[4] = catalog; - row[5] = null; - row[6] = tableName; - row[7] = foreignKeyCols.get(i).name; - row[8] = Integer.toString(i + 1); - row[9] = Integer.toString(onUpdateReferenceAction); - row[10] = Integer.toString(onDeleteReferenceAction); - row[11] = constraintName.name; - row[12] = null; - row[13] = Integer.toString(DatabaseMetaData.importedKeyNotDeferrable); + row[1] = null; // PKTABLE_SCHEM + row[2] = pkTable.name; // PKTABLE_NAME + row[3] = primaryKeyCols.get(i).name; // PKCOLUMN_NAME + row[4] = catalog; // FKTABLE_CAT + row[5] = null; // FKTABLE_SCHEM + row[6] = tableName; // FKTABLE_NAME + row[7] = foreignKeyCols.get(i).name; // FKCOLUMN_NAME + row[8] = Integer.toString(i + 1); // KEY_SEQ + row[9] = Integer.toString(onUpdateReferenceAction); // UPDATE_RULE + row[10] = Integer.toString(onDeleteReferenceAction); // DELETE_RULE + row[11] = constraintName.name; // FK_NAME + row[12] = null; // PK_NAME - unlike using information_schema, cannot know constraint name + row[13] = Integer.toString(DatabaseMetaData.importedKeyNotDeferrable); // DEFERRABILITY data.add(row); } } @@ -318,7 +313,7 @@ private static ResultSet getImportedKeys( } return result; }); - return SelectResultSet.createResultSet(columnNames, columnTypes, arr, connection.getProtocol()); + return CompleteResult.createResultSet(columnNames, dataTypes, arr, connection.getContext()); } /** @@ -390,39 +385,25 @@ private static ResultSet getImportedKeys( public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { - String database = catalog; // We avoid using information schema queries by default, because this appears to be an expensive // query (CONJ-41). if (table == null) { throw new SQLException("'table' parameter in getImportedKeys cannot be null"); } - if (database == null && connection.nullCatalogMeansCurrent) { - /* Treat null catalog as current */ - return getImportedKeysUsingInformationSchema("", table); - } - - if (database == null) { - return getImportedKeysUsingInformationSchema(null, table); - } - - if (database.isEmpty()) { - database = connection.getCatalog(); - if (database == null || database.isEmpty()) { - return getImportedKeysUsingInformationSchema(database, table); - } + if (catalog == null || catalog.isEmpty()) { + return getImportedKeysUsingInformationSchema(catalog, table); } try { - return getImportedKeysUsingShowCreateTable(database, table); + return getImportedKeysUsingShowCreateTable(catalog, table); } catch (Exception e) { // Likely, parsing failed, try out I_S query. - return getImportedKeysUsingInformationSchema(database, table); + return getImportedKeysUsingInformationSchema(catalog, table); } } private String dataTypeClause(String fullTypeColumnName) { - Options options = urlParser.getOptions(); return " CASE data_type" + " WHEN 'bit' THEN " + Types.BIT @@ -489,7 +470,7 @@ private String dataTypeClause(String fullTypeColumnName) { + " WHEN 'timestamp' THEN " + Types.TIMESTAMP + " WHEN 'tinyint' THEN " - + (options.tinyInt1isBit + + (conf.tinyInt1isBit() ? "IF(" + fullTypeColumnName + " like 'tinyint(1)%'," @@ -499,7 +480,7 @@ private String dataTypeClause(String fullTypeColumnName) { + ") " : Types.TINYINT) + " WHEN 'year' THEN " - + (options.yearIsDateType ? Types.DATE : Types.SMALLINT) + + (conf.yearIsDateType() ? Types.DATE : Types.SMALLINT) + " ELSE " + Types.OTHER + " END "; @@ -507,9 +488,9 @@ private String dataTypeClause(String fullTypeColumnName) { private ResultSet executeQuery(String sql) throws SQLException { Statement stmt = connection.createStatement(); - SelectResultSet rs = (SelectResultSet) stmt.executeQuery(sql); + Result rs = (Result) stmt.executeQuery(sql); rs.setStatement(null); // bypass Hibernate statement tracking (CONJ-49) - rs.setForceTableAlias(); + rs.useAliasAsName(); return rs; } @@ -517,7 +498,11 @@ private String escapeQuote(String value) { if (value == null) { return "NULL"; } - return "'" + Utils.escapeString(value, connection.getProtocol().noBackslashEscapes()) + "'"; + return "'" + + escapeString( + value, + (connection.getContext().getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) > 0) + + "'"; } /** @@ -536,14 +521,7 @@ private String escapeQuote(String value) { * @return part of SQL query ,that restricts search for the catalog. */ private String catalogCond(String columnName, String catalog) { - if (catalog == null) { - /* Treat null catalog as current */ - if (connection.nullCatalogMeansCurrent) { - return "(ISNULL(database()) OR (" + columnName + " = database()))"; - } - return "(1 = 1)"; - } - if (catalog.isEmpty()) { + if (catalog == null || catalog.isEmpty()) { return "(ISNULL(database()) OR (" + columnName + " = database()))"; } return "(" + columnName + " = " + escapeQuote(catalog) + ")"; @@ -557,13 +535,7 @@ private String patternCond(String columnName, String tableName) { } String predicate = (tableName.indexOf('%') == -1 && tableName.indexOf('_') == -1) ? "=" : "LIKE"; - return " AND " - + columnName - + " " - + predicate - + " '" - + Utils.escapeString(tableName, true) - + "' "; + return " AND " + columnName + " " + predicate + " '" + escapeString(tableName, true) + "' "; } /** @@ -777,12 +749,12 @@ public ResultSet getTables( public ResultSet getColumns( String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { - Options options = urlParser.getOptions(); + String sql = "SELECT TABLE_SCHEMA TABLE_CAT, NULL TABLE_SCHEM, TABLE_NAME, COLUMN_NAME," + dataTypeClause("COLUMN_TYPE") + " DATA_TYPE," - + columnTypeClause(options) + + DataTypeClause(conf) + " TYPE_NAME, " + " CASE DATA_TYPE" + " WHEN 'time' THEN " @@ -798,7 +770,7 @@ public ResultSet getColumns( + (datePrecisionColumnExist ? "IF(DATETIME_PRECISION = 0, 19, CAST(20 + DATETIME_PRECISION as signed integer))" : "19") - + (options.yearIsDateType ? "" : " WHEN 'year' THEN 5") + + (conf.yearIsDateType() ? "" : " WHEN 'year' THEN 5") + " ELSE " + " IF(NUMERIC_PRECISION IS NULL, LEAST(CHARACTER_MAXIMUM_LENGTH," + Integer.MAX_VALUE @@ -807,9 +779,9 @@ public ResultSet getColumns( + " COLUMN_SIZE, 65535 BUFFER_LENGTH, " + " CONVERT (CASE DATA_TYPE" + " WHEN 'year' THEN " - + (options.yearIsDateType ? "NUMERIC_SCALE" : "0") + + (conf.yearIsDateType() ? "NUMERIC_SCALE" : "0") + " WHEN 'tinyint' THEN " - + (options.tinyInt1isBit ? "0" : "NUMERIC_SCALE") + + (conf.tinyInt1isBit() ? "0" : "NUMERIC_SCALE") + " ELSE NUMERIC_SCALE END, UNSIGNED INTEGER) DECIMAL_DIGITS," + " 10 NUM_PREC_RADIX, IF(IS_NULLABLE = 'yes',1,0) NULLABLE,COLUMN_COMMENT REMARKS," + " COLUMN_DEFAULT COLUMN_DEF, 0 SQL_DATA_TYPE, 0 SQL_DATETIME_SUB, " @@ -922,7 +894,7 @@ public ResultSet getExportedKeys(String catalog, String schema, String table) + " WHEN 'SET DEFAULT' THEN 4" + " END DELETE_RULE," + " RC.CONSTRAINT_NAME FK_NAME," - + " 'PRIMARY' PK_NAME," + + " RC.UNIQUE_CONSTRAINT_NAME PK_NAME," + importedKeyNotDeferrable + " DEFERRABILITY" + " FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU" @@ -969,7 +941,7 @@ public ResultSet getImportedKeysUsingInformationSchema(String catalog, String ta + " WHEN 'SET DEFAULT' THEN 4" + " END DELETE_RULE," + " RC.CONSTRAINT_NAME FK_NAME," - + " NULL PK_NAME," + + " RC.UNIQUE_CONSTRAINT_NAME PK_NAME," + importedKeyNotDeferrable + " DEFERRABILITY" + " FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU" @@ -1009,13 +981,10 @@ public ResultSet getImportedKeysUsingShowCreateTable(String catalog, String tabl connection .createStatement() .executeQuery( - "SHOW CREATE TABLE " - + MariaDbConnection.quoteIdentifier(catalog) - + "." - + MariaDbConnection.quoteIdentifier(table)); + "SHOW CREATE TABLE " + quoteIdentifier(catalog) + "." + quoteIdentifier(table)); if (rs.next()) { String tableDef = rs.getString(2); - return MariaDbDatabaseMetaData.getImportedKeys(tableDef, table, catalog, connection); + return getImportedKeys(tableDef, table, catalog, connection); } throw new SQLException("Fail to retrieve table information using SHOW CREATE TABLE"); } @@ -1075,22 +1044,34 @@ public ResultSet getBestRowIdentifier( if (table == null) { throw new SQLException("'table' parameter cannot be null in getBestRowIdentifier()"); } + boolean hasIsGeneratedCol = + (connection.isMariaDbServer() + && connection.getContext().getVersion().versionGreaterOrEqual(10, 2, 0)); String sql = "SELECT " - + DatabaseMetaData.bestRowUnknown + + bestRowSession + " SCOPE, COLUMN_NAME," + dataTypeClause("COLUMN_TYPE") + " DATA_TYPE, DATA_TYPE TYPE_NAME," + " IF(NUMERIC_PRECISION IS NULL, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION) COLUMN_SIZE, 0 BUFFER_LENGTH," + " NUMERIC_SCALE DECIMAL_DIGITS," - + " 1 PSEUDO_COLUMN" + + (hasIsGeneratedCol + ? ("IF(IS_GENERATED='NEVER'," + bestRowNotPseudo + "," + bestRowPseudo + ")") + : bestRowNotPseudo) + + " PSEUDO_COLUMN" + " FROM INFORMATION_SCHEMA.COLUMNS" - + " WHERE COLUMN_KEY IN('PRI', 'MUL', 'UNI')" - + " AND " + + " WHERE (COLUMN_KEY = 'PRI'" + + " OR (COLUMN_KEY = 'UNI' AND NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE COLUMN_KEY = " + + "'PRI' AND " + catalogCond("TABLE_SCHEMA", catalog) + " AND TABLE_NAME = " - + escapeQuote(table); + + escapeQuote(table) + + " ))) AND " + + catalogCond("TABLE_SCHEMA", catalog) + + " AND TABLE_NAME = " + + escapeQuote(table) + + (nullable ? "" : " AND IS_NULLABLE = 'NO'"); return executeQuery(sql); } @@ -1180,11 +1161,11 @@ public boolean allTablesAreSelectable() { @Override public String getURL() { - return urlParser.getInitialUrl(); + return conf.initialUrl(); } public String getUserName() { - return urlParser.getUsername(); + return conf.user(); } public boolean isReadOnly() { @@ -1215,22 +1196,14 @@ public boolean nullsAreSortedAtEnd() { * @throws SQLException in case of socket error. */ public String getDatabaseProductName() throws SQLException { - if (urlParser.getOptions().useMysqlMetadata) { - return "MySQL"; - } - if (connection.getProtocol().isServerMariaDb() - && connection - .getProtocol() - .getServerVersion() - .toLowerCase(Locale.ROOT) - .contains("mariadb")) { + if (connection.getContext().getVersion().isMariaDBServer()) { return "MariaDB"; } return "MySQL"; } public String getDatabaseProductVersion() { - return connection.getProtocol().getServerVersion(); + return connection.getContext().getVersion().getServerVersion(); } public String getDriverName() { @@ -1808,7 +1781,7 @@ public int getMaxSchemaNameLength() { } public int getMaxProcedureNameLength() { - return 256; + return 64; } public int getMaxCatalogNameLength() { @@ -1844,7 +1817,7 @@ public int getMaxUserNameLength() { } public int getDefaultTransactionIsolation() { - return Connection.TRANSACTION_REPEATABLE_READ; + return java.sql.Connection.TRANSACTION_REPEATABLE_READ; } /** @@ -1866,14 +1839,14 @@ public boolean supportsTransactions() { * @param level one of the transaction isolation levels defined in java.sql.Connection * * @return true if so; false otherwise - * @see Connection + * @see java.sql.Connection */ public boolean supportsTransactionIsolationLevel(int level) { switch (level) { - case Connection.TRANSACTION_READ_UNCOMMITTED: - case Connection.TRANSACTION_READ_COMMITTED: - case Connection.TRANSACTION_REPEATABLE_READ: - case Connection.TRANSACTION_SERIALIZABLE: + case java.sql.Connection.TRANSACTION_READ_UNCOMMITTED: + case java.sql.Connection.TRANSACTION_READ_COMMITTED: + case java.sql.Connection.TRANSACTION_REPEATABLE_READ: + case java.sql.Connection.TRANSACTION_SERIALIZABLE: return true; default: return false; @@ -1962,7 +1935,7 @@ public ResultSet getProcedures(String catalog, String schemaPattern, String proc /* Is INFORMATION_SCHEMA.PARAMETERS available ?*/ private boolean haveInformationSchemaParameters() { - return connection.getProtocol().versionGreaterOrEqual(5, 5, 3); + return connection.getContext().getVersion().versionGreaterOrEqual(5, 5, 3); } /** @@ -2536,16 +2509,16 @@ public ResultSet getVersionColumns(String catalog, String schema, String table) * @param parentSchema a schema name; must match the schema name as it is stored in the database; * "" retrieves those without a schema; null means drop schema name from the * selection criteria - * @param parentTable the name of the table that exports the key; must match the table name as it - * is stored in the database + * @param parentTable the name of the table that exports the key; pattern, or null (means any + * table) value * @param foreignCatalog a catalog name; must match the catalog name as it is stored in the * database; "" retrieves those without a catalog; null means drop catalog name * from the selection criteria * @param foreignSchema a schema name; must match the schema name as it is stored in the database; * "" retrieves those without a schema; null means drop schema name from the * selection criteria - * @param foreignTable the name of the table that imports the key; must match the table name as it - * is stored in the database + * @param foreignTable the name of the table that imports the key; pattern, or null (means any + * table) value is stored in the database * @return ResultSet - each row is a foreign key column description * @throws SQLException if a database access error occurs * @see #getImportedKeys @@ -2578,23 +2551,19 @@ public ResultSet getCrossReference( + " WHEN 'SET DEFAULT' THEN 4" + " END DELETE_RULE," + " RC.CONSTRAINT_NAME FK_NAME," - + " NULL PK_NAME," + + " RC.UNIQUE_CONSTRAINT_NAME PK_NAME," + importedKeyNotDeferrable - + " DEFERRABILITY" - + " FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU" + + " DEFERRABILITY " + + "FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU" + " INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC" + " ON KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA" - + " AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME" - + " WHERE " + + " AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME " + + "WHERE " + catalogCond("KCU.REFERENCED_TABLE_SCHEMA", parentCatalog) + " AND " + catalogCond("KCU.TABLE_SCHEMA", foreignCatalog) - + " AND " - + " KCU.REFERENCED_TABLE_NAME = " - + escapeQuote(parentTable) - + " AND " - + " KCU.TABLE_NAME = " - + escapeQuote(foreignTable) + + patternCond("KCU.REFERENCED_TABLE_NAME", parentTable) + + patternCond("KCU.TABLE_NAME", foreignTable) + " ORDER BY FKTABLE_CAT, FKTABLE_SCHEM, FKTABLE_NAME, KEY_SEQ"; return executeQuery(sql); @@ -2666,25 +2635,25 @@ public ResultSet getTypeInfo() { "FIXED_PREC_SCALE", "AUTO_INCREMENT", "LOCAL_TYPE_NAME", "MINIMUM_SCALE", "MAXIMUM_SCALE", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "NUM_PREC_RADIX" }; - ColumnType[] columnTypes = { - ColumnType.VARCHAR, - ColumnType.INTEGER, - ColumnType.INTEGER, - ColumnType.VARCHAR, - ColumnType.VARCHAR, - ColumnType.VARCHAR, - ColumnType.INTEGER, - ColumnType.BIT, - ColumnType.SMALLINT, - ColumnType.BIT, - ColumnType.BIT, - ColumnType.BIT, - ColumnType.VARCHAR, - ColumnType.SMALLINT, - ColumnType.SMALLINT, - ColumnType.INTEGER, - ColumnType.INTEGER, - ColumnType.INTEGER + DataType[] dataTypes = { + DataType.VARCHAR, + DataType.INTEGER, + DataType.INTEGER, + DataType.VARCHAR, + DataType.VARCHAR, + DataType.VARCHAR, + DataType.INTEGER, + DataType.BIT, + DataType.SMALLINT, + DataType.BIT, + DataType.BIT, + DataType.BIT, + DataType.VARCHAR, + DataType.SMALLINT, + DataType.SMALLINT, + DataType.INTEGER, + DataType.INTEGER, + DataType.INTEGER }; String[][] data = { @@ -3311,8 +3280,7 @@ public ResultSet getTypeInfo() { } }; - return SelectResultSet.createResultSet( - columnNames, columnTypes, data, connection.getProtocol()); + return CompleteResult.createResultSet(columnNames, dataTypes, data, connection.getContext()); } /** @@ -3374,7 +3342,9 @@ public ResultSet getIndexInfo( String sql = "SELECT TABLE_SCHEMA TABLE_CAT, NULL TABLE_SCHEM, TABLE_NAME, NON_UNIQUE, " - + " TABLE_SCHEMA INDEX_QUALIFIER, INDEX_NAME, 3 TYPE," + + " TABLE_SCHEMA INDEX_QUALIFIER, INDEX_NAME, " + + tableIndexOther + + " TYPE," + " SEQ_IN_INDEX ORDINAL_POSITION, COLUMN_NAME, COLLATION ASC_OR_DESC," + " CARDINALITY, NULL PAGES, NULL FILTER_CONDITION" + " FROM INFORMATION_SCHEMA.STATISTICS" @@ -3525,7 +3495,7 @@ public ResultSet getUDTs( return executeQuery(sql); } - public Connection getConnection() { + public org.mariadb.jdbc.Connection getConnection() { return connection; } @@ -3719,11 +3689,11 @@ public int getResultSetHoldability() { } public int getDatabaseMajorVersion() { - return connection.getProtocol().getMajorServerVersion(); + return connection.getContext().getVersion().getMajorVersion(); } public int getDatabaseMinorVersion() { - return connection.getProtocol().getMinorServerVersion(); + return connection.getContext().getVersion().getMinorVersion(); } @Override @@ -3778,59 +3748,36 @@ public boolean autoCommitFailureClosesAllResultSets() { * @return A ResultSet object; each row is a supported client info property */ public ResultSet getClientInfoProperties() { - ColumnDefinition[] columns = new ColumnDefinition[4]; - columns[0] = ColumnDefinition.create("NAME", ColumnType.STRING); - columns[1] = ColumnDefinition.create("MAX_LEN", ColumnType.INTEGER); - columns[2] = ColumnDefinition.create("DEFAULT_VALUE", ColumnType.STRING); - columns[3] = ColumnDefinition.create("DESCRIPTION", ColumnType.STRING); - - byte[] sixteenMb = - new byte[] { - (byte) 49, (byte) 54, (byte) 55, (byte) 55, (byte) 55, (byte) 50, (byte) 49, (byte) 53 - }; - byte[] empty = new byte[0]; + String[] columnNames = new String[] {"NAME", "MAX_LEN", "DEFAULT_VALUE", "DESCRIPTION"}; - ColumnType[] types = - new ColumnType[] { - ColumnType.STRING, ColumnType.INTEGER, ColumnType.STRING, ColumnType.STRING + DataType[] types = + new DataType[] { + DataType.VARSTRING, DataType.INTEGER, DataType.VARSTRING, DataType.VARSTRING }; - List rows = new ArrayList<>(3); - - rows.add( - StandardPacketInputStream.create( - new byte[][] { - "ApplicationName".getBytes(), - sixteenMb, - empty, - "The name of the application currently utilizing the connection".getBytes() - }, - types)); - - rows.add( - StandardPacketInputStream.create( - new byte[][] { - "ClientUser".getBytes(), - sixteenMb, - empty, - ("The name of the user that the application using the connection is performing work for. " - + "This may not be the same as the user name that was used in establishing the connection.") - .getBytes() - }, - types)); - - rows.add( - StandardPacketInputStream.create( - new byte[][] { - "ClientHostname".getBytes(), - sixteenMb, - empty, - "The hostname of the computer the application using the connection is running on" - .getBytes() - }, - types)); - - return new SelectResultSet( - columns, rows, connection.getProtocol(), ResultSet.TYPE_SCROLL_INSENSITIVE); + String[][] data = + new String[][] { + new String[] { + "ApplicationName", + "16777215", + "", + "The name of the application currently utilizing the connection" + }, + new String[] { + "ClientUser", + "16777215", + "", + "The name of the user that the application using the connection is performing work for. " + + "This may not be the same as the user name that was used in establishing the connection." + }, + new String[] { + "ClientHostname", + "16777215", + "", + "The hostname of the computer the application using the connection is running on" + } + }; + + return CompleteResult.createResultSet(columnNames, types, data, connection.getContext()); } /** @@ -3908,4 +3855,22 @@ public long getMaxLogicalLobSize() { public boolean supportsRefCursors() { return false; } + + public class Identifier { + + public String schema; + public String name; + + /** + * Identifier string value. + * + * @return the datas. + */ + public String toString() { + if (schema != null) { + return schema + "." + name; + } + return name; + } + } } diff --git a/src/main/java/org/mariadb/jdbc/Driver.java b/src/main/java/org/mariadb/jdbc/Driver.java index 213f050f9..963b73128 100644 --- a/src/main/java/org/mariadb/jdbc/Driver.java +++ b/src/main/java/org/mariadb/jdbc/Driver.java @@ -1,5 +1,4 @@ /* - * * MariaDB Client for Java * * Copyright (c) 2012-2014 Monty Program Ab. @@ -18,61 +17,61 @@ * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to Monty Program Ab info@montyprogram.com. * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * */ package org.mariadb.jdbc; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Field; -import java.sql.*; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.util.ArrayList; import java.util.List; import java.util.Properties; -import org.mariadb.jdbc.internal.util.DeRegister; -import org.mariadb.jdbc.internal.util.constant.HaMode; -import org.mariadb.jdbc.internal.util.constant.Version; -import org.mariadb.jdbc.util.DefaultOptions; -import org.mariadb.jdbc.util.Options; +import java.util.concurrent.locks.ReentrantLock; +import org.mariadb.jdbc.client.Client; +import org.mariadb.jdbc.client.ClientImpl; +import org.mariadb.jdbc.client.MultiPrimaryClient; +import org.mariadb.jdbc.client.MultiPrimaryReplicaClient; +import org.mariadb.jdbc.util.Version; public final class Driver implements java.sql.Driver { static { try { - DriverManager.registerDriver(new Driver(), new DeRegister()); + DriverManager.registerDriver(new Driver()); } catch (SQLException e) { - throw new RuntimeException("Could not register driver", e); } } + protected static Connection connect(Configuration configuration) throws SQLException { + HostAddress hostAddress = null; + ReentrantLock lock = new ReentrantLock(); + Client client; + switch (configuration.haMode()) { + case LOADBALANCE: + case SEQUENTIAL: + client = new MultiPrimaryClient(configuration, lock); + break; + + case REPLICATION: + // additional check + client = new MultiPrimaryReplicaClient(configuration, lock); + break; + + default: + if (configuration.addresses().size() > 0) { + hostAddress = configuration.addresses().get(0); + } + client = new ClientImpl(configuration, hostAddress, false, lock, false); + break; + } + return new Connection(configuration, lock, client); + } + /** * Connect to the given connection string. * @@ -81,13 +80,8 @@ public final class Driver implements java.sql.Driver { * @throws SQLException if it is not possible to connect */ public Connection connect(final String url, final Properties props) throws SQLException { - - UrlParser urlParser = UrlParser.parse(url, props); - if (urlParser == null || urlParser.getHostAddresses() == null) { - return null; - } else { - return MariaDbConnection.newConnection(urlParser, null); - } + Configuration configuration = Configuration.parse(url, props); + return connect(configuration); } /** @@ -98,7 +92,7 @@ public Connection connect(final String url, final Properties props) throws SQLEx */ @Override public boolean acceptsURL(String url) { - return UrlParser.acceptsUrl(url); + return Configuration.acceptsUrl(url); } /** @@ -110,32 +104,39 @@ public boolean acceptsURL(String url) { * @throws SQLException if there is a problem getting the property info */ public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { - Options options; if (url != null && !url.isEmpty()) { - UrlParser urlParser = UrlParser.parse(url, info); - if (urlParser == null || urlParser.getOptions() == null) { + Configuration conf = Configuration.parse(url, info); + if (conf == null) { return new DriverPropertyInfo[0]; } - options = urlParser.getOptions(); - } else { - options = DefaultOptions.parse(HaMode.NONE, "", info, null); - } - List props = new ArrayList<>(); - for (DefaultOptions o : DefaultOptions.values()) { - try { - Field field = Options.class.getField(o.getOptionName()); - Object value = field.get(options); - DriverPropertyInfo propertyInfo = - new DriverPropertyInfo(field.getName(), value == null ? null : value.toString()); - propertyInfo.description = o.getDescription(); - propertyInfo.required = o.isRequired(); - props.add(propertyInfo); - } catch (NoSuchFieldException | IllegalAccessException e) { - // eat error + Properties propDesc = new Properties(); + try (InputStream inputStream = + Driver.class.getClassLoader().getResourceAsStream("driver.properties")) { + propDesc.load(inputStream); + } catch (IOException io) { + } + + List props = new ArrayList<>(); + for (Field field : Configuration.Builder.class.getDeclaredFields()) { + if (!field.getName().startsWith("_")) { + try { + Field fieldConf = Configuration.class.getDeclaredField(field.getName()); + fieldConf.setAccessible(true); + Object obj = fieldConf.get(conf); + String value = obj == null ? null : obj.toString(); + DriverPropertyInfo propertyInfo = new DriverPropertyInfo(field.getName(), value); + propertyInfo.description = value == null ? "" : (String) propDesc.get(field.getName()); + propertyInfo.required = false; + props.add(propertyInfo); + } catch (IllegalAccessException | NoSuchFieldException e) { + // eat error + } + } } + return props.toArray(new DriverPropertyInfo[props.size()]); } - return props.toArray(new DriverPropertyInfo[props.size()]); + return new DriverPropertyInfo[0]; } /** diff --git a/src/main/java/org/mariadb/jdbc/FunctionStatement.java b/src/main/java/org/mariadb/jdbc/FunctionStatement.java new file mode 100644 index 000000000..b6caa0386 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/FunctionStatement.java @@ -0,0 +1,77 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +import java.sql.*; +import java.util.concurrent.locks.ReentrantLock; +import org.mariadb.jdbc.client.result.Result; +import org.mariadb.jdbc.util.ParameterList; + +public class FunctionStatement extends BaseCallableStatement implements CallableStatement { + + public FunctionStatement( + Connection con, + String databaseName, + String procedureName, + String arguments, + ReentrantLock lock, + boolean canUseServerTimeout, + boolean canUseServerMaxRows, + int resultSetType, + int resultSetConcurrency) + throws SQLException { + super( + "SELECT " + procedureName + ((arguments == null) ? "()" : arguments), + con, + lock, + databaseName, + procedureName, + canUseServerTimeout, + canUseServerMaxRows, + resultSetType, + resultSetConcurrency, + 0); + registerOutParameter(1, null); + } + + @Override + public boolean isFunction() { + return true; + } + + @Override + protected void handleParameterOutput() throws SQLException { + this.outputResult = (Result) this.results.remove(this.results.size() - 1); + this.outputResult.next(); + } + + @Override + protected void validParameters() throws SQLException { + // remove first parameter, as it's an output param only + ParameterList newParameters = new ParameterList(parameters.size() - 1); + for (int i = 0; i < parameters.size() - 1; i++) { + newParameters.set(i, parameters.get(i + 1)); + } + parameters = newParameters; + super.validParameters(); + } +} diff --git a/src/main/java/org/mariadb/jdbc/HostAddress.java b/src/main/java/org/mariadb/jdbc/HostAddress.java index ca8c3a0b2..54efc0812 100644 --- a/src/main/java/org/mariadb/jdbc/HostAddress.java +++ b/src/main/java/org/mariadb/jdbc/HostAddress.java @@ -1,5 +1,4 @@ /* - * * MariaDB Client for Java * * Copyright (c) 2012-2014 Monty Program Ab. @@ -18,82 +17,41 @@ * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to Monty Program Ab info@montyprogram.com. * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * */ package org.mariadb.jdbc; +import java.sql.SQLException; import java.util.ArrayList; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.mariadb.jdbc.internal.logging.Logger; -import org.mariadb.jdbc.internal.logging.LoggerFactory; -import org.mariadb.jdbc.internal.util.constant.HaMode; -import org.mariadb.jdbc.internal.util.constant.ParameterConstant; +import java.util.Objects; +import org.mariadb.jdbc.util.constants.HaMode; public class HostAddress { - private static final Logger logger = LoggerFactory.getLogger(HostAddress.class); - public String host; public int port; - public String type = null; - - private HostAddress() {} + public Boolean primary; /** - * Constructor. type is master. + * Constructor. * * @param host host * @param port port + * @param primary is primary */ - public HostAddress(String host, int port) { + private HostAddress(String host, int port, Boolean primary) { this.host = host; this.port = port; - this.type = ParameterConstant.TYPE_MASTER; + this.primary = primary; } - /** - * Constructor. - * - * @param host host - * @param port port - * @param type type - */ - public HostAddress(String host, int port, String type) { - this.host = host; - this.port = port; - this.type = type; + public static HostAddress from(String host, int port) { + return new HostAddress(host, port, null); + } + + public static HostAddress from(String host, int port, boolean primary) { + return new HostAddress(host, port, primary); } /** @@ -102,9 +60,10 @@ public HostAddress(String host, int port, String type) { * @param spec list of endpoints in one of the forms 1 - host1,....,hostN:port (missing port * default to MariaDB default 3306 2 - host:port,...,host:port * @param haMode High availability mode + * @throws SQLException for wrong spec * @return parsed endpoints */ - public static List parse(String spec, HaMode haMode) { + public static List parse(String spec, HaMode haMode) throws SQLException { if (spec == null) { throw new IllegalArgumentException("Invalid connection URL, host address must not be empty "); } @@ -115,62 +74,53 @@ public static List parse(String spec, HaMode haMode) { int size = tokens.length; List arr = new ArrayList<>(size); - // Aurora using cluster end point mustn't have any other host - if (haMode == HaMode.AURORA) { - Pattern clusterPattern = - Pattern.compile( - "(.+)\\.(?:cluster-|cluster-ro-)([a-z0-9]+\\.[a-z0-9\\-]+\\.rds\\.amazonaws\\.com)", - Pattern.CASE_INSENSITIVE); - Matcher matcher = clusterPattern.matcher(spec); - - if (!matcher.find()) { - logger.warn( - "Aurora recommended connection URL must only use cluster end-point like " - + "\"jdbc:mariadb:aurora://xx.cluster-yy.zz.rds.amazonaws.com\". " - + "Using end-point permit auto-discovery of new replicas"); - } - } - - for (String token : tokens) { + for (int i = 0; i < tokens.length; i++) { + String token = tokens[i]; if (token.startsWith("address=")) { - arr.add(parseParameterHostAddress(token)); + arr.add(parseParameterHostAddress(token, haMode, i == 0)); } else { - arr.add(parseSimpleHostAddress(token)); + arr.add(parseSimpleHostAddress(token, haMode, i == 0)); } } - if (haMode == HaMode.REPLICATION) { - for (int i = 0; i < size; i++) { - if (i == 0 && arr.get(i).type == null) { - arr.get(i).type = ParameterConstant.TYPE_MASTER; - } else if (i != 0 && arr.get(i).type == null) { - arr.get(i).type = ParameterConstant.TYPE_SLAVE; - } - } - } return arr; } - private static HostAddress parseSimpleHostAddress(String str) { - HostAddress result = new HostAddress(); + private static HostAddress parseSimpleHostAddress(String str, HaMode haMode, boolean first) { + String host; + int port = 3306; + Boolean primary = null; + if (str.charAt(0) == '[') { /* IPv6 addresses in URLs are enclosed in square brackets */ int ind = str.indexOf(']'); - result.host = str.substring(1, ind); + host = str.substring(1, ind); if (ind != (str.length() - 1) && str.charAt(ind + 1) == ':') { - result.port = getPort(str.substring(ind + 2)); + port = getPort(str.substring(ind + 2)); } } else if (str.contains(":")) { /* Parse host:port */ String[] hostPort = str.split(":"); - result.host = hostPort[0]; - result.port = getPort(hostPort[1]); + host = hostPort[0]; + port = getPort(hostPort[1]); } else { /* Just host name is given */ - result.host = str; - result.port = 3306; + host = str; } - return result; + + if (primary == null) { + switch (haMode) { + case REPLICATION: + primary = first; + break; + + default: + primary = true; + break; + } + } + + return new HostAddress(host, port, primary); } private static int getPort(String portString) { @@ -181,8 +131,12 @@ private static int getPort(String portString) { } } - private static HostAddress parseParameterHostAddress(String str) { - HostAddress result = new HostAddress(); + private static HostAddress parseParameterHostAddress(String str, HaMode haMode, boolean first) + throws SQLException { + String host = null; + int port = 3306; + Boolean primary = null; + String[] array = str.split("(?=\\()|(?<=\\))"); for (int i = 1; i < array.length; i++) { String[] token = array[i].replace("(", "").replace(")", "").trim().split("="); @@ -192,17 +146,40 @@ private static HostAddress parseParameterHostAddress(String str) { } String key = token[0].toLowerCase(); String value = token[1].toLowerCase(); - if ("host".equals(key)) { - result.host = value.replace("[", "").replace("]", ""); - } else if ("port".equals(key)) { - result.port = getPort(value); - } else if ("type".equals(key) - && (value.equals(ParameterConstant.TYPE_MASTER) - || value.equals(ParameterConstant.TYPE_SLAVE))) { - result.type = value; + + switch (key) { + case "host": + host = value.replace("[", "").replace("]", ""); + break; + case "port": + port = getPort(value); + break; + case "type": + if ("master".equalsIgnoreCase(value) || "primary".equalsIgnoreCase(value)) { + primary = true; + } else if ("slave".equalsIgnoreCase(value) || "replica".equalsIgnoreCase(value)) { + primary = false; + } else { + throw new SQLException( + String.format("Wrong type value %s (possible value primary/replica)", array[i])); + } + break; + } + } + + if (primary == null) { + switch (haMode) { + case REPLICATION: + primary = first; + break; + + default: + primary = true; + break; } } - return result; + + return new HostAddress(host, port, primary); } /** @@ -214,13 +191,13 @@ private static HostAddress parseParameterHostAddress(String str) { public static String toString(List addrs) { StringBuilder str = new StringBuilder(); for (int i = 0; i < addrs.size(); i++) { - if (addrs.get(i).type != null) { + if (addrs.get(i).primary != null) { str.append("address=(host=") .append(addrs.get(i).host) .append(")(port=") .append(addrs.get(i).port) .append(")(type=") - .append(addrs.get(i).type) + .append(addrs.get(i).primary ? "primary" : "replica") .append(")"); } else { boolean isIPv6 = addrs.get(i).host != null && addrs.get(i).host.contains(":"); @@ -244,13 +221,13 @@ public static String toString(List addrs) { public static String toString(HostAddress[] addrs) { StringBuilder str = new StringBuilder(); for (int i = 0; i < addrs.length; i++) { - if (addrs[i].type != null) { + if (addrs[i].primary != null) { str.append("address=(host=") .append(addrs[i].host) .append(")(port=") .append(addrs[i].port) .append(")(type=") - .append(addrs[i].type) + .append(addrs[i].primary ? "primary" : "replica") .append(")"); } else { boolean isIPv6 = addrs[i].host != null && addrs[i].host.contains(":"); @@ -266,37 +243,23 @@ public static String toString(HostAddress[] addrs) { @Override public String toString() { - return "HostAddress{" - + "host='" - + host - + '\'' - + ", port=" - + port - + ((type != null) ? (", type='" + type + "'") : "") - + "}"; + return String.format( + "address=(host=%s)(port=%s)%s", + host, port, ((primary != null) ? ("(type=" + (primary ? "primary)" : "replica)")) : "")); } @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - - HostAddress that = (HostAddress) obj; - + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HostAddress that = (HostAddress) o; return port == that.port - && (host != null - ? host.equals(that.host) - : that.host == null && !(type != null ? !type.equals(that.type) : that.type != null)); + && Objects.equals(host, that.host) + && Objects.equals(primary, that.primary); } @Override public int hashCode() { - int result = host != null ? host.hashCode() : 0; - result = 31 * result + port; - return result; + return Objects.hash(host, port, primary); } } diff --git a/src/main/java/org/mariadb/jdbc/LocalInfileInterceptor.java b/src/main/java/org/mariadb/jdbc/LocalInfileInterceptor.java deleted file mode 100644 index 8242e0339..000000000 --- a/src/main/java/org/mariadb/jdbc/LocalInfileInterceptor.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -public interface LocalInfileInterceptor { - - boolean validate(String fileName); -} diff --git a/src/main/java/org/mariadb/jdbc/MariaDbBlob.java b/src/main/java/org/mariadb/jdbc/MariaDbBlob.java index f4297b2c3..78386ef8d 100644 --- a/src/main/java/org/mariadb/jdbc/MariaDbBlob.java +++ b/src/main/java/org/mariadb/jdbc/MariaDbBlob.java @@ -1,5 +1,4 @@ /* - * * MariaDB Client for Java * * Copyright (c) 2012-2014 Monty Program Ab. @@ -18,36 +17,6 @@ * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to Monty Program Ab info@montyprogram.com. * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * */ package org.mariadb.jdbc; @@ -56,7 +25,6 @@ import java.sql.Blob; import java.sql.SQLException; import java.util.Arrays; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; public class MariaDbBlob implements Blob, Serializable { @@ -79,7 +47,7 @@ public MariaDbBlob() { */ public MariaDbBlob(byte[] bytes) { if (bytes == null) { - throw new NullPointerException("byte array is null"); + throw new IllegalArgumentException("byte array is null"); } data = bytes; offset = 0; @@ -95,28 +63,13 @@ public MariaDbBlob(byte[] bytes) { */ public MariaDbBlob(byte[] bytes, int offset, int length) { if (bytes == null) { - throw new NullPointerException("byte array is null"); + throw new IllegalArgumentException("byte array is null"); } data = bytes; this.offset = offset; this.length = Math.min(bytes.length - offset, length); } - private void writeObject(ObjectOutputStream out) throws IOException { - if (offset != 0 || data.length != length) { - data = Arrays.copyOfRange(data, offset, offset + length); - offset = 0; - length = 0; - } - out.defaultWriteObject(); - } - - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - in.defaultReadObject(); - offset = 0; - length = data.length; - } - /** * Returns the number of bytes in the BLOB value designated by this Blob * object. @@ -146,7 +99,7 @@ public long length() { */ public byte[] getBytes(final long pos, final int length) throws SQLException { if (pos < 1) { - throw ExceptionFactory.INSTANCE.create( + throw new SQLException( String.format("Out of range (position should be > 0, but is %s)", pos)); } final int offset = this.offset + (int) (pos - 1); @@ -181,13 +134,13 @@ public InputStream getBinaryStream() throws SQLException { */ public InputStream getBinaryStream(final long pos, final long length) throws SQLException { if (pos < 1) { - throw ExceptionFactory.INSTANCE.create("Out of range (position should be > 0)"); + throw new SQLException("Out of range (position should be > 0)"); } if (pos - 1 > this.length) { - throw ExceptionFactory.INSTANCE.create("Out of range (position > stream size)"); + throw new SQLException("Out of range (position > stream size)"); } if (pos + length - 1 > this.length) { - throw ExceptionFactory.INSTANCE.create("Out of range (position + length - 1 > streamSize)"); + throw new SQLException("Out of range (position + length - 1 > streamSize)"); } return new ByteArrayInputStream(data, this.offset + (int) pos - 1, (int) length); @@ -207,11 +160,11 @@ public long position(final byte[] pattern, final long start) throws SQLException return 0; } if (start < 1) { - throw ExceptionFactory.INSTANCE.create( + throw new SQLException( String.format("Out of range (position should be > 0, but is %s)", start)); } if (start > this.length) { - throw ExceptionFactory.INSTANCE.create("Out of range (start > stream size)"); + throw new SQLException("Out of range (start > stream size)"); } outer: @@ -258,7 +211,7 @@ public long position(final Blob pattern, final long start) throws SQLException { */ public int setBytes(final long pos, final byte[] bytes) throws SQLException { if (pos < 1) { - throw ExceptionFactory.INSTANCE.create("pos should be > 0, first position is 1."); + throw new SQLException("pos should be > 0, first position is 1."); } final int arrayPos = (int) pos - 1; @@ -310,7 +263,7 @@ public int setBytes(final long pos, final byte[] bytes, final int offset, final throws SQLException { if (pos < 1) { - throw ExceptionFactory.INSTANCE.create("pos should be > 0, first position is 1."); + throw new SQLException("pos should be > 0, first position is 1."); } final int arrayPos = (int) pos - 1; @@ -357,7 +310,7 @@ public int setBytes(final long pos, final byte[] bytes, final int offset, final */ public OutputStream setBinaryStream(final long pos) throws SQLException { if (pos < 1) { - throw ExceptionFactory.INSTANCE.create("Invalid position in blob"); + throw new SQLException("Invalid position in blob"); } if (offset > 0) { byte[] tmp = new byte[length]; @@ -394,4 +347,79 @@ public void free() { this.offset = 0; this.length = 0; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MariaDbBlob that = (MariaDbBlob) o; + + if (length != that.length) return false; + + for (int i = 0; i < length; i++) { + if (data[offset + i] != that.data[that.offset + i]) return false; + } + return true; + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(data); + result = 31 * result + offset; + result = 31 * result + length; + return result; + } + + class BlobOutputStream extends OutputStream { + + private final MariaDbBlob blob; + private int pos; + + public BlobOutputStream(MariaDbBlob blob, int pos) { + this.blob = blob; + this.pos = pos; + } + + @Override + public void write(int bit) throws IOException { + + if (this.pos >= blob.length) { + byte[] tmp = new byte[2 * blob.length + 1]; + System.arraycopy(blob.data, blob.offset, tmp, 0, blob.length); + blob.data = tmp; + pos -= blob.offset; + blob.offset = 0; + blob.length++; + } + blob.data[pos++] = (byte) bit; + } + + @Override + public void write(byte[] buf, int off, int len) throws IOException { + if (off < 0) { + throw new IOException("Invalid offset " + off); + } + if (len < 0) { + throw new IOException("Invalid len " + len); + } + int realLen = Math.min(buf.length - off, len); + if (pos + realLen >= blob.length) { + int newLen = 2 * blob.length + realLen; + byte[] tmp = new byte[newLen]; + System.arraycopy(blob.data, blob.offset, tmp, 0, blob.length); + blob.data = tmp; + pos -= blob.offset; + blob.offset = 0; + blob.length = pos + realLen; + } + System.arraycopy(buf, off, blob.data, pos, realLen); + pos += realLen; + } + + @Override + public void write(byte[] buf) throws IOException { + write(buf, 0, buf.length); + } + } } diff --git a/src/main/java/org/mariadb/jdbc/MariaDbClob.java b/src/main/java/org/mariadb/jdbc/MariaDbClob.java index 84808fd61..d81969d38 100644 --- a/src/main/java/org/mariadb/jdbc/MariaDbClob.java +++ b/src/main/java/org/mariadb/jdbc/MariaDbClob.java @@ -1,5 +1,4 @@ /* - * * MariaDB Client for Java * * Copyright (c) 2012-2014 Monty Program Ab. @@ -18,36 +17,6 @@ * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to Monty Program Ab info@montyprogram.com. * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * */ package org.mariadb.jdbc; @@ -58,7 +27,6 @@ import java.sql.Clob; import java.sql.NClob; import java.sql.SQLException; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; public class MariaDbClob extends MariaDbBlob implements Clob, NClob, Serializable { @@ -109,19 +77,15 @@ public String toString() { public String getSubString(long pos, int length) throws SQLException { if (pos < 1) { - throw ExceptionFactory.INSTANCE.create("position must be >= 1"); + throw new SQLException("position must be >= 1"); } if (length < 0) { - throw ExceptionFactory.INSTANCE.create("length must be > 0"); + throw new SQLException("length must be > 0"); } - try { - String val = toString(); - return val.substring((int) pos - 1, Math.min((int) pos - 1 + length, val.length())); - } catch (Exception e) { - throw new SQLException(e); - } + String val = toString(); + return val.substring((int) pos - 1, Math.min((int) pos - 1 + length, val.length())); } public Reader getCharacterStream() { @@ -142,8 +106,7 @@ public Reader getCharacterStream() { public Reader getCharacterStream(long pos, long length) throws SQLException { String val = toString(); if (val.length() < (int) pos - 1 + length) { - throw ExceptionFactory.INSTANCE.create( - "pos + length is greater than the number of characters in the Clob"); + throw new SQLException("pos + length is greater than the number of characters in the Clob"); } String sub = val.substring((int) pos - 1, (int) pos - 1 + (int) length); return new StringReader(sub); @@ -210,13 +173,30 @@ private int utf8Position(int charPosition) { * @throws SQLException if UTF-8 conversion failed */ public int setString(long pos, String str) throws SQLException { + if (str == null) { + throw new SQLException("cannot add null string"); + } + if (pos < 0) { + throw new SQLException("position must be >= 0"); + } int bytePosition = utf8Position((int) pos - 1); super.setBytes(bytePosition + 1 - offset, str.getBytes(StandardCharsets.UTF_8)); return str.length(); } public int setString(long pos, String str, int offset, int len) throws SQLException { - return setString(pos, str.substring(offset, offset + len)); + if (str == null) { + throw new SQLException("cannot add null string"); + } + + if (offset < 0) { + throw new SQLException("offset must be >= 0"); + } + + if (len < 0) { + throw new SQLException("len must be > 0"); + } + return setString(pos, str.substring(offset, Math.min(offset + len, str.length()))); } public OutputStream setAsciiStream(long pos) throws SQLException { diff --git a/src/main/java/org/mariadb/jdbc/MariaDbConnection.java b/src/main/java/org/mariadb/jdbc/MariaDbConnection.java deleted file mode 100644 index 183484756..000000000 --- a/src/main/java/org/mariadb/jdbc/MariaDbConnection.java +++ /dev/null @@ -1,1755 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.net.SocketException; -import java.sql.*; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; -import java.util.UUID; -import java.util.concurrent.Executor; -import java.util.concurrent.locks.ReentrantLock; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.mariadb.jdbc.internal.logging.Logger; -import org.mariadb.jdbc.internal.logging.LoggerFactory; -import org.mariadb.jdbc.internal.protocol.Protocol; -import org.mariadb.jdbc.internal.util.CallableStatementCache; -import org.mariadb.jdbc.internal.util.ConnectionState; -import org.mariadb.jdbc.internal.util.Utils; -import org.mariadb.jdbc.internal.util.dao.CallableStatementCacheKey; -import org.mariadb.jdbc.internal.util.dao.CloneableCallableStatement; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; -import org.mariadb.jdbc.internal.util.pool.GlobalStateInfo; -import org.mariadb.jdbc.internal.util.pool.Pools; -import org.mariadb.jdbc.util.Options; - -@SuppressWarnings("Annotator") -public class MariaDbConnection implements Connection { - - private static final Logger logger = LoggerFactory.getLogger(MariaDbConnection.class); - - /** - * Pattern to check the correctness of callable statement query string Legal queries, as - * documented in JDK have the form: {[?=]call[(arg1,..,,argn)]} - */ - private static final Pattern CALLABLE_STATEMENT_PATTERN = - Pattern.compile( - "^(\\s*\\{)?\\s*((\\?\\s*=)?(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*" - + "call(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*((((`[^`]+`)|([^`\\}]+))\\.)?" - + "((`[^`]+`)|([^`\\}\\(]+)))\\s*(\\(.*\\))?(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*" - + "\\s*(#.*)?)\\s*(\\}\\s*)?$", - Pattern.CASE_INSENSITIVE | Pattern.DOTALL); - /** Check that query can be executed with PREPARE. */ - private static final Pattern PREPARABLE_STATEMENT_PATTERN = - Pattern.compile( - "^(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*(SELECT|UPDATE|INSERT|DELETE|REPLACE|DO|CALL)", - Pattern.CASE_INSENSITIVE); - - public final ReentrantLock lock; - /** the protocol to communicate with. */ - private final Protocol protocol; - /** the properties for the client. */ - private final Options options; - - public MariaDbPooledConnection pooledConnection; - protected boolean nullCatalogMeansCurrent; - private CallableStatementCache callableStatementCache; - private volatile int lowercaseTableNames = -1; - private boolean canUseServerTimeout; - private boolean sessionStateAware; - private int stateFlag = 0; - private int defaultTransactionIsolation = 0; - private ExceptionFactory exceptionFactory; - - private boolean warningsCleared; - - /** - * Creates a new connection with a given protocol and query factory. - * - * @param protocol the protocol to use. - */ - public MariaDbConnection(Protocol protocol) { - this.protocol = protocol; - options = protocol.getOptions(); - canUseServerTimeout = protocol.versionGreaterOrEqual(10, 1, 2); - sessionStateAware = protocol.sessionStateAware(); - nullCatalogMeansCurrent = options.nullCatalogMeansCurrent; - if (options.cacheCallableStmts) { - callableStatementCache = CallableStatementCache.newInstance(options.callableStmtCacheSize); - } - this.lock = protocol.getLock(); - this.exceptionFactory = ExceptionFactory.of(this.getServerThreadId(), this.options); - } - - /** - * Create new connection Object. - * - * @param urlParser parser - * @param globalInfo global info - * @return connection object - * @throws SQLException if any connection error occur - */ - public static MariaDbConnection newConnection(UrlParser urlParser, GlobalStateInfo globalInfo) - throws SQLException { - if (urlParser.getOptions().pool) { - return Pools.retrievePool(urlParser).getConnection(); - } - - Protocol protocol = Utils.retrieveProxy(urlParser, globalInfo); - return new MariaDbConnection(protocol); - } - - public static String quoteIdentifier(String string) { - return "`" + string.replaceAll("`", "``") + "`"; - } - - /** - * UnQuote string. - * - * @param string value - * @return unquote string - * @deprecated since 1.3.0 - */ - @Deprecated - public static String unquoteIdentifier(String string) { - if (string != null && string.startsWith("`") && string.endsWith("`") && string.length() >= 2) { - return string.substring(1, string.length() - 1).replace("``", "`"); - } - return string; - } - - protected Protocol getProtocol() { - return protocol; - } - - /** - * creates a new statement. - * - * @return a statement - * @throws SQLException if we cannot create the statement. - */ - public Statement createStatement() throws SQLException { - checkConnection(); - return new MariaDbStatement( - this, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, exceptionFactory); - } - - /** - * Creates a Statement object that will generate ResultSet objects with - * the given type and concurrency. This method is the same as the createStatement - * method above, but it allows the default result set type and concurrency to be overridden. The - * holdability of the created result sets can be determined by calling {@link #getHoldability}. - * - * @param resultSetType a result set type; one of ResultSet.TYPE_FORWARD_ONLY, - * ResultSet.TYPE_SCROLL_INSENSITIVE, or ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of ResultSet.CONCUR_READ_ONLY - * or ResultSet.CONCUR_UPDATABLE - * @return a new Statement object that will generate ResultSet objects - * with the given type and concurrency - */ - public Statement createStatement(final int resultSetType, final int resultSetConcurrency) { - return new MariaDbStatement(this, resultSetType, resultSetConcurrency, exceptionFactory); - } - - /** - * Creates a Statement object that will generate ResultSet objects with - * the given type, concurrency, and holdability. This method is the same as the - * createStatement method above, but it allows the default result set type, concurrency, - * and holdability to be overridden. - * - * @param resultSetType one of the following ResultSet constants: - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, or - * ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency one of the following ResultSet constants: - * ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE - * @param resultSetHoldability one of the following ResultSet constants: - * ResultSet.HOLD_CURSORS_OVER_COMMIT or ResultSet.CLOSE_CURSORS_AT_COMMIT - * @return a new Statement object that will generate ResultSet objects - * with the given type, concurrency, and holdability - * @see ResultSet - */ - public Statement createStatement( - final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) { - return new MariaDbStatement(this, resultSetType, resultSetConcurrency, exceptionFactory); - } - - private void checkConnection() throws SQLException { - if (protocol.isExplicitClosed()) { - throw exceptionFactory.create("createStatement() is called on closed connection", "08000"); - } - if (protocol.isClosed() && protocol.getProxy() != null) { - lock.lock(); - try { - protocol.getProxy().reconnect(); - } finally { - lock.unlock(); - } - } - } - - /** - * Create a new client prepared statement. - * - * @param sql the query. - * @return a client prepared statement. - * @throws SQLException if there is a problem preparing the statement. - */ - public ClientSidePreparedStatement clientPrepareStatement(final String sql) throws SQLException { - return new ClientSidePreparedStatement( - this, - sql, - ResultSet.TYPE_FORWARD_ONLY, - ResultSet.CONCUR_READ_ONLY, - Statement.RETURN_GENERATED_KEYS, - exceptionFactory); - } - - /** - * Create a new server prepared statement. - * - * @param sql the query. - * @return a server prepared statement. - * @throws SQLException if there is a problem preparing the statement. - */ - public ServerSidePreparedStatement serverPrepareStatement(final String sql) throws SQLException { - return new ServerSidePreparedStatement( - this, - sql, - ResultSet.TYPE_FORWARD_ONLY, - ResultSet.CONCUR_READ_ONLY, - Statement.RETURN_GENERATED_KEYS, - exceptionFactory); - } - - /** - * creates a new prepared statement. - * - * @param sql the query. - * @return a prepared statement. - * @throws SQLException if there is a problem preparing the statement. - */ - public PreparedStatement prepareStatement(final String sql) throws SQLException { - return internalPrepareStatement( - sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, Statement.NO_GENERATED_KEYS); - } - - /** - * Creates a PreparedStatement object that will generate ResultSet - * objects with the given type and concurrency. This method is the same as the - * prepareStatement method above, but it allows the default result set type and concurrency - * to be overridden. The holdability of the created result sets can be determined by calling - * {@link #getHoldability}. - * - * @param sql a String object that is the SQL statement to be sent to the database; - * may contain one or more '?' IN parameters - * @param resultSetType a result set type; one of ResultSet.TYPE_FORWARD_ONLY, - * ResultSet.TYPE_SCROLL_INSENSITIVE, or ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of ResultSet.CONCUR_READ_ONLY - * or ResultSet.CONCUR_UPDATABLE - * @return a new PreparedStatement object containing the pre-compiled SQL statement that will - * produce ResultSet objects with the given type and concurrency - * @throws SQLException if a database access error occurs, this method is called on a closed - * connection or the given parameters are notResultSet constants indicating type - * and concurrency - */ - public PreparedStatement prepareStatement( - final String sql, final int resultSetType, final int resultSetConcurrency) - throws SQLException { - return internalPrepareStatement( - sql, resultSetType, resultSetConcurrency, Statement.NO_GENERATED_KEYS); - } - - /** - * Creates a PreparedStatement object that will generate ResultSet - * objects with the given type, concurrency, and holdability. - * - *

This method is the same as the prepareStatement method above, but it allows the - * default result set type, concurrency, and holdability to be overridden. - * - * @param sql a String object that is the SQL statement to be sent to the database; - * may contain one or more '?' IN parameters - * @param resultSetType one of the following ResultSet constants: - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, or - * ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency one of the following ResultSet constants: - * ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE - * @param resultSetHoldability one of the following ResultSet constants: - * ResultSet.HOLD_CURSORS_OVER_COMMIT or ResultSet.CLOSE_CURSORS_AT_COMMIT - * @return a new PreparedStatement object, containing the pre-compiled SQL statement, - * that will generate ResultSet objects with the given type, concurrency, and - * holdability - * @throws SQLException if a database access error occurs, this method is called on a closed - * connection or the given parameters are not ResultSet constants indicating - * type, concurrency, and holdability - * @see ResultSet - */ - public PreparedStatement prepareStatement( - final String sql, - final int resultSetType, - final int resultSetConcurrency, - final int resultSetHoldability) - throws SQLException { - return internalPrepareStatement( - sql, resultSetType, resultSetConcurrency, Statement.NO_GENERATED_KEYS); - } - - /** - * Creates a default PreparedStatement object that has the capability to retrieve - * auto-generated keys. The given constant tells the driver whether it should make auto-generated - * keys available for retrieval. This parameter is ignored if the SQL statement is not an - * INSERT statement, or an SQL statement able to return auto-generated keys (the list of - * such statements is vendor-specific). - * - *

Note: This method is optimized for handling parametric SQL statements that benefit - * from precompilation. If the driver supports precompilation, the method prepareStatement - * will send the statement to the database for precompilation. Some drivers may not - * support precompilation. In this case, the statement may not be sent to the database until the - * PreparedStatement object is executed. This has no direct effect on users; however, - * it does affect which methods throw certain SQLExceptions. - * - *

Result sets created using the returned PreparedStatement object will by default - * be type TYPE_FORWARD_ONLY and have a concurrency level of CONCUR_READ_ONLY - * . The holdability of the created result sets can be determined by calling {@link - * #getHoldability}. - * - * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders - * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be returned; one - * of Statement.RETURN_GENERATED_KEYS or Statement.NO_GENERATED_KEYS - * @return a new PreparedStatement object, containing the pre-compiled SQL statement, - * that will have the capability of returning auto-generated keys - * @throws SQLException if a database access error occurs, this method is called on a closed - * connection or the given parameter is not a Statement constant indicating - * whether auto-generated keys should be returned - */ - public PreparedStatement prepareStatement(final String sql, final int autoGeneratedKeys) - throws SQLException { - return internalPrepareStatement( - sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, autoGeneratedKeys); - } - - /** - * Creates a default PreparedStatement object capable of returning the auto-generated - * keys designated by the given array. This array contains the indexes of the columns in the - * target table that contain the auto-generated keys that should be made available. The driver - * will ignore the array if the SQL statement is not an INSERT statement, or an SQL - * statement able to return auto-generated keys (the list of such statements is vendor-specific). - * - *

An SQL statement with or without IN parameters can be pre-compiled and stored in a - * PreparedStatement object. This object can then be used to efficiently execute this - * statement multiple times. - * - *

Note: This method is optimized for handling parametric SQL statements that benefit - * from precompilation. If the driver supports precompilation, the method prepareStatement - * will send the statement to the database for precompilation. Some drivers may not - * support precompilation. In this case, the statement may not be sent to the database until the - * PreparedStatement object is executed. This has no direct effect on users; however, - * it does affect which methods throw certain SQLExceptions. - * - *

Result sets created using the returned PreparedStatement object will by default - * be type TYPE_FORWARD_ONLY and have a concurrency level of CONCUR_READ_ONLY - * . The holdability of the created result sets can be determined by calling {@link - * #getHoldability}. - * - * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders - * @param columnIndexes an array of column indexes indicating the columns that should be returned - * from the inserted row or rows - * @return a new PreparedStatement object, containing the pre-compiled statement, - * that is capable of returning the auto-generated keys designated by the given array of - * column indexes - * @throws SQLException if a database access error occurs or this method is called on a closed - * connection - */ - public PreparedStatement prepareStatement(final String sql, final int[] columnIndexes) - throws SQLException { - return prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); - } - - /** - * Creates a default PreparedStatement object capable of returning the auto-generated - * keys designated by the given array. This array contains the names of the columns in the target - * table that contain the auto-generated keys that should be returned. The driver will ignore the - * array if the SQL statement is not an INSERT statement, or an SQL statement able to - * return auto-generated keys (the list of such statements is vendor-specific). - * - *

An SQL statement with or without IN parameters can be pre-compiled and stored in a - * PreparedStatement object. This object can then be used to efficiently execute this - * statement multiple times. - * - *

Note: This method is optimized for handling parametric SQL statements that benefit - * from precompilation. If the driver supports precompilation, the method prepareStatement - * will send the statement to the database for precompilation. Some drivers may not - * support precompilation. In this case, the statement may not be sent to the database until the - * PreparedStatement object is executed. This has no direct effect on users; however, - * it does affect which methods throw certain SQLExceptions. - * - *

Result sets created using the returned PreparedStatement object will by default - * be type TYPE_FORWARD_ONLY and have a concurrency level of CONCUR_READ_ONLY - * . The holdability of the created result sets can be determined by calling {@link - * #getHoldability}. - * - * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders - * @param columnNames an array of column names indicating the columns that should be returned from - * the inserted row or rows - * @return a new PreparedStatement object, containing the pre-compiled statement, - * that is capable of returning the auto-generated keys designated by the given array of - * column names - * @throws SQLException if a database access error occurs or this method is called on a closed - * connection - */ - public PreparedStatement prepareStatement(final String sql, final String[] columnNames) - throws SQLException { - return prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); - } - - /** - * Send ServerPrepareStatement or ClientPrepareStatement depending on SQL query and options If - * server side and PREPARE can be delayed, a facade will be return, to have a fallback on client - * prepareStatement. - * - * @param sql sql query - * @param resultSetScrollType one of the following ResultSet constants: - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, or - * ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of ResultSet.CONCUR_READ_ONLY - * or ResultSet.CONCUR_UPDATABLE - * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be returned; one - * of Statement.RETURN_GENERATED_KEYS or Statement.NO_GENERATED_KEYS - * @return PrepareStatement - * @throws SQLException if a connection error occur during the server preparation. - */ - private PreparedStatement internalPrepareStatement( - final String sql, - final int resultSetScrollType, - final int resultSetConcurrency, - final int autoGeneratedKeys) - throws SQLException { - - if (sql != null) { - - String sqlQuery = Utils.nativeSql(sql, protocol); - - if (options.useServerPrepStmts && PREPARABLE_STATEMENT_PATTERN.matcher(sqlQuery).find()) { - // prepare isn't delayed -> if prepare fail, fallback to client preparedStatement? - checkConnection(); - try { - return new ServerSidePreparedStatement( - this, - sqlQuery, - resultSetScrollType, - resultSetConcurrency, - autoGeneratedKeys, - exceptionFactory); - } catch (SQLNonTransientConnectionException e) { - throw e; - } catch (SQLException e) { - // on some specific case, server cannot prepared data (CONJ-238) - // will use clientPreparedStatement - } - } - return new ClientSidePreparedStatement( - this, - sqlQuery, - resultSetScrollType, - resultSetConcurrency, - autoGeneratedKeys, - exceptionFactory); - } else { - throw new SQLException("SQL value can not be NULL"); - } - } - - /** - * Creates a CallableStatement object for calling database stored procedures. The - * CallableStatement object provides methods for setting up its IN and OUT - * parameters, and methods for executing the call to a stored procedure. example : {?= call - * <procedure-name>[(<arg1>,<arg2>, ...)]} or {call - * <procedure-name>[(<arg1>,<arg2>, ...)]} - * - *

Note: This method is optimized for handling stored procedure call statements. - * - * @param sql an SQL statement that may contain one or more '?' parameter placeholders. Typically - * this statement is specified using JDBC call escape syntax. - * @return a new default CallableStatement object containing the pre-compiled SQL - * statement - * @throws SQLException if a database access error occurs or this method is called on a closed - * connection - */ - public CallableStatement prepareCall(final String sql) throws SQLException { - return prepareCall(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); - } - - /** - * Creates a CallableStatement object that will generate ResultSet - * objects with the given type and concurrency. This method is the same as the prepareCall - * method above, but it allows the default result set type and concurrency to be - * overridden. The holdability of the created result sets can be determined by calling {@link - * #getHoldability}. - * - * @param sql a String object that is the SQL statement to be sent to the database; - * may contain on or more '?' parameters - * @param resultSetType a result set type; one of ResultSet.TYPE_FORWARD_ONLY, - * ResultSet.TYPE_SCROLL_INSENSITIVE, or ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of ResultSet.CONCUR_READ_ONLY - * or ResultSet.CONCUR_UPDATABLE - * @return a new CallableStatement object containing the pre-compiled SQL statement - * that will produce ResultSet objects with the given type and concurrency - * @throws SQLException if a database access error occurs, this method is called on a closed - * connection or the given parameters are not ResultSet constants indicating type - * and concurrency - */ - public CallableStatement prepareCall( - final String sql, final int resultSetType, final int resultSetConcurrency) - throws SQLException { - checkConnection(); - - Matcher matcher = CALLABLE_STATEMENT_PATTERN.matcher(sql); - if (!matcher.matches()) { - throw new SQLSyntaxErrorException( - "invalid callable syntax. must be like {[?=]call [(?,?, ...)]}\n but was : " - + sql); - } - - String query = Utils.nativeSql(matcher.group(2), protocol); - - boolean isFunction = (matcher.group(3) != null); - String databaseAndProcedure = matcher.group(8); - String database = matcher.group(10); - String procedureName = matcher.group(13); - String arguments = matcher.group(16); - if (database == null && sessionStateAware) { - database = protocol.getDatabase(); - } - - if (database != null && options.cacheCallableStmts) { - - if (callableStatementCache.containsKey(new CallableStatementCacheKey(database, query))) { - try { - CallableStatement callableStatement = - callableStatementCache.get(new CallableStatementCacheKey(database, query)); - if (callableStatement != null) { - // Clone to avoid side effect like having some open resultSet. - return ((CloneableCallableStatement) callableStatement).clone(this); - } - } catch (CloneNotSupportedException cloneNotSupportedException) { - cloneNotSupportedException.printStackTrace(); - } - } - CallableStatement callableStatement = - createNewCallableStatement( - query, - procedureName, - isFunction, - databaseAndProcedure, - database, - arguments, - resultSetType, - resultSetConcurrency, - exceptionFactory); - callableStatementCache.put(new CallableStatementCacheKey(database, query), callableStatement); - return callableStatement; - } - return createNewCallableStatement( - query, - procedureName, - isFunction, - databaseAndProcedure, - database, - arguments, - resultSetType, - resultSetConcurrency, - exceptionFactory); - } - - /** - * Creates a CallableStatement object that will generate ResultSet - * objects with the given type and concurrency. This method is the same as the prepareCall - * method above, but it allows the default result set type, result set concurrency type - * and holdability to be overridden. - * - * @param sql a String object that is the SQL statement to be sent to the database; - * may contain on or more '?' parameters - * @param resultSetType one of the following ResultSet constants: - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, or - * ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency one of the following ResultSet constants: - * ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE - * @param resultSetHoldability one of the following ResultSet constants: - * ResultSet.HOLD_CURSORS_OVER_COMMIT or ResultSet.CLOSE_CURSORS_AT_COMMIT - * @return a new CallableStatement object, containing the pre-compiled SQL statement, - * that will generate ResultSet objects with the given type, concurrency, and - * holdability - * @throws SQLException if a database access error occurs, this method is called on a closed - * connection or the given parameters are not ResultSet constants indicating - * type, concurrency, and holdability - * @see ResultSet - */ - public CallableStatement prepareCall( - final String sql, - final int resultSetType, - final int resultSetConcurrency, - final int resultSetHoldability) - throws SQLException { - return prepareCall(sql); - } - - private CallableStatement createNewCallableStatement( - String query, - String procedureName, - boolean isFunction, - String databaseAndProcedure, - String database, - String arguments, - int resultSetType, - final int resultSetConcurrency, - ExceptionFactory exceptionFactory) - throws SQLException { - if (isFunction) { - return new MariaDbFunctionStatement( - this, - database, - databaseAndProcedure, - (arguments == null) ? "()" : arguments, - resultSetType, - resultSetConcurrency, - exceptionFactory); - } else { - return new MariaDbProcedureStatement( - query, - this, - procedureName, - database, - resultSetType, - resultSetConcurrency, - exceptionFactory); - } - } - - @Override - public String nativeSQL(final String sql) throws SQLException { - return Utils.nativeSql(sql, protocol); - } - - /** - * returns true if statements on this connection are auto commited. - * - * @return true if auto commit is on. - * @throws SQLException if there is an error - */ - public boolean getAutoCommit() throws SQLException { - return protocol.getAutocommit(); - } - - /** - * Sets whether this connection is auto commited. - * - * @param autoCommit if it should be auto commited. - * @throws SQLException if something goes wrong talking to the server. - */ - public void setAutoCommit(boolean autoCommit) throws SQLException { - if (autoCommit == getAutoCommit()) { - return; - } - - try (Statement stmt = createStatement()) { - stateFlag |= ConnectionState.STATE_AUTOCOMMIT; - stmt.executeUpdate("set autocommit=" + ((autoCommit) ? "1" : "0")); - } - } - - /** - * Sends commit to the server. - * - * @throws SQLException if there is an error commiting. - */ - public void commit() throws SQLException { - lock.lock(); - try { - if (protocol.inTransaction()) { - try (Statement st = createStatement()) { - st.execute("COMMIT"); - } - } - } finally { - lock.unlock(); - } - } - - /** - * Rolls back a transaction. - * - * @throws SQLException if there is an error rolling back. - */ - public void rollback() throws SQLException { - lock.lock(); - try { - if (protocol.inTransaction()) { - try (Statement st = createStatement()) { - st.execute("ROLLBACK"); - } - } - } finally { - lock.unlock(); - } - } - - /** - * Undoes all changes made after the given Savepoint object was set. - * - *

This method should be used only when auto-commit has been disabled. - * - * @param savepoint the Savepoint object to roll back to - * @throws SQLException if a database access error occurs, this method is called while - * participating in a distributed transaction, this method is called on a closed connection, - * the Savepoint object is no longer valid, or this Connection - * object is currently in auto-commit mode - * @see Savepoint - * @see #rollback - */ - public void rollback(final Savepoint savepoint) throws SQLException { - try (Statement st = createStatement()) { - st.execute("ROLLBACK TO SAVEPOINT `" + savepoint.getSavepointName() + "`"); - } - } - - /** - * close the connection. - * - * @throws SQLException if there is a problem talking to the server. - */ - public void close() throws SQLException { - if (pooledConnection != null) { - rollback(); - pooledConnection.fireConnectionClosed(); - return; - } - protocol.closeExplicit(); - } - - /** - * checks if the connection is closed. - * - * @return true if the connection is closed - */ - public boolean isClosed() { - return protocol.isClosed(); - } - - /** - * returns the meta data about the database. - * - * @return meta data about the db. - */ - public DatabaseMetaData getMetaData() { - return new MariaDbDatabaseMetaData(this, protocol.getUrlParser()); - } - - /** - * Retrieves whether this Connection object is in read-only mode. - * - * @return true if this Connection object is read-only; false - * otherwise - * @throws SQLException SQLException if a database access error occurs or this method is called on - * a closed connection - */ - public boolean isReadOnly() throws SQLException { - return protocol.getReadonly(); - } - - /** - * Sets whether this connection is read only. - * - * @param readOnly true if it should be read only. - * @throws SQLException if there is a problem - */ - public void setReadOnly(final boolean readOnly) throws SQLException { - try { - logger.debug( - "conn={}({}) - set read-only to value {} {}", - protocol.getServerThreadId(), - protocol.isMasterConnection() ? "M" : "S", - readOnly); - stateFlag |= ConnectionState.STATE_READ_ONLY; - protocol.setReadonly(readOnly); - } catch (SQLException e) { - throw exceptionFactory.create(e); - } - } - - /** - * Retrieves this Connection object's current catalog name. - * - * @return the current catalog name or null if there is none - * @throws SQLException if a database access error occurs or this method is called on a closed - * connection - * @see #setCatalog - */ - public String getCatalog() throws SQLException { - return protocol.getCatalog(); - } - - /** - * Sets the given catalog name in order to select a subspace of this Connection - * object's database in which to work. - * - *

If the driver does not support catalogs, it will silently ignore this request. MariaDB - * treats catalogs and databases as equivalent - * - * @param catalog the name of a catalog (subspace in this Connection object's - * database) in which to work - * @throws SQLException if a database access error occurs or this method is called on a closed - * connection - * @see #getCatalog - */ - public void setCatalog(final String catalog) throws SQLException { - if (catalog == null) { - throw new SQLException("The catalog name may not be null", "XAE05"); - } - try { - stateFlag |= ConnectionState.STATE_DATABASE; - protocol.setCatalog(catalog); - } catch (SQLException e) { - throw exceptionFactory.create(e); - } - } - - public boolean isServerMariaDb() throws SQLException { - return protocol.isServerMariaDb(); - } - - public boolean versionGreaterOrEqual(int major, int minor, int patch) { - return protocol.versionGreaterOrEqual(major, minor, patch); - } - - /** - * Retrieves this Connection object's current transaction isolation level. - * - * @return the current transaction isolation level, which will be one of the following constants: - * Connection.TRANSACTION_READ_UNCOMMITTED, - * Connection.TRANSACTION_READ_COMMITTED, Connection.TRANSACTION_REPEATABLE_READ - * , Connection.TRANSACTION_SERIALIZABLE, or - * Connection.TRANSACTION_NONE. - * @throws SQLException if a database access error occurs or this method is called on a closed - * connection - * @see #setTransactionIsolation - */ - public int getTransactionIsolation() throws SQLException { - Statement stmt = createStatement(); - String sql = "SELECT @@tx_isolation"; - - if (!protocol.isServerMariaDb()) { - if ((protocol.getMajorServerVersion() >= 8 && protocol.versionGreaterOrEqual(8, 0, 3)) - || (protocol.getMajorServerVersion() < 8 && protocol.versionGreaterOrEqual(5, 7, 20))) { - sql = "SELECT @@transaction_isolation"; - } - } - - ResultSet rs = stmt.executeQuery(sql); - if (rs.next()) { - final String response = rs.getString(1); - switch (response) { - case "REPEATABLE-READ": - return Connection.TRANSACTION_REPEATABLE_READ; - - case "READ-UNCOMMITTED": - return Connection.TRANSACTION_READ_UNCOMMITTED; - - case "READ-COMMITTED": - return Connection.TRANSACTION_READ_COMMITTED; - - case "SERIALIZABLE": - return Connection.TRANSACTION_SERIALIZABLE; - - default: - throw exceptionFactory.create( - String.format( - "Could not get transaction isolation level: Invalid value \"%s\"", response)); - } - } - throw exceptionFactory.create("Failed to retrieve transaction isolation"); - } - - /** - * Attempts to change the transaction isolation level for this Connection object to - * the one given. The constants defined in the interface Connection are the possible - * transaction isolation levels. - * - *

Note: If this method is called during a transaction, the result is - * implementation-defined. - * - * @param level one of the following Connection constants: - * Connection.TRANSACTION_READ_UNCOMMITTED, Connection.TRANSACTION_READ_COMMITTED - * , Connection.TRANSACTION_REPEATABLE_READ, or - * Connection.TRANSACTION_SERIALIZABLE. (Note that Connection.TRANSACTION_NONE - * cannot be used because it specifies that transactions are not supported.) - * @throws SQLException if a database access error occurs, this method is called on a closed - * connection or the given parameter is not one of the Connection constants - * @see DatabaseMetaData#supportsTransactionIsolationLevel - * @see #getTransactionIsolation - */ - public void setTransactionIsolation(final int level) throws SQLException { - try { - stateFlag |= ConnectionState.STATE_TRANSACTION_ISOLATION; - protocol.setTransactionIsolation(level); - } catch (SQLException e) { - throw exceptionFactory.create(e); - } - } - - /** - * Retrieves the first warning reported by calls on this Connection object. If there - * is more than one warning, subsequent warnings will be chained to the first one and can be - * retrieved by calling the method SQLWarning.getNextWarning on the warning that was - * retrieved previously. - * - *

This method may not be called on a closed connection; doing so will cause an - * SQLException to be thrown. - * - *

Note: Subsequent warnings will be chained to this SQLWarning. - * - * @return the first SQLWarning object or null if there are none - * @throws SQLException if a database access error occurs or this method is called on a closed - * connection - * @see SQLWarning - */ - public SQLWarning getWarnings() throws SQLException { - if (warningsCleared || isClosed() || !protocol.hasWarnings()) { - return null; - } - - SQLWarning last = null; - SQLWarning first = null; - - try (Statement st = this.createStatement()) { - try (ResultSet rs = st.executeQuery("show warnings")) { - // returned result set has 'level', 'code' and 'message' columns, in this order. - while (rs.next()) { - int code = rs.getInt(2); - String message = rs.getString(3); - SQLWarning warning = new SQLWarning(message, null, code); - if (first == null) { - first = warning; - last = warning; - } else { - last.setNextWarning(warning); - last = warning; - } - } - } - } - return first; - } - - /** - * Clears all warnings reported for this Connection object. After a call to this - * method, the method getWarnings returns null until a new warning is - * reported for this Connection object. - * - * @throws SQLException SQLException if a database access error occurs or this method is called on - * a closed connection - */ - public void clearWarnings() throws SQLException { - if (this.isClosed()) { - throw exceptionFactory.create( - "Connection.clearWarnings cannot be called on a closed connection"); - } - warningsCleared = true; - } - - /** Reenable warnings, when next statement is executed. */ - public void reenableWarnings() { - warningsCleared = false; - } - - /** - * Retrieves the Map object associated with this Connection object. - * Unless the application has added an entry, the type map returned will be empty. - * - * @return the java.util.Map object associated with this Connection - * object - * @see #setTypeMap - * @since 1.2 - */ - public Map> getTypeMap() { - return new HashMap<>(); - } - - /** - * Installs the given TypeMap object as the type map for this Connection - * object. The type map will be used for the custom mapping of SQL structured types and distinct - * types. - * - * @param map the java.util.Map object to install as the replacement for this - * Connection object's default type map - * @throws SQLException if a database access error occurs, this method is called on a closed - * connection or the given parameter is not a java.util.Map object - * @see #getTypeMap - */ - public void setTypeMap(final Map> map) throws SQLException { - throw exceptionFactory.notSupported("TypeMap are not supported"); - } - - /** - * Retrieves the current holdability of ResultSet objects created using this - * Connection object. - * - * @return the holdability, one of ResultSet.HOLD_CURSORS_OVER_COMMIT or - * ResultSet.CLOSE_CURSORS_AT_COMMIT - * @see #setHoldability - * @see DatabaseMetaData#getResultSetHoldability - * @see ResultSet - * @since 1.4 - */ - public int getHoldability() { - return ResultSet.HOLD_CURSORS_OVER_COMMIT; - } - - /** - * Changes the default holdability of ResultSet objects created using this - * Connection object to the given holdability. The default holdability of ResultSet - * objects can be be determined by invoking {@link - * DatabaseMetaData#getResultSetHoldability}. - * - * @param holdability a ResultSet holdability constant; one of - * ResultSet.HOLD_CURSORS_OVER_COMMIT or ResultSet.CLOSE_CURSORS_AT_COMMIT - * @see #getHoldability - * @see DatabaseMetaData#getResultSetHoldability - * @see ResultSet - */ - @Override - public void setHoldability(final int holdability) { - // not handled - } - - /** - * Creates an unnamed savepoint in the current transaction and returns the new Savepoint - * * object that represents it. - * - *

if setSavepoint is invoked outside of an active transaction, a transaction will be started - * at this newly created savepoint. - * - * @return the new Savepoint object - * @throws SQLException if a database access error occurs, this method is called while - * participating in a distributed transaction, this method is called on a closed connection or - * this Connection object is currently in auto-commit mode - * @see Savepoint - * @since 1.4 - */ - public Savepoint setSavepoint() throws SQLException { - String randomName = UUID.randomUUID().toString(); - return setSavepoint(randomName); - } - - /** - * Creates a savepoint with the given name in the current transaction and returns the new - * Savepoint object that represents it. if setSavepoint is invoked outside of an active - * transaction, a transaction will be started at this newly created savepoint. - * - * @param name a String containing the name of the savepoint - * @return the new Savepoint object - * @throws SQLException if a database access error occurs, this method is called while - * participating in a distributed transaction, this method is called on a closed connection or - * this Connection object is currently in auto-commit mode - * @see Savepoint - * @since 1.4 - */ - public Savepoint setSavepoint(final String name) throws SQLException { - Savepoint savepoint = new MariaDbSavepoint(name); - try (Statement st = createStatement()) { - st.execute("SAVEPOINT `" + savepoint.getSavepointName() + "`"); - } - return savepoint; - } - - /** - * Removes the specified Savepoint and subsequent Savepoint objects from - * the current transaction. Any reference to the savepoint after it have been removed will cause - * an SQLException to be thrown. - * - * @param savepoint the Savepoint object to be removed - * @throws SQLException if a database access error occurs, this method is called on a closed - * connection or the given Savepoint object is not a valid savepoint in the - * current transaction - */ - public void releaseSavepoint(final Savepoint savepoint) throws SQLException { - try (Statement st = createStatement()) { - st.execute("RELEASE SAVEPOINT `" + savepoint.getSavepointName() + "`"); - } - } - - /** - * Constructs an object that implements the Clob interface. The object returned - * initially contains no data. The setAsciiStream, setCharacterStream - * and setString methods of the Clob interface may be used to add data - * to the Clob. - * - * @return An object that implements the Clob interface - */ - public Clob createClob() { - return new MariaDbClob(); - } - - /** - * Constructs an object that implements the Blob interface. The object returned - * initially contains no data. The setBinaryStream and setBytes methods - * of the Blob interface may be used to add data to the Blob. - * - * @return An object that implements the Blob interface - */ - public Blob createBlob() { - return new MariaDbBlob(); - } - - /** - * Constructs an object that implements the NClob interface. The object returned - * initially contains no data. The setAsciiStream, setCharacterStream - * and setString methods of the NClob interface may be used to add data - * to the NClob. - * - * @return An object that implements the NClob interface - */ - public NClob createNClob() { - return new MariaDbClob(); - } - - /** - * Constructs an object that implements the SQLXML interface. The object returned - * initially contains no data. The createXmlStreamWriter object and setString - * method of the SQLXML interface may be used to add data to the SQLXML - * object. - * - * @return An object that implements the SQLXML interface - * @throws SQLException if an object that implements the SQLXML interface can not be - * constructed, this method is called on a closed connection or a database access error - * occurs. - */ - @Override - public SQLXML createSQLXML() throws SQLException { - throw exceptionFactory.notSupported("SQLXML type is not supported"); - } - - /** - * Returns true if the connection has not been closed and is still valid. The driver shall submit - * a query on the connection or use some other mechanism that positively verifies the connection - * is still valid when this method is called. - * - *

The query submitted by the driver to validate the connection shall be executed in the - * context of the current transaction. - * - * @param timeout - The time in seconds to wait for the database operation used to validate the - * connection to complete. If the timeout period expires before the operation completes, this - * method returns false. A value of 0 indicates a timeout is not applied to the database - * operation. - * @return true if the connection is valid, false otherwise - * @throws SQLException if the value supplied for timeout is less then 0 - * @see DatabaseMetaData#getClientInfoProperties - * @since 1.6 - */ - public boolean isValid(final int timeout) throws SQLException { - if (timeout < 0) { - throw new SQLException("the value supplied for timeout is negative"); - } - if (isClosed()) { - return false; - } - - try { - return protocol.isValid(timeout * 1000); - } catch (SQLException e) { - // eat - return false; - } - } - - private void checkClientClose(final String name) throws SQLClientInfoException { - if (protocol.isExplicitClosed()) { - Map failures = new HashMap<>(); - failures.put(name, ClientInfoStatus.REASON_UNKNOWN); - throw new SQLClientInfoException("setClientInfo() is called on closed connection", failures); - } - } - - private void checkClientReconnect(final String name) throws SQLClientInfoException { - if (protocol.isClosed() && protocol.getProxy() != null) { - lock.lock(); - try { - protocol.getProxy().reconnect(); - } catch (SQLException sqle) { - Map failures = new HashMap<>(); - failures.put(name, ClientInfoStatus.REASON_UNKNOWN); - throw new SQLClientInfoException("Connection closed", failures, sqle); - } finally { - lock.unlock(); - } - } - } - - private void checkClientValidProperty(final String name) throws SQLClientInfoException { - if (name == null - || (!"ApplicationName".equals(name) - && !"ClientUser".equals(name) - && !"ClientHostname".equals(name))) { - Map failures = new HashMap<>(); - failures.put(name, ClientInfoStatus.REASON_UNKNOWN_PROPERTY); - throw new SQLClientInfoException( - "setClientInfo() parameters can only be \"ApplicationName\",\"ClientUser\" or \"ClientHostname\", " - + "but was : " - + name, - failures); - } - } - - private String buildClientQuery(final String name, final String value) { - StringBuilder escapeQuery = new StringBuilder("SET @").append(name).append("="); - if (value == null) { - escapeQuery.append("null"); - } else { - escapeQuery.append("'"); - int charsOffset = 0; - int charsLength = value.length(); - char charValue; - if (protocol.noBackslashEscapes()) { - while (charsOffset < charsLength) { - charValue = value.charAt(charsOffset); - if (charValue == '\'') { - escapeQuery.append('\''); // add a single escape quote - } - escapeQuery.append(charValue); - charsOffset++; - } - } else { - while (charsOffset < charsLength) { - charValue = value.charAt(charsOffset); - if (charValue == '\'' || charValue == '\\' || charValue == '"' || charValue == 0) { - escapeQuery.append('\\'); // add escape slash - } - escapeQuery.append(charValue); - charsOffset++; - } - } - escapeQuery.append("'"); - } - return escapeQuery.toString(); - } - - /** - * Sets the value of the client info property specified by name to the value specified by value. - * - *

Applications may use the DatabaseMetaData.getClientInfoProperties method to - * determine the client info properties supported by the driver and the maximum length that may be - * specified for each property. - * - *

The driver stores the value specified in a suitable location in the database. For example in - * a special register, session parameter, or system table column. For efficiency the driver may - * defer setting the value in the database until the next time a statement is executed or - * prepared. Other than storing the client information in the appropriate place in the database, - * these methods shall not alter the behavior of the connection in anyway. The values supplied to - * these methods are used for accounting, diagnostics and debugging purposes only. - * - *

The driver shall generate a warning if the client info name specified is not recognized by - * the driver. - * - *

If the value specified to this method is greater than the maximum length for the property - * the driver may either truncate the value and generate a warning or generate a - * SQLClientInfoException. If the driver generates a SQLClientInfoException, - * the value specified was not set on the connection. - * - *

The following are standard client info properties. Drivers are not required to support these - * properties however if the driver supports a client info property that can be described by one - * of the standard properties, the standard property name should be used. - * - *

    - *
  • ApplicationName - The name of the application currently utilizing the connection - *
  • ClientUser - The name of the user that the application using the connection is performing - * work for. This may not be the same as the user name that was used in establishing the - * connection. - *
  • ClientHostname - The hostname of the computer the application using the connection is - * running on. - *
- * - * @param name The name of the client info property to set - * @param value The value to set the client info property to. If the value is null, the current - * value of the specified property is cleared. - * @throws SQLClientInfoException if the database server returns an error while setting the client - * info value on the database server or this method is called on a closed connection - * @since 1.6 - */ - public void setClientInfo(final String name, final String value) throws SQLClientInfoException { - checkClientClose(name); - checkClientReconnect(name); - checkClientValidProperty(name); - - try { - Statement statement = createStatement(); - statement.execute(buildClientQuery(name, value)); - } catch (SQLException sqle) { - Map failures = new HashMap<>(); - failures.put(name, ClientInfoStatus.REASON_UNKNOWN); - throw new SQLClientInfoException("unexpected error during setClientInfo", failures, sqle); - } - } - - /** - * Returns a list containing the name and current value of each client info property supported by - * the driver. The value of a client info property may be null if the property has not been set - * and does not have a default value. - * - * @return A Properties object that contains the name and current value of each of - * the client info properties supported by the driver. - * @throws SQLException if the database server returns an error when fetching the client info - * values from the database or this method is called on a closed connection - */ - public Properties getClientInfo() throws SQLException { - checkConnection(); - Properties properties = new Properties(); - try (Statement statement = createStatement()) { - try (ResultSet rs = - statement.executeQuery("SELECT @ApplicationName, @ClientUser, @ClientHostname")) { - if (rs.next()) { - if (rs.getString(1) != null) { - properties.setProperty("ApplicationName", rs.getString(1)); - } - if (rs.getString(2) != null) { - properties.setProperty("ClientUser", rs.getString(2)); - } - if (rs.getString(3) != null) { - properties.setProperty("ClientHostname", rs.getString(3)); - } - return properties; - } - } - } - properties.setProperty("ApplicationName", null); - properties.setProperty("ClientUser", null); - properties.setProperty("ClientHostname", null); - return properties; - } - - /** - * Sets the value of the connection's client info properties. The Properties object - * contains the names and values of the client info properties to be set. The set of client info - * properties contained in the properties list replaces the current set of client info properties - * on the connection. If a property that is currently set on the connection is not present in the - * properties list, that property is cleared. Specifying an empty properties list will clear all - * of the properties on the connection. See setClientInfo (String, String) for more - * information. - * - *

If an error occurs in setting any of the client info properties, a - * SQLClientInfoException is thrown. The SQLClientInfoException contains - * information indicating which client info properties were not set. The state of the client - * information is unknown because some databases do not allow multiple client info properties to - * be set atomically. For those databases, one or more properties may have been set before the - * error occurred. - * - * @param properties the list of client info properties to set - * @throws SQLClientInfoException if the database server returns an error while setting the - * clientInfo values on the database server or this method is called on a closed connection - * @see Connection#setClientInfo(String, String) setClientInfo(String, String) - * @since 1.6 - */ - public void setClientInfo(final Properties properties) throws SQLClientInfoException { - Map propertiesExceptions = new HashMap<>(); - for (String name : new String[] {"ApplicationName", "ClientUser", "ClientHostname"}) { - try { - setClientInfo(name, properties.getProperty(name)); - } catch (SQLClientInfoException e) { - propertiesExceptions.putAll(e.getFailedProperties()); - } - } - - if (!propertiesExceptions.isEmpty()) { - String errorMsg = - "setClientInfo errors : the following properties where not set : " - + propertiesExceptions.keySet(); - throw new SQLClientInfoException(errorMsg, propertiesExceptions); - } - } - - /** - * Returns the value of the client info property specified by name. This method may return null if - * the specified client info property has not been set and does not have a default value. This - * method will also return null if the specified client info property name is not supported by the - * driver. Applications may use the DatabaseMetaData.getClientInfoProperties method - * to determine the client info properties supported by the driver. - * - * @param name The name of the client info property to retrieve - * @return The value of the client info property specified - * @throws SQLException if the database server returns an error when fetching the client info - * value from the database or this method is called on a closed connection - * @see DatabaseMetaData#getClientInfoProperties - * @since 1.6 - */ - public String getClientInfo(final String name) throws SQLException { - checkConnection(); - if (!"ApplicationName".equals(name) - && !"ClientUser".equals(name) - && !"ClientHostname".equals(name)) { - throw new SQLException( - "name must be \"ApplicationName\", \"ClientUser\" or \"ClientHostname\", but was \"" - + name - + "\""); - } - try (Statement statement = createStatement()) { - try (ResultSet rs = statement.executeQuery("SELECT @" + name)) { - if (rs.next()) { - return rs.getString(1); - } - } - } - return null; - } - - /** - * Factory method for creating Array objects. Note: When createArrayOf is used - * to create an array object that maps to a primitive data type, then it is implementation-defined - * whether the Array object is an array of that primitive data type or an array of - * Object. Note: The JDBC driver is responsible for mapping the elements - * Object array to the default JDBC SQL type defined in java.sql.Types for the given - * class of Object. The default mapping is specified in Appendix B of the JDBC - * specification. If the resulting JDBC type is not the appropriate type for the given typeName - * then it is implementation defined whether an SQLException is thrown or the driver - * supports the resulting conversion. - * - * @param typeName the SQL name of the type the elements of the array map to. The typeName is a - * database-specific name which may be the name of a built-in type, a user-defined type or a - * standard SQL type supported by this database. This is the value returned by - * Array.getBaseTypeName - * @param elements the elements that populate the returned object - * @return an Array object whose elements map to the specified SQL type - * @throws SQLException if a database error occurs, the JDBC type is not appropriate for the - * typeName and the conversion is not supported, the typeName is null or this method is called - * on a closed connection - */ - public Array createArrayOf(final String typeName, final Object[] elements) throws SQLException { - throw exceptionFactory.notSupported("Array type is not supported"); - } - - /** - * Factory method for creating Struct objects. - * - * @param typeName the SQL type name of the SQL structured type that this Struct - * object maps to. The typeName is the name of a user-defined type that has been defined for - * this database. It is the value returned by Struct.getSQLTypeName. - * @param attributes the attributes that populate the returned object - * @return a Struct object that maps to the given SQL type and is populated with the given - * attributes - * @throws SQLException if a database error occurs, the typeName is null or this method is called - * on a closed connection - */ - public Struct createStruct(final String typeName, final Object[] attributes) throws SQLException { - throw exceptionFactory.notSupported("Struct type is not supported"); - } - - /** - * Returns an object that implements the given interface to allow access to non-standard methods, - * or standard methods not exposed by the proxy. If the receiver implements the interface then the - * result is the receiver or a proxy for the receiver. If the receiver is a wrapper and the - * wrapped object implements the interface then the result is the wrapped object or a proxy for - * the wrapped object. Otherwise return the the result of calling unwrap recursively - * on the wrapped object or a proxy for that result. If the receiver is not a wrapper and does not - * implement the interface, then an SQLException is thrown. - * - * @param iface A Class defining an interface that the result must implement. - * @return an object that implements the interface. May be a proxy for the actual implementing - * object. - * @throws SQLException If no object found that implements the interface - * @since 1.6 - */ - public T unwrap(final Class iface) throws SQLException { - try { - if (isWrapperFor(iface)) { - return iface.cast(this); - } else { - throw new SQLException("The receiver is not a wrapper for " + iface.getName()); - } - } catch (Exception e) { - throw new SQLException("The receiver is not a wrapper and does not implement the interface"); - } - } - - /** - * Returns true if this either implements the interface argument or is directly or indirectly a - * wrapper for an object that does. Returns false otherwise. If this implements the interface then - * return true, else if this is a wrapper then return the result of recursively calling - * isWrapperFor on the wrapped object. If this does not implement the interface and is not - * a wrapper, return false. This method should be implemented as a low-cost operation compared to - * unwrap so that callers can use this method to avoid expensive unwrap - * calls that may fail. If this method returns true then calling unwrap with the same - * argument should succeed. - * - * @param iface a Class defining an interface. - * @return true if this implements the interface or directly or indirectly wraps an object that - * does. - * @since 1.6 - */ - public boolean isWrapperFor(final Class iface) { - return iface.isInstance(this); - } - - /** - * returns the username for the connection. - * - * @return the username. - */ - @Deprecated - public String getUsername() { - return protocol.getUsername(); - } - - /** - * returns the hostname for the connection. - * - * @return the hostname. - */ - @Deprecated - public String getHostname() { - return protocol.getHost(); - } - - /** - * returns the port for the connection. - * - * @return the port - */ - @Deprecated - public int getPort() { - return protocol.getPort(); - } - - protected boolean getPinGlobalTxToPhysicalConnection() { - return protocol.getPinGlobalTxToPhysicalConnection(); - } - - /** If failover is not activated, will close connection when a connection error occur. */ - public void setHostFailed() { - if (protocol.getProxy() == null) { - protocol.setHostFailedWithoutProxy(); - } - } - - /** - * Are table case sensitive or not . Default Value: 0 (Unix), 1 (Windows), 2 (Mac OS X). If set to - * 0 (the default on Unix-based systems), table names and aliases and database names are compared - * in a case-sensitive manner. If set to 1 (the default on Windows), names are stored in lowercase - * and not compared in a case-sensitive manner. If set to 2 (the default on Mac OS X), names are - * stored as declared, but compared in lowercase. - * - * @return int value. - * @throws SQLException if a connection error occur - */ - public int getLowercaseTableNames() throws SQLException { - if (lowercaseTableNames == -1) { - try (Statement st = createStatement()) { - try (ResultSet rs = st.executeQuery("select @@lower_case_table_names")) { - rs.next(); - lowercaseTableNames = rs.getInt(1); - } - } - } - return lowercaseTableNames; - } - - /** - * Abort connection. - * - * @param executor executor - * @throws SQLException if security manager doesn't permit it. - */ - public void abort(Executor executor) throws SQLException { - if (this.isClosed()) { - return; - } - - SQLPermission sqlPermission = new SQLPermission("callAbort"); - SecurityManager securityManager = System.getSecurityManager(); - if (securityManager != null) { - securityManager.checkPermission(sqlPermission); - } - if (executor == null) { - throw exceptionFactory.create("Cannot abort the connection: null executor passed"); - } - - executor.execute(protocol::abort); - } - - /** - * Get network timeout. - * - * @return timeout - * @throws SQLException if database socket error occur - */ - public int getNetworkTimeout() throws SQLException { - return this.protocol.getTimeout(); - } - - public String getSchema() { - // We support only catalog - return null; - } - - public void setSchema(String arg0) { - // We support only catalog, and JDBC indicate "If the driver does not support schemas, it will - // silently ignore this request." - } - - /** - * Change network timeout. - * - * @param executor executor (can be null) - * @param milliseconds network timeout in milliseconds. - * @throws SQLException if security manager doesn't permit it. - */ - public void setNetworkTimeout(Executor executor, final int milliseconds) throws SQLException { - if (this.isClosed()) { - throw exceptionFactory.create( - "Connection.setNetworkTimeout cannot be called on a closed connection"); - } - if (milliseconds < 0) { - throw exceptionFactory.create( - "Connection.setNetworkTimeout cannot be called with a negative timeout"); - } - SQLPermission sqlPermission = new SQLPermission("setNetworkTimeout"); - SecurityManager securityManager = System.getSecurityManager(); - if (securityManager != null) { - securityManager.checkPermission(sqlPermission); - } - try { - stateFlag |= ConnectionState.STATE_NETWORK_TIMEOUT; - protocol.setTimeout(milliseconds); - } catch (SocketException se) { - throw exceptionFactory.create("Cannot set the network timeout", se); - } - } - - public long getServerThreadId() { - return protocol.getServerThreadId(); - } - - public boolean canUseServerTimeout() { - return canUseServerTimeout; - } - - public void setDefaultTransactionIsolation(int defaultTransactionIsolation) { - this.defaultTransactionIsolation = defaultTransactionIsolation; - } - - /** - * Reset connection set has it was after creating a "fresh" new connection. - * defaultTransactionIsolation must have been initialized. - * - *

BUT : - session variable state are reset only if option useResetConnection is set and - if - * using the option "useServerPrepStmts", PREPARE statement are still prepared - * - * @throws SQLException if resetting operation failed - */ - public void reset() throws SQLException { - // COM_RESET_CONNECTION exist since mysql 5.7.3 and mariadb 10.2.4 - // but not possible to use it with mysql waiting for https://bugs.mysql.com/bug.php?id=97633 - // correction. - // and mariadb only since https://jira.mariadb.org/browse/MDEV-18281 - boolean useComReset = - options.useResetConnection - && protocol.isServerMariaDb() - && (protocol.versionGreaterOrEqual(10, 3, 13) - || (protocol.getMajorServerVersion() == 10 - && protocol.getMinorServerVersion() == 2 - && protocol.versionGreaterOrEqual(10, 2, 22))); - - if (useComReset) { - protocol.reset(); - } - - if (stateFlag != 0) { - - try { - - if ((stateFlag & ConnectionState.STATE_NETWORK_TIMEOUT) != 0) { - setNetworkTimeout(null, options.socketTimeout); - } - - if ((stateFlag & ConnectionState.STATE_AUTOCOMMIT) != 0) { - setAutoCommit(options.autocommit); - } - - if ((stateFlag & ConnectionState.STATE_DATABASE) != 0) { - protocol.resetDatabase(); - } - - if ((stateFlag & ConnectionState.STATE_READ_ONLY) != 0) { - setReadOnly(false); // default to master connection - } - - // COM_RESET_CONNECTION reset transaction isolation - if (!useComReset && (stateFlag & ConnectionState.STATE_TRANSACTION_ISOLATION) != 0) { - setTransactionIsolation(defaultTransactionIsolation); - } - - stateFlag = 0; - - } catch (SQLException sqle) { - throw exceptionFactory.create("error resetting connection"); - } - } - - warningsCleared = true; - } - - public boolean includeDeadLockInfo() { - return options.includeInnodbStatusInDeadlockExceptions; - } - - public boolean includeThreadsTraces() { - return options.includeThreadDumpInDeadlockExceptions; - } -} diff --git a/src/main/java/org/mariadb/jdbc/MariaDbDataSource.java b/src/main/java/org/mariadb/jdbc/MariaDbDataSource.java index a430d87c2..744d60193 100644 --- a/src/main/java/org/mariadb/jdbc/MariaDbDataSource.java +++ b/src/main/java/org/mariadb/jdbc/MariaDbDataSource.java @@ -1,379 +1,60 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - package org.mariadb.jdbc; import java.io.PrintWriter; +import java.sql.*; import java.sql.Connection; -import java.sql.SQLException; -import java.util.Collections; -import java.util.Properties; import java.util.logging.Logger; -import javax.sql.*; -import org.mariadb.jdbc.internal.util.constant.HaMode; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; -import org.mariadb.jdbc.util.DefaultOptions; -import org.mariadb.jdbc.util.Options; - -public class MariaDbDataSource implements DataSource, ConnectionPoolDataSource, XADataSource { - - private UrlParser urlParser; - - private String hostname; - private Integer port = 3306; - private Integer connectTimeoutInMs; - private String database; - private String url; - private String user; - private String password; - private String properties; - - /** - * Constructor. - * - * @param hostname hostname (ipv4, ipv6, dns name) - * @param port server port - * @param database database name - */ - public MariaDbDataSource(String hostname, int port, String database) { - this.hostname = hostname; - this.port = port; - this.database = database; - } +import javax.sql.DataSource; - public MariaDbDataSource(String url) { - this.url = url; - } +public class MariaDbDataSource implements DataSource { - /** Default constructor. hostname will be localhost, port 3306. */ - public MariaDbDataSource() {} + private final Configuration conf; - /** - * Gets the name of the database. - * - * @return the name of the database for this data source - */ - public String getDatabaseName() { - if (database != null) { - return database; - } - return (urlParser != null && urlParser.getDatabase() != null) ? urlParser.getDatabase() : ""; - } - - /** - * Sets the database name. - * - * @param database the name of the database - * @throws SQLException if connection information are erroneous - */ - public void setDatabaseName(String database) throws SQLException { - this.database = database; - reInitializeIfNeeded(); - } - - /** - * Gets the username. - * - * @return the username to use when connecting to the database - */ - public String getUser() { - if (user != null) { - return user; - } - return urlParser != null ? urlParser.getUsername() : null; - } - - /** - * Sets the username. - * - * @param user the username - * @throws SQLException if connection information are erroneous - */ - public void setUser(String user) throws SQLException { - this.user = user; - reInitializeIfNeeded(); - } - - /** - * Gets the username. - * - * @return the username to use when connecting to the database - */ - public String getUserName() { - return getUser(); - } - - /** - * Sets the username. - * - * @param userName the username - * @throws SQLException if connection information are erroneous - */ - public void setUserName(String userName) throws SQLException { - setUser(userName); - } - - /** - * Sets the password. - * - * @param password the password - * @throws SQLException if connection information are erroneous - */ - public void setPassword(String password) throws SQLException { - this.password = password; - reInitializeIfNeeded(); - } - - /** - * Returns the port number. - * - * @return the port number - */ - public int getPort() { - if (port != 0) { - return port; - } - return urlParser != null ? urlParser.getHostAddresses().get(0).port : 3306; - } - - /** - * Sets the database port. - * - * @param port the port - * @throws SQLException if connection information are erroneous - */ - public void setPort(int port) throws SQLException { - this.port = port; - reInitializeIfNeeded(); - } - - /** - * Returns the port number. - * - * @return the port number - */ - public int getPortNumber() { - return getPort(); - } - - /** - * Sets the port number. - * - * @param port the port - * @throws SQLException if connection information are erroneous - * @see #setPort - */ - public void setPortNumber(int port) throws SQLException { - if (port > 0) { - setPort(port); - } - } - - @Deprecated - public void setProperties(String properties) throws SQLException { - this.properties = properties; - reInitializeIfNeeded(); - } - - /** - * Sets the connection string URL. - * - * @param url the connection string - * @throws SQLException if error in URL - */ - public void setUrl(String url) throws SQLException { - this.url = url; - reInitializeIfNeeded(); - } - - /** - * Returns the name of the database server. - * - * @return the name of the database server - */ - public String getServerName() { - if (hostname != null) { - return hostname; + public MariaDbDataSource(String url) throws SQLException { + if (Configuration.acceptsUrl(url)) { + conf = Configuration.parse(url); + } else { + throw new SQLException(String.format("Wrong mariaDB url :", url)); } - boolean hasHost = urlParser != null && this.urlParser.getHostAddresses().get(0).host != null; - return (hasHost) ? this.urlParser.getHostAddresses().get(0).host : "localhost"; } /** - * Sets the server name. - * - * @param serverName the server name - * @throws SQLException if connection information are erroneous - */ - public void setServerName(String serverName) throws SQLException { - hostname = serverName; - reInitializeIfNeeded(); - } - - /** - * Attempts to establish a connection with the data source that this DataSource - * object represents. + * Attempts to establish a connection with the data source that this {@code DataSource} object + * represents. * * @return a connection to the data source * @throws SQLException if a database access error occurs + * @throws SQLTimeoutException when the driver has determined that the timeout value specified by + * the {@code setLoginTimeout} method has been exceeded and has at least tried to cancel the + * current database connection attempt */ + @Override public Connection getConnection() throws SQLException { - try { - if (urlParser == null) { - initialize(); - } - - return MariaDbConnection.newConnection(urlParser, null); - } catch (SQLException e) { - throw ExceptionFactory.INSTANCE.create(e); - } + return Driver.connect(conf); } /** - * Attempts to establish a connection with the data source that this DataSource - * object represents. + * Attempts to establish a connection with the data source that this {@code DataSource} object + * represents. * * @param username the database user on whose behalf the connection is being made * @param password the user's password * @return a connection to the data source * @throws SQLException if a database access error occurs + * @throws SQLTimeoutException when the driver has determined that the timeout value specified by + * the {@code setLoginTimeout} method has been exceeded and has at least tried to cancel the + * current database connection attempt */ - public Connection getConnection(final String username, final String password) - throws SQLException { + @Override + public Connection getConnection(String username, String password) throws SQLException { try { - if (urlParser == null) { - this.user = username; - this.password = password; - initialize(); - } - - UrlParser urlParser = (UrlParser) this.urlParser.clone(); - urlParser.setUsername(username); - urlParser.setPassword(password); - return MariaDbConnection.newConnection(urlParser, null); - - } catch (SQLException e) { - throw ExceptionFactory.INSTANCE.create(e); - } catch (CloneNotSupportedException cloneException) { - throw ExceptionFactory.INSTANCE.create("Error in configuration"); + Configuration conf = this.conf.clone(username, password); + return Driver.connect(conf); + } catch (CloneNotSupportedException cloneNotSupportedException) { + throw new SQLException(cloneNotSupportedException); } } - /** - * Retrieves the log writer for this DataSource object. - * - *

The log writer is a character output stream to which all logging and tracing messages for - * this data source will be printed. This includes messages printed by the methods of this object, - * messages printed by methods of other objects manufactured by this object, and so on. Messages - * printed to a data source specific log writer are not printed to the log writer associated with - * the java.sql.DriverManager class. When a DataSource object is - * created, the log writer is initially null; in other words, the default is for logging to be - * disabled. - * - * @return the log writer for this data source or null if logging is disabled - * @see #setLogWriter - */ - public PrintWriter getLogWriter() { - return null; - } - - /** - * Sets the log writer for this DataSource object to the given - * java.io.PrintWriter object. - * - *

The log writer is a character output stream to which all logging and tracing messages for - * this data source will be printed. This includes messages printed by the methods of this object, - * messages printed by methods of other objects manufactured by this object, and so on. Messages - * printed to a data source- specific log writer are not printed to the log writer associated with - * the java.sql.DriverManager class. When a DataSource object is created - * the log writer is initially null; in other words, the default is for logging to be disabled. - * - * @param out the new log writer; to disable logging, set to null - * @see #getLogWriter - */ - public void setLogWriter(final PrintWriter out) { - // not implemented - } - - /** - * Gets the maximum time in seconds that this data source can wait while attempting to connect to - * a database. A value of zero means that the timeout is the default system timeout if there is - * one; otherwise, it means that there is no timeout. When a DataSource object is - * created, the login timeout is initially zero. - * - * @return the data source login time limit - * @see #setLoginTimeout - */ - public int getLoginTimeout() { - if (connectTimeoutInMs != null) { - return connectTimeoutInMs / 1000; - } - return (urlParser != null) ? urlParser.getOptions().connectTimeout / 1000 : 30; - } - - /** - * Sets the maximum time in seconds that this data source will wait while attempting to connect to - * a database. A value of zero specifies that the timeout is the default system timeout if there - * is one; otherwise, it specifies that there is no timeout. When a DataSource object - * is created, the login timeout is initially zero. - * - * @param seconds the data source login time limit - * @see #getLoginTimeout - */ - @Override - public void setLoginTimeout(final int seconds) { - connectTimeoutInMs = seconds * 1000; - } - /** * Returns an object that implements the given interface to allow access to non-standard methods, * or standard methods not exposed by the proxy. @@ -389,19 +70,13 @@ public void setLoginTimeout(final int seconds) { * @return an object that implements the interface. May be a proxy for the actual implementing * object. * @throws SQLException If no object found that implements the interface - * @since 1.6 */ - public T unwrap(final Class iface) throws SQLException { - try { - if (isWrapperFor(iface)) { - return iface.cast(this); - } else { - throw new SQLException( - "The receiver is not a wrapper and does not implement the interface"); - } - } catch (Exception e) { - throw new SQLException("The receiver is not a wrapper and does not implement the interface"); + @Override + public T unwrap(Class iface) throws SQLException { + if (isWrapperFor(iface)) { + return iface.cast(this); } + throw new SQLException("Datasource is not a wrapper for " + iface.getName()); } /** @@ -414,107 +89,76 @@ public T unwrap(final Class iface) throws SQLException { * calls that may fail. If this method returns true then calling unwrap with the same * argument should succeed. * - * @param interfaceOrWrapper a Class defining an interface. + * @param iface a Class defining an interface. * @return true if this implements the interface or directly or indirectly wraps an object that * does. * @throws SQLException if an error occurs while determining whether this is a wrapper for an * object with the given interface. */ - public boolean isWrapperFor(final Class interfaceOrWrapper) throws SQLException { - return interfaceOrWrapper.isInstance(this); + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return iface.isInstance(this); } /** - * Attempts to establish a physical database connection that can be used as a pooled connection. + * Implementation doesn't use logwriter * - * @return a PooledConnection object that is a physical connection to the database - * that this ConnectionPoolDataSource object represents + * @return the log writer for this data source or null if logging is disabled * @throws SQLException if a database access error occurs + * @see #setLogWriter */ - public PooledConnection getPooledConnection() throws SQLException { - return new MariaDbPooledConnection((MariaDbConnection) getConnection()); + @Override + public PrintWriter getLogWriter() throws SQLException { + return null; } /** - * Attempts to establish a physical database connection that can be used as a pooled connection. + * Implementation doesn't use logwriter * - * @param user the database user on whose behalf the connection is being made - * @param password the user's password - * @return a PooledConnection object that is a physical connection to the database - * that this ConnectionPoolDataSource object represents + * @param out the new log writer; to disable logging, set to null * @throws SQLException if a database access error occurs + * @see #getLogWriter */ - public PooledConnection getPooledConnection(String user, String password) throws SQLException { - return new MariaDbPooledConnection((MariaDbConnection) getConnection(user, password)); - } - @Override - public XAConnection getXAConnection() throws SQLException { - return new MariaXaConnection((MariaDbConnection) getConnection()); - } + public void setLogWriter(PrintWriter out) throws SQLException {} + /** + * Gets the maximum time in seconds that this data source can wait while attempting to connect to + * a database. A value of zero means that the timeout is the default system timeout if there is + * one; otherwise, it means that there is no timeout. When a DataSource object is + * created, the login timeout is initially to 30s. + * + * @return the data source login time limit + * @see #setLoginTimeout + */ @Override - public XAConnection getXAConnection(String user, String password) throws SQLException { - return new MariaXaConnection((MariaDbConnection) getConnection(user, password)); - } - - public Logger getParentLogger() { - return null; + public int getLoginTimeout() { + return conf.connectTimeout() / 1000; } /** - * For testing purpose only. + * Sets the maximum time in seconds that this data source will wait while attempting to connect to + * a database. A value of zero specifies that the timeout is the default system timeout if there + * is one; otherwise, it specifies that there is no timeout. When a DataSource object + * is created, the login timeout is initially 30s. * - * @return current url parser. + * @param seconds the data source login time limit + * @throws SQLException if a database access error occurs. + * @see #getLoginTimeout + * @since 1.4 */ - protected UrlParser getUrlParser() { - return urlParser; - } - - private void reInitializeIfNeeded() throws SQLException { - if (urlParser != null) { - initialize(); - } + @Override + public void setLoginTimeout(int seconds) throws SQLException { + conf.connectTimeout(seconds * 1000); } - protected synchronized void initialize() throws SQLException { - if (url != null && !url.isEmpty()) { - Properties props = new Properties(); - if (user != null) { - props.setProperty("user", user); - } - if (password != null) { - props.setProperty("password", password); - } - if (database != null) { - props.setProperty("database", database); - } - if (connectTimeoutInMs != null) { - props.setProperty("connectTimeout", String.valueOf(connectTimeoutInMs)); - } - - urlParser = UrlParser.parse(url, props); - - } else { - Options options = DefaultOptions.defaultValues(HaMode.NONE); - options.user = user; - options.password = password; - - urlParser = - new UrlParser( - database, - Collections.singletonList( - new HostAddress( - (hostname == null || hostname.isEmpty()) ? "localhost" : hostname, - port == null ? 3306 : port)), - options, - HaMode.NONE); - if (properties != null) { - urlParser.setProperties(properties); - } - if (connectTimeoutInMs != null) { - urlParser.getOptions().connectTimeout = connectTimeoutInMs; - } - } + /** + * Not implemented + * + * @return the parent Logger for this data source + */ + @Override + public Logger getParentLogger() { + return null; } } diff --git a/src/main/java/org/mariadb/jdbc/MariaDbFunctionStatement.java b/src/main/java/org/mariadb/jdbc/MariaDbFunctionStatement.java deleted file mode 100644 index 9c382b5f7..000000000 --- a/src/main/java/org/mariadb/jdbc/MariaDbFunctionStatement.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.sql.ResultSet; -import java.sql.SQLException; -import org.mariadb.jdbc.internal.com.read.resultset.SelectResultSet; -import org.mariadb.jdbc.internal.com.send.parameters.ParameterHolder; -import org.mariadb.jdbc.internal.util.dao.CloneableCallableStatement; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; - -public class MariaDbFunctionStatement extends CallableFunctionStatement - implements CloneableCallableStatement { - - private SelectResultSet outputResultSet = null; - - /** - * Specific implementation of CallableStatement to handle function call, represent by call like - * {?= call procedure-name[(arg1,arg2, ...)]}. - * - * @param connection current connection - * @param databaseName database name - * @param procedureName function name - * @param arguments function args - * @param resultSetType a result set type; one of ResultSet.TYPE_FORWARD_ONLY, - * ResultSet.TYPE_SCROLL_INSENSITIVE, or ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of ResultSet.CONCUR_READ_ONLY - * or ResultSet.CONCUR_UPDATABLE - * @param exceptionFactory Exception factory - * @throws SQLException exception - */ - public MariaDbFunctionStatement( - MariaDbConnection connection, - String databaseName, - String procedureName, - String arguments, - int resultSetType, - final int resultSetConcurrency, - ExceptionFactory exceptionFactory) - throws SQLException { - super( - connection, - "SELECT " + procedureName + ((arguments == null) ? "()" : arguments), - resultSetType, - resultSetConcurrency, - exceptionFactory); - parameterMetadata = - new CallableParameterMetaData(connection, databaseName, procedureName, true); - super.initFunctionData(getParameterCount() + 1); - } - - protected SelectResultSet getResult() throws SQLException { - if (outputResultSet == null) { - throw new SQLException("No output result"); - } - return outputResultSet; - } - - /** - * Clone statement. - * - * @param connection connection - * @return Clone statement. - * @throws CloneNotSupportedException if any error occur. - */ - public MariaDbFunctionStatement clone(MariaDbConnection connection) - throws CloneNotSupportedException { - MariaDbFunctionStatement clone = (MariaDbFunctionStatement) super.clone(connection); - clone.outputResultSet = null; - return clone; - } - - /** - * Executes the CALL statement. - * - * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0 - * for SQL statements that return nothing - * @throws SQLException if a database access error occurs; this method is called on a closed - * PreparedStatement or the SQL statement returns a ResultSet object - */ - @Override - public int executeUpdate() throws SQLException { - connection.lock.lock(); - try { - super.execute(); - retrieveOutputResult(); - if (results != null && results.getResultSet() == null) { - return 0; - } - return getUpdateCount(); - } finally { - connection.lock.unlock(); - } - } - - private void retrieveOutputResult() throws SQLException { - outputResultSet = results.getResultSet(); - if (outputResultSet != null) { - outputResultSet.next(); - } - } - - public void setParameter(final int parameterIndex, final ParameterHolder holder) - throws SQLException { - super.setParameter(parameterIndex - 1, holder); - } - - @Override - public ResultSet executeQuery() throws SQLException { - connection.lock.lock(); - try { - super.execute(); - retrieveOutputResult(); - if (results != null && results.getResultSet() == null) { - return results.getResultSet(); - } - return SelectResultSet.createEmptyResultSet(); - } finally { - connection.lock.unlock(); - } - } - - @Override - public boolean execute() throws SQLException { - connection.lock.lock(); - try { - super.execute(); - retrieveOutputResult(); - return results != null && results.getResultSet() == null; - } finally { - connection.lock.unlock(); - } - } -} diff --git a/src/main/java/org/mariadb/jdbc/MariaDbParameterMetaData.java b/src/main/java/org/mariadb/jdbc/MariaDbParameterMetaData.java deleted file mode 100644 index e663eb854..000000000 --- a/src/main/java/org/mariadb/jdbc/MariaDbParameterMetaData.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.sql.ParameterMetaData; -import java.sql.SQLException; -import org.mariadb.jdbc.internal.ColumnType; -import org.mariadb.jdbc.internal.com.read.resultset.ColumnDefinition; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; - -/** Very basic info about the parameterized query, only reliable method is getParameterCount(). */ -public class MariaDbParameterMetaData implements ParameterMetaData { - - private final ColumnDefinition[] parametersInformation; - - public MariaDbParameterMetaData(ColumnDefinition[] parametersInformation) { - this.parametersInformation = parametersInformation; - } - - private void checkAvailable() throws SQLException { - if (this.parametersInformation == null) { - throw new SQLException("Parameter metadata not available for these statement", "S1C00"); - } - } - - @Override - public int getParameterCount() throws SQLException { - checkAvailable(); - return parametersInformation.length; - } - - private ColumnDefinition getParameterInformation(int param) throws SQLException { - checkAvailable(); - if (param >= 1 && param <= parametersInformation.length) { - return parametersInformation[param - 1]; - } - throw new SQLException( - "Parameter metadata out of range : param was " - + param - + " and must be 1 <= param <=" - + parametersInformation.length, - "07009"); - } - - @Override - public int isNullable(final int param) throws SQLException { - if (getParameterInformation(param).isNotNull()) { - return ParameterMetaData.parameterNoNulls; - } else { - return ParameterMetaData.parameterNullable; - } - } - - @Override - public boolean isSigned(int param) throws SQLException { - return getParameterInformation(param).isSigned(); - } - - @Override - public int getPrecision(int param) throws SQLException { - long length = getParameterInformation(param).getLength(); - return (length > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) length; - } - - @Override - public int getScale(int param) throws SQLException { - if (ColumnType.isNumeric(getParameterInformation(param).getColumnType())) { - return getParameterInformation(param).getDecimals(); - } - return 0; - } - - /** - * Parameter type are not sent by server. See https://jira.mariadb.org/browse/CONJ-568 and - * https://jira.mariadb.org/browse/MDEV-15031 - * - * @param param parameter number - * @return SQL type from java.sql.Types - * @throws SQLException a feature not supported, since server doesn't sent the right information - */ - @Override - public int getParameterType(int param) throws SQLException { - throw ExceptionFactory.INSTANCE.notSupported( - "Getting parameter type metadata are not supported"); - } - - @Override - public String getParameterTypeName(int param) throws SQLException { - return getParameterInformation(param).getColumnType().getTypeName(); - } - - @Override - public String getParameterClassName(int param) throws SQLException { - return getParameterInformation(param).getColumnType().getClassName(); - } - - @Override - public int getParameterMode(int param) { - return parameterModeIn; - } - - @Override - public T unwrap(final Class iface) throws SQLException { - try { - if (isWrapperFor(iface)) { - return iface.cast(this); - } else { - throw new SQLException("The receiver is not a wrapper for " + iface.getName()); - } - } catch (Exception e) { - throw new SQLException("The receiver is not a wrapper and does not implement the interface"); - } - } - - public boolean isWrapperFor(final Class iface) throws SQLException { - return iface.isInstance(this); - } -} diff --git a/src/main/java/org/mariadb/jdbc/MariaDbPoolDataSource.java b/src/main/java/org/mariadb/jdbc/MariaDbPoolDataSource.java deleted file mode 100644 index b1479052c..000000000 --- a/src/main/java/org/mariadb/jdbc/MariaDbPoolDataSource.java +++ /dev/null @@ -1,675 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - */ - -package org.mariadb.jdbc; - -import java.io.Closeable; -import java.io.PrintWriter; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Collections; -import java.util.List; -import java.util.Properties; -import java.util.logging.Logger; -import javax.sql.*; -import org.mariadb.jdbc.internal.util.constant.HaMode; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; -import org.mariadb.jdbc.internal.util.pool.Pool; -import org.mariadb.jdbc.internal.util.pool.Pools; -import org.mariadb.jdbc.util.DefaultOptions; -import org.mariadb.jdbc.util.Options; - -public class MariaDbPoolDataSource - implements ConnectionPoolDataSource, DataSource, XADataSource, Closeable, AutoCloseable { - - private UrlParser urlParser; - private Pool pool; - - private String hostname; - private Integer port; - private Integer connectTimeout; - private String database; - private String url; - private String user; - private String password; - private String poolName; - private Integer maxPoolSize; - private Integer minPoolSize; - private Integer maxIdleTime; - private Boolean staticGlobal; - private Integer poolValidMinDelay; - - /** - * Constructor. - * - * @param hostname hostname (ipv4, ipv6, dns name) - * @param port server port - * @param database database name - */ - public MariaDbPoolDataSource(String hostname, int port, String database) { - this.hostname = hostname; - this.port = port; - this.database = database; - } - - public MariaDbPoolDataSource(String url) { - this.url = url; - } - - /** Default constructor. hostname will be localhost, port 3306. */ - public MariaDbPoolDataSource() {} - - /** - * Gets the name of the database. - * - * @return the name of the database for this data source - */ - public String getDatabaseName() { - if (database != null) { - return database; - } - return (urlParser != null && urlParser.getDatabase() != null) ? urlParser.getDatabase() : ""; - } - - /** - * Sets the database name. - * - * @param database the name of the database - * @throws SQLException if error in URL - */ - public void setDatabaseName(String database) throws SQLException { - checkNotInitialized(); - this.database = database; - } - - private void checkNotInitialized() throws SQLException { - if (pool != null) { - throw new SQLException("can not perform a configuration change once initialized"); - } - } - - /** - * Gets the username. - * - * @return the username to use when connecting to the database - */ - public String getUser() { - if (user != null) { - return user; - } - return urlParser != null ? urlParser.getUsername() : null; - } - - /** - * Sets the username. - * - * @param user the username - * @throws SQLException if error in URL - */ - public void setUser(String user) throws SQLException { - checkNotInitialized(); - this.user = user; - } - - /** - * Sets the password. - * - * @param password the password - * @throws SQLException if error in URL - */ - public void setPassword(String password) throws SQLException { - checkNotInitialized(); - this.password = password; - } - - /** - * Returns the port number. - * - * @return the port number - */ - public int getPort() { - if (port != null && port != 0) { - return port; - } - return urlParser != null ? urlParser.getHostAddresses().get(0).port : 3306; - } - - /** - * Sets the database port. - * - * @param port the port - * @throws SQLException if error in URL - */ - public void setPort(int port) throws SQLException { - checkNotInitialized(); - this.port = port; - } - - /** - * Returns the port number. - * - * @return the port number - */ - public int getPortNumber() { - return getPort(); - } - - /** - * Sets the port number. - * - * @param port the port - * @throws SQLException if error in URL - * @see #setPort - */ - public void setPortNumber(int port) throws SQLException { - checkNotInitialized(); - if (port > 0) { - setPort(port); - } - } - - /** - * Sets the connection string URL. - * - * @param url the connection string - * @throws SQLException if error in URL - */ - public void setUrl(String url) throws SQLException { - checkNotInitialized(); - this.url = url; - } - - /** - * Returns the name of the database server. - * - * @return the name of the database server - */ - public String getServerName() { - if (hostname != null) { - return hostname; - } - boolean hasHost = urlParser != null && this.urlParser.getHostAddresses().get(0).host != null; - return (hasHost) ? this.urlParser.getHostAddresses().get(0).host : "localhost"; - } - - /** - * Sets the server name. - * - * @param serverName the server name - * @throws SQLException if error in URL - */ - public void setServerName(String serverName) throws SQLException { - checkNotInitialized(); - hostname = serverName; - } - - /** - * Attempts to establish a connection with the data source that this DataSource - * object represents. - * - * @return a connection to the data source - * @throws SQLException if a database access error occurs - */ - public Connection getConnection() throws SQLException { - try { - if (pool == null) { - initialize(); - } - return pool.getConnection(); - } catch (SQLException e) { - throw ExceptionFactory.INSTANCE.create(e); - } - } - - /** - * Attempts to establish a connection with the data source that this DataSource - * object represents. - * - * @param username the database user on whose behalf the connection is being made - * @param password the user's password - * @return a connection to the data source - * @throws SQLException if a database access error occurs - */ - public Connection getConnection(final String username, final String password) - throws SQLException { - try { - if (pool == null) { - this.user = username; - this.password = password; - - initialize(); - return pool.getConnection(); - } - - if ((urlParser.getUsername() != null - ? urlParser.getUsername().equals(username) - : username == null) - && (urlParser.getPassword() != null - ? urlParser.getPassword().equals(password) - : (password == null || password.isEmpty()))) { - return pool.getConnection(); - } - - // username / password are different from the one already used to initialize pool - // -> return a real new connection. - - UrlParser urlParser = (UrlParser) this.urlParser.clone(); - urlParser.setUsername(username); - urlParser.setPassword(password); - return MariaDbConnection.newConnection(urlParser, pool.getGlobalInfo()); - - } catch (SQLException e) { - throw ExceptionFactory.INSTANCE.create(e); - } catch (CloneNotSupportedException cloneException) { - throw new SQLException("Error in configuration"); - } - } - - /** - * Attempts to establish a physical database connection that can be used as a pooled connection. - * - * @return a PooledConnection object that is a physical connection to the database - * that this ConnectionPoolDataSource object represents - * @throws SQLException if a database access error occurs if the JDBC driver does not support this - * method - */ - public PooledConnection getPooledConnection() throws SQLException { - return new MariaDbPooledConnection((MariaDbConnection) getConnection()); - } - - /** - * Attempts to establish a physical database connection that can be used as a pooled connection. - * - * @param user the database user on whose behalf the connection is being made - * @param password the user's password - * @return a PooledConnection object that is a physical connection to the database - * that this ConnectionPoolDataSource object represents - * @throws SQLException if a database access error occurs - */ - public PooledConnection getPooledConnection(String user, String password) throws SQLException { - return new MariaDbPooledConnection((MariaDbConnection) getConnection(user, password)); - } - - /** - * Retrieves the log writer for this DataSource object. - * - *

The log writer is a character output stream to which all logging and tracing messages for - * this data source will be printed. This includes messages printed by the methods of this object, - * messages printed by methods of other objects manufactured by this object, and so on. Messages - * printed to a data source specific log writer are not printed to the log writer associated with - * the java.sql.DriverManager class. - * - *

When a DataSource object is created, the log writer is initially null; in other - * words, the default is for logging to be disabled. - * - * @return the log writer for this data source or null if logging is disabled - * @see #setLogWriter - */ - public PrintWriter getLogWriter() { - return null; - } - - /** - * Sets the log writer for this DataSource object to the given - * java.io.PrintWriter object. - * - *

The log writer is a character output stream to which all logging and tracing messages for - * this data source will be printed. This includes messages printed by the methods of this object, - * messages printed by methods of other objects manufactured by this object, and so on. Messages - * printed to a data source- specific log writer are not printed to the log writer associated with - * the java.sql.DriverManager class. When a DataSource object is created - * the log writer is initially null; in other words, the default is for logging to be disabled. - * - * @param out the new log writer; to disable logging, set to null - * @see #getLogWriter - * @since 1.4 - */ - public void setLogWriter(final PrintWriter out) { - // not implemented - } - - /** - * Gets the maximum time in seconds that this data source can wait while attempting to connect to - * a database. A value of zero means that the timeout is the default system timeout if there is - * one; otherwise, it means that there is no timeout. When a DataSource object is - * created, the login timeout is initially zero. - * - * @return the data source login time limit - * @see #setLoginTimeout - * @since 1.4 - */ - public int getLoginTimeout() { - if (connectTimeout != null) { - return connectTimeout / 1000; - } - return (urlParser != null) ? urlParser.getOptions().connectTimeout / 1000 : 0; - } - - /** - * Sets the maximum time in seconds that this data source will wait while attempting to connect to - * a database. A value of zero specifies that the timeout is the default system timeout if there - * is one; otherwise, it specifies that there is no timeout. When a DataSource object - * is created, the login timeout is initially zero. - * - * @param seconds the data source login time limit - * @throws SQLException if a database access error occurs. - * @see #getLoginTimeout - * @since 1.4 - */ - @Override - public void setLoginTimeout(final int seconds) throws SQLException { - checkNotInitialized(); - connectTimeout = seconds * 1000; - } - - /** - * Returns an object that implements the given interface to allow access to non-standard methods, - * or standard methods not exposed by the proxy. - * - *

If the receiver implements the interface then the result is the receiver or a proxy for the - * receiver. If the receiver is a wrapper and the wrapped object implements the interface then the - * result is the wrapped object or a proxy for the wrapped object. Otherwise return the the result - * of calling unwrap recursively on the wrapped object or a proxy for that result. If - * the receiver is not a wrapper and does not implement the interface, then an SQLException - * is thrown. - * - * @param iface A Class defining an interface that the result must implement. - * @return an object that implements the interface. May be a proxy for the actual implementing - * object. - * @throws SQLException If no object found that implements the interface - * @since 1.6 - */ - public T unwrap(final Class iface) throws SQLException { - try { - if (isWrapperFor(iface)) { - return iface.cast(this); - } else { - throw new SQLException( - "The receiver is not a wrapper and does not implement the interface"); - } - } catch (Exception e) { - throw new SQLException("The receiver is not a wrapper and does not implement the interface"); - } - } - - /** - * Returns true if this either implements the interface argument or is directly or indirectly a - * wrapper for an object that does. Returns false otherwise. If this implements the interface then - * return true, else if this is a wrapper then return the result of recursively calling - * isWrapperFor on the wrapped object. If this does not implement the interface and is not - * a wrapper, return false. This method should be implemented as a low-cost operation compared to - * unwrap so that callers can use this method to avoid expensive unwrap - * calls that may fail. If this method returns true then calling unwrap with the same - * argument should succeed. - * - * @param interfaceOrWrapper a Class defining an interface. - * @return true if this implements the interface or directly or indirectly wraps an object that - * does. - * @throws SQLException if an error occurs while determining whether this is a wrapper for an - * object with the given interface. - * @since 1.6 - */ - public boolean isWrapperFor(final Class interfaceOrWrapper) throws SQLException { - return interfaceOrWrapper.isInstance(this); - } - - @Override - public XAConnection getXAConnection() throws SQLException { - return new MariaXaConnection((MariaDbConnection) getConnection()); - } - - @Override - public XAConnection getXAConnection(String user, String password) throws SQLException { - return new MariaXaConnection((MariaDbConnection) getConnection(user, password)); - } - - public Logger getParentLogger() { - return null; - } - - /** - * For testing purpose only. - * - * @return current url parser. - */ - protected UrlParser getUrlParser() { - return urlParser; - } - - public String getPoolName() { - return (pool != null) ? pool.getPoolTag() : poolName; - } - - public void setPoolName(String poolName) throws SQLException { - checkNotInitialized(); - this.poolName = poolName; - } - - /** - * Pool maximum connection size. - * - * @return current value. - */ - public int getMaxPoolSize() { - if (maxPoolSize == null) { - return 8; - } - return maxPoolSize; - } - - public void setMaxPoolSize(int maxPoolSize) throws SQLException { - checkNotInitialized(); - this.maxPoolSize = maxPoolSize; - } - - /** - * Get minimum pool size (pool will grow at creation untile reaching this size). Null mean use the - * pool maximum pool size. - * - * @return current value. - */ - public int getMinPoolSize() { - if (minPoolSize == null) { - return getMaxPoolSize(); - } - return minPoolSize; - } - - public void setMinPoolSize(int minPoolSize) throws SQLException { - checkNotInitialized(); - this.minPoolSize = minPoolSize; - } - - /** - * Max time a connection can be idle. - * - * @return current value. - */ - public int getMaxIdleTime() { - if (maxIdleTime == null) { - return 600; - } - return maxIdleTime; - } - - public void setMaxIdleTime(int maxIdleTime) throws SQLException { - checkNotInitialized(); - this.maxIdleTime = maxIdleTime; - } - - public Boolean getStaticGlobal() { - return staticGlobal; - } - - public void setStaticGlobal(Boolean staticGlobal) { - this.staticGlobal = staticGlobal; - } - - /** - * If connection has been used in less time than poolValidMinDelay, then no connection validation - * will be done (0=mean validation every time). - * - * @return current value of poolValidMinDelay - */ - public Integer getPoolValidMinDelay() { - if (poolValidMinDelay == null) { - return 1000; - } - return poolValidMinDelay; - } - - public void setPoolValidMinDelay(Integer poolValidMinDelay) { - this.poolValidMinDelay = poolValidMinDelay; - } - - private synchronized void initializeUrlParser() throws SQLException { - - if (url != null && !url.isEmpty()) { - Properties props = new Properties(); - props.setProperty("pool", "true"); - if (user != null) { - props.setProperty("user", user); - } - if (password != null) { - props.setProperty("password", password); - } - if (poolName != null) { - props.setProperty("poolName", poolName); - } - - if (database != null) { - props.setProperty("database", database); - } - if (maxPoolSize != null) { - props.setProperty("maxPoolSize", String.valueOf(maxPoolSize)); - } - if (minPoolSize != null) { - props.setProperty("minPoolSize", String.valueOf(minPoolSize)); - } - if (maxIdleTime != null) { - props.setProperty("maxIdleTime", String.valueOf(maxIdleTime)); - } - if (connectTimeout != null) { - props.setProperty("connectTimeout", String.valueOf(connectTimeout)); - } - if (staticGlobal != null) { - props.setProperty("staticGlobal", String.valueOf(staticGlobal)); - } - if (poolValidMinDelay != null) { - props.setProperty("poolValidMinDelay", String.valueOf(poolValidMinDelay)); - } - - urlParser = UrlParser.parse(url, props); - - } else { - - Options options = DefaultOptions.defaultValues(HaMode.NONE); - options.pool = true; - options.user = user; - options.password = password; - options.poolName = poolName; - - if (maxPoolSize != null) { - options.maxPoolSize = maxPoolSize; - } - if (minPoolSize != null) { - options.minPoolSize = minPoolSize; - } - if (maxIdleTime != null) { - options.maxIdleTime = maxIdleTime; - } - if (staticGlobal != null) { - options.staticGlobal = staticGlobal; - } - if (connectTimeout != null) { - options.connectTimeout = connectTimeout; - } - if (poolValidMinDelay != null) { - options.poolValidMinDelay = poolValidMinDelay; - } - - urlParser = - new UrlParser( - database, - Collections.singletonList( - new HostAddress( - (hostname == null || hostname.isEmpty()) ? "localhost" : hostname, - port == null ? 3306 : port)), - options, - HaMode.NONE); - } - } - - /** Close datasource. */ - public void close() { - try { - if (pool != null) { - pool.close(); - } - } catch (InterruptedException interrupted) { - // eat - } - } - - /** - * Initialize pool. - * - * @throws SQLException if connection string has error - */ - public synchronized void initialize() throws SQLException { - if (pool == null) { - initializeUrlParser(); - pool = Pools.retrievePool(urlParser); - } - } - - /** - * Get current idle threads. !! For testing purpose only !! - * - * @return current thread id's - */ - public List testGetConnectionIdleThreadIds() { - return pool.testGetConnectionIdleThreadIds(); - } - - /** - * Permit to create test that doesn't wait for maxIdleTime minimum value of 60 seconds. !! For - * testing purpose only !! - * - * @param maxIdleTime forced value of maxIdleTime option. - * @throws SQLException if connection string has error - */ - public void testForceMaxIdleTime(int maxIdleTime) throws SQLException { - initializeUrlParser(); - urlParser.getOptions().maxIdleTime = maxIdleTime; - pool = Pools.retrievePool(urlParser); - } - - /** - * Get pool. !! For testing purpose only !! - * - * @return pool - */ - public Pool testGetPool() { - return pool; - } -} diff --git a/src/main/java/org/mariadb/jdbc/MariaDbPooledConnection.java b/src/main/java/org/mariadb/jdbc/MariaDbPooledConnection.java deleted file mode 100644 index b07486d38..000000000 --- a/src/main/java/org/mariadb/jdbc/MariaDbPooledConnection.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicLong; -import javax.sql.*; - -public class MariaDbPooledConnection implements PooledConnection { - - private final MariaDbConnection connection; - private final List connectionEventListeners; - private final List statementEventListeners; - private final AtomicLong lastUsed; - - /** - * Constructor. - * - * @param connection connection to retrieve connection options - */ - public MariaDbPooledConnection(MariaDbConnection connection) { - this.connection = connection; - connection.pooledConnection = this; - statementEventListeners = new CopyOnWriteArrayList<>(); - connectionEventListeners = new CopyOnWriteArrayList<>(); - lastUsed = new AtomicLong(System.nanoTime()); - } - - /** - * Creates and returns a Connection object that is a handle for the physical - * connection that this PooledConnection object represents. The connection pool - * manager calls this method when an application has called the method - * DataSource.getConnection and there are no PooledConnection objects - * available. See the {@link PooledConnection interface description} for more information. - * - * @return a Connection object that is a handle to this PooledConnection - * object - */ - public MariaDbConnection getConnection() { - return connection; - } - - /** - * Closes the physical connection that this PooledConnection object represents. An - * application never calls this method directly; it is called by the connection pool module, or - * manager.
- * See the {@link PooledConnection interface description} for more information. - * - * @throws SQLException if a database access error occurs - */ - public void close() throws SQLException { - connection.pooledConnection = null; - connection.close(); - } - - /** - * Abort connection. - * - * @param executor executor - * @throws SQLException if a database access error occurs - */ - public void abort(Executor executor) throws SQLException { - connection.pooledConnection = null; - connection.abort(executor); - } - - /** - * Registers the given event failover so that it will be notified when an event occurs on this - * PooledConnection object. - * - * @param listener a component, usually the connection pool manager, that has implemented the - * ConnectionEventListener interface and wants to be notified when the connection - * is closed or has an error - * @see #removeConnectionEventListener - */ - public void addConnectionEventListener(ConnectionEventListener listener) { - connectionEventListeners.add(listener); - } - - /** - * Removes the given event failover from the list of components that will be notified when an - * event occurs on this PooledConnection object. - * - * @param listener a component, usually the connection pool manager, that has implemented the - * ConnectionEventListener interface and been registered with this - * PooledConnection object as a failover - * @see #addConnectionEventListener - */ - public void removeConnectionEventListener(ConnectionEventListener listener) { - connectionEventListeners.remove(listener); - } - - /** - * Registers a StatementEventListener with this PooledConnection object. - * Components that wish to be notified when PreparedStatements created by the - * connection are closed or are detected to be invalid may use this method to register a - * StatementEventListener with this PooledConnection object.
- * - * @param listener an component which implements the StatementEventListener interface - * that is to be registered with this PooledConnection object
- */ - public void addStatementEventListener(StatementEventListener listener) { - statementEventListeners.add(listener); - } - - /** - * Removes the specified StatementEventListener from the list of components that will - * be notified when the driver detects that a PreparedStatement has been closed or is - * invalid.
- * - * @param listener the component which implements the StatementEventListener - * interface that was previously registered with this PooledConnection object - *
- */ - public void removeStatementEventListener(StatementEventListener listener) { - statementEventListeners.remove(listener); - } - - /** - * Fire statement close event to listeners. - * - * @param st statement - */ - public void fireStatementClosed(Statement st) { - if (st instanceof PreparedStatement) { - StatementEvent event = new StatementEvent(this, (PreparedStatement) st); - for (StatementEventListener listener : statementEventListeners) { - listener.statementClosed(event); - } - } - } - - /** - * Fire statement error to listeners. - * - * @param st statement - * @param ex exception - */ - public void fireStatementErrorOccured(Statement st, SQLException ex) { - if (st instanceof PreparedStatement) { - StatementEvent event = new StatementEvent(this, (PreparedStatement) st, ex); - for (StatementEventListener listener : statementEventListeners) { - listener.statementErrorOccurred(event); - } - } - } - - /** Fire Connection close to listening listeners. */ - public void fireConnectionClosed() { - ConnectionEvent event = new ConnectionEvent(this); - for (ConnectionEventListener listener : connectionEventListeners) { - listener.connectionClosed(event); - } - } - - /** - * Fire connection error to listening listeners. - * - * @param ex exception - */ - public void fireConnectionErrorOccured(SQLException ex) { - ConnectionEvent event = new ConnectionEvent(this, ex); - for (ConnectionEventListener listener : connectionEventListeners) { - listener.connectionErrorOccurred(event); - } - } - - /** - * Indicate if there are any registered listener. - * - * @return true if no listener. - */ - public boolean noStmtEventListeners() { - return statementEventListeners.isEmpty(); - } - - /** - * Indicate last time this pool connection has been used. - * - * @return current last used time (nano). - */ - public AtomicLong getLastUsed() { - return lastUsed; - } - - /** Set last poolConnection use to now. */ - public void lastUsedToNow() { - lastUsed.set(System.nanoTime()); - } -} diff --git a/src/main/java/org/mariadb/jdbc/MariaDbProcedureStatement.java b/src/main/java/org/mariadb/jdbc/MariaDbProcedureStatement.java deleted file mode 100644 index 387fc1a90..000000000 --- a/src/main/java/org/mariadb/jdbc/MariaDbProcedureStatement.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.sql.SQLException; -import java.util.ArrayList; -import org.mariadb.jdbc.internal.com.read.resultset.SelectResultSet; -import org.mariadb.jdbc.internal.com.send.parameters.NullParameter; -import org.mariadb.jdbc.internal.com.send.parameters.ParameterHolder; -import org.mariadb.jdbc.internal.util.dao.CloneableCallableStatement; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; - -public class MariaDbProcedureStatement extends CallableProcedureStatement - implements CloneableCallableStatement { - - private SelectResultSet outputResultSet = null; - - /** - * Specific implementation of CallableStatement to handle function call, represent by call like - * {?= call procedure-name[(arg1,arg2, ...)]}. - * - * @param query query - * @param connection current connection - * @param procedureName procedure name - * @param database database - * @param resultSetType a result set type; one of ResultSet.TYPE_FORWARD_ONLY, - * ResultSet.TYPE_SCROLL_INSENSITIVE, or ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of ResultSet.CONCUR_READ_ONLY - * or ResultSet.CONCUR_UPDATABLE - * @param exceptionFactory Exception Factory - * @throws SQLException exception - */ - public MariaDbProcedureStatement( - String query, - MariaDbConnection connection, - String procedureName, - String database, - int resultSetType, - int resultSetConcurrency, - ExceptionFactory exceptionFactory) - throws SQLException { - super(connection, query, resultSetType, resultSetConcurrency, exceptionFactory); - this.parameterMetadata = - new CallableParameterMetaData(connection, database, procedureName, false); - setParamsAccordingToSetArguments(); - setParametersVariables(); - } - - private void setParamsAccordingToSetArguments() { - params = new ArrayList<>(parameterCount); - for (int index = 0; index < parameterCount; index++) { - params.add(new CallParameter()); - } - } - - private void setInputOutputParameterMap() { - if (outputParameterMapper == null) { - outputParameterMapper = new int[params.size()]; - int currentOutputMapper = 1; - - for (int index = 0; index < params.size(); index++) { - outputParameterMapper[index] = params.get(index).isOutput() ? currentOutputMapper++ : -1; - } - } - } - - protected SelectResultSet getOutputResult() throws SQLException { - if (outputResultSet == null) { - if (fetchSize != 0) { - results.loadFully(false, protocol); - outputResultSet = results.getCallableResultSet(); - if (outputResultSet != null) { - outputResultSet.next(); - return outputResultSet; - } - } - throw new SQLException("No output result."); - } - return outputResultSet; - } - - /** - * Clone statement. - * - * @param connection connection - * @return Clone statement. - * @throws CloneNotSupportedException if any error occur. - */ - public MariaDbProcedureStatement clone(MariaDbConnection connection) - throws CloneNotSupportedException { - MariaDbProcedureStatement clone = (MariaDbProcedureStatement) super.clone(connection); - clone.outputResultSet = null; - return clone; - } - - private void retrieveOutputResult() throws SQLException { - // resultSet will be just before last packet - outputResultSet = results.getCallableResultSet(); - if (outputResultSet != null) { - outputResultSet.next(); - } - } - - public void setParameter(final int parameterIndex, final ParameterHolder holder) - throws SQLException { - params.get(parameterIndex - 1).setInput(true); - super.setParameter(parameterIndex, holder); - } - - @Override - public boolean execute() throws SQLException { - connection.lock.lock(); - try { - validAllParameters(); - super.executeInternal(fetchSize); - retrieveOutputResult(); - return results != null && results.getResultSet() != null; - } finally { - connection.lock.unlock(); - } - } - - /** - * Valid that all parameters are set. - * - * @throws SQLException if set parameters is not right - */ - private void validAllParameters() throws SQLException { - - setInputOutputParameterMap(); - // Set value for OUT parameters - for (int index = 0; index < params.size(); index++) { - if (!params.get(index).isInput()) { - super.setParameter(index + 1, new NullParameter()); - } - } - validParameters(); - } - - @Override - public int[] executeBatch() throws SQLException { - if (!hasInOutParameters) { - return super.executeBatch(); - } else { - throw new SQLException("executeBatch not permit for procedure with output parameter"); - } - } -} diff --git a/src/main/java/org/mariadb/jdbc/MariaDbSavepoint.java b/src/main/java/org/mariadb/jdbc/MariaDbSavepoint.java deleted file mode 100644 index 2cbd89c7d..000000000 --- a/src/main/java/org/mariadb/jdbc/MariaDbSavepoint.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.sql.SQLException; -import java.sql.Savepoint; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; - -public class MariaDbSavepoint implements Savepoint { - - private final String name; - - public MariaDbSavepoint(final String name) { - this.name = name; - } - - /** - * Retrieves the generated ID for the savepoint that this Savepoint object - * represents. - * - * @return the numeric ID of this savepoint - */ - public int getSavepointId() throws SQLException { - throw ExceptionFactory.INSTANCE.notSupported("Doesn't support savepoint identifier"); - } - - /** - * Retrieves the name of the savepoint that this Savepoint object represents. - * - * @return the name of this savepoint - */ - public String getSavepointName() { - return name; - } - - @Override - public String toString() { - return "MariaDbSavepoint{" + "name='" + name + '\'' + '}'; - } -} diff --git a/src/main/java/org/mariadb/jdbc/MariaDbStatement.java b/src/main/java/org/mariadb/jdbc/MariaDbStatement.java deleted file mode 100755 index c36c1daf3..000000000 --- a/src/main/java/org/mariadb/jdbc/MariaDbStatement.java +++ /dev/null @@ -1,1547 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.io.InputStream; -import java.nio.charset.Charset; -import java.sql.*; -import java.util.*; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.mariadb.jdbc.internal.com.read.dao.Results; -import org.mariadb.jdbc.internal.com.read.resultset.SelectResultSet; -import org.mariadb.jdbc.internal.logging.Logger; -import org.mariadb.jdbc.internal.logging.LoggerFactory; -import org.mariadb.jdbc.internal.protocol.Protocol; -import org.mariadb.jdbc.internal.util.Utils; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; -import org.mariadb.jdbc.internal.util.scheduler.SchedulerServiceProviderHolder; -import org.mariadb.jdbc.util.Options; - -public class MariaDbStatement implements Statement, Cloneable { - - private static final Pattern identifierPattern = - Pattern.compile("[0-9a-zA-Z\\$_\\u0080-\\uFFFF]*", Pattern.UNICODE_CASE | Pattern.CANON_EQ); - private static final Pattern escapePattern = Pattern.compile("[\u0000'\"\b\n\r\t\u001A\\\\]"); - private static final Map mapper = new HashMap<>(); - // timeout scheduler - private static final Logger logger = LoggerFactory.getLogger(MariaDbStatement.class); - - static { - mapper.put("\u0000", "\\0"); - mapper.put("'", "\\\\'"); - mapper.put("\"", "\\\\\""); - mapper.put("\b", "\\\\b"); - mapper.put("\n", "\\\\n"); - mapper.put("\r", "\\\\r"); - mapper.put("\t", "\\\\t"); - mapper.put("\u001A", "\\\\Z"); - mapper.put("\\", "\\\\"); - } - - protected final ReentrantLock lock; - protected final int resultSetScrollType; - protected final int resultSetConcurrency; - protected final Options options; - protected final boolean canUseServerTimeout; - /** the protocol used to talk to the server. */ - protected Protocol protocol; - /** the Connection object. */ - protected MariaDbConnection connection; - - protected volatile boolean closed = false; - protected int queryTimeout; - protected long maxRows; - protected Results results; - protected int fetchSize; - protected volatile boolean executing; - protected ExceptionFactory exceptionFactory; - private ScheduledExecutorService timeoutScheduler; - // are warnings cleared? - private boolean warningsCleared; - private boolean mustCloseOnCompletion = false; - private List batchQueries; - private Future timerTaskFuture; - private boolean isTimedout; - private int maxFieldSize; - - /** - * Creates a new Statement. - * - * @param connection the connection to return in getConnection. - * @param resultSetScrollType one of the following ResultSet constants: - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, or - * ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of ResultSet.CONCUR_READ_ONLY - * or ResultSet.CONCUR_UPDATABLE - * @param exceptionFactory exception factory - */ - public MariaDbStatement( - MariaDbConnection connection, - int resultSetScrollType, - int resultSetConcurrency, - ExceptionFactory exceptionFactory) { - this.protocol = connection.getProtocol(); - this.connection = connection; - this.canUseServerTimeout = connection.canUseServerTimeout(); - this.resultSetScrollType = resultSetScrollType; - this.resultSetConcurrency = resultSetConcurrency; - this.lock = this.connection.lock; - this.options = this.protocol.getOptions(); - this.exceptionFactory = exceptionFactory; - this.fetchSize = this.options.defaultFetchSize; - } - - /** - * Clone statement. - * - * @param connection connection - * @return Clone statement. - * @throws CloneNotSupportedException if any error occur. - */ - public MariaDbStatement clone(MariaDbConnection connection) throws CloneNotSupportedException { - MariaDbStatement clone = (MariaDbStatement) super.clone(); - clone.connection = connection; - clone.protocol = connection.getProtocol(); - clone.timerTaskFuture = null; - clone.batchQueries = new ArrayList<>(); - clone.closed = false; - clone.warningsCleared = true; - clone.maxRows = 0; - clone.fetchSize = this.options.defaultFetchSize; - clone.exceptionFactory = - ExceptionFactory.of( - this.exceptionFactory.getThreadId(), this.exceptionFactory.getOptions()); - return clone; - } - - // Part of query prolog - setup timeout timer - protected void setTimerTask(boolean isBatch) { - assert (timerTaskFuture == null); - if (timeoutScheduler == null) { - timeoutScheduler = SchedulerServiceProviderHolder.getTimeoutScheduler(); - } - timerTaskFuture = - timeoutScheduler.schedule( - () -> { - try { - isTimedout = true; - if (!isBatch) { - protocol.cancelCurrentQuery(); - } - protocol.interrupt(); - } catch (Throwable e) { - // eat - } - }, - queryTimeout, - TimeUnit.SECONDS); - } - - /** - * Command prolog. - * - *

    - *
  1. clear previous query state - *
  2. launch timeout timer if needed - *
- * - * @param isBatch is batch - * @throws SQLException if statement is closed - */ - protected void executeQueryPrologue(boolean isBatch) throws SQLException { - executing = true; - if (closed) { - throw exceptionFactory - .raiseStatementError(connection, this) - .create("execute() is called on closed statement"); - } - protocol.prolog(maxRows, protocol.getProxy() != null, connection, this); - if (queryTimeout != 0 && (!canUseServerTimeout || isBatch)) { - setTimerTask(isBatch); - } - } - - private void stopTimeoutTask() { - if (timerTaskFuture != null) { - if (!timerTaskFuture.cancel(true)) { - // could not cancel, task either started or already finished - // we must now wait for task to finish to ensure state modifications are done - try { - timerTaskFuture.get(); - } catch (InterruptedException e) { - // reset interrupt status - Thread.currentThread().interrupt(); - } catch (ExecutionException e) { - // ignore error, likely due to interrupting during cancel - } - // we don't catch the exception if already canceled, that would indicate we tried - // to cancel in parallel (which this code currently is not designed for) - } - timerTaskFuture = null; - } - } - - /** - * Reset timeout after query, re-throw SQL exception. - * - * @param sqle current exception - * @return SQLException exception with new message in case of timer timeout. - */ - protected SQLException executeExceptionEpilogue(SQLException sqle) { - // if has a failover, closing the statement - if (sqle.getSQLState() != null && sqle.getSQLState().startsWith("08")) { - try { - close(); - } catch (SQLException sqlee) { - // eat exception - } - } - - if (sqle.getErrorCode() == 1148 && !options.allowLocalInfile) { - return exceptionFactory - .raiseStatementError(connection, this) - .create( - "Usage of LOCAL INFILE is disabled. " - + "To use it enable it via the connection property allowLocalInfile=true", - "42000", - 1148, - sqle); - } - - if (isTimedout) { - return exceptionFactory - .raiseStatementError(connection, this) - .create("Query timed out", "70100", 1317, sqle); - } - - SQLException sqlException = exceptionFactory.raiseStatementError(connection, this).create(sqle); - logger.error("error executing query", sqlException); - return sqlException; - } - - protected void executeEpilogue() { - stopTimeoutTask(); - isTimedout = false; - executing = false; - } - - protected void executeBatchEpilogue() { - executing = false; - stopTimeoutTask(); - isTimedout = false; - clearBatch(); - } - - private SQLException handleFailoverAndTimeout(SQLException sqle) { - - // if has a failover, closing the statement - if (sqle.getSQLState() != null && sqle.getSQLState().startsWith("08")) { - try { - close(); - } catch (SQLException sqlee) { - // eat exception - } - } - - if (isTimedout) { - return exceptionFactory - .raiseStatementError(connection, this) - .create("Query timed out", "70100", 1317, sqle); - } - return sqle; - } - - protected BatchUpdateException executeBatchExceptionEpilogue(SQLException initialSqle, int size) { - SQLException sqle = handleFailoverAndTimeout(initialSqle); - int[] ret; - if (results == null || !results.commandEnd()) { - ret = new int[size]; - Arrays.fill(ret, Statement.EXECUTE_FAILED); - } else { - ret = results.getCmdInformation().getUpdateCounts(); - } - sqle = exceptionFactory.raiseStatementError(connection, this).create(sqle); - logger.error("error executing query", sqle); - - return new BatchUpdateException( - sqle.getMessage(), sqle.getSQLState(), sqle.getErrorCode(), ret, sqle); - } - - /** - * Executes a query. - * - * @param sql the query - * @param fetchSize fetch size - * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be returned; one - * of Statement.RETURN_GENERATED_KEYS or Statement.NO_GENERATED_KEYS - * @return true if there was a result set, false otherwise. - * @throws SQLException the error description - */ - private boolean executeInternal(String sql, int fetchSize, int autoGeneratedKeys) - throws SQLException { - - lock.lock(); - try { - - executeQueryPrologue(false); - results = - new Results( - this, - fetchSize, - false, - 1, - false, - resultSetScrollType, - resultSetConcurrency, - autoGeneratedKeys, - protocol.getAutoIncrementIncrement(), - sql, - null); - protocol.executeQuery( - protocol.isMasterConnection(), results, getTimeoutSql(Utils.nativeSql(sql, protocol))); - results.commandEnd(); - return results.getResultSet() != null; - - } catch (SQLException exception) { - throw executeExceptionEpilogue(exception); - } finally { - executeEpilogue(); - lock.unlock(); - } - } - - /** - * Enquote String value. - * - * @param val string value to enquote - * @return enquoted string value - * @throws SQLException -not possible- - */ - public String enquoteLiteral(String val) throws SQLException { - - Matcher matcher = escapePattern.matcher(val); - StringBuffer escapedVal = new StringBuffer("'"); - - while (matcher.find()) { - matcher.appendReplacement(escapedVal, mapper.get(matcher.group())); - } - matcher.appendTail(escapedVal); - escapedVal.append("'"); - return escapedVal.toString(); - } - - /** - * Escaped identifier according to MariaDB requirement. - * - * @param identifier identifier - * @param alwaysQuote indicate if identifier must be enquoted even if not necessary. - * @return return escaped identifier, quoted when necessary or indicated with alwaysQuote. - * @see mariadb identifier name - * @throws SQLException if containing u0000 character - */ - public String enquoteIdentifier(String identifier, boolean alwaysQuote) throws SQLException { - if (isSimpleIdentifier(identifier)) { - return alwaysQuote ? "`" + identifier + "`" : identifier; - } else { - if (identifier.contains("\u0000")) { - throw exceptionFactory - .raiseStatementError(connection, this) - .create("Invalid name - containing u0000 character", "42000"); - } - - if (identifier.matches("^`.+`$")) { - identifier = identifier.substring(1, identifier.length() - 1); - } - return "`" + identifier.replace("`", "``") + "`"; - } - } - - /** - * Retrieves whether identifier is a simple SQL identifier. The first character is an alphabetic - * character from a through z, or from A through Z The string only contains alphanumeric - * characters or the characters "_" and "$" - * - * @param identifier identifier - * @return true if identifier doesn't have to be quoted - * @see mariadb identifier name - * @throws SQLException exception - */ - public boolean isSimpleIdentifier(String identifier) throws SQLException { - return identifier != null - && !identifier.isEmpty() - && identifierPattern.matcher(identifier).matches(); - } - - /** - * Enquote utf8 value. - * - * @param val value to enquote - * @return enquoted String value - * @throws SQLException - not possible - - */ - public String enquoteNCharLiteral(String val) throws SQLException { - return "N'" + val.replace("'", "''") + "'"; - } - - private String getTimeoutSql(String sql) { - if (queryTimeout != 0 && canUseServerTimeout) { - return "SET STATEMENT max_statement_time=" + queryTimeout + " FOR " + sql; - } - return sql; - } - - /** - * ! This method is for test only ! This permit sending query using specific charset. - * - * @param sql sql - * @param charset charset - * @return boolean if execution went well - * @throws SQLException if any exception occur - */ - public boolean testExecute(String sql, Charset charset) throws SQLException { - lock.lock(); - try { - - executeQueryPrologue(false); - results = - new Results( - this, - fetchSize, - false, - 1, - false, - resultSetScrollType, - resultSetConcurrency, - Statement.NO_GENERATED_KEYS, - protocol.getAutoIncrementIncrement(), - sql, - null); - protocol.executeQuery( - protocol.isMasterConnection(), - results, - getTimeoutSql(Utils.nativeSql(sql, protocol)), - charset); - results.commandEnd(); - return results.getResultSet() != null; - - } catch (SQLException exception) { - throw executeExceptionEpilogue(exception); - } finally { - executeEpilogue(); - lock.unlock(); - } - } - - /** - * executes a query. - * - * @param sql the query - * @return true if there was a result set, false otherwise. - * @throws SQLException if the query could not be sent to server - */ - public boolean execute(String sql) throws SQLException { - return executeInternal(sql, fetchSize, Statement.NO_GENERATED_KEYS); - } - - /** - * Executes the given SQL statement, which may return multiple results, and signals the driver - * that any auto-generated keys should be made available for retrieval. The driver will ignore - * this signal if the SQL statement is not an INSERT statement, or an SQL statement - * able to return auto-generated keys (the list of such statements is vendor-specific). - * - *

In some (uncommon) situations, a single SQL statement may return multiple result sets and/or - * update counts. Normally you can ignore this unless you are (1) executing a stored procedure - * that you know may return multiple results or (2) you are dynamically executing an unknown SQL - * string. The execute method executes an SQL statement and indicates the form of the - * first result. You must then use the methods getResultSet or getUpdateCount - * to retrieve the result, and getInternalMoreResults to move to any - * subsequent result(s). - * - * @param sql any SQL statement - * @param autoGeneratedKeys a constant indicating whether auto-generated keys should be made - * available for retrieval using the methodgetGeneratedKeys; one of the following - * constants: Statement.RETURN_GENERATED_KEYS or - * Statement.NO_GENERATED_KEYS - * @return true if the first result is a ResultSet object; false - * if it is an update count or there are no results - * @throws SQLException if a database access error occurs, this method is called on a closed - * Statement or the second parameter supplied to this method is not - * Statement.RETURN_GENERATED_KEYS or Statement.NO_GENERATED_KEYS. - * @see #getResultSet - * @see #getUpdateCount - * @see #getMoreResults - * @see #getGeneratedKeys - */ - public boolean execute(final String sql, final int autoGeneratedKeys) throws SQLException { - return executeInternal(sql, fetchSize, autoGeneratedKeys); - } - - /** - * Executes the given SQL statement, which may return multiple results, and signals the driver - * that the auto-generated keys indicated in the given array should be made available for - * retrieval. This array contains the indexes of the columns in the target table that contain the - * auto-generated keys that should be made available. The driver will ignore the array if the SQL - * statement is not an INSERT statement, or an SQL statement able to return - * auto-generated keys (the list of such statements is vendor-specific). - * - *

Under some (uncommon) situations, a single SQL statement may return multiple result sets - * and/or update counts. Normally you can ignore this unless you are (1) executing a stored - * procedure that you know may return multiple results or (2) you are dynamically executing an - * unknown SQL string. The execute method executes an SQL statement and indicates the - * form of the first result. You must then use the methods getResultSet or - * getUpdateCount to retrieve the result, and getInternalMoreResults to move - * to any subsequent result(s). - * - * @param sql any SQL statement - * @param columnIndexes an array of the indexes of the columns in the inserted row that should be - * made available for retrieval by a call to the method getGeneratedKeys - * @return true if the first result is a ResultSet object; false - * if it is an update count or there are no results - * @throws SQLException if a database access error occurs, this method is called on a closed - * Statement or the elements in the int array passed to this method - * are not valid column indexes - * @see #getResultSet - * @see #getUpdateCount - * @see #getMoreResults - */ - public boolean execute(final String sql, final int[] columnIndexes) throws SQLException { - return executeInternal(sql, fetchSize, Statement.RETURN_GENERATED_KEYS); - } - - /** - * Executes the given SQL statement, which may return multiple results, and signals the driver - * that the auto-generated keys indicated in the given array should be made available for - * retrieval. This array contains the names of the columns in the target table that contain the - * auto-generated keys that should be made available. The driver will ignore the array if the SQL - * statement is not an INSERT statement, or an SQL statement able to return - * auto-generated keys (the list of such statements is vendor-specific). - * - *

In some (uncommon) situations, a single SQL statement may return multiple result sets and/or - * update counts. Normally you can ignore this unless you are (1) executing a stored procedure - * that you know may return multiple results or (2) you are dynamically executing an unknown SQL - * string. - * - *

The execute method executes an SQL statement and indicates the form of the - * first result. You must then use the methods getResultSet or getUpdateCount - * to retrieve the result, and getInternalMoreResults to move to any - * subsequent result(s). - * - * @param sql any SQL statement - * @param columnNames an array of the names of the columns in the inserted row that should be made - * available for retrieval by a call to the method getGeneratedKeys - * @return true if the next result is a ResultSet object; false - * if it is an update count or there are no more results - * @throws SQLException if a database access error occurs, this method is called on a closed - * Statement or the elements of the String array passed to this - * method are not valid column names - * @see #getResultSet - * @see #getUpdateCount - * @see #getMoreResults - * @see #getGeneratedKeys - */ - public boolean execute(final String sql, final String[] columnNames) throws SQLException { - return executeInternal(sql, fetchSize, Statement.RETURN_GENERATED_KEYS); - } - - /** - * executes a select query. - * - * @param sql the query to send to the server - * @return a result set - * @throws SQLException if something went wrong - */ - public ResultSet executeQuery(String sql) throws SQLException { - if (executeInternal(sql, fetchSize, Statement.NO_GENERATED_KEYS)) { - return results.getResultSet(); - } - return SelectResultSet.createEmptyResultSet(); - } - - /** - * Executes an update. Result-set are permitted for historical reason, even if spec indicate to - * throw exception. - * - * @param sql the update query. - * @return update count - * @throws SQLException if the query could not be sent to server. - */ - public int executeUpdate(String sql) throws SQLException { - if (executeInternal(sql, fetchSize, Statement.NO_GENERATED_KEYS)) { - return 0; - } - return getUpdateCount(); - } - - /** - * Executes the given SQL statement and signals the driver with the given flag about whether the - * auto-generated keys produced by this Statement object should be made available for - * retrieval. The driver will ignore the flag if the SQL statement is not an INSERT - * statement, or an SQL statement able to return auto-generated keys (the list of such statements - * is vendor-specific). Result-set are permitted for historical reason, even if spec indicate to - * throw exception. - * - * @param sql an SQL Data Manipulation Language (DML) statement, such as INSERT, - * UPDATE or DELETE; or an SQL statement that returns nothing, such - * as a DDL statement. - * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be made available - * for retrieval; one of the following constants: Statement.RETURN_GENERATED_KEYS - * Statement.NO_GENERATED_KEYS - * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0 - * for SQL statements that return nothing - * @throws SQLException if a database access error occurs, this method is called on a closed - * Statement or the given constant is not one of those allowed - */ - public int executeUpdate(final String sql, final int autoGeneratedKeys) throws SQLException { - if (executeInternal(sql, fetchSize, autoGeneratedKeys)) { - return 0; - } - return getUpdateCount(); - } - - /** - * Executes the given SQL statement and signals the driver that the auto-generated keys indicated - * in the given array should be made available for retrieval. This array contains the indexes of - * the columns in the target table that contain the auto-generated keys that should be made - * available. The driver will ignore the array if the SQL statement is not an INSERT - * statement, or an SQL statement able to return auto-generated keys (the list of such statements - * is vendor-specific). Result-set are permitted for historical reason, even if spec indicate to - * throw exception. - * - * @param sql an SQL Data Manipulation Language (DML) statement, such as INSERT, - * UPDATE or DELETE; or an SQL statement that returns nothing, such - * as a DDL statement. - * @param columnIndexes an array of column indexes indicating the columns that should be returned - * from the inserted row - * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0 - * for SQL statements that return nothing - * @throws SQLException if a database access error occurs, this method is called on a closed - * Statement or the second argument supplied to this method is not an int - * array whose elements are valid column indexes - */ - public int executeUpdate(final String sql, final int[] columnIndexes) throws SQLException { - return executeUpdate(sql, Statement.RETURN_GENERATED_KEYS); - } - - /** - * Executes the given SQL statement and signals the driver that the auto-generated keys indicated - * in the given array should be made available for retrieval. This array contains the names of the - * columns in the target table that contain the auto-generated keys that should be made available. - * The driver will ignore the array if the SQL statement is not an INSERT statement, - * or an SQL statement able to return auto-generated keys (the list of such statements is - * vendor-specific). Result-set are permitted for historical reason, even if spec indicate to - * throw exception. - * - * @param sql an SQL Data Manipulation Language (DML) statement, such as INSERT, - * UPDATE or DELETE; or an SQL statement that returns nothing, such - * as a DDL statement. - * @param columnNames an array of the names of the columns that should be returned from the - * inserted row - * @return either the row count for INSERT, UPDATE, or DELETE - * statements, or 0 for SQL statements that return nothing - * @throws SQLException if a database access error occurs, this method is called on a closed - * Statement or the second argument supplied to this method is not a String - * array whose elements are valid column names - */ - public int executeUpdate(final String sql, final String[] columnNames) throws SQLException { - return executeUpdate(sql, Statement.RETURN_GENERATED_KEYS); - } - - /** - * Executes the given SQL statement, which may be an INSERT, UPDATE, or DELETE statement or an SQL - * statement that returns nothing, such as an SQL DDL statement. This method should be used when - * the returned row count may exceed Integer.MAX_VALUE. - * - * @param sql sql command - * @return update counts - * @throws SQLException if any error occur during execution - */ - @Override - public long executeLargeUpdate(String sql) throws SQLException { - if (executeInternal(sql, fetchSize, Statement.NO_GENERATED_KEYS)) { - return 0; - } - return getLargeUpdateCount(); - } - - /** - * Identical to executeLargeUpdate(String sql), with a flag that indicate that autoGeneratedKeys - * (primary key fields with "auto_increment") generated id's must be retrieved. - * - *

Those id's will be available using getGeneratedKeys() method. - * - * @param sql sql command - * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be made available - * for retrieval; one of the following constants: Statement.RETURN_GENERATED_KEYS - * Statement.NO_GENERATED_KEYS - * @return update counts - * @throws SQLException if any error occur during execution - */ - @Override - public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException { - if (executeInternal(sql, fetchSize, autoGeneratedKeys)) { - return 0; - } - return getLargeUpdateCount(); - } - - /** - * Identical to executeLargeUpdate(String sql, int autoGeneratedKeys) with autoGeneratedKeys = - * Statement.RETURN_GENERATED_KEYS set. - * - * @param sql sql command - * @param columnIndexes column Indexes - * @return update counts - * @throws SQLException if any error occur during execution - */ - @Override - public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException { - return executeLargeUpdate(sql, Statement.RETURN_GENERATED_KEYS); - } - - /** - * Identical to executeLargeUpdate(String sql, int autoGeneratedKeys) with autoGeneratedKeys = - * Statement.RETURN_GENERATED_KEYS set. - * - * @param sql sql command - * @param columnNames columns names - * @return update counts - * @throws SQLException if any error occur during execution - */ - @Override - public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException { - return executeLargeUpdate(sql, Statement.RETURN_GENERATED_KEYS); - } - - /** - * Releases this Statement object's database and JDBC resources immediately instead - * of waiting for this to happen when it is automatically closed. It is generally good practice to - * release resources as soon as you are finished with them to avoid tying up database resources. - * Calling the method close on a Statement object that is already closed - * has no effect. Note:When a Statement object is closed, its current - * ResultSet object, if one exists, is also closed. - * - * @throws SQLException if a database access error occurs - */ - public void close() throws SQLException { - lock.lock(); - try { - closed = true; - if (results != null) { - if (results.getFetchSize() != 0) { - skipMoreResults(); - } - - results.close(); - } - - if (connection == null - || connection.pooledConnection == null - || connection.pooledConnection.noStmtEventListeners()) { - return; - } - connection.pooledConnection.fireStatementClosed(this); - } finally { - protocol = null; - connection = null; - lock.unlock(); - } - } - - /** - * Retrieves the maximum number of bytes that can be returned for character and binary column - * values in a ResultSet object produced by this Statement object. This - * limit applies only to BINARY, VARBINARY, LONGVARBINARY, - * CHAR, VARCHAR, NCHAR, NVARCHAR, - * LONGNVARCHAR and LONGVARCHAR columns. If the limit is exceeded, the excess - * data is silently discarded. - * - * @return the current column size limit for columns storing character and binary values; zero - * means there is no limit - * @see #setMaxFieldSize - */ - public int getMaxFieldSize() { - return maxFieldSize; - } - - /** - * Sets the limit for the maximum number of bytes that can be returned for character and binary - * column values in a ResultSet object produced by this Statement - * object. This limit applies only to BINARY, VARBINARY, - * LONGVARBINARY, CHAR, VARCHAR, NCHAR, - * NVARCHAR, LONGNVARCHAR and LONGVARCHAR fields. If the limit is - * exceeded, the excess data is silently discarded. For maximum portability, use values greater - * than 256. - * - * @param max the new column size limit in bytes; zero means there is no limit - * @see #getMaxFieldSize - */ - public void setMaxFieldSize(final int max) { - maxFieldSize = max; - } - - /** - * Retrieves the maximum number of rows that a ResultSet object produced by this - * Statement object can contain. If this limit is exceeded, the excess rows are - * silently dropped. - * - * @return the current maximum number of rows for a ResultSet object produced by this - * Statement object; zero means there is no limit - * @see #setMaxRows - */ - public int getMaxRows() { - return (int) maxRows; - } - - /** - * Sets the limit for the maximum number of rows that any ResultSet object generated - * by this Statement object can contain to the given number. If the limit is - * exceeded, the excess rows are silently dropped. - * - * @param max the new max rows limit; zero means there is no limit - * @throws SQLException if the condition max >= 0 is not satisfied - * @see #getMaxRows - */ - public void setMaxRows(final int max) throws SQLException { - if (max < 0) { - throw exceptionFactory - .raiseStatementError(connection, this) - .create("max rows cannot be negative : asked for " + max, "42000"); - } - maxRows = max; - } - - /** - * Retrieves the maximum number of rows that a ResultSet object produced by this Statement object - * can contain. If this limit is exceeded, the excess rows are silently dropped. - * - * @return the current maximum number of rows for a ResultSet object produced by this Statement - * object; zero means there is no limit - */ - @Override - public long getLargeMaxRows() { - return maxRows; - } - - /** - * Sets the limit for the maximum number of rows that any ResultSet object generated by this - * Statement object can contain to the given number. If the limit is exceeded, the excess rows are - * silently dropped. - * - * @param max the new max rows limit; zero means there is no limit - * @throws SQLException if the condition max >= 0 is not satisfied - */ - @Override - public void setLargeMaxRows(long max) throws SQLException { - if (max < 0) { - throw exceptionFactory - .raiseStatementError(connection, this) - .create("max rows cannot be negative : setLargeMaxRows value is " + max, "42000"); - } - maxRows = max; - } - - /** - * Sets escape processing on or off. If escape scanning is on (the default), the driver will do - * escape substitution before sending the SQL statement to the database. Note: Since prepared - * statements have usually been parsed prior to making this call, disabling escape processing for - * PreparedStatements objects will have no effect. - * - * @param enable true to enable escape processing; false to disable it - */ - public void setEscapeProcessing(final boolean enable) { - // not handled - } - - /** - * Retrieves the number of seconds the driver will wait for a Statement object to - * execute. If the limit is exceeded, a SQLException is thrown. - * - * @return the current query timeout limit in seconds; zero means there is no limit - * @see #setQueryTimeout - */ - public int getQueryTimeout() { - return queryTimeout; - } - - /** - * Sets the number of seconds the driver will wait for a Statement object to execute - * to the given number of seconds. If the limit is exceeded, an SQLException is - * thrown. A JDBC driver must apply this limit to the execute, executeQuery - * and executeUpdate methods. - * - * @param seconds the new query timeout limit in seconds; zero means there is no limit - * @throws SQLException if a database access error occurs, this method is called on a closed - * Statement or the condition seconds >= 0 is not satisfied - * @see #getQueryTimeout - */ - public void setQueryTimeout(final int seconds) throws SQLException { - if (seconds < 0) { - throw exceptionFactory - .raiseStatementError(connection, this) - .create("Query timeout cannot be negative : asked for " + seconds, "42000"); - } - this.queryTimeout = seconds; - } - - /** - * Sets the inputStream that will be used for the next execute that uses "LOAD DATA LOCAL INFILE". - * The name specified as local file/URL will be ignored. - * - * @param inputStream inputStream instance, that will be used to send data to server - * @throws SQLException if statement is closed - */ - public void setLocalInfileInputStream(InputStream inputStream) throws SQLException { - checkClose(); - protocol.setLocalInfileInputStream(inputStream); - } - - /** - * Cancels this Statement object if both the DBMS and driver support aborting an SQL - * statement. This method can be used by one thread to cancel a statement that is being executed - * by another thread. - * - *

In case there is result-set from this Statement that are still streaming data from server, - * will cancel streaming. - * - * @throws SQLException if a database access error occurs or this method is called on a closed - * Statement - */ - public void cancel() throws SQLException { - checkClose(); - boolean locked = lock.tryLock(); - try { - if (executing) { - protocol.cancelCurrentQuery(); - } else if (results != null - && results.getFetchSize() != 0 - && !results.isFullyLoaded(protocol)) { - try { - protocol.cancelCurrentQuery(); - skipMoreResults(); - } catch (SQLException e) { - // eat exception - } - results.removeFetchSize(); - } - - } catch (SQLException e) { - logger.error("error cancelling query", e); - throw exceptionFactory.raiseStatementError(connection, this).create(e); - } finally { - if (locked) { - lock.unlock(); - } - } - } - - /** - * Retrieves the first warning reported by calls on this Statement object. Subsequent - * Statement object warnings will be chained to this SQLWarning object. - * - *

The warning chain is automatically cleared each time a statement is (re)executed. This - * method may not be called on a closed Statement object; doing so will cause an - * SQLException to be thrown. - * - *

Note: If you are processing a ResultSet object, any warnings associated - * with reads on that ResultSet object will be chained on it rather than on the - * Statement object that produced it. - * - * @return the first SQLWarning object or null if there are no warnings - * @throws SQLException if a database access error occurs or this method is called on a closed - * Statement - */ - public SQLWarning getWarnings() throws SQLException { - checkClose(); - if (!warningsCleared) { - return this.connection.getWarnings(); - } - return null; - } - - /** - * Clears all the warnings reported on this Statement object. After a call to this - * method, the method getWarnings will return null until a new warning - * is reported for this Statement object. - */ - public void clearWarnings() { - warningsCleared = true; - } - - /** - * Sets the SQL cursor name to the given String, which will be used by subsequent - * Statement object execute methods. This name can then be used in SQL - * positioned update or delete statements to identify the current row in the ResultSet - * object generated by this statement. If the database does not support positioned - * update/delete, this method is a noop. To insure that a cursor has the proper isolation level to - * support updates, the cursor's SELECT statement should have the form - * SELECT FOR UPDATE. If FOR UPDATE is not present, positioned updates may - * fail. - * - *

Note: By definition, the execution of positioned updates and deletes must be done by - * a different Statement object than the one that generated the ResultSet - * object being used for positioning. Also, cursor names must be unique within a - * connection. - * - * @param name the new cursor name, which must be unique within a connection - * @throws SQLException if a database access error occurs or this method is called on a closed - * Statement - */ - public void setCursorName(final String name) throws SQLException { - throw exceptionFactory - .raiseStatementError(connection, this) - .notSupported("Cursors are not supported"); - } - - /** - * Gets the connection that created this statement. - * - * @return the connection - */ - public MariaDbConnection getConnection() { - return this.connection; - } - - /** - * Retrieves any auto-generated keys created as a result of executing this Statement - * object. If this Statement object did not generate any keys, an empty - * ResultSet object is returned. - * - *

Note:If the columns which represent the auto-generated keys were not specified, the - * JDBC driver implementation will determine the columns which best represent the auto-generated - * keys. - * - * @return a ResultSet object containing the auto-generated key(s) generated by the - * execution of this Statement object - * @throws SQLException if a database access error occurs or this method is called on a closed - * Statement - */ - public ResultSet getGeneratedKeys() throws SQLException { - if (results != null) { - return results.getGeneratedKeys(protocol); - } - return SelectResultSet.createEmptyResultSet(); - } - - /** - * Retrieves the result set holdability for ResultSet objects generated by this - * Statement object. - * - * @return either ResultSet.HOLD_CURSORS_OVER_COMMIT or - * ResultSet.CLOSE_CURSORS_AT_COMMIT - * @since 1.4 - */ - public int getResultSetHoldability() { - return ResultSet.HOLD_CURSORS_OVER_COMMIT; - } - - /** - * Retrieves whether this Statement object has been closed. A Statement - * is closed if the method close has been called on it, or if it is automatically closed. - * - * @return true if this Statement object is closed; false if it is still open - * @since 1.6 - */ - public boolean isClosed() { - return closed; - } - - /** - * Returns a value indicating whether the Statement is poolable or not. - * - * @return true if the Statement is poolable; false - * otherwise - * @see Statement#setPoolable(boolean) setPoolable(boolean) - * @since 1.6 - */ - @Override - public boolean isPoolable() { - return false; - } - - /** - * Requests that a Statement be pooled or not pooled. The value specified is a hint - * to the statement pool implementation indicating whether the applicaiton wants the statement to - * be pooled. It is up to the statement pool manager as to whether the hint is used. - * - *

The poolable value of a statement is applicable to both internal statement caches - * implemented by the driver and external statement caches implemented by application servers and - * other applications. - * - *

By default, a Statement is not poolable when created, and a - * PreparedStatement and CallableStatement are poolable when created. - * - * @param poolable requests that the statement be pooled if true and that the statement not be - * pooled if false - * @since 1.6 - */ - @Override - public void setPoolable(final boolean poolable) { - // not handled - } - - /** - * Retrieves the current result as a ResultSet object. This method should be called only once per - * result. - * - * @return the current result as a ResultSet object or null if the result is an update count or - * there are no more results - * @throws SQLException if a database access error occurs or this method is called on a closed - * Statement - */ - public ResultSet getResultSet() throws SQLException { - checkClose(); - return results != null ? results.getResultSet() : null; - } - - /** - * Retrieves the current result as an update count; if the result is a ResultSet object or there - * are no more results, -1 is returned. This method should be called only once per result. - * - * @return the current result as an update count; -1 if the current result is a ResultSet object - * or there are no more results - */ - public int getUpdateCount() { - if (results != null && results.getCmdInformation() != null && !results.isBatch()) { - return results.getCmdInformation().getUpdateCount(); - } - return -1; - } - - /** - * Retrieves the current result as an update count; if the result is a ResultSet object or there - * are no more results, -1 is returned. - * - * @return last update count - */ - @Override - public long getLargeUpdateCount() { - if (results != null && results.getCmdInformation() != null && !results.isBatch()) { - return results.getCmdInformation().getLargeUpdateCount(); - } - return -1; - } - - protected void skipMoreResults() throws SQLException { - try { - protocol.skip(); - warningsCleared = false; - connection.reenableWarnings(); - } catch (SQLException e) { - logger.debug("error skipMoreResults", e); - throw exceptionFactory.raiseStatementError(connection, this).create(e); - } - } - - /** - * Moves to this Statement object's next result, returns true if it is a - * ResultSet object, and implicitly closes any current ResultSet - * object(s) obtained with the method getResultSet. There are no more results when - * the following is true: - * - *

 // stmt is a Statement object
-   * ((stmt.getInternalMoreResults() == false) && (stmt.getUpdateCount() == -1)) 
- * - * @return true if the next result is a ResultSet object; false - * if it is an update count or there are no more results - * @throws SQLException if a database access error occurs or this method is called on a closed - * Statement - * @see #execute - */ - public boolean getMoreResults() throws SQLException { - return getMoreResults(Statement.CLOSE_CURRENT_RESULT); - } - - /** - * Moves to this Statement object's next result, deals with any current - * ResultSet object(s) according to the instructions specified by the given flag, and - * returns true if the next result is a ResultSet object. There are no - * more results when the following is true: - * - *
 // stmt is a Statement object
-   * ((stmt.getInternalMoreResults(current) == false) && (stmt.getUpdateCount() == -1))
-   * 
- * - * @param current one of the following Statement constants indicating what should - * happen to current ResultSet objects obtained using the method - * getResultSet: Statement.CLOSE_CURRENT_RESULT, - * Statement.KEEP_CURRENT_RESULT, or Statement.CLOSE_ALL_RESULTS - * @return true if the next result is a ResultSet object; false - * if it is an update count or there are no more results - * @throws SQLException if a database access error occurs, this method is called on a closed - * Statement or the argument supplied is not one of the following: - * Statement.CLOSE_CURRENT_RESULT, Statement.KEEP_CURRENT_RESULT or - * Statement.CLOSE_ALL_RESULTS - * @see #execute - */ - public boolean getMoreResults(final int current) throws SQLException { - // if fetch size is set to read fully, other resultSet are put in cache - checkClose(); - return results != null && results.getMoreResults(current, protocol); - } - - /** - * Retrieves the direction for fetching rows from database tables that is the default for result - * sets generated from this Statement object. If this Statement object - * has not set a fetch direction by calling the method setFetchDirection, the return - * value is implementation-specific. - * - * @return the default fetch direction for result sets generated from this Statement - * object - * @see #setFetchDirection - * @since 1.2 - */ - public int getFetchDirection() { - return ResultSet.FETCH_FORWARD; - } - - /** - * Gives the driver a hint as to the direction in which rows will be processed in ResultSet - * objects created using this Statement object. The default value is - * ResultSet.FETCH_FORWARD. - * - *

Note that this method sets the default fetch direction for result sets generated by this - * Statement object. Each result set has its own methods for getting and setting its - * own fetch direction. - * - * @param direction the initial direction for processing rows - * @see #getFetchDirection - * @since 1.2 - */ - public void setFetchDirection(final int direction) { - // not implemented - } - - /** - * Retrieves the number of result set rows that is the default fetch size for ResultSet - * objects generated from this Statement object. If this Statement - * object has not set a fetch size by calling the method setFetchSize, the - * return value is implementation-specific. - * - * @return the default fetch size for result sets generated from this Statement - * object - * @see #setFetchSize - */ - public int getFetchSize() { - return this.fetchSize; - } - - /** - * Gives the JDBC driver a hint as to the number of rows that should be fetched from the database - * when more rows are needed for ResultSet objects generated by this Statement - * . If the value specified is zero, then the hint is ignored. The default value is zero. - * - * @param rows the number of rows to fetch - * @throws SQLException if a database access error occurs, this method is called on a closed - * Statement or the condition rows >= 0 is not satisfied. - * @see #getFetchSize - */ - public void setFetchSize(final int rows) throws SQLException { - if (rows < 0 && rows != Integer.MIN_VALUE) { - throw exceptionFactory.raiseStatementError(connection, this).create("invalid fetch size"); - } else if (rows == Integer.MIN_VALUE) { - // for compatibility Integer.MIN_VALUE is transform to 0 => streaming - this.fetchSize = 1; - return; - } - this.fetchSize = rows; - } - - /** - * Retrieves the result set concurrency for ResultSet objects generated by this - * Statement object. - * - * @return either ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE - * - * @since 1.2 - */ - public int getResultSetConcurrency() { - return resultSetConcurrency; - } - - /** - * Retrieves the result set type for ResultSet objects generated by this - * Statement object. - * - * @return one of ResultSet.TYPE_FORWARD_ONLY, - * ResultSet.TYPE_SCROLL_INSENSITIVE, or ResultSet.TYPE_SCROLL_SENSITIVE - */ - public int getResultSetType() { - return resultSetScrollType; - } - - /** - * Adds the given SQL command to the current list of commands for this Statement - * object. The send in this list can be executed as a batch by calling the method - * executeBatch. - * - * @param sql typically this is a SQL INSERT or UPDATE statement - * @throws SQLException if a database access error occurs, this method is called on a closed - * Statement or the driver does not support batch updates - * @see #executeBatch - * @see DatabaseMetaData#supportsBatchUpdates - */ - public void addBatch(final String sql) throws SQLException { - if (batchQueries == null) { - batchQueries = new ArrayList<>(); - } - if (sql == null) { - throw exceptionFactory - .raiseStatementError(connection, this) - .create("null cannot be set to addBatch( String sql)"); - } - batchQueries.add(sql); - } - - /** - * Empties this Statement object's current list of SQL send. - * - * @see #addBatch - * @see DatabaseMetaData#supportsBatchUpdates - * @since 1.2 - */ - public void clearBatch() { - if (batchQueries != null) { - batchQueries.clear(); - } - } - - /** - * Execute statements. depending on option, queries mays be rewritten : - * - *

those queries will be rewritten if possible to INSERT INTO ... VALUES (...) ; INSERT INTO - * ... VALUES (...); - * - *

if option rewriteBatchedStatements is set to true, rewritten to INSERT INTO ... VALUES - * (...), (...); - * - * @return an array of update counts containing one element for each command in the batch. The - * elements of the array are ordered according to the order in which send were added to the - * batch. - * @throws SQLException if a database access error occurs, this method is called on a closed - * Statement or the driver does not support batch statements. Throws {@link - * BatchUpdateException} (a subclass of SQLException) if one of the send sent to - * the database fails to execute properly or attempts to return a result set. - * @see #addBatch - * @see DatabaseMetaData#supportsBatchUpdates - * @since 1.3 - */ - public int[] executeBatch() throws SQLException { - checkClose(); - int size; - if (batchQueries == null || (size = batchQueries.size()) == 0) { - return new int[0]; - } - - lock.lock(); - try { - internalBatchExecution(size); - return results.getCmdInformation().getUpdateCounts(); - } catch (SQLException initialSqlEx) { - throw executeBatchExceptionEpilogue(initialSqlEx, size); - } finally { - executeBatchEpilogue(); - lock.unlock(); - } - } - - /** - * Execute batch, like executeBatch(), with returning results with long[]. For when row count may - * exceed Integer.MAX_VALUE. - * - * @return an array of update counts (one element for each command in the batch) - * @throws SQLException if a database error occur. - */ - @Override - public long[] executeLargeBatch() throws SQLException { - checkClose(); - int size; - if (batchQueries == null || (size = batchQueries.size()) == 0) { - return new long[0]; - } - - lock.lock(); - try { - internalBatchExecution(size); - return results.getCmdInformation().getLargeUpdateCounts(); - - } catch (SQLException initialSqlEx) { - throw executeBatchExceptionEpilogue(initialSqlEx, size); - } finally { - executeBatchEpilogue(); - lock.unlock(); - } - } - - /** - * Internal batch execution. - * - * @param size expected result-set size - * @throws SQLException throw exception if batch error occur - */ - private void internalBatchExecution(int size) throws SQLException { - - executeQueryPrologue(true); - results = - new Results( - this, - 0, - true, - size, - false, - resultSetScrollType, - resultSetConcurrency, - Statement.RETURN_GENERATED_KEYS, - protocol.getAutoIncrementIncrement(), - null, - null); - protocol.executeBatchStmt(protocol.isMasterConnection(), results, batchQueries); - results.commandEnd(); - } - - /** - * Returns an object that implements the given interface to allow access to non-standard methods, - * or standard methods not exposed by the proxy. - * - *

If the receiver implements the interface then the result is the receiver or a proxy for the - * receiver. If the receiver is a wrapper and the wrapped object implements the interface then the - * result is the wrapped object or a proxy for the wrapped object. Otherwise return the the result - * of calling unwrap recursively on the wrapped object or a proxy for that result. If - * the receiver is not a wrapper and does not implement the interface, then an SQLException - * is thrown. - * - * @param iface A Class defining an interface that the result must implement. - * @return an object that implements the interface. May be a proxy for the actual implementing - * object. - * @throws SQLException If no object found that implements the interface - * @since 1.6 - */ - @SuppressWarnings("unchecked") - public T unwrap(final Class iface) throws SQLException { - try { - if (isWrapperFor(iface)) { - return (T) this; - } else { - throw exceptionFactory - .raiseStatementError(connection, this) - .create("The receiver is not a wrapper and does not implement the interface", "42000"); - } - } catch (Exception e) { - throw exceptionFactory - .raiseStatementError(connection, this) - .create("The receiver is not a wrapper and does not implement the interface", "42000"); - } - } - - /** - * Returns true if this either implements the interface argument or is directly or indirectly a - * wrapper for an object that does. Returns false otherwise. If this implements the interface then - * return true, else if this is a wrapper then return the result of recursively calling - * isWrapperFor on the wrapped object. If this does not implement the interface and is not - * a wrapper, return false. This method should be implemented as a low-cost operation compared to - * unwrap so that callers can use this method to avoid expensive unwrap - * calls that may fail. If this method returns true then calling unwrap with the same - * argument should succeed. - * - * @param interfaceOrWrapper a Class defining an interface. - * @return true if this implements the interface or directly or indirectly wraps an object that - * does. - * @throws SQLException if an error occurs while determining whether this is a wrapper for an - * object with the given interface. - * @since 1.6 - */ - public boolean isWrapperFor(final Class interfaceOrWrapper) throws SQLException { - return interfaceOrWrapper.isInstance(this); - } - - public void closeOnCompletion() { - mustCloseOnCompletion = true; - } - - public boolean isCloseOnCompletion() { - return mustCloseOnCompletion; - } - - /** - * Check that close on completion is asked, and close if so. - * - * @param resultSet resultSet - * @throws SQLException if close has error - */ - public void checkCloseOnCompletion(ResultSet resultSet) throws SQLException { - if (mustCloseOnCompletion - && !closed - && results != null - && resultSet.equals(results.getResultSet())) { - close(); - } - } - - /** - * Check if statement is closed, and throw exception if so. - * - * @throws SQLException if statement close - */ - protected void checkClose() throws SQLException { - if (closed) { - throw exceptionFactory - .raiseStatementError(connection, this) - .create("Cannot do an operation on a closed statement"); - } - } -} diff --git a/src/main/java/org/mariadb/jdbc/MariaDbXid.java b/src/main/java/org/mariadb/jdbc/MariaDbXid.java deleted file mode 100644 index 55338d5e2..000000000 --- a/src/main/java/org/mariadb/jdbc/MariaDbXid.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.util.Arrays; -import javax.transaction.xa.Xid; - -public class MariaDbXid implements Xid { - - private final int formatId; - private final byte[] globalTransactionId; - private final byte[] branchQualifier; - - /** - * Global transaction identifier. - * - * @param formatId the format identifier part of the XID. - * @param globalTransactionId the global transaction identifier part of XID as an array of bytes. - * @param branchQualifier the transaction branch identifier part of XID as an array of bytes. - */ - public MariaDbXid(int formatId, byte[] globalTransactionId, byte[] branchQualifier) { - this.formatId = formatId; - this.globalTransactionId = globalTransactionId; - this.branchQualifier = branchQualifier; - } - - /** - * Equal implementation. - * - * @param obj object to compare - * @return true if object is MariaDbXi and as same parameters - */ - public boolean equals(Object obj) { - if (obj instanceof Xid) { - Xid other = (Xid) obj; - return formatId == other.getFormatId() - && Arrays.equals(globalTransactionId, other.getGlobalTransactionId()) - && Arrays.equals(branchQualifier, other.getBranchQualifier()); - } - return false; - } - - public int getFormatId() { - return formatId; - } - - public byte[] getGlobalTransactionId() { - return globalTransactionId; - } - - public byte[] getBranchQualifier() { - return branchQualifier; - } -} diff --git a/src/main/java/org/mariadb/jdbc/MariaXaConnection.java b/src/main/java/org/mariadb/jdbc/MariaXaConnection.java deleted file mode 100644 index 07542dbf6..000000000 --- a/src/main/java/org/mariadb/jdbc/MariaXaConnection.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import javax.sql.XAConnection; -import javax.transaction.xa.XAResource; - -public class MariaXaConnection extends MariaDbPooledConnection implements XAConnection { - - public MariaXaConnection(MariaDbConnection connection) { - super(connection); - } - - @Override - public XAResource getXAResource() { - return new MariaXaResource(getConnection()); - } -} diff --git a/src/main/java/org/mariadb/jdbc/MariaXaResource.java b/src/main/java/org/mariadb/jdbc/MariaXaResource.java deleted file mode 100644 index f918561da..000000000 --- a/src/main/java/org/mariadb/jdbc/MariaXaResource.java +++ /dev/null @@ -1,336 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import javax.transaction.xa.XAException; -import javax.transaction.xa.XAResource; -import javax.transaction.xa.Xid; -import org.mariadb.jdbc.internal.util.Utils; - -public class MariaXaResource implements XAResource { - - private final MariaDbConnection connection; - - public MariaXaResource(MariaDbConnection connection) { - this.connection = connection; - } - - protected static String xidToString(Xid xid) { - return "0x" - + Utils.byteArrayToHexString(xid.getGlobalTransactionId()) - + ",0x" - + Utils.byteArrayToHexString(xid.getBranchQualifier()) - + ",0x" - + Utils.intToHexString(xid.getFormatId()); - } - - private static String flagsToString(int flags) { - switch (flags) { - case TMJOIN: - return "JOIN"; - case TMONEPHASE: - return "ONE PHASE"; - case TMRESUME: - return "RESUME"; - case TMSUSPEND: - return "SUSPEND"; - default: - return ""; - } - } - - private XAException mapXaException(SQLException sqle) { - int xaErrorCode; - - switch (sqle.getErrorCode()) { - case 1397: - xaErrorCode = XAException.XAER_NOTA; - break; - case 1398: - xaErrorCode = XAException.XAER_INVAL; - break; - case 1399: - xaErrorCode = XAException.XAER_RMFAIL; - break; - case 1400: - xaErrorCode = XAException.XAER_OUTSIDE; - break; - case 1401: - xaErrorCode = XAException.XAER_RMERR; - break; - case 1402: - xaErrorCode = XAException.XA_RBROLLBACK; - break; - default: - xaErrorCode = 0; - break; - } - XAException xaException; - if (xaErrorCode != 0) { - xaException = new XAException(xaErrorCode); - } else { - xaException = new XAException(sqle.getMessage()); - } - xaException.initCause(sqle); - return xaException; - } - - /** - * Execute a query. - * - * @param command query to run. - * @throws XAException exception - */ - private void execute(String command) throws XAException { - try { - connection.createStatement().execute(command); - } catch (SQLException sqle) { - throw mapXaException(sqle); - } - } - - /** - * Commits the global transaction specified by xid. - * - * @param xid A global transaction identifier - * @param onePhase If true, the resource manager should use a one-phase commit protocol to commit - * the work done on behalf of xid. - * @throws XAException exception - */ - public void commit(Xid xid, boolean onePhase) throws XAException { - String command = "XA COMMIT " + xidToString(xid); - if (onePhase) { - command += " ONE PHASE"; - } - execute(command); - } - - /** - * Ends the work performed on behalf of a transaction branch. The resource manager disassociates - * the XA resource from the transaction branch specified and lets the transaction complete. - * - *

If TMSUSPEND is specified in the flags, the transaction branch is temporarily suspended in - * an incomplete state. The transaction context is in a suspended state and must be resumed via - * the start method with TMRESUME specified. - * - *

If TMFAIL is specified, the portion of work has failed. The resource manager may mark the - * transaction as rollback-only - * - *

If TMSUCCESS is specified, the portion of work has completed successfully. - * - * @param xid A global transaction identifier that is the same as the identifier used previously - * in the start method. - * @param flags One of TMSUCCESS, TMFAIL, or TMSUSPEND. - * @throws XAException An error has occurred. (XAException values are XAER_RMERR, XAER_RMFAILED, - * XAER_NOTA, XAER_INVAL, XAER_PROTO, or XA_RB*) - */ - public void end(Xid xid, int flags) throws XAException { - if (flags != TMSUCCESS && flags != TMSUSPEND && flags != TMFAIL) { - throw new XAException(XAException.XAER_INVAL); - } - - execute("XA END " + xidToString(xid) + " " + flagsToString(flags)); - } - - /** - * Tells the resource manager to forget about a heuristically completed transaction branch. - * - * @param xid A global transaction identifier. - */ - public void forget(Xid xid) { - // Not implemented by the server - } - - /** - * Obtains the current transaction timeout value set for this XAResource instance. If - * XAResource.setTransactionTimeout was not used prior to invoking this method, the return value - * is the default timeout set for the resource manager; otherwise, the value used in the previous - * setTransactionTimeout call is returned. - * - * @return the transaction timeout value in seconds. - */ - public int getTransactionTimeout() { - // not implemented - return 0; - } - - /** - * This method is called to determine if the resource manager instance represented by the target - * object is the same as the resource manager instance represented by the parameter xares. - * - * @param xaResource An XAResource object whose resource manager instance is to be compared with - * the target object. - * @return true if it's the same RM instance; otherwise false. - */ - @Override - public boolean isSameRM(XAResource xaResource) { - // Typically used by transaction manager to "join" transactions. We do not support joins, - // so always return false; - return false; - } - - /** - * Ask the resource manager to prepare for a transaction commit of the transaction specified in - * xid. - * - * @param xid A global transaction identifier. - * @return A value indicating the resource manager's vote on the outcome of the transaction. - * @throws XAException An error has occurred. Possible exception values are: XA_RB*, XAER_RMERR, - * XAER_RMFAIL, XAER_NOTA, XAER_INVAL, XAER_PROTO. - */ - public int prepare(Xid xid) throws XAException { - execute("XA PREPARE " + xidToString(xid)); - return XA_OK; - } - - /** - * Obtains a list of prepared transaction branches from a resource manager. The transaction - * manager calls this method during recovery to obtain the list of transaction branches that are - * currently in prepared or heuristically completed states. - * - * @param flags One of TMSTARTRSCAN, TMENDRSCAN, TMNOFLAGS. TMNOFLAGS must be used when no other - * flags are set in the parameter. - * @return The resource manager returns zero or more XIDs of the transaction branches. - * @throws XAException An error has occurred. Possible values are XAER_RMERR, XAER_RMFAIL, - * XAER_INVAL, and XAER_PROTO. - */ - public Xid[] recover(int flags) throws XAException { - // Return all Xid at once, when STARTRSCAN is specified - // Return zero-length array otherwise. - - if (((flags & TMSTARTRSCAN) == 0) && ((flags & TMENDRSCAN) == 0) && (flags != TMNOFLAGS)) { - throw new XAException(XAException.XAER_INVAL); - } - - if ((flags & TMSTARTRSCAN) == 0) { - return new MariaDbXid[0]; - } - - try { - ResultSet rs = connection.createStatement().executeQuery("XA RECOVER"); - ArrayList xidList = new ArrayList<>(); - - while (rs.next()) { - int formatId = rs.getInt(1); - int len1 = rs.getInt(2); - int len2 = rs.getInt(3); - byte[] arr = rs.getBytes(4); - - byte[] globalTransactionId = new byte[len1]; - byte[] branchQualifier = new byte[len2]; - System.arraycopy(arr, 0, globalTransactionId, 0, len1); - System.arraycopy(arr, len1, branchQualifier, 0, len2); - xidList.add(new MariaDbXid(formatId, globalTransactionId, branchQualifier)); - } - Xid[] xids = new Xid[xidList.size()]; - xidList.toArray(xids); - return xids; - } catch (SQLException sqle) { - throw mapXaException(sqle); - } - } - - /** - * Informs the resource manager to roll back work done on behalf of a transaction branch. - * - * @param xid A global transaction identifier. - * @throws XAException An error has occurred. - */ - public void rollback(Xid xid) throws XAException { - execute("XA ROLLBACK " + xidToString(xid)); - } - - /** - * Sets the current transaction timeout value for this XAResource instance. Once set, this timeout - * value is effective until setTransactionTimeout is invoked again with a different value. To - * reset the timeout value to the default value used by the resource manager, set the value to - * zero. If the timeout operation is performed successfully, the method returns true; otherwise - * false. If a resource manager does not support explicitly setting the transaction timeout value, - * this method returns false. - * - * @param timeout The transaction timeout value in seconds. - * @return true if the transaction timeout value is set successfully; otherwise false. - */ - public boolean setTransactionTimeout(int timeout) { - return false; // not implemented - } - - /** - * Starts work on behalf of a transaction branch specified in xid. If TMJOIN is specified, the - * start applies to joining a transaction previously seen by the resource manager. If TMRESUME is - * specified, the start applies to resuming a suspended transaction specified in the parameter - * xid. If neither TMJOIN nor TMRESUME is specified and the transaction specified by xid has - * previously been seen by the resource manager, the resource manager throws the XAException - * exception with XAER_DUPID error code. - * - * @param xid A global transaction identifier to be associated with the resource. - * @param flags One of TMNOFLAGS, TMJOIN, or TMRESUME. - * @throws XAException An error has occurred. - */ - public void start(Xid xid, int flags) throws XAException { - if (flags != TMJOIN && flags != TMRESUME && flags != TMNOFLAGS) { - throw new XAException(XAException.XAER_INVAL); - } - execute( - "XA START " - + xidToString(xid) - + " " - + flagsToString( - flags == TMJOIN && connection.getPinGlobalTxToPhysicalConnection() - ? TMRESUME - : flags)); - } -} diff --git a/src/main/java/org/mariadb/jdbc/MySQLDataSource.java b/src/main/java/org/mariadb/jdbc/MySQLDataSource.java deleted file mode 100644 index 49fa79fa8..000000000 --- a/src/main/java/org/mariadb/jdbc/MySQLDataSource.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -/** - * Keep the class name for compatibility - * - * @deprecated use class MariaDbDataSource directly - */ -public class MySQLDataSource extends MariaDbDataSource { - - /** - * Constructor. - * - * @param hostname hostname (ipv4, ipv6, dns name) - * @param port server port - * @param database database name - */ - public MySQLDataSource(String hostname, int port, String database) { - super(hostname, port, database); - } - - public MySQLDataSource(String url) { - super(url); - } - - /** Default constructor. hostname will be localhost, port 3306. */ - public MySQLDataSource() { - super(); - } -} diff --git a/src/main/java/org/mariadb/jdbc/ParameterMetaData.java b/src/main/java/org/mariadb/jdbc/ParameterMetaData.java new file mode 100644 index 000000000..786f6756d --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/ParameterMetaData.java @@ -0,0 +1,202 @@ +package org.mariadb.jdbc; + +import java.sql.SQLException; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; + +public class ParameterMetaData implements java.sql.ParameterMetaData { + + private final ColumnDefinitionPacket[] params; + private final ExceptionFactory exceptionFactory; + + public ParameterMetaData(ExceptionFactory exceptionFactory, ColumnDefinitionPacket[] params) { + this.params = params; + this.exceptionFactory = exceptionFactory; + } + + /** + * Retrieves the number of parameters in the PreparedStatement object for which this + * ParameterMetaData object contains information. + * + * @return the number of parameters + */ + @Override + public int getParameterCount() { + return params.length; + } + + private void checkIndex(int index) throws SQLException { + if (index < 1 || index > params.length) { + throw new SQLException( + String.format( + "Wrong index position. Is %s but must be in 1-%s range", index, params.length)); + } + } + + /** + * Retrieves whether null values are allowed in the designated parameter. + * + * @param idx the first parameter is 1, the second is 2, ... + * @return the nullability status of the given parameter; one of + * ParameterMetaData.parameterNoNulls, ParameterMetaData.parameterNullable + * @throws SQLException if wrong index + */ + @Override + public int isNullable(int idx) throws SQLException { + checkIndex(idx); + if (params[idx - 1].isNullable()) { + return java.sql.ParameterMetaData.parameterNullable; + } else { + return java.sql.ParameterMetaData.parameterNoNulls; + } + } + + /** + * Retrieves whether values for the designated parameter can be signed numbers. + * + * @param idx the first parameter is 1, the second is 2, ... + * @return true if so; false otherwise + * @throws SQLException if wrong index + */ + @Override + public boolean isSigned(int idx) throws SQLException { + checkIndex(idx); + return params[idx - 1].isSigned(); + } + + /** + * Retrieves the designated parameter's specified column size. + * + *

The returned value represents the maximum column size for the given parameter. For numeric + * data, this is the maximum precision. For character data, this is the length in characters. For + * datetime datatypes, this is the length in characters of the String representation (assuming the + * maximum allowed precision of the fractional seconds component). For binary data, this is the + * length in bytes. For the ROWID datatype, this is the length in bytes. 0 is returned for data + * types where the column size is not applicable. + * + * @param idx the first parameter is 1, the second is 2, ... + * @return precision + * @throws SQLException if wrong index + */ + @Override + public int getPrecision(int idx) throws SQLException { + checkIndex(idx); + return (int) params[idx - 1].getPrecision(); + } + + /** + * Retrieves the designated parameter's number of digits to right of the decimal point. 0 is + * returned for data types where the scale is not applicable. Parameter type are not sent by + * server. See * https://jira.mariadb.org/browse/CONJ-568 and + * https://jira.mariadb.org/browse/MDEV-15031 + * + * @param idx the first parameter is 1, the second is 2, ... + * @return scale + * @throws SQLException if a database access error occurs + */ + @Override + public int getScale(int idx) throws SQLException { + checkIndex(idx); + return params[idx - 1].getDecimals(); + } + + /** + * Retrieves the designated parameter's SQL type. Parameter type are not sent by server. See + * https://jira.mariadb.org/browse/CONJ-568 and https://jira.mariadb.org/browse/MDEV-15031 + * + * @param param the first parameter is 1, the second is 2, ... + * @return SQL type from java.sql.Types + * @throws SQLException because not supported + */ + @Override + public int getParameterType(int param) throws SQLException { + throw exceptionFactory.create("Getting parameter type metadata are not supported", "0A000", -1); + } + + /** + * Retrieves the designated parameter's database-specific type name. + * + * @param idx the first parameter is 1, the second is 2, ... + * @return type the name used by the database. If the parameter type is a user-defined type, then + * a fully-qualified type name is returned. + * @throws SQLException if wrong index + */ + @Override + public String getParameterTypeName(int idx) throws SQLException { + checkIndex(idx); + return params[idx - 1].getType().name(); + } + + /** + * Retrieves the fully-qualified name of the Java class whose instances should be passed to the + * method PreparedStatement.setObject. + * + * @param idx the first parameter is 1, the second is 2, ... + * @return the fully-qualified name of the class in the Java programming language that would be + * used by the method PreparedStatement.setObject to set the value in the + * specified parameter. This is the class name used for custom mapping. + * @throws SQLException if wrong index + */ + @Override + public String getParameterClassName(int idx) throws SQLException { + checkIndex(idx); + throw exceptionFactory.create("Unknown parameter metadata class name", "0A000"); + } + + /** + * Retrieves the designated parameter's mode. + * + * @param idx the first parameter is 1, the second is 2, ... + * @return mode of the parameter; one of ParameterMetaData.parameterModeIn, + * ParameterMetaData.parameterModeOut, or ParameterMetaData.parameterModeInOut + * ParameterMetaData.parameterModeUnknown. + */ + @Override + public int getParameterMode(int idx) throws SQLException { + checkIndex(idx); + return java.sql.ParameterMetaData.parameterModeIn; + } + + /** + * Returns an object that implements the given interface to allow access to non-standard methods, + * or standard methods not exposed by the proxy. + * + *

If the receiver implements the interface then the result is the receiver or a proxy for the + * receiver. If the receiver is a wrapper and the wrapped object implements the interface then the + * result is the wrapped object or a proxy for the wrapped object. Otherwise return the the result + * of calling unwrap recursively on the wrapped object or a proxy for that result. If + * the receiver is not a wrapper and does not implement the interface, then an SQLException + * is thrown. + * + * @param iface A Class defining an interface that the result must implement. + * @return an object that implements the interface. May be a proxy for the actual implementing + * object. + * @throws SQLException If no object found that implements the interface + */ + @Override + public T unwrap(Class iface) throws SQLException { + if (isWrapperFor(iface)) { + return iface.cast(this); + } + throw new SQLException("The receiver is not a wrapper for " + iface.getName()); + } + + /** + * Returns true if this either implements the interface argument or is directly or indirectly a + * wrapper for an object that does. Returns false otherwise. If this implements the interface then + * return true, else if this is a wrapper then return the result of recursively calling + * isWrapperFor on the wrapped object. If this does not implement the interface and is not + * a wrapper, return false. This method should be implemented as a low-cost operation compared to + * unwrap so that callers can use this method to avoid expensive unwrap + * calls that may fail. If this method returns true then calling unwrap with the same + * argument should succeed. + * + * @param iface a Class defining an interface. + * @return true if this implements the interface or directly or indirectly wraps an object that + * does. + */ + @Override + public boolean isWrapperFor(Class iface) { + return iface.isInstance(this); + } +} diff --git a/src/main/java/org/mariadb/jdbc/ProcedureStatement.java b/src/main/java/org/mariadb/jdbc/ProcedureStatement.java new file mode 100644 index 000000000..619f92b13 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/ProcedureStatement.java @@ -0,0 +1,87 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +import java.sql.CallableStatement; +import java.sql.SQLException; +import java.util.concurrent.locks.ReentrantLock; +import org.mariadb.jdbc.client.ServerVersion; +import org.mariadb.jdbc.client.result.Result; +import org.mariadb.jdbc.message.server.Completion; + +public class ProcedureStatement extends BaseCallableStatement implements CallableStatement { + + public ProcedureStatement( + Connection con, + String sql, + String databaseName, + String procedureName, + ReentrantLock lock, + boolean canUseServerTimeout, + boolean canUseServerMaxRows, + int resultSetType, + int resultSetConcurrency) + throws SQLException { + super( + sql, + con, + lock, + databaseName, + procedureName, + canUseServerTimeout, + canUseServerMaxRows, + resultSetType, + resultSetConcurrency, + 0); + } + + @Override + public boolean isFunction() { + return true; + } + + @Override + protected void handleParameterOutput() throws SQLException { + // resultset might be ended by an OK_packet + // note. correction for MySQL 5.6 that wrongly remove PS_OUT_PARAMETERS flag of resultset. + + ServerVersion version = con.getContext().getVersion(); + Completion compl = this.results.get(this.results.size() - 1); + if (compl instanceof Result + && (((Result) compl).isOutputParameter() + || (version.isMariaDBServer() && !version.versionGreaterOrEqual(10, 2, 0)) + || (!version.isMariaDBServer() && !version.versionGreaterOrEqual(5, 7, 0)))) { + this.outputResult = (Result) compl; + this.results.remove(this.results.size() - 1); + } else if (this.results.size() > 1) { + compl = this.results.get(this.results.size() - 2); + if (compl instanceof Result + && (((Result) compl).isOutputParameter() + || (version.isMariaDBServer() && !version.versionGreaterOrEqual(10, 2, 0)) + || (!version.isMariaDBServer() && !version.versionGreaterOrEqual(5, 7, 0)))) { + this.outputResult = (Result) compl; + this.results.remove(this.results.size() - 2); + } + } + this.outputResult.next(); + } +} diff --git a/src/main/java/org/mariadb/jdbc/ServerPreparedStatement.java b/src/main/java/org/mariadb/jdbc/ServerPreparedStatement.java new file mode 100644 index 000000000..25588261e --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/ServerPreparedStatement.java @@ -0,0 +1,659 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +import java.sql.*; +import java.util.*; +import java.util.concurrent.locks.ReentrantLock; +import javax.sql.StatementEvent; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.result.CompleteResult; +import org.mariadb.jdbc.client.result.Result; +import org.mariadb.jdbc.message.client.BulkExecutePacket; +import org.mariadb.jdbc.message.client.ClientMessage; +import org.mariadb.jdbc.message.client.ExecutePacket; +import org.mariadb.jdbc.message.client.PreparePacket; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.message.server.Completion; +import org.mariadb.jdbc.message.server.OkPacket; +import org.mariadb.jdbc.message.server.PrepareResultPacket; +import org.mariadb.jdbc.util.ParameterList; +import org.mariadb.jdbc.util.constants.Capabilities; + +public class ServerPreparedStatement extends BasePreparedStatement { + + protected PrepareResultPacket prepareResult; + + public ServerPreparedStatement( + String sql, + Connection con, + ReentrantLock lock, + boolean canUseServerTimeout, + boolean canUseServerMaxRows, + int autoGeneratedKeys, + int resultSetType, + int resultSetConcurrency, + int defaultFetchSize) + throws SQLException { + super( + sql, + con, + lock, + canUseServerTimeout, + canUseServerMaxRows, + autoGeneratedKeys, + resultSetType, + resultSetConcurrency, + defaultFetchSize); + + // MySQL server doesn't permit pipelining + if (!con.getContext().getVersion().isMariaDBServer()) { + prepareIfNotAlready(escapeTimeout(sql)); + } + parameters = new ParameterList(); + } + + private void prepareIfNotAlready(String cmd) throws SQLException { + if (prepareResult == null) { + prepareResult = con.getContext().getPrepareCache().get(cmd, this); + if (prepareResult == null) { + prepareResult = + (PrepareResultPacket) con.getClient().execute(new PreparePacket(cmd), this).get(0); + } + } + } + + protected void executeInternal() throws SQLException { + checkNotClosed(); + validParameters(); + lock.lock(); + String cmd = escapeTimeout(sql); + if (prepareResult == null) prepareResult = con.getContext().getPrepareCache().get(cmd, this); + if (prepareResult == null) { + try { + long serverCapabilities = con.getContext().getServerCapabilities(); + if ((serverCapabilities & Capabilities.MARIADB_CLIENT_STMT_BULK_OPERATIONS) > 0) { + executePipeline(cmd); + } else { + executeStandard(cmd); + } + } finally { + parameters = new ParameterList(); + lock.unlock(); + } + } else { + try { + ExecutePacket execute = new ExecutePacket(prepareResult, parameters, cmd, this); + results = + con.getClient() + .execute( + execute, + this, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion); + } finally { + parameters = new ParameterList(); + lock.unlock(); + } + } + } + + /** + * Send COM_STMT_PREPARE + COM_STMT_EXECUTE, then read for the 2 answers + * + * @param cmd command + * @throws SQLException if IOException / Command error + */ + private void executePipeline(String cmd) throws SQLException { + // server is 10.2+, permitting to execute last prepare with (-1) statement id. + // Server send prepare, followed by execute, in one exchange. + PreparePacket prepare = new PreparePacket(cmd); + ExecutePacket execute = new ExecutePacket(null, parameters, cmd, this); + try { + List res = + con.getClient() + .executePipeline( + new ClientMessage[] {prepare, execute}, + this, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion); + prepareResult = (PrepareResultPacket) res.get(0); + results = res.subList(1, res.size()); + } catch (SQLException ex) { + results = null; + throw ex; + } + } + + private void executeStandard(String cmd) throws SQLException { + // send COM_STMT_PREPARE + prepareIfNotAlready(cmd); + + // send COM_STMT_EXECUTE + ExecutePacket execute = new ExecutePacket(prepareResult, parameters, cmd, this); + results = + con.getClient() + .execute( + execute, + this, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion); + } + + private List executeInternalPreparedBatch() throws SQLException { + checkNotClosed(); + String cmd = escapeTimeout(sql); + if (prepareResult == null) prepareResult = con.getContext().getPrepareCache().get(cmd, this); + + long serverCapabilities = con.getContext().getServerCapabilities(); + if ((serverCapabilities & Capabilities.MARIADB_CLIENT_STMT_BULK_OPERATIONS) > 0) { + return con.getContext().getConf().useBulkStmts() + && autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS + ? executeBatchBulk(cmd) + : executeBatchPipeline(cmd); + } else { + return executeBatchStandard(cmd); + } + } + + /** + * Send COM_STMT_PREPARE + X * COM_STMT_BULK_EXECUTE, then read for the all answers + * + * @param cmd command + * @throws SQLException if IOException / Command error + */ + private List executeBatchBulk(String cmd) throws SQLException { + ClientMessage[] packets; + if (prepareResult == null) { + packets = + new ClientMessage[] { + new PreparePacket(cmd), new BulkExecutePacket(null, batchParameters, cmd, this) + }; + } else { + packets = new ClientMessage[] {new BulkExecutePacket(null, batchParameters, cmd, this)}; + } + try { + List res = + con.getClient() + .executePipeline( + packets, + this, + 0, + maxRows, + ResultSet.CONCUR_READ_ONLY, + ResultSet.TYPE_FORWARD_ONLY, + closeOnCompletion); + // in case of failover, prepare is done in failover, skipping prepare result + if (prepareResult == null && res.get(0) instanceof PrepareResultPacket) { + prepareResult = (PrepareResultPacket) res.get(0); + results = res.subList(1, res.size()); + } else { + results = res; + } + return results; + } catch (SQLException bue) { + results = null; + throw bue; + } + } + + /** + * Send COM_STMT_PREPARE + X * COM_STMT_EXECUTE, then read for the all answers + * + * @param cmd command + * @throws SQLException if Command error + */ + private List executeBatchPipeline(String cmd) throws SQLException { + // server is 10.2+, permitting to execute last prepare with (-1) statement id. + // Server send prepare, followed by execute, in one exchange. + int maxCmd = 250; + List res = new ArrayList<>(); + try { + int index = 0; + if (prepareResult == null) { + res.addAll(executeBunchPrepare(cmd, index, maxCmd)); + index += maxCmd; + } + + while (index < batchParameters.size()) { + res.addAll(executeBunch(cmd, index, maxCmd)); + index += maxCmd; + } + + results = res; + return results; + + } catch (SQLException bue) { + results = null; + throw bue; + } + } + + private List executeBunch(String cmd, int index, int maxCmd) throws SQLException { + int maxCmdToSend = Math.min(batchParameters.size() - index, maxCmd); + ClientMessage[] packets = new ClientMessage[maxCmdToSend]; + for (int i = index; i < index + maxCmdToSend; i++) { + packets[i - index] = new ExecutePacket(prepareResult, batchParameters.get(i), cmd, this); + } + return con.getClient() + .executePipeline( + packets, + this, + 0, + maxRows, + ResultSet.CONCUR_READ_ONLY, + ResultSet.TYPE_FORWARD_ONLY, + closeOnCompletion); + } + + private List executeBunchPrepare(String cmd, int index, int maxCmd) + throws SQLException { + int maxCmdToSend = Math.min(batchParameters.size() - index, maxCmd); + ClientMessage[] packets = new ClientMessage[maxCmdToSend + 1]; + packets[0] = new PreparePacket(cmd); + for (int i = index; i < index + maxCmdToSend; i++) { + packets[i + 1 - index] = new ExecutePacket(null, batchParameters.get(i), cmd, this); + } + List res = + con.getClient() + .executePipeline( + packets, + this, + 0, + maxRows, + ResultSet.CONCUR_READ_ONLY, + ResultSet.TYPE_FORWARD_ONLY, + closeOnCompletion); + // in case of failover, prepare is done in failover, skipping prepare result + if (res.get(0) instanceof PrepareResultPacket) { + prepareResult = (PrepareResultPacket) res.get(0); + return res.subList(1, res.size()); + } else { + return res; + } + } + + /** + * Send COM_STMT_PREPARE + read answer, then Send a COM_STMT_EXECUTE + read answer * n time + * + * @param cmd command + * @throws SQLException if IOException / Command error + */ + private List executeBatchStandard(String cmd) throws SQLException { + // send COM_STMT_PREPARE + List tmpResults = new ArrayList<>(); + SQLException error = null; + for (int i = 0; i < batchParameters.size(); i++) { + // prepare is in loop, because if connection fail, prepare is reset, and need to be re + // prepared + if (prepareResult == null) { + prepareResult = con.getContext().getPrepareCache().get(cmd, this); + if (prepareResult == null) { + prepareResult = + (PrepareResultPacket) con.getClient().execute(new PreparePacket(cmd), this).get(0); + } + } + try { + ExecutePacket execute = new ExecutePacket(prepareResult, batchParameters.get(i), cmd, this); + tmpResults.addAll(con.getClient().execute(execute, this)); + } catch (SQLException e) { + if (error == null) error = e; + } + } + + if (error != null) { + throw exceptionFactory().createBatchUpdate(tmpResults, batchParameters.size(), error); + } + this.results = tmpResults; + return tmpResults; + } + + /** + * Executes the SQL statement in this PreparedStatement object, which may be any kind + * of SQL statement. Some prepared statements return multiple results; the execute + * method handles these complex statements as well as the simpler form of statements handled by + * the methods executeQuery and executeUpdate. + * + *

The execute method returns a boolean to indicate the form of the + * first result. You must call either the method getResultSet or getUpdateCount + * to retrieve the result; you must call getMoreResults to move to any + * subsequent result(s). + * + * @return true if the first result is a ResultSet object; false + * if the first result is an update count or there is no result + * @throws SQLException if a database access error occurs; this method is called on a closed + * PreparedStatement or an argument is supplied to this method + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + * @see Statement#execute + * @see Statement#getResultSet + * @see Statement#getUpdateCount + * @see Statement#getMoreResults + */ + @Override + public boolean execute() throws SQLException { + executeInternal(); + handleParameterOutput(); + if (results.size() > 0) { + currResult = results.remove(0); + return currResult instanceof Result; + } + return false; + } + + @Override + public void setMaxRows(int max) throws SQLException { + super.setMaxRows(max); + if (canUseServerMaxRows && prepareResult != null) { + prepareResult.decrementUse(con.getClient(), this); + prepareResult = null; + } + } + + @Override + public void setLargeMaxRows(long max) throws SQLException { + super.setLargeMaxRows(max); + if (canUseServerMaxRows && prepareResult != null) { + prepareResult.decrementUse(con.getClient(), this); + prepareResult = null; + } + } + + @Override + public void setQueryTimeout(int seconds) throws SQLException { + super.setQueryTimeout(seconds); + if (prepareResult != null) { + prepareResult.decrementUse(con.getClient(), this); + prepareResult = null; + } + } + + /** + * Executes the SQL query in this PreparedStatement object and returns the + * ResultSet object generated by the query. + * + * @return a ResultSet object that contains the data produced by the query; never + * null + * @throws SQLException if a database access error occurs; this method is called on a closed + * PreparedStatement or the SQL statement does not return a ResultSet + * object + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + */ + @Override + public ResultSet executeQuery() throws SQLException { + executeInternal(); + handleParameterOutput(); + if (results.size() > 0) { + currResult = results.remove(0); + if (currResult instanceof Result) return (Result) currResult; + } + return new CompleteResult( + new ColumnDefinitionPacket[0], new ReadableByteBuf[0], con.getContext()); + } + + /** + * Executes the SQL statement in this PreparedStatement object, which must be an SQL + * Data Manipulation Language (DML) statement, such as INSERT, UPDATE or + * DELETE; or an SQL statement that returns nothing, such as a DDL statement. + * + * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0 + * for SQL statements that return nothing + * @throws SQLException if a database access error occurs; this method is called on a closed + * PreparedStatement or the SQL statement returns a ResultSet object + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + */ + @Override + public int executeUpdate() throws SQLException { + return (int) executeLargeUpdate(); + } + + /** + * Executes the SQL statement in this PreparedStatement object, which must be an SQL + * Data Manipulation Language (DML) statement, such as INSERT, UPDATE or + * DELETE; or an SQL statement that returns nothing, such as a DDL statement. + * + *

This method should be used when the returned row count may exceed {@link Integer#MAX_VALUE}. + * + *

The default implementation will throw {@code UnsupportedOperationException} + * + * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0 + * for SQL statements that return nothing + * @throws SQLException if a database access error occurs; this method is called on a closed + * PreparedStatement or the SQL statement returns a ResultSet object + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + * @since 1.8 + */ + @Override + public long executeLargeUpdate() throws SQLException { + executeInternal(); + handleParameterOutput(); + currResult = results.remove(0); + if (currResult instanceof Result) { + throw exceptionFactory() + .create("the given SQL statement produces an unexpected ResultSet object", "HY000"); + } + return ((OkPacket) currResult).getAffectedRows(); + } + + protected void handleParameterOutput() throws SQLException {} + + /** + * Adds a set of parameters to this PreparedStatement object's batch of commands. + * + * @throws SQLException if a database access error occurs or this method is called on a closed + * PreparedStatement + * @see Statement#addBatch + * @since 1.2 + */ + @Override + public void addBatch() throws SQLException { + validParameters(); + if (batchParameters == null) batchParameters = new ArrayList<>(); + batchParameters.add(parameters); + parameters = new ParameterList(parameters.size()); + } + + protected void validParameters() throws SQLException { + if (prepareResult != null) { + for (int i = 0; i < prepareResult.getParameters().length; i++) { + if (!parameters.containsKey(i)) { + throw exceptionFactory() + .create("Parameter at position " + (i + 1) + " is not set", "07004"); + } + } + } else { + + if (batchParameters != null + && !batchParameters.isEmpty() + && parameters.size() < batchParameters.get(0).size()) { + // ensure batch parameters set same number + throw exceptionFactory() + .create( + "batch set of parameters differ from previous set. All parameters must be set", + "07004"); + } + + // ensure all parameters are set + for (int i = 0; i < parameters.size(); i++) { + if (!parameters.containsKey(i)) { + throw exceptionFactory() + .create("Parameter at position " + (i + 1) + " is not set", "07004"); + } + } + } + } + + /** + * Retrieves a ResultSetMetaData object that contains information about the columns + * of the ResultSet object that will be returned when this PreparedStatement + * object is executed. + * + *

Because a PreparedStatement object is precompiled, it is possible to know about + * the ResultSet object that it will return without having to execute it. + * Consequently, it is possible to invoke the method getMetaData on a + * PreparedStatement object rather than waiting to execute it and then invoking the + * ResultSet.getMetaData method on the ResultSet object that is returned. + * + *

NOTE: Using this method may be expensive for some drivers due to the lack of + * underlying DBMS support. + * + * @return the description of a ResultSet object's columns or null if + * the driver cannot return a ResultSetMetaData object + * @throws SQLException if a database access error occurs or this method is called on a closed + * PreparedStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.2 + */ + @Override + public ResultSetMetaData getMetaData() throws SQLException { + + // send COM_STMT_PREPARE + if (prepareResult == null) { + prepareResult = + (PrepareResultPacket) + con.getClient().execute(new PreparePacket(escapeTimeout(sql)), this).get(0); + } + + return new org.mariadb.jdbc.client.result.ResultSetMetaData( + exceptionFactory(), prepareResult.getColumns(), con.getContext().getConf(), false, false); + } + + /** + * Retrieves the number, types and properties of this PreparedStatement object's + * parameters. + * + * @return a ParameterMetaData object that contains information about the number, + * types and properties for each parameter marker of this PreparedStatement + * object + * @throws SQLException if a database access error occurs or this method is called on a closed + * PreparedStatement + * @see ParameterMetaData + * @since 1.4 + */ + @Override + public java.sql.ParameterMetaData getParameterMetaData() throws SQLException { + // send COM_STMT_PREPARE + if (prepareResult == null) { + prepareResult = + (PrepareResultPacket) + con.getClient().execute(new PreparePacket(escapeTimeout(sql)), this).get(0); + } + + return new ParameterMetaData(exceptionFactory(), prepareResult.getParameters()); + } + + @Override + public int[] executeBatch() throws SQLException { + checkNotClosed(); + if (batchParameters == null || batchParameters.isEmpty()) return new int[0]; + lock.lock(); + try { + List res = executeInternalPreparedBatch(); + results = res; + + int[] updates = new int[batchParameters.size()]; + if (res.size() != batchParameters.size()) { + for (int i = 0; i < batchParameters.size(); i++) { + updates[i] = Statement.SUCCESS_NO_INFO; + } + } else { + for (int i = 0; i < res.size(); i++) { + updates[i] = (int) ((OkPacket) res.get(i)).getAffectedRows(); + } + } + currResult = results.remove(0); + return updates; + + } finally { + batchParameters = new ArrayList<>(); + lock.unlock(); + } + } + + @Override + public long[] executeLargeBatch() throws SQLException { + checkNotClosed(); + if (batchParameters == null || batchParameters.isEmpty()) return new long[0]; + lock.lock(); + try { + List res = executeInternalPreparedBatch(); + results = res; + + long[] updates = new long[batchParameters.size()]; + if (res.size() != batchParameters.size()) { + for (int i = 0; i < batchParameters.size(); i++) { + updates[i] = Statement.SUCCESS_NO_INFO; + } + } else { + for (int i = 0; i < res.size(); i++) { + updates[i] = ((OkPacket) res.get(i)).getAffectedRows(); + } + } + + currResult = results.remove(0); + return updates; + + } finally { + batchParameters = new ArrayList<>(); + lock.unlock(); + } + } + + @Override + public void close() throws SQLException { + if (prepareResult != null) { + prepareResult.decrementUse(con.getClient(), this); + prepareResult = null; + } + con.fireStatementClosed(new StatementEvent(con, this)); + super.close(); + } + + protected void resetPrepare() { + lock.lock(); + try { + prepareResult = null; + } finally { + lock.unlock(); + } + } + + public void reset() { + prepareResult = null; + } +} diff --git a/src/main/java/org/mariadb/jdbc/ServerSidePreparedStatement.java b/src/main/java/org/mariadb/jdbc/ServerSidePreparedStatement.java deleted file mode 100644 index 2dc278102..000000000 --- a/src/main/java/org/mariadb/jdbc/ServerSidePreparedStatement.java +++ /dev/null @@ -1,491 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - */ - -package org.mariadb.jdbc; - -import java.sql.*; -import java.util.*; -import org.mariadb.jdbc.internal.com.read.dao.Results; -import org.mariadb.jdbc.internal.com.read.resultset.SelectResultSet; -import org.mariadb.jdbc.internal.com.send.parameters.ParameterHolder; -import org.mariadb.jdbc.internal.logging.Logger; -import org.mariadb.jdbc.internal.logging.LoggerFactory; -import org.mariadb.jdbc.internal.util.dao.ServerPrepareResult; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; - -public class ServerSidePreparedStatement extends BasePrepareStatement implements Cloneable { - - private static final Logger logger = LoggerFactory.getLogger(ServerSidePreparedStatement.class); - - protected int parameterCount = -1; - private String sql; - private ServerPrepareResult serverPrepareResult = null; - private MariaDbResultSetMetaData metadata; - private MariaDbParameterMetaData parameterMetaData; - private Map currentParameterHolder; - private List queryParameters = new ArrayList<>(); - private boolean mustExecuteOnMaster; - - /** - * Constructor for creating Server prepared statement. - * - * @param connection current connection - * @param sql Sql String to prepare - * @param resultSetScrollType one of the following ResultSet constants: - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, or - * ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of ResultSet.CONCUR_READ_ONLY - * or ResultSet.CONCUR_UPDATABLE - * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be returned; one - * of Statement.RETURN_GENERATED_KEYS or Statement.NO_GENERATED_KEYS - * @param exceptionFactory Exception factory - * @throws SQLException exception - */ - public ServerSidePreparedStatement( - MariaDbConnection connection, - String sql, - int resultSetScrollType, - int resultSetConcurrency, - int autoGeneratedKeys, - ExceptionFactory exceptionFactory) - throws SQLException { - super( - connection, resultSetScrollType, resultSetConcurrency, autoGeneratedKeys, exceptionFactory); - this.sql = sql; - currentParameterHolder = Collections.synchronizedMap(new TreeMap<>()); - mustExecuteOnMaster = protocol.isMasterConnection(); - prepare(this.sql); - } - - /** - * Clone statement. - * - * @param connection connection - * @return Clone statement. - * @throws CloneNotSupportedException if any error occur. - */ - public ServerSidePreparedStatement clone(MariaDbConnection connection) - throws CloneNotSupportedException { - ServerSidePreparedStatement clone = (ServerSidePreparedStatement) super.clone(connection); - clone.metadata = metadata; - clone.parameterMetaData = parameterMetaData; - clone.queryParameters = new ArrayList<>(); - clone.mustExecuteOnMaster = mustExecuteOnMaster; - // force prepare - try { - clone.prepare(sql); - } catch (SQLException e) { - throw new CloneNotSupportedException("PrepareStatement not "); - } - return clone; - } - - private void prepare(String sql) throws SQLException { - try { - serverPrepareResult = protocol.prepare(sql, mustExecuteOnMaster); - setMetaFromResult(); - } catch (SQLException e) { - try { - this.close(); - } catch (Exception ee) { - // eat exception. - } - logger.error("error preparing query", e); - throw exceptionFactory.raiseStatementError(connection, this).create(e); - } - } - - private void setMetaFromResult() { - parameterCount = serverPrepareResult.getParameters().length; - metadata = - new MariaDbResultSetMetaData( - serverPrepareResult.getColumns(), protocol.getUrlParser().getOptions(), false, false); - parameterMetaData = new MariaDbParameterMetaData(serverPrepareResult.getParameters()); - } - - public void setParameter(final int parameterIndex, final ParameterHolder holder) - throws SQLException { - currentParameterHolder.put(parameterIndex - 1, holder); - } - - @Override - public void addBatch() throws SQLException { - validParameters(); - queryParameters.add(currentParameterHolder.values().toArray(new ParameterHolder[0])); - } - - /** - * Add batch. - * - * @param sql typically this is a SQL INSERT or UPDATE statement - * @throws SQLException every time since that method is forbidden on prepareStatement - */ - @Override - public void addBatch(final String sql) throws SQLException { - throw exceptionFactory - .raiseStatementError(connection, this) - .create("Cannot do addBatch(String) on preparedStatement"); - } - - public void clearBatch() { - queryParameters.clear(); - hasLongData = false; - } - - @Override - public ParameterMetaData getParameterMetaData() throws SQLException { - return parameterMetaData; - } - - @Override - public ResultSetMetaData getMetaData() throws SQLException { - return metadata; - } - - /** - * Submits a batch of send to the database for execution and if all send execute successfully, - * returns an array of update counts. The int elements of the array that is returned - * are ordered to correspond to the send in the batch, which are ordered according to the order in - * which they were added to the batch. The elements in the array returned by the method - * executeBatch may be one of the following: - * - *

    - *
  1. A number greater than or equal to zero -- indicates that the command was processed - * successfully and is an update count giving the number of rows in the database that were - * affected by the command's execution - *
  2. A value of SUCCESS_NO_INFO -- indicates that the command was processed - * successfully but that the number of rows affected is unknown. If one of the send in a - * batch update fails to execute properly, this method throws a BatchUpdateException - * , and a JDBC driver may or may not continue to process the remaining send in the - * batch. However, the driver's behavior must be consistent with a particular DBMS, either - * always continuing to process send or never continuing to process send. If the driver - * continues processing after a failure, the array returned by the method - * BatchUpdateException.getUpdateCounts will contain as many elements as there are - * send in the batch, and at least one of the elements will be the following: - *
  3. A value of EXECUTE_FAILED -- indicates that the command failed to execute - * successfully and occurs only if a driver continues to process send after a command fails - *
- * - *

The possible implementations and return values have been modified in the Java 2 SDK, - * Standard Edition, version 1.3 to accommodate the option of continuing to proccess send in a - * batch update after a BatchUpdateException object has been thrown. - * - * @return an array of update counts containing one element for each command in the batch. The - * elements of the array are ordered according to the order in which send were added to the - * batch. - * @throws SQLException if a database access error occurs, this method is called on a closed - * Statement or the driver does not support batch statements. Throws {@link - * BatchUpdateException} (a subclass of SQLException) if one of the send sent to - * the database fails to execute properly or attempts to return a result set. - * @see #addBatch - * @see DatabaseMetaData#supportsBatchUpdates - * @since 1.3 - */ - @Override - public int[] executeBatch() throws SQLException { - checkClose(); - int queryParameterSize = queryParameters.size(); - if (queryParameterSize == 0) { - return new int[0]; - } - executeBatchInternal(queryParameterSize); - return results.getCmdInformation().getUpdateCounts(); - } - - /** - * Execute batch, like executeBatch(), with returning results with long[]. For when row count may - * exceed Integer.MAX_VALUE. - * - * @return an array of update counts (one element for each command in the batch) - * @throws SQLException if a database error occur. - */ - @Override - public long[] executeLargeBatch() throws SQLException { - checkClose(); - int queryParameterSize = queryParameters.size(); - if (queryParameterSize == 0) { - return new long[0]; - } - executeBatchInternal(queryParameterSize); - return results.getCmdInformation().getLargeUpdateCounts(); - } - - private void executeBatchInternal(int queryParameterSize) throws SQLException { - lock.lock(); - executing = true; - try { - executeQueryPrologue(serverPrepareResult); - if (queryTimeout != 0) { - setTimerTask(true); - } - - results = - new Results( - this, - 0, - true, - queryParameterSize, - true, - resultSetScrollType, - resultSetConcurrency, - autoGeneratedKeys, - protocol.getAutoIncrementIncrement(), - null, - null); - - // if multi send capacity - if ((options.useBatchMultiSend || options.useBulkStmts) - && (protocol.executeBatchServer( - mustExecuteOnMaster, - serverPrepareResult, - results, - sql, - queryParameters, - hasLongData))) { - if (metadata == null) { - setMetaFromResult(); // first prepare - } - results.commandEnd(); - return; - } - - // send query one by one, reading results for each query before sending another one - SQLException exception = null; - if (queryTimeout > 0) { - for (int counter = 0; counter < queryParameterSize; counter++) { - ParameterHolder[] parameterHolder = queryParameters.get(counter); - try { - protocol.stopIfInterrupted(); - serverPrepareResult.resetParameterTypeHeader(); - protocol.executePreparedQuery( - mustExecuteOnMaster, serverPrepareResult, results, parameterHolder); - } catch (SQLException queryException) { - if (options.continueBatchOnError - && protocol.isConnected() - && !protocol.isInterrupted()) { - if (exception == null) { - exception = queryException; - } - } else { - throw queryException; - } - } - } - } else { - for (int counter = 0; counter < queryParameterSize; counter++) { - ParameterHolder[] parameterHolder = queryParameters.get(counter); - try { - serverPrepareResult.resetParameterTypeHeader(); - protocol.executePreparedQuery( - mustExecuteOnMaster, serverPrepareResult, results, parameterHolder); - } catch (SQLException queryException) { - if (options.continueBatchOnError) { - if (exception == null) { - exception = queryException; - } - } else { - throw queryException; - } - } - } - } - if (exception != null) { - throw exception; - } - - results.commandEnd(); - } catch (SQLException initialSqlEx) { - throw executeBatchExceptionEpilogue(initialSqlEx, queryParameterSize); - } finally { - executeBatchEpilogue(); - lock.unlock(); - } - } - - // must have "lock" locked before invoking - private void executeQueryPrologue(ServerPrepareResult serverPrepareResult) throws SQLException { - executing = true; - if (closed) { - throw exceptionFactory - .raiseStatementError(connection, this) - .create("execute() is called on closed statement"); - } - protocol.prologProxy( - serverPrepareResult, maxRows, protocol.getProxy() != null, connection, this); - } - - @Override - public ResultSet executeQuery() throws SQLException { - if (execute()) { - return results.getResultSet(); - } - return SelectResultSet.createEmptyResultSet(); - } - - /** - * Executes the SQL statement in this PreparedStatement object, which must be an SQL - * Data Manipulation Language (DML) statement, such as INSERT, UPDATE or - * DELETE; or an SQL statement that returns nothing, such as a DDL statement. - * Result-set are permitted for historical reason, even if spec indicate to throw exception. - * - * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0 - * for SQL statements that return nothing - * @throws SQLException if a database access error occurs; this method is called on a closed - * PreparedStatement - */ - @Override - public int executeUpdate() throws SQLException { - if (execute()) { - return 0; - } - return getUpdateCount(); - } - - @Override - public void clearParameters() { - currentParameterHolder.clear(); - } - - @Override - public boolean execute() throws SQLException { - return executeInternal(getFetchSize()); - } - - protected void validParameters() throws SQLException { - for (int i = 0; i < parameterCount; i++) { - if (currentParameterHolder.get(i) == null) { - logger.error("Parameter at position {} is not set", (i + 1)); - throw exceptionFactory - .raiseStatementError(connection, this) - .create("Parameter at position " + (i + 1) + " is not set", "07004"); - } - } - } - - protected boolean executeInternal(int fetchSize) throws SQLException { - validParameters(); - - lock.lock(); - try { - executeQueryPrologue(serverPrepareResult); - if (queryTimeout != 0) { - setTimerTask(false); - } - - ParameterHolder[] parameterHolders = - currentParameterHolder.values().toArray(new ParameterHolder[0]); - - results = - new Results( - this, - fetchSize, - false, - 1, - true, - resultSetScrollType, - resultSetConcurrency, - autoGeneratedKeys, - protocol.getAutoIncrementIncrement(), - sql, - parameterHolders); - - serverPrepareResult.resetParameterTypeHeader(); - protocol.executePreparedQuery( - mustExecuteOnMaster, serverPrepareResult, results, parameterHolders); - - results.commandEnd(); - return results.getResultSet() != null; - - } catch (SQLException exception) { - throw executeExceptionEpilogue(exception); - } finally { - executeEpilogue(); - lock.unlock(); - } - } - - /** - * Releases this Statement object's database and JDBC resources immediately instead - * of waiting for this to happen when it is automatically closed. It is generally good practice to - * release resources as soon as you are finished with them to avoid tying up database resources. - * - *

Calling the method close on a Statement object that is already - * closed has no effect. - * - *

Note:When a Statement object is closed, its current ResultSet - * object, if one exists, is also closed. - * - * @throws SQLException if a database access error occurs - */ - @Override - public void close() throws SQLException { - // No possible future use for the cached results, so these can be cleared - // This makes the cache eligible for garbage collection earlier if the statement is not - // immediately garbage collected - if (protocol != null && serverPrepareResult != null) { - try { - serverPrepareResult.getUnProxiedProtocol().releasePrepareStatement(serverPrepareResult); - } catch (SQLException e) { - // eat - } - } - super.close(); - } - - protected int getParameterCount() { - return parameterCount; - } - - /** - * Return sql String value. - * - * @return String representation - */ - public String toString() { - StringBuilder sb = new StringBuilder("sql : '" + serverPrepareResult.getSql() + "'"); - if (parameterCount > 0) { - sb.append(", parameters : ["); - for (int i = 0; i < parameterCount; i++) { - ParameterHolder holder = currentParameterHolder.get(i); - if (holder == null) { - sb.append("null"); - } else { - sb.append(holder.toString()); - } - if (i != parameterCount - 1) { - sb.append(","); - } - } - sb.append("]"); - } - return sb.toString(); - } - - /** - * Permit to retrieve current connection thread id, or -1 if unknown. - * - * @return current connection thread id. - */ - public long getServerThreadId() { - return serverPrepareResult.getUnProxiedProtocol().getServerThreadId(); - } -} diff --git a/src/main/java/org/mariadb/jdbc/SimpleParameterMetaData.java b/src/main/java/org/mariadb/jdbc/SimpleParameterMetaData.java deleted file mode 100644 index 921d76cfe..000000000 --- a/src/main/java/org/mariadb/jdbc/SimpleParameterMetaData.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.sql.ParameterMetaData; -import java.sql.SQLException; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; - -/** Very basic info about the parameterized query, only reliable method is getParameterCount(). */ -public class SimpleParameterMetaData implements ParameterMetaData { - - private final int parameterCount; - - public SimpleParameterMetaData(int parameterCount) { - this.parameterCount = parameterCount; - } - - @Override - public int getParameterCount() throws SQLException { - return parameterCount; - } - - @Override - public int isNullable(final int param) throws SQLException { - if (param < 1 || param > parameterCount) { - throw ExceptionFactory.INSTANCE.create( - String.format( - "Parameter metadata out of range : param was %s and must be in range 1 - %s", - param, parameterCount), - "07009"); - } - return ParameterMetaData.parameterNullableUnknown; - } - - @Override - public boolean isSigned(int param) throws SQLException { - if (param < 1 || param > parameterCount) { - throw ExceptionFactory.INSTANCE.create( - String.format( - "Parameter metadata out of range : param was %s and must be in range 1 - %s", - param, parameterCount), - "07009"); - } - return true; - } - - @Override - public int getPrecision(int param) throws SQLException { - if (param < 1 || param > parameterCount) { - throw ExceptionFactory.INSTANCE.create( - String.format( - "Parameter metadata out of range : param was %s and must be in range 1 - %s", - param, parameterCount), - "07009"); - } - throw ExceptionFactory.INSTANCE.create("Unknown parameter metadata precision"); - } - - @Override - public int getScale(int param) throws SQLException { - if (param < 1 || param > parameterCount) { - throw ExceptionFactory.INSTANCE.create( - String.format( - "Parameter metadata out of range : param was %s and must be in range 1 - %s", - param, parameterCount), - "07009"); - } - throw ExceptionFactory.INSTANCE.create("Unknown parameter metadata scale"); - } - - /** - * Parameter type are not sent by server. See https://jira.mariadb.org/browse/CONJ-568 and - * https://jira.mariadb.org/browse/MDEV-15031 - * - * @param param parameter number - * @return SQL type from java.sql.Types - * @throws SQLException a feature not supported, since server doesn't sent the right information - */ - @Override - public int getParameterType(int param) throws SQLException { - if (param < 1 || param > parameterCount) { - throw ExceptionFactory.INSTANCE.create( - String.format( - "Parameter metadata out of range : param was %s and must be in range 1 - %s", - param, parameterCount), - "07009"); - } - throw ExceptionFactory.INSTANCE.notSupported( - "Getting parameter type metadata are not supported"); - } - - @Override - public String getParameterTypeName(int param) throws SQLException { - if (param < 1 || param > parameterCount) { - throw ExceptionFactory.INSTANCE.create( - String.format( - "Parameter metadata out of range : param was %s and must be in range 1 - %s", - param, parameterCount), - "07009"); - } - throw ExceptionFactory.INSTANCE.create("Unknown parameter metadata type name"); - } - - @Override - public String getParameterClassName(int param) throws SQLException { - if (param < 1 || param > parameterCount) { - throw ExceptionFactory.INSTANCE.create( - String.format( - "Parameter metadata out of range : param was %s and must be in range 1 - %s", - param, parameterCount), - "07009"); - } - throw ExceptionFactory.INSTANCE.create("Unknown parameter metadata class name"); - } - - @Override - public int getParameterMode(int param) { - return parameterModeIn; - } - - @Override - public T unwrap(final Class iface) throws SQLException { - try { - if (isWrapperFor(iface)) { - return iface.cast(this); - } else { - throw new SQLException("The receiver is not a wrapper for " + iface.getName()); - } - } catch (Exception e) { - throw new SQLException("The receiver is not a wrapper and does not implement the interface"); - } - } - - public boolean isWrapperFor(final Class iface) throws SQLException { - return iface.isInstance(this); - } -} diff --git a/src/main/java/org/mariadb/jdbc/SslMode.java b/src/main/java/org/mariadb/jdbc/SslMode.java new file mode 100644 index 000000000..16265ea70 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/SslMode.java @@ -0,0 +1,56 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +public enum SslMode { + // NO SSL + DISABLE("disable", "DISABLED"), + + // Encryption only (no certificate and hostname validation) (DEVELOPMENT ONLY) + NO_VERIFICATION("no-verification", "REQUIRED"), + + // Encryption, certificates validation, BUT no hostname verification + VERIFY_CA("verify-ca", "VERIFY_CA"), + + // Standard SSL use: Encryption, certificate validation and hostname validation + VERIFY_FULL("verify-full", "VERIFY_IDENTITY"); + + private final String value; + private final String alias; + + SslMode(String value, String alias) { + this.value = value; + this.alias = alias; + } + + public static SslMode from(String value) { + for (SslMode sslMode : values()) { + if (sslMode.value.equalsIgnoreCase(value) + || sslMode.name().equalsIgnoreCase(value) + || sslMode.alias.equalsIgnoreCase(value)) { + return sslMode; + } + } + throw new IllegalArgumentException( + String.format("Wrong argument value '%s' for SslMode", value)); + } +} diff --git a/src/main/java/org/mariadb/jdbc/Statement.java b/src/main/java/org/mariadb/jdbc/Statement.java new file mode 100644 index 000000000..b7e503cf3 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/Statement.java @@ -0,0 +1,1417 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.result.CompleteResult; +import org.mariadb.jdbc.client.result.Result; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.client.QueryPacket; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.message.server.Completion; +import org.mariadb.jdbc.message.server.OkPacket; +import org.mariadb.jdbc.util.NativeSql; +import org.mariadb.jdbc.util.constants.ServerStatus; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; + +public class Statement implements java.sql.Statement { + + protected final int resultSetType; + protected final int resultSetConcurrency; + protected final ReentrantLock lock; + protected final boolean canUseServerTimeout; + protected final boolean canUseServerMaxRows; + protected final Connection con; + private List batchQueries; + protected int queryTimeout; + protected long maxRows; + protected int fetchSize; + protected int autoGeneratedKeys; + protected boolean closeOnCompletion; + protected boolean closed; + protected boolean escape; + protected List results; + protected Completion currResult; + + public Statement( + Connection con, + ReentrantLock lock, + boolean canUseServerTimeout, + boolean canUseServerMaxRows, + int autoGeneratedKeys, + int resultSetType, + int resultSetConcurrency, + int defaultFetchSize) { + this.con = con; + this.lock = lock; + this.resultSetConcurrency = resultSetConcurrency; + this.resultSetType = resultSetType; + this.autoGeneratedKeys = autoGeneratedKeys; + this.canUseServerTimeout = canUseServerTimeout; + this.canUseServerMaxRows = canUseServerMaxRows; + this.fetchSize = defaultFetchSize; + } + + protected ExceptionFactory exceptionFactory() { + return con.getContext().getExceptionFactory().of(this); + } + + /** + * Executes the given SQL statement, which returns a single ResultSet object. + * + *

Note:This method cannot be called on a PreparedStatement or + * CallableStatement. + * + * @param sql an SQL statement to be sent to the database, typically a static SQL SELECT + * statement + * @return a ResultSet object that contains the data produced by the given query; + * never null + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement, the given SQL statement produces anything other than a single + * ResultSet object, the method is called on a PreparedStatement or + * CallableStatement + * @throws java.sql.SQLTimeoutException when the driver has determined that the timeout value that + * was specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + */ + @Override + public ResultSet executeQuery(String sql) throws SQLException { + executeInternal(sql, Statement.NO_GENERATED_KEYS); + currResult = results.remove(0); + if (currResult instanceof Result) return (Result) currResult; + return new CompleteResult( + new ColumnDefinitionPacket[0], new ReadableByteBuf[0], con.getContext()); + } + + /** + * Executes the given SQL statement, which may be an INSERT, UPDATE, or + * DELETE statement or an SQL statement that returns nothing, such as an SQL DDL + * statement. + * + *

Note:This method cannot be called on a PreparedStatement or + * CallableStatement. + * + * @param sql an SQL Data Manipulation Language (DML) statement, such as INSERT, + * UPDATE or DELETE; or an SQL statement that returns nothing, such + * as a DDL statement. + * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0 + * for SQL statements that return nothing + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement, the given SQL statement produces a ResultSet object, + * the method is called on a PreparedStatement or CallableStatement + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + */ + @Override + public int executeUpdate(String sql) throws SQLException { + return executeUpdate(sql, Statement.NO_GENERATED_KEYS); + } + + /** + * Releases this Statement object's database and JDBC resources immediately instead + * of waiting for this to happen when it is automatically closed. It is generally good practice to + * release resources as soon as you are finished with them to avoid tying up database resources. + * + *

Calling the method close on a Statement object that is already + * closed has no effect. + * + *

Note:When a Statement object is closed, its current ResultSet + * object, if one exists, is also closed. + * + * @throws SQLException if a database access error occurs + */ + @Override + public void close() throws SQLException { + lock.lock(); + try { + if (!closed) { + closed = true; + + if (currResult != null && currResult instanceof ResultSet) { + ((ResultSet) currResult).close(); + } + + // close result-set + if (results != null) { + for (Completion completion : results) { + if (completion instanceof ResultSet) { + ((ResultSet) completion).close(); + } + } + } + } + } finally { + lock.unlock(); + } + } + + /** + * Retrieves the maximum number of bytes that can be returned for character and binary column + * values in a ResultSet object produced by this Statement object. This + * limit applies only to BINARY, VARBINARY, LONGVARBINARY, + * CHAR, VARCHAR, NCHAR, NVARCHAR, + * LONGNVARCHAR and LONGVARCHAR columns. If the limit is exceeded, the excess + * data is silently discarded. + * + * @return the current column size limit for columns storing character and binary values; zero + * means there is no limit + * @see #setMaxFieldSize + */ + @Override + public int getMaxFieldSize() { + return 0; + } + + /** + * NOT SUPPORTED. + * + * @see #getMaxFieldSize + */ + @Override + public void setMaxFieldSize(int max) {} + + /** + * Retrieves the maximum number of rows that a ResultSet object produced by this + * Statement object can contain. If this limit is exceeded, the excess rows are + * silently dropped. + * + * @return the current maximum number of rows for a ResultSet object produced by this + * Statement object; zero means there is no limit + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @see #setMaxRows + */ + @Override + public int getMaxRows() throws SQLException { + checkNotClosed(); + return (int) maxRows; + } + + /** + * Sets the limit for the maximum number of rows that any ResultSet object generated + * by this Statement object can contain to the given number. If the limit is + * exceeded, the excess rows are silently dropped. + * + * @param max the new max rows limit; zero means there is no limit + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement or the condition {@code max >= 0} is not satisfied + * @see #getMaxRows + */ + @Override + public void setMaxRows(int max) throws SQLException { + checkNotClosed(); + if (max < 0) { + throw exceptionFactory().create("max rows cannot be negative : asked for " + max, "42000"); + } + maxRows = max; + } + + /** + * Sets escape processing on or off. If escape scanning is on (the default), the driver will do + * escape substitution before sending the SQL statement to the database. + * + *

The {@code Connection} and {@code DataSource} property {@code escapeProcessing} may be used + * to change the default escape processing behavior. A value of true (the default) enables escape + * Processing for all {@code Statement} objects. A value of false disables escape processing for + * all {@code Statement} objects. The {@code setEscapeProcessing} method may be used to specify + * the escape processing behavior for an individual {@code Statement} object. + * + *

Note: Since prepared statements have usually been parsed prior to making this call, + * disabling escape processing for PreparedStatements objects will have no effect. + * + * @param enable true to enable escape processing; false to disable it + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + */ + @Override + public void setEscapeProcessing(boolean enable) throws SQLException { + checkNotClosed(); + this.escape = enable; + } + + /** + * Retrieves the number of seconds the driver will wait for a Statement object to + * execute. If the limit is exceeded, a SQLException is thrown. + * + * @return the current query timeout limit in seconds; zero means there is no limit + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @see #setQueryTimeout + */ + @Override + public int getQueryTimeout() throws SQLException { + checkNotClosed(); + return queryTimeout; + } + + /** + * Sets the number of seconds the driver will wait for a Statement object to execute + * to the given number of seconds. By default there is no limit on the amount of time allowed for + * a running statement to complete. If the limit is exceeded, an SQLTimeoutException + * is thrown. A JDBC driver must apply this limit to the execute, executeQuery + * and executeUpdate methods. + * + *

Note: JDBC driver implementations may also apply this limit to {@code + * ResultSet} methods (consult your driver vendor documentation for details). + * + *

Note: In the case of {@code Statement} batching, it is implementation + * defined as to whether the time-out is applied to individual SQL commands added via the {@code + * addBatch} method or to the entire batch of SQL commands invoked by the {@code executeBatch} + * method (consult your driver vendor documentation for details). + * + * @param seconds the new query timeout limit in seconds; zero means there is no limit + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement or the condition {@code seconds >= 0} is not satisfied + * @see #getQueryTimeout + */ + @Override + public void setQueryTimeout(int seconds) throws SQLException { + if (seconds < 0) { + throw exceptionFactory() + .create("Query timeout cannot be negative : asked for " + seconds, "42000"); + } + this.queryTimeout = seconds; + } + + /** + * Cancels this Statement object if both the DBMS and driver support aborting an SQL + * statement. This method can be used by one thread to cancel a statement that is being executed + * by another thread. + * + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + */ + @Override + public void cancel() throws SQLException { + checkNotClosed(); + boolean locked = lock.tryLock(); + // if any query is active, lock is set. + // this avoid trying to execute a KILL QUERY if no query is running. + if (!locked) { + con.cancelCurrentQuery(); + } else { + lock.unlock(); + } + } + + /** + * Retrieves the first warning reported by calls on this Statement object. Subsequent + * Statement object warnings will be chained to this SQLWarning object. + * + *

The warning chain is automatically cleared each time a statement is (re)executed. This + * method may not be called on a closed Statement object; doing so will cause an + * SQLException to be thrown. + * + *

Note: If you are processing a ResultSet object, any warnings associated + * with reads on that ResultSet object will be chained on it rather than on the + * Statement object that produced it. + * + * @return the first SQLWarning object or null if there are no warnings + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + */ + @Override + public SQLWarning getWarnings() throws SQLException { + return con.getWarnings(); + } + + /** + * Clears all the warnings reported on this Statement object. After a call to this + * method, the method getWarnings will return null until a new warning + * is reported for this Statement object. + * + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + */ + @Override + public void clearWarnings() throws SQLException { + con.getContext().setWarning(0); + } + + /** + * Sets the SQL cursor name to the given String, which will be used by subsequent + * Statement object execute methods. This name can then be used in SQL + * positioned update or delete statements to identify the current row in the ResultSet + * object generated by this statement. If the database does not support positioned + * update/delete, this method is a noop. To insure that a cursor has the proper isolation level to + * support updates, the cursor's SELECT statement should have the form + * SELECT FOR UPDATE. If FOR UPDATE is not present, positioned updates may + * fail. + * + *

Note: By definition, the execution of positioned updates and deletes must be done by + * a different Statement object than the one that generated the ResultSet + * object being used for positioning. Also, cursor names must be unique within a + * connection. + * + * @param name the new cursor name, which must be unique within a connection + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + */ + @Override + public void setCursorName(String name) throws SQLException { + throw exceptionFactory().notSupported("Cursors are not supported"); + } + + /** + * Executes the given SQL statement, which may return multiple results. In some (uncommon) + * situations, a single SQL statement may return multiple result sets and/or update counts. + * Normally you can ignore this unless you are (1) executing a stored procedure that you know may + * return multiple results or (2) you are dynamically executing an unknown SQL string. + * + *

The execute method executes an SQL statement and indicates the form of the + * first result. You must then use the methods getResultSet or getUpdateCount + * to retrieve the result, and getMoreResults to move to any subsequent + * result(s). + * + *

Note:This method cannot be called on a PreparedStatement or + * CallableStatement. + * + * @param sql any SQL statement + * @return true if the first result is a ResultSet object; false + * if it is an update count or there are no results + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement, the method is called on a PreparedStatement or + * CallableStatement + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + * @see #getResultSet + * @see #getUpdateCount + * @see #getMoreResults + */ + @Override + public boolean execute(String sql) throws SQLException { + return execute(sql, Statement.NO_GENERATED_KEYS); + } + + /** + * Retrieves the current result as a ResultSet object. This method should be called + * only once per result. + * + * @return the current result as a ResultSet object or null if the + * result is an update count or there are no more results + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @see #execute + */ + @Override + public ResultSet getResultSet() throws SQLException { + checkNotClosed(); + if (currResult instanceof Result) { + return (Result) currResult; + } + return null; + } + + /** + * Retrieves the current result as an update count; if the result is a ResultSet + * object or there are no more results, -1 is returned. This method should be called only once per + * result. + * + * @return the current result as an update count; -1 if the current result is a ResultSet + * object or there are no more results + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @see #execute + */ + @Override + public int getUpdateCount() throws SQLException { + checkNotClosed(); + if (currResult instanceof OkPacket) { + return (int) ((OkPacket) currResult).getAffectedRows(); + } + return -1; + } + + /** + * Moves to this Statement object's next result, returns true if it is a + * ResultSet object, and implicitly closes any current ResultSet + * object(s) obtained with the method getResultSet. + * + *

There are no more results when the following is true: + * + *

{@code
+   * // stmt is a Statement object
+   * ((stmt.getMoreResults() == false) && (stmt.getUpdateCount() == -1))
+   * }
+ * + * @return true if the next result is a ResultSet object; false + * if it is an update count or there are no more results + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @see #execute + */ + @Override + public boolean getMoreResults() throws SQLException { + return getMoreResults(Statement.CLOSE_CURRENT_RESULT); + } + + /** + * Retrieves the direction for fetching rows from database tables that is the default for result + * sets generated from this Statement object. If this Statement object + * has not set a fetch direction by calling the method setFetchDirection, the return + * value is implementation-specific. + * + * @return the default fetch direction for result sets generated from this Statement + * object + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @see #setFetchDirection + * @since 1.2 + */ + @Override + public int getFetchDirection() throws SQLException { + return ResultSet.FETCH_FORWARD; + } + + /** + * Gives the driver a hint as to the direction in which rows will be processed in ResultSet + * objects created using this Statement object. The default value is + * ResultSet.FETCH_FORWARD. + * + *

Note that this method sets the default fetch direction for result sets generated by this + * Statement object. Each result set has its own methods for getting and setting its + * own fetch direction. + * + * @param direction the initial direction for processing rows + */ + @Override + public void setFetchDirection(int direction) { + // not supported + } + + /** + * Retrieves the number of result set rows that is the default fetch size for ResultSet + * objects generated from this Statement object. If this Statement + * object has not set a fetch size by calling the method setFetchSize, the + * return value is implementation-specific. + * + * @return the default fetch size for result sets generated from this Statement + * object + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @see #setFetchSize + */ + @Override + public int getFetchSize() throws SQLException { + checkNotClosed(); + return this.fetchSize; + } + + /** + * Gives the JDBC driver a hint as to the number of rows that should be fetched from the database + * when more rows are needed for ResultSet objects generated by this Statement + * . If the value specified is zero, then the hint is ignored. The default value is zero. + * + * @param rows the number of rows to fetch + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement or the condition {@code rows >= 0} is not satisfied. + * @see #getFetchSize + * @since 1.2 + */ + @Override + public void setFetchSize(int rows) throws SQLException { + if (rows < 0) { + throw exceptionFactory().create("invalid fetch size"); + } + this.fetchSize = rows; + } + + /** + * Retrieves the result set concurrency for ResultSet objects generated by this + * Statement object. + * + * @return either ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE + * + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + */ + @Override + public int getResultSetConcurrency() throws SQLException { + checkNotClosed(); + return this.resultSetConcurrency; + } + + /** + * Retrieves the result set type for ResultSet objects generated by this + * Statement object. + * + * @return one of ResultSet.TYPE_FORWARD_ONLY, + * ResultSet.TYPE_SCROLL_INSENSITIVE, or ResultSet.TYPE_SCROLL_SENSITIVE + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @since 1.2 + */ + @Override + public int getResultSetType() throws SQLException { + return this.resultSetType; + } + + /** + * Adds the given SQL command to the current list of commands for this Statement + * object. The commands in this list can be executed as a batch by calling the method + * executeBatch. + * + *

Note:This method cannot be called on a PreparedStatement or + * CallableStatement. + * + * @param sql typically this is a SQL INSERT or UPDATE statement + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement, the driver does not support batch updates, the method is called on + * a PreparedStatement or CallableStatement + * @see #executeBatch + * @see DatabaseMetaData#supportsBatchUpdates + */ + @Override + public void addBatch(String sql) throws SQLException { + if (sql == null) { + throw exceptionFactory().create("null cannot be set to addBatch(String sql)"); + } + if (batchQueries == null) batchQueries = new ArrayList<>(); + batchQueries.add(escape ? NativeSql.parse(sql, con.getContext()) : sql); + } + + /** + * Empties this Statement object's current list of SQL commands. + * + *

+ * + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement or the driver does not support batch updates + * @see #addBatch + * @see DatabaseMetaData#supportsBatchUpdates + * @since 1.2 + */ + @Override + public void clearBatch() throws SQLException { + checkNotClosed(); + if (batchQueries == null) { + batchQueries = new ArrayList<>(); + } else { + batchQueries.clear(); + } + } + + /** + * Submits a batch of commands to the database for execution and if all commands execute + * successfully, returns an array of update counts. The int elements of the array + * that is returned are ordered to correspond to the commands in the batch, which are ordered + * according to the order in which they were added to the batch. The elements in the array + * returned by the method executeBatch may be one of the following: + * + *

    + *
  1. A number greater than or equal to zero -- indicates that the command was processed + * successfully and is an update count giving the number of rows in the database that were + * affected by the command's execution + *
  2. A value of SUCCESS_NO_INFO -- indicates that the command was processed + * successfully but that the number of rows affected is unknown + *

    If one of the commands in a batch update fails to execute properly, this method throws + * a BatchUpdateException, and a JDBC driver may or may not continue to process + * the remaining commands in the batch. However, the driver's behavior must be consistent + * with a particular DBMS, either always continuing to process commands or never continuing + * to process commands. If the driver continues processing after a failure, the array + * returned by the method BatchUpdateException.getUpdateCounts will contain as + * many elements as there are commands in the batch, and at least one of the elements will + * be the following: + *

  3. A value of EXECUTE_FAILED -- indicates that the command failed to execute + * successfully and occurs only if a driver continues to process commands after a command + * fails + *
+ * + *

The possible implementations and return values have been modified in the Java 2 SDK, + * Standard Edition, version 1.3 to accommodate the option of continuing to process commands in a + * batch update after a BatchUpdateException object has been thrown. + * + * @return an array of update counts containing one element for each command in the batch. The + * elements of the array are ordered according to the order in which commands were added to + * the batch. + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement or the driver does not support batch statements. Throws {@link + * BatchUpdateException} (a subclass of SQLException) if one of the commands sent + * to the database fails to execute properly or attempts to return a result set. + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + * @see #addBatch + * @see DatabaseMetaData#supportsBatchUpdates + * @since 1.2 + */ + @Override + public int[] executeBatch() throws SQLException { + checkNotClosed(); + if (batchQueries == null || batchQueries.isEmpty()) return new int[0]; + lock.lock(); + try { + List res = executeInternalBatch(); + results = res; + int[] updates = new int[res.size()]; + for (int i = 0; i < res.size(); i++) { + updates[i] = (int) ((OkPacket) res.get(i)).getAffectedRows(); + } + currResult = results.remove(0); + batchQueries.clear(); + return updates; + + } finally { + lock.unlock(); + } + } + + /** + * Retrieves the Connection object that produced this Statement object. + * + * @return the connection that produced this statement + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @since 1.2 + */ + @Override + public Connection getConnection() throws SQLException { + checkNotClosed(); + return con; + } + + /** + * Moves to this Statement object's next result, deals with any current + * ResultSet object(s) according to the instructions specified by the given flag, and + * returns true if the next result is a ResultSet object. + * + *

There are no more results when the following is true: + * + *

{@code
+   * // stmt is a Statement object
+   * ((stmt.getMoreResults(current) == false) && (stmt.getUpdateCount() == -1))
+   * }
+ * + * @param current one of the following Statement constants indicating what should + * happen to current ResultSet objects obtained using the method + * getResultSet: Statement.CLOSE_CURRENT_RESULT, + * Statement.KEEP_CURRENT_RESULT, or Statement.CLOSE_ALL_RESULTS + * @return true if the next result is a ResultSet object; false + * if it is an update count or there are no more results + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement or the argument supplied is not one of the following: + * Statement.CLOSE_CURRENT_RESULT, Statement.KEEP_CURRENT_RESULT or + * Statement.CLOSE_ALL_RESULTS + * @throws SQLFeatureNotSupportedException if DatabaseMetaData.supportsMultipleOpenResults + * returns false and either Statement.KEEP_CURRENT_RESULT or + * Statement.CLOSE_ALL_RESULTS are supplied as the argument. + * @see #execute + * @since 1.4 + */ + @Override + public boolean getMoreResults(int current) throws SQLException { + checkNotClosed(); + if (currResult instanceof ResultSet) { + lock.lock(); + try { + Result result = (Result) currResult; + if (current == java.sql.Statement.CLOSE_CURRENT_RESULT) { + result.close(); + } else { + result.fetchRemaining(); + } + if (result.streaming() + && (con.getContext().getServerStatus() & ServerStatus.MORE_RESULTS_EXISTS) > 0) { + con.getClient() + .readStreamingResults( + results, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion); + } + } finally { + lock.unlock(); + } + } + + if (results.size() > 0) { + currResult = results.remove(0); + return (currResult instanceof Result); + } + currResult = null; + return false; + } + + /** + * Permit to streaming result to fetch remaining results. + * + * @throws SQLException if socket error occurs. + */ + public void fetchRemaining() throws SQLException { + if (currResult != null && currResult instanceof ResultSet) { + Result result = (Result) currResult; + result.fetchRemaining(); + if (result.streaming() + && (con.getContext().getServerStatus() & ServerStatus.MORE_RESULTS_EXISTS) > 0) { + con.getClient() + .readStreamingResults( + results, 0, 0L, resultSetConcurrency, resultSetType, closeOnCompletion); + } + } + } + + /** + * Retrieves any auto-generated keys created as a result of executing this Statement + * object. If this Statement object did not generate any keys, an empty + * ResultSet object is returned. + * + *

Note:If the columns which represent the auto-generated keys were not specified, the + * JDBC driver implementation will determine the columns which best represent the auto-generated + * keys. + * + * @return a ResultSet object containing the auto-generated key(s) generated by the + * execution of this Statement object + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.4 + */ + @Override + public ResultSet getGeneratedKeys() throws SQLException { + checkNotClosed(); + if (autoGeneratedKeys != java.sql.Statement.RETURN_GENERATED_KEYS) { + throw new SQLException( + "Cannot return generated keys: query was not set with Statement.RETURN_GENERATED_KEYS"); + } + + if (currResult instanceof OkPacket) { + OkPacket ok = ((OkPacket) currResult); + String lastInsertId = String.valueOf(ok.getLastInsertId()); + return CompleteResult.createResultSet( + "insert_id", DataType.BIGINT, new String[] {lastInsertId}, con.getContext()); + } + + return new CompleteResult( + new ColumnDefinitionPacket[0], new ReadableByteBuf[0], con.getContext()); + } + + /** + * Executes the given SQL statement and signals the driver with the given flag about whether the + * auto-generated keys produced by this Statement object should be made available for + * retrieval. The driver will ignore the flag if the SQL statement is not an INSERT + * statement, or an SQL statement able to return auto-generated keys (the list of such statements + * is vendor-specific). + * + *

Note:This method cannot be called on a PreparedStatement or + * CallableStatement. + * + * @param sql an SQL Data Manipulation Language (DML) statement, such as INSERT, + * UPDATE or DELETE; or an SQL statement that returns nothing, such + * as a DDL statement. + * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be made available + * for retrieval; one of the following constants: Statement.RETURN_GENERATED_KEYS + * Statement.NO_GENERATED_KEYS + * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0 + * for SQL statements that return nothing + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement, the given SQL statement returns a ResultSet object, + * the given constant is not one of those allowed, the method is called on a + * PreparedStatement or CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method with a + * constant of Statement.RETURN_GENERATED_KEYS + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + * @since 1.4 + */ + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + executeInternal(sql, autoGeneratedKeys); + + currResult = results.remove(0); + if (currResult instanceof Result) { + throw exceptionFactory() + .create("the given SQL statement produces an unexpected ResultSet object", "HY000"); + } + return (int) ((OkPacket) currResult).getAffectedRows(); + } + + private void executeInternal(String sql, int autoGeneratedKeys) throws SQLException { + checkNotClosed(); + lock.lock(); + try { + this.autoGeneratedKeys = autoGeneratedKeys; + String cmd = escapeTimeout(sql); + results = + con.getClient() + .execute( + new QueryPacket(cmd), + this, + fetchSize, + maxRows, + this.resultSetConcurrency, + this.resultSetType, + closeOnCompletion); + } finally { + lock.unlock(); + } + } + + /** + * Executes the given SQL statement and signals the driver that the auto-generated keys indicated + * in the given array should be made available for retrieval. This array contains the indexes of + * the columns in the target table that contain the auto-generated keys that should be made + * available. The driver will ignore the array if the SQL statement is not an INSERT + * statement, or an SQL statement able to return auto-generated keys (the list of such statements + * is vendor-specific). + * + *

Note:This method cannot be called on a PreparedStatement or + * CallableStatement. + * + * @param sql an SQL Data Manipulation Language (DML) statement, such as INSERT, + * UPDATE or DELETE; or an SQL statement that returns nothing, such + * as a DDL statement. + * @param columnIndexes an array of column indexes indicating the columns that should be returned + * from the inserted row + * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0 + * for SQL statements that return nothing + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement, the SQL statement returns a ResultSet object,the + * second argument supplied to this method is not an int array whose elements are + * valid column indexes, the method is called on a PreparedStatement or + * CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + * @since 1.4 + */ + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + return executeUpdate(sql, Statement.RETURN_GENERATED_KEYS); + } + + /** + * Executes the given SQL statement and signals the driver that the auto-generated keys indicated + * in the given array should be made available for retrieval. This array contains the names of the + * columns in the target table that contain the auto-generated keys that should be made available. + * The driver will ignore the array if the SQL statement is not an INSERT statement, + * or an SQL statement able to return auto-generated keys (the list of such statements is + * vendor-specific). + * + *

Note:This method cannot be called on a PreparedStatement or + * CallableStatement. + * + * @param sql an SQL Data Manipulation Language (DML) statement, such as INSERT, + * UPDATE or DELETE; or an SQL statement that returns nothing, such + * as a DDL statement. + * @param columnNames an array of the names of the columns that should be returned from the + * inserted row + * @return either the row count for INSERT, UPDATE, or DELETE + * statements, or 0 for SQL statements that return nothing + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement, the SQL statement returns a ResultSet object, the + * second argument supplied to this method is not a String array whose elements + * are valid column names, the method is called on a PreparedStatement or + * CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + * @since 1.4 + */ + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + return executeUpdate(sql, Statement.RETURN_GENERATED_KEYS); + } + + /** + * Executes the given SQL statement, which may return multiple results, and signals the driver + * that any auto-generated keys should be made available for retrieval. The driver will ignore + * this signal if the SQL statement is not an INSERT statement, or an SQL statement + * able to return auto-generated keys (the list of such statements is vendor-specific). + * + *

In some (uncommon) situations, a single SQL statement may return multiple result sets and/or + * update counts. Normally you can ignore this unless you are (1) executing a stored procedure + * that you know may return multiple results or (2) you are dynamically executing an unknown SQL + * string. + * + *

The execute method executes an SQL statement and indicates the form of the + * first result. You must then use the methods getResultSet or getUpdateCount + * to retrieve the result, and getMoreResults to move to any subsequent + * result(s). + * + *

Note:This method cannot be called on a PreparedStatement or + * CallableStatement. + * + * @param sql any SQL statement + * @param autoGeneratedKeys a constant indicating whether auto-generated keys should be made + * available for retrieval using the method getGeneratedKeys; one of the + * following constants: Statement.RETURN_GENERATED_KEYS or + * Statement.NO_GENERATED_KEYS + * @return true if the first result is a ResultSet object; false + * if it is an update count or there are no results + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement, the second parameter supplied to this method is not + * Statement.RETURN_GENERATED_KEYS or Statement.NO_GENERATED_KEYS, the + * method is called on a PreparedStatement or CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method with a + * constant of Statement.RETURN_GENERATED_KEYS + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + * @see #getResultSet + * @see #getUpdateCount + * @see #getMoreResults + * @see #getGeneratedKeys + * @since 1.4 + */ + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + executeInternal(sql, autoGeneratedKeys); + + currResult = results.remove(0); + return currResult instanceof Result; + } + + protected String escapeTimeout(final String sql) throws SQLException { + String escapedSql = escape ? NativeSql.parse(sql, con.getContext()) : sql; + if (queryTimeout != 0 && canUseServerTimeout) { + if (canUseServerMaxRows && maxRows > 0 && con.getContext().getVersion().isMariaDBServer()) { + return "SET STATEMENT max_statement_time=" + + queryTimeout + + ", SQL_SELECT_LIMIT=" + + maxRows + + " FOR " + + escapedSql; + } + return "SET STATEMENT max_statement_time=" + queryTimeout + " FOR " + escapedSql; + } + if (canUseServerMaxRows && maxRows > 0 && con.getContext().getVersion().isMariaDBServer()) { + return "SET STATEMENT SQL_SELECT_LIMIT=" + maxRows + " FOR " + escapedSql; + } + return escapedSql; + } + + /** + * Executes the given SQL statement, which may return multiple results, and signals the driver + * that the auto-generated keys indicated in the given array should be made available for + * retrieval. This array contains the indexes of the columns in the target table that contain the + * auto-generated keys that should be made available. The driver will ignore the array if the SQL + * statement is not an INSERT statement, or an SQL statement able to return + * auto-generated keys (the list of such statements is vendor-specific). + * + *

Under some (uncommon) situations, a single SQL statement may return multiple result sets + * and/or update counts. Normally you can ignore this unless you are (1) executing a stored + * procedure that you know may return multiple results or (2) you are dynamically executing an + * unknown SQL string. + * + *

The execute method executes an SQL statement and indicates the form of the + * first result. You must then use the methods getResultSet or getUpdateCount + * to retrieve the result, and getMoreResults to move to any subsequent + * result(s). + * + *

Note:This method cannot be called on a PreparedStatement or + * CallableStatement. + * + * @param sql any SQL statement + * @param columnIndexes an array of the indexes of the columns in the inserted row that should be + * made available for retrieval by a call to the method getGeneratedKeys + * @return true if the first result is a ResultSet object; false + * if it is an update count or there are no results + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement, the elements in the int array passed to this method + * are not valid column indexes, the method is called on a PreparedStatement or + * CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + * @see #getResultSet + * @see #getUpdateCount + * @see #getMoreResults + * @since 1.4 + */ + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + return execute(sql, Statement.RETURN_GENERATED_KEYS); + } + + /** + * Executes the given SQL statement, which may return multiple results, and signals the driver + * that the auto-generated keys indicated in the given array should be made available for + * retrieval. This array contains the names of the columns in the target table that contain the + * auto-generated keys that should be made available. The driver will ignore the array if the SQL + * statement is not an INSERT statement, or an SQL statement able to return + * auto-generated keys (the list of such statements is vendor-specific). + * + *

In some (uncommon) situations, a single SQL statement may return multiple result sets and/or + * update counts. Normally you can ignore this unless you are (1) executing a stored procedure + * that you know may return multiple results or (2) you are dynamically executing an unknown SQL + * string. + * + *

The execute method executes an SQL statement and indicates the form of the + * first result. You must then use the methods getResultSet or getUpdateCount + * to retrieve the result, and getMoreResults to move to any subsequent + * result(s). + * + *

Note:This method cannot be called on a PreparedStatement or + * CallableStatement. + * + * @param sql any SQL statement + * @param columnNames an array of the names of the columns in the inserted row that should be made + * available for retrieval by a call to the method getGeneratedKeys + * @return true if the next result is a ResultSet object; false + * if it is an update count or there are no more results + * @throws SQLException if a database access error occurs, this method is called on a closed + * Statement,the elements of the String array passed to this method + * are not valid column names, the method is called on a PreparedStatement or + * CallableStatement + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @throws SQLTimeoutException when the driver has determined that the timeout value that was + * specified by the {@code setQueryTimeout} method has been exceeded and has at least + * attempted to cancel the currently running {@code Statement} + * @see #getResultSet + * @see #getUpdateCount + * @see #getMoreResults + * @see #getGeneratedKeys + * @since 1.4 + */ + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + return execute(sql, Statement.RETURN_GENERATED_KEYS); + } + + /** + * Retrieves the result set holdability for ResultSet objects generated by this + * Statement object. + * + * @return either ResultSet.HOLD_CURSORS_OVER_COMMIT or + * ResultSet.CLOSE_CURSORS_AT_COMMIT + * @throws SQLException if a database access error occurs or this method is called on a closed + * Statement + * @since 1.4 + */ + @Override + public int getResultSetHoldability() throws SQLException { + return ResultSet.HOLD_CURSORS_OVER_COMMIT; + } + + /** + * Retrieves whether this Statement object has been closed. A Statement + * is closed if the method close has been called on it, or if it is automatically closed. + * + * @return true if this Statement object is closed; false if it is still open + */ + @Override + public boolean isClosed() { + return closed; + } + + /** + * Returns a value indicating whether the Statement is poolable or not. + * + *

+ * + * @return true if the Statement is poolable; false + * otherwise + * @throws SQLException if this method is called on a closed Statement + * @see java.sql.Statement#setPoolable(boolean) setPoolable(boolean) + */ + @Override + public boolean isPoolable() throws SQLException { + checkNotClosed(); + return false; + } + + /** + * Requests that a Statement be pooled or not pooled. The value specified is a hint + * to the statement pool implementation indicating whether the application wants the statement to + * be pooled. It is up to the statement pool manager as to whether the hint is used. + * + *

The poolable value of a statement is applicable to both internal statement caches + * implemented by the driver and external statement caches implemented by application servers and + * other applications. + * + *

By default, a Statement is not poolable when created, and a + * PreparedStatement and CallableStatement are poolable when created. + * + *

+ * + * @param poolable requests that the statement be pooled if true and that the statement not be + * pooled if false + * @throws SQLException if this method is called on a closed Statement + */ + @Override + public void setPoolable(boolean poolable) throws SQLException { + checkNotClosed(); + } + + /** + * Specifies that this {@code Statement} will be closed when all its dependent result sets are + * closed. If execution of the {@code Statement} does not produce any result sets, this method has + * no effect. + * + *

Note: Multiple calls to {@code closeOnCompletion} do not toggle the effect + * on this {@code Statement}. However, a call to {@code closeOnCompletion} does effect both the + * subsequent execution of statements, and statements that currently have open, dependent, result + * sets. + * + * @throws SQLException if this method is called on a closed {@code Statement} + */ + @Override + public void closeOnCompletion() throws SQLException { + checkNotClosed(); + this.closeOnCompletion = true; + } + + /** + * Returns a value indicating whether this {@code Statement} will be closed when all its dependent + * result sets are closed. + * + * @return {@code true} if the {@code Statement} will be closed when all of its dependent result + * sets are closed; {@code false} otherwise + * @throws SQLException if this method is called on a closed {@code Statement} + */ + @Override + public boolean isCloseOnCompletion() throws SQLException { + checkNotClosed(); + return closeOnCompletion; + } + + /** + * Returns an object that implements the given interface to allow access to non-standard methods, + * or standard methods not exposed by the proxy. + * + *

If the receiver implements the interface then the result is the receiver or a proxy for the + * receiver. If the receiver is a wrapper and the wrapped object implements the interface then the + * result is the wrapped object or a proxy for the wrapped object. Otherwise return the the result + * of calling unwrap recursively on the wrapped object or a proxy for that result. If + * the receiver is not a wrapper and does not implement the interface, then an SQLException + * is thrown. + * + * @param iface A Class defining an interface that the result must implement. + * @return an object that implements the interface. May be a proxy for the actual implementing + * object. + * @throws SQLException If no object found that implements the interface + */ + @Override + @SuppressWarnings("unchecked") + public T unwrap(Class iface) throws SQLException { + if (isWrapperFor(iface)) { + return (T) this; + } + throw exceptionFactory() + .create("The receiver is not a wrapper and does not implement the interface", "42000"); + } + + /** + * Returns true if this either implements the interface argument or is directly or indirectly a + * wrapper for an object that does. Returns false otherwise. If this implements the interface then + * return true, else if this is a wrapper then return the result of recursively calling + * isWrapperFor on the wrapped object. If this does not implement the interface and is not + * a wrapper, return false. This method should be implemented as a low-cost operation compared to + * unwrap so that callers can use this method to avoid expensive unwrap + * calls that may fail. If this method returns true then calling unwrap with the same + * argument should succeed. + * + * @param iface a Class defining an interface. + * @return true if this implements the interface or directly or indirectly wraps an object that + * does. + */ + @Override + public boolean isWrapperFor(Class iface) { + if (iface == null) return false; + return iface.isInstance(this); + } + + /** + * Check if statement is closed, and throw exception if so. + * + * @throws SQLException if statement close + */ + protected void checkNotClosed() throws SQLException { + if (closed) { + throw exceptionFactory().create("Cannot do an operation on a closed statement"); + } + } + + /** + * Executes the given SQL statement, which may be an INSERT, UPDATE, or DELETE statement or an SQL + * statement that returns nothing, such as an SQL DDL statement. This method should be used when + * the returned row count may exceed Integer.MAX_VALUE. + * + * @param sql sql command + * @return update counts + * @throws SQLException if any error occur during execution + */ + @Override + public long executeLargeUpdate(String sql) throws SQLException { + return executeLargeUpdate(sql, Statement.NO_GENERATED_KEYS); + } + + /** + * Identical to executeLargeUpdate(String sql), with a flag that indicate that autoGeneratedKeys + * (primary key fields with "auto_increment") generated id's must be retrieved. + * + *

Those id's will be available using getGeneratedKeys() method. + * + * @param sql sql command + * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be made available + * for retrieval; one of the following constants: Statement.RETURN_GENERATED_KEYS + * Statement.NO_GENERATED_KEYS + * @return update counts + * @throws SQLException if any error occur during execution + */ + @Override + public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + executeInternal(sql, autoGeneratedKeys); + currResult = results.remove(0); + if (currResult instanceof Result) { + throw exceptionFactory() + .create("the given SQL statement produces an unexpected ResultSet object", "HY000"); + } + return ((OkPacket) currResult).getAffectedRows(); + } + + /** + * Identical to executeLargeUpdate(String sql, int autoGeneratedKeys) with autoGeneratedKeys = + * Statement.RETURN_GENERATED_KEYS set. + * + * @param sql sql command + * @param columnIndexes column Indexes + * @return update counts + * @throws SQLException if any error occur during execution + */ + @Override + public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException { + return executeLargeUpdate(sql, java.sql.Statement.RETURN_GENERATED_KEYS); + } + + /** + * Identical to executeLargeUpdate(String sql, int autoGeneratedKeys) with autoGeneratedKeys = + * Statement.RETURN_GENERATED_KEYS set. + * + * @param sql sql command + * @param columnNames columns names + * @return update counts + * @throws SQLException if any error occur during execution + */ + @Override + public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException { + return executeLargeUpdate(sql, java.sql.Statement.RETURN_GENERATED_KEYS); + } + + /** + * Retrieves the maximum number of rows that a ResultSet object produced by this Statement object + * can contain. If this limit is exceeded, the excess rows are silently dropped. + * + * @throws SQLException if this method is called on a closed Statement + * @return the current maximum number of rows for a ResultSet object produced by this Statement + * object; zero means there is no limit + */ + @Override + public long getLargeMaxRows() throws SQLException { + checkNotClosed(); + return maxRows; + } + + /** + * Sets the limit for the maximum number of rows that any ResultSet object generated by this + * Statement object can contain to the given number. If the limit is exceeded, the excess rows are + * silently dropped. + * + * @param max the new max rows limit; zero means there is no limit + * @throws SQLException if the condition max >= 0 is not satisfied + */ + @Override + public void setLargeMaxRows(long max) throws SQLException { + checkNotClosed(); + if (max < 0) { + throw exceptionFactory().create("max rows cannot be negative : asked for " + max, "42000"); + } + maxRows = max; + } + + /** + * Retrieves the current result as an update count; if the result is a ResultSet object or there + * are no more results, -1 is returned. + * + * @throws SQLException if this method is called on a closed Statement + * @return last update count + */ + @Override + public long getLargeUpdateCount() throws SQLException { + checkNotClosed(); + if (currResult instanceof OkPacket) { + return (int) ((OkPacket) currResult).getAffectedRows(); + } + return -1; + } + + /** + * Execute batch, like executeBatch(), with returning results with long[]. For when row count may + * exceed Integer.MAX_VALUE. + * + * @return an array of update counts (one element for each command in the batch) + * @throws SQLException if a database error occur. + */ + @Override + public long[] executeLargeBatch() throws SQLException { + checkNotClosed(); + if (batchQueries == null || batchQueries.isEmpty()) return new long[0]; + + lock.lock(); + try { + List res = executeInternalBatch(); + results = res; + long[] updates = new long[res.size()]; + for (int i = 0; i < res.size(); i++) { + updates[i] = ((OkPacket) res.get(i)).getAffectedRows(); + } + currResult = results.remove(0); + batchQueries.clear(); + return updates; + + } finally { + lock.unlock(); + } + } + + public List executeInternalBatch() throws SQLException { + + QueryPacket[] packets = new QueryPacket[batchQueries.size()]; + for (int i = 0; i < batchQueries.size(); i++) { + String sql = batchQueries.get(i); + packets[i] = new QueryPacket(sql); + } + return con.getClient() + .executePipeline( + packets, + this, + 0, + 0L, + ResultSet.CONCUR_READ_ONLY, + ResultSet.TYPE_FORWARD_ONLY, + closeOnCompletion); + } +} diff --git a/src/main/java/org/mariadb/jdbc/UrlParser.java b/src/main/java/org/mariadb/jdbc/UrlParser.java deleted file mode 100644 index 3de640f83..000000000 --- a/src/main/java/org/mariadb/jdbc/UrlParser.java +++ /dev/null @@ -1,530 +0,0 @@ -/* - * - * MariaDB Client for Java - * - * Copyright (c) 2012-2014 Monty Program Ab. - * Copyright (c) 2015-2020 MariaDB Corporation Ab. - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This library is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License along - * with this library; if not, write to Monty Program Ab info@montyprogram.com. - * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - */ - -package org.mariadb.jdbc; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Properties; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.mariadb.jdbc.credential.CredentialPlugin; -import org.mariadb.jdbc.credential.CredentialPluginLoader; -import org.mariadb.jdbc.internal.logging.LoggerFactory; -import org.mariadb.jdbc.internal.util.constant.HaMode; -import org.mariadb.jdbc.internal.util.constant.ParameterConstant; -import org.mariadb.jdbc.util.DefaultOptions; -import org.mariadb.jdbc.util.Options; - -/** - * parse and verification of URL. - * - *

basic syntax :
- * {@code - * jdbc:(mysql|mariadb):[replication:|failover|loadbalance:|aurora:]//[,]/[database>] - * [?=[&=]] } - * - *

hostDescription:
- * - simple :
- * {@code :}
- * (for example localhost:3306)
- *
- * - complex :
- * {@code address=[(type=(master|slave))][(port=)](host=)}
- *
- *
- * type is by default master
- * port is by default 3306
- * - *

host can be dns name, ipv4 or ipv6.
- * in case of ipv6 and simple host description, the ip must be written inside bracket.
- * exemple : {@code jdbc:mariadb://[2001:0660:7401:0200:0000:0000:0edf:bdd7]:3306}
- * - *

Some examples :
- * {@code jdbc:mariadb://localhost:3306/database?user=greg&password=pass}
- * {@code - * jdbc:mariadb://address=(type=master)(host=master1),address=(port=3307)(type=slave)(host=slave1)/database?user=greg&password=pass} - *
- */ -public class UrlParser implements Cloneable { - - private static final String DISABLE_MYSQL_URL = "disableMariaDbDriver"; - private static final Pattern URL_PARAMETER = - Pattern.compile("(\\/([^\\?]*))?(\\?(.+))*", Pattern.DOTALL); - private static final Pattern AWS_PATTERN = - Pattern.compile("(.+)\\.([a-z0-9\\-]+\\.rds\\.amazonaws\\.com)", Pattern.CASE_INSENSITIVE); - - private String database; - private Options options = null; - private List addresses; - private HaMode haMode; - private String initialUrl; - private boolean multiMaster; - private CredentialPlugin credentialPlugin; - - private UrlParser() {} - - /** - * Constructor. - * - * @param database database - * @param addresses list of hosts - * @param options connection option - * @param haMode High availability mode - * @throws SQLException if credential plugin cannot be loaded - */ - public UrlParser(String database, List addresses, Options options, HaMode haMode) - throws SQLException { - this.options = options; - this.database = database; - this.addresses = addresses; - this.haMode = haMode; - if (haMode == HaMode.AURORA) { - for (HostAddress hostAddress : addresses) { - hostAddress.type = null; - } - } else { - for (HostAddress hostAddress : addresses) { - if (hostAddress.type == null) { - hostAddress.type = ParameterConstant.TYPE_MASTER; - } - } - } - this.credentialPlugin = CredentialPluginLoader.get(options.credentialType); - DefaultOptions.postOptionProcess(options, credentialPlugin); - setInitialUrl(); - loadMultiMasterValue(); - } - - /** - * Tell if mariadb driver accept url string. (Correspond to interface - * java.jdbc.Driver.acceptsURL() method) - * - * @param url url String - * @return true if url string correspond. - */ - public static boolean acceptsUrl(String url) { - return (url != null) - && (url.startsWith("jdbc:mariadb:") - || (url.startsWith("jdbc:mysql:") && !url.contains(DISABLE_MYSQL_URL))); - } - - public static UrlParser parse(final String url) throws SQLException { - return parse(url, new Properties()); - } - - /** - * Parse url connection string with additional properties. - * - * @param url connection string - * @param prop properties - * @return UrlParser instance - * @throws SQLException if parsing exception occur - */ - public static UrlParser parse(final String url, Properties prop) throws SQLException { - if (url != null - && (url.startsWith("jdbc:mariadb:") - || url.startsWith("jdbc:mysql:") && !url.contains(DISABLE_MYSQL_URL))) { - UrlParser urlParser = new UrlParser(); - parseInternal(urlParser, url, (prop == null) ? new Properties() : prop); - return urlParser; - } - return null; - } - - /** - * Parses the connection URL in order to set the UrlParser instance with all the information - * provided through the URL. - * - * @param urlParser object instance in which all data from the connection url is stored - * @param url connection URL - * @param properties properties - * @throws SQLException if format is incorrect - */ - private static void parseInternal(UrlParser urlParser, String url, Properties properties) - throws SQLException { - try { - urlParser.initialUrl = url; - int separator = url.indexOf("//"); - if (separator == -1) { - throw new IllegalArgumentException( - "url parsing error : '//' is not present in the url " + url); - } - - urlParser.haMode = parseHaMode(url, separator); - - String urlSecondPart = url.substring(separator + 2); - int dbIndex = urlSecondPart.indexOf("/"); - int paramIndex = urlSecondPart.indexOf("?"); - - String hostAddressesString; - String additionalParameters; - if ((dbIndex < paramIndex && dbIndex < 0) || (dbIndex > paramIndex && paramIndex > -1)) { - hostAddressesString = urlSecondPart.substring(0, paramIndex); - additionalParameters = urlSecondPart.substring(paramIndex); - } else if ((dbIndex < paramIndex && dbIndex > -1) - || (dbIndex > paramIndex && paramIndex < 0)) { - hostAddressesString = urlSecondPart.substring(0, dbIndex); - additionalParameters = urlSecondPart.substring(dbIndex); - } else { - hostAddressesString = urlSecondPart; - additionalParameters = null; - } - - defineUrlParserParameters(urlParser, properties, hostAddressesString, additionalParameters); - setDefaultHostAddressType(urlParser); - urlParser.loadMultiMasterValue(); - } catch (IllegalArgumentException i) { - throw new SQLException("error parsing url : " + i.getMessage(), i); - } - } - - /* - Parse ConnectorJ compatible urls - jdbc:[mariadb|mysql]://host:port/database - Example: jdbc:mariadb://localhost:3306/test?user=root&password=passwd - */ - - /** - * Sets the parameters of the UrlParser instance: addresses, database and options. It parses - * through the additional parameters given in order to extract the database and the options for - * the connection. - * - * @param urlParser object instance in which all data from the connection URL is stored - * @param properties properties - * @param hostAddressesString string that holds all the host addresses - * @param additionalParameters string that holds all parameters defined for the connection - * @throws SQLException if credential plugin cannot be loaded - */ - private static void defineUrlParserParameters( - UrlParser urlParser, - Properties properties, - String hostAddressesString, - String additionalParameters) - throws SQLException { - - if (additionalParameters != null) { - //noinspection Annotator - Matcher matcher = URL_PARAMETER.matcher(additionalParameters); - matcher.find(); - urlParser.database = matcher.group(2); - urlParser.options = - DefaultOptions.parse(urlParser.haMode, matcher.group(4), properties, urlParser.options); - if (urlParser.database != null && urlParser.database.isEmpty()) { - urlParser.database = null; - } - } else { - urlParser.database = null; - urlParser.options = DefaultOptions.parse(urlParser.haMode, "", properties, urlParser.options); - } - urlParser.credentialPlugin = CredentialPluginLoader.get(urlParser.options.credentialType); - DefaultOptions.postOptionProcess(urlParser.options, urlParser.credentialPlugin); - - LoggerFactory.init( - urlParser.options.log - || urlParser.options.profileSql - || urlParser.options.slowQueryThresholdNanos != null); - urlParser.addresses = HostAddress.parse(hostAddressesString, urlParser.haMode); - } - - private static HaMode parseHaMode(String url, int separator) { - // parser is sure to have at least 2 colon, since jdbc:[mysql|mariadb]: is tested. - int firstColonPos = url.indexOf(':'); - int secondColonPos = url.indexOf(':', firstColonPos + 1); - int thirdColonPos = url.indexOf(':', secondColonPos + 1); - - if (thirdColonPos > separator || thirdColonPos == -1) { - if (secondColonPos == separator - 1) { - return HaMode.NONE; - } - thirdColonPos = separator; - } - - try { - String haModeString = - url.substring(secondColonPos + 1, thirdColonPos).toUpperCase(Locale.ROOT); - if ("FAILOVER".equals(haModeString)) { - haModeString = "LOADBALANCE"; - } - return HaMode.valueOf(haModeString); - } catch (IllegalArgumentException i) { - throw new IllegalArgumentException( - "wrong failover parameter format in connection String " + url); - } - } - - private static void setDefaultHostAddressType(UrlParser urlParser) { - if (urlParser.haMode == HaMode.AURORA) { - for (HostAddress hostAddress : urlParser.addresses) { - hostAddress.type = null; - } - } else { - for (HostAddress hostAddress : urlParser.addresses) { - if (hostAddress.type == null) { - hostAddress.type = ParameterConstant.TYPE_MASTER; - } - } - } - } - - private void setInitialUrl() { - StringBuilder sb = new StringBuilder(); - sb.append("jdbc:mariadb:"); - if (haMode != HaMode.NONE) { - sb.append(haMode.toString().toLowerCase(Locale.ROOT)).append(":"); - } - sb.append("//"); - - for (int i = 0; i < addresses.size(); i++) { - HostAddress hostAddress = addresses.get(i); - if (i > 0) { - sb.append(","); - } - sb.append("address=(host=") - .append(hostAddress.host) - .append(")") - .append("(port=") - .append(hostAddress.port) - .append(")"); - if (hostAddress.type != null) { - sb.append("(type=").append(hostAddress.type).append(")"); - } - } - - sb.append("/"); - if (database != null) { - sb.append(database); - } - DefaultOptions.propertyString(options, haMode, sb); - initialUrl = sb.toString(); - } - - /** - * Permit to set parameters not forced. if options useBatchMultiSend and usePipelineAuth are not - * explicitly set in connection string, value will default to true or false according if aurora - * detection. - * - * @return UrlParser for easy testing - */ - public UrlParser auroraPipelineQuirks() { - - // Aurora has issue with pipelining, depending on network speed. - // Driver must rely on information provided by user : hostname if dns, and HA mode.

- boolean disablePipeline = isAurora(); - - if (options.useBatchMultiSend == null) { - options.useBatchMultiSend = disablePipeline ? Boolean.FALSE : Boolean.TRUE; - } - - if (options.usePipelineAuth == null) { - options.usePipelineAuth = disablePipeline ? Boolean.FALSE : Boolean.TRUE; - } - return this; - } - - /** - * Detection of Aurora. - * - *

Aurora rely on MySQL, then cannot be identified by protocol. But Aurora doesn't permit some - * behaviour normally working with MySQL : pipelining. So Driver must identified if server is - * Aurora to disable pipeline options that are enable by default. - * - * @return true if aurora. - */ - public boolean isAurora() { - if (haMode == HaMode.AURORA) { - return true; - } - if (addresses != null) { - for (HostAddress hostAddress : addresses) { - Matcher matcher = AWS_PATTERN.matcher(hostAddress.host); - if (matcher.find()) { - return true; - } - } - } - return false; - } - - /** - * Parse url connection string. - * - * @param url connection string - * @throws SQLException if url format is incorrect - */ - public void parseUrl(String url) throws SQLException { - if (acceptsUrl(url)) { - parseInternal(this, url, new Properties()); - } - } - - public String getUsername() { - return options.user; - } - - public void setUsername(String username) { - options.user = username; - } - - public String getPassword() { - return options.password; - } - - public void setPassword(String password) { - options.password = password; - } - - public String getDatabase() { - return database; - } - - public void setDatabase(String database) { - this.database = database; - } - - public List getHostAddresses() { - return this.addresses; - } - - public Options getOptions() { - return options; - } - - protected void setProperties(String urlParameters) { - DefaultOptions.parse(this.haMode, urlParameters, this.options); - setInitialUrl(); - } - - public CredentialPlugin getCredentialPlugin() { - return credentialPlugin; - } - - /** - * ToString implementation. - * - * @return String value - */ - public String toString() { - return initialUrl; - } - - public String getInitialUrl() { - return initialUrl; - } - - public HaMode getHaMode() { - return haMode; - } - - @Override - public boolean equals(Object parser) { - if (this == parser) { - return true; - } - if (!(parser instanceof UrlParser)) { - return false; - } - - UrlParser urlParser = (UrlParser) parser; - return (initialUrl != null - ? initialUrl.equals(urlParser.getInitialUrl()) - : urlParser.getInitialUrl() == null) - && (getUsername() != null - ? getUsername().equals(urlParser.getUsername()) - : urlParser.getUsername() == null) - && (getPassword() != null - ? getPassword().equals(urlParser.getPassword()) - : urlParser.getPassword() == null); - } - - @Override - public int hashCode() { - int result = options.password != null ? options.password.hashCode() : 0; - result = 31 * result + (options.user != null ? options.user.hashCode() : 0); - result = 31 * result + initialUrl.hashCode(); - return result; - } - - private void loadMultiMasterValue() { - if (haMode == HaMode.SEQUENTIAL - || haMode == HaMode.REPLICATION - || haMode == HaMode.LOADBALANCE) { - boolean firstMaster = false; - for (HostAddress host : addresses) { - if (host.type.equals(ParameterConstant.TYPE_MASTER)) { - if (firstMaster) { - multiMaster = true; - return; - } else { - firstMaster = true; - } - } - } - } - multiMaster = false; - } - - public boolean isMultiMaster() { - return multiMaster; - } - - @Override - public Object clone() throws CloneNotSupportedException { - UrlParser tmpUrlParser = (UrlParser) super.clone(); - tmpUrlParser.options = (Options) options.clone(); - tmpUrlParser.addresses = new ArrayList<>(); - tmpUrlParser.addresses.addAll(addresses); - return tmpUrlParser; - } -} diff --git a/src/main/java/org/mariadb/jdbc/client/Client.java b/src/main/java/org/mariadb/jdbc/client/Client.java new file mode 100644 index 000000000..dd62121c9 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/Client.java @@ -0,0 +1,95 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client; + +import java.sql.SQLException; +import java.util.List; +import java.util.concurrent.Executor; +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.message.client.ClientMessage; +import org.mariadb.jdbc.message.server.Completion; +import org.mariadb.jdbc.message.server.PrepareResultPacket; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; + +public interface Client extends AutoCloseable { + + List execute(ClientMessage message) throws SQLException; + + List execute(ClientMessage message, org.mariadb.jdbc.Statement stmt) + throws SQLException; + + List execute( + ClientMessage message, + org.mariadb.jdbc.Statement stmt, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException; + + List executePipeline( + ClientMessage[] messages, + org.mariadb.jdbc.Statement stmt, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException; + + void readStreamingResults( + List completions, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException; + + void closePrepare(PrepareResultPacket prepare) throws SQLException; + + void transactionReplay(TransactionSaver transactionSaver) throws SQLException; + + void abort(Executor executor) throws SQLException; + + void close() throws SQLException; + + void setReadOnly(boolean readOnly) throws SQLException; + + int getSocketTimeout(); + + void setSocketTimeout(int milliseconds) throws SQLException; + + boolean isClosed(); + + boolean isPrimary(); + + Context getContext(); + + ExceptionFactory getExceptionFactory(); + + void reset(ExceptionFactory exceptionFactory); + + HostAddress getHostAddress(); +} diff --git a/src/main/java/org/mariadb/jdbc/client/ClientImpl.java b/src/main/java/org/mariadb/jdbc/client/ClientImpl.java new file mode 100644 index 000000000..f1b1607a7 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/ClientImpl.java @@ -0,0 +1,775 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLPermission; +import java.time.ZoneId; +import java.util.*; +import java.util.concurrent.Executor; +import java.util.concurrent.locks.ReentrantLock; +import javax.net.ssl.SSLSocket; +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.ServerPreparedStatement; +import org.mariadb.jdbc.client.context.BaseContext; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.client.context.RedoContext; +import org.mariadb.jdbc.client.result.Result; +import org.mariadb.jdbc.client.result.StreamingResult; +import org.mariadb.jdbc.message.client.*; +import org.mariadb.jdbc.message.server.Completion; +import org.mariadb.jdbc.message.server.InitialHandshakePacket; +import org.mariadb.jdbc.message.server.PrepareResultPacket; +import org.mariadb.jdbc.plugin.credential.Credential; +import org.mariadb.jdbc.plugin.credential.CredentialPlugin; +import org.mariadb.jdbc.util.MutableInt; +import org.mariadb.jdbc.util.Security; +import org.mariadb.jdbc.util.constants.Capabilities; +import org.mariadb.jdbc.util.constants.HaMode; +import org.mariadb.jdbc.util.constants.ServerStatus; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; +import org.mariadb.jdbc.util.log.Logger; +import org.mariadb.jdbc.util.log.Loggers; + +public class ClientImpl implements Client, AutoCloseable { + + private static final Logger logger = Loggers.getLogger(ClientImpl.class); + + private static Integer MAX_ALLOWED_PACKET = 0; + + private final Socket socket; + private final Context context; + private final MutableInt sequence = new MutableInt(0); + private final ReentrantLock lock; + private final Configuration conf; + private final HostAddress hostAddress; + private boolean closed = false; + private ExceptionFactory exceptionFactory; + private PacketWriter writer; + private PacketReader reader; + private OutputStream out; + private org.mariadb.jdbc.Statement streamStmt = null; + private ClientMessage streamMsg = null; + private int socketTimeout; + + public ClientImpl( + Configuration conf, + HostAddress hostAddress, + boolean saveTransaction, + ReentrantLock lock, + boolean skipPostCommands) + throws SQLException { + this.conf = conf; + this.lock = lock; + this.hostAddress = hostAddress; + this.exceptionFactory = new ExceptionFactory(conf, hostAddress); + + List galeraAllowedStates = + conf.galeraAllowedState() == null + ? Collections.emptyList() + : Arrays.asList(conf.galeraAllowedState().split(",")); + String host = hostAddress != null ? hostAddress.host : null; + int port = hostAddress != null ? hostAddress.port : 3306; + + this.socket = ConnectionHelper.createSocket(host, port, conf); + assignStream(this.socket, conf); + + try { + + final InitialHandshakePacket handshake = + InitialHandshakePacket.decode(reader.readPacket(true)); + this.exceptionFactory.setThreadId(handshake.getThreadId()); + this.context = + conf.haMode() != HaMode.NONE + ? new RedoContext(handshake, conf, this.exceptionFactory, new PrepareCache(100, this)) + : new BaseContext( + handshake, conf, this.exceptionFactory, new PrepareCache(100, this)); + this.reader.setServerThreadId(handshake.getThreadId(), hostAddress); + this.writer.setServerThreadId(handshake.getThreadId(), hostAddress); + this.writer.setContext(context); + + long clientCapabilities = + ConnectionHelper.initializeClientCapabilities(conf, this.context.getServerCapabilities()); + byte exchangeCharset = ConnectionHelper.decideLanguage(handshake); + + SSLSocket sslSocket = + ConnectionHelper.sslWrapper( + host, socket, clientCapabilities, exchangeCharset, context, writer); + + if (sslSocket != null) { + assignStream(sslSocket, conf); + this.reader.setServerThreadId(handshake.getThreadId(), hostAddress); + this.writer.setServerThreadId(handshake.getThreadId(), hostAddress); + } + + String authenticationPluginType = handshake.getAuthenticationPluginType(); + CredentialPlugin credentialPlugin = conf.credentialPlugin(); + if (credentialPlugin != null && credentialPlugin.defaultAuthenticationPluginType() != null) { + authenticationPluginType = credentialPlugin.defaultAuthenticationPluginType(); + } + Credential credential = ConnectionHelper.loadCredential(credentialPlugin, conf, hostAddress); + + new HandshakeResponse( + authenticationPluginType, + context.getSeed(), + conf, + host, + clientCapabilities, + exchangeCharset) + .encode(writer, context); + writer.flush(); + + ConnectionHelper.authenticationHandler(credential, writer, reader, context); + ConnectionHelper.compressionHandler(conf, context); + + if (conf.socketTimeout() > 0) setSocketTimeout(conf.socketTimeout()); + + if (!skipPostCommands) { + postConnectionQueries(); + galeraStateValidation(galeraAllowedStates); + } + + } catch (IOException ioException) { + destroySocket(); + + String errorMsg = + String.format( + "Could not connect to %s:%s : %s", host, socket.getPort(), ioException.getMessage()); + if (host == null) { + errorMsg = String.format("Could not connect to socket : %s", ioException.getMessage()); + } + + throw exceptionFactory.create(errorMsg, "08000", ioException); + } catch (SQLException sqlException) { + destroySocket(); + throw sqlException; + } + } + + private void assignStream(Socket socket, Configuration conf) throws SQLException { + try { + out = socket.getOutputStream(); + this.writer = new PacketWriter(out, conf.maxQuerySizeToLog(), sequence); + this.reader = new PacketReader(socket.getInputStream(), conf, sequence); + } catch (IOException ioe) { + destroySocket(); + throw exceptionFactory.create("Socket error: " + ioe.getMessage(), "08000", ioe); + } + } + + /** Closing socket in case of Connection error after socket creation. */ + private void destroySocket() { + closed = true; + try { + this.reader.close(); + } catch (IOException ee) { + // eat exception + } + try { + this.writer.close(); + } catch (IOException ee) { + // eat exception + } + try { + this.socket.close(); + } catch (IOException ee) { + // eat exception + } + } + + private void galeraStateValidation(List galeraAllowedStates) throws SQLException { + if (hostAddress != null + && Boolean.TRUE.equals(hostAddress.primary) + && !galeraAllowedStates.isEmpty()) { + List res = execute(new QueryPacket("show status like 'wsrep_local_state'")); + if (res.isEmpty()) { + throw exceptionFactory.create("fail to validate Galera state"); + } + ResultSet rs = (ResultSet) res.get(0); + rs.next(); + if (!galeraAllowedStates.contains(rs.getString(2))) { + throw exceptionFactory.create( + String.format("fail to validate Galera state (State is %s)", rs.getString(2))); + } + } + } + + private void postConnectionQueries() throws SQLException { + + try { + execute(createSessionVariableQuery()); + } catch (SQLException sqlException) { + // timezone is not valid + if (conf.timezone() != null) { + throw exceptionFactory.create( + String.format( + "Setting configured timezone '%s' fail on server.\nLook at https://mariadb.com/kb/en/mysql_tzinfo_to_sql/ to load tz data on server, or set timezone=disable to disable setting client timezone.", + conf.timezone())); + } else { + throw exceptionFactory.create( + String.format( + "Setting configured client timezone '%s' fail on server.\nLook at https://mariadb.com/kb/en/mysql_tzinfo_to_sql/ to load tz data on server, or set timezone=disable to disable setting client timezone.", + ZoneId.systemDefault().getId())); + } + } + + if (conf.assureReadOnly() + && !this.hostAddress.primary + && context.getVersion().versionGreaterOrEqual(5, 6, 5)) { + execute(new QueryPacket("SET SESSION TRANSACTION READ ONLY")); + } + + writer.setMaxAllowedPacket( + conf.maxAllowedPacket() != null + ? conf.maxAllowedPacket() + : getMaxAllowedPacketInstance(this)); + } + + public QueryPacket createSessionVariableQuery() { + // In JDBC, connection must start in autocommit mode + // [CONJ-269] we cannot rely on serverStatus & ServerStatus.AUTOCOMMIT before this command to + // avoid this command. + // if autocommit=0 is set on server configuration, DB always send Autocommit on serverStatus + // flag + // after setting autocommit, we can rely on serverStatus value + StringBuilder sb = new StringBuilder(); + sb.append("autocommit=") + .append(conf.autocommit() ? "1" : "0") + .append(", sql_mode = concat(@@sql_mode,',STRICT_TRANS_TABLES')"); + + // force schema tracking if available + if ((context.getServerCapabilities() & Capabilities.CLIENT_SESSION_TRACK) != 0) { + sb.append(", session_track_schema=1"); + } + + // add configured session variable if configured + if (conf.sessionVariables() != null && !conf.sessionVariables().isEmpty()) { + sb.append(",").append(Security.parseSessionVariables(conf.sessionVariables())); + } + + // force client timezone to connection to ensure result of now(), ... + if (conf.timezone() != null && !"disable".equalsIgnoreCase(conf.timezone())) { + String timeZone = + (conf.timezone() != null) ? conf.timezone() : ZoneId.systemDefault().getId(); + sb.append(",time_zone='").append(timeZone).append("'"); + } + return new QueryPacket("set " + sb.toString()); + } + + public static Integer getMaxAllowedPacketInstance(ClientImpl clientImpl) throws SQLException { + if (MAX_ALLOWED_PACKET == 0) { + synchronized (MAX_ALLOWED_PACKET) { + if (MAX_ALLOWED_PACKET == 0) { + try { + QueryPacket requestSessionVariables = new QueryPacket("SELECT @@max_allowed_packet"); + List completion = clientImpl.execute(requestSessionVariables); + Result result = (Result) completion.get(0); + result.next(); + MAX_ALLOWED_PACKET = Integer.parseInt(result.getString(1)); + } catch (SQLException sqlException) { + // fallback in case of galera non primary nodes that permit only show / set command, + // not SELECT when not part of quorum + List res = + clientImpl.execute( + new QueryPacket("SHOW VARIABLES WHERE Variable_name = 'max_allowed_packet'")); + if (res.isEmpty()) { + throw new SQLException("fail to readAdditionalData"); + } + ResultSet rs = (ResultSet) res.get(0); + rs.next(); + if (logger.isDebugEnabled()) { + logger.debug("server data {} = {}", rs.getString(1), rs.getString(2)); + } + // max_allowed_packet + MAX_ALLOWED_PACKET = Integer.parseInt(rs.getString(2)); + } + } + } + } + return MAX_ALLOWED_PACKET; + } + + public void setReadOnly(boolean readOnly) throws SQLException { + if (conf.haMode() == HaMode.NONE) { + execute( + new QueryPacket("SET SESSION TRANSACTION " + (readOnly ? "READ ONLY" : "READ WRITE"))); + } + } + + public int sendQuery(ClientMessage message) throws SQLException { + checkNotClosed(); + try { + return message.encodePacket(writer, context); + } catch (IOException ioException) { + destroySocket(); + throw exceptionFactory + .withSql(message.description()) + .create("Socket error", "08000", ioException); + } + } + + public List execute(ClientMessage message) throws SQLException { + return execute( + message, null, 0, 0L, ResultSet.CONCUR_READ_ONLY, ResultSet.TYPE_FORWARD_ONLY, false); + } + + public List execute(ClientMessage message, org.mariadb.jdbc.Statement stmt) + throws SQLException { + return execute( + message, stmt, 0, 0L, ResultSet.CONCUR_READ_ONLY, ResultSet.TYPE_FORWARD_ONLY, false); + } + + public List executePipeline( + ClientMessage[] messages, + org.mariadb.jdbc.Statement stmt, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException { + List results = new ArrayList<>(); + + int readCounter = 0; + int[] responseMsg = new int[messages.length]; + try { + for (int i = 0; i < messages.length; i++) { + responseMsg[i] = sendQuery(messages[i]); + } + for (; readCounter < messages.length; readCounter++) { + for (int j = 0; j < responseMsg[readCounter]; j++) { + results.addAll( + readResponse( + stmt, + messages[readCounter], + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion)); + } + } + return results; + } catch (SQLException sqlException) { + + // read remaining results + for (int i = ++readCounter; i < messages.length; i++) { + for (int j = 0; j < responseMsg[readCounter]; j++) { + try { + results.addAll( + readResponse( + stmt, + messages[readCounter], + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion)); + } catch (SQLException e) { + // eat + } + } + } + + // prepare associated to PrepareStatement need to be uncached + for (int i = 0; i < results.size(); i++) { + if (results.get(i) instanceof PrepareResultPacket + && stmt instanceof ServerPreparedStatement) { + try { + ((PrepareResultPacket) results.get(i)) + .decrementUse(this, (ServerPreparedStatement) stmt); + } catch (SQLException e) { + // eat + } + } + } + + int batchUpdateLength = 0; + for (int i = 0; i < messages.length; i++) { + batchUpdateLength += messages[i].batchUpdateLength(); + } + throw exceptionFactory.createBatchUpdate( + results, batchUpdateLength, responseMsg, sqlException); + } + } + + public List execute( + ClientMessage message, + org.mariadb.jdbc.Statement stmt, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException { + sendQuery(message); + return readResponse( + stmt, message, fetchSize, maxRows, resultSetConcurrency, resultSetType, closeOnCompletion); + } + + public List readResponse( + org.mariadb.jdbc.Statement stmt, + ClientMessage message, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException { + checkNotClosed(); + if (streamStmt != null) { + streamStmt.fetchRemaining(); + streamStmt = null; + } + List completions = new ArrayList<>(); + readResults( + stmt, + message, + completions, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion); + return completions; + } + + public List readResponse(ClientMessage message) throws SQLException { + checkNotClosed(); + if (streamStmt != null) { + streamStmt.fetchRemaining(); + streamStmt = null; + } + List completions = new ArrayList<>(); + readResults( + null, + message, + completions, + 0, + 0L, + ResultSet.CONCUR_READ_ONLY, + ResultSet.TYPE_FORWARD_ONLY, + false); + return completions; + } + + public void closePrepare(PrepareResultPacket prepare) throws SQLException { + checkNotClosed(); + try { + new ClosePreparePacket(prepare.getStatementId()).encode(writer, context); + } catch (IOException ioException) { + destroySocket(); + throw exceptionFactory.create( + "Socket error during post connection queries: " + ioException.getMessage(), + "08000", + ioException); + } + } + + public void transactionReplay(TransactionSaver transactionSaver) throws SQLException { + List buffers = transactionSaver.getBuffers(); + try { + // replay all but last + PrepareResultPacket prepare; + for (int i = 0; i < buffers.size() - 1; i++) { + RedoableClientMessage querySaver = buffers.get(i); + int responseNo; + if (querySaver instanceof RedoableWithPrepareClientMessage) { + // command is a prepare statement query + // redo on new connection need to re-prepare query + // and substitute statement id + RedoableWithPrepareClientMessage redoable = + ((RedoableWithPrepareClientMessage) querySaver); + String cmd = redoable.getCommand(); + prepare = context.getPrepareCache().get(cmd, redoable.prep()); + if (prepare == null) { + PreparePacket preparePacket = new PreparePacket(cmd); + sendQuery(preparePacket); + prepare = (PrepareResultPacket) readPacket(preparePacket); + } + responseNo = querySaver.reExecutePacket(writer, context, prepare); + } else { + responseNo = querySaver.reExecutePacket(writer, context, null); + } + for (int j = 0; j < responseNo; j++) { + readResponse(querySaver); + } + } + } catch (IOException e) { + throw exceptionFactory.create("Socket error during transaction replay", "08000", e); + } + } + + private void readResults(ClientMessage message, List results) throws SQLException { + readResults( + null, + message, + results, + 0, + 0L, + ResultSet.CONCUR_READ_ONLY, + ResultSet.TYPE_FORWARD_ONLY, + false); + } + + public void readStreamingResults( + List completions, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException { + if (streamStmt != null) { + readResults( + streamStmt, + streamMsg, + completions, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion); + } + } + + private void readResults( + org.mariadb.jdbc.Statement stmt, + ClientMessage message, + List completions, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException { + completions.add( + readPacket( + stmt, + message, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion)); + + while ((context.getServerStatus() & ServerStatus.MORE_RESULTS_EXISTS) > 0) { + completions.add( + readPacket( + stmt, + message, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion)); + } + } + + public Completion readPacket(ClientMessage message) throws SQLException { + return readPacket( + null, message, 0, 0L, ResultSet.CONCUR_READ_ONLY, ResultSet.TYPE_FORWARD_ONLY, false); + } + + /** + * Read server response packet. + * + * @see server response + * packets + * @param stmt current statement (null if internal) + * @param message current message + * @param fetchSize default fetch size + * @param resultSetConcurrency concurrency + * @param resultSetType type + * @param closeOnCompletion must resultset close statement on completion + * @throws SQLException if any exception + */ + public Completion readPacket( + org.mariadb.jdbc.Statement stmt, + ClientMessage message, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException { + try { + Completion completion = + message.readPacket( + stmt, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion, + reader, + context, + exceptionFactory, + lock); + if (completion instanceof StreamingResult && !((StreamingResult) completion).loaded()) { + streamStmt = stmt; + streamMsg = message; + } + return completion; + } catch (IOException ioException) { + destroySocket(); + throw exceptionFactory + .withSql(message.description()) + .create("Socket error", "08000", ioException); + } + } + + private void checkNotClosed() throws SQLException { + if (closed) { + throw exceptionFactory.create("Connection is closed", "08000", 1220); + } + } + + private void closeSocket() { + try { + try { + long maxCurrentMillis = System.currentTimeMillis() + 10; + socket.shutdownOutput(); + socket.setSoTimeout(3); + InputStream is = socket.getInputStream(); + //noinspection StatementWithEmptyBody + while (is.read() != -1 && System.currentTimeMillis() < maxCurrentMillis) { + // read byte + } + } catch (Throwable t) { + // eat exception + } + writer.close(); + reader.close(); + } catch (IOException e) { + // eat + } finally { + try { + socket.close(); + } catch (IOException e) { + // socket closed, if any error, so not throwing error + } + } + } + + public boolean isClosed() { + return closed; + } + + public Context getContext() { + return context; + } + + public void abort(Executor executor) throws SQLException { + + SQLPermission sqlPermission = new SQLPermission("callAbort"); + SecurityManager securityManager = System.getSecurityManager(); + if (securityManager != null) { + securityManager.checkPermission(sqlPermission); + } + if (executor == null) { + throw exceptionFactory.create("Cannot abort the connection: null executor passed"); + } + + // fireConnectionClosed(new ConnectionEvent(this)); + boolean lockStatus = lock.tryLock(); + + if (!this.closed) { + this.closed = true; + if (!lockStatus) { + // lock not available : query is running + // force end by executing an KILL connection + try (ClientImpl cli = new ClientImpl(conf, hostAddress, false, new ReentrantLock(), true)) { + cli.execute(new QueryPacket("KILL " + context.getThreadId())); + } catch (SQLException e) { + // eat + } + } else { + try { + QuitPacket.INSTANCE.encode(writer, context); + } catch (IOException e) { + // eat + } + } + closeSocket(); + } + + if (lockStatus) { + lock.unlock(); + } + } + + public int getSocketTimeout() { + return this.socketTimeout; + } + + public void setSocketTimeout(int milliseconds) throws SQLException { + try { + socketTimeout = milliseconds; + socket.setSoTimeout(milliseconds); + } catch (SocketException se) { + throw exceptionFactory.create("Cannot set the network timeout", se); + } + } + + public void close() throws SQLException { + boolean locked = lock.tryLock(); + + if (!this.closed) { + this.closed = true; + try { + QuitPacket.INSTANCE.encode(writer, context); + } catch (IOException e) { + // eat + } + closeSocket(); + } + + if (locked) { + lock.unlock(); + } + } + + public boolean isPrimary() { + return hostAddress.primary; + } + + public ExceptionFactory getExceptionFactory() { + return exceptionFactory; + } + + public void reset(ExceptionFactory exceptionFactory) { + this.exceptionFactory = exceptionFactory; + this.context.resetPrepareCache(new PrepareCache(250, this)); + } + + public HostAddress getHostAddress() { + return hostAddress; + } +} diff --git a/src/main/java/org/mariadb/jdbc/client/ConnectionHelper.java b/src/main/java/org/mariadb/jdbc/client/ConnectionHelper.java new file mode 100644 index 000000000..fab46c8b9 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/ConnectionHelper.java @@ -0,0 +1,397 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLNonTransientConnectionException; +import java.util.Arrays; +import java.util.List; +import javax.net.SocketFactory; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.SslMode; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.client.socket.SocketHandlerFunction; +import org.mariadb.jdbc.client.socket.SocketUtility; +import org.mariadb.jdbc.message.client.SslRequestPacket; +import org.mariadb.jdbc.message.server.AuthSwitchPacket; +import org.mariadb.jdbc.message.server.ErrorPacket; +import org.mariadb.jdbc.message.server.InitialHandshakePacket; +import org.mariadb.jdbc.plugin.authentication.AuthenticationPlugin; +import org.mariadb.jdbc.plugin.authentication.AuthenticationPluginLoader; +import org.mariadb.jdbc.plugin.credential.Credential; +import org.mariadb.jdbc.plugin.credential.CredentialPlugin; +import org.mariadb.jdbc.plugin.tls.TlsSocketPlugin; +import org.mariadb.jdbc.plugin.tls.TlsSocketPluginLoader; +import org.mariadb.jdbc.util.ConfigurableSocketFactory; +import org.mariadb.jdbc.util.constants.Capabilities; + +public class ConnectionHelper { + + private static final SocketHandlerFunction socketHandler; + + static { + SocketHandlerFunction init; + try { + init = SocketUtility.getSocketHandler(); + } catch (Throwable t) { + SocketHandlerFunction defaultSocketHandler = (conf, host) -> standardSocket(conf, host); + init = defaultSocketHandler; + } + socketHandler = init; + } + + /** + * Create socket accordingly to options. + * + * @param conf Url options + * @param host hostName ( mandatory only for named pipe) + * @return a nex socket + * @throws IOException if connection error occur + */ + public static Socket createSocket(Configuration conf, String host) throws IOException { + return socketHandler.apply(conf, host); + } + + /** + * Use standard socket implementation. + * + * @param conf url options + * @param host host to connect + * @return socket + * @throws IOException in case of error establishing socket. + */ + public static Socket standardSocket(Configuration conf, String host) throws IOException { + SocketFactory socketFactory; + String socketFactoryName = conf.socketFactory(); + if (socketFactoryName != null) { + try { + @SuppressWarnings("unchecked") + Class socketFactoryClass = + (Class) Class.forName(socketFactoryName); + if (socketFactoryClass != null) { + Constructor constructor = socketFactoryClass.getConstructor(); + socketFactory = constructor.newInstance(); + if (socketFactoryClass.isInstance(ConfigurableSocketFactory.class)) { + ((ConfigurableSocketFactory) socketFactory).setConfiguration(conf, host); + } + return socketFactory.createSocket(); + } + } catch (Exception exp) { + throw new IOException( + "Socket factory failed to initialized with option \"socketFactory\" set to \"" + + conf.socketFactory() + + "\"", + exp); + } + } + socketFactory = SocketFactory.getDefault(); + return socketFactory.createSocket(); + } + + public static Socket createSocket(final String host, final int port, final Configuration conf) + throws SQLException { + Socket socket; + try { + socket = createSocket(conf, host); + socket.setTcpNoDelay(true); + + socket.setSoTimeout(conf.socketTimeout()); + if (conf.tcpKeepAlive()) { + socket.setKeepAlive(true); + } + if (conf.tcpAbortiveClose()) { + socket.setSoLinger(true, 0); + } + + // Bind the socket to a particular interface if the connection property + // localSocketAddress has been defined. + if (conf.localSocketAddress() != null) { + InetSocketAddress localAddress = new InetSocketAddress(conf.localSocketAddress(), 0); + socket.bind(localAddress); + } + + if (!socket.isConnected()) { + InetSocketAddress sockAddr = conf.pipe() == null ? new InetSocketAddress(host, port) : null; + socket.connect(sockAddr, conf.connectTimeout()); + } + return socket; + + } catch (IOException ioe) { + throw new SQLNonTransientConnectionException( + String.format( + "Socket fail to connect to host:%s, port:%s. %s", host, port, ioe.getMessage()), + "08000", + ioe); + } + } + + public static long initializeClientCapabilities( + final Configuration configuration, final long serverCapabilities) { + long capabilities = + Capabilities.IGNORE_SPACE + | Capabilities.CLIENT_PROTOCOL_41 + | Capabilities.TRANSACTIONS + | Capabilities.SECURE_CONNECTION + | Capabilities.MULTI_RESULTS + | Capabilities.PS_MULTI_RESULTS + | Capabilities.PLUGIN_AUTH + | Capabilities.CONNECT_ATTRS + | Capabilities.PLUGIN_AUTH_LENENC_CLIENT_DATA + | Capabilities.CLIENT_SESSION_TRACK + | Capabilities.MARIADB_CLIENT_STMT_BULK_OPERATIONS; + + if (!configuration.useAffectedRows()) { + capabilities |= Capabilities.FOUND_ROWS; + } + + if (configuration.allowMultiQueries() || (configuration.rewriteBatchedStatements())) { + capabilities |= Capabilities.MULTI_STATEMENTS; + } + + if ((serverCapabilities & Capabilities.CLIENT_DEPRECATE_EOF) != 0) { + capabilities |= Capabilities.CLIENT_DEPRECATE_EOF; + } + + if (configuration.useCompression() && ((serverCapabilities & Capabilities.COMPRESS) != 0)) { + capabilities |= Capabilities.COMPRESS; + } + + if (!configuration.database().isEmpty()) { + capabilities |= Capabilities.CONNECT_WITH_DB; + } + return capabilities; + } + + /** + * Default collation used for string exchanges with server. Always return 4 bytes utf8 collation + * for server that permit it. + * + * @param handshake initial handshake packet + * @return collation byte + */ + public static byte decideLanguage(InitialHandshakePacket handshake) { + short serverLanguage = handshake.getDefaultCollation(); + // return current server utf8mb4 collation + if (serverLanguage == 45 // utf8mb4_general_ci + || serverLanguage == 46 // utf8mb4_bin + || (serverLanguage >= 224 && serverLanguage <= 247)) { + return (byte) serverLanguage; + } + if (handshake.getMajorServerVersion() == 5 && handshake.getMinorServerVersion() <= 1) { + // 5.1 version doesn't know 4 bytes utf8 + return (byte) 33; // utf8_general_ci + } + return (byte) 224; // UTF8MB4_UNICODE_CI; + } + + public static void authenticationHandler( + Credential credential, PacketWriter writer, PacketReader reader, Context context) + throws SQLException, IOException { + + writer.permitTrace(true); + Configuration conf = context.getConf(); + ReadableByteBuf buf = reader.readPacket(false); + + authentication_loop: + while (true) { + switch (buf.getByte() & 0xFF) { + case 0xFE: + // ************************************************************************************* + // Authentication Switch Request see + // https://mariadb.com/kb/en/library/connection/#authentication-switch-request + // ************************************************************************************* + AuthSwitchPacket authSwitchPacket = AuthSwitchPacket.decode(buf, context); + AuthenticationPlugin authenticationPlugin = + AuthenticationPluginLoader.get(authSwitchPacket.getPlugin()); + + authenticationPlugin.initialize(credential.getPassword(), context.getSeed(), conf); + buf = authenticationPlugin.process(writer, reader, context); + break; + + case 0xFF: + // ************************************************************************************* + // ERR_Packet + // see https://mariadb.com/kb/en/library/err_packet/ + // ************************************************************************************* + ErrorPacket errorPacket = new ErrorPacket(buf, context); + throw context + .getExceptionFactory() + .create( + errorPacket.getMessage(), errorPacket.getSqlState(), errorPacket.getErrorCode()); + + case 0x00: + // ************************************************************************************* + // OK_Packet -> Authenticated ! + // see https://mariadb.com/kb/en/library/ok_packet/ + // ************************************************************************************* + buf.skip(); // 0x00 OkPacket Header + buf.skip(buf.readLengthNotNull()); // affectedRows + buf.skip(buf.readLengthNotNull()); + // insertId + context.setServerStatus(buf.readShort()); + break authentication_loop; + + default: + throw context + .getExceptionFactory() + .create( + "unexpected data during authentication (header=" + (buf.getUnsignedByte()), + "08000"); + } + } + writer.permitTrace(true); + } + + public static Credential loadCredential( + CredentialPlugin credentialPlugin, Configuration configuration, HostAddress hostAddress) + throws SQLException { + if (credentialPlugin != null) { + return credentialPlugin.initialize(configuration, configuration.user(), hostAddress).get(); + } + return new Credential(configuration.user(), configuration.password()); + } + + public static SSLSocket sslWrapper( + final String host, + final Socket socket, + long clientCapabilities, + final byte exchangeCharset, + Context context, + PacketWriter writer) + throws SQLException, IOException { + + Configuration conf = context.getConf(); + if (conf.sslMode() != SslMode.DISABLE) { + + if ((context.getServerCapabilities() & Capabilities.SSL) == 0) { + context + .getExceptionFactory() + .create("Trying to connect with ssl, but ssl not enabled in the server", "08000"); + } + + clientCapabilities |= Capabilities.SSL; + SslRequestPacket.create(clientCapabilities, exchangeCharset).encode(writer, context); + + TlsSocketPlugin socketPlugin = TlsSocketPluginLoader.get(conf.tlsSocketType()); + SSLSocketFactory sslSocketFactory = + socketPlugin.getSocketFactory(conf, context.getExceptionFactory()); + SSLSocket sslSocket = socketPlugin.createSocket(socket, sslSocketFactory); + + enabledSslProtocolSuites(sslSocket, conf); + enabledSslCipherSuites(sslSocket, conf); + + sslSocket.setUseClientMode(true); + sslSocket.startHandshake(); + + // perform hostname verification + // (rfc2818 indicate that if "client has external information as to the expected identity of + // the server, the hostname check MAY be omitted") + if (conf.sslMode() == SslMode.VERIFY_FULL) { + SSLSession session = sslSocket.getSession(); + try { + socketPlugin.verify(host, session, conf, context.getThreadId()); + } catch (SSLException ex) { + throw context + .getExceptionFactory() + .create( + "SSL hostname verification failed : " + + ex.getMessage() + + "\nThis verification can be disabled using the option \"disableSslHostnameVerification\" " + + "but won't prevent man-in-the-middle attacks anymore", + "08006"); + } + } + return sslSocket; + } + return null; + } + + /** + * Return possible protocols : values of option enabledSslProtocolSuites is set, or default to + * "TLSv1,TLSv1.1". MariaDB versions ≥ 10.0.15 and ≥ 5.5.41 supports TLSv1.2 if compiled + * with openSSL (default). MySQL community versions ≥ 5.7.10 is compile with yaSSL, so max TLS + * is TLSv1.1. + * + * @param sslSocket current sslSocket + * @throws SQLException if protocol isn't a supported protocol + */ + static void enabledSslProtocolSuites(SSLSocket sslSocket, Configuration conf) + throws SQLException { + if (conf.enabledSslProtocolSuites() != null) { + List possibleProtocols = Arrays.asList(sslSocket.getSupportedProtocols()); + String[] protocols = conf.enabledSslProtocolSuites().split("[,;\\s]+"); + for (String protocol : protocols) { + if (!possibleProtocols.contains(protocol)) { + throw new SQLException( + "Unsupported SSL protocol '" + + protocol + + "'. Supported protocols : " + + possibleProtocols.toString().replace("[", "").replace("]", "")); + } + } + sslSocket.setEnabledProtocols(protocols); + } + } + + /** + * Set ssl socket cipher according to options. + * + * @param sslSocket current ssl socket + * @param conf configuration + * @throws SQLException if a cipher isn't known + */ + static void enabledSslCipherSuites(SSLSocket sslSocket, Configuration conf) throws SQLException { + if (conf.enabledSslCipherSuites() != null) { + List possibleCiphers = Arrays.asList(sslSocket.getSupportedCipherSuites()); + String[] ciphers = conf.enabledSslCipherSuites().split("[,;\\s]+"); + for (String cipher : ciphers) { + if (!possibleCiphers.contains(cipher)) { + throw new SQLException( + "Unsupported SSL cipher '" + + cipher + + "'. Supported ciphers : " + + possibleCiphers.toString().replace("[", "").replace("]", "")); + } + } + sslSocket.setEnabledCipherSuites(ciphers); + } + } + + public static void compressionHandler(Configuration configuration, Context context) + throws SQLException { + if (configuration.useCompression() + && ((context.getServerCapabilities() & Capabilities.COMPRESS) != 0)) { + // TODO write compression handler + throw new SQLFeatureNotSupportedException("Compression not implemented yet !"); + } + } +} diff --git a/src/main/java/org/mariadb/jdbc/client/MultiPrimaryClient.java b/src/main/java/org/mariadb/jdbc/client/MultiPrimaryClient.java new file mode 100644 index 000000000..302d6ccbf --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/MultiPrimaryClient.java @@ -0,0 +1,443 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client; + +import java.sql.*; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.locks.ReentrantLock; +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.Statement; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.message.client.ChangeDbPacket; +import org.mariadb.jdbc.message.client.ClientMessage; +import org.mariadb.jdbc.message.client.QueryPacket; +import org.mariadb.jdbc.message.client.RedoableWithPrepareClientMessage; +import org.mariadb.jdbc.message.server.Completion; +import org.mariadb.jdbc.message.server.PrepareResultPacket; +import org.mariadb.jdbc.util.constants.ConnectionState; +import org.mariadb.jdbc.util.constants.HaMode; +import org.mariadb.jdbc.util.constants.ServerStatus; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; +import org.mariadb.jdbc.util.log.Logger; +import org.mariadb.jdbc.util.log.Loggers; + +public class MultiPrimaryClient implements Client { + private static final Logger logger = Loggers.getLogger(MultiPrimaryClient.class); + + protected static final ConcurrentMap blacklist = new ConcurrentHashMap<>(); + protected static final long BLACKLIST_TIMEOUT = 30_000L; + protected final Configuration conf; + protected boolean closed = false; + protected final ReentrantLock lock; + protected Client currentClient; + + public MultiPrimaryClient(Configuration conf, ReentrantLock lock) throws SQLException { + this.conf = conf; + this.lock = lock; + currentClient = connectHost(false); + } + + protected Client connectHost(boolean readOnly) throws SQLException { + HaMode haMode = conf.haMode(); + Optional host; + int maxRetries = conf.retriesAllDown(); + SQLNonTransientConnectionException lastSqle = null; + while ((host = haMode.getAvailableHost(conf.addresses(), blacklist, !readOnly)).isPresent()) { + try { + return new ClientImpl(conf, host.get(), true, lock, false); + } catch (SQLNonTransientConnectionException sqle) { + blacklist.putIfAbsent(host.get(), System.currentTimeMillis() + BLACKLIST_TIMEOUT); + maxRetries--; + } catch (SQLException sqle) { + throw sqle; + } + } + + while (maxRetries > 0) { + try { + // All server corresponding to type are blacklisted + // return the one with lower blacklist timeout + host = + blacklist.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .filter(e -> e.getKey().primary != readOnly) + .findFirst() + .map(e -> e.getKey()); + if (host.isPresent()) { + Client client = new ClientImpl(conf, host.get(), true, lock, false); + blacklist.remove(host.get()); + return client; + } + } catch (SQLNonTransientConnectionException sqle) { + lastSqle = sqle; + if (host.isPresent()) { + blacklist.putIfAbsent(host.get(), System.currentTimeMillis() + BLACKLIST_TIMEOUT); + } + maxRetries--; + try { + // wait 250ms before looping through + Thread.sleep(250); + } catch (InterruptedException interrupted) { + // interrupted, continue + } + } catch (SQLException sqle) { + throw sqle; + } + } + throw lastSqle; + } + + protected void reConnect() throws SQLException { + + blacklist.putIfAbsent( + currentClient.getHostAddress(), System.currentTimeMillis() + BLACKLIST_TIMEOUT); + logger.info("Connection error on {}", currentClient.getHostAddress()); + try { + Client oldClient = currentClient; + // remove cached prepare from existing server prepare statement + oldClient.getContext().getPrepareCache().reset(); + + currentClient = connectHost(false); + syncNewState(oldClient); + + if (!transactionReplay(oldClient)) { + // transaction cannot be replayed, but connection is now up again. + // changing exception to SQLTransientConnectionException + throw new SQLTransientConnectionException( + String.format( + "Driver has reconnect connection after a " + + "communications " + + "link " + + "failure with %s, but wasn't able to replay transaction", + oldClient.getHostAddress()), + "25S03"); + } + + } catch (SQLNonTransientConnectionException sqle) { + currentClient = null; + closed = true; + throw sqle; + } catch (SQLException sqle) { + throw sqle; + } + } + + protected boolean transactionReplay(Client oldCli) throws SQLException { + // transaction replay + if ((oldCli.getContext().getServerStatus() | ServerStatus.IN_TRANSACTION) > 0) { + if (!oldCli.getContext().getTransactionSaver().isCleanState()) return false; + currentClient.transactionReplay(oldCli.getContext().getTransactionSaver()); + } + return true; + } + + public void syncNewState(Client oldCli) throws SQLException { + Context oldCtx = oldCli.getContext(); + currentClient.getExceptionFactory().setConnection(oldCli.getExceptionFactory()); + if ((oldCtx.getStateFlag() & ConnectionState.STATE_AUTOCOMMIT) > 0) { + if ((oldCtx.getServerStatus() & ServerStatus.AUTOCOMMIT) != (currentClient.getContext().getServerStatus() & ServerStatus.AUTOCOMMIT) ) { + currentClient.getContext().addStateFlag(ConnectionState.STATE_AUTOCOMMIT); + currentClient.execute( + new QueryPacket( + "set autocommit=" + + (((oldCtx.getServerStatus() & ServerStatus.AUTOCOMMIT) > 0) ? "1" : "0"))); + } + } + + if ((oldCtx.getStateFlag() & ConnectionState.STATE_DATABASE) > 0 + && currentClient.getContext().getDatabase() != oldCtx.getDatabase()) { + currentClient.getContext().addStateFlag(ConnectionState.STATE_DATABASE); + currentClient.execute(new ChangeDbPacket(oldCtx.getDatabase())); + } + + if ((oldCtx.getStateFlag() & ConnectionState.STATE_NETWORK_TIMEOUT) > 0) { + currentClient.setSocketTimeout(oldCli.getSocketTimeout()); + } + + if ((oldCtx.getStateFlag() & ConnectionState.STATE_READ_ONLY) > 0 + && conf.assureReadOnly() + && !currentClient.getHostAddress().primary + && currentClient.getContext().getVersion().versionGreaterOrEqual(5, 6, 5)) { + currentClient.execute(new QueryPacket("SET SESSION TRANSACTION READ ONLY")); + } + + if ((oldCtx.getStateFlag() & ConnectionState.STATE_TRANSACTION_ISOLATION) > 0 + && currentClient.getContext().getTransactionIsolationLevel() != oldCtx.getTransactionIsolationLevel()) { + String query = "SET SESSION TRANSACTION ISOLATION LEVEL"; + switch (oldCtx.getTransactionIsolationLevel()) { + case java.sql.Connection.TRANSACTION_READ_UNCOMMITTED: + query += " READ UNCOMMITTED"; + break; + case java.sql.Connection.TRANSACTION_READ_COMMITTED: + query += " READ COMMITTED"; + break; + case java.sql.Connection.TRANSACTION_REPEATABLE_READ: + query += " REPEATABLE READ"; + break; + case java.sql.Connection.TRANSACTION_SERIALIZABLE: + query += " SERIALIZABLE"; + break; + default: + throw new SQLException("Unsupported transaction isolation level"); + } + currentClient + .getContext() + .setTransactionIsolationLevel(oldCtx.getTransactionIsolationLevel()); + currentClient.execute(new QueryPacket(query)); + } + } + + @Override + public List execute(ClientMessage message) throws SQLException { + return execute( + message, null, 0, 0L, ResultSet.CONCUR_READ_ONLY, ResultSet.TYPE_FORWARD_ONLY, false); + } + + @Override + public List execute(ClientMessage message, org.mariadb.jdbc.Statement stmt) + throws SQLException { + return execute( + message, stmt, 0, 0L, ResultSet.CONCUR_READ_ONLY, ResultSet.TYPE_FORWARD_ONLY, false); + } + + @Override + public List execute( + ClientMessage message, + Statement stmt, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException { + + if (closed) { + throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220); + } + + try { + return currentClient.execute( + message, + stmt, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion); + } catch (SQLNonTransientConnectionException e) { + reConnect(); + if (message instanceof RedoableWithPrepareClientMessage) { + ((RedoableWithPrepareClientMessage) message).rePrepare(currentClient); + } + return currentClient.execute( + message, + stmt, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion); + } + } + + @Override + public List executePipeline( + ClientMessage[] messages, + Statement stmt, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException { + if (closed) { + throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220); + } + + try { + return currentClient.executePipeline( + messages, + stmt, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion); + } catch (SQLException e) { + if (e instanceof SQLNonTransientConnectionException + || (e.getCause() != null && e.getCause() instanceof SQLNonTransientConnectionException)) { + reConnect(); + Arrays.stream(messages) + .filter(RedoableWithPrepareClientMessage.class::isInstance) + .map(RedoableWithPrepareClientMessage.class::cast) + .forEach( + rd -> { + try { + rd.rePrepare(currentClient); + } catch (SQLException sqle) { + // eat + } + }); + return currentClient.executePipeline( + messages, + stmt, + fetchSize, + maxRows, + resultSetConcurrency, + resultSetType, + closeOnCompletion); + } + throw e; + } + } + + @Override + public void readStreamingResults( + List completions, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException { + if (closed) { + throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220); + } + + try { + currentClient.readStreamingResults( + completions, fetchSize, maxRows, resultSetConcurrency, resultSetType, closeOnCompletion); + } catch (SQLNonTransientConnectionException e) { + reConnect(); + currentClient.readStreamingResults( + completions, fetchSize, maxRows, resultSetConcurrency, resultSetType, closeOnCompletion); + } + } + + @Override + public void closePrepare(PrepareResultPacket prepare) throws SQLException { + if (closed) { + throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220); + } + + try { + currentClient.closePrepare(prepare); + } catch (SQLNonTransientConnectionException e) { + reConnect(); + } + } + + @Override + public void transactionReplay(TransactionSaver transactionSaver) throws SQLException {} + + @Override + public void abort(Executor executor) throws SQLException { + if (closed) { + throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220); + } + + try { + currentClient.abort(executor); + } catch (SQLNonTransientConnectionException e) { + reConnect(); + currentClient.abort(executor); + } + } + + @Override + public void close() throws SQLException { + if (closed) { + throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220); + } + closed = true; + currentClient.close(); + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + if (closed) { + throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220); + } + + try { + currentClient.setReadOnly(readOnly); + } catch (SQLNonTransientConnectionException e) { + reConnect(); + currentClient.setReadOnly(readOnly); + } + } + + @Override + public int getSocketTimeout() { + return currentClient.getSocketTimeout(); + } + + @Override + public void setSocketTimeout(int milliseconds) throws SQLException { + if (closed) { + throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220); + } + + try { + currentClient.setSocketTimeout(milliseconds); + } catch (SQLNonTransientConnectionException e) { + reConnect(); + currentClient.setSocketTimeout(milliseconds); + } + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public Context getContext() { + return currentClient.getContext(); + } + + @Override + public ExceptionFactory getExceptionFactory() { + return currentClient.getExceptionFactory(); + } + + @Override + public void reset(ExceptionFactory exceptionFactory) { + currentClient.reset(exceptionFactory); + } + + @Override + public HostAddress getHostAddress() { + return currentClient.getHostAddress(); + } + + public boolean isPrimary() { + return getHostAddress().primary; + } +} diff --git a/src/main/java/org/mariadb/jdbc/client/MultiPrimaryReplicaClient.java b/src/main/java/org/mariadb/jdbc/client/MultiPrimaryReplicaClient.java new file mode 100644 index 000000000..38bcf3da3 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/MultiPrimaryReplicaClient.java @@ -0,0 +1,335 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client; + +import java.sql.SQLException; +import java.sql.SQLNonTransientConnectionException; +import java.sql.SQLTransientConnectionException; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.locks.ReentrantLock; +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.Statement; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.message.client.ClientMessage; +import org.mariadb.jdbc.message.server.Completion; +import org.mariadb.jdbc.message.server.PrepareResultPacket; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; +import org.mariadb.jdbc.util.log.Logger; +import org.mariadb.jdbc.util.log.Loggers; + +public class MultiPrimaryReplicaClient extends MultiPrimaryClient { + private static final Logger logger = Loggers.getLogger(MultiPrimaryReplicaClient.class); + protected static final long NEXT_TRY_TIMEOUT = 30_000L; + private Client replicaClient; + private Client primaryClient; + private boolean requestReadOnly; + private long nextTryReplica = -1; + private long nextTryPrimary = -1; + + public MultiPrimaryReplicaClient(Configuration conf, ReentrantLock lock) throws SQLException { + super(conf, lock); + primaryClient = currentClient; + + try { + replicaClient = connectHost(true); + } catch (SQLException e) { + replicaClient = null; + nextTryReplica = System.currentTimeMillis() + NEXT_TRY_TIMEOUT; + } + } + + private void reconnectIfNeeded() { + if (!closed) { + + // try reconnect primary + if (primaryClient == null && nextTryPrimary < System.currentTimeMillis()) { + try { + primaryClient = connectHost(false); + nextTryPrimary = -1; + } catch (SQLException e) { + nextTryPrimary = System.currentTimeMillis() + NEXT_TRY_TIMEOUT; + } + } + + // try reconnect replica + if (replicaClient == null && nextTryReplica < System.currentTimeMillis()) { + try { + replicaClient = connectHost(true); + nextTryReplica = -1; + if (requestReadOnly) { + syncNewState(primaryClient); + currentClient = replicaClient; + } + } catch (SQLException e) { + nextTryReplica = System.currentTimeMillis() + NEXT_TRY_TIMEOUT; + } + } + } + } + + @Override + protected void reConnect() throws SQLException { + blacklist.putIfAbsent( + currentClient.getHostAddress(), System.currentTimeMillis() + BLACKLIST_TIMEOUT); + logger.info("Connection error on {}", currentClient.getHostAddress()); + try { + Client oldClient = currentClient; + if (oldClient.isPrimary()) { + primaryClient = null; + } else { + replicaClient = null; + } + + // remove cached prepare from existing server prepare statement + oldClient.getContext().getPrepareCache().reset(); + + try { + currentClient = connectHost(requestReadOnly); + if (requestReadOnly) { + nextTryReplica = -1; + replicaClient = currentClient; + } else { + nextTryPrimary = -1; + primaryClient = currentClient; + } + + } catch (SQLNonTransientConnectionException e) { + if (requestReadOnly) { + nextTryReplica = System.currentTimeMillis() + NEXT_TRY_TIMEOUT; + if (primaryClient != null) { + // connector will use primary client until some replica is up + currentClient = primaryClient; + } else { + // replication fails, and no primary connection + // trying to create new primary connection + try { + primaryClient = connectHost(false); + currentClient = primaryClient; + nextTryPrimary = -1; + } catch (SQLNonTransientConnectionException ee) { + closed = true; + throw new SQLNonTransientConnectionException( + String.format( + "Driver has failed to reconnect connection after a " + + "communications " + + "failure with %s", + oldClient.getHostAddress()), + "08000"); + } + } + } else { + throw new SQLNonTransientConnectionException( + String.format( + "Driver has failed to reconnect master connection after a " + + "communications " + + "failure with %s", + oldClient.getHostAddress()), + "08000"); + } + } + + syncNewState(oldClient); + + if (!requestReadOnly && !transactionReplay(oldClient)) { + // transaction cannot be replayed, but connection is now up again. + // changing exception to SQLTransientConnectionException + throw new SQLTransientConnectionException( + String.format( + "Driver has reconnect connection after a " + + "communications " + + "link " + + "failure with %s, but wasn't able to replay transaction", + oldClient.getHostAddress()), + "25S03"); + } + + } catch (SQLNonTransientConnectionException sqle) { + currentClient = null; + closed = true; + if (replicaClient != null) { + replicaClient.close(); + } + throw sqle; + } catch (SQLException sqle) { + throw sqle; + } + } + + @Override + public List execute( + ClientMessage message, + Statement stmt, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException { + reconnectIfNeeded(); + return super.execute( + message, stmt, fetchSize, maxRows, resultSetConcurrency, resultSetType, closeOnCompletion); + } + + @Override + public List executePipeline( + ClientMessage[] messages, + Statement stmt, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException { + reconnectIfNeeded(); + return super.executePipeline( + messages, stmt, fetchSize, maxRows, resultSetConcurrency, resultSetType, closeOnCompletion); + } + + @Override + public void readStreamingResults( + List completions, + int fetchSize, + long maxRows, + int resultSetConcurrency, + int resultSetType, + boolean closeOnCompletion) + throws SQLException { + reconnectIfNeeded(); + super.readStreamingResults( + completions, fetchSize, maxRows, resultSetConcurrency, resultSetType, closeOnCompletion); + } + + @Override + public void closePrepare(PrepareResultPacket prepare) throws SQLException { + reconnectIfNeeded(); + super.closePrepare(prepare); + } + + @Override + public void abort(Executor executor) throws SQLException { + reconnectIfNeeded(); + super.abort(executor); + } + + @Override + public void close() throws SQLException { + if (closed) { + throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220); + } + closed = true; + try { + primaryClient.close(); + } catch (SQLException e) { + // eat + } + try { + replicaClient.close(); + } catch (SQLException e) { + // eat + } + primaryClient = null; + replicaClient = null; + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + if (closed) { + throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220); + } + if (readOnly) { + // changed ? + if (!requestReadOnly) { + if (replicaClient != null) { + currentClient = replicaClient; + } else if (nextTryReplica < System.currentTimeMillis()) { + try { + replicaClient = connectHost(true); + currentClient = replicaClient; + syncNewState(primaryClient); + } catch (SQLException e) { + nextTryReplica = System.currentTimeMillis() + NEXT_TRY_TIMEOUT; + } + } + } + } else { + // changed ? + if (requestReadOnly) { + if (primaryClient != null) { + currentClient = primaryClient; + syncNewState(replicaClient); + } else if (primaryClient == null && nextTryPrimary < System.currentTimeMillis()) { + try { + primaryClient = connectHost(false); + nextTryPrimary = -1; + } catch (SQLException e) { + nextTryPrimary = System.currentTimeMillis() + NEXT_TRY_TIMEOUT; + throw new SQLNonTransientConnectionException( + "Driver has failed to reconnect a primary connection", "08000"); + } + } + } + } + requestReadOnly = readOnly; + } + + @Override + public int getSocketTimeout() { + reconnectIfNeeded(); + return super.getSocketTimeout(); + } + + @Override + public void setSocketTimeout(int milliseconds) throws SQLException { + reconnectIfNeeded(); + super.setSocketTimeout(milliseconds); + } + + @Override + public Context getContext() { + reconnectIfNeeded(); + return super.getContext(); + } + + @Override + public ExceptionFactory getExceptionFactory() { + reconnectIfNeeded(); + return super.getExceptionFactory(); + } + + @Override + public void reset(ExceptionFactory exceptionFactory) { + reconnectIfNeeded(); + super.reset(exceptionFactory); + } + + @Override + public HostAddress getHostAddress() { + reconnectIfNeeded(); + return super.getHostAddress(); + } + + public boolean isPrimary() { + return getHostAddress().primary; + } +} diff --git a/src/main/java/org/mariadb/jdbc/client/PacketReader.java b/src/main/java/org/mariadb/jdbc/client/PacketReader.java new file mode 100644 index 000000000..a47195c34 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/PacketReader.java @@ -0,0 +1,270 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client; + +import java.io.BufferedInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.util.MutableInt; +import org.mariadb.jdbc.util.log.Logger; +import org.mariadb.jdbc.util.log.LoggerHelper; +import org.mariadb.jdbc.util.log.Loggers; + +public class PacketReader { + + private static final int REUSABLE_buf_LENGTH = 1024; + private static final int MAX_PACKET_SIZE = 0xffffff; + private static final Logger logger = Loggers.getLogger(PacketReader.class); + + private final byte[] header = new byte[4]; + private final byte[] reusableArray = new byte[REUSABLE_buf_LENGTH]; + private final InputStream inputStream; + private final int maxQuerySizeToLog; + + private final MutableInt sequence; + private int lastPacketLength; + private String serverThreadLog = ""; + + /** + * Constructor of standard socket MySQL packet stream reader. + * + * @param in stream + * @param conf connection options + */ + public PacketReader(InputStream in, Configuration conf, MutableInt sequence) { + this.inputStream = + conf.useReadAheadInput() + ? new ReadAheadBufferedStream(in) + : new BufferedInputStream(in, 16384); + this.maxQuerySizeToLog = conf.maxQuerySizeToLog(); + this.sequence = sequence; + } + + /** + * Constructor for single Data (using text format). + * + * @param value value + * @return buf + */ + public static byte[] create(byte[] value) { + if (value == null) { + return new byte[] {(byte) 251}; + } + + int length = value.length; + if (length < 251) { + + byte[] buf = new byte[length + 1]; + buf[0] = (byte) length; + System.arraycopy(value, 0, buf, 1, length); + return buf; + + } else if (length < 65536) { + + byte[] buf = new byte[length + 3]; + buf[0] = (byte) 0xfc; + buf[1] = (byte) length; + buf[2] = (byte) (length >>> 8); + System.arraycopy(value, 0, buf, 3, length); + return buf; + + } else if (length < 16777216) { + + byte[] buf = new byte[length + 4]; + buf[0] = (byte) 0xfd; + buf[1] = (byte) length; + buf[2] = (byte) (length >>> 8); + buf[3] = (byte) (length >>> 16); + System.arraycopy(value, 0, buf, 4, length); + return buf; + + } else { + + byte[] buf = new byte[length + 9]; + buf[0] = (byte) 0xfe; + buf[1] = (byte) length; + buf[2] = (byte) (length >>> 8); + buf[3] = (byte) (length >>> 16); + buf[4] = (byte) (length >>> 24); + // byte[] cannot have a more than 4 byte length size, so buf[5] -> buf[8] = 0x00; + System.arraycopy(value, 0, buf, 9, length); + return buf; + } + } + + /** + * Get current input stream for creating compress input stream, to avoid losing already read bytes + * in case of pipelining. + * + * @return input stream. + */ + public InputStream getInputStream() { + return inputStream; + } + + /** + * Get next packet. If packet is more than 16M, read as many packet needed to finish packet. + * (first that has not length = 16Mb) + * + * @param reUsable if can use existing reusable buf to avoid creating array + * @return array packet. + * @throws IOException if socket exception occur. + */ + public ReadableByteBuf readPacket(boolean reUsable) throws IOException { + + // *************************************************** + // Read 4 byte header + // *************************************************** + int remaining = 4; + int off = 0; + do { + int count = inputStream.read(header, off, remaining); + if (count < 0) { + throw new EOFException( + "unexpected end of stream, read " + + off + + " bytes from 4 (socket was closed by server)"); + } + remaining -= count; + off += count; + } while (remaining > 0); + + lastPacketLength = (header[0] & 0xff) + ((header[1] & 0xff) << 8) + ((header[2] & 0xff) << 16); + sequence.set(header[3]); + + // prepare array + byte[] rawBytes; + if (reUsable && lastPacketLength < REUSABLE_buf_LENGTH) { + rawBytes = reusableArray; + } else { + rawBytes = new byte[lastPacketLength]; + } + + // *************************************************** + // Read content + // *************************************************** + remaining = lastPacketLength; + off = 0; + do { + int count = inputStream.read(rawBytes, off, remaining); + if (count < 0) { + throw new EOFException( + "unexpected end of stream, read " + + (lastPacketLength - remaining) + + " bytes from " + + lastPacketLength + + " (socket was closed by server)"); + } + remaining -= count; + off += count; + } while (remaining > 0); + + if (logger.isTraceEnabled()) { + logger.trace( + "read: {}\n{}", + serverThreadLog, + LoggerHelper.hex(header, rawBytes, 0, lastPacketLength, maxQuerySizeToLog)); + } + + // *************************************************** + // In case content length is big, content will be separate in many 16Mb packets + // *************************************************** + if (lastPacketLength == MAX_PACKET_SIZE) { + int packetLength; + do { + remaining = 4; + off = 0; + do { + int count = inputStream.read(header, off, remaining); + if (count < 0) { + throw new EOFException("unexpected end of stream, read " + off + " bytes from 4"); + } + remaining -= count; + off += count; + } while (remaining > 0); + + packetLength = (header[0] & 0xff) + ((header[1] & 0xff) << 8) + ((header[2] & 0xff) << 16); + sequence.set(header[3]); + + int currentbufLength = rawBytes.length; + byte[] newRawBytes = new byte[currentbufLength + packetLength]; + System.arraycopy(rawBytes, 0, newRawBytes, 0, currentbufLength); + rawBytes = newRawBytes; + + // *************************************************** + // Read content + // *************************************************** + remaining = packetLength; + off = currentbufLength; + do { + int count = inputStream.read(rawBytes, off, remaining); + if (count < 0) { + throw new EOFException( + "unexpected end of stream, read " + + (packetLength - remaining) + + " bytes from " + + packetLength); + } + remaining -= count; + off += count; + } while (remaining > 0); + + if (logger.isTraceEnabled()) { + logger.trace( + "read: {}{}", + serverThreadLog, + LoggerHelper.hex( + header, rawBytes, currentbufLength, packetLength, maxQuerySizeToLog)); + } + + lastPacketLength += packetLength; + } while (packetLength == MAX_PACKET_SIZE); + + return new ReadableByteBuf(sequence, rawBytes, rawBytes.length); + } + + return new ReadableByteBuf(sequence, rawBytes, lastPacketLength); + } + + public MutableInt getSequence() { + return sequence; + } + + public void close() throws IOException { + inputStream.close(); + } + + /** + * Set server thread id. + * + * @param serverThreadId current server thread id. + * @param hostAddress host information + */ + public void setServerThreadId(long serverThreadId, HostAddress hostAddress) { + Boolean isMaster = hostAddress != null ? hostAddress.primary : null; + this.serverThreadLog = + "conn=" + serverThreadId + ((isMaster != null) ? "(" + (isMaster ? "M" : "S") + ")" : ""); + } +} diff --git a/src/main/java/org/mariadb/jdbc/internal/io/output/AbstractPacketOutputStream.java b/src/main/java/org/mariadb/jdbc/client/PacketWriter.java similarity index 57% rename from src/main/java/org/mariadb/jdbc/internal/io/output/AbstractPacketOutputStream.java rename to src/main/java/org/mariadb/jdbc/client/PacketWriter.java index 0320147d1..afcbfd39d 100644 --- a/src/main/java/org/mariadb/jdbc/internal/io/output/AbstractPacketOutputStream.java +++ b/src/main/java/org/mariadb/jdbc/client/PacketWriter.java @@ -1,5 +1,4 @@ /* - * * MariaDB Client for Java * * Copyright (c) 2012-2014 Monty Program Ab. @@ -18,221 +17,88 @@ * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to Monty Program Ab info@montyprogram.com. * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * */ -package org.mariadb.jdbc.internal.io.output; +package org.mariadb.jdbc.client; -import java.io.*; +import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; -import org.mariadb.jdbc.internal.io.LruTraceCache; -import org.mariadb.jdbc.internal.util.exceptions.MaxAllowedPacketException; - -public abstract class AbstractPacketOutputStream extends FilterOutputStream - implements PacketOutputStream { - +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.util.MutableInt; +import org.mariadb.jdbc.util.exceptions.MaxAllowedPacketException; +import org.mariadb.jdbc.util.log.Logger; +import org.mariadb.jdbc.util.log.LoggerHelper; +import org.mariadb.jdbc.util.log.Loggers; + +public class PacketWriter { + + public static final int SMALL_BUFFER_SIZE = 8192; + private static final Logger logger = Loggers.getLogger(PacketWriter.class); private static final byte QUOTE = (byte) '\''; private static final byte DBL_QUOTE = (byte) '"'; private static final byte ZERO_BYTE = (byte) '\0'; private static final byte BACKSLASH = (byte) '\\'; - - private static final int SMALL_BUFFER_SIZE = 8192; private static final int MEDIUM_BUFFER_SIZE = 128 * 1024; private static final int LARGE_BUFFER_SIZE = 1024 * 1024; - protected final int maxQuerySizeToLog; + private static final int MAX_PACKET_LENGTH = 0x00ffffff + 4; + protected final MutableInt sequence; + private final int maxQuerySizeToLog; + private final OutputStream out; protected byte[] buf; - protected int pos; - protected int maxAllowedPacket = Integer.MAX_VALUE; - protected long cmdLength; - protected boolean permitTrace; - protected int seqNo = 0; - protected String serverThreadLog = ""; - protected LruTraceCache traceCache = null; + protected int pos = 4; + private int maxPacketLength = MAX_PACKET_LENGTH; + private int maxAllowedPacket = Integer.MAX_VALUE; + private long cmdLength; + private boolean permitTrace = true; + private String serverThreadLog = ""; private int mark = -1; - private boolean bufferContainDataAfterMark = false; - protected long threadId; + private boolean bufContainDataAfterMark = false; /** * Common feature to write data into socket, creating MariaDB Packet. * * @param out socket outputStream * @param maxQuerySizeToLog maximum query size to log - * @param threadId thread id */ - public AbstractPacketOutputStream(OutputStream out, int maxQuerySizeToLog, long threadId) { - super(out); + public PacketWriter(OutputStream out, int maxQuerySizeToLog, MutableInt sequence) { + this.out = out; buf = new byte[SMALL_BUFFER_SIZE]; this.maxQuerySizeToLog = maxQuerySizeToLog; cmdLength = 0; - this.threadId = threadId; + this.sequence = sequence; } - public abstract int getMaxPacketLength(); - - public abstract void startPacket(int seqNo); - - protected abstract void flushBuffer(boolean commandEnd) throws IOException; - - /** - * Buffer growing use 4 size only to avoid creating/copying that are expensive operations. - * possible size - * - *

    - *
  1. SMALL_BUFFER_SIZE = 8k (default) - *
  2. MEDIUM_BUFFER_SIZE = 128k - *
  3. LARGE_BUFFER_SIZE = 1M - *
  4. getMaxPacketLength = 16M (+ 4 is using no compression) - *
- * - * @param len length to add - */ - private void growBuffer(int len) throws IOException { - int bufferLength = buf.length; - int newCapacity; - if (bufferLength == SMALL_BUFFER_SIZE) { - if (len + pos < MEDIUM_BUFFER_SIZE) { - newCapacity = MEDIUM_BUFFER_SIZE; - } else if (len + pos < LARGE_BUFFER_SIZE) { - newCapacity = LARGE_BUFFER_SIZE; - } else { - newCapacity = getMaxPacketLength(); - } - } else if (bufferLength == MEDIUM_BUFFER_SIZE) { - if (len + pos < LARGE_BUFFER_SIZE) { - newCapacity = LARGE_BUFFER_SIZE; - } else { - newCapacity = getMaxPacketLength(); - } - } else if (bufferContainDataAfterMark) { - // want to add some information to buffer without having the command Header - // must grow buffer until having all the query - newCapacity = Math.max(len + pos, getMaxPacketLength()); - } else { - newCapacity = getMaxPacketLength(); - } - - if (mark != -1 && len + pos > newCapacity) { - // buffer is > 16M with mark. - // flush until mark, reset pos at beginning - flushBufferStopAtMark(); - - if (len + pos <= bufferLength) { - return; - } - - // need to keep all data, buffer can grow more than maxPacketLength - // grow buffer if needed - if (len + pos > newCapacity) { - newCapacity = len + pos; - } - } - - byte[] newBuf = new byte[newCapacity]; - System.arraycopy(buf, 0, newBuf, 0, pos); - buf = newBuf; + public int pos() { + return pos; } - /** - * Send empty packet. - * - * @param seqNo packet sequence - * @throws IOException if socket error occur. - */ - public void writeEmptyPacket(int seqNo) throws IOException { - startPacket(seqNo); - writeEmptyPacket(); - out.flush(); - cmdLength = 0; + public int pos(int pos) { + return this.pos = pos; } - public abstract void writeEmptyPacket() throws IOException; - /** - * Send packet to socket. + * Write byte into buf, flush buf to socket if needed. * + * @param value byte to send * @throws IOException if socket error occur. */ - public void flush() throws IOException { - flushBuffer(true); - out.flush(); - - // if buffer is big, and last query doesn't use at least half of it, resize buffer to default - // value - if (buf.length > SMALL_BUFFER_SIZE && cmdLength * 2 < buf.length) { - buf = new byte[SMALL_BUFFER_SIZE]; - } - - if (cmdLength >= maxAllowedPacket) { - throw new MaxAllowedPacketException( - "query size (" + cmdLength + ") is >= to max_allowed_packet (" + maxAllowedPacket + ")", - true); - } - } - - public boolean checkRemainingSize(int len) { - return getMaxPacketLength() - pos > len; - } - - /** - * Count query size. If query size is greater than max_allowed_packet and nothing has been already - * send, throw an exception to avoid having the connection closed. - * - * @param length additional length to query size - * @throws MaxAllowedPacketException if query has not to be send. - */ - public void checkMaxAllowedLength(int length) throws MaxAllowedPacketException { - if (cmdLength + length >= maxAllowedPacket && cmdLength == 0) { - // launch exception only if no packet has been send. - throw new MaxAllowedPacketException( - "query size (" - + (cmdLength + length) - + ") is >= to max_allowed_packet (" - + maxAllowedPacket - + ")", - false); + public void writeByte(int value) throws IOException { + if (pos >= buf.length) { + if (pos >= maxPacketLength && !bufContainDataAfterMark) { + // buf is more than a Packet, must flushbuf() + writeSocket(false); + } else { + growBuffer(1); + } } - } - - public boolean exceedMaxLength() { - return cmdLength + (pos - initialPacketPos()) >= maxAllowedPacket; - } - - public OutputStream getOutputStream() { - return out; + buf[pos++] = (byte) value; } /** - * Write short value into buffer. flush buffer if too small. + * Write short value into buf. flush buf if too small. * * @param value short value * @throws IOException if socket error occur @@ -243,7 +109,7 @@ public void writeShort(short value) throws IOException { byte[] arr = new byte[2]; arr[0] = (byte) value; arr[1] = (byte) (value >> 8); - write(arr, 0, 2); + writeBytes(arr, 0, 2); return; } @@ -253,7 +119,7 @@ public void writeShort(short value) throws IOException { } /** - * Write int value into buffer. flush buffer if too small. + * Write int value into buf. flush buf if too small. * * @param value int value * @throws IOException if socket error occur @@ -266,7 +132,7 @@ public void writeInt(int value) throws IOException { arr[1] = (byte) (value >> 8); arr[2] = (byte) (value >> 16); arr[3] = (byte) (value >> 24); - write(arr, 0, 4); + writeBytes(arr, 0, 4); return; } @@ -278,7 +144,7 @@ public void writeInt(int value) throws IOException { } /** - * Write long value into buffer. flush buffer if too small. + * Write long value into buf. flush buf if too small. * * @param value long value * @throws IOException if socket error occur @@ -295,7 +161,7 @@ public void writeLong(long value) throws IOException { arr[5] = (byte) (value >> 40); arr[6] = (byte) (value >> 48); arr[7] = (byte) (value >> 56); - write(arr, 0, 8); + writeBytes(arr, 0, 8); return; } @@ -310,38 +176,97 @@ public void writeLong(long value) throws IOException { pos += 8; } + public void writeDouble(double value) throws IOException { + writeLong(Double.doubleToLongBits(value)); + } + + public void writeFloat(float value) throws IOException { + writeInt(Float.floatToIntBits(value)); + } + /** - * Write byte value, len times into buffer. flush buffer if too small. + * Write byte value, len times into buf. flush buf if too small. * * @param value byte value * @param len number of time to write value. * @throws IOException if socket error occur. */ - public void writeBytes(byte value, int len) throws IOException { + public void writeBytes(int value, int len) throws IOException { if (len > buf.length - pos) { // not enough space remaining byte[] arr = new byte[len]; - Arrays.fill(arr, value); - write(arr, 0, len); + Arrays.fill(arr, (byte) value); + writeBytes(arr, 0, len); return; } for (int i = pos; i < pos + len; i++) { - buf[i] = value; + buf[i] = (byte) value; } pos += len; } + public void writeBytes(byte[] arr) throws IOException { + writeBytes(arr, 0, arr.length); + } + /** - * Write field length into buffer, flush socket if needed. + * Write byte array to buf. If buf is full, flush socket. + * + * @param arr byte array + * @param off offset + * @param len byte length to write + * @throws IOException if socket error occur + */ + public void writeBytes(byte[] arr, int off, int len) throws IOException { + if (len > buf.length - pos) { + if (buf.length != maxPacketLength) { + growBuffer(len); + } + + // max buf size + if (len > buf.length - pos) { + + if (mark != -1) { + growBuffer(len); + if (mark != -1) { + flushBufferStopAtMark(); + } + + } else { + // not enough space in buf, will stream : + // fill buf and flush until all data are snd + int remainingLen = len; + do { + int lenToFillbuf = Math.min(maxPacketLength - pos, remainingLen); + System.arraycopy(arr, off, buf, pos, lenToFillbuf); + remainingLen -= lenToFillbuf; + off += lenToFillbuf; + pos += lenToFillbuf; + if (remainingLen > 0) { + writeSocket(false); + } else { + break; + } + } while (true); + return; + } + } + } + + System.arraycopy(arr, off, buf, pos, len); + pos += len; + } + + /** + * Write field length into buf, flush socket if needed. * * @param length field length * @throws IOException if socket error occur. */ - public void writeFieldLength(long length) throws IOException { + public void writeLength(long length) throws IOException { if (length < 251) { - - write((byte) length); + writeByte((byte) length); return; } @@ -353,7 +278,7 @@ public void writeFieldLength(long length) throws IOException { arr[0] = (byte) 0xfc; arr[1] = (byte) length; arr[2] = (byte) (length >>> 8); - write(arr, 0, 3); + writeBytes(arr, 0, 3); return; } @@ -373,7 +298,7 @@ public void writeFieldLength(long length) throws IOException { arr[1] = (byte) length; arr[2] = (byte) (length >>> 8); arr[3] = (byte) (length >>> 16); - write(arr, 0, 4); + writeBytes(arr, 0, 4); return; } @@ -397,7 +322,7 @@ public void writeFieldLength(long length) throws IOException { arr[6] = (byte) (length >>> 40); arr[7] = (byte) (length >>> 48); arr[8] = (byte) (length >>> 56); - write(arr, 0, 9); + writeBytes(arr, 0, 9); return; } @@ -413,102 +338,95 @@ public void writeFieldLength(long length) throws IOException { pos += 9; } - /** - * Write byte into buffer, flush buffer to socket if needed. - * - * @param value byte to send - * @throws IOException if socket error occur. - */ - public void write(int value) throws IOException { - if (pos >= buf.length) { - if (pos >= getMaxPacketLength() && !bufferContainDataAfterMark) { - // buffer is more than a Packet, must flushBuffer() - flushBuffer(false); - } else { - growBuffer(1); - } - } - buf[pos++] = (byte) value; + public void writeAscii(String str) throws IOException { + byte[] arr = str.getBytes(StandardCharsets.US_ASCII); + writeBytes(arr, 0, arr.length); } - public void write(byte[] arr) throws IOException { - write(arr, 0, arr.length); - } + public void writeString(String str) throws IOException { + int charsLength = str.length(); - /** - * Write byte array to buffer. If buffer is full, flush socket. - * - * @param arr byte array - * @param off offset - * @param len byte length to write - * @throws IOException if socket error occur - */ - public void write(byte[] arr, int off, int len) throws IOException { - if (len > buf.length - pos) { - if (buf.length != getMaxPacketLength()) { - growBuffer(len); - } + // not enough space remaining + if (charsLength * 3 >= buf.length - pos) { + byte[] arr = str.getBytes(StandardCharsets.UTF_8); + writeBytes(arr, 0, arr.length); + return; + } - // max buffer size - if (len > buf.length - pos) { + // create UTF-8 byte array + // since java char are internally using UTF-16 using surrogate's pattern, 4 bytes unicode + // characters will + // represent 2 characters : example "\uD83C\uDFA4" = 🎤 unicode 8 "no microphones" + // so max size is 3 * charLength + // (escape characters are 1 byte encoded, so length might only be 2 when escape) + // + 2 for the quotes for text protocol + int charsOffset = 0; + char currChar; - if (mark != -1) { - growBuffer(len); - if (mark != -1) { - flushBufferStopAtMark(); - } + // quick loop if only ASCII chars for faster escape + for (; + charsOffset < charsLength && (currChar = str.charAt(charsOffset)) < 0x80; + charsOffset++) { + buf[pos++] = (byte) currChar; + } - } else { - // not enough space in buffer, will stream : - // fill buffer and flush until all data are snd - int remainingLen = len; - do { - int lenToFillBuffer = Math.min(getMaxPacketLength() - pos, remainingLen); - System.arraycopy(arr, off, buf, pos, lenToFillBuffer); - remainingLen -= lenToFillBuffer; - off += lenToFillBuffer; - pos += lenToFillBuffer; - if (remainingLen > 0) { - flushBuffer(false); + // if quick loop not finished + while (charsOffset < charsLength) { + currChar = str.charAt(charsOffset++); + if (currChar < 0x80) { + buf[pos++] = (byte) currChar; + } else if (currChar < 0x800) { + buf[pos++] = (byte) (0xc0 | (currChar >> 6)); + buf[pos++] = (byte) (0x80 | (currChar & 0x3f)); + } else if (currChar >= 0xD800 && currChar < 0xE000) { + // reserved for surrogate - see https://en.wikipedia.org/wiki/UTF-16 + if (currChar < 0xDC00) { + // is high surrogate + if (charsOffset + 1 > charsLength) { + buf[pos++] = (byte) 0x63; + } else { + char nextChar = str.charAt(charsOffset); + if (nextChar >= 0xDC00 && nextChar < 0xE000) { + // is low surrogate + int surrogatePairs = + ((currChar << 10) + nextChar) + (0x010000 - (0xD800 << 10) - 0xDC00); + buf[pos++] = (byte) (0xf0 | ((surrogatePairs >> 18))); + buf[pos++] = (byte) (0x80 | ((surrogatePairs >> 12) & 0x3f)); + buf[pos++] = (byte) (0x80 | ((surrogatePairs >> 6) & 0x3f)); + buf[pos++] = (byte) (0x80 | (surrogatePairs & 0x3f)); + charsOffset++; } else { - break; + // must have low surrogate + buf[pos++] = (byte) 0x3f; } - } while (true); - return; + } + } else { + // low surrogate without high surrogate before + buf[pos++] = (byte) 0x3f; } + } else { + buf[pos++] = (byte) (0xe0 | ((currChar >> 12))); + buf[pos++] = (byte) (0x80 | ((currChar >> 6) & 0x3f)); + buf[pos++] = (byte) (0x80 | (currChar & 0x3f)); } } - - System.arraycopy(arr, off, buf, pos, len); - pos += len; - } - - public void write(String str) throws IOException { - write(str, false, false); } /** * Write string to socket. * * @param str string - * @param escape must be escape * @param noBackslashEscapes escape method * @throws IOException if socket error occur */ - public void write(String str, boolean escape, boolean noBackslashEscapes) throws IOException { + public void writeStringEscaped(String str, boolean noBackslashEscapes) throws IOException { int charsLength = str.length(); // not enough space remaining - if (charsLength * 3 + 2 >= buf.length - pos) { + if (charsLength * 3 >= buf.length - pos) { byte[] arr = str.getBytes(StandardCharsets.UTF_8); - if (escape) { - write(QUOTE); - writeBytesEscaped(arr, arr.length, noBackslashEscapes); - write(QUOTE); - } else { - write(arr, 0, arr.length); - } + writeBytesEscaped(arr, arr.length, noBackslashEscapes); return; } @@ -523,34 +441,22 @@ public void write(String str, boolean escape, boolean noBackslashEscapes) throws char currChar; // quick loop if only ASCII chars for faster escape - if (escape) { - buf[pos++] = QUOTE; - if (noBackslashEscapes) { - for (; - charsOffset < charsLength && (currChar = str.charAt(charsOffset)) < 0x80; - charsOffset++) { - if (currChar == QUOTE) { - buf[pos++] = QUOTE; - } - buf[pos++] = (byte) currChar; - } - } else { - for (; - charsOffset < charsLength && (currChar = str.charAt(charsOffset)) < 0x80; - charsOffset++) { - if (currChar == BACKSLASH - || currChar == QUOTE - || currChar == 0 - || currChar == DBL_QUOTE) { - buf[pos++] = BACKSLASH; - } - buf[pos++] = (byte) currChar; + if (noBackslashEscapes) { + for (; + charsOffset < charsLength && (currChar = str.charAt(charsOffset)) < 0x80; + charsOffset++) { + if (currChar == QUOTE) { + buf[pos++] = QUOTE; } + buf[pos++] = (byte) currChar; } } else { for (; charsOffset < charsLength && (currChar = str.charAt(charsOffset)) < 0x80; charsOffset++) { + if (currChar == BACKSLASH || currChar == QUOTE || currChar == 0 || currChar == DBL_QUOTE) { + buf[pos++] = BACKSLASH; + } buf[pos++] = (byte) currChar; } } @@ -559,17 +465,15 @@ public void write(String str, boolean escape, boolean noBackslashEscapes) throws while (charsOffset < charsLength) { currChar = str.charAt(charsOffset++); if (currChar < 0x80) { - if (escape) { - if (noBackslashEscapes) { - if (currChar == QUOTE) { - buf[pos++] = QUOTE; - } - } else if (currChar == BACKSLASH - || currChar == QUOTE - || currChar == ZERO_BYTE - || currChar == DBL_QUOTE) { - buf[pos++] = BACKSLASH; + if (noBackslashEscapes) { + if (currChar == QUOTE) { + buf[pos++] = QUOTE; } + } else if (currChar == BACKSLASH + || currChar == QUOTE + || currChar == ZERO_BYTE + || currChar == DBL_QUOTE) { + buf[pos++] = BACKSLASH; } buf[pos++] = (byte) currChar; } else if (currChar < 0x800) { @@ -607,99 +511,6 @@ public void write(String str, boolean escape, boolean noBackslashEscapes) throws buf[pos++] = (byte) (0x80 | (currChar & 0x3f)); } } - if (escape) { - buf[pos++] = QUOTE; - } - } - - /** - * Write stream into socket. - * - * @param is inputStream - * @param escape must be escape - * @param noBackslashEscapes escape method - * @throws IOException if socket error occur - */ - public void write(InputStream is, boolean escape, boolean noBackslashEscapes) throws IOException { - byte[] array = new byte[4096]; - int len; - if (escape) { - while ((len = is.read(array)) > 0) { - writeBytesEscaped(array, len, noBackslashEscapes); - } - } else { - while ((len = is.read(array)) > 0) { - write(array, 0, len); - } - } - } - - /** - * Write stream into socket. - * - * @param is inputStream - * @param length write length - * @param escape must be escape - * @param noBackslashEscapes escape method - * @throws IOException if socket error occur - */ - public void write(InputStream is, long length, boolean escape, boolean noBackslashEscapes) - throws IOException { - byte[] array = new byte[4096]; - int len; - while (length > 0 && (len = is.read(array, 0, Math.min(4096, (int) length))) > 0) { - if (escape) { - writeBytesEscaped(array, len, noBackslashEscapes); - } else { - write(array, 0, len); - } - length -= len; - } - } - - /** - * Write reader into socket. - * - * @param reader reader - * @param escape must be escape - * @param noBackslashEscapes escape method - * @throws IOException if socket error occur - */ - public void write(Reader reader, boolean escape, boolean noBackslashEscapes) throws IOException { - char[] buffer = new char[4096]; - int len; - while ((len = reader.read(buffer)) >= 0) { - byte[] data = new String(buffer, 0, len).getBytes(StandardCharsets.UTF_8); - if (escape) { - writeBytesEscaped(data, data.length, noBackslashEscapes); - } else { - write(data); - } - } - } - - /** - * Write reader into socket. - * - * @param reader reader - * @param length write length - * @param escape must be escape - * @param noBackslashEscapes escape method - * @throws IOException if socket error occur - */ - public void write(Reader reader, long length, boolean escape, boolean noBackslashEscapes) - throws IOException { - char[] buffer = new char[4096]; - int len; - while (length > 0 && (len = reader.read(buffer, 0, Math.min((int) length, 4096))) >= 0) { - byte[] data = new String(buffer, 0, len).getBytes(StandardCharsets.UTF_8); - if (escape) { - writeBytesEscaped(data, data.length, noBackslashEscapes); - } else { - write(data); - } - length -= len; - } } /** @@ -714,13 +525,13 @@ public void writeBytesEscaped(byte[] bytes, int len, boolean noBackslashEscapes) throws IOException { if (len * 2 > buf.length - pos) { - // makes buffer bigger (up to 16M) - if (buf.length != getMaxPacketLength()) { + // makes buf bigger (up to 16M) + if (buf.length != maxPacketLength) { growBuffer(len * 2); } - // data may be bigger than buffer. - // must flush buffer when full (and reset position to 0) + // data may be bigger than buf. + // must flush buf when full (and reset position to 0) if (len * 2 > buf.length - pos) { if (mark != -1) { @@ -731,18 +542,18 @@ public void writeBytesEscaped(byte[] bytes, int len, boolean noBackslashEscapes) } else { - // not enough space in buffer, will fill buffer + // not enough space in buf, will fill buf if (noBackslashEscapes) { for (int i = 0; i < len; i++) { if (QUOTE == bytes[i]) { buf[pos++] = QUOTE; if (buf.length <= pos) { - flushBuffer(false); + writeSocket(false); } } buf[pos++] = bytes[i]; if (buf.length <= pos) { - flushBuffer(false); + writeSocket(false); } } } else { @@ -753,12 +564,12 @@ public void writeBytesEscaped(byte[] bytes, int len, boolean noBackslashEscapes) || bytes[i] == ZERO_BYTE) { buf[pos++] = '\\'; if (buf.length <= pos) { - flushBuffer(false); + writeSocket(false); } } buf[pos++] = bytes[i]; if (buf.length <= pos) { - flushBuffer(false); + writeSocket(false); } } } @@ -767,7 +578,7 @@ public void writeBytesEscaped(byte[] bytes, int len, boolean noBackslashEscapes) } } - // sure to have enough place filling buffer directly + // sure to have enough place filling buf directly if (noBackslashEscapes) { for (int i = 0; i < len; i++) { if (QUOTE == bytes[i]) { @@ -788,11 +599,144 @@ public void writeBytesEscaped(byte[] bytes, int len, boolean noBackslashEscapes) } } + /** + * buf growing use 4 size only to avoid creating/copying that are expensive operations. possible + * size + * + *
    + *
  1. SMALL_buf_SIZE = 8k (default) + *
  2. MEDIUM_buf_SIZE = 128k + *
  3. LARGE_buf_SIZE = 1M + *
  4. getMaxPacketLength = 16M (+ 4 if using no compression) + *
+ * + * @param len length to add + */ + private void growBuffer(int len) throws IOException { + int bufLength = buf.length; + int newCapacity; + if (bufLength == SMALL_BUFFER_SIZE) { + if (len + pos < MEDIUM_BUFFER_SIZE) { + newCapacity = MEDIUM_BUFFER_SIZE; + } else if (len + pos < LARGE_BUFFER_SIZE) { + newCapacity = LARGE_BUFFER_SIZE; + } else { + newCapacity = maxPacketLength; + } + } else if (bufLength == MEDIUM_BUFFER_SIZE) { + if (len + pos < LARGE_BUFFER_SIZE) { + newCapacity = LARGE_BUFFER_SIZE; + } else { + newCapacity = maxPacketLength; + } + } else if (bufContainDataAfterMark) { + // want to add some information to buf without having the command Header + // must grow buf until having all the query + newCapacity = Math.max(len + pos, maxPacketLength); + } else { + newCapacity = maxPacketLength; + } + + if (mark != -1 && len + pos > newCapacity) { + // buf is > 16M with mark. + // flush until mark, reset pos at beginning + flushBufferStopAtMark(); + + if (len + pos <= bufLength) { + return; + } + + // need to keep all data, buf can grow more than maxPacketLength + // grow buf if needed + if (len + pos > newCapacity) { + newCapacity = len + pos; + } + } + + byte[] newBuf = new byte[newCapacity]; + System.arraycopy(buf, 0, newBuf, 0, pos); + buf = newBuf; + } + + /** + * Send empty packet. + * + * @throws IOException if socket error occur. + */ + public void writeEmptyPacket() throws IOException { + + buf[0] = (byte) 0x00; + buf[1] = (byte) 0x00; + buf[2] = (byte) 0x00; + buf[3] = (byte) this.sequence.incrementAndGet(); + out.write(buf, 0, 4); + + if (logger.isTraceEnabled()) { + logger.trace( + "send com : content length=0 {}{}", serverThreadLog, LoggerHelper.hex(buf, 0, 4)); + } + out.flush(); + cmdLength = 0; + } + + /** + * Send packet to socket. + * + * @throws IOException if socket error occur. + */ + public void flush() throws IOException { + writeSocket(true); + out.flush(); + // if buf is big, and last query doesn't use at least half of it, resize buf to default + // value + if (buf.length > SMALL_BUFFER_SIZE && cmdLength * 2 < buf.length) { + buf = new byte[SMALL_BUFFER_SIZE]; + } + pos = 4; + cmdLength = 0; + mark = -1; + } + + public boolean checkRemainingSize(int len) { + return maxPacketLength - pos > len; + } + + /** + * Count query size. If query size is greater than max_allowed_packet and nothing has been already + * send, throw an exception to avoid having the connection closed. + * + * @param length additional length to query size + * @throws MaxAllowedPacketException if query has not to be send. + */ + public void checkMaxAllowedLength(int length) throws MaxAllowedPacketException { + if (cmdLength + length >= maxAllowedPacket && cmdLength == 0) { + // launch exception only if no packet has been send. + throw new MaxAllowedPacketException( + "query size (" + + (cmdLength + length) + + ") is >= to max_allowed_packet (" + + maxAllowedPacket + + ")", + false); + } + } + + public boolean exceedMaxLength() { + return cmdLength + (pos - initialPacketPos()) >= maxAllowedPacket; + } + + public OutputStream getOutputStream() { + return out; + } + public int getMaxAllowedPacket() { return maxAllowedPacket; } - public abstract void setMaxAllowedPacket(int maxAllowedPacket); + public void setMaxAllowedPacket(int maxAllowedPacket) { + this.maxAllowedPacket = maxAllowedPacket; + maxPacketLength = Math.min(MAX_PACKET_LENGTH, maxAllowedPacket + 4); + } public void permitTrace(boolean permitTrace) { this.permitTrace = permitTrace; @@ -802,23 +746,18 @@ public void permitTrace(boolean permitTrace) { * Set server thread id. * * @param serverThreadId current server thread id. - * @param isMaster is server master + * @param hostAddress host information */ - public void setServerThreadId(long serverThreadId, Boolean isMaster) { + public void setServerThreadId(long serverThreadId, HostAddress hostAddress) { + Boolean isMaster = hostAddress != null ? hostAddress.primary : null; this.serverThreadLog = "conn=" + serverThreadId + ((isMaster != null) ? "(" + (isMaster ? "M" : "S") + ")" : ""); } - public void setTraceCache(LruTraceCache traceCache) { - this.traceCache = traceCache; - } - - @Override public void mark() { mark = pos; } - @Override public boolean isMarked() { return mark != -1; } @@ -828,22 +767,21 @@ public boolean isMarked() { * * @throws IOException if flush fail. */ - @Override public void flushBufferStopAtMark() throws IOException { final int end = pos; pos = mark; - flushBuffer(true); + writeSocket(true); out.flush(); - startPacket(0); + initPacket(); System.arraycopy(buf, mark, buf, pos, end - mark); pos += end - mark; mark = -1; - bufferContainDataAfterMark = true; + bufContainDataAfterMark = true; } - public boolean bufferIsDataAfterMark() { - return bufferContainDataAfterMark; + public boolean bufIsDataAfterMark() { + return bufContainDataAfterMark; } /** @@ -852,14 +790,69 @@ public boolean bufferIsDataAfterMark() { * @return bytes after mark flag */ public byte[] resetMark() { + pos = mark; mark = -1; - if (bufferContainDataAfterMark) { + if (bufContainDataAfterMark) { byte[] data = Arrays.copyOfRange(buf, initialPacketPos(), pos); - startPacket(0); - bufferContainDataAfterMark = false; + initPacket(); + bufContainDataAfterMark = false; return data; } return null; } + + public int initialPacketPos() { + return 4; + } + + public void initPacket(String command) { + initPacket(); + } + + public void initPacket() { + sequence.set(-1); + pos = 4; + cmdLength = 0; + } + + /** + * Flush the internal buf. + * + * @param commandEnd command end + * @throws IOException id connection error occur. + */ + protected void writeSocket(boolean commandEnd) throws IOException { + if (pos > 4) { + buf[0] = (byte) (pos - 4); + buf[1] = (byte) ((pos - 4) >>> 8); + buf[2] = (byte) ((pos - 4) >>> 16); + buf[3] = (byte) this.sequence.incrementAndGet(); + checkMaxAllowedLength(pos - 4); + out.write(buf, 0, pos); + cmdLength += pos - 4; + + if (logger.isTraceEnabled()) { + if (permitTrace) { + logger.trace( + "send: {}\n{}", serverThreadLog, LoggerHelper.hex(buf, 0, pos, maxQuerySizeToLog)); + } else { + logger.trace("send: content length={} {} com=", pos - 4, serverThreadLog); + } + } + + // if last com fill the max size, must send an empty com to indicate command end. + if (commandEnd && pos == maxPacketLength) { + writeEmptyPacket(); + } + + pos = 4; + } + } + + public void close() throws IOException { + out.close(); + } + + public void setContext(Context context) {} } diff --git a/src/main/java/org/mariadb/jdbc/client/PrepareCache.java b/src/main/java/org/mariadb/jdbc/client/PrepareCache.java new file mode 100644 index 000000000..c9d377fbf --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/PrepareCache.java @@ -0,0 +1,71 @@ +package org.mariadb.jdbc.client; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.mariadb.jdbc.ServerPreparedStatement; +import org.mariadb.jdbc.message.server.CachedPrepareResultPacket; +import org.mariadb.jdbc.message.server.PrepareResultPacket; + +public class PrepareCache extends LinkedHashMap { + + private static final long serialVersionUID = -8922905563713952695L; + private final int maxSize; + private final ClientImpl con; + + public PrepareCache(int size, ClientImpl con) { + super(size, .75f, true); + this.maxSize = size; + this.con = con; + } + + @Override + public boolean removeEldestEntry(Map.Entry eldest) { + if (this.size() > maxSize) { + eldest.getValue().unCache(con); + return true; + } + return false; + } + + public synchronized CachedPrepareResultPacket get( + String key, ServerPreparedStatement preparedStatement) { + CachedPrepareResultPacket prepare = super.get(key); + if (prepare != null && preparedStatement != null) { + prepare.incrementUse(preparedStatement); + } + return prepare; + } + + public synchronized CachedPrepareResultPacket put( + String key, CachedPrepareResultPacket result, ServerPreparedStatement preparedStatement) { + CachedPrepareResultPacket cached = super.get(key); + + // if there is already some cached data, return existing cached data + if (cached != null) { + cached.incrementUse(preparedStatement); + result.unCache(con); + return cached; + } + + if (result.cache()) { + result.incrementUse(preparedStatement); + super.put(key, result); + } + return null; + } + + public CachedPrepareResultPacket get(Object key) { + throw new IllegalStateException("not available method"); + } + + public CachedPrepareResultPacket put(String key, PrepareResultPacket result) { + throw new IllegalStateException("not available method"); + } + + public void reset() { + for (CachedPrepareResultPacket prep : values()) { + prep.reset(); + } + this.clear(); + } +} diff --git a/src/main/java/org/mariadb/jdbc/internal/io/input/ReadAheadBufferedStream.java b/src/main/java/org/mariadb/jdbc/client/ReadAheadBufferedStream.java similarity index 75% rename from src/main/java/org/mariadb/jdbc/internal/io/input/ReadAheadBufferedStream.java rename to src/main/java/org/mariadb/jdbc/client/ReadAheadBufferedStream.java index 94bfe02cc..d95d0ed94 100644 --- a/src/main/java/org/mariadb/jdbc/internal/io/input/ReadAheadBufferedStream.java +++ b/src/main/java/org/mariadb/jdbc/client/ReadAheadBufferedStream.java @@ -31,20 +31,40 @@ * */ -package org.mariadb.jdbc.internal.io.input; +package org.mariadb.jdbc.client; +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; /** - * Permit to buffer socket data, reading not only asked bytes, but available number of bytes when + * Permit to buf socket data, reading not only asked bytes, but available number of bytes when * possible. */ public class ReadAheadBufferedStream extends FilterInputStream { private static final int BUF_SIZE = 16384; - private volatile byte[] buf; + private final byte[] buf; private int end; private int pos; @@ -61,7 +81,7 @@ public ReadAheadBufferedStream(InputStream in) { */ public synchronized int read() throws IOException { if (pos >= end) { - fillBuffer(1); + fillbuf(1); if (pos >= end) { return -1; } @@ -72,7 +92,7 @@ public synchronized int read() throws IOException { /** * Returing byte array, from cache of reading socket if needed. * - * @param externalBuf buffer to fill + * @param externalBuf buf to fill * @param off offset * @param len length to read * @return number of added bytes @@ -90,8 +110,8 @@ public synchronized int read(byte[] externalBuf, int off, int len) throws IOExce // read if (end - pos <= 0) { if (len - totalReads >= buf.length) { - // buffer length is less than asked byte and buffer is empty - // => filling directly into external buffer + // buf length is less than asked byte and buf is empty + // => filling directly into external buf int reads = super.read(externalBuf, off + totalReads, len - totalReads); if (reads <= 0) { return (totalReads == 0) ? -1 : totalReads; @@ -100,15 +120,15 @@ public synchronized int read(byte[] externalBuf, int off, int len) throws IOExce } else { - // filling internal buffer - fillBuffer(len - totalReads); + // filling internal buf + fillbuf(len - totalReads); if (end <= 0) { return (totalReads == 0) ? -1 : totalReads; } } } - // copy internal value to buffer. + // copy internal value to buf. int copyLength = Math.min(len - totalReads, end - pos); System.arraycopy(buf, pos, externalBuf, off + totalReads, copyLength); pos += copyLength; @@ -121,12 +141,12 @@ public synchronized int read(byte[] externalBuf, int off, int len) throws IOExce } /** - * Fill buffer with required length, or available bytes. + * Fill buf with required length, or available bytes. * * @param minNeededBytes asked number of bytes * @throws IOException in case of failing reading stream. */ - private void fillBuffer(int minNeededBytes) throws IOException { + private void fillbuf(int minNeededBytes) throws IOException { int lengthToReallyRead = Math.min(BUF_SIZE, Math.max(super.available(), minNeededBytes)); end = super.read(buf, 0, lengthToReallyRead); pos = 0; diff --git a/src/main/java/org/mariadb/jdbc/client/ReadableByteBuf.java b/src/main/java/org/mariadb/jdbc/client/ReadableByteBuf.java new file mode 100644 index 000000000..bd289b5b1 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/ReadableByteBuf.java @@ -0,0 +1,288 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client; + +import java.nio.charset.StandardCharsets; +import org.mariadb.jdbc.util.MutableInt; + +public final class ReadableByteBuf { + private final MutableInt sequence; + private final int limit; + private final byte[] buf; + private int pos; + private int mark = -1; + + public ReadableByteBuf(MutableInt sequence, byte[] buf, int limit) { + this.sequence = sequence; + this.pos = 0; + this.buf = buf; + this.limit = limit; + } + + public int readableBytes() { + return limit - pos; + } + + public int pos() { + return pos; + } + + public byte[] buf() { + return buf; + } + + public ReadableByteBuf pos(int pos) { + this.pos = pos; + return this; + } + + public ReadableByteBuf mark() { + mark = pos; + return this; + } + + public ReadableByteBuf reset() { + if (mark == -1) throw new IllegalStateException("mark was not set"); + pos = mark; + return this; + } + + public ReadableByteBuf skip() { + pos++; + return this; + } + + public ReadableByteBuf skip(int length) { + pos += length; + return this; + } + + public MutableInt getSequence() { + return sequence; + } + + public byte getByte() { + return buf[pos]; + } + + public byte getByte(int index) { + return buf[index]; + } + + public short getUnsignedByte() { + return (short) (buf[pos] & 0xff); + } + + public short getUnsignedByte(int index) { + return (short) (buf[index] & 0xff); + } + + public short getShort(int index) { + return (short) ((buf[index] & 0xff) | (buf[index + 1] << 8)); + } + + public int getUnsignedShort(int index) { + return getShort(index) & 0xffff; + } + + public int getMedium(int index) { + int value = getUnsignedMedium(index); + if ((value & 0x800000) != 0) { + value |= 0xff000000; + } + return value; + } + + public int getUnsignedMedium(int index) { + return (buf[index] & 0xff) + ((buf[index + 1] & 0xff) << 8) | (buf[index + 2] << 16); + } + + public int getInt(int index) { + return ((buf[index] & 0xff) + + ((buf[index + 1] & 0xff) << 8) + + ((buf[index + 2] & 0xff) << 16) + + ((buf[index + 3] & 0xff) << 24)); + } + + public long getUnsignedInt(int index) { + return getInt(index) & 0xffffffff; + } + + public long getLong(int index) { + return ((buf[index] & 0xff) + + ((buf[index + 1] & 0xff) << 8) + + ((buf[index + 2] & 0xff) << 16) + + ((buf[index + 3] & 0xff) << 24) + + ((buf[index + 4] & 0xff) << 32) + + ((buf[index + 5] & 0xff) << 40) + + ((buf[index + 6] & 0xff) << 48) + + ((buf[index + 7] & 0xff) << 56)); + } + + public ReadableByteBuf getBytes(int index, byte[] dst) { + System.arraycopy(buf, index, dst, 0, dst.length); + return this; + } + + public int readLengthNotNull() { + int type = (buf[pos++] & 0xff); + switch (type) { + case 251: + throw new IllegalStateException("Must not have null length"); + case 252: + return readUnsignedShort(); + case 253: + return readUnsignedMedium(); + case 254: + return (int) readLong(); + default: + return type; + } + } + + public Integer readLength() { + int type = readUnsignedByte(); + switch (type) { + case 251: + return null; + case 252: + return readUnsignedShort(); + case 253: + return readUnsignedMedium(); + case 254: + return (int) readLong(); + default: + return type; + } + } + + public byte readByte() { + return buf[pos++]; + } + + public short readUnsignedByte() { + return (short) (buf[pos++] & 0xff); + } + + public short readShort() { + return (short) ((buf[pos++] & 0xff) | (buf[pos++] << 8)); + } + + public int readUnsignedShort() { + return ((buf[pos++] & 0xff) | (buf[pos++] << 8)) & 0xffff; + } + + public int readMedium() { + int value = readUnsignedMedium(); + if ((value & 0x800000) != 0) { + value |= 0xff000000; + } + return value; + } + + public int readUnsignedMedium() { + return buf[pos++] & 0xff | (buf[pos++] & 0xff) << 8 | (buf[pos++] & 0xff) << 16; + } + + public int readInt() { + return ((buf[pos++] & 0xff) + + ((buf[pos++] & 0xff) << 8) + + ((buf[pos++] & 0xff) << 16) + + ((buf[pos++] & 0xff) << 24)); + } + + public long readUnsignedInt() { + return ((buf[pos++] & 0xff) + + ((buf[pos++] & 0xff) << 8) + + ((buf[pos++] & 0xff) << 16) + + ((buf[pos++] & 0xff) << 24)) + & 0xffffffffL; + } + + public long readLong() { + return ((buf[pos++] & 0xffL) + + ((buf[pos++] & 0xffL) << 8) + + ((buf[pos++] & 0xffL) << 16) + + ((buf[pos++] & 0xffL) << 24) + + ((buf[pos++] & 0xffL) << 32) + + ((buf[pos++] & 0xffL) << 40) + + ((buf[pos++] & 0xffL) << 48) + + ((buf[pos++] & 0xffL) << 56)); + } + + public ReadableByteBuf readBytes(byte[] dst) { + System.arraycopy(buf, pos, dst, 0, dst.length); + pos += dst.length; + return this; + } + + public byte[] readBytesNullEnd() { + int initialPosition = pos; + int cnt = 0; + while (readableBytes() > 0 && (buf[pos++] != 0)) { + cnt++; + } + byte[] dst = new byte[cnt]; + System.arraycopy(buf, initialPosition, dst, 0, dst.length); + return dst; + } + + public ReadableByteBuf readLengthBuffer() { + int len = readLengthNotNull(); + byte[] tmp = new byte[len]; + readBytes(tmp); + return new ReadableByteBuf(sequence, tmp, len); + } + + public String readString(int length) { + pos += length; + return new String(buf, pos - length, length, StandardCharsets.UTF_8); + } + + public String readAscii(int length) { + pos += length; + return new String(buf, pos - length, length, StandardCharsets.US_ASCII); + } + + public String readStringNullEnd() { + int initialPosition = pos; + int cnt = 0; + while (readableBytes() > 0 && (buf[pos++] != 0)) { + cnt++; + } + return new String(buf, initialPosition, cnt, StandardCharsets.UTF_8); + } + + public String readStringEof() { + int initialPosition = pos; + pos = limit; + return new String(buf, initialPosition, pos - initialPosition, StandardCharsets.UTF_8); + } + + public float readFloat() { + return Float.intBitsToFloat(readInt()); + } + + public double readDouble() { + return Double.longBitsToDouble(readLong()); + } +} diff --git a/src/main/java/org/mariadb/jdbc/client/ServerVersion.java b/src/main/java/org/mariadb/jdbc/client/ServerVersion.java new file mode 100644 index 000000000..733533977 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/ServerVersion.java @@ -0,0 +1,164 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client; + +import java.util.Objects; + +public class ServerVersion { + + private final String serverVersion; + private final int majorVersion; + private final int minorVersion; + private final int patchVersion; + private final boolean mariaDBServer; + + public ServerVersion(String serverVersion, boolean mariaDBServer) { + this.serverVersion = serverVersion; + this.mariaDBServer = mariaDBServer; + int[] parsed = parseVersion(serverVersion); + this.majorVersion = parsed[0]; + this.minorVersion = parsed[1]; + this.patchVersion = parsed[2]; + } + + public boolean isMariaDBServer() { + return mariaDBServer; + } + + public int getMajorVersion() { + return majorVersion; + } + + public int getMinorVersion() { + return minorVersion; + } + + public int getPatchVersion() { + return patchVersion; + } + + public String getServerVersion() { + return serverVersion; + } + + /** + * Utility method to check if database version is greater than parameters. + * + * @param major major version + * @param minor minor version + * @param patch patch version + * @return true if version is greater than parameters + */ + public boolean versionGreaterOrEqual(int major, int minor, int patch) { + if (this.majorVersion > major) { + return true; + } + + if (this.majorVersion < major) { + return false; + } + + /* + * Major versions are equal, compare minor versions + */ + if (this.minorVersion > minor) { + return true; + } + if (this.minorVersion < minor) { + return false; + } + + // Minor versions are equal, compare patch version. + return this.patchVersion >= patch; + } + + private int[] parseVersion(String serverVersion) { + int length = serverVersion.length(); + char car; + int offset = 0; + int type = 0; + int val = 0; + int majorVersion = 0; + int minorVersion = 0; + int patchVersion = 0; + + main_loop: + for (; offset < length; offset++) { + car = serverVersion.charAt(offset); + if (car < '0' || car > '9') { + switch (type) { + case 0: + majorVersion = val; + break; + case 1: + minorVersion = val; + break; + case 2: + patchVersion = val; + break main_loop; + default: + break; + } + type++; + val = 0; + } else { + val = val * 10 + car - 48; + } + } + + // serverVersion finished by number like "5.5.57", assign patchVersion + if (type == 2) { + patchVersion = val; + } + return new int[] {majorVersion, minorVersion, patchVersion}; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServerVersion that = (ServerVersion) o; + return mariaDBServer == that.mariaDBServer && serverVersion.equals(that.serverVersion); + } + + @Override + public int hashCode() { + return Objects.hash(serverVersion, mariaDBServer); + } + + @Override + public String toString() { + return "ServerVersion{" + + "serverVersion='" + + serverVersion + + '\'' + + ", majorVersion=" + + majorVersion + + ", minorVersion=" + + minorVersion + + ", patchVersion=" + + patchVersion + + ", mariaDBServer=" + + mariaDBServer + + '}'; + } +} diff --git a/src/main/java/org/mariadb/jdbc/client/TransactionSaver.java b/src/main/java/org/mariadb/jdbc/client/TransactionSaver.java new file mode 100644 index 000000000..54d12ebaf --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/TransactionSaver.java @@ -0,0 +1,32 @@ +package org.mariadb.jdbc.client; + +import java.util.ArrayList; +import java.util.List; +import org.mariadb.jdbc.message.client.RedoableClientMessage; + +public class TransactionSaver { + private final List buffers = new ArrayList<>(); + private transient boolean cleanState = true; + + public void add(RedoableClientMessage clientMessage) { + buffers.add(clientMessage); + } + + public void dirty() { + buffers.clear(); + cleanState = false; + } + + public void clear() { + buffers.clear(); + cleanState = true; + } + + public List getBuffers() { + return buffers; + } + + public boolean isCleanState() { + return cleanState; + } +} diff --git a/src/main/java/org/mariadb/jdbc/client/context/BaseContext.java b/src/main/java/org/mariadb/jdbc/client/context/BaseContext.java new file mode 100644 index 000000000..62a783fda --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/context/BaseContext.java @@ -0,0 +1,156 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client.context; + +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.client.PrepareCache; +import org.mariadb.jdbc.client.ServerVersion; +import org.mariadb.jdbc.client.TransactionSaver; +import org.mariadb.jdbc.message.client.RedoableClientMessage; +import org.mariadb.jdbc.message.server.InitialHandshakePacket; +import org.mariadb.jdbc.util.constants.Capabilities; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; + +public class BaseContext implements Context { + + private final long threadId; + private final long serverCapabilities; + private final byte[] seed; + private final ServerVersion version; + private final boolean eofDeprecated; + private final Configuration conf; + private final ExceptionFactory exceptionFactory; + protected int serverStatus; + private String database; + private int transactionIsolationLevel; + private int warning; + private PrepareCache prepareCache; + private int stateFlag = 0; + + public BaseContext( + InitialHandshakePacket handshake, + Configuration conf, + ExceptionFactory exceptionFactory, + PrepareCache prepareCache) { + this.threadId = handshake.getThreadId(); + this.seed = handshake.getSeed(); + this.serverCapabilities = handshake.getCapabilities(); + this.serverStatus = handshake.getServerStatus(); + this.version = new ServerVersion(handshake.getServerVersion(), handshake.isMariaDBServer()); + this.eofDeprecated = (serverCapabilities & Capabilities.CLIENT_DEPRECATE_EOF) > 0; + this.conf = conf; + this.database = conf.database(); + this.exceptionFactory = exceptionFactory; + this.prepareCache = prepareCache; + } + + public long getThreadId() { + return threadId; + } + + public byte[] getSeed() { + return seed; + } + + public long getServerCapabilities() { + return serverCapabilities; + } + + public int getServerStatus() { + return serverStatus; + } + + public void setServerStatus(int serverStatus) { + this.serverStatus = serverStatus; + } + + public String getDatabase() { + return database; + } + + public void setDatabase(String database) { + this.database = database; + } + + public ServerVersion getVersion() { + return version; + } + + public boolean isEofDeprecated() { + return eofDeprecated; + } + + public int getWarning() { + return warning; + } + + public void setWarning(int warning) { + this.warning = warning; + } + + public ExceptionFactory getExceptionFactory() { + return exceptionFactory; + } + + public Configuration getConf() { + return conf; + } + + public int getTransactionIsolationLevel() { + return transactionIsolationLevel; + } + + public void setTransactionIsolationLevel(int transactionIsolationLevel) { + this.transactionIsolationLevel = transactionIsolationLevel; + } + + public PrepareCache getPrepareCache() { + return prepareCache; + } + + public void resetPrepareCache(PrepareCache prepareCache) { + this.prepareCache = prepareCache; + } + + public int getStateFlag() { + return stateFlag; + } + + public void setStateFlag(int stateFlag) { + this.stateFlag = stateFlag; + } + + public void addStateFlag(int state) { + stateFlag |= state; + } + + public void saveRedo(RedoableClientMessage msg) {} + + public TransactionSaver getTransactionSaver() { + return null; + } + + @Override + public String toString() { + return "ConnectionContext{" + "threadId=" + threadId + ", version=" + version + '}'; + } +} diff --git a/src/main/java/org/mariadb/jdbc/client/context/Context.java b/src/main/java/org/mariadb/jdbc/client/context/Context.java new file mode 100644 index 000000000..41c8541c8 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/context/Context.java @@ -0,0 +1,76 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client.context; + +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.client.PrepareCache; +import org.mariadb.jdbc.client.ServerVersion; +import org.mariadb.jdbc.client.TransactionSaver; +import org.mariadb.jdbc.message.client.RedoableClientMessage; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; + +public interface Context { + + long getThreadId(); + + byte[] getSeed(); + + long getServerCapabilities(); + + int getServerStatus(); + + void setServerStatus(int serverStatus); + + String getDatabase(); + + void setDatabase(String database); + + ServerVersion getVersion(); + + boolean isEofDeprecated(); + + int getWarning(); + + void setWarning(int warning); + + ExceptionFactory getExceptionFactory(); + + Configuration getConf(); + + int getTransactionIsolationLevel(); + + void setTransactionIsolationLevel(int transactionIsolationLevel); + + PrepareCache getPrepareCache(); + + void resetPrepareCache(PrepareCache prepareCache); + + int getStateFlag(); + + void setStateFlag(int stateFlag); + + void addStateFlag(int state); + + void saveRedo(RedoableClientMessage msg); + + TransactionSaver getTransactionSaver(); +} diff --git a/src/main/java/org/mariadb/jdbc/client/context/RedoContext.java b/src/main/java/org/mariadb/jdbc/client/context/RedoContext.java new file mode 100644 index 000000000..65dd56ac9 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/context/RedoContext.java @@ -0,0 +1,57 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client.context; + +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.client.PrepareCache; +import org.mariadb.jdbc.client.TransactionSaver; +import org.mariadb.jdbc.message.client.RedoableClientMessage; +import org.mariadb.jdbc.message.server.InitialHandshakePacket; +import org.mariadb.jdbc.util.constants.ServerStatus; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; + +public class RedoContext extends BaseContext { + + private final TransactionSaver transactionSaver; + + public RedoContext( + InitialHandshakePacket handshake, + Configuration conf, + ExceptionFactory exceptionFactory, + PrepareCache prepareCache) { + super(handshake, conf, exceptionFactory, prepareCache); + this.transactionSaver = new TransactionSaver(); + } + + public void setServerStatus(int serverStatus) { + this.serverStatus = serverStatus; + if ((serverStatus & ServerStatus.IN_TRANSACTION) == 0) transactionSaver.clear(); + } + + public void saveRedo(RedoableClientMessage msg) { + transactionSaver.add(msg); + } + + public TransactionSaver getTransactionSaver() { + return transactionSaver; + } +} diff --git a/src/main/java/org/mariadb/jdbc/client/result/CompleteResult.java b/src/main/java/org/mariadb/jdbc/client/result/CompleteResult.java new file mode 100644 index 000000000..bfeeaf8cb --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/result/CompleteResult.java @@ -0,0 +1,299 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client.result; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import org.mariadb.jdbc.Statement; +import org.mariadb.jdbc.client.PacketReader; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class CompleteResult extends Result { + + protected static final int BEFORE_FIRST_POS = -1; + + public CompleteResult( + Statement stmt, + boolean binaryProtocol, + long maxRows, + ColumnDefinitionPacket[] metadataList, + PacketReader reader, + Context context, + int resultSetType, + boolean closeOnCompletion) + throws IOException, SQLException { + + super( + stmt, + binaryProtocol, + maxRows, + metadataList, + reader, + context, + resultSetType, + closeOnCompletion); + this.data = new ReadableByteBuf[10]; + if (maxRows > 0) { + while (readNext() && dataSize < maxRows) {} + if (!loaded) skipRemaining(); + } else { + while (readNext()) {} + } + loaded = true; + } + + public CompleteResult( + ColumnDefinitionPacket[] metadataList, ReadableByteBuf[] data, Context context) { + super(metadataList, data, context); + } + + public static ResultSet createResultSet( + String columnName, DataType columnType, String[] data, Context context) { + return createResultSet( + new String[] {columnName}, new DataType[] {columnType}, new String[][] {data}, context); + } + + /** + * Create a result set from given data. Useful for creating "fake" resultSets for + * DatabaseMetaData, (one example is MariaDbDatabaseMetaData.getTypeInfo()) + * + * @param columnNames - string array of column names + * @param columnTypes - column types + * @param data - each element of this array represents a complete row in the ResultSet. Each value + * is given in its string representation, as in MariaDB text protocol, except boolean (BIT(1)) + * values that are represented as "1" or "0" strings + * @param context connection context + * @return resultset + */ + public static ResultSet createResultSet( + String[] columnNames, DataType[] columnTypes, String[][] data, Context context) { + + int columnNameLength = columnNames.length; + ColumnDefinitionPacket[] columns = new ColumnDefinitionPacket[columnNameLength]; + + for (int i = 0; i < columnNameLength; i++) { + columns[i] = ColumnDefinitionPacket.create(columnNames[i], columnTypes[i]); + } + + List rows = new ArrayList<>(); + for (String[] rowData : data) { + assert rowData.length == columnNameLength; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + for (int i = 0; i < rowData.length; i++) { + + if (rowData[i] != null) { + byte[] bb = rowData[i].getBytes(); + int len = bb.length; + if (len < 251) { + baos.write((byte) len); + } else if (len < 65536) { + baos.write((byte) 0xfc); + baos.write((byte) len); + baos.write((byte) (len >>> 8)); + } else { + // assumption : len < 16777216 + baos.write((byte) 0xfd); + baos.write((byte) len); + baos.write((byte) (len >>> 8)); + baos.write((byte) (len >>> 16)); + } + baos.write(bb, 0, bb.length); + } else { + baos.write((byte) 0xfb); + } + } + byte[] bb = baos.toByteArray(); + rows.add(new ReadableByteBuf(null, bb, bb.length)); + } + return new CompleteResult(columns, rows.toArray(new ReadableByteBuf[0]), context); + } + + @Override + public boolean next() throws SQLException { + if (rowPointer < dataSize - 1) { + row.setRow(data[++rowPointer]); + return true; + } else { + // all data are reads and pointer is after last + row.setRow(null); + rowPointer = dataSize; + return false; + } + } + + @Override + public boolean streaming() { + return false; + } + + @Override + public void fetchRemaining() throws SQLException {} + + @Override + public boolean isAfterLast() throws SQLException { + checkClose(); + if (rowPointer < dataSize) { + // has remaining results + return false; + } else { + + // has read all data and pointer is after last result + // so result would have to always to be true, + // but when result contain no row at all jdbc say that must return false + return dataSize > 0; + } + } + + @Override + public boolean isFirst() throws SQLException { + checkClose(); + return rowPointer == 0 && dataSize > 0; + } + + @Override + public boolean isLast() throws SQLException { + checkClose(); + return rowPointer == dataSize - 1 && dataSize > 0; + } + + @Override + public void beforeFirst() throws SQLException { + checkClose(); + rowPointer = BEFORE_FIRST_POS; + row.setRow(null); + } + + @Override + public void afterLast() throws SQLException { + checkClose(); + row.setRow(null); + rowPointer = dataSize; + } + + @Override + public boolean first() throws SQLException { + checkClose(); + rowPointer = 0; + if (dataSize == 0) { + row.setRow(null); + return false; + } + row.setRow(data[rowPointer]); + return true; + } + + @Override + public boolean last() throws SQLException { + checkClose(); + rowPointer = dataSize - 1; + if (rowPointer == BEFORE_FIRST_POS) { + row.setRow(null); + return false; + } + row.setRow(data[rowPointer]); + return dataSize > 0; + } + + @Override + public int getRow() throws SQLException { + checkClose(); + return rowPointer == dataSize ? 0 : rowPointer + 1; + } + + @Override + public boolean absolute(int idx) throws SQLException { + checkClose(); + if (idx == 0 || idx > dataSize) { + rowPointer = idx == 0 ? BEFORE_FIRST_POS : dataSize; + row.setRow(null); + return false; + } + + if (idx > 0) { + rowPointer = idx - 1; + row.setRow(data[rowPointer]); + return true; + } else { + if (dataSize + idx >= 0) { + // absolute position reverse from ending resultSet + rowPointer = dataSize + idx; + row.setRow(data[rowPointer]); + return true; + } + rowPointer = BEFORE_FIRST_POS; + row.setRow(null); + return false; + } + } + + @Override + public boolean relative(int rows) throws SQLException { + checkClose(); + int newPos = rowPointer + rows; + if (newPos <= -1) { + rowPointer = BEFORE_FIRST_POS; + row.setRow(null); + return false; + } else if (newPos >= dataSize) { + rowPointer = dataSize; + row.setRow(null); + return false; + } else { + rowPointer = newPos; + row.setRow(data[rowPointer]); + return true; + } + } + + @Override + public boolean previous() throws SQLException { + checkClose(); + if (rowPointer > BEFORE_FIRST_POS) { + rowPointer--; + if (rowPointer != BEFORE_FIRST_POS) { + row.setRow(data[rowPointer]); + return true; + } + } + row.setRow(null); + return false; + } + + @Override + public int getFetchSize() throws SQLException { + checkClose(); + return 0; + } + + @Override + public void setFetchSize(int rows) throws SQLException { + checkClose(); + } +} diff --git a/src/main/java/org/mariadb/jdbc/client/result/Result.java b/src/main/java/org/mariadb/jdbc/client/result/Result.java new file mode 100644 index 000000000..f2eb7d28b --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/result/Result.java @@ -0,0 +1,1254 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client.result; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.net.URL; +import java.sql.*; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Map; +import org.mariadb.jdbc.client.PacketReader; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.BinaryRowDecoder; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.RowDecoder; +import org.mariadb.jdbc.codec.TextRowDecoder; +import org.mariadb.jdbc.codec.list.*; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.message.server.Completion; +import org.mariadb.jdbc.message.server.ErrorPacket; +import org.mariadb.jdbc.util.constants.ServerStatus; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; + +public abstract class Result implements ResultSet, Completion { + + protected final int resultSetType; + protected final ExceptionFactory exceptionFactory; + protected final PacketReader reader; + protected final Context context; + private final int maxIndex; + private final boolean closeOnCompletion; + protected ColumnDefinitionPacket[] metadataList; + protected RowDecoder row; + protected int dataSize = 0; + protected ReadableByteBuf[] data; + protected boolean loaded; + protected boolean outputParameter; + protected int rowPointer = -1; + protected boolean closed; + protected Statement statement; + protected long maxRows; + private boolean forceAlias; + + public Result( + org.mariadb.jdbc.Statement stmt, + boolean binaryProtocol, + long maxRows, + ColumnDefinitionPacket[] metadataList, + PacketReader reader, + Context context, + int resultSetType, + boolean closeOnCompletion) { + this.maxRows = maxRows; + this.statement = stmt; + this.closeOnCompletion = closeOnCompletion; + this.metadataList = metadataList; + this.maxIndex = this.metadataList.length; + this.reader = reader; + this.exceptionFactory = context.getExceptionFactory(); + this.context = context; + this.resultSetType = resultSetType; + row = + binaryProtocol + ? new BinaryRowDecoder(this.maxIndex, metadataList, context.getConf()) + : new TextRowDecoder(this.maxIndex, metadataList, context.getConf()); + } + + public Result(ColumnDefinitionPacket[] metadataList, ReadableByteBuf[] data, Context context) { + this.metadataList = metadataList; + this.maxIndex = this.metadataList.length; + this.reader = null; + this.exceptionFactory = context.getExceptionFactory(); + this.context = context; + this.data = data; + this.dataSize = data.length; + this.statement = null; + this.resultSetType = TYPE_FORWARD_ONLY; + this.closeOnCompletion = false; + row = new TextRowDecoder(maxIndex, metadataList, context.getConf()); + } + + @SuppressWarnings("fallthrough") + protected boolean readNext() throws SQLException, IOException { + ReadableByteBuf buf = reader.readPacket(false); + switch (buf.getByte()) { + case (byte) 0xFF: + loaded = true; + ErrorPacket errorPacket = new ErrorPacket(buf, context); + throw exceptionFactory.create( + errorPacket.getMessage(), errorPacket.getSqlState(), errorPacket.getErrorCode()); + + case (byte) 0xFE: + if ((context.isEofDeprecated() && buf.readableBytes() < 0xffffff) + || (!context.isEofDeprecated() && buf.readableBytes() < 8)) { + + buf.skip(); // skip header + int serverStatus; + int warnings; + + if (!context.isEofDeprecated()) { + // EOF_Packet + warnings = buf.readUnsignedShort(); + serverStatus = buf.readUnsignedShort(); + } else { + // OK_Packet with a 0xFE header + buf.skip(buf.readLengthNotNull()); // skip update count + buf.skip(buf.readLengthNotNull()); // skip insert id + serverStatus = buf.readUnsignedShort(); + warnings = buf.readUnsignedShort(); + } + outputParameter = (serverStatus & ServerStatus.PS_OUT_PARAMETERS) != 0; + context.setServerStatus(serverStatus); + context.setWarning(warnings); + loaded = true; + return false; + } + + // continue reading rows + + default: + if (dataSize + 1 >= data.length) { + growDataArray(); + } + data[dataSize++] = buf; + } + return true; + } + + @SuppressWarnings("fallthrough") + protected void skipRemaining() throws SQLException, IOException { + while (true) { + ReadableByteBuf buf = reader.readPacket(true); + switch (buf.getUnsignedByte()) { + case 0xFF: + loaded = true; + ErrorPacket errorPacket = new ErrorPacket(buf, context); + throw exceptionFactory.create( + errorPacket.getMessage(), errorPacket.getSqlState(), errorPacket.getErrorCode()); + + case 0xFE: + if ((context.isEofDeprecated() && buf.readableBytes() < 0xffffff) + || (!context.isEofDeprecated() && buf.readableBytes() < 8)) { + + buf.skip(); // skip header + int serverStatus; + int warnings; + + if (!context.isEofDeprecated()) { + // EOF_Packet + warnings = buf.readUnsignedShort(); + serverStatus = buf.readUnsignedShort(); + } else { + // OK_Packet with a 0xFE header + buf.skip(buf.readLengthNotNull()); // skip update count + buf.skip(buf.readLengthNotNull()); // skip insert id + serverStatus = buf.readUnsignedShort(); + warnings = buf.readUnsignedShort(); + } + outputParameter = (serverStatus & ServerStatus.PS_OUT_PARAMETERS) != 0; + context.setServerStatus(serverStatus); + context.setWarning(warnings); + loaded = true; + return; + } + } + } + } + + /** Grow data array. */ + private void growDataArray() { + int newCapacity = data.length + (data.length >> 1); + data = Arrays.copyOf(data, newCapacity); + } + + @Override + public abstract boolean next() throws SQLException; + + public abstract boolean streaming(); + + public abstract void fetchRemaining() throws SQLException; + + public boolean loaded() { + return loaded; + } + + public boolean isOutputParameter() { + return outputParameter; + } + + @Override + public void close() throws SQLException { + this.fetchRemaining(); + this.closed = true; + if (closeOnCompletion) { + statement.close(); + } + } + + protected ReadableByteBuf getCurrentRowData() { + return data[0]; + } + + protected void addRowData(ReadableByteBuf buf) { + if (dataSize + 1 >= data.length) { + growDataArray(); + } + data[dataSize++] = buf; + } + + protected void updateRowData(ReadableByteBuf rawData) { + data[rowPointer] = rawData; + row.setRow(rawData); + } + + @Override + public boolean wasNull() throws SQLException { + return row.wasNull(); + } + + @Override + public String getString(int columnIndex) throws SQLException { + return row.getValue(columnIndex, StringCodec.INSTANCE); + } + + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + Boolean b = row.getValue(columnIndex, BooleanCodec.INSTANCE); + return (b == null) ? false : b; + } + + @Override + public byte getByte(int columnIndex) throws SQLException { + Byte b = row.getValue(columnIndex, ByteCodec.INSTANCE); + return (b == null) ? 0 : b; + } + + @Override + public short getShort(int columnIndex) throws SQLException { + Short b = row.getValue(columnIndex, ShortCodec.INSTANCE); + return (b == null) ? 0 : b; + } + + @Override + public int getInt(int columnIndex) throws SQLException { + Integer b = row.getValue(columnIndex, IntCodec.INSTANCE); + return (b == null) ? 0 : b; + } + + @Override + public long getLong(int columnIndex) throws SQLException { + Long b = row.getValue(columnIndex, LongCodec.INSTANCE); + return (b == null) ? 0L : b; + } + + @Override + public float getFloat(int columnIndex) throws SQLException { + Float b = row.getValue(columnIndex, FloatCodec.INSTANCE); + return (b == null) ? 0F : b; + } + + @Override + public double getDouble(int columnIndex) throws SQLException { + Double b = row.getValue(columnIndex, DoubleCodec.INSTANCE); + return (b == null) ? 0D : b; + } + + @Override + @Deprecated + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { + BigDecimal d = row.getValue(columnIndex, BigDecimalCodec.INSTANCE); + if (d == null) return d; + return d.setScale(scale, BigDecimal.ROUND_HALF_DOWN); + } + + @Override + public byte[] getBytes(int columnIndex) throws SQLException { + return row.getValue(columnIndex, ByteArrayCodec.INSTANCE); + } + + @Override + public Date getDate(int columnIndex) throws SQLException { + return row.getValue(columnIndex, DateCodec.INSTANCE, null); + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + return row.getValue(columnIndex, TimeCodec.INSTANCE, null); + } + + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + return row.getValue(columnIndex, TimestampCodec.INSTANCE, null); + } + + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException { + return row.getValue(columnIndex, StreamCodec.INSTANCE); + } + + @Override + @Deprecated + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + return row.getValue(columnIndex, StreamCodec.INSTANCE); + } + + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException { + return row.getValue(columnIndex, StreamCodec.INSTANCE); + } + + @Override + public String getString(String columnLabel) throws SQLException { + return row.getValue(columnLabel, StringCodec.INSTANCE); + } + + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + Boolean b = row.getValue(columnLabel, BooleanCodec.INSTANCE); + return (b == null) ? false : b; + } + + @Override + public byte getByte(String columnLabel) throws SQLException { + Byte b = row.getValue(columnLabel, ByteCodec.INSTANCE); + return (b == null) ? 0 : b; + } + + @Override + public short getShort(String columnLabel) throws SQLException { + Short b = row.getValue(columnLabel, ShortCodec.INSTANCE); + return (b == null) ? 0 : b; + } + + @Override + public int getInt(String columnLabel) throws SQLException { + Integer b = row.getValue(columnLabel, IntCodec.INSTANCE); + return (b == null) ? 0 : b; + } + + @Override + public long getLong(String columnLabel) throws SQLException { + Long b = row.getValue(columnLabel, LongCodec.INSTANCE); + return (b == null) ? 0L : b; + } + + @Override + public float getFloat(String columnLabel) throws SQLException { + Float b = row.getValue(columnLabel, FloatCodec.INSTANCE); + return (b == null) ? 0F : b; + } + + @Override + public double getDouble(String columnLabel) throws SQLException { + Double b = row.getValue(columnLabel, DoubleCodec.INSTANCE); + return (b == null) ? 0D : b; + } + + @Override + @Deprecated + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + BigDecimal d = row.getValue(columnLabel, BigDecimalCodec.INSTANCE); + if (d == null) return d; + return d.setScale(scale, BigDecimal.ROUND_HALF_DOWN); + } + + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + return row.getValue(columnLabel, ByteArrayCodec.INSTANCE); + } + + @Override + public Date getDate(String columnLabel) throws SQLException { + return row.getValue(columnLabel, DateCodec.INSTANCE, null); + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + return row.getValue(columnLabel, TimeCodec.INSTANCE, null); + } + + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + return row.getValue(columnLabel, TimestampCodec.INSTANCE, null); + } + + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + return row.getValue(columnLabel, StreamCodec.INSTANCE); + } + + @Override + @Deprecated + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + return row.getValue(columnLabel, StreamCodec.INSTANCE); + } + + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + return row.getValue(columnLabel, StreamCodec.INSTANCE); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + if (this.statement == null) { + return null; + } + return this.statement.getWarnings(); + } + + @Override + public void clearWarnings() throws SQLException { + if (this.statement != null) { + this.statement.clearWarnings(); + } + } + + @Override + public String getCursorName() throws SQLException { + throw exceptionFactory.notSupported("Cursors are not supported"); + } + + @Override + public ResultSetMetaData getMetaData() { + return new ResultSetMetaData( + exceptionFactory, metadataList, context.getConf(), forceAlias, false); + } + + @Override + public Object getObject(int columnIndex) throws SQLException { + if (columnIndex < 1 || columnIndex > maxIndex) { + throw new SQLException( + String.format( + "Wrong index position. Is %s but must be in 1-%s range", columnIndex, maxIndex)); + } + Codec defaultCodec = metadataList[columnIndex - 1].getDefaultCodec(context.getConf()); + return row.getValue(columnIndex, defaultCodec, null); + } + + @Override + public Object getObject(String columnLabel) throws SQLException { + return getObject(row.getIndex(columnLabel)); + } + + @Override + public int findColumn(String columnLabel) throws SQLException { + return row.getIndex(columnLabel); + } + + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException { + return row.getValue(columnIndex, ReaderCodec.INSTANCE); + } + + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException { + return row.getValue(columnLabel, ReaderCodec.INSTANCE); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + return row.getValue(columnIndex, BigDecimalCodec.INSTANCE); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + return row.getValue(columnLabel, BigDecimalCodec.INSTANCE); + } + + protected void checkClose() throws SQLException { + if (closed) { + throw exceptionFactory.create("Operation not permit on a closed resultSet", "HY000"); + } + } + + protected void checkNotForwardOnly() throws SQLException { + if (resultSetType == ResultSet.TYPE_FORWARD_ONLY) { + throw exceptionFactory.create("Operation not permit on TYPE_FORWARD_ONLY resultSet", "HY000"); + } + } + + @Override + public boolean isBeforeFirst() throws SQLException { + checkClose(); + return rowPointer == -1 && dataSize > 0; + } + + @Override + public abstract boolean isAfterLast() throws SQLException; + + @Override + public abstract boolean isFirst() throws SQLException; + + @Override + public abstract boolean isLast() throws SQLException; + + @Override + public abstract void beforeFirst() throws SQLException; + + @Override + public abstract void afterLast() throws SQLException; + + @Override + public abstract boolean first() throws SQLException; + + @Override + public abstract boolean last() throws SQLException; + + @Override + public abstract int getRow() throws SQLException; + + @Override + public abstract boolean absolute(int row) throws SQLException; + + @Override + public abstract boolean relative(int rows) throws SQLException; + + @Override + public abstract boolean previous() throws SQLException; + + @Override + public int getFetchDirection() { + return FETCH_UNKNOWN; + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + if (direction == FETCH_REVERSE) { + throw exceptionFactory.create( + "Invalid operation. Allowed direction are ResultSet.FETCH_FORWARD and ResultSet.FETCH_UNKNOWN"); + } + } + + @Override + public int getType() { + return resultSetType; + } + + @Override + public int getConcurrency() { + return CONCUR_READ_ONLY; + } + + @Override + public boolean rowUpdated() throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public boolean rowInserted() throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public boolean rowDeleted() throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateNull(int columnIndex) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateString(int columnIndex, String x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateNull(String columnLabel) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateString(String columnLabel, String x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) + throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) + throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void insertRow() throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateRow() throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void deleteRow() throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void refreshRow() throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void cancelRowUpdates() throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void moveToInsertRow() throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void moveToCurrentRow() throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public Statement getStatement() throws SQLException { + return statement; + } + + public void setStatement(Statement stmt) throws SQLException { + statement = stmt; + } + + public void useAliasAsName() { + for (ColumnDefinitionPacket packet : metadataList) { + packet.useAliasAsName(); + } + forceAlias = true; + } + + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { + throw exceptionFactory.notSupported( + "Method ResultSet.getObject(int columnIndex, Map> map) not supported"); + } + + @Override + public Ref getRef(int columnIndex) throws SQLException { + throw exceptionFactory.notSupported("Method ResultSet.getRef not supported"); + } + + @Override + public Blob getBlob(int columnIndex) throws SQLException { + return row.getValue(columnIndex, BlobCodec.INSTANCE); + } + + @Override + public Clob getClob(int columnIndex) throws SQLException { + return row.getValue(columnIndex, ClobCodec.INSTANCE); + } + + @Override + public Array getArray(int columnIndex) throws SQLException { + throw exceptionFactory.notSupported("Method ResultSet.getArray not supported"); + } + + @Override + public Object getObject(String columnLabel, Map> map) throws SQLException { + throw exceptionFactory.notSupported( + "Method ResultSet.getObject(String columnLabel, Map> map) not supported"); + } + + @Override + public Ref getRef(String columnLabel) throws SQLException { + throw exceptionFactory.notSupported("Method ResultSet.getRef not supported"); + } + + @Override + public Blob getBlob(String columnLabel) throws SQLException { + return row.getValue(columnLabel, BlobCodec.INSTANCE); + } + + @Override + public Clob getClob(String columnLabel) throws SQLException { + return row.getValue(columnLabel, ClobCodec.INSTANCE); + } + + @Override + public Array getArray(String columnLabel) throws SQLException { + throw exceptionFactory.notSupported("Method ResultSet.getArray not supported"); + } + + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + return row.getValue(columnIndex, DateCodec.INSTANCE, cal); + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + return row.getValue(columnLabel, DateCodec.INSTANCE, cal); + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + return row.getValue(columnIndex, TimeCodec.INSTANCE, cal); + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + return row.getValue(columnLabel, TimeCodec.INSTANCE, cal); + } + + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + return row.getValue(columnIndex, TimestampCodec.INSTANCE, cal); + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + return row.getValue(columnLabel, TimestampCodec.INSTANCE, cal); + } + + @Override + public URL getURL(int columnIndex) throws SQLException { + String s = row.getValue(columnIndex, StringCodec.INSTANCE); + if (s == null) return null; + try { + return new URL(s); + } catch (MalformedURLException e) { + throw exceptionFactory.create(String.format("Could not parse '%s' as URL", s)); + } + } + + @Override + public URL getURL(String columnLabel) throws SQLException { + return getURL(row.getIndex(columnLabel)); + } + + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + throw exceptionFactory.notSupported("Method ResultSet.updateRef not supported"); + } + + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + throw exceptionFactory.notSupported("Method ResultSet.updateRef not supported"); + } + + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + throw exceptionFactory.notSupported("Array are not supported"); + } + + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + throw exceptionFactory.notSupported("Array are not supported"); + } + + @Override + public RowId getRowId(int columnIndex) throws SQLException { + throw exceptionFactory.notSupported("RowId are not supported"); + } + + @Override + public RowId getRowId(String columnLabel) throws SQLException { + throw exceptionFactory.notSupported("RowId are not supported"); + } + + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + throw exceptionFactory.notSupported("RowId are not supported"); + } + + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + throw exceptionFactory.notSupported("RowId are not supported"); + } + + @Override + public int getHoldability() throws SQLException { + return ResultSet.HOLD_CURSORS_OVER_COMMIT; + } + + @Override + public boolean isClosed() throws SQLException { + return closed; + } + + @Override + public void updateNString(int columnIndex, String nString) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateNString(String columnLabel, String nString) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateNClob(int columnIndex, NClob nClob) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateNClob(String columnLabel, NClob nClob) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public NClob getNClob(int columnIndex) throws SQLException { + return (NClob) row.getValue(columnIndex, ClobCodec.INSTANCE); + } + + @Override + public NClob getNClob(String columnLabel) throws SQLException { + return (NClob) row.getValue(columnLabel, ClobCodec.INSTANCE); + } + + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException { + throw exceptionFactory.notSupported("Method ResultSet.getSQLXML not supported"); + } + + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException { + throw exceptionFactory.notSupported("Method ResultSet.getSQLXML not supported"); + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public String getNString(int columnIndex) throws SQLException { + return row.getValue(columnIndex, StringCodec.INSTANCE); + } + + @Override + public String getNString(String columnLabel) throws SQLException { + return row.getValue(columnLabel, StringCodec.INSTANCE); + } + + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException { + return row.getValue(columnIndex, ReaderCodec.INSTANCE); + } + + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException { + return row.getValue(columnLabel, ReaderCodec.INSTANCE); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) + throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) + throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) + throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) + throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) + throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) + throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + return row.getValue(columnIndex, type, null); + } + + @Override + public T getObject(String columnLabel, Class type) throws SQLException { + return getObject(row.getIndex(columnLabel), type); + } + + @Override + public T unwrap(Class iface) throws SQLException { + if (isWrapperFor(iface)) { + return iface.cast(this); + } + throw new SQLException("The receiver is not a wrapper for " + iface.getName()); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return iface.isInstance(this); + } + + @Override + public void updateObject(int columnIndex, Object x, SQLType targetSqlType, int scaleOrLength) + throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateObject(String columnLabel, Object x, SQLType targetSqlType, int scaleOrLength) + throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateObject(int columnIndex, Object x, SQLType targetSqlType) throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } + + @Override + public void updateObject(String columnLabel, Object x, SQLType targetSqlType) + throws SQLException { + throw exceptionFactory.notSupported("Not supported when using CONCUR_READ_ONLY concurrency"); + } +} diff --git a/src/main/java/org/mariadb/jdbc/MariaDbResultSetMetaData.java b/src/main/java/org/mariadb/jdbc/client/result/ResultSetMetaData.java similarity index 74% rename from src/main/java/org/mariadb/jdbc/MariaDbResultSetMetaData.java rename to src/main/java/org/mariadb/jdbc/client/result/ResultSetMetaData.java index d5438f603..3099cc7c1 100644 --- a/src/main/java/org/mariadb/jdbc/MariaDbResultSetMetaData.java +++ b/src/main/java/org/mariadb/jdbc/client/result/ResultSetMetaData.java @@ -20,39 +20,41 @@ * */ -package org.mariadb.jdbc; +package org.mariadb.jdbc.client.result; -import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Types; -import org.mariadb.jdbc.internal.ColumnType; -import org.mariadb.jdbc.internal.com.read.resultset.ColumnDefinition; -import org.mariadb.jdbc.internal.util.constant.ColumnFlags; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; -import org.mariadb.jdbc.util.Options; +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.util.constants.ColumnFlags; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; -public class MariaDbResultSetMetaData implements ResultSetMetaData { +public class ResultSetMetaData implements java.sql.ResultSetMetaData { - private final ColumnDefinition[] fieldPackets; - private final Options options; + private final ExceptionFactory exceptionFactory; + private final ColumnDefinitionPacket[] fieldPackets; + private final Configuration conf; private final boolean forceAlias; private final boolean updatable; /** * Constructor. * + * @param exceptionFactory default exception handler * @param fieldPackets column informations - * @param options connection options + * @param conf connection options * @param forceAlias force table and column name alias as original data * @param updatable is column updatable */ - public MariaDbResultSetMetaData( - final ColumnDefinition[] fieldPackets, - final Options options, + public ResultSetMetaData( + final ExceptionFactory exceptionFactory, + final ColumnDefinitionPacket[] fieldPackets, + final Configuration conf, final boolean forceAlias, final boolean updatable) { + this.exceptionFactory = exceptionFactory; this.fieldPackets = fieldPackets; - this.options = options; + this.conf = conf; this.forceAlias = forceAlias; this.updatable = updatable; } @@ -74,7 +76,7 @@ public int getColumnCount() { * @throws SQLException if a database access error occurs */ public boolean isAutoIncrement(final int column) throws SQLException { - return (getColumnInformation(column).getFlags() & ColumnFlags.AUTO_INCREMENT) != 0; + return (getColumn(column).getFlags() & ColumnFlags.AUTO_INCREMENT) != 0; } /** @@ -85,7 +87,7 @@ public boolean isAutoIncrement(final int column) throws SQLException { * @throws SQLException if a database access error occurs */ public boolean isCaseSensitive(final int column) throws SQLException { - return (getColumnInformation(column).getFlags() & ColumnFlags.BINARY_COLLATION) != 0; + return true; } /** @@ -117,10 +119,10 @@ public boolean isCurrency(final int column) { * @throws SQLException if a database access error occurs */ public int isNullable(final int column) throws SQLException { - if ((getColumnInformation(column).getFlags() & ColumnFlags.NOT_NULL) == 0) { - return ResultSetMetaData.columnNullable; + if ((getColumn(column).getFlags() & ColumnFlags.NOT_NULL) == 0) { + return java.sql.ResultSetMetaData.columnNullable; } else { - return ResultSetMetaData.columnNoNulls; + return java.sql.ResultSetMetaData.columnNoNulls; } } @@ -132,7 +134,7 @@ public int isNullable(final int column) throws SQLException { * @throws SQLException if a database access error occurs */ public boolean isSigned(int column) throws SQLException { - return getColumnInformation(column).isSigned(); + return getColumn(column).isSigned(); } /** @@ -143,7 +145,7 @@ public boolean isSigned(int column) throws SQLException { * @throws SQLException if a database access error occurs */ public int getColumnDisplaySize(final int column) throws SQLException { - return getColumnInformation(column).getDisplaySize(); + return getColumn(column).getDisplaySize(); } /** @@ -157,20 +159,21 @@ public int getColumnDisplaySize(final int column) throws SQLException { * @throws SQLException if a database access error occurs */ public String getColumnLabel(final int column) throws SQLException { - return getColumnInformation(column).getName(); + return getColumn(column).getColumnAlias(); } /** * Get the designated column's name. * - * @param column the first column is 1, the second is 2, ... + * @param idx the first column is 1, the second is 2, ... * @return column name * @throws SQLException if a database access error occurs */ - public String getColumnName(final int column) throws SQLException { - String columnName = getColumnInformation(column).getOriginalName(); - if ("".equals(columnName) || options.useOldAliasMetadataBehavior || forceAlias) { - return getColumnLabel(column); + public String getColumnName(final int idx) throws SQLException { + ColumnDefinitionPacket column = getColumn(idx); + String columnName = column.getColumn(); + if ("".equals(columnName) || forceAlias) { + return column.getColumnAlias(); } return columnName; } @@ -183,7 +186,7 @@ public String getColumnName(final int column) throws SQLException { * @throws SQLException if a database access error occurs */ public String getCatalogName(int column) throws SQLException { - return getColumnInformation(column).getDatabase(); + return getColumn(column).getSchema(); } /** @@ -199,19 +202,19 @@ public String getCatalogName(int column) throws SQLException { * @throws SQLException if a database access error occurs */ public int getPrecision(final int column) throws SQLException { - return (int) getColumnInformation(column).getPrecision(); + return (int) getColumn(column).getPrecision(); } /** * Gets the designated column's number of digits to right of the decimal point. 0 is returned for * data types where the scale is not applicable. * - * @param column the first column is 1, the second is 2, ... + * @param index the first column is 1, the second is 2, ... * @return scale * @throws SQLException if a database access error occurs */ - public int getScale(final int column) throws SQLException { - return getColumnInformation(column).getDecimals(); + public int getScale(final int index) throws SQLException { + return getColumn(index).getDecimals(); } /** @@ -223,17 +226,14 @@ public int getScale(final int column) throws SQLException { */ public String getTableName(final int column) throws SQLException { if (forceAlias) { - return getColumnInformation(column).getTable(); + return getColumn(column).getTableAlias(); } - if (options.blankTableNameMeta) { + if (conf.blankTableNameMeta()) { return ""; } - if (options.useOldAliasMetadataBehavior) { - return getColumnInformation(column).getTable(); - } - return getColumnInformation(column).getOriginalTable(); + return getColumn(column).getTable(); } public String getSchemaName(int column) { @@ -249,45 +249,7 @@ public String getSchemaName(int column) { * @see Types */ public int getColumnType(final int column) throws SQLException { - ColumnDefinition ci = getColumnInformation(column); - switch (ci.getColumnType()) { - case BIT: - if (ci.getLength() == 1) { - return Types.BIT; - } - return Types.VARBINARY; - case TINYINT: - if (ci.getLength() == 1 && options.tinyInt1isBit) { - return Types.BIT; - } - return Types.TINYINT; - case YEAR: - if (options.yearIsDateType) { - return Types.DATE; - } - return Types.SMALLINT; - case BLOB: - if (ci.getLength() < 0 || ci.getLength() > 16777215) { - return Types.LONGVARBINARY; - } - return Types.VARBINARY; - case VARCHAR: - case VARSTRING: - if (ci.isBinary()) { - return Types.VARBINARY; - } - if (ci.getLength() < 0) { - return Types.LONGVARCHAR; - } - return Types.VARCHAR; - case STRING: - if (ci.isBinary()) { - return Types.BINARY; - } - return Types.CHAR; - default: - return ci.getColumnType().getSqlType(); - } + return getColumn(column).getColumnType(conf); } /** @@ -299,9 +261,7 @@ public int getColumnType(final int column) throws SQLException { * @throws SQLException if a database access error occurs */ public String getColumnTypeName(final int column) throws SQLException { - ColumnDefinition ci = getColumnInformation(column); - return ColumnType.getColumnTypeName( - ci.getColumnType(), ci.getLength(), ci.isSigned(), ci.isBinary()); + return getColumn(column).getType().name(); } /** @@ -313,9 +273,11 @@ public String getColumnTypeName(final int column) throws SQLException { */ public boolean isReadOnly(final int column) throws SQLException { if (column >= 1 && column <= fieldPackets.length) { - return !updatable; + ColumnDefinitionPacket ci = getColumn(column); + return (ci.getTable() == null || ci.getTable().isEmpty()) + && (ci.getColumn() == null || ci.getColumn().isEmpty()); } - throw ExceptionFactory.INSTANCE.create(String.format("no column with index %s", column)); + throw exceptionFactory.create(String.format("no column with index %s", column)); } /** @@ -352,17 +314,14 @@ public boolean isDefinitelyWritable(final int column) throws SQLException { * @throws SQLException if a database access error occurs */ public String getColumnClassName(int column) throws SQLException { - ColumnDefinition ci = getColumnInformation(column); - ColumnType type = ci.getColumnType(); - return ColumnType.getClassName( - type, (int) ci.getLength(), ci.isSigned(), ci.isBinary(), options); + return getColumn(column).getDefaultCodec(conf).className(); } - private ColumnDefinition getColumnInformation(int column) throws SQLException { + private ColumnDefinitionPacket getColumn(int column) throws SQLException { if (column >= 1 && column <= fieldPackets.length) { return fieldPackets[column - 1]; } - throw ExceptionFactory.INSTANCE.create("No such column"); + throw exceptionFactory.create("No such column"); } /** @@ -381,15 +340,10 @@ private ColumnDefinition getColumnInformation(int column) throws SQLException { * @throws SQLException If no object found that implements the interface */ public T unwrap(final Class iface) throws SQLException { - try { - if (isWrapperFor(iface)) { - return iface.cast(this); - } else { - throw new SQLException("The receiver is not a wrapper for " + iface.getName()); - } - } catch (Exception e) { - throw new SQLException("The receiver is not a wrapper and does not implement the interface"); + if (isWrapperFor(iface)) { + return iface.cast(this); } + throw new SQLException("The receiver is not a wrapper for " + iface.getName()); } /** diff --git a/src/main/java/org/mariadb/jdbc/client/result/StreamingResult.java b/src/main/java/org/mariadb/jdbc/client/result/StreamingResult.java new file mode 100644 index 000000000..3719b25c3 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/result/StreamingResult.java @@ -0,0 +1,373 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client.result; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.concurrent.locks.ReentrantLock; +import org.mariadb.jdbc.Statement; +import org.mariadb.jdbc.client.PacketReader; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class StreamingResult extends Result { + + private final ReentrantLock lock; + private int dataFetchTime = 0; + private int fetchSize; + + public StreamingResult( + Statement stmt, + boolean binaryProtocol, + long maxRows, + ColumnDefinitionPacket[] metadataList, + PacketReader reader, + Context context, + int fetchSize, + ReentrantLock lock, + int resultSetType, + boolean closeOnCompletion) + throws IOException, SQLException { + + super( + stmt, + binaryProtocol, + maxRows, + metadataList, + reader, + context, + resultSetType, + closeOnCompletion); + this.lock = lock; + this.dataFetchTime = 0; + this.fetchSize = fetchSize; + this.data = new ReadableByteBuf[Math.max(fetchSize, 10)]; + + addStreamingValue(); + } + + @Override + public boolean streaming() { + return true; + } + + /** + * This permit to replace current stream results by next ones. + * + * @throws SQLException if server return an unexpected error + */ + private void nextStreamingValue() throws SQLException { + + // if resultSet can be back to some previous value + if (resultSetType == TYPE_FORWARD_ONLY) { + rowPointer = 0; + dataSize = 0; + } + + addStreamingValue(); + } + + private void addStreamingValue() throws SQLException { + lock.lock(); + try { + // read only fetchSize values + int fetchSizeTmp = + (maxRows <= 0) + ? fetchSize + : Math.min(fetchSize, Math.max(0, (int) (maxRows - dataFetchTime * fetchSize))); + while (fetchSizeTmp > 0 && readNext()) { + fetchSizeTmp--; + } + dataFetchTime++; + if (maxRows > 0 && dataFetchTime * fetchSize >= maxRows && !loaded) skipRemaining(); + } catch (IOException ioe) { + throw exceptionFactory.create("Error while streaming resultSet data", "08000", ioe); + } finally { + lock.unlock(); + } + } + + /** + * When protocol has a current Streaming result (this) fetch all to permit another query is + * executing. + * + * @throws SQLException if any error occur + */ + public void fetchRemaining() throws SQLException { + if (!loaded) { + while (!loaded) { + addStreamingValue(); + } + dataFetchTime++; + } + } + + @Override + public boolean next() throws SQLException { + checkClose(); + if (rowPointer < dataSize - 1) { + rowPointer++; + row.setRow(data[rowPointer]); + return true; + } else { + if (!loaded) { + lock.lock(); + try { + if (!loaded) { + nextStreamingValue(); + } + } finally { + lock.unlock(); + } + + if (resultSetType == TYPE_FORWARD_ONLY) { + // resultSet has been cleared. next value is pointer 0. + rowPointer = 0; + if (dataSize > 0) { + row.setRow(data[rowPointer]); + return true; + } + row.setRow(null); + return false; + } else { + // cursor can move backward, so driver must keep the results. + // results have been added to current resultSet + rowPointer++; + if (dataSize > rowPointer) { + row.setRow(data[rowPointer]); + return true; + } + row.setRow(null); + return false; + } + } + + // all data are reads and pointer is after last + rowPointer = dataSize; + row.setRow(null); + return false; + } + } + + @Override + public boolean isAfterLast() throws SQLException { + checkClose(); + if (rowPointer < dataSize) { + // has remaining results + return false; + } else { + // has read all data and pointer is after last result + // so result would have to always to be true, + // but when result contain no row at all jdbc say that must return false + return dataSize > 0 || dataFetchTime > 1; + } + } + + @Override + public boolean isFirst() throws SQLException { + checkClose(); + checkNotForwardOnly(); + return rowPointer == 0 && dataSize > 0; + } + + @Override + public boolean isLast() throws SQLException { + checkClose(); + if (rowPointer < dataSize - 1) { + return false; + } else if (loaded) { + return rowPointer == dataSize - 1 && dataSize > 0; + } else { + // when streaming and not having read all results, + // must read next packet to know if next packet is an EOF packet or some additional data + if (!loaded) { + addStreamingValue(); + } + + if (loaded) { + // now driver is sure when data ends. + return rowPointer == dataSize - 1 && dataSize > 0; + } + + // There is data remaining + return false; + } + } + + @Override + public void beforeFirst() throws SQLException { + checkClose(); + checkNotForwardOnly(); + row.setRow(null); + rowPointer = -1; + } + + @Override + public void afterLast() throws SQLException { + checkClose(); + checkNotForwardOnly(); + fetchRemaining(); + row.setRow(null); + rowPointer = dataSize; + } + + @Override + public boolean first() throws SQLException { + checkClose(); + checkNotForwardOnly(); + + rowPointer = 0; + if (dataSize > 0) { + row.setRow(data[rowPointer]); + return true; + } + row.setRow(null); + return false; + } + + @Override + public boolean last() throws SQLException { + checkClose(); + fetchRemaining(); + rowPointer = dataSize - 1; + if (dataSize > 0) { + row.setRow(data[rowPointer]); + return true; + } + row.setRow(null); + return false; + } + + @Override + public int getRow() throws SQLException { + checkClose(); + if (resultSetType == TYPE_FORWARD_ONLY) { + return 0; + } + return rowPointer + 1; + } + + @Override + public boolean absolute(int idx) throws SQLException { + checkClose(); + checkNotForwardOnly(); + + if (idx == 0) { + rowPointer = -1; + row.setRow(null); + return false; + } + + if (idx > 0 && idx <= dataSize) { + rowPointer = idx - 1; + row.setRow(data[rowPointer]); + return true; + } + + // if streaming, must read additional results. + fetchRemaining(); + + if (idx > 0) { + if (idx <= dataSize) { + rowPointer = idx - 1; + row.setRow(data[rowPointer]); + return true; + } + + rowPointer = dataSize; // go to afterLast() position + row.setRow(null); + return false; + + } else { + + if (dataSize + idx >= 0) { + // absolute position reverse from ending resultSet + rowPointer = dataSize + idx; + row.setRow(data[rowPointer]); + return true; + } + row.setRow(null); + rowPointer = -1; // go to before first position + return false; + } + } + + @Override + public boolean relative(int rows) throws SQLException { + checkClose(); + int newPos = rowPointer + rows; + if (newPos <= -1) { + checkNotForwardOnly(); + rowPointer = -1; + row.setRow(null); + return false; + } + + while (newPos >= dataSize) { + if (loaded) { + rowPointer = dataSize; + row.setRow(null); + return false; + } + addStreamingValue(); + } + + rowPointer = newPos; + row.setRow(data[rowPointer]); + return true; + } + + @Override + public boolean previous() throws SQLException { + checkClose(); + checkNotForwardOnly(); + if (rowPointer > -1) { + rowPointer--; + if (rowPointer != -1) { + row.setRow(data[rowPointer]); + return true; + } + } + row.setRow(null); + return false; + } + + @Override + public int getFetchSize() { + return this.fetchSize; + } + + @Override + public void setFetchSize(int fetchSize) throws SQLException { + if (fetchSize < 0) { + throw exceptionFactory.create(String.format("invalid fetch size %s", fetchSize)); + } + if (fetchSize == 0) { + // fetch all results + while (!loaded) { + addStreamingValue(); + } + } + this.fetchSize = fetchSize; + } +} diff --git a/src/main/java/org/mariadb/jdbc/client/result/UpdatableResult.java b/src/main/java/org/mariadb/jdbc/client/result/UpdatableResult.java new file mode 100644 index 000000000..a9e0e3623 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/result/UpdatableResult.java @@ -0,0 +1,1136 @@ +package org.mariadb.jdbc.client.result; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.sql.*; +import org.mariadb.jdbc.BasePreparedStatement; +import org.mariadb.jdbc.Connection; +import org.mariadb.jdbc.Statement; +import org.mariadb.jdbc.client.PacketReader; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.BinaryRowDecoder; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.Codecs; +import org.mariadb.jdbc.codec.Parameter; +import org.mariadb.jdbc.codec.list.*; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.util.ParameterList; + +public class UpdatableResult extends CompleteResult { + + private static final int STATE_STANDARD = 0; + private static final int STATE_UPDATE = 1; + private static final int STATE_UPDATED = 2; + private static final int STATE_INSERT = 3; + + private String database; + private String table; + private boolean canInsert; + private boolean isAutoincrementPk; + private int savedRowPointer; + private boolean canUpdate; + private String changeError; + private int state = STATE_STANDARD; + private ParameterList parameters; + + public UpdatableResult( + Statement stmt, + boolean binaryProtocol, + long maxRows, + ColumnDefinitionPacket[] metadataList, + PacketReader reader, + Context context, + int resultSetType, + boolean closeOnCompletion) + throws IOException, SQLException { + super( + stmt, + binaryProtocol, + maxRows, + metadataList, + reader, + context, + resultSetType, + closeOnCompletion); + checkIfUpdatable(); + parameters = new ParameterList(metadataList.length); + } + + private void checkIfUpdatable() throws SQLException { + isAutoincrementPk = false; + canInsert = true; + canUpdate = true; + + // check that resultSet concern one table and database exactly + database = null; + table = null; + for (ColumnDefinitionPacket columnDefinition : metadataList) { + if (columnDefinition.getSchema() == null || columnDefinition.getSchema().isEmpty()) { + cannotUpdateInsertRow( + "The result-set contains fields without without any database information"); + return; + } else { + if (database != null && !database.equals(columnDefinition.getSchema())) { + cannotUpdateInsertRow("The result-set contains more than one database"); + return; + } + database = columnDefinition.getSchema(); + } + + if (columnDefinition.getTable() == null || columnDefinition.getTable().isEmpty()) { + cannotUpdateInsertRow( + "The result-set contains fields without without any table information"); + return; + } else { + if (table != null && !table.equals(columnDefinition.getTable())) { + cannotUpdateInsertRow("The result-set contains fields on different tables"); + return; + } + table = columnDefinition.getTable(); + } + } + + if (database == null || table == null) { + cannotUpdateInsertRow("The result-set does not contain any table information"); + return; + } + + // check that listed column contain primary field + for (ColumnDefinitionPacket col : metadataList) { + if (col.isPrimaryKey()) { + isAutoincrementPk = col.isAutoIncrement(); + return; + } + } + + canUpdate = false; + changeError = "Cannot update rows, since primary field is not present in query"; + + // check that table contain a generated primary field + // to check if insert are still possible + ResultSet rs = + statement + .getConnection() + .createStatement() + .executeQuery("SHOW COLUMNS FROM `" + database + "`.`" + table + "`"); + + boolean primaryFound = false; + while (rs.next()) { + if ("PRI".equals(rs.getString("Key"))) { + boolean canBeNull = "YES".equals(rs.getString("Null")); + boolean hasDefault = rs.getString("Default") != null; + boolean generated = rs.getString("Extra") != null && !rs.getString("Extra").isEmpty(); + isAutoincrementPk = + rs.getString("Extra") != null && rs.getString("Extra").contains("auto_increment"); + if (!canBeNull && !hasDefault && !generated) { + canInsert = false; + changeError = + String.format("primary field `%s` is not present in query", rs.getString("Field")); + } + return; + } + } + canInsert = false; + } + + private void cannotUpdateInsertRow(String reason) { + changeError = reason; + canUpdate = false; + canInsert = false; + } + + private void checkUpdatable(int position) throws SQLException { + if (position <= 0 || position > metadataList.length) { + throw exceptionFactory.create("No such column: " + position, "22023"); + } + + if (state == STATE_STANDARD) { + state = STATE_UPDATE; + } + if (state == STATE_UPDATE) { + if (rowPointer <= BEFORE_FIRST_POS) { + throw new SQLDataException("Current position is before the first row", "22023"); + } + if (rowPointer >= dataSize) { + throw new SQLDataException("Current position is after the last row", "22023"); + } + if (!canUpdate) { + throw exceptionFactory.create("ResultSet cannot be updated. " + changeError); + } + } + } + + @Override + public boolean rowUpdated() throws SQLException { + return super.rowUpdated(); + } + + @Override + public boolean rowInserted() throws SQLException { + return super.rowInserted(); + } + + @Override + public boolean rowDeleted() throws SQLException { + return super.rowDeleted(); + } + + @Override + public void updateNull(int columnIndex) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, Parameter.NULL_PARAMETER); + } + + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(BooleanCodec.INSTANCE, x)); + } + + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(ByteCodec.INSTANCE, x)); + } + + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(ShortCodec.INSTANCE, x)); + } + + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(IntCodec.INSTANCE, x)); + } + + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(LongCodec.INSTANCE, x)); + } + + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(FloatCodec.INSTANCE, x)); + } + + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(DoubleCodec.INSTANCE, x)); + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(BigDecimalCodec.INSTANCE, x)); + } + + @Override + public void updateString(int columnIndex, String x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(StringCodec.INSTANCE, x)); + } + + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(ByteArrayCodec.INSTANCE, x)); + } + + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(DateCodec.INSTANCE, x)); + } + + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(TimeCodec.INSTANCE, x)); + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(TimestampCodec.INSTANCE, x)); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, (long) length)); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, (long) length)); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, x, (long) length)); + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + updateInternalObject(columnIndex, x, (long) scaleOrLength); + } + + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + updateInternalObject(columnIndex, x, null); + } + + @Override + public void updateNull(String columnLabel) throws SQLException { + updateNull(row.getIndex(columnLabel)); + } + + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + updateBoolean(row.getIndex(columnLabel), x); + } + + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + updateByte(row.getIndex(columnLabel), x); + } + + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + updateShort(row.getIndex(columnLabel), x); + } + + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + updateInt(row.getIndex(columnLabel), x); + } + + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + updateLong(row.getIndex(columnLabel), x); + } + + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + updateFloat(row.getIndex(columnLabel), x); + } + + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + updateDouble(row.getIndex(columnLabel), x); + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + updateBigDecimal(row.getIndex(columnLabel), x); + } + + @Override + public void updateString(String columnLabel, String x) throws SQLException { + updateString(row.getIndex(columnLabel), x); + } + + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + updateBytes(row.getIndex(columnLabel), x); + } + + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + updateDate(row.getIndex(columnLabel), x); + } + + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + updateTime(row.getIndex(columnLabel), x); + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + updateTimestamp(row.getIndex(columnLabel), x); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + updateAsciiStream(row.getIndex(columnLabel), x, length); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) + throws SQLException { + updateBinaryStream(row.getIndex(columnLabel), x, length); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) + throws SQLException { + updateCharacterStream(row.getIndex(columnLabel), reader, length); + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + updateObject(row.getIndex(columnLabel), x, scaleOrLength); + } + + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + updateObject(row.getIndex(columnLabel), x); + } + + @Override + public void insertRow() throws SQLException { + if (state == STATE_INSERT) { + + // Create query will all field with WHERE clause contain primary field. + // if field are not updated, value DEFAULT will be set + // (if field has no default, then insert will throw an exception that will be return to + // user) + + String insertSql = buildInsertQuery(); + try (PreparedStatement insertPreparedStatement = + ((Connection) statement.getConnection()) + .prepareInternal( + insertSql, + Statement.RETURN_GENERATED_KEYS, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + row instanceof BinaryRowDecoder)) { + + int paramPos = 0; + for (int pos = 0; pos < metadataList.length; pos++) { + ColumnDefinitionPacket colInfo = metadataList[pos]; + Parameter param = parameters.size() > pos ? parameters.get(pos) : null; + if (param != null) { + ((BasePreparedStatement) insertPreparedStatement).setParameter(paramPos++, param); + } else if (!colInfo.isPrimaryKey() && !colInfo.hasDefault()) { + ((BasePreparedStatement) insertPreparedStatement) + .setParameter(paramPos++, Parameter.NULL_PARAMETER); + } + } + ResultSet insertRs = insertPreparedStatement.executeQuery(); + if (context.getVersion().isMariaDBServer() + && context.getVersion().versionGreaterOrEqual(10, 5, 1)) { + if (insertRs.next()) { + ReadableByteBuf rowByte = ((Result) insertRs).getCurrentRowData(); + addRowData(rowByte); + } + } else if (isAutoincrementPk) { + // primary is auto_increment (only one field) + ResultSet rsKey = insertPreparedStatement.getGeneratedKeys(); + if (rsKey.next()) { + try (PreparedStatement refreshPreparedStatement = prepareRefreshStmt()) { + refreshPreparedStatement.setObject(1, rsKey.getObject(1)); + Result rs = (Result) refreshPreparedStatement.executeQuery(); + // update row data only if not deleted externally + if (rs.next()) { + addRowData(rs.getCurrentRowData()); + } + } + } + } else { + addRowData(refreshRawData()); + } + } + parameters = new ParameterList(parameters.size()); + } + } + + /** + * Build insert query + * + * @return + * @throws SQLException + */ + private String buildInsertQuery() throws SQLException { + StringBuilder insertSql = new StringBuilder("INSERT `" + database + "`.`" + table + "` ( "); + StringBuilder valueClause = new StringBuilder(); + StringBuilder returningClause = new StringBuilder(); + + boolean firstParam = true; + + for (int pos = 0; pos < metadataList.length; pos++) { + ColumnDefinitionPacket colInfo = metadataList[pos]; + + if (pos != 0) { + returningClause.append(", "); + } + returningClause.append("`").append(colInfo.getColumn()).append("`"); + + Parameter param = parameters.size() > pos ? parameters.get(pos) : null; + if (param != null) { + if (!firstParam) { + insertSql.append(","); + valueClause.append(", "); + } + insertSql.append("`").append(colInfo.getColumn()).append("`"); + valueClause.append("?"); + firstParam = false; + } else { + if (colInfo.isPrimaryKey()) { + if (colInfo.isAutoIncrement() || colInfo.hasDefault()) { + if (!colInfo.isAutoIncrement() + && (!context.getVersion().isMariaDBServer() + || !context.getVersion().versionGreaterOrEqual(10, 5, 1))) { + // driver cannot know generated default value like uuid(). + // but for server 10.5+, will use RETURNING to know primary key + throw exceptionFactory.create( + String.format( + "Cannot call insertRow() not setting value for primary key %s " + + "with default value before server 10.5", + colInfo.getColumn())); + } + } else { + throw exceptionFactory.create( + String.format( + "Cannot call insertRow() not setting value for primary key %s", + colInfo.getColumn())); + } + } else if (!colInfo.hasDefault()) { + if (!firstParam) { + insertSql.append(","); + valueClause.append(", "); + } + firstParam = false; + insertSql.append("`").append(colInfo.getColumn()).append("`"); + valueClause.append("?"); + } + } + } + insertSql.append(") VALUES (").append(valueClause).append(")"); + if (context.getVersion().isMariaDBServer() + && context.getVersion().versionGreaterOrEqual(10, 5, 1)) { + insertSql.append(" RETURNING ").append(returningClause); + } + return insertSql.toString(); + } + + private String refreshStmt() { + // Construct SELECT query according to column metadata, with WHERE part containing primary + // fields + StringBuilder selectSql = new StringBuilder("SELECT "); + StringBuilder whereClause = new StringBuilder(" WHERE "); + + boolean firstPrimary = true; + for (int pos = 0; pos < metadataList.length; pos++) { + ColumnDefinitionPacket colInfo = metadataList[pos]; + if (pos != 0) { + selectSql.append(","); + } + selectSql.append("`").append(colInfo.getColumn()).append("`"); + + if (colInfo.isPrimaryKey()) { + if (!firstPrimary) { + whereClause.append("AND "); + } + firstPrimary = false; + whereClause.append("`").append(colInfo.getColumn()).append("` = ? "); + } + } + selectSql + .append(" FROM `") + .append(database) + .append("`.`") + .append(table) + .append("`") + .append(whereClause); + return selectSql.toString(); + } + + private PreparedStatement prepareRefreshStmt() throws SQLException { + // row's raw bytes must be encoded according to current resultSet type + // so use Server or Client PrepareStatement accordingly + return ((Connection) statement.getConnection()) + .prepareInternal( + refreshStmt(), + Statement.RETURN_GENERATED_KEYS, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + row instanceof BinaryRowDecoder); + } + + private ReadableByteBuf refreshRawData() throws SQLException { + int fieldsPrimaryIndex = 0; + try (PreparedStatement refreshPreparedStatement = prepareRefreshStmt()) { + for (int pos = 0; pos < metadataList.length; pos++) { + ColumnDefinitionPacket colInfo = metadataList[pos]; + if (colInfo.isPrimaryKey()) { + if (state != STATE_STANDARD && parameters.size() > pos && parameters.get(pos) != null) { + // Row has just been updated using updateRow() methods. + // updateRow might have changed primary key, so must use the new value. + Parameter value = parameters.get(pos); + ((BasePreparedStatement) refreshPreparedStatement) + .setParameter(fieldsPrimaryIndex++, value); + } else { + refreshPreparedStatement.setObject(++fieldsPrimaryIndex, getObject(pos + 1)); + } + } + } + + Result rs = (Result) refreshPreparedStatement.executeQuery(); + rs.next(); + return rs.getCurrentRowData(); + } + } + + private String updateQuery() { + StringBuilder updateSql = new StringBuilder("UPDATE `" + database + "`.`" + table + "` SET "); + StringBuilder whereClause = new StringBuilder(" WHERE "); + + boolean firstUpdate = true; + boolean firstPrimary = true; + for (int pos = 0; pos < metadataList.length; pos++) { + ColumnDefinitionPacket colInfo = metadataList[pos]; + + if (colInfo.isPrimaryKey()) { + if (!firstPrimary) { + whereClause.append("AND "); + } + firstPrimary = false; + whereClause.append("`").append(colInfo.getColumn()).append("` = ? "); + } + + if (parameters.size() > pos && parameters.get(pos) != null) { + if (!firstUpdate) { + updateSql.append(","); + } + firstUpdate = false; + updateSql.append("`").append(colInfo.getColumn()).append("` = ? "); + } + } + if (firstUpdate) return null; + return updateSql.append(whereClause.toString()).toString(); + } + + @Override + public void updateRow() throws SQLException { + + if (state == STATE_INSERT) { + throw exceptionFactory.create("Cannot call updateRow() when inserting a new row"); + } + + if (rowPointer < 0) { + throw exceptionFactory.create("Current position is before the first row", "22023"); + } + + if (rowPointer >= dataSize) { + throw exceptionFactory.create("Current position is after the last row", "22023"); + } + + if (state == STATE_UPDATE) { + + // state is STATE_UPDATE, meaning that at least one field is modified, update query can be + // run. + // Construct UPDATE query according to modified field only + String updateQuery = updateQuery(); + if (updateQuery != null) { + try (PreparedStatement preparedStatement = + ((Connection) statement.getConnection()) + .prepareInternal( + updateQuery, + Statement.RETURN_GENERATED_KEYS, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + row instanceof BinaryRowDecoder)) { + + int fieldsIndex = 0; + for (int pos = 0; pos < metadataList.length; pos++) { + if (parameters.size() > pos) { + Parameter param = parameters.get(pos); + if (param != null) { + ((BasePreparedStatement) preparedStatement).setParameter(fieldsIndex++, param); + } + } + } + + for (int pos = 0; pos < metadataList.length; pos++) { + ColumnDefinitionPacket colInfo = metadataList[pos]; + if (colInfo.isPrimaryKey()) { + ((BasePreparedStatement) preparedStatement) + .setObject(++fieldsIndex, getObject(pos + 1)); + } + } + + preparedStatement.execute(); + } + state = STATE_UPDATED; + refreshRow(); + } + parameters = new ParameterList(parameters.size()); + state = STATE_STANDARD; + } + } + + @Override + public void deleteRow() throws SQLException { + + if (state == STATE_INSERT) { + throw exceptionFactory.create("Cannot call deleteRow() when inserting a new row"); + } + if (!canUpdate) { + throw exceptionFactory.create("ResultSet cannot be updated. " + changeError); + } + if (rowPointer < 0) { + throw new SQLDataException("Current position is before the first row", "22023"); + } + if (rowPointer >= dataSize) { + throw new SQLDataException("Current position is after the last row", "22023"); + } + + // Create query with WHERE clause contain primary field. + StringBuilder deleteSql = + new StringBuilder("DELETE FROM `" + database + "`.`" + table + "` WHERE "); + boolean firstPrimary = true; + for (int pos = 0; pos < metadataList.length; pos++) { + ColumnDefinitionPacket colInfo = metadataList[pos]; + if (colInfo.isPrimaryKey()) { + if (!firstPrimary) { + deleteSql.append("AND "); + } + firstPrimary = false; + deleteSql.append("`").append(colInfo.getColumn()).append("` = ? "); + } + } + + try (PreparedStatement deletePreparedStatement = + ((Connection) statement.getConnection()) + .prepareInternal( + deleteSql.toString(), + Statement.RETURN_GENERATED_KEYS, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + false)) { + + int fieldsPrimaryIndex = 1; + for (int pos = 0; pos < metadataList.length; pos++) { + ColumnDefinitionPacket colInfo = metadataList[pos]; + if (colInfo.isPrimaryKey()) { + deletePreparedStatement.setObject(fieldsPrimaryIndex++, getObject(pos + 1)); + } + } + + deletePreparedStatement.executeUpdate(); + + // remove data + System.arraycopy(data, rowPointer + 1, data, rowPointer, dataSize - 1 - rowPointer); + data[dataSize - 1] = null; + dataSize--; + previous(); + } + } + + @Override + public void refreshRow() throws SQLException { + if (state == STATE_INSERT) { + throw exceptionFactory.create("Cannot call deleteRow() when inserting a new row"); + } + if (rowPointer < 0) { + throw exceptionFactory.create("Current position is before the first row", "22023"); + } + if (rowPointer >= data.length) { + throw exceptionFactory.create("Current position is after the last row", "22023"); + } + if (canUpdate) { + updateRowData(refreshRawData()); + } + } + + @Override + public void cancelRowUpdates() throws SQLException { + parameters = new ParameterList(parameters.size()); + state = STATE_STANDARD; + } + + @Override + public void moveToInsertRow() throws SQLException { + if (!canInsert) { + throw exceptionFactory.create("No row can be inserted. " + changeError); + } + parameters = new ParameterList(parameters.size()); + state = STATE_INSERT; + savedRowPointer = rowPointer; + } + + @Override + public void moveToCurrentRow() throws SQLException { + state = STATE_STANDARD; + resetToRowPointer(); + } + + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + super.updateRef(columnIndex, x); + } + + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + super.updateRef(columnLabel, x); + } + + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(BlobCodec.INSTANCE, x)); + } + + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + updateBlob(row.getIndex(columnLabel), x); + } + + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(ClobCodec.INSTANCE, x)); + } + + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + updateClob(row.getIndex(columnLabel), x); + } + + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + super.updateArray(columnIndex, x); + } + + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + super.updateArray(columnLabel, x); + } + + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + super.updateRowId(columnIndex, x); + } + + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + super.updateRowId(columnLabel, x); + } + + @Override + public void updateNString(int columnIndex, String nString) throws SQLException { + updateString(columnIndex, nString); + } + + @Override + public void updateNString(String columnLabel, String nString) throws SQLException { + updateString(columnLabel, nString); + } + + @Override + public void updateNClob(int columnIndex, NClob nClob) throws SQLException { + updateClob(columnIndex, nClob); + } + + @Override + public void updateNClob(String columnLabel, NClob nClob) throws SQLException { + updateClob(columnLabel, nClob); + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + super.updateSQLXML(columnIndex, xmlObject); + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + super.updateSQLXML(columnLabel, xmlObject); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + updateCharacterStream(columnIndex, x, length); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) + throws SQLException { + updateCharacterStream(columnLabel, reader, length); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, length)); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, length)); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, x, length)); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) + throws SQLException { + updateAsciiStream(row.getIndex(columnLabel), x, length); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) + throws SQLException { + updateBinaryStream(row.getIndex(columnLabel), x, length); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) + throws SQLException { + updateCharacterStream(row.getIndex(columnLabel), reader, length); + } + + @Override + public void updateBlob(int columnIndex, InputStream x, long length) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, length)); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) + throws SQLException { + updateBlob(row.getIndex(columnLabel), inputStream, length); + } + + @Override + public void updateClob(int columnIndex, Reader x, long length) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, x, length)); + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + super.updateClob(row.getIndex(columnLabel), reader, length); + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { + updateClob(columnIndex, reader, length); + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { + updateClob(columnLabel, reader, length); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + updateCharacterStream(columnIndex, x); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + updateCharacterStream(columnLabel, reader); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x)); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x)); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, x)); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + updateAsciiStream(row.getIndex(columnLabel), x); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + updateBinaryStream(row.getIndex(columnLabel), x); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + updateCharacterStream(row.getIndex(columnLabel), reader); + } + + @Override + public void updateBlob(int columnIndex, InputStream x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x)); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + updateBlob(row.getIndex(columnLabel), inputStream); + } + + @Override + public void updateClob(int columnIndex, Reader x) throws SQLException { + checkUpdatable(columnIndex); + parameters.set(columnIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, x)); + } + + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException { + updateClob(row.getIndex(columnLabel), reader); + } + + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + updateClob(columnIndex, reader); + } + + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException { + updateClob(columnLabel, reader); + } + + @Override + public void updateObject(int columnIndex, Object x, SQLType targetSqlType, int scaleOrLength) + throws SQLException { + updateInternalObject(columnIndex, x, (long) scaleOrLength); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void updateInternalObject(int columnIndex, Object x, Long scaleOrLength) + throws SQLException { + checkUpdatable(columnIndex); + if (x == null) { + parameters.set(columnIndex - 1, Parameter.NULL_PARAMETER); + return; + } + + for (Codec codec : Codecs.LIST) { + if (codec.canEncode(x)) { + Parameter p = new Parameter(codec, x, scaleOrLength); + parameters.set(columnIndex - 1, p); + return; + } + } + + throw new SQLException(String.format("Type %s not supported type", x.getClass().getName())); + } + + @Override + public void updateObject(String columnLabel, Object x, SQLType targetSqlType, int scaleOrLength) + throws SQLException { + updateObject(row.getIndex(columnLabel), x, targetSqlType, scaleOrLength); + } + + @Override + public void updateObject(int columnIndex, Object x, SQLType targetSqlType) throws SQLException { + updateInternalObject(columnIndex, x, null); + } + + @Override + public void updateObject(String columnLabel, Object x, SQLType targetSqlType) + throws SQLException { + updateObject(row.getIndex(columnLabel), x, targetSqlType); + } + + @Override + public int getConcurrency() { + return CONCUR_UPDATABLE; + } + + private void resetToRowPointer() { + rowPointer = savedRowPointer; + if (rowPointer != BEFORE_FIRST_POS && rowPointer < dataSize - 1) { + row.setRow(data[rowPointer]); + } else { + // all data are reads and pointer is after last + row.setRow(null); + } + savedRowPointer = -1; + } + + @Override + public void beforeFirst() throws SQLException { + if (state == STATE_INSERT) { + state = STATE_UPDATE; + resetToRowPointer(); + } + super.beforeFirst(); + } + + @Override + public boolean first() throws SQLException { + if (state == STATE_INSERT) { + state = STATE_UPDATE; + resetToRowPointer(); + } + return super.first(); + } + + @Override + public boolean last() throws SQLException { + if (state == STATE_INSERT) { + state = STATE_UPDATE; + resetToRowPointer(); + } + return super.last(); + } + + @Override + public void afterLast() throws SQLException { + if (state == STATE_INSERT) { + state = STATE_UPDATE; + resetToRowPointer(); + } + super.afterLast(); + } + + @Override + public boolean absolute(int row) throws SQLException { + if (state == STATE_INSERT) { + state = STATE_UPDATE; + resetToRowPointer(); + } + return super.absolute(row); + } + + @Override + public boolean relative(int rows) throws SQLException { + if (state == STATE_INSERT) { + state = STATE_UPDATE; + resetToRowPointer(); + } + return super.relative(rows); + } + + @Override + public boolean next() throws SQLException { + if (state == STATE_INSERT) { + state = STATE_UPDATE; + resetToRowPointer(); + } + return super.next(); + } + + @Override + public boolean previous() throws SQLException { + if (state == STATE_INSERT) { + state = STATE_UPDATE; + resetToRowPointer(); + } + return super.previous(); + } +} diff --git a/src/main/java/org/mariadb/jdbc/internal/io/socket/NamedPipeSocket.java b/src/main/java/org/mariadb/jdbc/client/socket/NamedPipeSocket.java similarity index 73% rename from src/main/java/org/mariadb/jdbc/internal/io/socket/NamedPipeSocket.java rename to src/main/java/org/mariadb/jdbc/client/socket/NamedPipeSocket.java index 8c28965ef..a35681356 100644 --- a/src/main/java/org/mariadb/jdbc/internal/io/socket/NamedPipeSocket.java +++ b/src/main/java/org/mariadb/jdbc/client/socket/NamedPipeSocket.java @@ -1,5 +1,4 @@ /* - * * MariaDB Client for Java * * Copyright (c) 2012-2014 Monty Program Ab. @@ -18,39 +17,9 @@ * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to Monty Program Ab info@montyprogram.com. * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * */ -package org.mariadb.jdbc.internal.io.socket; +package org.mariadb.jdbc.client.socket; import com.sun.jna.platform.win32.Kernel32; import java.io.*; diff --git a/src/main/java/org/mariadb/jdbc/client/socket/SocketHandlerFunction.java b/src/main/java/org/mariadb/jdbc/client/socket/SocketHandlerFunction.java new file mode 100644 index 000000000..9cb563dc9 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/socket/SocketHandlerFunction.java @@ -0,0 +1,32 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client.socket; + +import java.io.IOException; +import java.net.Socket; +import org.mariadb.jdbc.Configuration; + +@FunctionalInterface +public interface SocketHandlerFunction { + + Socket apply(Configuration conf, String host) throws IOException; +} diff --git a/src/main/java/org/mariadb/jdbc/client/socket/SocketUtility.java b/src/main/java/org/mariadb/jdbc/client/socket/SocketUtility.java new file mode 100644 index 000000000..808844879 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/socket/SocketUtility.java @@ -0,0 +1,60 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client.socket; + +import com.sun.jna.Platform; +import java.io.IOException; +import org.mariadb.jdbc.client.ConnectionHelper; + +public class SocketUtility { + + /** + * Create socket according to options. In case of compilation ahead of time, will throw an error + * if dependencies found, then use default socket implementation. + * + * @return Socket + */ + @SuppressWarnings("unchecked") + public static SocketHandlerFunction getSocketHandler() { + try { + // forcing use of JNA to ensure AOT compilation + Platform.getOSType(); + + return (conf, host) -> { + if (conf.pipe() != null) { + return new NamedPipeSocket(host, conf.pipe()); + } else if (conf.localSocket() != null) { + try { + return new UnixDomainSocket(conf.localSocket()); + } catch (RuntimeException re) { + throw new IOException(re.getMessage(), re.getCause()); + } + } else { + return ConnectionHelper.standardSocket(conf, host); + } + }; + } catch (Throwable cle) { + // jna jar's are not in classpath + } + return (options, host) -> ConnectionHelper.standardSocket(options, host); + } +} diff --git a/src/main/java/org/mariadb/jdbc/internal/io/socket/UnixDomainSocket.java b/src/main/java/org/mariadb/jdbc/client/socket/UnixDomainSocket.java similarity index 81% rename from src/main/java/org/mariadb/jdbc/internal/io/socket/UnixDomainSocket.java rename to src/main/java/org/mariadb/jdbc/client/socket/UnixDomainSocket.java index ad4e8ebf5..6b29f4e39 100644 --- a/src/main/java/org/mariadb/jdbc/internal/io/socket/UnixDomainSocket.java +++ b/src/main/java/org/mariadb/jdbc/client/socket/UnixDomainSocket.java @@ -1,5 +1,4 @@ /* - * * MariaDB Client for Java * * Copyright (c) 2012-2014 Monty Program Ab. @@ -18,39 +17,9 @@ * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to Monty Program Ab info@montyprogram.com. * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * */ -package org.mariadb.jdbc.internal.io.socket; +package org.mariadb.jdbc.client.socket; import com.sun.jna.LastErrorException; import com.sun.jna.Native; diff --git a/src/main/java/org/mariadb/jdbc/internal/protocol/tls/DefaultTlsSocketPlugin.java b/src/main/java/org/mariadb/jdbc/client/tls/DefaultTlsSocketPlugin.java similarity index 62% rename from src/main/java/org/mariadb/jdbc/internal/protocol/tls/DefaultTlsSocketPlugin.java rename to src/main/java/org/mariadb/jdbc/client/tls/DefaultTlsSocketPlugin.java index 74189aeef..a59a9f8a2 100644 --- a/src/main/java/org/mariadb/jdbc/internal/protocol/tls/DefaultTlsSocketPlugin.java +++ b/src/main/java/org/mariadb/jdbc/client/tls/DefaultTlsSocketPlugin.java @@ -20,7 +20,7 @@ * */ -package org.mariadb.jdbc.internal.protocol.tls; +package org.mariadb.jdbc.client.tls; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -35,18 +35,23 @@ import java.security.cert.X509Certificate; import java.sql.SQLException; import javax.net.ssl.*; -import org.mariadb.jdbc.internal.logging.Logger; -import org.mariadb.jdbc.internal.logging.LoggerFactory; -import org.mariadb.jdbc.internal.util.exceptions.ExceptionFactory; -import org.mariadb.jdbc.tls.TlsSocketPlugin; -import org.mariadb.jdbc.util.Options; +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.plugin.tls.TlsSocketPlugin; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; +import org.mariadb.jdbc.util.log.Logger; +import org.mariadb.jdbc.util.log.Loggers; public class DefaultTlsSocketPlugin implements TlsSocketPlugin { - private static final Logger logger = LoggerFactory.getLogger(DefaultTlsSocketPlugin.class); + private static final Logger logger = Loggers.getLogger(DefaultTlsSocketPlugin.class); private static KeyManager loadClientCerts( - String keyStoreUrl, String keyStorePassword, String keyPassword, String storeType) + String keyStoreUrl, + String keyStorePassword, + String keyPassword, + String storeType, + ExceptionFactory exceptionFactory) throws SQLException { + InputStream inStream = null; try { @@ -65,13 +70,13 @@ private static KeyManager loadClientCerts( (keyPassword == null) ? keyStorePasswordChars : keyPassword.toCharArray(); return new MariaDbX509KeyManager(ks, keyStoreChars); } catch (GeneralSecurityException generalSecurityEx) { - throw ExceptionFactory.INSTANCE.create( + throw exceptionFactory.create( "Failed to create keyStore instance", "08000", generalSecurityEx); } catch (FileNotFoundException fileNotFoundEx) { - throw ExceptionFactory.INSTANCE.create( + throw exceptionFactory.create( "Failed to find keyStore file. Option keyStore=" + keyStoreUrl, "08000", fileNotFoundEx); } catch (IOException ioEx) { - throw ExceptionFactory.INSTANCE.create( + throw exceptionFactory.create( "Failed to read keyStore file. Option keyStore=" + keyStoreUrl, "08000", ioEx); } finally { try { @@ -95,57 +100,60 @@ public String type() { } @Override - public SSLSocketFactory getSocketFactory(Options options) throws SQLException { + public SSLSocketFactory getSocketFactory(Configuration conf, ExceptionFactory exceptionFactory) + throws SQLException { TrustManager[] trustManager = null; KeyManager[] keyManager = null; - if (options.trustServerCertificate - || options.serverSslCert != null - || options.trustStore != null) { - trustManager = new X509TrustManager[] {new MariaDbX509TrustManager(options)}; - } - - if (options.keyStore != null) { - keyManager = - new KeyManager[] { - loadClientCerts( - options.keyStore, - options.keyStorePassword, - options.keyPassword, - options.keyStoreType) - }; - } else { - String keyStore = System.getProperty("javax.net.ssl.keyStore"); - String keyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword"); - if (keyStore != null) { - try { - keyManager = - new KeyManager[] { - loadClientCerts(keyStore, keyStorePassword, keyStorePassword, options.keyStoreType) - }; - } catch (SQLException queryException) { - keyManager = null; - logger.error("Error loading keymanager from system properties", queryException); - } - } + if (conf.serverSslCert() != null) { + trustManager = new X509TrustManager[] {new MariaDbX509TrustManager(conf, exceptionFactory)}; } + // if (conf.keyStore != null) { + // keyManager = + // new KeyManager[] { + // loadClientCerts( + // options.keyStore, + // options.keyStorePassword, + // options.keyPassword, + // options.keyStoreType, + // exceptionFactory) + // }; + // } else { + // String keyStore = System.getProperty("javax.net.ssl.keyStore"); + // String keyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword"); + // if (keyStore != null) { + // try { + // keyManager = + // new KeyManager[] { + // loadClientCerts( + // keyStore, + // keyStorePassword, + // keyStorePassword, + // options.keyStoreType, + // exceptionFactory) + // }; + // } catch (SQLException queryException) { + // keyManager = null; + // logger.error("Error loading keymanager from system properties", queryException); + // } + // } + // } try { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManager, trustManager, null); return sslContext.getSocketFactory(); } catch (KeyManagementException keyManagementEx) { - throw ExceptionFactory.INSTANCE.create( - "Could not initialize SSL context", "08000", keyManagementEx); + throw exceptionFactory.create("Could not initialize SSL context", "08000", keyManagementEx); } catch (NoSuchAlgorithmException noSuchAlgorithmEx) { - throw ExceptionFactory.INSTANCE.create( + throw exceptionFactory.create( "SSLContext TLS Algorithm not unknown", "08000", noSuchAlgorithmEx); } } @Override - public void verify(String host, SSLSession session, Options options, long serverThreadId) + public void verify(String host, SSLSession session, Configuration conf, long serverThreadId) throws SSLException { HostnameVerifierImpl hostnameVerifier = new HostnameVerifierImpl(); if (!hostnameVerifier.verify(host, session, serverThreadId)) { diff --git a/src/main/java/org/mariadb/jdbc/internal/protocol/tls/HostnameVerifierImpl.java b/src/main/java/org/mariadb/jdbc/client/tls/HostnameVerifierImpl.java similarity index 85% rename from src/main/java/org/mariadb/jdbc/internal/protocol/tls/HostnameVerifierImpl.java rename to src/main/java/org/mariadb/jdbc/client/tls/HostnameVerifierImpl.java index 8650c1acd..4af145d78 100644 --- a/src/main/java/org/mariadb/jdbc/internal/protocol/tls/HostnameVerifierImpl.java +++ b/src/main/java/org/mariadb/jdbc/client/tls/HostnameVerifierImpl.java @@ -1,5 +1,4 @@ /* - * * MariaDB Client for Java * * Copyright (c) 2012-2014 Monty Program Ab. @@ -18,39 +17,9 @@ * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to Monty Program Ab info@montyprogram.com. * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * */ -package org.mariadb.jdbc.internal.protocol.tls; +package org.mariadb.jdbc.client.tls; import java.net.InetAddress; import java.net.UnknownHostException; @@ -58,6 +27,7 @@ import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.*; +import java.util.regex.Pattern; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; @@ -65,13 +35,22 @@ import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import javax.security.auth.x500.X500Principal; -import org.mariadb.jdbc.internal.logging.Logger; -import org.mariadb.jdbc.internal.logging.LoggerFactory; -import org.mariadb.jdbc.internal.util.Utils; +import org.mariadb.jdbc.util.log.Logger; +import org.mariadb.jdbc.util.log.Loggers; public class HostnameVerifierImpl implements HostnameVerifier { - private static final Logger logger = LoggerFactory.getLogger(HostnameVerifierImpl.class); + private static final Logger logger = Loggers.getLogger(HostnameVerifierImpl.class); + private static final Pattern IP_V4 = + Pattern.compile( + "^(([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){1}" + + "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){2}" + + "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"); + private static final Pattern IP_V6 = Pattern.compile("^[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){7}$"); + private static final Pattern IP_V6_COMPRESSED = + Pattern.compile( + "^(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,5})?)" + + "::(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,5})?)$"); /** * DNS verification : Matching is performed using the matching rules specified by [RFC2459]. If @@ -86,7 +65,7 @@ public class HostnameVerifierImpl implements HostnameVerifier { * @return true if matching */ private static boolean matchDns(String hostname, String tlsDnsPattern) throws SSLException { - boolean hostIsIp = Utils.isIPv4(hostname) || Utils.isIPv6(hostname); + boolean hostIsIp = isIPv4(hostname) || isIPv6(hostname); StringTokenizer hostnameSt = new StringTokenizer(hostname.toLowerCase(Locale.ROOT), "."); StringTokenizer templateSt = new StringTokenizer(tlsDnsPattern.toLowerCase(Locale.ROOT), "."); if (hostnameSt.countTokens() != templateSt.countTokens()) { @@ -177,9 +156,9 @@ private static String normaliseAddress(String hostname) { private static String normalizedHostMsg(String normalizedHost) { StringBuilder msg = new StringBuilder(); - if (Utils.isIPv4(normalizedHost)) { + if (isIPv4(normalizedHost)) { msg.append("IPv4 host \""); - } else if (Utils.isIPv6(normalizedHost)) { + } else if (isIPv6(normalizedHost)) { msg.append("IPv6 host \""); } else { msg.append("DNS host \""); @@ -188,6 +167,14 @@ private static String normalizedHostMsg(String normalizedHost) { return msg.toString(); } + public static boolean isIPv4(final String ip) { + return IP_V4.matcher(ip).matches(); + } + + public static boolean isIPv6(final String ip) { + return IP_V6.matcher(ip).matches() || IP_V6_COMPRESSED.matcher(ip).matches(); + } + private SubjectAltNames getSubjectAltNames(X509Certificate cert) throws CertificateParsingException { Collection> entries = cert.getSubjectAlternativeNames(); @@ -268,7 +255,7 @@ public void verify(String host, X509Certificate cert, long serverThreadId) throw // *********************************************************** // Host is IPv4 : Check corresponding entries in subject alternative names // *********************************************************** - if (Utils.isIPv4(lowerCaseHost)) { + if (isIPv4(lowerCaseHost)) { for (GeneralName entry : subjectAltNames.getGeneralNames()) { if (logger.isTraceEnabled()) { logger.trace( @@ -283,7 +270,7 @@ public void verify(String host, X509Certificate cert, long serverThreadId) throw return; } } - } else if (Utils.isIPv6(lowerCaseHost)) { + } else if (isIPv6(lowerCaseHost)) { // *********************************************************** // Host is IPv6 : Check corresponding entries in subject alternative names // *********************************************************** @@ -299,7 +286,7 @@ public void verify(String host, X509Certificate cert, long serverThreadId) throw } if (entry.extension == Extension.IP - && !Utils.isIPv4(entry.value) + && !isIPv4(entry.value) && normalisedHost.equals(normaliseAddress(entry.value))) { return; } diff --git a/src/main/java/org/mariadb/jdbc/internal/protocol/tls/MariaDbX509KeyManager.java b/src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509KeyManager.java similarity index 75% rename from src/main/java/org/mariadb/jdbc/internal/protocol/tls/MariaDbX509KeyManager.java rename to src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509KeyManager.java index 5abe6e2a8..9b29580e4 100644 --- a/src/main/java/org/mariadb/jdbc/internal/protocol/tls/MariaDbX509KeyManager.java +++ b/src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509KeyManager.java @@ -1,5 +1,4 @@ /* - * * MariaDB Client for Java * * Copyright (c) 2012-2014 Monty Program Ab. @@ -18,39 +17,9 @@ * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to Monty Program Ab info@montyprogram.com. * - * This particular MariaDB Client for Java file is work - * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to - * the following copyright and notice provisions: - * - * Copyright (c) 2009-2011, Marcus Eriksson - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of the driver nor the names of its contributors may not be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * */ -package org.mariadb.jdbc.internal.protocol.tls; +package org.mariadb.jdbc.client.tls; import java.net.Socket; import java.security.*; diff --git a/src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509TrustManager.java b/src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509TrustManager.java new file mode 100644 index 000000000..7c221ae01 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/client/tls/MariaDbX509TrustManager.java @@ -0,0 +1,175 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.client.tls; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.*; +import java.sql.SQLException; +import java.util.Collection; +import java.util.UUID; +import java.util.zip.GZIPInputStream; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.SslMode; +import org.mariadb.jdbc.util.exceptions.ExceptionFactory; + +public class MariaDbX509TrustManager implements X509TrustManager { + + private X509TrustManager trustManager; + + public MariaDbX509TrustManager(Configuration conf, ExceptionFactory exceptionFactory) + throws SQLException { + + if (SslMode.NO_VERIFICATION == conf.sslMode()) { + return; + } + + KeyStore ks; + try { + ks = KeyStore.getInstance(KeyStore.getDefaultType()); + } catch (GeneralSecurityException generalSecurityEx) { + throw exceptionFactory.create( + "Failed to create keystore instance", "08000", generalSecurityEx); + } + + InputStream inStream = null; + try { + // generate a keyStore from the provided cert + inStream = getInputStreamFromPath(conf.serverSslCert()); + + // Note: KeyStore requires it be loaded even if you don't load anything into it + // (will be initialized with "javax.net.ssl.trustStore") values. + ks.load(null); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Collection caList = cf.generateCertificates(inStream); + for (Certificate ca : caList) { + ks.setCertificateEntry(UUID.randomUUID().toString(), ca); + } + + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + trustManager = (X509TrustManager) tm; + break; + } + } + + if (trustManager == null) { + throw new SQLException("No X509TrustManager found"); + } + + } catch (IOException ioEx) { + throw exceptionFactory.create("Failed load keyStore", "08000", ioEx); + } catch (GeneralSecurityException generalSecurityEx) { + throw exceptionFactory.create( + "Failed to store certificate from serverSslCert into a keyStore", + "08000", + generalSecurityEx); + } finally { + if (inStream != null) { + try { + inStream.close(); + } catch (IOException ioEx) { + // ignore error + } + } + } + } + + private static InputStream getInputStreamFromPath(String path) throws IOException { + InputStream is; + String protocol = path.replaceFirst("^(\\w+):.+$", "$1").toLowerCase(); + switch (protocol) { + case "http": + case "https": + HttpURLConnection connection = (HttpURLConnection) new URL(path).openConnection(); + int code = connection.getResponseCode(); + if (code >= 400) throw new IOException("Server returned error code #" + code); + is = connection.getInputStream(); + String contentEncoding = connection.getContentEncoding(); + if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) + is = new GZIPInputStream(is); + break; + case "file": + is = new URL(path).openStream(); + break; + case "classpath": + is = + Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream(path.replaceFirst("^\\w+:", "")); + break; + default: + if (path.startsWith("-----")) { + is = new ByteArrayInputStream(path.getBytes()); + break; + } + throw new IOException( + String.format("Wrong value for option `serverSslCert` (value: '%s')", path)); + } + return is; + } + + /** + * Check client trusted. + * + * @param x509Certificates certificate + * @param string string + * @throws CertificateException exception + */ + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String string) + throws CertificateException { + if (trustManager == null) { + return; + } + trustManager.checkClientTrusted(x509Certificates, string); + } + + /** + * Check server trusted. + * + * @param x509Certificates certificate + * @param string string + * @throws CertificateException exception + */ + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String string) + throws CertificateException { + if (trustManager == null) { + return; + } + trustManager.checkServerTrusted(x509Certificates, string); + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/BinaryRowDecoder.java b/src/main/java/org/mariadb/jdbc/codec/BinaryRowDecoder.java new file mode 100644 index 000000000..92ccc369b --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/BinaryRowDecoder.java @@ -0,0 +1,213 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec; + +import java.sql.SQLDataException; +import java.sql.SQLException; +import java.util.Calendar; +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class BinaryRowDecoder extends RowDecoder { + + private byte[] nullBitmap; + + public BinaryRowDecoder(int columnCount, ColumnDefinitionPacket[] columns, Configuration conf) { + super(columnCount, columns, conf); + } + + /** + * Get value. + * + * @param newIndex REAL index (0 = first) + * @param codec codec + * @return value + * @throws SQLException if cannot decode value + */ + @SuppressWarnings("unchecked") + @Override + public T getValue(int newIndex, Codec codec, Calendar cal) throws SQLException { + if (newIndex < 1 || newIndex > columnCount) { + throw new SQLException( + String.format( + "Wrong index position. Is %s but must be in 1-%s range", newIndex, columnCount)); + } + if (buf == null) { + throw new SQLDataException("wrong row position", "22023"); + } + // check NULL-Bitmap that indicate if field is null + if ((nullBitmap[(newIndex + 1) / 8] & (1 << ((newIndex + 1) % 8))) != 0) { + index = newIndex - 1; + return null; + } + setPosition(newIndex - 1); + return codec.decodeBinary(buf, length, columns[index], cal); + } + + @Override + public T decode(Codec codec, Calendar cal) throws SQLException { + return codec.decodeBinary(buf, length, columns[index], cal); + } + + @Override + public void setRow(ReadableByteBuf buf) { + if (buf != null) { + this.buf = buf; + buf.pos(1); // skip 0x00 header + nullBitmap = new byte[(columnCount + 9) / 8]; + buf.readBytes(nullBitmap); + this.buf.mark(); + } else { + this.buf = null; + } + index = -1; + } + + @Override + public boolean wasNull() { + return (nullBitmap[(index + 2) / 8] & (1 << ((index + 2) % 8))) > 0; + } + + /** + * Set length and pos indicator to asked index. + * + * @param newIndex index (0 is first). + */ + @Override + public void setPosition(int newIndex) { + + if (index >= newIndex) { + index = 0; + buf.reset(); + } else { + index++; + } + + while (index < newIndex) { + if ((nullBitmap[(index + 2) / 8] & (1 << ((index + 2) % 8))) == 0) { + // skip bytes + switch (columns[index].getType()) { + case BIGINT: + case DOUBLE: + buf.skip(8); + break; + + case INTEGER: + case MEDIUMINT: + case FLOAT: + buf.skip(4); + break; + + case SMALLINT: + case YEAR: + buf.skip(2); + break; + + case TINYINT: + buf.skip(1); + break; + + default: + int type = this.buf.readUnsignedByte(); + switch (type) { + case 251: + break; + + case 252: + this.buf.skip(this.buf.readUnsignedShort()); + break; + + case 253: + this.buf.skip(this.buf.readUnsignedMedium()); + break; + + case 254: + this.buf.skip((int) this.buf.readLong()); + break; + + default: + this.buf.skip(type); + break; + } + break; + } + } + index++; + } + + // read asked field position and length + switch (columns[index].getType()) { + case BIGINT: + case DOUBLE: + length = 8; + return; + + case INTEGER: + case MEDIUMINT: + case FLOAT: + length = 4; + return; + + case SMALLINT: + case YEAR: + length = 2; + return; + + case TINYINT: + length = 1; + return; + + default: + // field with variable length + int len = this.buf.readUnsignedByte(); + switch (len) { + case 251: + // null length field + // must never occur + // null value are set in NULL-Bitmap, not send with a null length indicator. + throw new IllegalStateException( + "null data is encoded in binary protocol but NULL-Bitmap is not set"); + + case 252: + // length is encoded on 3 bytes (0xfc header + 2 bytes indicating length) + length = this.buf.readUnsignedShort(); + return; + + case 253: + // length is encoded on 4 bytes (0xfd header + 3 bytes indicating length) + length = this.buf.readUnsignedMedium(); + return; + + case 254: + // length is encoded on 9 bytes (0xfe header + 8 bytes indicating length) + length = (int) this.buf.readLong(); + return; + + default: + // length is encoded on 1 bytes (is then less than 251) + length = len; + return; + } + } + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/Codec.java b/src/main/java/org/mariadb/jdbc/codec/Codec.java new file mode 100644 index 000000000..f1e5ef89a --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/Codec.java @@ -0,0 +1,77 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec; + +import java.io.IOException; +import java.sql.SQLDataException; +import java.sql.SQLException; +import java.util.Calendar; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public interface Codec { + + String className(); + + boolean canDecode(ColumnDefinitionPacket column, Class type); + + boolean canEncode(Object value); + + T decodeText( + final ReadableByteBuf buffer, + final int length, + final ColumnDefinitionPacket column, + final Calendar cal) + throws SQLDataException; + + T decodeBinary( + final ReadableByteBuf buffer, + final int length, + final ColumnDefinitionPacket column, + final Calendar cal) + throws SQLDataException; + + void encodeText(PacketWriter encoder, Context context, T value, Calendar cal, Long length) + throws IOException, SQLException; + + void encodeBinary(PacketWriter encoder, Context context, T value, Calendar cal) + throws IOException, SQLException; + + default boolean canEncodeLongData() { + return false; + } + + default void encodeLongData(PacketWriter encoder, Context context, T value, Long length) + throws IOException, SQLException { + throw new SQLException("Data is not supposed to be send in COM_STMT_LONG_DATA"); + } + + default byte[] encodeLongDataReturning( + PacketWriter encoder, Context context, T value, Long length) + throws IOException, SQLException { + throw new SQLException("Data is not supposed to be send in COM_STMT_LONG_DATA"); + } + + DataType getBinaryEncodeType(); +} diff --git a/src/main/java/org/mariadb/jdbc/codec/Codecs.java b/src/main/java/org/mariadb/jdbc/codec/Codecs.java new file mode 100644 index 000000000..53ab68bf0 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/Codecs.java @@ -0,0 +1,56 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec; + +import org.mariadb.jdbc.codec.list.*; + +public class Codecs { + + public static final Codec[] LIST = + new Codec[] { + BigDecimalCodec.INSTANCE, + BigIntegerCodec.INSTANCE, + BooleanCodec.INSTANCE, + BlobCodec.INSTANCE, + ByteArrayCodec.INSTANCE, + ByteCodec.INSTANCE, + BitSetCodec.INSTANCE, + ClobCodec.INSTANCE, + DoubleCodec.INSTANCE, + LongCodec.INSTANCE, + FloatCodec.INSTANCE, + IntCodec.INSTANCE, + LocalDateCodec.INSTANCE, + LocalDateTimeCodec.INSTANCE, + LocalTimeCodec.INSTANCE, + DurationCodec.INSTANCE, + ReaderCodec.INSTANCE, + TimeCodec.INSTANCE, + ZonedDateTimeCodec.INSTANCE, + TimestampCodec.INSTANCE, + DateCodec.INSTANCE, + ShortCodec.INSTANCE, + StreamCodec.INSTANCE, + StringCodec.INSTANCE, + TinyIntCodec.INSTANCE + }; +} diff --git a/src/main/java/org/mariadb/jdbc/codec/DataType.java b/src/main/java/org/mariadb/jdbc/codec/DataType.java new file mode 100644 index 000000000..b182a16f1 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/DataType.java @@ -0,0 +1,76 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec; + +public enum DataType { + OLDDECIMAL(0), + TINYINT(1), + SMALLINT(2), + INTEGER(3), + FLOAT(4), + DOUBLE(5), + NULL(6), + TIMESTAMP(7), + BIGINT(8), + MEDIUMINT(9), + DATE(10), + TIME(11), + DATETIME(12), + YEAR(13), + NEWDATE(14), + VARCHAR(15), + BIT(16), + JSON(245), + DECIMAL(246), + ENUM(247), + SET(248), + TINYBLOB(249), + MEDIUMBLOB(250), + LONGBLOB(251), + BLOB(252), + VARSTRING(253), + STRING(254), + GEOMETRY(255); + + static final DataType[] typeMap; + + static { + typeMap = new DataType[256]; + for (DataType v : values()) { + typeMap[v.mariadbType] = v; + } + } + + private final int mariadbType; + + DataType(int mariadbType) { + this.mariadbType = mariadbType; + } + + public int get() { + return mariadbType; + } + + public static DataType of(int typeValue) { + return typeMap[typeValue]; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/Parameter.java b/src/main/java/org/mariadb/jdbc/codec/Parameter.java new file mode 100644 index 000000000..5b8f19705 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/Parameter.java @@ -0,0 +1,114 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.Calendar; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.context.Context; + +public class Parameter { + @SuppressWarnings({"rawtypes", "unchecked"}) + public static final Parameter NULL_PARAMETER = + new Parameter(null, null) { + @Override + public void encodeText(PacketWriter encoder, Context context) throws IOException { + encoder.writeAscii("null"); + } + + @Override + public DataType getBinaryEncodeType() { + return DataType.VARCHAR; + } + + @Override + public boolean isNull() { + return true; + } + }; + + private final Codec codec; + private final T value; + private final Calendar cal; + private final Long length; + + public Parameter(Codec codec, T value) { + this.codec = codec; + this.value = value; + this.cal = null; + this.length = null; + } + + public Parameter(Codec codec, T value, Long length) { + this.codec = codec; + this.value = value; + this.cal = null; + this.length = length; + } + + public Parameter(Codec codec, T value, Calendar cal) { + this.codec = codec; + this.value = value; + this.cal = cal; + this.length = null; + } + + public void encodeText(PacketWriter encoder, Context context) throws IOException, SQLException { + codec.encodeText(encoder, context, this.value, this.cal, length); + } + + public void encodeBinary(PacketWriter encoder, Context context) throws IOException, SQLException { + codec.encodeBinary(encoder, context, this.value, this.cal); + } + + public void encodeLongData(PacketWriter encoder, Context context) + throws IOException, SQLException { + codec.encodeLongData(encoder, context, this.value, length); + } + + public byte[] encodeLongDataReturning(PacketWriter encoder, Context context) + throws IOException, SQLException { + return codec.encodeLongDataReturning(encoder, context, this.value, length); + } + + public Long getLength() { + return length; + } + + public boolean canEncodeLongData() { + return codec.canEncodeLongData(); + } + + public DataType getBinaryEncodeType() { + return codec.getBinaryEncodeType(); + } + + public boolean isNull() { + return false; + } + + @Override + public String toString() { + return "Parameter{codec=" + codec.className() + ", value=" + value + '}'; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/RowDecoder.java b/src/main/java/org/mariadb/jdbc/codec/RowDecoder.java new file mode 100644 index 000000000..a0b32633b --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/RowDecoder.java @@ -0,0 +1,196 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec; + +import java.sql.SQLDataException; +import java.sql.SQLException; +import java.util.*; +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public abstract class RowDecoder { + protected static final int NULL_LENGTH = -1; + + private final Configuration conf; + protected ReadableByteBuf buf; + protected ColumnDefinitionPacket[] columns; + + protected int length; + protected int index; + protected int columnCount; + private Map mapper = null; + + public RowDecoder(int columnCount, ColumnDefinitionPacket[] columns, Configuration conf) { + this.columnCount = columnCount; + this.columns = columns; + this.conf = conf; + } + + public void setRow(ReadableByteBuf buf) { + if (buf == null) { + this.buf = null; + } else { + this.buf = buf; + this.buf.pos(0); + index = -1; + } + } + + protected IllegalArgumentException noDecoderException( + ColumnDefinitionPacket column, Class type) { + + if (type.isArray()) { + if (EnumSet.of( + DataType.TINYINT, + DataType.SMALLINT, + DataType.MEDIUMINT, + DataType.INTEGER, + DataType.BIGINT) + .contains(column.getType())) { + throw new IllegalArgumentException( + String.format( + "No decoder for type %s[] and column type %s(%s)", + type.getComponentType().getName(), + column.getType().toString(), + column.isSigned() ? "signed" : "unsigned")); + } + throw new IllegalArgumentException( + String.format( + "No decoder for type %s[] and column type %s", + type.getComponentType().getName(), column.getType().toString())); + } + if (EnumSet.of( + DataType.TINYINT, + DataType.SMALLINT, + DataType.MEDIUMINT, + DataType.INTEGER, + DataType.BIGINT) + .contains(column.getType())) { + throw new IllegalArgumentException( + String.format( + "No decoder for type %s and column type %s(%s)", + type.getName(), + column.getType().toString(), + column.isSigned() ? "signed" : "unsigned")); + } + throw new IllegalArgumentException( + String.format( + "No decoder for type %s and column type %s", + type.getName(), column.getType().toString())); + } + + public abstract void setPosition(int position); + + public abstract T decode(Codec codec, Calendar calendar) throws SQLException; + + @SuppressWarnings("unchecked") + public T getValue(int index, Class type, Calendar calendar) throws SQLException { + if (buf == null) { + throw new SQLDataException("wrong row position", "22023"); + } + if (index < 1 || index > columnCount) { + throw new SQLException( + String.format( + "Wrong index position. Is %s but must be in 1-%s range", index, columnCount)); + } + + setPosition(index - 1); + + if (wasNull()) { + if (type.isPrimitive()) { + throw new SQLException( + String.format("Cannot return null for primitive %s", type.getName())); + } + return null; + } + + ColumnDefinitionPacket column = columns[index - 1]; + // type generic, return "natural" java type + if (Object.class == type || type == null) { + Codec defaultCodec = ((Codec) column.getDefaultCodec(conf)); + return decode(defaultCodec, calendar); + } + + for (Codec codec : Codecs.LIST) { + if (codec.canDecode(column, type)) { + return decode((Codec) codec, calendar); + } + } + buf.skip(length); + throw new SQLException( + String.format("Type %s not supported type for %s type", type, column.getType().name())); + } + + public abstract boolean wasNull(); + + public T getValue(int index, Codec codec) throws SQLException { + return getValue(index, codec, null); + } + + public abstract T getValue(int index, Codec codec, Calendar cal) throws SQLException; + + public T getValue(String label, Codec codec) throws SQLException { + if (buf == null) { + throw new SQLDataException("wrong row position", "22023"); + } + return getValue(getIndex(label), codec); + } + + public T getValue(String label, Codec codec, Calendar cal) throws SQLException { + if (buf == null) { + throw new SQLDataException("wrong row position", "22023"); + } + return getValue(getIndex(label), codec, cal); + } + + public int getIndex(String label) throws SQLException { + if (label == null) throw new SQLException("null is not a valid label value"); + if (mapper == null) { + mapper = new HashMap<>(); + for (int i = 0; i < columnCount; i++) { + ColumnDefinitionPacket ci = columns[i]; + String columnAlias = ci.getColumnAlias(); + if (columnAlias != null) { + columnAlias = columnAlias.toLowerCase(Locale.ROOT); + mapper.putIfAbsent(columnAlias, i + 1); + + String tableAlias = ci.getTableAlias(); + if (tableAlias != null) { + mapper.putIfAbsent(tableAlias.toLowerCase(Locale.ROOT) + "." + columnAlias, i + 1); + } else { + String table = ci.getTable(); + if (table != null) { + mapper.putIfAbsent(table.toLowerCase(Locale.ROOT) + "." + columnAlias, i + 1); + } + } + } + } + } + Integer ind = mapper.get(label.toLowerCase(Locale.ROOT)); + if (ind == null) { + String keys = Arrays.toString(mapper.keySet().toArray(new String[0])); + throw new SQLException(String.format("Unknown label '%s'. Possible value %s", label, keys)); + } + return ind; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/TextRowDecoder.java b/src/main/java/org/mariadb/jdbc/codec/TextRowDecoder.java new file mode 100644 index 000000000..430ec75fe --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/TextRowDecoder.java @@ -0,0 +1,128 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec; + +import java.sql.SQLDataException; +import java.sql.SQLException; +import java.util.Calendar; +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class TextRowDecoder extends RowDecoder { + + public TextRowDecoder(int columnCount, ColumnDefinitionPacket[] columns, Configuration conf) { + super(columnCount, columns, conf); + } + + /** + * Get value. + * + * @param index REAL index (0 = first) + * @param codec codec + * @return value + * @throws SQLException if cannot decode value + */ + @Override + public T getValue(int index, Codec codec, Calendar cal) throws SQLException { + if (index < 1 || index > columnCount) { + throw new SQLException( + String.format( + "Wrong index position. Is %s but must be in 1-%s range", index, columnCount)); + } + if (buf == null) { + throw new SQLDataException("wrong row position", "22023"); + } + ColumnDefinitionPacket column = columns[index - 1]; + + setPosition(index - 1); + if (length == NULL_LENGTH) { + return null; + } + return codec.decodeText(buf, length, column, cal); + } + + @Override + public T decode(Codec codec, Calendar cal) throws SQLException { + return codec.decodeText(buf, length, columns[index], cal); + } + + @Override + public boolean wasNull() { + return length == NULL_LENGTH; + } + + /** + * Set length and pos indicator to asked index. + * + * @param newIndex index (0 is first). + */ + @Override + public void setPosition(int newIndex) { + if (index >= newIndex) { + index = 0; + buf.pos(0); + } else { + index++; + } + + while (index < newIndex) { + int type = this.buf.readUnsignedByte(); + switch (type) { + case 252: + buf.skip(buf.readUnsignedShort()); + break; + case 253: + buf.skip(buf.readUnsignedMedium()); + break; + case 254: + buf.skip((int) (4 + buf.readUnsignedInt())); + break; + case 251: + break; + default: + buf.skip(type); + break; + } + index++; + } + + short type = this.buf.readUnsignedByte(); + switch (type) { + case 251: + length = NULL_LENGTH; + break; + case 252: + length = buf.readUnsignedShort(); + break; + case 253: + length = buf.readUnsignedMedium(); + break; + case 254: + length = (int) buf.readUnsignedInt(); + buf.skip(4); + break; + default: + length = type; + break; + } + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/BigDecimalCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/BigDecimalCodec.java new file mode 100644 index 000000000..b469545e0 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/BigDecimalCodec.java @@ -0,0 +1,218 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.SQLDataException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class BigDecimalCodec implements Codec { + + public static final BigDecimalCodec INSTANCE = new BigDecimalCodec(); + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.TINYINT, + DataType.SMALLINT, + DataType.MEDIUMINT, + DataType.INTEGER, + DataType.FLOAT, + DataType.DOUBLE, + DataType.BIGINT, + DataType.BIT, + DataType.DECIMAL, + DataType.OLDDECIMAL, + DataType.YEAR, + DataType.DECIMAL, + DataType.VARCHAR, + DataType.VARSTRING, + DataType.STRING); + + public String className() { + return BigDecimal.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(BigDecimal.class); + } + + public boolean canEncode(Object value) { + return value instanceof BigDecimal; + } + + @Override + public BigDecimal decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case TINYINT: + case SMALLINT: + case MEDIUMINT: + case INTEGER: + case BIGINT: + case FLOAT: + case DOUBLE: + case DECIMAL: + case OLDDECIMAL: + return new BigDecimal(buf.readAscii(length)); + + case VARCHAR: + case VARSTRING: + case STRING: + String str = buf.readString(length); + try { + return new BigDecimal(str); + } catch (NumberFormatException nfe) { + throw new SQLDataException( + String.format("value '%s' cannot be decoded as BigDecimal", str)); + } + + case BIT: + long result = 0; + for (int i = 0; i < Math.min(length, 8); i++) { + byte b = buf.readByte(); + result = (result << 8) + (b & 0xff); + } + if (length > 8) { + buf.skip(length - 8); + } + return BigDecimal.valueOf(result); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as BigDecimal", column.getType())); + } + } + + @Override + public BigDecimal decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + switch (column.getType()) { + case TINYINT: + if (!column.isSigned()) { + return BigDecimal.valueOf(buf.readUnsignedByte()); + } + return BigDecimal.valueOf((int) buf.readByte()); + + case YEAR: + case SMALLINT: + if (!column.isSigned()) { + return BigDecimal.valueOf(buf.readUnsignedShort()); + } + return BigDecimal.valueOf((int) buf.readShort()); + + case MEDIUMINT: + if (!column.isSigned()) { + int val = buf.readUnsignedMedium(); + buf.skip(); + return BigDecimal.valueOf(val); + } + return BigDecimal.valueOf(buf.readInt()); + + case INTEGER: + if (!column.isSigned()) { + return BigDecimal.valueOf(buf.readUnsignedInt()); + } + return BigDecimal.valueOf(buf.readInt()); + + case BIGINT: + BigInteger val; + if (column.isSigned()) { + val = BigInteger.valueOf(buf.readLong()); + } else { + // need BIG ENDIAN, so reverse order + byte[] bb = new byte[8]; + for (int i = 7; i >= 0; i--) { + bb[i] = buf.readByte(); + } + val = new BigInteger(1, bb); + } + + return new BigDecimal(String.valueOf(val)).setScale(column.getDecimals()); + + case FLOAT: + return BigDecimal.valueOf(buf.readFloat()); + + case DOUBLE: + return BigDecimal.valueOf(buf.readDouble()); + + case BIT: + long result = 0; + for (int i = 0; i < Math.min(length, 8); i++) { + byte b = buf.readByte(); + result = (result << 8) + (b & 0xff); + } + if (length > 8) { + buf.skip(length - 8); + } + return BigDecimal.valueOf(result); + + case VARCHAR: + case VARSTRING: + case STRING: + case DECIMAL: + case OLDDECIMAL: + String str = buf.readString(length); + try { + return new BigDecimal(str); + } catch (NumberFormatException nfe) { + throw new SQLDataException( + String.format("value '%s' cannot be decoded as BigDecimal", str)); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as BigDecimal", column.getType())); + } + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, BigDecimal value, Calendar cal, Long length) + throws IOException { + encoder.writeAscii(value.toPlainString()); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, BigDecimal value, Calendar cal) + throws IOException { + String asciiFormat = value.toPlainString(); + encoder.writeLength(asciiFormat.length()); + encoder.writeAscii(asciiFormat); + } + + public DataType getBinaryEncodeType() { + return DataType.DECIMAL; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/BigIntegerCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/BigIntegerCodec.java new file mode 100644 index 000000000..6d72a8a44 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/BigIntegerCodec.java @@ -0,0 +1,213 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.SQLDataException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class BigIntegerCodec implements Codec { + + public static final BigIntegerCodec INSTANCE = new BigIntegerCodec(); + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.TINYINT, + DataType.SMALLINT, + DataType.MEDIUMINT, + DataType.INTEGER, + DataType.BIGINT, + DataType.DECIMAL, + DataType.YEAR, + DataType.DOUBLE, + DataType.DECIMAL, + DataType.OLDDECIMAL, + DataType.FLOAT, + DataType.BIT, + DataType.VARCHAR, + DataType.VARSTRING, + DataType.STRING); + + public String className() { + return BigInteger.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(BigInteger.class); + } + + public boolean canEncode(Object value) { + return value instanceof BigInteger; + } + + @Override + public BigInteger decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + switch (column.getType()) { + case FLOAT: + case DOUBLE: + case DECIMAL: + case OLDDECIMAL: + return new BigDecimal(buf.readAscii(length)).toBigInteger(); + + case VARCHAR: + case VARSTRING: + case STRING: + String str2 = buf.readString(length); + try { + return new BigDecimal(str2).toBigInteger(); + } catch (NumberFormatException nfe) { + throw new SQLDataException( + String.format("value '%s' cannot be decoded as BigInteger", str2)); + } + + case BIT: + long result = 0; + for (int i = 0; i < Math.min(length, 8); i++) { + byte b = buf.readByte(); + result = (result << 8) + (b & 0xff); + } + if (length > 8) { + buf.skip(length - 8); + } + return BigInteger.valueOf(result); + + case TINYINT: + case SMALLINT: + case MEDIUMINT: + case INTEGER: + case BIGINT: + return new BigInteger(buf.readAscii(length)); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as BigInteger", column.getType())); + } + } + + @Override + public BigInteger decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + switch (column.getType()) { + case BIT: + long result = 0; + for (int i = 0; i < Math.min(length, 8); i++) { + byte b = buf.readByte(); + result = (result << 8) + (b & 0xff); + } + if (length > 8) { + buf.skip(length - 8); + } + return BigInteger.valueOf(result); + case TINYINT: + if (!column.isSigned()) { + return BigInteger.valueOf(buf.readUnsignedByte()); + } + return BigInteger.valueOf((int) buf.readByte()); + + case YEAR: + case SMALLINT: + if (!column.isSigned()) { + return BigInteger.valueOf(buf.readUnsignedShort()); + } + return BigInteger.valueOf((int) buf.readShort()); + + case MEDIUMINT: + if (!column.isSigned()) { + return BigInteger.valueOf((buf.readUnsignedMedium())); + } + return BigInteger.valueOf(buf.readMedium()); + + case INTEGER: + if (!column.isSigned()) { + return BigInteger.valueOf(buf.readUnsignedInt()); + } + return BigInteger.valueOf(buf.readInt()); + + case FLOAT: + return BigDecimal.valueOf(buf.readFloat()).toBigInteger(); + + case DOUBLE: + return BigDecimal.valueOf(buf.readDouble()).toBigInteger(); + + case DECIMAL: + return new BigDecimal(buf.readAscii(length)).toBigInteger(); + + case BIGINT: + if (column.isSigned()) return BigInteger.valueOf(buf.readLong()); + + // need BIG ENDIAN, so reverse order + byte[] bb = new byte[8]; + for (int i = 7; i >= 0; i--) { + bb[i] = buf.readByte(); + } + return new BigInteger(1, bb); + case VARCHAR: + case VARSTRING: + case STRING: + String str = buf.readString(length); + try { + return new BigInteger(str); + } catch (NumberFormatException nfe) { + throw new SQLDataException( + String.format("value '%s' cannot be decoded as BigInteger", str)); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as BigInteger", column.getType())); + } + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, BigInteger value, Calendar cal, Long length) + throws IOException { + encoder.writeAscii(value.toString()); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, BigInteger value, Calendar cal) + throws IOException { + String asciiFormat = value.toString(); + encoder.writeLength(asciiFormat.length()); + encoder.writeAscii(asciiFormat); + } + + public DataType getBinaryEncodeType() { + return DataType.DECIMAL; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/BitSetCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/BitSetCodec.java new file mode 100644 index 000000000..b2ab87821 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/BitSetCodec.java @@ -0,0 +1,109 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.util.BitSet; +import java.util.Calendar; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class BitSetCodec implements Codec { + + public static final BitSetCodec INSTANCE = new BitSetCodec(); + + public static BitSet parseBit(ReadableByteBuf buf, int length) { + byte[] arr = new byte[length]; + buf.readBytes(arr); + revertOrder(arr); + return BitSet.valueOf(arr); + } + + public static void revertOrder(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + public String className() { + return BitSet.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return column.getType() == DataType.BIT && type.isAssignableFrom(BitSet.class); + } + + @Override + public BitSet decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) { + return parseBit(buf, length); + } + + @Override + public BitSet decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) { + return parseBit(buf, length); + } + + public boolean canEncode(Object value) { + return value instanceof BitSet; + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, BitSet value, Calendar cal, Long length) + throws IOException { + byte[] bytes = value.toByteArray(); + revertOrder(bytes); + + StringBuilder sb = new StringBuilder(bytes.length * Byte.SIZE + 3); + sb.append("b'"); + for (int i = 0; i < Byte.SIZE * bytes.length; i++) + sb.append((bytes[i / Byte.SIZE] << i % Byte.SIZE & 0x80) == 0 ? '0' : '1'); + sb.append("'"); + encoder.writeAscii(sb.toString()); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, BitSet value, Calendar cal) + throws IOException { + byte[] bytes = value.toByteArray(); + revertOrder(bytes); + encoder.writeLength(bytes.length); + encoder.writeBytes(bytes); + } + + public DataType getBinaryEncodeType() { + return DataType.BLOB; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/BlobCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/BlobCodec.java new file mode 100644 index 000000000..3000e34cd --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/BlobCodec.java @@ -0,0 +1,252 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.sql.Blob; +import java.sql.SQLDataException; +import java.sql.SQLException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.MariaDbBlob; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.util.constants.ServerStatus; + +public class BlobCodec implements Codec { + + public static final BlobCodec INSTANCE = new BlobCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.BIT, + DataType.BLOB, + DataType.TINYBLOB, + DataType.MEDIUMBLOB, + DataType.LONGBLOB, + DataType.STRING, + DataType.VARSTRING, + DataType.VARCHAR); + + public String className() { + return Blob.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(Blob.class); + } + + @Override + @SuppressWarnings("fallthrough") + public Blob decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case STRING: + case VARCHAR: + case VARSTRING: + if (!column.isBinary()) { + buf.skip(length); + throw new SQLDataException( + String.format( + "Data type %s (not binary) cannot be decoded as Blob", column.getType())); + } + case BIT: + case TINYBLOB: + case MEDIUMBLOB: + case LONGBLOB: + case BLOB: + case GEOMETRY: + buf.skip(length); + return new MariaDbBlob(buf.buf(), buf.pos() - length, length); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Blob", column.getType())); + } + } + + @Override + @SuppressWarnings("fallthrough") + public Blob decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case STRING: + case VARCHAR: + case VARSTRING: + if (!column.isBinary()) { + buf.skip(length); + throw new SQLDataException( + String.format( + "Data type %s (not binary) cannot be decoded as Blob", column.getType())); + } + case BIT: + case TINYBLOB: + case MEDIUMBLOB: + case LONGBLOB: + case BLOB: + case GEOMETRY: + buf.skip(length); + return new MariaDbBlob(buf.buf(), buf.pos() - length, length); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Blob", column.getType())); + } + } + + public boolean canEncode(Object value) { + return value instanceof Blob; + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, Blob value, Calendar cal, Long maxLength) + throws IOException, SQLException { + encoder.writeBytes(ByteArrayCodec.BINARY_PREFIX); + byte[] array = new byte[4096]; + InputStream is = value.getBinaryStream(); + int len; + + if (maxLength == null) { + while ((len = is.read(array)) > 0) { + encoder.writeBytesEscaped( + array, len, (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0); + } + } else { + long maxLen = maxLength; + while ((len = is.read(array)) > 0 && maxLen > 0) { + encoder.writeBytesEscaped( + array, + Math.min(len, (int) maxLen), + (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0); + maxLen -= len; + } + } + encoder.writeByte('\''); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, Blob value, Calendar cal) + throws IOException, SQLException { + long length; + InputStream is = value.getBinaryStream(); + try { + length = value.length(); + + // if not have thrown an error + encoder.writeLength(length); + byte[] array = new byte[4096]; + int len; + while ((len = is.read(array)) > 0) { + encoder.writeBytes(array, 0, len); + } + + } catch (SQLException sqle) { + + // length is not known + byte[] blobBytes = new byte[4096]; + int pos = 0; + byte[] array = new byte[4096]; + + int len; + while ((len = is.read(array)) > 0) { + if (blobBytes.length - (pos + 1) < len) { + byte[] newBlobBytes = new byte[blobBytes.length + 65536]; + System.arraycopy(blobBytes, 0, newBlobBytes, 0, blobBytes.length); + pos = blobBytes.length; + blobBytes = newBlobBytes; + } + System.arraycopy(array, 0, blobBytes, pos, len); + pos += len; + } + encoder.writeLength(pos); + encoder.writeBytes(blobBytes, 0, pos); + } + } + + @Override + public void encodeLongData(PacketWriter encoder, Context context, Blob value, Long maxLength) + throws IOException, SQLException { + if (maxLength == null) { + byte[] array = new byte[4096]; + InputStream is = value.getBinaryStream(); + int len; + while ((len = is.read(array)) > 0) { + encoder.writeBytes(array, 0, len); + } + } else { + long maxLen = maxLength; + byte[] array = new byte[4096]; + InputStream is = value.getBinaryStream(); + int len; + while ((len = is.read(array)) > 0 && maxLen > 0) { + encoder.writeBytes(array, 0, Math.min(len, (int) maxLen)); + maxLen -= len; + } + } + } + + @Override + public byte[] encodeLongDataReturning( + PacketWriter encoder, Context context, Blob value, Long maxLength) + throws IOException, SQLException { + ByteArrayOutputStream bb = new ByteArrayOutputStream(); + if (maxLength == null) { + byte[] array = new byte[4096]; + InputStream is = value.getBinaryStream(); + int len; + while ((len = is.read(array)) > 0) { + bb.write(array, 0, len); + } + } else { + long maxLen = maxLength; + byte[] array = new byte[4096]; + InputStream is = value.getBinaryStream(); + int len; + while ((len = is.read(array)) > 0 && maxLen > 0) { + bb.write(array, 0, Math.min(len, (int) maxLen)); + maxLen -= len; + } + } + byte[] val = bb.toByteArray(); + encoder.writeBytes(val); + return val; + } + + public DataType getBinaryEncodeType() { + return DataType.BLOB; + } + + public boolean canEncodeLongData() { + return true; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/BooleanCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/BooleanCodec.java new file mode 100644 index 000000000..637d0392b --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/BooleanCodec.java @@ -0,0 +1,164 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.math.BigDecimal; +import java.sql.SQLDataException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class BooleanCodec implements Codec { + + public static final BooleanCodec INSTANCE = new BooleanCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.VARCHAR, + DataType.VARSTRING, + DataType.STRING, + DataType.BIGINT, + DataType.INTEGER, + DataType.MEDIUMINT, + DataType.SMALLINT, + DataType.TINYINT, + DataType.DECIMAL, + DataType.OLDDECIMAL, + DataType.FLOAT, + DataType.DOUBLE, + DataType.BIT); + + public String className() { + return Boolean.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) + && ((type.isPrimitive() && type == Boolean.TYPE) || type.isAssignableFrom(Boolean.class)); + } + + public boolean canEncode(Object value) { + return value instanceof Boolean; + } + + @Override + public Boolean decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case BIT: + return ByteCodec.parseBit(buf, length) != 0; + + case VARCHAR: + case VARSTRING: + case STRING: + String s = buf.readAscii(length); + return !"0".equals(s); + + case DECIMAL: + case OLDDECIMAL: + case FLOAT: + case DOUBLE: + return new BigDecimal(buf.readAscii(length)).intValue() != 0; + + case TINYINT: + case SMALLINT: + case MEDIUMINT: + case INTEGER: + case BIGINT: + case YEAR: + String val = buf.readAscii(length); + return !"0".equals(val); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Boolean", column.getType())); + } + } + + @Override + public Boolean decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case BIT: + return ByteCodec.parseBit(buf, length) != 0; + + case VARCHAR: + case VARSTRING: + case STRING: + return !"0".equals(buf.readAscii(length)); + + case DECIMAL: + case OLDDECIMAL: + return new BigDecimal(buf.readAscii(length)).intValue() != 0; + + case FLOAT: + return ((int) buf.readFloat()) != 0; + + case DOUBLE: + return ((int) buf.readDouble()) != 0; + + case TINYINT: + return buf.readByte() != 0; + + case YEAR: + case SMALLINT: + return buf.readShort() != 0; + + case MEDIUMINT: + case INTEGER: + return buf.readInt() != 0; + case BIGINT: + return buf.readLong() != 0; + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Boolean", column.getType())); + } + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, Boolean value, Calendar cal, Long maxLength) + throws IOException { + encoder.writeAscii(value ? "1" : "0"); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, Boolean value, Calendar cal) + throws IOException { + encoder.writeByte(value ? 1 : 0); + } + + public DataType getBinaryEncodeType() { + return DataType.TINYINT; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/ByteArrayCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/ByteArrayCodec.java new file mode 100644 index 000000000..141edf4ee --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/ByteArrayCodec.java @@ -0,0 +1,164 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.sql.SQLDataException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.util.constants.ServerStatus; + +public class ByteArrayCodec implements Codec { + + public static final byte[] BINARY_PREFIX = {'_', 'b', 'i', 'n', 'a', 'r', 'y', ' ', '\''}; + + public static final ByteArrayCodec INSTANCE = new ByteArrayCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.BLOB, + DataType.TINYBLOB, + DataType.MEDIUMBLOB, + DataType.LONGBLOB, + DataType.BIT, + DataType.GEOMETRY, + DataType.VARSTRING, + DataType.VARCHAR, + DataType.STRING); + + public String className() { + return byte[].class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) + && ((type.isPrimitive() && type == Byte.TYPE && type.isArray()) + || type.isAssignableFrom(byte[].class)); + } + + @Override + public byte[] decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case BLOB: + case TINYBLOB: + case MEDIUMBLOB: + case LONGBLOB: + case STRING: + case VARSTRING: + case VARCHAR: + byte[] arr = new byte[length]; + buf.readBytes(arr); + return arr; + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as byte[]", column.getType())); + } + } + + @Override + public byte[] decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case BLOB: + case TINYBLOB: + case MEDIUMBLOB: + case LONGBLOB: + case STRING: + case VARSTRING: + case VARCHAR: + byte[] arr = new byte[length]; + buf.readBytes(arr); + return arr; + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as byte[]", column.getType())); + } + } + + public boolean canEncode(Object value) { + return value instanceof byte[]; + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, byte[] value, Calendar cal, Long maxLength) + throws IOException { + encoder.writeBytes(BINARY_PREFIX); + encoder.writeBytesEscaped( + value, + maxLength == null ? value.length : Math.min(value.length, maxLength.intValue()), + (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0); + encoder.writeByte('\''); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, byte[] value, Calendar cal) + throws IOException { + encoder.writeLength(value.length); + encoder.writeBytes(value); + } + + @Override + public void encodeLongData(PacketWriter encoder, Context context, byte[] value, Long length) + throws IOException { + if (length == null) { + encoder.writeBytes(value); + } else { + encoder.writeBytes(value, 0, length.intValue()); + } + } + + @Override + public byte[] encodeLongDataReturning( + PacketWriter encoder, Context context, byte[] value, Long length) throws IOException { + if (length == null || length.intValue() >= value.length) { + encoder.writeBytes(value); + return value; + } else { + encoder.writeBytes(value, 0, length.intValue()); + byte[] bb = new byte[length.intValue()]; + System.arraycopy(value, 0, bb, 0, length.intValue()); + return bb; + } + } + + public boolean canEncodeLongData() { + return true; + } + + public DataType getBinaryEncodeType() { + return DataType.BLOB; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/ByteCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/ByteCodec.java new file mode 100644 index 000000000..8ac852f88 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/ByteCodec.java @@ -0,0 +1,259 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.sql.SQLDataException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class ByteCodec implements Codec { + + public static final ByteCodec INSTANCE = new ByteCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.TINYINT, + DataType.SMALLINT, + DataType.MEDIUMINT, + DataType.INTEGER, + DataType.BIGINT, + DataType.YEAR, + DataType.BIT, + DataType.FLOAT, + DataType.DOUBLE, + DataType.OLDDECIMAL, + DataType.BLOB, + DataType.TINYBLOB, + DataType.MEDIUMBLOB, + DataType.LONGBLOB, + DataType.DECIMAL, + DataType.ENUM, + DataType.VARSTRING, + DataType.STRING, + DataType.VARCHAR); + + public static long parseBit(ReadableByteBuf buf, int length) { + if (length == 1) { + return buf.readUnsignedByte(); + } + long val = 0; + int idx = 0; + do { + val += ((long) buf.readUnsignedByte()) << (8 * length); + idx++; + } while (idx < length); + return val; + } + + public String className() { + return Byte.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) + && ((type.isPrimitive() && type == Byte.TYPE) || type.isAssignableFrom(Byte.class)); + } + + public boolean canEncode(Object value) { + return value instanceof Byte; + } + + @Override + public Byte decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + long result; + switch (column.getType()) { + case TINYINT: + case SMALLINT: + case MEDIUMINT: + case INTEGER: + case BIGINT: + case YEAR: + result = LongCodec.parseNotEmpty(buf, length); + break; + + case BIT: + Byte val = buf.readByte(); + if (length > 1) buf.skip(length - 1); + return val; + + case FLOAT: + case DOUBLE: + case OLDDECIMAL: + case DECIMAL: + case ENUM: + case VARCHAR: + case VARSTRING: + case STRING: + String str = buf.readString(length); + try { + result = new BigDecimal(str).setScale(0, RoundingMode.DOWN).byteValueExact(); + } catch (NumberFormatException | ArithmeticException nfe) { + throw new SQLDataException( + String.format("value '%s' (%s) cannot be decoded as Byte", str, column.getType())); + } + break; + + case BLOB: + case TINYBLOB: + case MEDIUMBLOB: + case LONGBLOB: + if (length > 0) { + byte b = buf.readByte(); + buf.skip(length - 1); + return b; + } + throw new SQLDataException("empty String value cannot be decoded as Byte"); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Byte", column.getType())); + } + + if ((byte) result != result || (result < 0 && !column.isSigned())) { + throw new SQLDataException("byte overflow"); + } + + return (byte) result; + } + + @Override + public Byte decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + long result; + switch (column.getType()) { + case TINYINT: + result = column.isSigned() ? buf.readByte() : buf.readUnsignedByte(); + break; + + case YEAR: + case SMALLINT: + result = column.isSigned() ? buf.readShort() : buf.readUnsignedShort(); + break; + + case MEDIUMINT: + result = column.isSigned() ? buf.readMedium() : buf.readUnsignedMedium(); + buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges ! + break; + + case INTEGER: + result = column.isSigned() ? buf.readInt() : buf.readUnsignedInt(); + break; + + case BIGINT: + if (column.isSigned()) { + result = buf.readLong(); + } else { + // need BIG ENDIAN, so reverse order + byte[] bb = new byte[8]; + for (int i = 7; i >= 0; i--) { + bb[i] = buf.readByte(); + } + BigInteger val = new BigInteger(1, bb); + result = val.longValue(); + } + break; + + case BIT: + Byte val = buf.readByte(); + if (length > 1) buf.skip(length - 1); + return val; + + case FLOAT: + result = (long) buf.readFloat(); + break; + + case DOUBLE: + result = (long) buf.readDouble(); + break; + + case OLDDECIMAL: + case DECIMAL: + case ENUM: + case VARCHAR: + case VARSTRING: + case STRING: + String str = buf.readString(length); + try { + result = new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValue(); + } catch (NumberFormatException nfe) { + throw new SQLDataException( + String.format("value '%s' (%s) cannot be decoded as Byte", str, column.getType())); + } + break; + + case BLOB: + case TINYBLOB: + case MEDIUMBLOB: + case LONGBLOB: + if (length > 0) { + byte b = buf.readByte(); + buf.skip(length - 1); + return b; + } + throw new SQLDataException("empty String value cannot be decoded as Byte"); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Byte", column.getType())); + } + + if ((byte) result != result) { + throw new SQLDataException("byte overflow"); + } + + return (byte) result; + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, Byte value, Calendar cal, Long maxLength) + throws IOException { + encoder.writeAscii(Integer.toString((int) value)); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, Byte value, Calendar cal) + throws IOException { + encoder.writeByte(value); + } + + public DataType getBinaryEncodeType() { + return DataType.TINYINT; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/ClobCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/ClobCodec.java new file mode 100644 index 000000000..c3d2c97f9 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/ClobCodec.java @@ -0,0 +1,178 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.sql.Clob; +import java.sql.NClob; +import java.sql.SQLDataException; +import java.sql.SQLException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.MariaDbClob; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.util.constants.ServerStatus; + +public class ClobCodec implements Codec { + + public static final ClobCodec INSTANCE = new ClobCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of(DataType.VARCHAR, DataType.VARSTRING, DataType.STRING); + + public String className() { + return Clob.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) + && (type.isAssignableFrom(Clob.class) || type.isAssignableFrom(NClob.class)); + } + + public boolean canEncode(Object value) { + return value instanceof Clob; + } + + @Override + public Clob decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case STRING: + case VARCHAR: + case VARSTRING: + Clob clob = new MariaDbClob(buf.buf(), buf.pos(), length); + buf.skip(length); + return clob; + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Clob", column.getType())); + } + } + + @Override + public Clob decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case STRING: + case VARCHAR: + case VARSTRING: + Clob clob = new MariaDbClob(buf.buf(), buf.pos(), length); + buf.skip(length); + return clob; + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Clob", column.getType())); + } + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, Clob value, Calendar cal, Long maxLen) + throws IOException, SQLException { + Reader reader = value.getCharacterStream(); + char[] buf = new char[4096]; + int len; + encoder.writeByte('\''); + while ((len = reader.read(buf)) >= 0) { + byte[] data = new String(buf, 0, len).getBytes(StandardCharsets.UTF_8); + encoder.writeBytesEscaped( + data, data.length, (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0); + } + encoder.writeByte('\''); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, Clob value, Calendar cal) + throws IOException, SQLException { + // prefer use of encodeLongData, because length is unknown + Reader reader = value.getCharacterStream(); + byte[] clobBytes = new byte[4096]; + int pos = 0; + char[] buf = new char[4096]; + + int len; + while ((len = reader.read(buf)) > 0) { + byte[] data = new String(buf, 0, len).getBytes(StandardCharsets.UTF_8); + if (clobBytes.length - (pos + 1) < data.length) { + byte[] newBlobBytes = new byte[clobBytes.length + 65536]; + System.arraycopy(clobBytes, 0, newBlobBytes, 0, clobBytes.length); + pos = clobBytes.length; + clobBytes = newBlobBytes; + } + System.arraycopy(data, 0, clobBytes, pos, data.length); + pos += len; + } + encoder.writeLength(pos); + encoder.writeBytes(clobBytes, 0, pos); + } + + @Override + public void encodeLongData(PacketWriter encoder, Context context, Clob value, Long length) + throws IOException, SQLException { + Reader reader = value.getCharacterStream(); + char[] buf = new char[4096]; + int len; + while ((len = reader.read(buf)) >= 0) { + byte[] data = new String(buf, 0, len).getBytes(StandardCharsets.UTF_8); + encoder.writeBytes(data, 0, data.length); + } + } + + @Override + public byte[] encodeLongDataReturning( + PacketWriter encoder, Context context, Clob value, Long length) + throws IOException, SQLException { + ByteArrayOutputStream bb = new ByteArrayOutputStream(); + Reader reader = value.getCharacterStream(); + char[] buf = new char[4096]; + int len; + while ((len = reader.read(buf)) >= 0) { + byte[] data = new String(buf, 0, len).getBytes(StandardCharsets.UTF_8); + bb.write(data, 0, data.length); + } + byte[] val = bb.toByteArray(); + encoder.writeBytes(val); + return val; + } + + public boolean canEncodeLongData() { + return true; + } + + public DataType getBinaryEncodeType() { + return DataType.VARSTRING; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/DateCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/DateCodec.java new file mode 100644 index 000000000..6dd0ca815 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/DateCodec.java @@ -0,0 +1,221 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.sql.Date; +import java.sql.SQLDataException; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class DateCodec implements Codec { + + public static final DateCodec INSTANCE = new DateCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.DATE, + DataType.NEWDATE, + DataType.DATETIME, + DataType.TIMESTAMP, + DataType.YEAR, + DataType.VARSTRING, + DataType.VARCHAR, + DataType.STRING); + + public String className() { + return Date.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(Date.class); + } + + public boolean canEncode(Object value) { + return value instanceof Date; + } + + @Override + public Date decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + switch (column.getType()) { + case YEAR: + short y = (short) LongCodec.parseNotEmpty(buf, length); + if (column.getLength() == 2) { + // YEAR(2) - deprecated + if (y <= 69) { + y += 2000; + } else { + y += 1900; + } + } + return Date.valueOf(y + "-01-01"); + + case VARCHAR: + case VARSTRING: + case STRING: + case DATE: + String val = buf.readString(length); + String[] stDatePart = val.split("-| "); + if (stDatePart.length < 3) { + throw new SQLDataException( + String.format("value '%s' (%s) cannot be decoded as Date", val, column.getType())); + } + + try { + int year = Integer.valueOf(stDatePart[0]); + int month = Integer.valueOf(stDatePart[1]); + int dayOfMonth = Integer.valueOf(stDatePart[2]); + Calendar c = cal == null ? Calendar.getInstance() : cal; + synchronized (c) { + c.clear(); + c.set(Calendar.YEAR, year); + c.set(Calendar.MONTH, month - 1); + c.set(Calendar.DAY_OF_MONTH, dayOfMonth); + return new Date(c.getTimeInMillis()); + } + + } catch (NumberFormatException nfe) { + throw new SQLDataException( + String.format("value '%s' (%s) cannot be decoded as Date", val, column.getType())); + } + + case TIMESTAMP: + case DATETIME: + Timestamp lt = TimestampCodec.INSTANCE.decodeText(buf, length, column, cal); + return new Date(lt.getTime()); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Date", column.getType())); + } + } + + @Override + public Date decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + switch (column.getType()) { + case YEAR: + int v = buf.readShort(); + + if (column.getLength() == 2) { + // YEAR(2) - deprecated + if (v <= 69) { + v += 2000; + } else { + v += 1900; + } + } + return Date.valueOf(v + "-01-01"); + + case VARCHAR: + case VARSTRING: + case STRING: + String val = buf.readString(length); + String[] stDatePart = val.split("-| "); + if (stDatePart.length < 3) { + throw new SQLDataException( + String.format("value '%s' (%s) cannot be decoded as Date", val, column.getType())); + } + + try { + int year = Integer.valueOf(stDatePart[0]); + int month = Integer.valueOf(stDatePart[1]); + int dayOfMonth = Integer.valueOf(stDatePart[2]); + Calendar c = cal == null ? Calendar.getInstance() : cal; + synchronized (c) { + c.clear(); + c.set(Calendar.YEAR, year); + c.set(Calendar.MONTH, month - 1); + c.set(Calendar.DAY_OF_MONTH, dayOfMonth); + return new Date(c.getTimeInMillis()); + } + + } catch (NumberFormatException nfe) { + throw new SQLDataException( + String.format("value '%s' (%s) cannot be decoded as Date", val, column.getType())); + } + + case DATE: + Calendar c = cal == null ? Calendar.getInstance() : cal; + synchronized (c) { + c.clear(); + c.set(Calendar.YEAR, buf.readShort()); + c.set(Calendar.MONTH, buf.readByte() - 1); + c.set(Calendar.DAY_OF_MONTH, buf.readByte()); + return new Date(c.getTimeInMillis()); + } + + case TIMESTAMP: + case DATETIME: + Timestamp lt = TimestampCodec.INSTANCE.decodeBinary(buf, length, column, cal); + return new Date(lt.getTime()); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Date", column.getType())); + } + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, Date val, Calendar providedCal, Long maxLen) + throws IOException { + Calendar cal = providedCal == null ? Calendar.getInstance() : providedCal; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + sdf.setTimeZone(cal.getTimeZone()); + String dateString = sdf.format(val); + + encoder.writeByte('\''); + encoder.writeAscii(dateString); + encoder.writeByte('\''); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, Date value, Calendar providedCal) + throws IOException { + Calendar cal = providedCal == null ? Calendar.getInstance() : providedCal; + cal.setTimeInMillis(value.getTime()); + encoder.writeByte(4); // length + encoder.writeShort((short) cal.get(Calendar.YEAR)); + encoder.writeByte(((cal.get(Calendar.MONTH) + 1) & 0xff)); + encoder.writeByte((cal.get(Calendar.DAY_OF_MONTH) & 0xff)); + } + + public DataType getBinaryEncodeType() { + return DataType.DATE; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/DoubleCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/DoubleCodec.java new file mode 100644 index 000000000..11bb75c49 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/DoubleCodec.java @@ -0,0 +1,188 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.SQLDataException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class DoubleCodec implements Codec { + + public static final DoubleCodec INSTANCE = new DoubleCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.TINYINT, + DataType.SMALLINT, + DataType.MEDIUMINT, + DataType.INTEGER, + DataType.FLOAT, + DataType.DOUBLE, + DataType.BIGINT, + DataType.YEAR, + DataType.OLDDECIMAL, + DataType.DECIMAL, + DataType.VARCHAR, + DataType.VARSTRING, + DataType.STRING); + + public String className() { + return Double.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) + && ((type.isPrimitive() && type == Double.TYPE) || type.isAssignableFrom(Double.class)); + } + + public boolean canEncode(Object value) { + return value instanceof Double; + } + + @Override + public Double decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case TINYINT: + case SMALLINT: + case MEDIUMINT: + case INTEGER: + case BIGINT: + case FLOAT: + case DOUBLE: + case OLDDECIMAL: + case DECIMAL: + return Double.valueOf(buf.readAscii(length)); + + case VARCHAR: + case VARSTRING: + case STRING: + String str2 = buf.readString(length); + try { + return Double.valueOf(str2); + } catch (NumberFormatException nfe) { + throw new SQLDataException(String.format("value '%s' cannot be decoded as Double", str2)); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Double", column.getType())); + } + } + + @Override + public Double decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case TINYINT: + if (!column.isSigned()) { + return (double) buf.readUnsignedByte(); + } + return (double) buf.readByte(); + + case YEAR: + case SMALLINT: + if (!column.isSigned()) { + return (double) buf.readUnsignedShort(); + } + return (double) buf.readShort(); + + case MEDIUMINT: + double d; + d = column.isSigned() ? buf.readMedium() : buf.readUnsignedMedium(); + buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges ! + return d; + + case INTEGER: + if (!column.isSigned()) { + return (double) buf.readUnsignedInt(); + } + return (double) buf.readInt(); + + case BIGINT: + if (column.isSigned()) { + return (double) buf.readLong(); + } else { + // need BIG ENDIAN, so reverse order + byte[] bb = new byte[8]; + for (int i = 7; i >= 0; i--) { + bb[i] = buf.readByte(); + } + return new BigInteger(1, bb).doubleValue(); + } + + case FLOAT: + return (double) buf.readFloat(); + + case DOUBLE: + return buf.readDouble(); + + case OLDDECIMAL: + case DECIMAL: + return new BigDecimal(buf.readAscii(length)).doubleValue(); + + case VARCHAR: + case VARSTRING: + case STRING: + String str2 = buf.readString(length); + try { + return Double.valueOf(str2); + } catch (NumberFormatException nfe) { + throw new SQLDataException(String.format("value '%s' cannot be decoded as Double", str2)); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Double", column.getType())); + } + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, Double value, Calendar cal, Long maxLen) + throws IOException { + encoder.writeAscii(String.valueOf(value)); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, Double value, Calendar cal) + throws IOException { + encoder.writeDouble(value); + } + + public DataType getBinaryEncodeType() { + return DataType.DOUBLE; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/DurationCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/DurationCodec.java new file mode 100644 index 000000000..00b23ce1f --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/DurationCodec.java @@ -0,0 +1,201 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.sql.SQLDataException; +import java.time.Duration; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class DurationCodec implements Codec { + + public static final DurationCodec INSTANCE = new DurationCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.TIME, + DataType.DATETIME, + DataType.TIMESTAMP, + DataType.VARSTRING, + DataType.VARCHAR, + DataType.STRING); + + public String className() { + return Duration.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(Duration.class); + } + + public boolean canEncode(Object value) { + return value instanceof Duration; + } + + @Override + public Duration decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + int[] parts; + switch (column.getType()) { + case TIMESTAMP: + case DATETIME: + parts = LocalDateTimeCodec.parseTimestamp(buf.readAscii(length)); + if (parts == null) return null; + return Duration.ZERO + .plusDays(parts[2] - 1) + .plusHours(parts[3]) + .plusMinutes(parts[4]) + .plusSeconds(parts[5]) + .plusNanos(parts[6]); + + case TIME: + case VARCHAR: + case VARSTRING: + case STRING: + parts = LocalTimeCodec.parseTime(buf, length, column); + Duration d = + Duration.ZERO + .plusHours(parts[1]) + .plusMinutes(parts[2]) + .plusSeconds(parts[3]) + .plusNanos(parts[4]); + if (parts[0] == -1) return d.negated(); + return d; + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Duration", column.getType())); + } + } + + @Override + public Duration decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) { + + long days = 0; + int hours = 0; + int minutes = 0; + int seconds = 0; + long microseconds = 0; + + switch (column.getType()) { + case TIME: + boolean negate = false; + if (length > 0) { + negate = buf.readUnsignedByte() == 0x01; + if (length > 4) { + days = buf.readUnsignedInt(); + if (length > 7) { + hours = buf.readByte(); + minutes = buf.readByte(); + seconds = buf.readByte(); + if (length > 8) { + microseconds = buf.readInt(); + } + } + } + } + + Duration duration = + Duration.ZERO + .plusDays(days) + .plusHours(hours) + .plusMinutes(minutes) + .plusSeconds(seconds) + .plusNanos(microseconds * 1000); + if (negate) return duration.negated(); + return duration; + + default: + buf.readUnsignedShort(); // skip year + buf.readByte(); // skip month + days = buf.readByte(); + if (length > 4) { + hours = buf.readByte(); + minutes = buf.readByte(); + seconds = buf.readByte(); + + if (length > 7) { + microseconds = buf.readUnsignedInt(); + } + } + return Duration.ZERO + .plusDays(days - 1) + .plusHours(hours) + .plusMinutes(minutes) + .plusSeconds(seconds) + .plusNanos(microseconds * 1000); + } + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, Duration val, Calendar cal, Long maxLen) + throws IOException { + long s = val.getSeconds(); + long microSecond = val.getNano() / 1000; + encoder.writeByte('\''); + if (microSecond != 0) { + encoder.writeAscii( + String.format("%d:%02d:%02d.%06d", s / 3600, (s % 3600) / 60, (s % 60), microSecond)); + } else { + encoder.writeAscii(String.format("%d:%02d:%02d", s / 3600, (s % 3600) / 60, (s % 60))); + } + encoder.writeByte('\''); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, Duration value, Calendar cal) + throws IOException { + int nano = value.getNano(); + if (nano > 0) { + encoder.writeByte((byte) 12); + encoder.writeByte((byte) (value.isNegative() ? 1 : 0)); + encoder.writeInt((int) value.toDays()); + encoder.writeByte((byte) (value.toHours() - 24 * value.toDays())); + encoder.writeByte((byte) (value.toMinutes() - 60 * value.toHours())); + encoder.writeByte((byte) (value.getSeconds() - 60 * value.toMinutes())); + encoder.writeInt(nano / 1000); + } else { + encoder.writeByte((byte) 8); + encoder.writeByte((byte) (value.isNegative() ? 1 : 0)); + encoder.writeInt((int) value.toDays()); + encoder.writeByte((byte) (value.toHours() - 24 * value.toDays())); + encoder.writeByte((byte) (value.toMinutes() - 60 * value.toHours())); + encoder.writeByte((byte) (value.getSeconds() - 60 * value.toMinutes())); + } + } + + public DataType getBinaryEncodeType() { + return DataType.TIME; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/FloatCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/FloatCodec.java new file mode 100644 index 000000000..994a4e991 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/FloatCodec.java @@ -0,0 +1,187 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.SQLDataException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class FloatCodec implements Codec { + + public static final FloatCodec INSTANCE = new FloatCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.TINYINT, + DataType.SMALLINT, + DataType.MEDIUMINT, + DataType.INTEGER, + DataType.FLOAT, + DataType.BIGINT, + DataType.OLDDECIMAL, + DataType.DECIMAL, + DataType.YEAR, + DataType.DOUBLE, + DataType.VARCHAR, + DataType.VARSTRING, + DataType.STRING); + + public String className() { + return Float.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) + && ((type.isPrimitive() && type == Float.TYPE) || type.isAssignableFrom(Float.class)); + } + + public boolean canEncode(Object value) { + return value instanceof Float; + } + + @Override + public Float decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case TINYINT: + case SMALLINT: + case MEDIUMINT: + case INTEGER: + case BIGINT: + case DOUBLE: + case OLDDECIMAL: + case DECIMAL: + case FLOAT: + return Float.valueOf(buf.readAscii(length)); + + case VARCHAR: + case VARSTRING: + case STRING: + String val = buf.readString(length); + try { + return Float.valueOf(val); + } catch (NumberFormatException nfe) { + throw new SQLDataException(String.format("value '%s' cannot be decoded as Float", val)); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Float", column.getType())); + } + } + + @Override + public Float decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case TINYINT: + if (!column.isSigned()) { + return (float) buf.readUnsignedByte(); + } + return (float) buf.readByte(); + + case YEAR: + case SMALLINT: + if (!column.isSigned()) { + return (float) buf.readUnsignedShort(); + } + return (float) buf.readShort(); + + case MEDIUMINT: + float f = column.isSigned() ? buf.readMedium() : buf.readUnsignedMedium(); + buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges ! + return f; + + case INTEGER: + if (!column.isSigned()) { + return (float) buf.readUnsignedInt(); + } + return (float) buf.readInt(); + + case BIGINT: + if (column.isSigned()) { + return (float) buf.readLong(); + } else { + // need BIG ENDIAN, so reverse order + byte[] bb = new byte[8]; + for (int i = 7; i >= 0; i--) { + bb[i] = buf.readByte(); + } + return new BigInteger(1, bb).floatValue(); + } + + case FLOAT: + return buf.readFloat(); + + case DOUBLE: + return (float) buf.readDouble(); + + case OLDDECIMAL: + case DECIMAL: + return new BigDecimal(buf.readAscii(length)).floatValue(); + + case VARCHAR: + case VARSTRING: + case STRING: + String str2 = buf.readString(length); + try { + return Float.valueOf(str2); + } catch (NumberFormatException nfe) { + throw new SQLDataException(String.format("value '%s' cannot be decoded as Float", str2)); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Float", column.getType())); + } + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, Float value, Calendar cal, Long maxLen) + throws IOException { + encoder.writeAscii(String.valueOf(value)); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, Float value, Calendar cal) + throws IOException { + encoder.writeFloat(value); + } + + public DataType getBinaryEncodeType() { + return DataType.FLOAT; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/IntCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/IntCodec.java new file mode 100644 index 000000000..92325df0b --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/IntCodec.java @@ -0,0 +1,242 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.sql.SQLDataException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class IntCodec implements Codec { + + public static final IntCodec INSTANCE = new IntCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.FLOAT, + DataType.DOUBLE, + DataType.OLDDECIMAL, + DataType.VARCHAR, + DataType.DECIMAL, + DataType.ENUM, + DataType.VARSTRING, + DataType.STRING, + DataType.TINYINT, + DataType.SMALLINT, + DataType.MEDIUMINT, + DataType.INTEGER, + DataType.BIGINT, + DataType.BIT, + DataType.YEAR); + + public String className() { + return Integer.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) + && ((type.isPrimitive() && type == Integer.TYPE) || type.isAssignableFrom(Integer.class)); + } + + public boolean canEncode(Object value) { + return value instanceof Integer; + } + + @Override + public Integer decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + long result; + switch (column.getType()) { + case TINYINT: + case SMALLINT: + case MEDIUMINT: + case YEAR: + return (int) LongCodec.parseNotEmpty(buf, length); + + case INTEGER: + result = LongCodec.parseNotEmpty(buf, length); + break; + + case BIGINT: + result = LongCodec.parseNotEmpty(buf, length); + if (result < 0 & !column.isSigned()) { + throw new SQLDataException("int overflow"); + } + break; + + case BIT: + result = 0; + for (int i = 0; i < Math.min(length, 8); i++) { + byte b = buf.readByte(); + result = (result << 8) + (b & 0xff); + } + if (length > 8) { + buf.skip(length - 8); + } + break; + + case FLOAT: + case DOUBLE: + case OLDDECIMAL: + case VARCHAR: + case DECIMAL: + case ENUM: + case VARSTRING: + case STRING: + String str = buf.readString(length); + try { + result = new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValue(); + break; + } catch (NumberFormatException nfe) { + throw new SQLDataException(String.format("value '%s' cannot be decoded as Integer", str)); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Integer", column.getType())); + } + + int res = (int) result; + if (res != result) { + throw new SQLDataException("integer overflow"); + } + return res; + } + + @Override + public Integer decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + long result; + switch (column.getType()) { + case TINYINT: + result = column.isSigned() ? buf.readByte() : buf.readUnsignedByte(); + break; + + case YEAR: + case SMALLINT: + result = column.isSigned() ? buf.readShort() : buf.readUnsignedShort(); + break; + + case MEDIUMINT: + result = column.isSigned() ? buf.readMedium() : buf.readUnsignedMedium(); + buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges ! + break; + + case INTEGER: + result = column.isSigned() ? buf.readInt() : buf.readUnsignedInt(); + break; + + case BIGINT: + if (column.isSigned()) { + result = buf.readLong(); + break; + } else { + // need BIG ENDIAN, so reverse order + byte[] bb = new byte[8]; + for (int i = 7; i >= 0; i--) { + bb[i] = buf.readByte(); + } + BigInteger val = new BigInteger(1, bb); + try { + return val.intValueExact(); + } catch (ArithmeticException ae) { + throw new SQLDataException( + String.format("value '%s' cannot be decoded as Integer", val.toString())); + } + } + + case BIT: + result = 0; + for (int i = 0; i < Math.min(length, 8); i++) { + byte b = buf.readByte(); + result = (result << 8) + (b & 0xff); + } + if (length > 8) { + buf.skip(length - 8); + } + break; + + case FLOAT: + result = (long) buf.readFloat(); + break; + + case DOUBLE: + result = (long) buf.readDouble(); + break; + + case OLDDECIMAL: + case VARCHAR: + case DECIMAL: + case ENUM: + case VARSTRING: + case STRING: + String str = buf.readString(length); + try { + result = new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValue(); + break; + } catch (NumberFormatException nfe) { + throw new SQLDataException(String.format("value '%s' cannot be decoded as Integer", str)); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Integer", column.getType())); + } + + int res = (int) result; + if (res != result) { + throw new SQLDataException("integer overflow"); + } + + return res; + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, Integer value, Calendar cal, Long maxLen) + throws IOException { + encoder.writeAscii(String.valueOf(value)); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, Integer value, Calendar cal) + throws IOException { + encoder.writeInt(value); + } + + public DataType getBinaryEncodeType() { + return DataType.INTEGER; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/LocalDateCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/LocalDateCodec.java new file mode 100644 index 000000000..99856a1ef --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/LocalDateCodec.java @@ -0,0 +1,234 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.sql.SQLDataException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class LocalDateCodec implements Codec { + + public static final LocalDateCodec INSTANCE = new LocalDateCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.DATE, + DataType.NEWDATE, + DataType.DATETIME, + DataType.TIMESTAMP, + DataType.YEAR, + DataType.VARSTRING, + DataType.VARCHAR, + DataType.STRING); + + public static int[] parseDate(ReadableByteBuf buf, int length) { + int[] datePart = new int[] {0, 0, 0}; + int partIdx = 0; + int idx = 0; + + while (idx++ < length) { + byte b = buf.readByte(); + if (b == '-') { + partIdx++; + continue; + } + datePart[partIdx] = datePart[partIdx] * 10 + b - 48; + } + + if (datePart[0] == 0 && datePart[1] == 0 && datePart[2] == 0) { + return null; + } + return datePart; + } + + public String className() { + return LocalDate.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(LocalDate.class); + } + + public boolean canEncode(Object value) { + return value instanceof LocalDate; + } + + @Override + public LocalDate decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + int[] parts; + switch (column.getType()) { + case YEAR: + short y = (short) LongCodec.parseNotEmpty(buf, length); + + if (length == 2 && column.getLength() == 2) { + // YEAR(2) - deprecated + if (y <= 69) { + y += 2000; + } else { + y += 1900; + } + } + + return LocalDate.of(y, 1, 1); + case NEWDATE: + case DATE: + parts = parseDate(buf, length); + break; + + case TIMESTAMP: + case DATETIME: + parts = LocalDateTimeCodec.parseTimestamp(buf.readAscii(length)); + break; + + case VARSTRING: + case VARCHAR: + case STRING: + String val = buf.readString(length); + String[] stDatePart = val.split("-| "); + if (stDatePart.length < 3) { + throw new SQLDataException( + String.format("value '%s' (%s) cannot be decoded as Date", val, column.getType())); + } + + try { + int year = Integer.valueOf(stDatePart[0]); + int month = Integer.valueOf(stDatePart[1]); + int dayOfMonth = Integer.valueOf(stDatePart[2]); + return LocalDate.of(year, month, dayOfMonth); + } catch (NumberFormatException nfe) { + throw new SQLDataException( + String.format("value '%s' (%s) cannot be decoded as Date", val, column.getType())); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Date", column.getType())); + } + if (parts == null) return null; + return LocalDate.of(parts[0], parts[1], parts[2]); + } + + @Override + public LocalDate decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + int year; + int month = 1; + int dayOfMonth = 1; + + switch (column.getType()) { + case TIMESTAMP: + case DATETIME: + year = buf.readUnsignedShort(); + month = buf.readByte(); + dayOfMonth = buf.readByte(); + + if (length > 4) { + buf.skip(length - 4); + } + return LocalDate.of(year, month, dayOfMonth); + + case STRING: + case VARCHAR: + case VARSTRING: + String val = buf.readString(length); + String[] stDatePart = val.split("-| "); + if (stDatePart.length < 3) { + throw new SQLDataException( + String.format("value '%s' (%s) cannot be decoded as Date", val, column.getType())); + } + + try { + year = Integer.valueOf(stDatePart[0]); + month = Integer.valueOf(stDatePart[1]); + dayOfMonth = Integer.valueOf(stDatePart[2]); + return LocalDate.of(year, month, dayOfMonth); + } catch (NumberFormatException nfe) { + throw new SQLDataException( + String.format("value '%s' (%s) cannot be decoded as Date", val, column.getType())); + } + + case DATE: + case YEAR: + year = buf.readUnsignedShort(); + + if (column.getLength() == 2) { + // YEAR(2) - deprecated + if (year <= 69) { + year += 2000; + } else { + year += 1900; + } + } + + if (length >= 4) { + month = buf.readByte(); + dayOfMonth = buf.readByte(); + } + return LocalDate.of(year, month, dayOfMonth); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Date", column.getType())); + } + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, LocalDate val, Calendar cal, Long maxLen) + throws IOException { + encoder.writeByte('\''); + encoder.writeAscii(val.format(DateTimeFormatter.ISO_LOCAL_DATE)); + encoder.writeByte('\''); + } + + @Override + public void encodeBinary( + PacketWriter encoder, Context context, LocalDate value, Calendar providedCal) + throws IOException { + encoder.writeByte(7); // length + encoder.writeShort((short) value.get(ChronoField.YEAR)); + encoder.writeByte(value.get(ChronoField.MONTH_OF_YEAR)); + encoder.writeByte(value.get(ChronoField.DAY_OF_MONTH)); + encoder.writeBytes(new byte[] {0, 0, 0}); + } + + public DataType getBinaryEncodeType() { + return DataType.DATE; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/LocalDateTimeCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/LocalDateTimeCodec.java new file mode 100644 index 000000000..b5e778aa9 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/LocalDateTimeCodec.java @@ -0,0 +1,268 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.sql.SQLDataException; +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class LocalDateTimeCodec implements Codec { + + public static final LocalDateTimeCodec INSTANCE = new LocalDateTimeCodec(); + public static final DateTimeFormatter TIMESTAMP_FORMAT = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS"); + public static final DateTimeFormatter TIMESTAMP_FORMAT_NO_FRACTIONAL = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + public static final DateTimeFormatter MARIADB_LOCAL_DATE_TIME; + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.DATETIME, + DataType.TIMESTAMP, + DataType.VARSTRING, + DataType.VARCHAR, + DataType.STRING, + DataType.TIME, + DataType.DATE); + + static { + MARIADB_LOCAL_DATE_TIME = + new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendLiteral(' ') + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .toFormatter(); + } + + public static int[] parseTimestamp(String raw) { + int nanoLen = -1; + int[] timestampsPart = new int[] {0, 0, 0, 0, 0, 0, 0}; + int partIdx = 0; + for (int idx = 0; idx < raw.length(); idx++) { + char b = raw.charAt(idx); + if (b == '-' || b == ' ' || b == ':') { + partIdx++; + continue; + } + if (b == '.') { + partIdx++; + nanoLen = 0; + continue; + } + if (nanoLen >= 0) nanoLen++; + timestampsPart[partIdx] = timestampsPart[partIdx] * 10 + b - 48; + } + if (timestampsPart[0] == 0 && timestampsPart[1] == 0 && timestampsPart[2] == 0) { + if (timestampsPart[3] == 0 + && timestampsPart[4] == 0 + && timestampsPart[5] == 0 + && timestampsPart[6] == 0) return null; + timestampsPart[1] = 1; + timestampsPart[2] = 1; + } + + // fix non leading tray for nanoseconds + if (nanoLen >= 0) { + for (int begin = 0; begin < 6 - nanoLen; begin++) { + timestampsPart[6] = timestampsPart[6] * 10; + } + timestampsPart[6] = timestampsPart[6] * 1000; + } + return timestampsPart; + } + + public String className() { + return LocalDateTime.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) + && type.isAssignableFrom(LocalDateTime.class); + } + + public boolean canEncode(Object value) { + return value instanceof LocalDateTime; + } + + @Override + public LocalDateTime decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + int[] parts; + switch (column.getType()) { + case STRING: + case VARCHAR: + case VARSTRING: + String val = buf.readString(length); + try { + parts = parseTimestamp(val); + if (parts == null) return null; + return LocalDateTime.of(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5]) + .plusNanos(parts[6]); + } catch (DateTimeException dte) { + throw new SQLDataException( + String.format( + "value '%s' (%s) cannot be decoded as LocalDateTime", val, column.getType())); + } + + case DATE: + parts = LocalDateCodec.parseDate(buf, length); + if (parts == null) return null; + return LocalDateTime.of(parts[0], parts[1], parts[2], 0, 0, 0); + + case DATETIME: + case TIMESTAMP: + parts = parseTimestamp(buf.readAscii(length)); + if (parts == null) return null; + return LocalDateTime.of(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5]) + .plusNanos(parts[6]); + + case TIME: + parts = LocalTimeCodec.parseTime(buf, length, column); + return LocalDateTime.of(1970, 1, 1, parts[1] % 24, parts[2], parts[3]).plusNanos(parts[4]); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as LocalDateTime", column.getType())); + } + } + + @Override + public LocalDateTime decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + int year = 1970; + int month = 1; + long dayOfMonth = 1; + int hour = 0; + int minutes = 0; + int seconds = 0; + long microseconds = 0; + + switch (column.getType()) { + case TIME: + // specific case for TIME, to handle value not in 00:00:00-23:59:59 + buf.skip(5); // skip negative and days + hour = buf.readByte(); + minutes = buf.readByte(); + seconds = buf.readByte(); + if (length > 8) { + microseconds = buf.readUnsignedInt(); + } + break; + + case STRING: + case VARCHAR: + case VARSTRING: + String val = buf.readString(length); + try { + int[] parts = parseTimestamp(val); + if (parts == null) return null; + return LocalDateTime.of(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5]) + .plusNanos(parts[6]); + } catch (DateTimeException dte) { + throw new SQLDataException( + String.format( + "value '%s' (%s) cannot be decoded as LocalDateTime", val, column.getType())); + } + + case DATE: + case TIMESTAMP: + case DATETIME: + year = buf.readUnsignedShort(); + month = buf.readByte(); + dayOfMonth = buf.readByte(); + + if (length > 4) { + hour = buf.readByte(); + minutes = buf.readByte(); + seconds = buf.readByte(); + + if (length > 7) { + microseconds = buf.readUnsignedInt(); + } + } + break; + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as LocalDateTime", column.getType())); + } + + return LocalDateTime.of(year, month, (int) dayOfMonth, hour, minutes, seconds) + .plusNanos(microseconds * 1000); + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, LocalDateTime val, Calendar cal, Long maxLen) + throws IOException { + encoder.writeByte('\''); + encoder.writeAscii( + val.format(val.getNano() != 0 ? TIMESTAMP_FORMAT : TIMESTAMP_FORMAT_NO_FRACTIONAL)); + encoder.writeByte('\''); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, LocalDateTime value, Calendar cal) + throws IOException { + + int nano = value.getNano(); + if (nano > 0) { + encoder.writeByte((byte) 11); + encoder.writeShort((short) value.get(ChronoField.YEAR)); + encoder.writeByte(value.get(ChronoField.MONTH_OF_YEAR)); + encoder.writeByte(value.get(ChronoField.DAY_OF_MONTH)); + encoder.writeByte(value.get(ChronoField.HOUR_OF_DAY)); + encoder.writeByte(value.get(ChronoField.MINUTE_OF_HOUR)); + encoder.writeByte(value.get(ChronoField.SECOND_OF_MINUTE)); + encoder.writeInt(nano / 1000); + } else { + encoder.writeByte((byte) 7); + encoder.writeShort((short) value.get(ChronoField.YEAR)); + encoder.writeByte(value.get(ChronoField.MONTH_OF_YEAR)); + encoder.writeByte(value.get(ChronoField.DAY_OF_MONTH)); + encoder.writeByte(value.get(ChronoField.HOUR_OF_DAY)); + encoder.writeByte(value.get(ChronoField.MINUTE_OF_HOUR)); + encoder.writeByte(value.get(ChronoField.SECOND_OF_MINUTE)); + } + } + + public DataType getBinaryEncodeType() { + return DataType.DATETIME; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/LocalTimeCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/LocalTimeCodec.java new file mode 100644 index 000000000..fd6b67f01 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/LocalTimeCodec.java @@ -0,0 +1,286 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.sql.SQLDataException; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class LocalTimeCodec implements Codec { + + public static final LocalTimeCodec INSTANCE = new LocalTimeCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.TIME, + DataType.DATETIME, + DataType.TIMESTAMP, + DataType.VARSTRING, + DataType.VARCHAR, + DataType.STRING); + + public static int[] parseTime(ReadableByteBuf buf, int length, ColumnDefinitionPacket column) + throws SQLDataException { + int initialPos = buf.pos(); + int[] parts = new int[5]; + parts[0] = 1; + int idx = 1; + int partLength = 0; + byte b; + int i = 0; + if (length > 0 && buf.getByte() == '-') { + buf.skip(); + i++; + parts[0] = -1; + } + + for (; i < length; i++) { + b = buf.readByte(); + if (b == ':' || b == '.') { + idx++; + partLength = 0; + continue; + } + if (b < '0' || b > '9') { + buf.pos(initialPos); + String val = buf.readString(length); + throw new SQLDataException( + String.format("%s value '%s' cannot be decoded as Time", column.getType(), val)); + } + partLength++; + parts[idx] = parts[idx] * 10 + (b - '0'); + } + + if (idx < 2) { + buf.pos(initialPos); + String val = buf.readString(length); + throw new SQLDataException( + String.format("%s value '%s' cannot be decoded as Time", column.getType(), val)); + } + + // set nano real value + if (idx == 4) { + for (i = 0; i < 9 - partLength; i++) { + parts[4] = parts[4] * 10; + } + } + return parts; + } + + public String className() { + return LocalTime.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(LocalTime.class); + } + + public boolean canEncode(Object value) { + return value instanceof LocalTime; + } + + @Override + public LocalTime decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + int[] parts; + switch (column.getType()) { + case TIMESTAMP: + case DATETIME: + parts = LocalDateTimeCodec.parseTimestamp(buf.readString(length)); + if (parts == null) return null; + return LocalTime.of(parts[3], parts[4], parts[5], parts[6]); + + case TIME: + parts = parseTime(buf, length, column); + parts[1] = parts[1] % 24; + if (parts[0] == -1) { + // negative + long seconds = (24 * 60 * 60 - (parts[1] * 3600 + parts[2] * 60 + parts[3])); + return LocalTime.ofNanoOfDay(seconds * 1_000_000_000 - parts[4]); + } + return LocalTime.of(parts[1] % 24, parts[2], parts[3], parts[4]); + + case VARSTRING: + case VARCHAR: + case STRING: + String val = buf.readString(length); + try { + if (val.contains(" ")) { + return LocalDateTime.parse( + val, + LocalDateTimeCodec.MARIADB_LOCAL_DATE_TIME.withZone( + cal.getTimeZone().toZoneId())) + .toLocalTime(); + } else { + return LocalTime.parse(val); + } + } catch (DateTimeParseException e) { + throw new SQLDataException( + String.format( + "value '%s' (%s) cannot be decoded as LocalTime", val, column.getType())); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as LocalTime", column.getType())); + } + } + + @Override + public LocalTime decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + int hour = 0; + int minutes = 0; + int seconds = 0; + long microseconds = 0; + switch (column.getType()) { + case TIMESTAMP: + case DATETIME: + buf.skip(4); // skip year, month and day + if (length > 4) { + hour = buf.readByte(); + minutes = buf.readByte(); + seconds = buf.readByte(); + + if (length > 7) { + microseconds = buf.readInt(); + } + } + return LocalTime.of(hour, minutes, seconds).plusNanos(microseconds * 1000); + + case TIME: + boolean negate = buf.readByte() == 1; + if (length > 4) { + buf.skip(4); // skip days + if (length > 7) { + hour = buf.readByte(); + minutes = buf.readByte(); + seconds = buf.readByte(); + if (length > 8) { + microseconds = buf.readInt(); + } + } + } + if (negate) { + // negative + long nanos = (24 * 60 * 60 - (hour * 3600 + minutes * 60 + seconds)); + return LocalTime.ofNanoOfDay(nanos * 1_000_000_000 - microseconds * 1000); + } + return LocalTime.of(hour % 24, minutes, seconds, (int) microseconds * 1000); + + case VARSTRING: + case VARCHAR: + case STRING: + String val = buf.readString(length); + try { + if (val.contains(" ")) { + return LocalDateTime.parse( + val, + LocalDateTimeCodec.MARIADB_LOCAL_DATE_TIME.withZone( + cal.getTimeZone().toZoneId())) + .toLocalTime(); + } else { + return LocalTime.parse(val); + } + } catch (DateTimeParseException e) { + throw new SQLDataException( + String.format( + "value '%s' (%s) cannot be decoded as LocalTime", val, column.getType())); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as LocalTime", column.getType())); + } + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, LocalTime val, Calendar cal, Long maxLen) + throws IOException { + StringBuilder dateString = new StringBuilder(15); + dateString + .append(val.getHour() < 10 ? "0" : "") + .append(val.getHour()) + .append(val.getMinute() < 10 ? ":0" : ":") + .append(val.getMinute()) + .append(val.getSecond() < 10 ? ":0" : ":") + .append(val.getSecond()); + + int microseconds = val.getNano() / 1000; + if (microseconds > 0) { + dateString.append("."); + if (microseconds % 1000 == 0) { + dateString.append(Integer.toString(microseconds / 1000 + 1000).substring(1)); + } else { + dateString.append(Integer.toString(microseconds + 1000000).substring(1)); + } + } + + encoder.writeByte('\''); + encoder.writeAscii(dateString.toString()); + encoder.writeByte('\''); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, LocalTime value, Calendar cal) + throws IOException { + int nano = value.getNano(); + if (nano > 0) { + encoder.writeByte((byte) 12); + encoder.writeByte((byte) 0); + encoder.writeInt(0); + encoder.writeByte((byte) value.get(ChronoField.HOUR_OF_DAY)); + encoder.writeByte((byte) value.get(ChronoField.MINUTE_OF_HOUR)); + encoder.writeByte((byte) value.get(ChronoField.SECOND_OF_MINUTE)); + encoder.writeInt(nano / 1000); + } else { + encoder.writeByte((byte) 8); + encoder.writeByte((byte) 0); + encoder.writeInt(0); + encoder.writeByte((byte) value.get(ChronoField.HOUR_OF_DAY)); + encoder.writeByte((byte) value.get(ChronoField.MINUTE_OF_HOUR)); + encoder.writeByte((byte) value.get(ChronoField.SECOND_OF_MINUTE)); + } + } + + public DataType getBinaryEncodeType() { + return DataType.TIME; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/LongCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/LongCodec.java new file mode 100644 index 000000000..8e9124f5f --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/LongCodec.java @@ -0,0 +1,257 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.sql.SQLDataException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class LongCodec implements Codec { + + public static final LongCodec INSTANCE = new LongCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.FLOAT, + DataType.DOUBLE, + DataType.OLDDECIMAL, + DataType.VARCHAR, + DataType.DECIMAL, + DataType.ENUM, + DataType.VARSTRING, + DataType.STRING, + DataType.TINYINT, + DataType.SMALLINT, + DataType.MEDIUMINT, + DataType.INTEGER, + DataType.BIGINT, + DataType.BIT, + DataType.YEAR); + + public static long parseNotEmpty(ReadableByteBuf buf, int length) { + + boolean negate = false; + int idx = 1; + long result = buf.readByte() - 48; + + if (result == -3) { // minus sign + negate = true; + idx = 2; + result = buf.readByte() - 48; + } + + while (idx++ < length) { + result = result * 10 + buf.readByte() - 48; + } + + if (negate) result = -1 * result; + return result; + } + + public String className() { + return Long.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) + && ((type.isPrimitive() && type == Integer.TYPE) || type.isAssignableFrom(Long.class)); + } + + public boolean canEncode(Object value) { + return value instanceof Long; + } + + @Override + public Long decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + long result; + switch (column.getType()) { + case TINYINT: + case SMALLINT: + case MEDIUMINT: + case INTEGER: + case YEAR: + return parseNotEmpty(buf, length); + + case BIGINT: + if (column.isSigned()) { + return parseNotEmpty(buf, length); + } else { + BigInteger val = new BigInteger(buf.readAscii(length)); + try { + return val.longValueExact(); + } catch (ArithmeticException ae) { + throw new SQLDataException( + String.format("value '%s' cannot be decoded as Long", val.toString())); + } + } + + case BIT: + result = 0; + for (int i = 0; i < Math.min(length, 8); i++) { + byte b = buf.readByte(); + result = (result << 8) + (b & 0xff); + } + if (length > 8) { + buf.skip(length - 8); + } + return result; + + case DOUBLE: + case FLOAT: + case DECIMAL: + String str2 = buf.readAscii(length); + try { + return new BigDecimal(str2).setScale(0, RoundingMode.DOWN).longValue(); + } catch (NumberFormatException nfe) { + throw new SQLDataException(String.format("value '%s' cannot be decoded as Long", str2)); + } + + case VARCHAR: + case VARSTRING: + case STRING: + String str = buf.readString(length); + try { + return new BigInteger(str).longValueExact(); + } catch (NumberFormatException nfe) { + throw new SQLDataException(String.format("value '%s' cannot be decoded as Long", str)); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Long", column.getType())); + } + } + + @Override + public Long decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + + switch (column.getType()) { + case BIT: + long result = 0; + for (int i = 0; i < Math.min(length, 8); i++) { + byte b = buf.readByte(); + result = (result << 8) + (b & 0xff); + } + if (length > 8) { + buf.skip(length - 8); + } + return result; + + case TINYINT: + if (!column.isSigned()) { + return (long) buf.readUnsignedByte(); + } + return (long) buf.readByte(); + + case YEAR: + case SMALLINT: + if (!column.isSigned()) { + return (long) buf.readUnsignedShort(); + } + return (long) buf.readShort(); + + case MEDIUMINT: + long l = column.isSigned() ? buf.readMedium() : buf.readUnsignedMedium(); + buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges ! + return l; + + case INTEGER: + if (!column.isSigned()) { + return buf.readUnsignedInt(); + } + return (long) buf.readInt(); + + case BIGINT: + if (column.isSigned()) { + return buf.readLong(); + } else { + // need BIG ENDIAN, so reverse order + byte[] bb = new byte[8]; + for (int i = 7; i >= 0; i--) { + bb[i] = buf.readByte(); + } + BigInteger val = new BigInteger(1, bb); + try { + return val.longValueExact(); + } catch (ArithmeticException ae) { + throw new SQLDataException( + String.format("value '%s' cannot be decoded as Long", val.toString())); + } + } + + case FLOAT: + return (long) buf.readFloat(); + + case DOUBLE: + return (long) buf.readDouble(); + + case VARSTRING: + case VARCHAR: + case STRING: + case OLDDECIMAL: + case DECIMAL: + String str = buf.readString(length); + try { + return new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValueExact(); + } catch (NumberFormatException nfe) { + throw new SQLDataException(String.format("value '%s' cannot be decoded as Long", str)); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Long", column.getType())); + } + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, Long value, Calendar cal, Long maxLen) + throws IOException { + encoder.writeAscii(String.valueOf(value)); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, Long value, Calendar cal) + throws IOException { + encoder.writeLong(value); + } + + public DataType getBinaryEncodeType() { + return DataType.BIGINT; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/ReaderCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/ReaderCodec.java new file mode 100644 index 000000000..f071664d8 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/ReaderCodec.java @@ -0,0 +1,198 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.sql.SQLDataException; +import java.sql.SQLException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.util.constants.ServerStatus; + +public class ReaderCodec implements Codec { + + public static final ReaderCodec INSTANCE = new ReaderCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of(DataType.STRING, DataType.VARCHAR, DataType.VARSTRING); + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(Reader.class); + } + + public String className() { + return Reader.class.getName(); + } + + @Override + public Reader decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case STRING: + case VARCHAR: + case VARSTRING: + return new StringReader(buf.readString(length)); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Reader", column.getType())); + } + } + + @Override + public Reader decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case STRING: + case VARCHAR: + case VARSTRING: + return new StringReader(buf.readString(length)); + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Reader", column.getType())); + } + } + + public boolean canEncode(Object value) { + return value instanceof Reader; + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, Reader reader, Calendar cal, Long maxLen) + throws IOException, SQLException { + encoder.writeByte('\''); + char[] buf = new char[4096]; + int len; + if (maxLen == null) { + while ((len = reader.read(buf)) >= 0) { + byte[] data = new String(buf, 0, len).getBytes(StandardCharsets.UTF_8); + encoder.writeBytesEscaped( + data, + data.length, + (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0); + } + } else { + while ((len = reader.read(buf)) >= 0) { + byte[] data = + new String(buf, 0, Math.min(len, maxLen.intValue())).getBytes(StandardCharsets.UTF_8); + maxLen -= len; + encoder.writeBytesEscaped( + data, + data.length, + (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0); + } + } + encoder.writeByte('\''); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, Reader reader, Calendar cal) + throws IOException, SQLException { + // prefer use of encodeLongData, because length is unknown + byte[] clobBytes = new byte[4096]; + int pos = 0; + char[] buf = new char[4096]; + + int len; + while ((len = reader.read(buf)) > 0) { + byte[] data = new String(buf, 0, len).getBytes(StandardCharsets.UTF_8); + if (clobBytes.length - (pos + 1) < data.length) { + byte[] newBlobBytes = new byte[clobBytes.length + 65536]; + System.arraycopy(clobBytes, 0, newBlobBytes, 0, clobBytes.length); + pos = clobBytes.length; + clobBytes = newBlobBytes; + } + System.arraycopy(data, 0, clobBytes, pos, data.length); + pos += len; + } + encoder.writeLength(pos); + encoder.writeBytes(clobBytes, 0, pos); + } + + @Override + public void encodeLongData(PacketWriter encoder, Context context, Reader reader, Long length) + throws IOException { + char[] buf = new char[4096]; + int len; + if (length == null) { + while ((len = reader.read(buf)) >= 0) { + byte[] data = new String(buf, 0, len).getBytes(StandardCharsets.UTF_8); + encoder.writeBytes(data, 0, data.length); + } + } else { + long maxLen = length; + while ((len = reader.read(buf)) >= 0 && maxLen > 0) { + byte[] data = + new String(buf, 0, Math.min(len, (int) maxLen)).getBytes(StandardCharsets.UTF_8); + maxLen -= len; + encoder.writeBytes(data, 0, data.length); + } + } + } + + @Override + public byte[] encodeLongDataReturning( + PacketWriter encoder, Context context, Reader reader, Long length) + throws IOException, SQLException { + ByteArrayOutputStream bb = new ByteArrayOutputStream(); + char[] buf = new char[4096]; + int len; + if (length == null) { + while ((len = reader.read(buf)) >= 0) { + byte[] data = new String(buf, 0, len).getBytes(StandardCharsets.UTF_8); + bb.write(data, 0, data.length); + } + } else { + long maxLen = length; + while ((len = reader.read(buf)) >= 0 && maxLen > 0) { + byte[] data = + new String(buf, 0, Math.min(len, (int) maxLen)).getBytes(StandardCharsets.UTF_8); + maxLen -= len; + bb.write(data, 0, data.length); + } + } + byte[] val = bb.toByteArray(); + encoder.writeBytes(val); + return val; + } + + public DataType getBinaryEncodeType() { + return DataType.VARSTRING; + } + + public boolean canEncodeLongData() { + return true; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/ShortCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/ShortCodec.java new file mode 100644 index 000000000..1c5b797b9 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/ShortCodec.java @@ -0,0 +1,220 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.SQLDataException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class ShortCodec implements Codec { + + public static final ShortCodec INSTANCE = new ShortCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.FLOAT, + DataType.DOUBLE, + DataType.OLDDECIMAL, + DataType.VARCHAR, + DataType.DECIMAL, + DataType.ENUM, + DataType.VARSTRING, + DataType.STRING, + DataType.TINYINT, + DataType.SMALLINT, + DataType.MEDIUMINT, + DataType.INTEGER, + DataType.BIGINT, + DataType.BIT, + DataType.YEAR); + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) + && ((type.isPrimitive() && type == Short.TYPE) || type.isAssignableFrom(Short.class)); + } + + public boolean canEncode(Object value) { + return value instanceof Short; + } + + public String className() { + return Short.class.getName(); + } + + @Override + public Short decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + long result; + switch (column.getType()) { + case TINYINT: + case SMALLINT: + case MEDIUMINT: + case INTEGER: + case BIGINT: + case YEAR: + result = LongCodec.parseNotEmpty(buf, length); + break; + + case BIT: + result = 0; + for (int i = 0; i < Math.min(length, 8); i++) { + byte b = buf.readByte(); + result = (result << 8) + (b & 0xff); + } + if (length > 8) { + buf.skip(length - 8); + } + break; + + case FLOAT: + case DOUBLE: + case OLDDECIMAL: + case VARCHAR: + case DECIMAL: + case ENUM: + case VARSTRING: + case STRING: + String str = buf.readString(length); + try { + result = new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValue(); + break; + } catch (NumberFormatException nfe) { + throw new SQLDataException(String.format("value '%s' cannot be decoded as Short", str)); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Short", column.getType())); + } + + if ((short) result != result || (result < 0 && !column.isSigned())) { + throw new SQLDataException("Short overflow"); + } + + return (short) result; + } + + @Override + public Short decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + long result; + switch (column.getType()) { + case TINYINT: + result = column.isSigned() ? buf.readByte() : buf.readUnsignedByte(); + break; + + case YEAR: + case SMALLINT: + result = column.isSigned() ? buf.readShort() : buf.readUnsignedShort(); + break; + + case MEDIUMINT: + result = column.isSigned() ? buf.readMedium() : buf.readUnsignedMedium(); + buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges ! + break; + + case INTEGER: + result = column.isSigned() ? buf.readInt() : buf.readUnsignedInt(); + break; + + case BIGINT: + result = buf.readLong(); + if (result < 0 & !column.isSigned()) { + throw new SQLDataException("int overflow"); + } + break; + + case BIT: + result = 0; + for (int i = 0; i < Math.min(length, 8); i++) { + byte b = buf.readByte(); + result = (result << 8) + (b & 0xff); + } + if (length > 8) { + buf.skip(length - 8); + } + break; + + case FLOAT: + result = (long) buf.readFloat(); + break; + + case DOUBLE: + result = (long) buf.readDouble(); + break; + + case OLDDECIMAL: + case VARCHAR: + case DECIMAL: + case ENUM: + case VARSTRING: + case STRING: + String str = buf.readString(length); + try { + result = new BigDecimal(str).setScale(0, RoundingMode.DOWN).longValue(); + break; + } catch (NumberFormatException nfe) { + throw new SQLDataException(String.format("value '%s' cannot be decoded as Short", str)); + } + + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Short", column.getType())); + } + + if ((short) result != result || (result < 0 && !column.isSigned())) { + throw new SQLDataException("Short overflow"); + } + + return (short) result; + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, Short value, Calendar cal, Long maxLen) + throws IOException { + encoder.writeAscii(String.valueOf(value)); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, Short value, Calendar cal) + throws IOException { + encoder.writeShort(value); + } + + public DataType getBinaryEncodeType() { + return DataType.SMALLINT; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/StreamCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/StreamCodec.java new file mode 100644 index 000000000..aff4eeaeb --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/StreamCodec.java @@ -0,0 +1,203 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.*; +import java.sql.SQLDataException; +import java.sql.SQLException; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.util.constants.ServerStatus; + +public class StreamCodec implements Codec { + + public static final StreamCodec INSTANCE = new StreamCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.BLOB, + DataType.TINYBLOB, + DataType.MEDIUMBLOB, + DataType.LONGBLOB, + DataType.VARCHAR, + DataType.VARSTRING, + DataType.STRING); + + public String className() { + return InputStream.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(InputStream.class); + } + + @Override + public InputStream decodeText( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case STRING: + case VARCHAR: + case VARSTRING: + case BLOB: + case TINYBLOB: + case MEDIUMBLOB: + case LONGBLOB: + ByteArrayInputStream is = new ByteArrayInputStream(buf.buf(), buf.pos(), length); + buf.skip(length); + return is; + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Stream", column.getType())); + } + } + + @Override + public InputStream decodeBinary( + ReadableByteBuf buf, int length, ColumnDefinitionPacket column, Calendar cal) + throws SQLDataException { + switch (column.getType()) { + case STRING: + case VARCHAR: + case VARSTRING: + case BLOB: + case TINYBLOB: + case MEDIUMBLOB: + case LONGBLOB: + ByteArrayInputStream is = new ByteArrayInputStream(buf.buf(), buf.pos(), length); + buf.skip(length); + return is; + default: + buf.skip(length); + throw new SQLDataException( + String.format("Data type %s cannot be decoded as Stream", column.getType())); + } + } + + public boolean canEncode(Object value) { + return value instanceof InputStream; + } + + @Override + public void encodeText( + PacketWriter encoder, Context context, InputStream value, Calendar cal, Long maxLen) + throws IOException { + encoder.writeBytes(ByteArrayCodec.BINARY_PREFIX); + byte[] array = new byte[4096]; + int len; + + if (maxLen == null) { + while ((len = value.read(array)) > 0) { + encoder.writeBytesEscaped( + array, len, (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0); + } + } else { + while ((len = value.read(array)) > 0 && maxLen > 0) { + encoder.writeBytesEscaped( + array, + Math.min(len, maxLen.intValue()), + (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0); + maxLen -= len; + } + } + encoder.writeByte('\''); + } + + @Override + public void encodeBinary(PacketWriter encoder, Context context, InputStream value, Calendar cal) + throws IOException { + // length is not known + byte[] blobBytes = new byte[4096]; + int pos = 0; + byte[] array = new byte[4096]; + + int len; + while ((len = value.read(array)) > 0) { + if (blobBytes.length - (pos + 1) < len) { + byte[] newBlobBytes = new byte[blobBytes.length + 65536]; + System.arraycopy(blobBytes, 0, newBlobBytes, 0, blobBytes.length); + pos = blobBytes.length; + blobBytes = newBlobBytes; + } + System.arraycopy(array, 0, blobBytes, pos, len); + pos += len; + } + encoder.writeLength(pos); + encoder.writeBytes(blobBytes, 0, pos); + } + + @Override + public void encodeLongData(PacketWriter encoder, Context context, InputStream value, Long length) + throws IOException { + byte[] array = new byte[4096]; + int len; + if (length == null) { + while ((len = value.read(array)) > 0) { + encoder.writeBytes(array, 0, len); + } + } else { + long maxLen = length; + while ((len = value.read(array)) > 0 && maxLen > 0) { + encoder.writeBytes(array, 0, Math.min(len, (int) maxLen)); + maxLen -= len; + } + } + } + + @Override + public byte[] encodeLongDataReturning( + PacketWriter encoder, Context context, InputStream value, Long length) + throws IOException, SQLException { + ByteArrayOutputStream bb = new ByteArrayOutputStream(); + byte[] array = new byte[4096]; + int len; + if (length == null) { + while ((len = value.read(array)) > 0) { + bb.write(array, 0, len); + } + } else { + long maxLen = length; + while ((len = value.read(array)) > 0 && maxLen > 0) { + bb.write(array, 0, Math.min(len, (int) maxLen)); + maxLen -= len; + } + } + byte[] val = bb.toByteArray(); + encoder.writeBytes(val); + return val; + } + + public DataType getBinaryEncodeType() { + return DataType.BLOB; + } + + public boolean canEncodeLongData() { + return true; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/StringCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/StringCodec.java new file mode 100644 index 000000000..51f52ca11 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/StringCodec.java @@ -0,0 +1,291 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; +import org.mariadb.jdbc.util.constants.ServerStatus; + +public class StringCodec implements Codec { + + public static final StringCodec INSTANCE = new StringCodec(); + + private static final EnumSet COMPATIBLE_TYPES = + EnumSet.of( + DataType.BIT, + DataType.OLDDECIMAL, + DataType.TINYINT, + DataType.SMALLINT, + DataType.INTEGER, + DataType.FLOAT, + DataType.DOUBLE, + DataType.TIMESTAMP, + DataType.BIGINT, + DataType.MEDIUMINT, + DataType.DATE, + DataType.TIME, + DataType.DATETIME, + DataType.YEAR, + DataType.NEWDATE, + DataType.JSON, + DataType.DECIMAL, + DataType.ENUM, + DataType.SET, + DataType.VARCHAR, + DataType.VARSTRING, + DataType.STRING); + + public static String zeroFillingIfNeeded(String value, ColumnDefinitionPacket col) { + if (col.isZeroFill()) { + StringBuilder zeroAppendStr = new StringBuilder(); + long zeroToAdd = col.getDisplaySize() - value.length(); + while (zeroToAdd-- > 0) { + zeroAppendStr.append("0"); + } + return zeroAppendStr.append(value).toString(); + } + return value; + } + + public String className() { + return String.class.getName(); + } + + public boolean canDecode(ColumnDefinitionPacket column, Class type) { + return COMPATIBLE_TYPES.contains(column.getType()) && type.isAssignableFrom(String.class); + } + + public boolean canEncode(Object value) { + return value instanceof String; + } + + public String decodeText( + final ReadableByteBuf buf, + final int length, + final ColumnDefinitionPacket column, + final Calendar cal) { + if (column.getType() == DataType.BIT) { + byte[] bytes = new byte[length]; + buf.readBytes(bytes); + StringBuilder sb = new StringBuilder(bytes.length * Byte.SIZE + 3); + sb.append("b'"); + boolean firstByteNonZero = false; + for (int i = 0; i < Byte.SIZE * bytes.length; i++) { + boolean b = (bytes[i / Byte.SIZE] & 1 << (Byte.SIZE - 1 - (i % Byte.SIZE))) > 0; + if (b) { + sb.append('1'); + firstByteNonZero = true; + } else if (firstByteNonZero) { + sb.append('0'); + } + } + sb.append("'"); + return sb.toString(); + } + return buf.readString(length); + } + + public String decodeBinary( + final ReadableByteBuf buf, + final int length, + final ColumnDefinitionPacket column, + final Calendar cal) { + switch (column.getType()) { + case BIT: + byte[] bytes = new byte[length]; + buf.readBytes(bytes); + StringBuilder sb = new StringBuilder(bytes.length * Byte.SIZE + 3); + sb.append("b'"); + boolean firstByteNonZero = false; + for (int i = 0; i < Byte.SIZE * bytes.length; i++) { + boolean b = (bytes[i / Byte.SIZE] & 1 << (Byte.SIZE - 1 - (i % Byte.SIZE))) > 0; + if (b) { + sb.append('1'); + firstByteNonZero = true; + } else if (firstByteNonZero) { + sb.append('0'); + } + } + sb.append("'"); + return sb.toString(); + + case TINYINT: + if (!column.isSigned()) { + return String.valueOf(buf.readUnsignedByte()); + } + return String.valueOf(buf.readByte()); + + case YEAR: + String s = String.valueOf(buf.readUnsignedShort()); + while (s.length() < column.getLength()) s = "0" + s; + return s; + + case SMALLINT: + if (!column.isSigned()) { + return String.valueOf(buf.readUnsignedShort()); + } + return String.valueOf(buf.readShort()); + + case MEDIUMINT: + String mediumStr = + String.valueOf(column.isSigned() ? buf.readMedium() : buf.readUnsignedMedium()); + buf.skip(); // MEDIUMINT is encoded on 4 bytes in exchanges ! + return mediumStr; + + case INTEGER: + if (!column.isSigned()) { + return String.valueOf(buf.readUnsignedInt()); + } + return String.valueOf(buf.readInt()); + + case BIGINT: + BigInteger val; + if (column.isSigned()) { + val = BigInteger.valueOf(buf.readLong()); + } else { + // need BIG ENDIAN, so reverse order + byte[] bb = new byte[8]; + for (int ii = 7; ii >= 0; ii--) { + bb[ii] = buf.readByte(); + } + val = new BigInteger(1, bb); + } + + return new BigDecimal(String.valueOf(val)).setScale(column.getDecimals()).toPlainString(); + + case FLOAT: + return String.valueOf(buf.readFloat()); + + case DOUBLE: + return String.valueOf(buf.readDouble()); + + case TIME: + long tDays = 0; + int tHours = 0; + int tMinutes = 0; + int tSeconds = 0; + long tMicroseconds = 0; + boolean negate = false; + + if (length > 0) { + negate = buf.readByte() == 0x01; + if (length > 4) { + tDays = buf.readUnsignedInt(); + if (length > 7) { + tHours = buf.readByte(); + tMinutes = buf.readByte(); + tSeconds = buf.readByte(); + if (length > 8) { + tMicroseconds = buf.readInt(); + } + } + } + } + int totalHour = (int) (tDays * 24 + tHours); + String stTime = + (negate ? "-" : "") + + (totalHour < 10 ? "0" : "") + + totalHour + + ":" + + (tMinutes < 10 ? "0" : "") + + tMinutes + + ":" + + (tSeconds < 10 ? "0" : "") + + tSeconds; + if (column.getDecimals() == 0) return stTime; + String stMicro = String.valueOf(tMicroseconds); + while (stMicro.length() < column.getDecimals()) { + stMicro = "0" + stMicro; + } + return stTime + "." + stMicro; + + case DATE: + int dateYear = buf.readUnsignedShort(); + int dateMonth = buf.readByte(); + int dateDay = buf.readByte(); + if (length > 4) { + buf.skip(length - 4); + } + return LocalDate.of(dateYear, dateMonth, dateDay).toString(); + + case DATETIME: + case TIMESTAMP: + int year = buf.readUnsignedShort(); + int month = buf.readByte(); + int day = buf.readByte(); + int hour = 0; + int minutes = 0; + int seconds = 0; + long microseconds = 0; + + if (length > 4) { + hour = buf.readByte(); + minutes = buf.readByte(); + seconds = buf.readByte(); + + if (length > 7) { + microseconds = buf.readUnsignedInt(); + } + } + LocalDateTime dateTime = + LocalDateTime.of(year, month, day, hour, minutes, seconds) + .plusNanos(microseconds * 1000); + return dateTime.toLocalDate().toString() + ' ' + dateTime.toLocalTime().toString(); + + default: + return buf.readString(length); + } + } + + public void encodeText( + PacketWriter encoder, Context context, String value, Calendar cal, Long maxLen) + throws IOException { + encoder.writeByte('\''); + encoder.writeStringEscaped( + maxLen == null ? value : value.substring(0, maxLen.intValue()), + (context.getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) != 0); + encoder.writeByte('\''); + } + + public void encodeBinary(PacketWriter writer, Context context, String value, Calendar cal) + throws IOException { + byte[] b = value.getBytes(StandardCharsets.UTF_8); + writer.writeLength(b.length); + writer.writeBytes(b); + } + + public DataType getBinaryEncodeType() { + return DataType.VARSTRING; + } +} diff --git a/src/main/java/org/mariadb/jdbc/codec/list/TimeCodec.java b/src/main/java/org/mariadb/jdbc/codec/list/TimeCodec.java new file mode 100644 index 000000000..e7178c647 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/codec/list/TimeCodec.java @@ -0,0 +1,245 @@ +/* + * MariaDB Client for Java + * + * Copyright (c) 2012-2014 Monty Program Ab. + * Copyright (c) 2015-2020 MariaDB Corporation Ab. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with this library; if not, write to Monty Program Ab info@montyprogram.com. + * + */ + +package org.mariadb.jdbc.codec.list; + +import java.io.IOException; +import java.sql.SQLDataException; +import java.sql.Time; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.EnumSet; +import org.mariadb.jdbc.client.PacketWriter; +import org.mariadb.jdbc.client.ReadableByteBuf; +import org.mariadb.jdbc.client.context.Context; +import org.mariadb.jdbc.codec.Codec; +import org.mariadb.jdbc.codec.DataType; +import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; + +public class TimeCodec implements Codec

+ + + +